包裹 | 努格特 |
---|---|
流畅的Docker | |
微软测试 | |
XUnit测试 |
该库支持使用Fluent API进行docker
和docker-compose
交互。 Linux、Windows 和 Mac 均支持它。它还支持传统的docker-machine
交互。
Fluent API 使用示例
using (
var container =
new Builder ( ) . UseContainer ( )
. UseImage ( "kiasaki/alpine-postgres" )
. ExposePort ( 5432 )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. WaitForPort ( "5432/tcp" , 30000 /*30s*/ )
. Build ( )
. Start ( ) )
{
var config = container . GetConfiguration ( true ) ;
Assert . AreEqual ( ServiceRunningState . Running , config . State . ToServiceState ( ) ) ;
}
这会启动一个 postgres 并等待它准备好。要使用 compose,只需这样做:
注意:使用 AssumeComposeVersion(ComposeVersion.V2) 来使用 V2 行为,默认值仍然是 V1(今年晚些时候将更改为默认值 V2)
var file = Path . Combine ( Directory . GetCurrentDirectory ( ) ,
( TemplateString ) "Resources/ComposeTests/WordPress/docker-compose.yml" ) ;
// @formatter:off
using ( var svc = new Builder ( )
. UseContainer ( )
. UseCompose ( )
. FromFile ( file )
. RemoveOrphans ( )
. WaitForHttp ( "wordpress" , "http://localhost:8000/wp-admin/install.php" )
. Build ( ) . Start ( ) )
// @formatter:on
{
// We now have a running WordPress with a MySql database
var installPage = await "http://localhost:8000/wp-admin/install.php" . Wget ( ) ;
Assert . IsTrue ( installPage . IndexOf ( "https://wordpress.org/" , StringComparison . Ordinal ) != - 1 ) ;
Assert . AreEqual ( 1 , svc . Hosts . Count ) ; // The host used by compose
Assert . AreEqual ( 2 , svc . Containers . Count ) ; // We can access each individual container
Assert . AreEqual ( 2 , svc . Images . Count ) ; // And the images used.
}
:bulb Linux 用户注意事项: Docker 默认情况下需要sudo ,并且默认情况下库期望执行用户不需要执行sudo即可与 docker 守护进程对话。更多描述可以在与 Docker 守护进程对话一章中找到。
Fluent API构建一项或多项服务。每个服务可以是复合的或单一的。因此,可以启动多个基于docker-compose 的服务并将每个服务作为单个服务进行管理,或者挖掘并使用每个docker-compose服务上的所有底层服务。也可以直接使用服务,例如
var file = Path . Combine ( Directory . GetCurrentDirectory ( ) ,
( TemplateString ) "Resources/ComposeTests/WordPress/docker-compose.yml" ) ;
using ( var svc = new DockerComposeCompositeService ( DockerHost , new DockerComposeConfig
{
ComposeFilePath = new List < string > { file } , ForceRecreate = true , RemoveOrphans = true ,
StopOnDispose = true
} ) )
{
svc . Start ( ) ;
// We now have a running WordPress with a MySql database
var installPage = await $ "http://localhost:8000/wp-admin/install.php" . Wget ( ) ;
Assert . IsTrue ( installPage . IndexOf ( "https://wordpress.org/" , StringComparison . Ordinal ) != - 1 ) ;
}
上面的示例从单个撰写文件创建了docker-compose服务。当服务被处置时,所有底层服务都会自动停止。
该库受 .NET 完整 4.51 框架及更高版本、.NET 标准 1.6、2.0 支持。它分为三层,每层都是可访问的:
大多数服务方法都是扩展方法,没有硬连线到服务本身,这使得它们轻量级且可定制。由于一切都是可访问的,因此很容易为使用第 1 层命令提供功能的服务添加扩展方法。
我确实欢迎贡献,尽管目前还没有贡献指南,但请确保在执行 Pull 请求时遵守.editorconfig 。否则构建将会失败。今年迟早我会更新一个真正的指南。
所有命令都需要DockerUri
才能使用。它是本地或远程 docker 守护进程的 Uri。它可以是可发现的或硬编码的。可以通过以下方式发现本地DockerUri
var hosts = new Hosts ( ) . Discover ( ) ;
var _docker = hosts . FirstOrDefault ( x => x . IsNative ) ?? hosts . FirstOrDefault ( x => x . Name == "default" ) ;
截断的示例将检查本机或 docker beta“本机”主机,如果没有选择 docker-machine“默认”作为主机。如果您使用 docker-machine 并且没有机器存在或未启动,则可以通过例如"test-machine".Create(1024,20000000,1)
轻松创建/启动 docker-machine。这将创建一台名为“test-machine”的 docker 机器,具有 1GB RAM、20GB 磁盘并使用一个 CPU。
现在可以使用 Uri 使用命令进行通信。例如,获取客户端和服务器 docker 二进制文件的版本:
var result = _docker . Host . Version ( _docker . Certificates ) ;
Debug . WriteLine ( result . Data ) ; // Will Print the Client and Server Version and API Versions respectively.
所有命令都会返回 CommandResponse ,以便可以通过response.Success
检查成功因素。如果有任何与命令关联的数据,则会在response.Data
属性中返回。
然后,使用命令启动和停止包括删除容器就很简单了,如下所示。下面启动一个容器并对其进行 PS,然后将其删除。
var id = _docker . Host . Run ( "nginx:latest" , null , _docker . Certificates ) . Data ;
var ps = _docker . Host . Ps ( null , _docker . Certificates ) . Data ;
_docker . Host . RemoveContainer ( id , true , true , null , _docker . Certificates ) ;
在windows上运行时,可以选择运行linux或windows容器。使用LinuxDaemon
或WindowsDaemon
来控制与哪个守护进程通信。
_docker . LinuxDaemon ( ) ; // ensures that it will talk to linux daemon, if windows daemon it will switch
当需要使用连续流的事件或日志时,某些命令会返回数据流。 Streams 可用于后台任务并支持CancellationToken
。下面的示例尾部日志。
using ( var logs = _docker . Host . Logs ( id , _docker . Certificates ) )
{
while ( ! logs . IsFinished )
{
var line = logs . TryRead ( 5000 ) ; // Do a read with timeout
if ( null == line )
{
break ;
}
Debug . WriteLine ( line ) ;
}
}
命令存在实用方法。它们有不同的风格,例如网络等。例如,读完日志时:
using ( var logs = _docker . Host . Logs ( id , _docker . Certificates ) )
{
foreach ( var line in logs . ReadToEnd ( ) )
{
Debug . WriteLine ( line ) ;
}
}
该库的最高层是流畅的 API,您可以在其中定义和控制机器、图像和容器。例如,要设置一个负载均衡器,其中两个 Nodejs 服务器从 Redis 服务器读取数据,可能如下所示(如果在存储库中找不到,则节点映像是自定义构建的)。
var fullPath = ( TemplateString ) @"${TEMP}/fluentdockertest/${RND}" ;
var nginx = Path . Combine ( fullPath , "nginx.conf" ) ;
Directory . CreateDirectory ( fullPath ) ;
typeof ( NsResolver ) . ResourceExtract ( fullPath , "index.js" ) ;
using ( var services = new Builder ( )
// Define custom node image to be used
. DefineImage ( "mariotoffia/nodetest" ) . ReuseIfAlreadyExists ( )
. From ( "ubuntu" )
. Maintainer ( "Mario Toffia <[email protected]>" )
. Run ( "apt-get update &&" ,
"apt-get -y install curl &&" ,
"curl -sL https://deb.nodesource.com/setup | sudo bash - &&" ,
"apt-get -y install python build-essential nodejs" )
. Run ( "npm install -g nodemon" )
. Add ( "emb:Ductus.FluentDockerTest/Ductus.FluentDockerTest.MultiContainerTestFiles/package.txt" ,
"/tmp/package.json" )
. Run ( "cd /tmp && npm install" )
. Run ( "mkdir -p /src && cp -a /tmp/node_modules /src/" )
. UseWorkDir ( "/src" )
. Add ( "index.js" , "/src" )
. ExposePorts ( 8080 )
. Command ( "nodemon" , "/src/index.js" ) . Builder ( )
// Redis Db Backend
. UseContainer ( ) . WithName ( "redis" ) . UseImage ( "redis" ) . Builder ( )
// Node server 1 & 2
. UseContainer ( ) . WithName ( "node1" ) . UseImage ( "mariotoffia/nodetest" ) . Link ( "redis" ) . Builder ( )
. UseContainer ( ) . WithName ( "node2" ) . UseImage ( "mariotoffia/nodetest" ) . Link ( "redis" ) . Builder ( )
// Nginx as load balancer
. UseContainer ( ) . WithName ( "nginx" ) . UseImage ( "nginx" ) . Link ( "node1" , "node2" )
. CopyOnStart ( nginx , "/etc/nginx/nginx.conf" )
. ExposePort ( 80 ) . Builder ( )
. Build ( ) . Start ( ) )
{
Assert . AreEqual ( 4 , services . Containers . Count ) ;
var ep = services . Containers . First ( x => x . Name == "nginx" ) . ToHostExposedEndpoint ( "80/tcp" ) ;
Assert . IsNotNull ( ep ) ;
var round1 = $ "http:// { ep . Address } : { ep . Port } " . Wget ( ) ;
Assert . AreEqual ( "This page has been viewed 1 times!" , round1 ) ;
var round2 = $ "http:// { ep . Address } : { ep . Port } " . Wget ( ) ;
Assert . AreEqual ( "This page has been viewed 2 times!" , round2 ) ;
}
上面的示例定义了一个Dockerfile ,并为节点映像构建它。然后它使用普通的 redis 和 nginx。如果您只想使用现有的Dockerfile,可以这样做。
using ( var services = new Builder ( )
. DefineImage ( "mariotoffia/nodetest" ) . ReuseIfAlreadyExists ( )
. FromFile ( "/tmp/Dockerfile" )
. Build ( ) . Start ( ) )
{
// Container either build to reused if found in registry and started here.
}
Fluent API 支持从定义一个 docker-machine 到一组 docker 实例。它具有内置支持,例如在Build()
完成之前等待容器内的特定端口或进程,因此可以在 using 语句中安全地使用。如果对等待超时等进行具体管理,您始终可以构建并启动容器并使用扩展方法来等待容器本身。
要创建容器,只需省略启动即可。例如:
using (
var container =
new Builder ( ) . UseContainer ( )
. UseImage ( "kiasaki/alpine-postgres" )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. Build ( ) )
{
Assert . AreEqual ( ServiceRunningState . Stopped , container . State ) ;
}
本示例创建一个带有 postgres 的容器,配置一个环境变量。在 using 语句中可以启动IContainerService
。因此,每个构建的容器都包装在IContainerService
中。还可以使用IHostService.GetContainers(...)
来获取已创建、正在运行和已退出的容器。从IHostService
还可以获取本地存储库中的所有图像以从中创建容器。
当您想运行单个容器时,请使用 Fluent 或容器服务启动方法。例如:
using (
var container =
new Builder ( ) . UseContainer ( )
. UseImage ( "kiasaki/alpine-postgres" )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. Build ( )
. Start ( ) )
{
var config = container . GetConfiguration ( ) ;
Assert . AreEqual ( ServiceRunningState . Running , container . State ) ;
Assert . IsTrue ( config . Config . Env . Any ( x => x == "POSTGRES_PASSWORD=mysecretpassword" ) ) ;
}
默认情况下,运行 Dispose 方法时容器会停止并删除,为了将容器保留在 archve 中,请使用 Fluent API 上的KeepContainer()
。当调用Dispose()
时,它将被停止但不会被删除。处置后也可以保持其运行。
可以显式或随机地公开端口。无论哪种方式,都可以解析 IP(如果是机器)和端口(如果是随机端口)以在代码中使用。例如:
using (
var container =
new Builder ( ) . UseContainer ( )
. UseImage ( "kiasaki/alpine-postgres" )
. ExposePort ( 40001 , 5432 )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. Build ( )
. Start ( ) )
{
var endpoint = container . ToHostExposedEndpoint ( "5432/tcp" ) ;
Assert . AreEqual ( 40001 , endpoint . Port ) ;
}
这里我们将容器端口 5432 显式映射到主机端口 40001。请注意container.ToHostExposedEndpoint(...)
的使用。这是为了始终解析到工作 IP 和端口以与 docker 容器进行通信。也可以映射随机端口,即让 Docker 选择一个可用端口。例如:
using (
var container =
new Builder ( ) . UseContainer ( )
. UseImage ( "kiasaki/alpine-postgres" )
. ExposePort ( 5432 )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. Build ( )
. Start ( ) )
{
var endpoint = container . ToHostExposedEndpoint ( "5432/tcp" ) ;
Assert . AreNotEqual ( 0 , endpoint . Port ) ;
}
这里唯一的区别是,当使用ExposePort(...)
配置容器时,仅使用一个参数。相同的用法适用于其他情况,因此对于代码来说是透明的。
为了在开始连接之前知道某个服务何时启动并运行。可以等待特定端口打开。例如:
using (
var container =
new Builder ( ) . UseContainer ( )
. UseImage ( "kiasaki/alpine-postgres" )
. ExposePort ( 5432 )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. WaitForPort ( "5432/tcp" , 30000 /*30s*/ )
. Build ( )
. Start ( ) )
{
var config = container . GetConfiguration ( true ) ;
Assert . AreEqual ( ServiceRunningState . Running , config . State . ToServiceState ( ) ) ;
}
在上面的示例中,我们等待容器端口 5432 在 30 秒内打开。如果失败,它将抛出异常,因此容器将被处置并删除(因为我们没有任何保留容器等配置)。
using (
var container =
new Builder ( ) . UseContainer ( )
. UseImage ( "kiasaki/alpine-postgres" )
. ExposePort ( 5432 )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. WaitForPort ( "5432/tcp" , 30000 /*30s*/ , "127.0.0.1" )
. Build ( )
. Start ( ) )
{
var config = container . GetConfiguration ( true ) ;
Assert . AreEqual ( ServiceRunningState . Running , config . State . ToServiceState ( ) ) ;
}
有时无法通过本地 IP 和端口直接访问容器,例如容器在环回接口 ( 127.0.0.1 ) 上有一个公开端口,这是从程序访问容器的唯一方法。上面的示例强制地址为127.0.0.1 ,但仍解析主机端口。默认情况下, FluentDocker使用容器上的网络检查来确定网络配置。
有时仅仅等待端口是不够的。有时,等待容器进程更为重要。因此,Fluent API 中存在等待处理方法以及容器对象上的扩展方法。例如:
using (
var container =
new Builder ( ) . UseContainer ( )
. UseImage ( "kiasaki/alpine-postgres" )
. ExposePort ( 5432 )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. WaitForProcess ( "postgres" , 30000 /*30s*/ )
. Build ( )
. Start ( ) )
{
var config = container . GetConfiguration ( true ) ;
Assert . AreEqual ( ServiceRunningState . Running , config . State . ToServiceState ( ) ) ;
}
在上面的示例中,当容器内启动进程“postgres”时, Build()
将返回控制权。
为了使用容器,有时需要将容器中的卷安装到主机上,或者只是从容器复制或复制到容器。根据您运行的是机器还是 Docker,本机卷映射具有必须可从虚拟机访问的限制。
正常的用例是让网络服务器在 Docker 容器上提供内容,并且用户在主机文件系统上编辑文件。在这种情况下,需要将 docker 容器卷安装到主机上。例如:
const string html = "<html><head>Hello World</head><body><h1>Hello world</h1></body></html>" ;
var hostPath = ( TemplateString ) @"${TEMP}/fluentdockertest/${RND}" ;
Directory . CreateDirectory ( hostPath ) ;
using (
var container =
new Builder ( ) . UseContainer ( )
. UseImage ( "nginx:latest" )
. ExposePort ( 80 )
. Mount ( hostPath , "/usr/share/nginx/html" , MountType . ReadOnly )
. Build ( )
. Start ( )
. WaitForPort ( "80/tcp" , 30000 /*30s*/ ) )
{
File . WriteAllText ( Path . Combine ( hostPath , "hello.html" ) , html ) ;
var response = $ "http:// { container . ToHostExposedEndpoint ( "80/tcp" ) } /hello.html" . Wget ( ) ;
Assert . AreEqual ( html , response ) ;
}
在上面的示例中,nginx 容器启动并将“/usr/share/nginx/html”安装到(随机,在临时目录中)主机路径。 HTML 文件被复制到主机路径中,当完成对 nginx docker 容器的 HTTP 获取时,将提供相同的文件。
有时需要将文件复制到容器中或从容器中复制文件。例如,复制配置文件,对其进行配置然后将其复制回来。更常见的场景是在容器启动之前将配置文件复制到容器中。多容器示例在启动之前复制 nginx 配置文件。因此,可以避免为这样一个简单的任务手动创建 Dockerfile 和镜像。相反,只需使用官方或自定义映像,复制配置并运行。
using ( new Builder ( ) . UseContainer ( )
. UseImage ( "kiasaki/alpine-postgres" )
. ExposePort ( 5432 )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. Build ( )
. Start ( )
. CopyFrom ( "/etc/conf.d" , fullPath ) )
{
var files = Directory . EnumerateFiles ( Path . Combine ( fullPath , "conf.d" ) ) . ToArray ( ) ;
Assert . IsTrue ( files . Any ( x => x . EndsWith ( "pg-restore" ) ) ) ;
Assert . IsTrue ( files . Any ( x => x . EndsWith ( "postgresql" ) ) ) ;
}
上面的示例将目录从正在运行的容器复制到主机路径 (fullPath)。注意这里使用了扩展方法,因此没有使用 Fluent API(因为 CopyFrom 在 Start() 之后)。如果您想在开始之前从容器复制文件,请改用 Fluent API。
using ( new Builder ( ) . UseContainer ( )
. UseImage ( "kiasaki/alpine-postgres" )
. ExposePort ( 5432 )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. CopyOnStart ( "/etc/conf.d" , fullPath )
. Build ( )
. Start ( ) )
{
}
下面的示例说明了一种更常见的场景,其中文件被复制到容器。此示例使用扩展方法而不是 Fluent API 版本。它在复制之前和复制之后拍摄 Diff 快照。后者中存在 hello.html。
using (
var container =
new Builder ( ) . UseContainer ( )
. UseImage ( "kiasaki/alpine-postgres" )
. ExposePort ( 5432 )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. Build ( )
. Start ( )
. WaitForProcess ( "postgres" , 30000 /*30s*/ )
. Diff ( out before )
. CopyTo ( "/bin" , fullPath ) )
{
var after = container . Diff ( ) ;
Assert . IsFalse ( before . Any ( x => x . Item == "/bin/hello.html" ) ) ;
Assert . IsTrue ( after . Any ( x => x . Item == "/bin/hello.html" ) ) ;
}
有时在IContainerService.Dispose()
中复制文件很有用(就在容器停止之前)。因此,存在一个流畅的 API 来确保它能够做到这一点。
using ( new Builder ( ) . UseContainer ( )
. UseImage ( "kiasaki/alpine-postgres" )
. ExposePort ( 5432 )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. CopyOnDispose ( "/etc/conf.d" , fullPath )
. Build ( )
. Start ( ) )
{
}
为了分析容器,存在导出扩展方法和流畅的 API 方法。最值得注意的是在释放IContainerService
时导出容器的可能性。
using ( new Builder ( ) . UseContainer ( )
. UseImage ( "kiasaki/alpine-postgres" )
. ExposePort ( 5432 )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. ExportOnDispose ( fullPath )
. Build ( )
. Start ( ) )
{
}
这将在主机(完整路径)上生成容器导出(tar 文件)。如果您希望将其分解(去皮),请改用ExportExplodedOnDispose
方法。当然,您可以随时使用容器上的扩展方法导出容器。
单元测试时的一个有用技巧是在单元测试由于某种原因失败时导出容器状态,因此存在一个 Fluent API,可以在满足特定 Lambda 条件时导出。例如:
var failure = false ;
using ( new Builder ( ) . UseContainer ( )
. UseImage ( "kiasaki/alpine-postgres" )
. ExposePort ( 5432 )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. ExportOnDispose ( fullPath , svc => failure )
. Build ( )
. Start ( ) )
{
failure = true ;
}
当 using 语句处置容器时,此代码片段将导出容器,因为 failure 变量设置为 true 并在ExportOnDispose
表达式中使用。
所有服务都可以通过钩子进行扩展。在ExportOnDispose(path, lambda)
中,当服务状态设置为在状态为Removing
时执行 lambda 时,会安装一个 Hook。可以即时安装和移除钩子。如果在同一个服务实例上注册了多个钩子,并且具有相同的ServiceRunningState
,它们将按照安装顺序执行。
如果您希望在服务上设置(或执行)状态时执行某些操作(例如Starting
),那么这些钩子特别有用。 Fluent API 在某些情况下会使用这些 API,例如复制文件、导出等。
FluentDocker 支持所有 docker 网络命令。它可以通过_docker.NetworkLs()
发现网络,它会发现NetworkRow
中定义的所有网络和一些简单参数。它还可以通过_docker.NetworkInspect(network:"networkId")
进行检查以获取有关网络的更深入信息(例如网络中有哪些容器以及 Ipam 配置)。
为了创建新网络,请使用_docker.NetworkCreate("name_of_network")
。还可以提供NetworkCreateParams
,其中所有内容都可以自定义,例如创建覆盖网络或更改 Ipam 配置。要删除网络,只需使用_docker.NetworkRm(network:"networkId")
。
请注意,如果网络附加有任何容器,则不会删除网络!
创建网络后,可以使用_docker.NetworkConnect("containerId","networkId")
将一个或多个容器放入其中。请注意,容器一次可能位于多个网络中,因此可以在隔离的网络之间代理请求。要断开容器与网络的连接,只需执行_docker.NetworkDisconnect("containerId","networkId")
即可。
以下示例运行一个容器,创建一个新网络,并将正在运行的容器连接到网络。然后它断开容器的连接,将其删除,并删除网络。
var cmd = _docker . Run ( "postgres:9.6-alpine" , new ContainerCreateParams
{
PortMappings = new [ ] { "40001:5432" } ,
Environment = new [ ] { "POSTGRES_PASSWORD=mysecretpassword" }
} , _certificates ) ;
var container = cmd . Data ;
var network = string . Empty ;
var created = _docker . NetworkCreate ( "test-network" ) ;
if ( created . Success )
network = created . Data [ 0 ] ;
_docker . NetworkConnect ( container , network ) ;
// Container is now running and has address in the newly created 'test-network'
_docker . NetworkDisconnect ( container , id , true /*force*/ ) ;
_docker . RemoveContainer ( container , true , true ) ;
// Now it is possible to delete the network since it has been disconnected from the network
_docker . NetworkRm ( network : network ) ;
还可以使用 Fluent 构建器构建新的或重用现有的 Docker 网络。然后可以在构建容器时引用这些内容。可以构建多个 docker 网络,并将一个容器一次连接到多个网络。
using ( var nw = new Builder ( ) . UseNetwork ( "test-network" ) )
{
using (
var container =
new DockerBuilder ( )
. WithImage ( "kiasaki/alpine-postgres" )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. ExposePorts ( "5432" )
. UseNetwork ( nw )
. WaitForPort ( "5432/tcp" , 30000 /*30s*/ )
. Build ( ) )
{
container . Create ( ) . Start ( ) ;
}
}
上面的代码片段创建了一个名为test-network 的新网络,然后创建一个附加到test-network 的容器。当在nw上调用Dispose()
时,它将删除网络。还可以通过UseIpV4
或UseIpV6
在网络内进行静态IP容器分配。例如:
using ( var nw = Fd . UseNetwork ( "unit-test-nw" )
. UseSubnet ( "10.18.0.0/16" ) . Build ( ) )
{
using (
var container =
Fd . UseContainer ( )
. UseImage ( "postgres:9.6-alpine" )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. ExposePort ( 5432 )
. UseNetwork ( nw )
. UseIpV4 ( "10.18.0.22" )
. WaitForPort ( "5432/tcp" , 30000 /*30s*/ )
. Build ( )
. Start ( ) )
{
var ip = container . GetConfiguration ( ) . NetworkSettings . Networks [ "unit-test-nw" ] . IPAddress ;
Assert . AreEqual ( "10.18.0.22" , ip ) ;
}
}
上面的示例创建了一个新的网络unit-test-nw ,其 ip-range 为10.18.0.0/16 。这是在新容器中使用的。容器的 IP 设置为10.18.0.22 ,并且由于UseIpV4
命令而处于静态。
FluentDocker 支持通过命令和 Fluent API 进行 docker 卷管理。因此,可以完全控制容器中使用的体积,例如是否应处置、重复使用、使用什么驱动程序等。
var volume = _docker . VolumeCreate ( "test-volume" , "local" , opts : {
{ "type" , "nfs" } ,
{ "o=addr" , "192.168.1.1,rw" } ,
{ "device" , ":/path/to/dir" }
} ) ;
var cfg = _docker . VolumeInspect ( _certificates , "test-volume" ) ;
_docker . VolumeRm ( force : true , id : "test-volume" ) ;
上面的代码片段创建了一个名为test-volume 的新卷,并且是NFS类型。然后它检查刚刚创建的卷,最后强制删除该卷。
还可以使用流畅的 API 来创建或使用卷。然后可以在构建容器时使用它们。当卷的创建很特殊或需要控制生命周期时,这尤其有用。
using ( var vol = new Builder ( ) . UseVolume ( "test-volume" ) . RemoveOnDispose ( ) . Build ( ) )
{
using (
var container =
new Builder ( ) . UseContainer ( )
. UseImage ( "postgres:9.6-alpine" )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. MountVolume ( vol , "/var/lib/postgresql/data" , MountType . ReadWrite )
. Build ( )
. Start ( ) )
{
var config = container . GetConfiguration ( ) ;
Assert . AreEqual ( 1 , config . Mounts . Length ) ;
Assert . AreEqual ( "test-volume" , config . Mounts [ 0 ] . Name ) ;
}
}
上面的示例创建了一个名为test-volume 的新卷,并计划在IVolumeService
上调用Dispose()
时将其删除。容器已创建,并将新创建的卷以读/写访问模式挂载到/var/lib/postgresql/data 。由于容器位于卷的using
语句的范围内,因此它的生命周期跨越整个容器生命周期,然后被删除。
FluentDocker支持,连接docker事件机制来监听其发送的事件。
using ( var events = Fd . Native ( ) . Events ( ) )
{
using (
var container =
new Builder ( ) . UseContainer ( )
. UseImage ( "postgres:9.6-alpine" )
. ExposePort ( 5432 )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. WaitForPort ( "5432/tcp" , 30000 /*30s*/ )
. Build ( )
. Start ( ) )
{
FdEvent e ;
while ( ( e = events . TryRead ( 3000 ) ) != null )
{
if ( e . Type == EventType . Container && e . Action == EventAction . Start )
break ;
}
}
}
事件侦听器是全局的,可以处理许多EventAction
类型。
例如
根据操作的不同,事件类型可能有所不同,例如EventAction.Kill
的ContainerKillEvent
。所有事件均源自FdEvent
。这意味着所有共享属性都在基本事件中,显式属性在派生事件中。
例如,“ContainerKillEvent”包含以下属性:
public sealed class ContainerKillActor : EventActor
{
/// <summary>
/// The image name and label such as "alpine:latest".
/// </summary>
public string Image { get ; set ; }
/// <summary>
/// Name of the container.
/// </summary>
public string Name { get ; set ; }
/// <summary>
/// The signal that the container has been signalled.
/// </summary>
public string Signal { get ; set ; }
}
此事件循环可用于拾取事件并驱动实例化的IService
实例。或者,如果您需要对网络添加或删除等做出反应。
在完整的框架中,它使用System.Diagnostics.Debugger.Log
进行详细日志记录。对于.net core,它使用标准的Microsoft.Extensions.Logging.ILog
来记录。两者都使用Ductus.FluentDocker类别,因此可以配置为参与或不参与日志记录并配置不同的日志记录目标。
在 .net core 中,您可以在应用程序配置文件中提供日志记录段。
{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Ductus.FluentDocker": "None"
}
}
}
请查看 https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging/?view=aspnetcore-2.1 了解更多信息。有关完整框架,请查看 appconfig 中所需的XML ,以获取 https://docs.microsoft.com/en-us/dotnet/framework/wcf/diagnostics/tracing/configuring-tracing 中描述的完整框架。
有一种通过( Ductus.FluentDocker.Services
) Logging.Enabled()
或Logging.Disabled()
禁用/启用日志记录的快速方法。这将强制启用/禁用日志记录。
可以覆盖FluentDocker从客户端角度解析容器 IP 的默认机制,例如WaitForPort
。这可以在ContainerBuilder
基础上被覆盖。
下面的示例覆盖默认行为。当它返回null
时,默认解析器就会启动。
using (
var container =
Fd . UseContainer ( )
. UseImage ( "postgres:9.6-alpine" )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. ExposePort ( 5432 )
. UseCustomResolver ( (
ports , portAndProto , dockerUri ) =>
{
if ( null == ports || string . IsNullOrEmpty ( portAndProto ) )
return null ;
if ( ! ports . TryGetValue ( portAndProto , out var endpoints ) )
return null ;
if ( null == endpoints || endpoints . Length == 0 )
return null ;
if ( CommandExtensions . IsNative ( ) )
return endpoints [ 0 ] ;
if ( CommandExtensions . IsEmulatedNative ( ) )
return CommandExtensions . IsDockerDnsAvailable ( )
? new IPEndPoint ( CommandExtensions . EmulatedNativeAddress ( ) , endpoints [ 0 ] . Port )
: new IPEndPoint ( IPAddress . Loopback , endpoints [ 0 ] . Port ) ;
if ( Equals ( endpoints [ 0 ] . Address , IPAddress . Any ) && null != dockerUri )
return new IPEndPoint ( IPAddress . Parse ( dockerUri . Host ) , endpoints [ 0 ] . Port ) ;
return endpoints [ 0 ] ;
} )
. WaitForPort ( "5432/tcp" , 30000 /*30s*/ )
. Build ( )
. Start ( ) )
{
var state = container . GetConfiguration ( true /*force*/ ) . State . ToServiceState ( ) ;
Assert . AreEqual ( ServiceRunningState . Running , state ) ;
}
在不使用 docker-machine 的情况下,使用FluentAPI与远程 docker 守护进程对话的支持有限。这可以通过手动创建DockerHostService
实例或在HostBuilder
上使用FromUri
来完成。
using ( var container = Fd . UseHost ( ) .
FromUri ( Settings . DockerUri , isWindowsHost : true ) .
UseContainer ( ) .
Build ( ) )
{
}
上面的示例从设置连接到自定义DockerUri
,并且是一个 Windows 容器 Docker 守护进程。
FromUri
使用DockerUri
创建IHostService
。这个uri是任意的。它还支持其他属性(见下文)。 public HostBuilder FromUri (
DockerUri uri ,
string name = null ,
bool isNative = true ,
bool stopWhenDisposed = false ,
bool isWindowsHost = false ,
string certificatePath = null ) { /*...*/ }
它将对所有参数使用“合理”默认值。大多数情况下uri就足够了。例如,如果不提供certificatePath,它将尝试从环境DOCKER_CERT_PATH获取它。如果在环境中没有找到,则默认为无。
UseHost
采用实例化的IHostService
实现。 该库支持docker-compose使用现有的 compose 文件来呈现服务并管理其生命周期。
以下示例将具有启动MySql和WordPress 的撰写文件。因此,单个组合服务下面将有两个容器服务。默认情况下,当调用Dispose()
时,它将停止服务并进行清理。这可以被流畅配置中的KeepContainers()
覆盖。
version : ' 3.3 '
services :
db :
image : mysql:5.7
volumes :
- db_data:/var/lib/mysql
restart : always
environment :
MYSQL_ROOT_PASSWORD : somewordpress
MYSQL_DATABASE : wordpress
MYSQL_USER : wordpress
MYSQL_PASSWORD : wordpress
wordpress :
depends_on :
- db
image : wordpress:latest
ports :
- " 8000:80 "
restart : always
environment :
WORDPRESS_DB_HOST : db:3306
WORDPRESS_DB_USER : wordpress
WORDPRESS_DB_PASSWORD : wordpress
volumes :
db_data :
上面的文件就是docker-compose文件,用来拼接完整的服务。
var file = Path . Combine ( Directory . GetCurrentDirectory ( ) ,
( TemplateString ) "Resources/ComposeTests/WordPress/docker-compose.yml" ) ;
using ( var svc = new Builder ( )
. UseContainer ( )
. UseCompose ( )
. FromFile ( file )
. RemoveOrphans ( )
. Build ( ) . Start ( ) )
{
var installPage = await "http://localhost:8000/wp-admin/install.php" . Wget ( ) ;
Assert . IsTrue ( installPage . IndexOf ( "https://wordpress.org/" , StringComparison . Ordinal ) != - 1 ) ;
Assert . AreEqual ( 1 , svc . Hosts . Count ) ;
Assert . AreEqual ( 2 , svc . Containers . Count ) ;
Assert . AreEqual ( 2 , svc . Images . Count ) ;
Assert . AreEqual ( 5 , svc . Services . Count ) ;
}
上面的代码片段流畅地配置了docker-compose服务并调用安装页面来验证 WordPress 是否确实正常工作。
还可以执行单个容器支持的所有操作,例如复制、导出、等待操作。例如:
var file = Path . Combine ( Directory . GetCurrentDirectory ( ) ,
( TemplateString ) "Resources/ComposeTests/WordPress/docker-compose.yml" ) ;
// @formatter:off
using ( new Builder ( )
. UseContainer ( )
. UseCompose ( )
. FromFile ( file )
. RemoveOrphans ( )
. WaitForHttp ( "wordpress" , "http://localhost:8000/wp-admin/install.php" , continuation : ( resp , cnt ) =>
resp . Body . IndexOf ( "https://wordpress.org/" , StringComparison . Ordinal ) != - 1 ? 0 : 500 )
. Build ( ) . Start ( ) )
// @formatter:on
{
// Since we have waited - this shall now always work.
var installPage = await "http://localhost:8000/wp-admin/install.php" . Wget ( ) ;
Assert . IsTrue ( installPage . IndexOf ( "https://wordpress.org/" , StringComparison . Ordinal ) != - 1 ) ;
}
上面的代码片段启动了 wordpress docker compose 项目并检查URL http://localhost:8000/wp-admin/install.php 是否返回正文中的某个值(在本例中为“https://wordpress.org” /”)。如果不是,则返回500 ,并且WaitForHttp
函数将等待 500 毫秒,然后再次调用。这也适用于任何自定义 lambda,只需使用WaitFor
即可。因此,可以在继续使用范围之前查询数据库。
对于 Linux 和 Mac 用户,有多种选项可用于对套接字进行身份验证。 FluentDocker支持 no sudo 、不带任何密码的sudo (用户在 /etc/sudoer 中添加为 NOPASSWD )或带密码的sudo 。默认情况下,FluentDocker 希望能够在没有任何sudo 的情况下进行通信。这些选项是全局的,但可以在运行时更改。
SudoMechanism . None . SetSudo ( ) ; // This is the default
SudoMechanism . Password . SetSudo ( "<my-sudo-password>" ) ;
SudoMechanism . NoPassword . SetSudo ( ) ;
如果您希望关闭sudo来与 docker 守护进程通信,您可以按照 docker 教程进行最后一步,将用户添加到 docker 组。
FluentDocker 支持连接到远程 docker 守护进程。流畅的 API 支持例如
new Builder ( ) . UseHost ( ) . UseMachine ( ) . WithName ( "remote-daemon" )
这需要在docker-machine注册表中预先设置条目。还可以定义基于SSH的docker-machine注册表整体来连接到远程守护进程。
using (
var container =
new Builder ( ) . UseHost ( )
. UseSsh ( "192.168.1.27" ) . WithName ( "remote-daemon" )
. WithSshUser ( "solo" ) . WithSshKeyPath ( "${E_LOCALAPPDATA}/lxss/home/martoffi/.ssh/id_rsa" )
. UseContainer ( )
. UseImage ( "postgres:9.6-alpine" )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. Build ( ) )
{
Assert . AreEqual ( ServiceRunningState . Stopped , container . State ) ;
}
此示例将创建一个名为Remote-daemon 的新docker-machine注册表项,该条目使用 IP 地址为192.168.1.27的SSH和SSH用户alone 。如果已经找到名为Remote-daemon 的条目,它将重用该条目。然后,它获取具有远程守护程序的正确证书和URL 的IHostService 。因此,可以在远程守护进程上创建一个 docker 容器,在这种情况下它是postgres镜像。当它处置容器时,像往常一样,它会从远程 Docker 中删除它。 IHostService确保获取所有必要的证书以验证连接。
上面的示例生成此docker-machine注册表项。
C:Usersmartoffi>docker-machine ls
NAME ACTIVE DRIVER STATE URL SWARM DOCKER ERRORS
remote-daemon * generic Running tcp://192.168.1.27:2376 v18.06.1-ce
为了使用UseSsh(...)
必须设置无密码的SSH隧道。此外,必须允许使用隧道的用户访问 docker 守护进程。
按照这些教程如何设置SSH隧道并确保用户可以访问 docker 守护进程。
基本上使用ssh-keygen -t rsa
创建一个新的 rsa 密钥以与SSH隧道一起使用,然后通过ssh-copy-id {username}@{host}
将其复制到远程主机。
按照第二个教程中的指定编辑 /etc/sudoers。
完成此操作后,您现在可以通过通用驱动程序或上面指定的 Fluent API 访问远程 docker 守护进程。要按照示例中指定的方式手动执行相同的操作,它看起来像这样。
C:Usersmartoffi>docker-machine.exe create --driver generic --generic-ip-address=192.168.1.27 --generic-ssh-key="%localappdata%/lxss/home/martoffi/.ssh/id_rsa" --generic-ssh-user=solo remote-daemon
Running pre-create checks...
Creating machine...
(remote-daemon) Importing SSH key...
Waiting for machine to be running, this may take a few minutes...
Detecting operating system of created instance...
Waiting for SSH to be available...
Detecting the provisioner...
Provisioning with ubuntu(systemd)...
Installing Docker...
Copying certs to the local machine directory...
Copying certs to the remote machine...
Setting Docker configuration on the remote daemon...
Checking connection to Docker...
Docker is up and running!
To see how to connect your Docker Client to the Docker Engine running on this virtual machine, run: docker-machine.exe env remote-daemon
现在注册表项已创建,可以为终端 docker 设置环境。
C:Usersmartoffi>docker-machine.exe env remote-daemon
SET DOCKER_TLS_VERIFY=1
SET DOCKER_HOST=tcp://192.168.1.24:2376
SET DOCKER_CERT_PATH=C:Usersmartoffi.dockermachinemachinesremote-daemon
SET DOCKER_MACHINE_NAME=remote-daemon
SET COMPOSE_CONVERT_WINDOWS_PATHS=true
REM Run this command to configure your shell:
REM @FOR /f "tokens=*" %i IN ('docker-machine.exe env remote-daemon') DO @%i
运行此命令以使 docker 客户端使用远程 docker 守护进程。
@FOR /f "tokens=*" %i IN ('docker-machine.exe env remote-daemon') DO @%i
所有使用docker
二进制文件的命令现在都将在远程 docker 守护进程上执行。
通过机器创建和查询 Hyper-V docker 机器时,需要提升进程,因为 Hyper-V 在标准用户模式下不会响应 API 调用。
可以为 docker 容器指定健康检查,以根据此类活动报告容器的状态。以下示例使用健康检查容器是否已退出。可以检查配置(确保强制刷新)运行状况检查报告的状态。
using (
var container =
Fd . UseContainer ( )
. UseImage ( "postgres:latest" , force : true )
. HealthCheck ( "exit" )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. Build ( )
. Start ( ) )
{
var config = container . GetConfiguration ( true ) ;
AreEqual ( HealthState . Starting , config . State . Health . Status ) ;
}
可以通过Fluent API和ContainerCreateParams
向 docker 容器指定 ulimit,例如限制打开文件的数量等。例如,当将打开文件的数量限制为 2048(软和硬)时,使用Fluent API可能看起来像这样。
using (
var container =
Fd . UseContainer ( )
. UseImage ( "postgres:latest" , force : true )
. UseUlimit ( Ulimit . NoFile , 2048 , 2048 )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. Build ( )
. Start ( ) )
{
// Do stuff
}
此存储库包含三个 nuget 包,一个用于流畅访问,一个用于 ms-test 基类,另一个用于测试时使用的 xunit 基类。例如,在单元测试中,可以启动 postgres 容器并等待数据库启动。
public class PostgresXUnitTests : IClassFixture < PostgresTestBase >
{
[ Fact ]
public void Test ( )
{
// We now have a running postgres
// and a valid connection string to use.
}
}
using (
var container =
new DockerBuilder ( )
. WithImage ( "kiasaki/alpine-postgres" )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. ExposePorts ( "5432" )
. WaitForPort ( "5432/tcp" , 30000 /*30s*/ )
. Build ( ) )
{
container . Create ( ) . Start ( ) ;
}
还可以重用抽象基类,例如 postgres 测试库来简化容器的单元测试并使其干净。
[ TestClass ]
public class PostgresMsTests : PostgresTestBase
{
[ TestMethod ]
public void Test ( )
{
// We now have a running postgres
// and a valid connection string to use.
}
}
FluentDockerTestBase
允许简单的覆盖来轻松执行任何自定义 docker 支持的测试。只需创建一个测试类并从FluentDockerTestBase
派生并覆盖合适的方法即可。例如。
protected override DockerBuilder Build ( )
{
return new DockerBuilder ( )
. WithImage ( "kiasaki/alpine-postgres" )
. WithEnvironment ( $ "POSTGRES_PASSWORD= { PostgresPassword } " )
. ExposePorts ( "5432" )
. WaitForPort ( "5432/tcp" , 30000 /*30s*/ ) ;
}
这将使用 docker 镜像 postgres:latest 创建一个构建器并设置一个环境字符串,它还将向主机公开 postgres 数据库端口 5432,以便可以连接到容器内的数据库。最后它将等待端口 5432。这可确保数据库正在运行并已正确启动。如果超时(在本例中设置为 30 秒),它将引发异常,并且容器将被停止并删除。注意主机端口不是5432!使用Container.GetHostPort("5432/tcp")
获取主机端口。主机 IP 可以通过Container.Host
属性检索,因此在与容器中的应用程序通信时应使用该主机 IP。
当容器成功拉取、创建和启动时是否需要回调。
protected override void OnContainerInitialized ( )
{
ConnectionString = string . Format ( PostgresConnectionString , Container . Host ,
Container . GetHostPort ( "5432/tcp" ) , PostgresUser ,
PostgresPassword , PostgresDb ) ;
}
此示例在新启动的容器中呈现到 postgresql 数据库的正确连接字符串。这可用于使用 Npgsql、EF7、NHibernate、Marten 或其他兼容工具进行连接。如果从 docker 存储库中提取镜像或者无法创建/启动容器,则不会调用此方法。
如果需要覆盖关闭前容器挂钩。
protected virtual void OnContainerTearDown ( )
{
// Do stuff before container is shut down.
}
注意,如果未命名容器,如果没有正确处置,docker容器仍然会运行,必须手动删除。这是一个功能而不是错误,因为您可能希望在测试中运行多个容器。 DockerContainer
类管理容器的实例 ID,因此只与它交互,而不与其他容器交互。
创建/启动新容器时,它将首先检查本地存储库是否已存在容器映像,如果未找到,则会下载它。这可能需要一些时间,并且只有一个调试日志(如果启用)可以监视下载过程。
当发生未处理的异常并且应用程序FailFast即快速终止时,它不会调用finally
子句。因此, using
语句中的WaitForPort
失败将不会释放容器服务。因此容器仍在运行。要解决此问题,可以使用全局 try...catch 或在本地注入一个,例如
try
{
using ( var container =
new Builder ( ) . UseContainer ( )
. UseImage ( "postgres:9.6-alpine" )
. ExposePort ( 5432 )
. WithEnvironment ( "POSTGRES_PASSWORD=postgres" )
. WaitForPort ( "5777/tcp" , 10000 ) // Fail here since 5777 is not valid port
. Build ( ) )
{
container . Start ( ) ; // FluentDockerException is thrown here since WaitForPort is executed
}
} catch { throw ; }
但这仅当应用程序因WaitForPort
中抛出FluentDockerException
而终止时才会发生,否则它将正确处理容器,因此不需要try...catch
。
这也可以使用Fd.Build
函数来解决(有关更多信息,请参阅使用生成器扩展)。
Fd
类是一个静态类,它提供了构建和运行单个容器和组合容器的便捷方法。要构建容器,只需使用:
var build = Fd . Build ( c => c . UseContainer ( )
. UseImage ( "postgres:9.6-alpine" )
. ExposePort ( 5432 )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. WaitForPort ( "5432/tcp" , TimeSpan . FromSeconds ( 30 ) ) ) ;
// This is the equivalent of
var build = new Builder ( ) . UseContainer ( )
. UseImage ( "postgres:9.6-alpine" )
. ExposePort ( 5432 )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. WaitForPort ( "5432/tcp" , TimeSpan . FromSeconds ( 30 ) ) ;
然后,这可以用于在安全的using
子句中启动容器,即使未捕获异常,该子句也保证被处置。
build . Container ( svc =>
{
var config = svc . GetConfiguration ( ) ;
// Do stuff...
} ) ;
在执行Container
方法后,在这种情况下容器将停止并被删除。这相当于
// This is equivalent of
try
{
using ( var svc = build . Build ( ) )
{
svc . Start ( ) ;
var config = svc . GetConfiguration ( ) ;
// Do stuff...
}
}
catch
{
Log ( .. . ) ;
throw ;
}
还可以将构建器和运行结合起来,例如通过:
Fd . Container ( c => c . UseContainer ( )
. UseImage ( "postgres:9.6-alpine" )
. ExposePort ( 5432 )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. WaitForPort ( "5432/tcp" , TimeSpan . FromSeconds ( 30 ) ) ,
svc =>
{
var config = svc . GetConfiguration ( ) ;
// Do stuff...
} ) ;
上面的示例将构建容器、启动、停止、最后删除容器。即使抛出Exception
,它也会被Disposed
。当然,可以像使用container
一样使用composite
扩展方法来使用复合容器。