Repository files navigation

Quality Gate StatusBuild status

PackageNuGet
FluentDockerNuGet
Microsoft TestNuGet
XUnit TestNuGet

FluentDocker

This library enables docker and docker-compose interactions usinga Fluent API. It is supported on Linux, Windows and Mac. It also has support for the legazy docker-machine interactions.

Sample Fluent API usage

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",30000/*30s*/).Build().Start()){varconfig=container.GetConfiguration(true);Assert.AreEqual(ServiceRunningState.Running,config.State.ToServiceState());}

This fires up a postgres and waits for it to be ready. To use compose, just do it like this:

💡 NOTE: Use the AssumeComposeVersion(ComposeVersion.V2) to use the V2 behaviour, default is still V1 (to be changed to default to V2 later this year)

varfile=Path.Combine(Directory.GetCurrentDirectory(),(TemplateString)"Resources/ComposeTests/WordPress/docker-compose.yml");// @formatter:offusing(varsvc=newBuilder().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 varinstallPage=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 composeAssert.AreEqual(2,svc.Containers.Count);// We can access each individual containerAssert.AreEqual(2,svc.Images.Count);// And the images used.}

:bulb Note for Linux Users: Docker requires sudo by default and the library by default expects that executing user do not need to do sudo in order to talk to the docker daemon. More description can be found in the Talking to Docker Daemon chapter.

The fluent API builds up one or more services. Each service may be composite or singular. Therefore it is possible to e.g. fire up several docker-compose based services and manage each of them as a single service or dig in and use all underlying services on each docker-compose service. It is also possible to use services directly e.g.

varfile=Path.Combine(Directory.GetCurrentDirectory(),(TemplateString)"Resources/ComposeTests/WordPress/docker-compose.yml");using(varsvc=newDockerComposeCompositeService(DockerHost,newDockerComposeConfig{ComposeFilePath=newList<string>{file},ForceRecreate=true,RemoveOrphans=true,StopOnDispose=true})){svc.Start();// We now have a running WordPress with a MySql databasevarinstallPage=await$"http://localhost:8000/wp-admin/install.php".Wget();Assert.IsTrue(installPage.IndexOf("https://wordpress.org/",StringComparison.Ordinal)!=-1);}

The above example creates a docker-compose service from a single compose file. When the service is disposed all underlying services is automatically stopped.

The library is supported by .NET full 4.51 framework and higher, .NET standard 1.6, 2.0. It is divided into three thin layers, each layer is accessible:

  1. Docker Binaries interactions - Static commands and docker environment
  2. Services - thin service layer to manage machines, containers etc.
  3. Fluent API - API to build/discover services to be used

The Majority of the service methods are extension methods and not hardwired into the service itself, making them lightweight and customizable. Since everything is accessible it is e.g. easy to add extensions method for a service that uses the layer 1 commands to provide functionality.

Contribution

I do welcome contribution, though there is no contribution guideline as of yet, make sure to adhere to .editorconfig when doing the Pull Requests. Otherwise the build will fail. I'll update with a real guideline sooner or later this year.

Basic Usage of Commands (Layer 1)

All commands needs a DockerUri to work with. It is the Uri to the docker daemon, either locally or remote. It can be discoverable or hardcoded. Discovery of local DockerUri can be done by

varhosts=newHosts().Discover();var_docker=hosts.FirstOrDefault(x =>x.IsNative)??hosts.FirstOrDefault(x =>x.Name=="default");

The example snipped will check for native, or docker beta "native" hosts, if not choose the docker-machine "default" as host. If you're using docker-machine and no machine exists or is not started it is easy to create / start a docker-machine by e.g. "test-machine".Create(1024,20000000,1). This will create a docker machine named "test-machine" with 1GB of RAM, 20GB Disk, and use one CPU.

It is now possible to use the Uri to communicate using the commands. For example to get the version of client and server docker binaries:

varresult=_docker.Host.Version(_docker.Certificates);Debug.WriteLine(result.Data);// Will Print the Client and Server Version and API Versions respectively.

All commands return a CommandResponse such that it is possible to check success factor by response.Success. If any data associated with the command it is returned in the response.Data property.

Then it is simple as below to start and stop include delete a container using the commands. Below starts a container and do a PS on it and then deletes it.

varid=_docker.Host.Run("nginx:latest",null,_docker.Certificates).Data;varps=_docker.Host.Ps(null,_docker.Certificates).Data;_docker.Host.RemoveContainer(id,true,true,null,_docker.Certificates);

When running on windows, one can choose to run linux or windows container. Use the LinuxDaemon or WindowsDaemon to control which daemon to talk to.

_docker.LinuxDaemon();// ensures that it will talk to linux daemon, if windows daemon it will switch

Some commands returns a stream of data when e.g. events or logs is wanted using a continuous stream. Streams can be used in background tasks and support CancellationToken. Below example tails a log.

using(varlogs=_docker.Host.Logs(id,_docker.Certificates)){while(!logs.IsFinished){varline=logs.TryRead(5000);// Do a read with timeoutif(null==line){break;}Debug.WriteLine(line);}}

Utility methods exists for commands. They come in different flaviours such as networking etc. For example when reading a log to the end:

using(varlogs=_docker.Host.Logs(id,_docker.Certificates)){foreach(varlineinlogs.ReadToEnd()){Debug.WriteLine(line);}}

Using Fluent API

The highest layer of this library is the fluent API where you can define and control machines, images, and containers. For example to setup a load balancer with two nodejs servers reading from a redis server can look like this (node image is custom built if not found in the repository).

varfullPath=(TemplateString)@"${TEMP}/fluentdockertest/${RND}";varnginx=Path.Combine(fullPath,"nginx.conf");Directory.CreateDirectory(fullPath);typeof(NsResolver).ResourceExtract(fullPath,"index.js");using(varservices=newBuilder()// Define custom node image to be used.DefineImage("mariotoffia/nodetest").ReuseIfAlreadyExists().From("ubuntu").Maintainer("Mario Toffia <mario.toffia@xyz.com>").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);varep=services.Containers.First(x =>x.Name=="nginx").ToHostExposedEndpoint("80/tcp");Assert.IsNotNull(ep);varround1=$"http://{ep.Address}:{ep.Port}".Wget();Assert.AreEqual("This page has been viewed 1 times!",round1);varround2=$"http://{ep.Address}:{ep.Port}".Wget();Assert.AreEqual("This page has been viewed 2 times!",round2);}

The above example defines a Dockerfile, builds it, for the node image. It then uses vanilla redis and nginx. If you just want to use an existing Dockerfile it can be done like this.

using(varservices=newBuilder().DefineImage("mariotoffia/nodetest").ReuseIfAlreadyExists().FromFile("/tmp/Dockerfile").Build().Start()){// Container either build to reused if found in registry and started here.}

The fluent API supports from defining a docker-machine to a set of docker instances. It has built-in support for e.g. waiting for a specific port or a process within the container before Build() completes and thus can be safely be used within a using statement. If specific management on wait timeouts etc. you can always build and start the container and use extension methods to do the waiting on the container itself.

To create a container just omit the start. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build()){Assert.AreEqual(ServiceRunningState.Stopped,container.State);}

This example creates a container with postgres, configure one environment variable. Within the using statement it is possible to start the IContainerService. Thus each built container is wrapped in a IContainerService. It is also possible to use the IHostService.GetContainers(...) to obtain the created, running, and exited containers. From the IHostService it is also possible to get all the images in the local repository to create containers from.

Whe you want to run a single container do use the fluent or container service start method. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start()){varconfig=container.GetConfiguration();Assert.AreEqual(ServiceRunningState.Running,container.State);Assert.IsTrue(config.Config.Env.Any(x =>x=="POSTGRES_PASSWORD=mysecretpassword"));}

By default the container is stopped and deleted when the Dispose method is run, in order to keep the container in archve, use the KeepContainer() on the fluent API. When Dispose() is invoked it will be stopped but not deleted. It is also possible to keep it running after dispose as well.

Working with ports

It is possible to expose ports both explicit or randomly. Either way it is possible to resolve the IP (in case of machine) and the port (in case of random port) to use in code. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(40001,5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start()){varendpoint=container.ToHostExposedEndpoint("5432/tcp");Assert.AreEqual(40001,endpoint.Port);}

Here we map the container port 5432 to host port 40001 explicitly. Note the use of container.ToHostExposedEndpoint(...). This is to always resolve to a working ip and port to communicate with the docker container. It is also possible to map a random port, i.e. let Docker choose a available port. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start()){varendpoint=container.ToHostExposedEndpoint("5432/tcp");Assert.AreNotEqual(0,endpoint.Port);}

The only difference here is that only one argument is used when ExposePort(...) was used to configure the container. The same usage applies otherwise and thus is transparent for the code.

In order to know when a certain service is up and running before starting to e.g. connect to it. It is possible to wait for a specific port to be open. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",30000/*30s*/).Build().Start()){varconfig=container.GetConfiguration(true);Assert.AreEqual(ServiceRunningState.Running,config.State.ToServiceState());}

In the above example we wait for the container port 5432 to be opened within 30 seconds. If it fails, it will throw an exception and thus the container will be disposed and removed (since we dont have any keep container etc. configuration).

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",30000/*30s*/,"127.0.0.1").Build().Start()){varconfig=container.GetConfiguration(true);Assert.AreEqual(ServiceRunningState.Running,config.State.ToServiceState());}

Sometimes it is not possible to directly reach the container, by local ip and port, instead e.g. the container has an exposed port on the loopback interface (127.0.0.1) and that is the only way of reaching the container from the program. The above example forces the address to be 127.0.0.1 but still resolves the host port. By default, FluentDocker uses the network inspect on the container to determine the network configuration.

Sometime it is not sufficient to just wait for a port. Sometimes a container process is much more vital to wait for. Therefore a wait for process method exist in the fluent API as well as an extension method on the container object. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForProcess("postgres",30000/*30s*/).Build().Start()){varconfig=container.GetConfiguration(true);Assert.AreEqual(ServiceRunningState.Running,config.State.ToServiceState());}

In the above example Build() will return control when the process "postgres" have been started within the container.

Filesystem & Files

In order to make use of containers, sometimes it is necessary to mount volumes in the container onto the host or just copy from or to the container. Depending on if you're running machine or docker natively volume mapping have the constraint that it must be reachable from the virtual machine.

A normal use case is to have e.g. a webserver serving content on a docker container and the user edits files on the host file system. In such scenario it is necessary to mount a docker container volume onto the host. For example:

conststringhtml="<html><head>Hello World</head><body><h1>Hello world</h1></body></html>";varhostPath=(TemplateString)@"${TEMP}/fluentdockertest/${RND}";Directory.CreateDirectory(hostPath);using(varcontainer=newBuilder().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);varresponse=$"http://{container.ToHostExposedEndpoint("80/tcp")}/hello.html".Wget();Assert.AreEqual(html,response);}

In the above example a nginx container is started and mounts '/usr/share/nginx/html' onto a (random, in temp directory) host path. A HTML file is copied into the host path and when a HTTP get towards the nginx docker container is done, that same file is served.

Sometimes it is necessary copy files to and from a container. For example copy a configuration file, configure it and copy it back. More common scenario is to copy a configuration file to the container, just before it is started. The multi container example copies a nginx configuration file just before it is started. Thus it is possible to avoid manually creating a Dockerfile and a image for such a simple task. Instead just use e.g. an official or custom image, copy configuration and run.

using(newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start().CopyFrom("/etc/conf.d",fullPath)){varfiles=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")));}

Above example copies a directory to a host path (fullPath) from a running container. Note the use of extension method here, thus not using the fluent API (since CopyFrom is after Start()). If you want to copy files from the container just before starting use the Fluent API instead.

using(newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").CopyOnStart("/etc/conf.d",fullPath).Build().Start()){}

The below example illustrates a much more common scenario where files are copied to the container. This example makes use of the extension method instead of the fluent API version. It takes a Diff snapshot before copy and then just after the copy. In the latter the hello.html is present.

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start().WaitForProcess("postgres",30000/*30s*/).Diff(outbefore).CopyTo("/bin",fullPath)){varafter=container.Diff();Assert.IsFalse(before.Any(x =>x.Item=="/bin/hello.html"));Assert.IsTrue(after.Any(x =>x.Item=="/bin/hello.html"));}

Sometime is it useful to copy files in the IContainerService.Dispose() (just before container has stopped). Therefore a fluent API exists to ensure that it will just do that.

using(newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").CopyOnDispose("/etc/conf.d",fullPath).Build().Start()){}

In order to analyze a container, export extension method and fluent API methods exists. Most notably is the possibility to export a container when a IContainerService is disposed.

using(newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").ExportOnDispose(fullPath).Build().Start()){}

This will produce a container export (tar file) on the host (fullPath). If you rather have it exploded (un-tared) use the ExportExplodedOnDispose method instead. Of course you can export the container any time using a extension method on the container.

A useful trick when it comes to unit-testing is to export the container state when the unit test fails for some reason, therefore it exists a Fluent API that will export when a certain Lambda condition is met. For example:

varfailure=false;using(newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").ExportOnDispose(fullPath, svc =>failure).Build().Start()){failure=true;}

This snippet will export the container when the using statement is disposing the container since the failure variable is set to true and is used in the ExportOnDispose expression.

IService Hooks

All services can be extended with hooks. In the ExportOnDispose(path, lambda) installs a Hook when the service state is set to execute the lambda when the state is Removing. It is possible to install and remove hooks on the fly. If multiple hooks are registered on same service instance, with same ServiceRunningState, they will be executed in installation order.

The hooks are particulary good if you want something to be executed when a state is about to be set (or executed) on the service such as Starting. The Fluent API makes use of those in some situations such as Copy files, export, etc.

Docker Networking

FluentDocker do support all docker network commands. It can discover networks by _docker.NetworkLs() where it discovers all networks and some simple parameters defined in NetworkRow. It can also inspect to gain deeper information about the network (such as which containers is in the network and Ipam configuration) by _docker.NetworkInspect(network:"networkId").

In order to create a new network, use the _docker.NetworkCreate("name_of_network"). It is also possible to supply NetworkCreateParams where everything can be customized such as creating a overlay network och change the Ipam configuration. To delete a network, just use the _docker.NetworkRm(network:"networkId").

Note that networks are not deleted if there are any containers attached to it!

When a network is created it is possible to put one or more containers into it using the _docker.NetworkConnect("containerId","networkId"). Note that containers may be in several networks at a time, thus can proxy request between isolated networks. To disconnect a container from a network, simple do a _docker.NetworkDisconnect("containerId","networkId").

The following sample runs a container, creates a new network, and connects the running container into the network. It then disconnect the container, delete it, and delete the network.

varcmd=_docker.Run("postgres:9.6-alpine",newContainerCreateParams{PortMappings=new[]{"40001:5432"},Environment=new[]{"POSTGRES_PASSWORD=mysecretpassword"}},_certificates);varcontainer=cmd.Data;varnetwork=string.Empty;varcreated=_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 Networking

It is also possible to use a fluent builder to build new or reuse existing docker networks. Those can then be referenced while building containers. It is possible to build more than one docker network and attach a container to more than one network at a time.

using(varnw=newBuilder().UseNetwork("test-network")){using(varcontainer=newDockerBuilder().WithImage("kiasaki/alpine-postgres").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").ExposePorts("5432").UseNetwork(nw).WaitForPort("5432/tcp",30000/*30s*/).Build()){container.Create().Start();}}

The above code snippet creates a new network called test-network and then creates a container that is attached to the test-network. When the Dispose() is called on nw it will remove the network. It is also possible to do static IP container assignments within the network by UseIpV4 or UseIpV6. For example:

using(varnw=Fd.UseNetwork("unit-test-nw").UseSubnet("10.18.0.0/16").Build()){using(varcontainer=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()){varip=container.GetConfiguration().NetworkSettings.Networks["unit-test-nw"].IPAddress;Assert.AreEqual("10.18.0.22",ip);}}

The above example creates a new network unit-test-nw with ip-range 10.18.0.0/16. It is the used in the new container. The IP for the container is set to 10.18.0.22 and is static due to UseIpV4 command.

Volume Support

FluentDocker supports docker volume management both from commands and from a fluent API. Therefore it is possible to have total control on volumes used in container such if it shall be disposed, reused, what driver to use etc.

varvolume=_docker.VolumeCreate("test-volume","local",opts:{{"type","nfs"},{"o=addr","192.168.1.1,rw"},{"device",":/path/to/dir"}});varcfg=_docker.VolumeInspect(_certificates,"test-volume");_docker.VolumeRm(force:true,id:"test-volume");

The above snippet creates a new volume with name test-volume and is of NFS type. It then inspects the just created volume and lastly force delete the volume.

Fluent Volume API

It is also possible to use a fluent API to create or use volumes. They can then be used when building a container. This is especially useful when creation of volumes are special or lifetime needs to be controlled.

using(varvol=newBuilder().UseVolume("test-volume").RemoveOnDispose().Build()){using(varcontainer=newBuilder().UseContainer().UseImage("postgres:9.6-alpine").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").MountVolume(vol,"/var/lib/postgresql/data",MountType.ReadWrite).Build().Start()){varconfig=container.GetConfiguration();Assert.AreEqual(1,config.Mounts.Length);Assert.AreEqual("test-volume",config.Mounts[0].Name);}}

The above sample creates a new volume called test-volume and it is scheduled to be delete when Dispose() is invoked on the IVolumeService. The container is created and mounts the newly created volume to /var/lib/postgresql/data as read/write access mode. Since the container is within the scope of the using statement of the volume it's lifetime spans the whole container lifetime and then gets deleted.

Events

FluentDocker supports, connecting to the docker event mechanism to listen to the events it sends.

using(varevents=Fd.Native().Events()){using(varcontainer=newBuilder().UseContainer().UseImage("postgres:9.6-alpine").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",30000/*30s*/).Build().Start()){FdEvente;while((e=events.TryRead(3000))!=null){if(e.Type==EventType.Container&&e.Action==EventAction.Start)break;}}}

Event listener is global and may handle many EventAction types.

For example

  • Pull
  • Create
  • Start
  • Kill
  • Die
  • Connect
  • Disconnect
  • Stop
  • Destroy

Depending on action, the event type may differ such as ContainerKillEvent for EventAction.Kill. All events derive from FdEvent. That means all shared properties is in the base event and the explicit ones are in the derived.

For example, the ´ContainerKillEvent` contains the following properties:

publicsealedclassContainerKillActor:EventActor{/// <summary>/// The image name and label such as "alpine:latest"./// </summary>publicstringImage{get;set;}/// <summary>/// Name of the container./// </summary>publicstringName{get;set;}/// <summary>/// The signal that the container has been signalled./// </summary>publicstringSignal{get;set;}}

This event loop may be used to pick up events and drive your instantiated IService instances. Or if you need to react to e.g. a network is added or deleted.

Logging

In the full framework it uses verbose logging using the System.Diagnostics.Debugger.Log. For .net core it uses the standard Microsoft.Extensions.Logging.ILog to log. Both are using the category Ductus.FluentDocker and therefore may be configured to participate in logging or not and configure different logging destinations.

In .net core you can provide the logging segment in the application config file.

{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Ductus.FluentDocker": "None"
}
}
}

Please check the https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging/?view=aspnetcore-2.1 for more information. For full framework please check out the XML needed in the appconfig for the full framework described in https://docs.microsoft.com/en-us/dotnet/framework/wcf/diagnostics/tracing/configuring-tracing.

There's a quick way of disabling / enabling logging via (Ductus.FluentDocker.Services) Logging.Enabled() or Logging.Disabled(). This will forcefully enable / disable logging.

Custom IPEndpoint Resolvers

It is possible to override the default mechanism of FluentDocker resolves the container IP from the clients perspective in e.g. WaitForPort. This can be overridden on ContainerBuilder basis.

The below sample, overrides the default behaviour. When it returns null the default resolver kicks in.

using(varcontainer=Fd.UseContainer().UseImage("postgres:9.6-alpine").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").ExposePort(5432).UseCustomResolver((ports,portAndProto,dockerUri)=>{if(null==ports||string.IsNullOrEmpty(portAndProto))returnnull;if(!ports.TryGetValue(portAndProto,outvarendpoints))returnnull;if(null==endpoints||endpoints.Length==0)returnnull;if(CommandExtensions.IsNative())returnendpoints[0];if(CommandExtensions.IsEmulatedNative())returnCommandExtensions.IsDockerDnsAvailable()?newIPEndPoint(CommandExtensions.EmulatedNativeAddress(),endpoints[0].Port):newIPEndPoint(IPAddress.Loopback,endpoints[0].Port);if(Equals(endpoints[0].Address,IPAddress.Any)&&null!=dockerUri)returnnewIPEndPoint(IPAddress.Parse(dockerUri.Host),endpoints[0].Port);returnendpoints[0];}).WaitForPort("5432/tcp",30000/*30s*/).Build().Start()){varstate=container.GetConfiguration(true/*force*/).State.ToServiceState();Assert.AreEqual(ServiceRunningState.Running,state);}

Talking to custom docker daemon URI without docker-machine

There's limited support to use the FluentAPI to talk to a remote docker daemon without using docker-machine. This is done either by manually creating a instance of a DockerHostService or use FromUri on HostBuilder.

using(varcontainer=Fd.UseHost().FromUri(Settings.DockerUri,isWindowsHost:true).UseContainer().Build()){}

The above sample connects to a custom DockerUri from a setting and is a windows container docker daemon.

  • FromUri that uses a DockerUri to create a IHostService. This uri is arbitrary. It also support other properties (see below).
publicHostBuilderFromUri(DockerUriuri,stringname=null,boolisNative=true,boolstopWhenDisposed=false,boolisWindowsHost=false,stringcertificatePath=null){/*...*/}

It will use "sensible" defaults on all parameters. Most of the case the uri is sufficient. For example if not providing the certificatePath it will try to get it from the environment DOCKER_CERT_PATH. If not found in the environment, it will default to none.

  • UseHost that takes a instantiated IHostService implementation.

Docker Compose Support

The library support docker-compose to use existing compose files to render services and manage lifetime of such.

The following sample will do have compose file that fires up a MySql and a WordPress. Therefore the single compose service will have two container services below it. By default, it will stop the services and clean up when Dispose() is invoked. This can be overridden by KeepContainers() in the fluent configuration.

version: '3.3'services:
db:
image: mysql:5.7volumes:
- db_data:/var/lib/mysqlrestart: alwaysenvironment:
MYSQL_ROOT_PASSWORD: somewordpressMYSQL_DATABASE: wordpressMYSQL_USER: wordpressMYSQL_PASSWORD: wordpresswordpress:
depends_on:
- dbimage: wordpress:latestports:
- "8000:80"restart: alwaysenvironment:
WORDPRESS_DB_HOST: db:3306WORDPRESS_DB_USER: wordpressWORDPRESS_DB_PASSWORD: wordpressvolumes:
db_data:

The above file is the docker-compose file to stitch up the complete service.

varfile=Path.Combine(Directory.GetCurrentDirectory(),(TemplateString)"Resources/ComposeTests/WordPress/docker-compose.yml");using(varsvc=newBuilder().UseContainer().UseCompose().FromFile(file).RemoveOrphans().Build().Start()){varinstallPage=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);}

The above snippet is fluently configuring the docker-compose service and invokes the install page to verify that WordPress is indeed working.

It is also possible to do all the operations that a single container supports such as copy on, export, wait operations. For example:

varfile=Path.Combine(Directory.GetCurrentDirectory(),(TemplateString)"Resources/ComposeTests/WordPress/docker-compose.yml");// @formatter:offusing(newBuilder().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. varinstallPage=await"http://localhost:8000/wp-admin/install.php".Wget();Assert.IsTrue(installPage.IndexOf("https://wordpress.org/",StringComparison.Ordinal)!=-1);}

The above snippet fires up the wordpress docker compose project and checks the URLhttp://localhost:8000/wp-admin/install.php it it returns a certain value in the body (in this case "https://wordpress.org/"). If not it returns 500 and the WaitForHttp function will wait 500 milliseconds before invoking again. This works for any custom lambda as well, just use WaitFor instead. Thus it is possible to e.g. query a database before continuing inside the using scope.

Talking to Docker Daemon

For Linux and Mac users there are several options how to authenticate towards the socket. FluentDocker supports no sudo, sudo without any password (user added as NOPASSWD in /etc/sudoer), or sudo with password. The default is that FluentDocker expects to be able to talk without any sudo. The options ar global but can be changed in runtime.

SudoMechanism.None.SetSudo();// This is the defaultSudoMechanism.Password.SetSudo("<my-sudo-password>");SudoMechanism.NoPassword.SetSudo();

If you wish to turn off sudo to communicate with the docker daemon, you can follow a docker tutorial and do the last step of adding your user to docker group.

Connecting to Remote Docker Daemons

FluentDocker supports connection to remote docker daemons. The fluent API supports e.g.

newBuilder().UseHost().UseMachine().WithName("remote-daemon")

where this requires a already pre-setup entry in the docker-machine registry. It is also possible to define SSH based docker-machine registry entires to connect to remote daemon.

using(varcontainer=newBuilder().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);}

This example will create a new docker-machine registry entry named remote-daemon that uses SSH with ip address of 192.168.1.27 and the SSH user solo. If a entry is already found named remote-daemon it will just reuse this entry. Then it gets a IHostService with correct certificates and URL for the remote daemon. Thus, it is possible then to create a docker container on the remote daemon, in thus case it is the postgres image. When it disposes the container, as usual it deletes it from the remote docker. The IHostService do make sure to pick upp all necessary certificates in order to authenticate the connection.

The above example produces this docker-machine registry entry.

C:\Users\martoffi>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

In order to use UseSsh(...) a SSH tunnel with no password must been set up. In addition the user that uses the tunnel must be allowed to access the docker daemon.

Follow these tutorial how to setup the SSH tunnel and make sure the user can access the docker daemon.

  1. https://www.kevinkuszyk.com/2016/11/28/connect-your-docker-client-to-a-remote-docker-host/
  2. https://askubuntu.com/questions/192050/how-to-run-sudo-command-with-no-password

Basically create a new rsa key to use with the SSH tunnel using ssh-keygen -t rsa and then copy it to the remote host by ssh-copy-id {username}@{host}.

Edit the /etc/sudoers as specified in the second tutorial.

When this is done, you now can access the remote docker daemon by the generic driver or the fluent API specified above. To do the same thing manually as specified in the example it would look something like this.

C:\Users\martoffi>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

Now the registry entry is created, it is possible set the environment for the terminal docker.

C:\Users\martoffi>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:\Users\martoffi\.docker\machine\machines\remote-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

Run this to make docker client use the remote docker daemon.

@FOR /f "tokens=*" %i IN ('docker-machine.exe env remote-daemon') DO @%i

All commands using the docker binary will now execute on the remote docker daemon.

Hyper-V

When creating and querying, via machine, a hyper-v docker machine the process needs to be elevated since Hyper-V will not respond to API calls in standard user mode.

Misc

Health Check

It is possible to specify a health check for the docker container to report the state of the container based on such activity. The following example is using a health check that the container has exited or not. It is possible to check the configuration (make sure to force refresh) what status the health check is reporting.

using(varcontainer=Fd.UseContainer().UseImage("postgres:latest",force:true).HealthCheck("exit").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start()){varconfig=container.GetConfiguration(true);AreEqual(HealthState.Starting,config.State.Health.Status);}

ulimit

It is possible via the Fluent API and ContainerCreateParams specify ulimit to the docker container to e.g. limit the number of open files etc. For example using the Fluent API could look like this when restricting the number of open files to 2048 (both soft and hard).

using(varcontainer=Fd.UseContainer().UseImage("postgres:latest",force:true).UseUlimit(Ulimit.NoFile,2048,2048).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start()){// Do stuff}

Test Support

This repo contains three nuget packages, one for the fluent access, one for ms-test base classes and another for xunit base classes to be used while testing. For example in a unit-test it is possible to fire up a postgres container and wait when the the db has booted.

XUnit

publicclassPostgresXUnitTests:IClassFixture<PostgresTestBase>{[Fact]publicvoidTest(){// We now have a running postgres// and a valid connection string to use.}}

MSTest

using(varcontainer=newDockerBuilder().WithImage("kiasaki/alpine-postgres").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").ExposePorts("5432").WaitForPort("5432/tcp",30000/*30s*/).Build()){container.Create().Start();}

It is also possible to re-use abstract base classes, for example postgres test base to simplify and make clean unittest towards a container.

[TestClass]publicclassPostgresMsTests:PostgresTestBase{[TestMethod]publicvoidTest(){// We now have a running postgres// and a valid connection string to use.}}

The FluentDockerTestBase allows for simple overrides to do whatever custom docker backed test easily. Just create a test class and derive from the FluentDockerTestBase and override suitable methods. For example.

protectedoverrideDockerBuilderBuild(){returnnewDockerBuilder().WithImage("kiasaki/alpine-postgres").WithEnvironment($"POSTGRES_PASSWORD={PostgresPassword}").ExposePorts("5432").WaitForPort("5432/tcp",30000/*30s*/);}

This will create a builder with docker image postgres:latest and set one environment string, it will also expose the postgres db port 5432 to the host so one can connect to the db within the container. Lastly it will wait for the port 5432. This ensures that the db is running and have properly booted. If timeout, in this example set to 30 seconds, it will throw an exception and the container is stopped and removed. Note that the host port is not 5432! Use Container.GetHostPort("5432/tcp") to get the host port. The host ip can be retrieved by Container.Host property and thus shall be used when communicating with the app in the container.

If a callback is needed when the container has been successfully pulled, created, and started.

protectedoverridevoidOnContainerInitialized(){ConnectionString=string.Format(PostgresConnectionString,Container.Host,Container.GetHostPort("5432/tcp"),PostgresUser,PostgresPassword,PostgresDb);}

This example renders a proper connection string to the postgresql db within the newly spun up container. This can be used to connect using Npgsql, EF7, NHibernate, Marten or other compatible tools. This method will not be called if pulling of the image from the docker repository or it could not create/start the container.

If a before shutdown container hook is wanted override.

protectedvirtualvoidOnContainerTearDown(){// Do stuff before container is shut down.}

Note that if un-named container, if not properly disposed, the docker container will still run and must be manually removed. This is a feature not a bug since you might want several containers running in your test. The DockerContainer class manages the instance id of the container and thus only interact with it and no other container.

When creating / starting a new container it will first check the local repository if the container image is already present and will download it if not found. This may take some time and there's just a Debug Log if enabled it is possible to monitor the download process.

Miscellanious

Unhandled Exceptions

When a unhandled exception occurs and the application FailFast i.e. terminates quickly it will not invoke finally clause. Thus a failing WaitForPort inside a using statement will not dispose the container service. Therefore the container is is still running. To fix this, either have a global try...catch or inject one locally e.g.

try{using(varcontainer=newBuilder().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;}

But it this is only when application termination is done due to the FluentDockerException thrown in the WaitForPort, otherwise it will dispose the container properly and thus the try...catch is not needed.

This could also be solved using the Fd.Build functions (see Using Builder Extensions for more information).

Using Builder Extensions

The class Fd is a static class that provides convenience methods for building and running single and composed containers. To build a container just use:

varbuild=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 ofvarbuild=newBuilder().UseContainer().UseImage("postgres:9.6-alpine").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",TimeSpan.FromSeconds(30));

This can then be used to start the containers within a safeusing clause that is guaranteed to be disposed even if uncaught exception.

build.Container(svc =>{varconfig=svc.GetConfiguration();// Do stuff...});

After the Container method has been executed the container is in this case stopped and removed. This is the quivalent of

// This is equivalent oftry{using(varsvc=build.Build()){svc.Start();varconfig=svc.GetConfiguration();// Do stuff... }}catch{Log(...);throw;}

It is also possible to combine builder and running e.g. via:

Fd.Container(c =>c.UseContainer().UseImage("postgres:9.6-alpine").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",TimeSpan.FromSeconds(30)),
svc =>{varconfig=svc.GetConfiguration();// Do stuff...});

The above example will build the container, start, stop, and finally delete the container. Even if and Exception is thrown it will be Disposed. Of course it is possible to use compsed container using composite extension methods as with container.

About

Use docker, docker-compose local and remote in tests and your .NET core/full framework apps via a FluentAPI

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

Quality Gate StatusBuild status

PackageNuGet
FluentDockerNuGet
Microsoft TestNuGet
XUnit TestNuGet

FluentDocker

This library enables docker and docker-compose interactions usinga Fluent API. It is supported on Linux, Windows and Mac. It also has support for the legazy docker-machine interactions.

Sample Fluent API usage

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",30000/*30s*/).Build().Start()){varconfig=container.GetConfiguration(true);Assert.AreEqual(ServiceRunningState.Running,config.State.ToServiceState());}

This fires up a postgres and waits for it to be ready. To use compose, just do it like this:

💡 NOTE: Use the AssumeComposeVersion(ComposeVersion.V2) to use the V2 behaviour, default is still V1 (to be changed to default to V2 later this year)

varfile=Path.Combine(Directory.GetCurrentDirectory(),(TemplateString)"Resources/ComposeTests/WordPress/docker-compose.yml");// @formatter:offusing(varsvc=newBuilder().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 varinstallPage=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 composeAssert.AreEqual(2,svc.Containers.Count);// We can access each individual containerAssert.AreEqual(2,svc.Images.Count);// And the images used.}

:bulb Note for Linux Users: Docker requires sudo by default and the library by default expects that executing user do not need to do sudo in order to talk to the docker daemon. More description can be found in the Talking to Docker Daemon chapter.

The fluent API builds up one or more services. Each service may be composite or singular. Therefore it is possible to e.g. fire up several docker-compose based services and manage each of them as a single service or dig in and use all underlying services on each docker-compose service. It is also possible to use services directly e.g.

varfile=Path.Combine(Directory.GetCurrentDirectory(),(TemplateString)"Resources/ComposeTests/WordPress/docker-compose.yml");using(varsvc=newDockerComposeCompositeService(DockerHost,newDockerComposeConfig{ComposeFilePath=newList<string>{file},ForceRecreate=true,RemoveOrphans=true,StopOnDispose=true})){svc.Start();// We now have a running WordPress with a MySql databasevarinstallPage=await$"http://localhost:8000/wp-admin/install.php".Wget();Assert.IsTrue(installPage.IndexOf("https://wordpress.org/",StringComparison.Ordinal)!=-1);}

The above example creates a docker-compose service from a single compose file. When the service is disposed all underlying services is automatically stopped.

The library is supported by .NET full 4.51 framework and higher, .NET standard 1.6, 2.0. It is divided into three thin layers, each layer is accessible:

  1. Docker Binaries interactions - Static commands and docker environment
  2. Services - thin service layer to manage machines, containers etc.
  3. Fluent API - API to build/discover services to be used

The Majority of the service methods are extension methods and not hardwired into the service itself, making them lightweight and customizable. Since everything is accessible it is e.g. easy to add extensions method for a service that uses the layer 1 commands to provide functionality.

Contribution

I do welcome contribution, though there is no contribution guideline as of yet, make sure to adhere to .editorconfig when doing the Pull Requests. Otherwise the build will fail. I'll update with a real guideline sooner or later this year.

Basic Usage of Commands (Layer 1)

All commands needs a DockerUri to work with. It is the Uri to the docker daemon, either locally or remote. It can be discoverable or hardcoded. Discovery of local DockerUri can be done by

varhosts=newHosts().Discover();var_docker=hosts.FirstOrDefault(x =>x.IsNative)??hosts.FirstOrDefault(x =>x.Name=="default");

The example snipped will check for native, or docker beta "native" hosts, if not choose the docker-machine "default" as host. If you're using docker-machine and no machine exists or is not started it is easy to create / start a docker-machine by e.g. "test-machine".Create(1024,20000000,1). This will create a docker machine named "test-machine" with 1GB of RAM, 20GB Disk, and use one CPU.

It is now possible to use the Uri to communicate using the commands. For example to get the version of client and server docker binaries:

varresult=_docker.Host.Version(_docker.Certificates);Debug.WriteLine(result.Data);// Will Print the Client and Server Version and API Versions respectively.

All commands return a CommandResponse such that it is possible to check success factor by response.Success. If any data associated with the command it is returned in the response.Data property.

Then it is simple as below to start and stop include delete a container using the commands. Below starts a container and do a PS on it and then deletes it.

varid=_docker.Host.Run("nginx:latest",null,_docker.Certificates).Data;varps=_docker.Host.Ps(null,_docker.Certificates).Data;_docker.Host.RemoveContainer(id,true,true,null,_docker.Certificates);

When running on windows, one can choose to run linux or windows container. Use the LinuxDaemon or WindowsDaemon to control which daemon to talk to.

_docker.LinuxDaemon();// ensures that it will talk to linux daemon, if windows daemon it will switch

Some commands returns a stream of data when e.g. events or logs is wanted using a continuous stream. Streams can be used in background tasks and support CancellationToken. Below example tails a log.

using(varlogs=_docker.Host.Logs(id,_docker.Certificates)){while(!logs.IsFinished){varline=logs.TryRead(5000);// Do a read with timeoutif(null==line){break;}Debug.WriteLine(line);}}

Utility methods exists for commands. They come in different flaviours such as networking etc. For example when reading a log to the end:

using(varlogs=_docker.Host.Logs(id,_docker.Certificates)){foreach(varlineinlogs.ReadToEnd()){Debug.WriteLine(line);}}

Using Fluent API

The highest layer of this library is the fluent API where you can define and control machines, images, and containers. For example to setup a load balancer with two nodejs servers reading from a redis server can look like this (node image is custom built if not found in the repository).

varfullPath=(TemplateString)@"${TEMP}/fluentdockertest/${RND}";varnginx=Path.Combine(fullPath,"nginx.conf");Directory.CreateDirectory(fullPath);typeof(NsResolver).ResourceExtract(fullPath,"index.js");using(varservices=newBuilder()// Define custom node image to be used.DefineImage("mariotoffia/nodetest").ReuseIfAlreadyExists().From("ubuntu").Maintainer("Mario Toffia <mario.toffia@xyz.com>").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);varep=services.Containers.First(x =>x.Name=="nginx").ToHostExposedEndpoint("80/tcp");Assert.IsNotNull(ep);varround1=$"http://{ep.Address}:{ep.Port}".Wget();Assert.AreEqual("This page has been viewed 1 times!",round1);varround2=$"http://{ep.Address}:{ep.Port}".Wget();Assert.AreEqual("This page has been viewed 2 times!",round2);}

The above example defines a Dockerfile, builds it, for the node image. It then uses vanilla redis and nginx. If you just want to use an existing Dockerfile it can be done like this.

using(varservices=newBuilder().DefineImage("mariotoffia/nodetest").ReuseIfAlreadyExists().FromFile("/tmp/Dockerfile").Build().Start()){// Container either build to reused if found in registry and started here.}

The fluent API supports from defining a docker-machine to a set of docker instances. It has built-in support for e.g. waiting for a specific port or a process within the container before Build() completes and thus can be safely be used within a using statement. If specific management on wait timeouts etc. you can always build and start the container and use extension methods to do the waiting on the container itself.

To create a container just omit the start. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build()){Assert.AreEqual(ServiceRunningState.Stopped,container.State);}

This example creates a container with postgres, configure one environment variable. Within the using statement it is possible to start the IContainerService. Thus each built container is wrapped in a IContainerService. It is also possible to use the IHostService.GetContainers(...) to obtain the created, running, and exited containers. From the IHostService it is also possible to get all the images in the local repository to create containers from.

Whe you want to run a single container do use the fluent or container service start method. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start()){varconfig=container.GetConfiguration();Assert.AreEqual(ServiceRunningState.Running,container.State);Assert.IsTrue(config.Config.Env.Any(x =>x=="POSTGRES_PASSWORD=mysecretpassword"));}

By default the container is stopped and deleted when the Dispose method is run, in order to keep the container in archve, use the KeepContainer() on the fluent API. When Dispose() is invoked it will be stopped but not deleted. It is also possible to keep it running after dispose as well.

Working with ports

It is possible to expose ports both explicit or randomly. Either way it is possible to resolve the IP (in case of machine) and the port (in case of random port) to use in code. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(40001,5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start()){varendpoint=container.ToHostExposedEndpoint("5432/tcp");Assert.AreEqual(40001,endpoint.Port);}

Here we map the container port 5432 to host port 40001 explicitly. Note the use of container.ToHostExposedEndpoint(...). This is to always resolve to a working ip and port to communicate with the docker container. It is also possible to map a random port, i.e. let Docker choose a available port. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start()){varendpoint=container.ToHostExposedEndpoint("5432/tcp");Assert.AreNotEqual(0,endpoint.Port);}

The only difference here is that only one argument is used when ExposePort(...) was used to configure the container. The same usage applies otherwise and thus is transparent for the code.

In order to know when a certain service is up and running before starting to e.g. connect to it. It is possible to wait for a specific port to be open. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",30000/*30s*/).Build().Start()){varconfig=container.GetConfiguration(true);Assert.AreEqual(ServiceRunningState.Running,config.State.ToServiceState());}

In the above example we wait for the container port 5432 to be opened within 30 seconds. If it fails, it will throw an exception and thus the container will be disposed and removed (since we dont have any keep container etc. configuration).

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",30000/*30s*/,"127.0.0.1").Build().Start()){varconfig=container.GetConfiguration(true);Assert.AreEqual(ServiceRunningState.Running,config.State.ToServiceState());}

Sometimes it is not possible to directly reach the container, by local ip and port, instead e.g. the container has an exposed port on the loopback interface (127.0.0.1) and that is the only way of reaching the container from the program. The above example forces the address to be 127.0.0.1 but still resolves the host port. By default, FluentDocker uses the network inspect on the container to determine the network configuration.

Sometime it is not sufficient to just wait for a port. Sometimes a container process is much more vital to wait for. Therefore a wait for process method exist in the fluent API as well as an extension method on the container object. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForProcess("postgres",30000/*30s*/).Build().Start()){varconfig=container.GetConfiguration(true);Assert.AreEqual(ServiceRunningState.Running,config.State.ToServiceState());}

In the above example Build() will return control when the process "postgres" have been started within the container.

Filesystem & Files

In order to make use of containers, sometimes it is necessary to mount volumes in the container onto the host or just copy from or to the container. Depending on if you're running machine or docker natively volume mapping have the constraint that it must be reachable from the virtual machine.

A normal use case is to have e.g. a webserver serving content on a docker container and the user edits files on the host file system. In such scenario it is necessary to mount a docker container volume onto the host. For example:

conststringhtml="<html><head>Hello World</head><body><h1>Hello world</h1></body></html>";varhostPath=(TemplateString)@"${TEMP}/fluentdockertest/${RND}";Directory.CreateDirectory(hostPath);using(varcontainer=newBuilder().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);varresponse=$"http://{container.ToHostExposedEndpoint("80/tcp")}/hello.html".Wget();Assert.AreEqual(html,response);}

In the above example a nginx container is started and mounts '/usr/share/nginx/html' onto a (random, in temp directory) host path. A HTML file is copied into the host path and when a HTTP get towards the nginx docker container is done, that same file is served.

Sometimes it is necessary copy files to and from a container. For example copy a configuration file, configure it and copy it back. More common scenario is to copy a configuration file to the container, just before it is started. The multi container example copies a nginx configuration file just before it is started. Thus it is possible to avoid manually creating a Dockerfile and a image for such a simple task. Instead just use e.g. an official or custom image, copy configuration and run.

using(newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start().CopyFrom("/etc/conf.d",fullPath)){varfiles=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")));}

Above example copies a directory to a host path (fullPath) from a running container. Note the use of extension method here, thus not using the fluent API (since CopyFrom is after Start()). If you want to copy files from the container just before starting use the Fluent API instead.

using(newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").CopyOnStart("/etc/conf.d",fullPath).Build().Start()){}

The below example illustrates a much more common scenario where files are copied to the container. This example makes use of the extension method instead of the fluent API version. It takes a Diff snapshot before copy and then just after the copy. In the latter the hello.html is present.

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start().WaitForProcess("postgres",30000/*30s*/).Diff(outbefore).CopyTo("/bin",fullPath)){varafter=container.Diff();Assert.IsFalse(before.Any(x =>x.Item=="/bin/hello.html"));Assert.IsTrue(after.Any(x =>x.Item=="/bin/hello.html"));}

Sometime is it useful to copy files in the IContainerService.Dispose() (just before container has stopped). Therefore a fluent API exists to ensure that it will just do that.

using(newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").CopyOnDispose("/etc/conf.d",fullPath).Build().Start()){}

In order to analyze a container, export extension method and fluent API methods exists. Most notably is the possibility to export a container when a IContainerService is disposed.

using(newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").ExportOnDispose(fullPath).Build().Start()){}

This will produce a container export (tar file) on the host (fullPath). If you rather have it exploded (un-tared) use the ExportExplodedOnDispose method instead. Of course you can export the container any time using a extension method on the container.

A useful trick when it comes to unit-testing is to export the container state when the unit test fails for some reason, therefore it exists a Fluent API that will export when a certain Lambda condition is met. For example:

varfailure=false;using(newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").ExportOnDispose(fullPath, svc =>failure).Build().Start()){failure=true;}

This snippet will export the container when the using statement is disposing the container since the failure variable is set to true and is used in the ExportOnDispose expression.

IService Hooks

All services can be extended with hooks. In the ExportOnDispose(path, lambda) installs a Hook when the service state is set to execute the lambda when the state is Removing. It is possible to install and remove hooks on the fly. If multiple hooks are registered on same service instance, with same ServiceRunningState, they will be executed in installation order.

The hooks are particulary good if you want something to be executed when a state is about to be set (or executed) on the service such as Starting. The Fluent API makes use of those in some situations such as Copy files, export, etc.

Docker Networking

FluentDocker do support all docker network commands. It can discover networks by _docker.NetworkLs() where it discovers all networks and some simple parameters defined in NetworkRow. It can also inspect to gain deeper information about the network (such as which containers is in the network and Ipam configuration) by _docker.NetworkInspect(network:"networkId").

In order to create a new network, use the _docker.NetworkCreate("name_of_network"). It is also possible to supply NetworkCreateParams where everything can be customized such as creating a overlay network och change the Ipam configuration. To delete a network, just use the _docker.NetworkRm(network:"networkId").

Note that networks are not deleted if there are any containers attached to it!

When a network is created it is possible to put one or more containers into it using the _docker.NetworkConnect("containerId","networkId"). Note that containers may be in several networks at a time, thus can proxy request between isolated networks. To disconnect a container from a network, simple do a _docker.NetworkDisconnect("containerId","networkId").

The following sample runs a container, creates a new network, and connects the running container into the network. It then disconnect the container, delete it, and delete the network.

varcmd=_docker.Run("postgres:9.6-alpine",newContainerCreateParams{PortMappings=new[]{"40001:5432"},Environment=new[]{"POSTGRES_PASSWORD=mysecretpassword"}},_certificates);varcontainer=cmd.Data;varnetwork=string.Empty;varcreated=_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 Networking

It is also possible to use a fluent builder to build new or reuse existing docker networks. Those can then be referenced while building containers. It is possible to build more than one docker network and attach a container to more than one network at a time.

using(varnw=newBuilder().UseNetwork("test-network")){using(varcontainer=newDockerBuilder().WithImage("kiasaki/alpine-postgres").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").ExposePorts("5432").UseNetwork(nw).WaitForPort("5432/tcp",30000/*30s*/).Build()){container.Create().Start();}}

The above code snippet creates a new network called test-network and then creates a container that is attached to the test-network. When the Dispose() is called on nw it will remove the network. It is also possible to do static IP container assignments within the network by UseIpV4 or UseIpV6. For example:

using(varnw=Fd.UseNetwork("unit-test-nw").UseSubnet("10.18.0.0/16").Build()){using(varcontainer=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()){varip=container.GetConfiguration().NetworkSettings.Networks["unit-test-nw"].IPAddress;Assert.AreEqual("10.18.0.22",ip);}}

The above example creates a new network unit-test-nw with ip-range 10.18.0.0/16. It is the used in the new container. The IP for the container is set to 10.18.0.22 and is static due to UseIpV4 command.

Volume Support

FluentDocker supports docker volume management both from commands and from a fluent API. Therefore it is possible to have total control on volumes used in container such if it shall be disposed, reused, what driver to use etc.

varvolume=_docker.VolumeCreate("test-volume","local",opts:{{"type","nfs"},{"o=addr","192.168.1.1,rw"},{"device",":/path/to/dir"}});varcfg=_docker.VolumeInspect(_certificates,"test-volume");_docker.VolumeRm(force:true,id:"test-volume");

The above snippet creates a new volume with name test-volume and is of NFS type. It then inspects the just created volume and lastly force delete the volume.

Fluent Volume API

It is also possible to use a fluent API to create or use volumes. They can then be used when building a container. This is especially useful when creation of volumes are special or lifetime needs to be controlled.

using(varvol=newBuilder().UseVolume("test-volume").RemoveOnDispose().Build()){using(varcontainer=newBuilder().UseContainer().UseImage("postgres:9.6-alpine").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").MountVolume(vol,"/var/lib/postgresql/data",MountType.ReadWrite).Build().Start()){varconfig=container.GetConfiguration();Assert.AreEqual(1,config.Mounts.Length);Assert.AreEqual("test-volume",config.Mounts[0].Name);}}

The above sample creates a new volume called test-volume and it is scheduled to be delete when Dispose() is invoked on the IVolumeService. The container is created and mounts the newly created volume to /var/lib/postgresql/data as read/write access mode. Since the container is within the scope of the using statement of the volume it's lifetime spans the whole container lifetime and then gets deleted.

Events

FluentDocker supports, connecting to the docker event mechanism to listen to the events it sends.

using(varevents=Fd.Native().Events()){using(varcontainer=newBuilder().UseContainer().UseImage("postgres:9.6-alpine").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",30000/*30s*/).Build().Start()){FdEvente;while((e=events.TryRead(3000))!=null){if(e.Type==EventType.Container&&e.Action==EventAction.Start)break;}}}

Event listener is global and may handle many EventAction types.

For example

  • Pull
  • Create
  • Start
  • Kill
  • Die
  • Connect
  • Disconnect
  • Stop
  • Destroy

Depending on action, the event type may differ such as ContainerKillEvent for EventAction.Kill. All events derive from FdEvent. That means all shared properties is in the base event and the explicit ones are in the derived.

For example, the ´ContainerKillEvent` contains the following properties:

publicsealedclassContainerKillActor:EventActor{/// <summary>/// The image name and label such as "alpine:latest"./// </summary>publicstringImage{get;set;}/// <summary>/// Name of the container./// </summary>publicstringName{get;set;}/// <summary>/// The signal that the container has been signalled./// </summary>publicstringSignal{get;set;}}

This event loop may be used to pick up events and drive your instantiated IService instances. Or if you need to react to e.g. a network is added or deleted.

Logging

In the full framework it uses verbose logging using the System.Diagnostics.Debugger.Log. For .net core it uses the standard Microsoft.Extensions.Logging.ILog to log. Both are using the category Ductus.FluentDocker and therefore may be configured to participate in logging or not and configure different logging destinations.

In .net core you can provide the logging segment in the application config file.

{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Ductus.FluentDocker": "None"
}
}
}

Please check the https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging/?view=aspnetcore-2.1 for more information. For full framework please check out the XML needed in the appconfig for the full framework described in https://docs.microsoft.com/en-us/dotnet/framework/wcf/diagnostics/tracing/configuring-tracing.

There's a quick way of disabling / enabling logging via (Ductus.FluentDocker.Services) Logging.Enabled() or Logging.Disabled(). This will forcefully enable / disable logging.

Custom IPEndpoint Resolvers

It is possible to override the default mechanism of FluentDocker resolves the container IP from the clients perspective in e.g. WaitForPort. This can be overridden on ContainerBuilder basis.

The below sample, overrides the default behaviour. When it returns null the default resolver kicks in.

using(varcontainer=Fd.UseContainer().UseImage("postgres:9.6-alpine").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").ExposePort(5432).UseCustomResolver((ports,portAndProto,dockerUri)=>{if(null==ports||string.IsNullOrEmpty(portAndProto))returnnull;if(!ports.TryGetValue(portAndProto,outvarendpoints))returnnull;if(null==endpoints||endpoints.Length==0)returnnull;if(CommandExtensions.IsNative())returnendpoints[0];if(CommandExtensions.IsEmulatedNative())returnCommandExtensions.IsDockerDnsAvailable()?newIPEndPoint(CommandExtensions.EmulatedNativeAddress(),endpoints[0].Port):newIPEndPoint(IPAddress.Loopback,endpoints[0].Port);if(Equals(endpoints[0].Address,IPAddress.Any)&&null!=dockerUri)returnnewIPEndPoint(IPAddress.Parse(dockerUri.Host),endpoints[0].Port);returnendpoints[0];}).WaitForPort("5432/tcp",30000/*30s*/).Build().Start()){varstate=container.GetConfiguration(true/*force*/).State.ToServiceState();Assert.AreEqual(ServiceRunningState.Running,state);}

Talking to custom docker daemon URI without docker-machine

There's limited support to use the FluentAPI to talk to a remote docker daemon without using docker-machine. This is done either by manually creating a instance of a DockerHostService or use FromUri on HostBuilder.

using(varcontainer=Fd.UseHost().FromUri(Settings.DockerUri,isWindowsHost:true).UseContainer().Build()){}

The above sample connects to a custom DockerUri from a setting and is a windows container docker daemon.

  • FromUri that uses a DockerUri to create a IHostService. This uri is arbitrary. It also support other properties (see below).
publicHostBuilderFromUri(DockerUriuri,stringname=null,boolisNative=true,boolstopWhenDisposed=false,boolisWindowsHost=false,stringcertificatePath=null){/*...*/}

It will use "sensible" defaults on all parameters. Most of the case the uri is sufficient. For example if not providing the certificatePath it will try to get it from the environment DOCKER_CERT_PATH. If not found in the environment, it will default to none.

  • UseHost that takes a instantiated IHostService implementation.

Docker Compose Support

The library support docker-compose to use existing compose files to render services and manage lifetime of such.

The following sample will do have compose file that fires up a MySql and a WordPress. Therefore the single compose service will have two container services below it. By default, it will stop the services and clean up when Dispose() is invoked. This can be overridden by KeepContainers() in the fluent configuration.

version: '3.3'services:
db:
image: mysql:5.7volumes:
- db_data:/var/lib/mysqlrestart: alwaysenvironment:
MYSQL_ROOT_PASSWORD: somewordpressMYSQL_DATABASE: wordpressMYSQL_USER: wordpressMYSQL_PASSWORD: wordpresswordpress:
depends_on:
- dbimage: wordpress:latestports:
- "8000:80"restart: alwaysenvironment:
WORDPRESS_DB_HOST: db:3306WORDPRESS_DB_USER: wordpressWORDPRESS_DB_PASSWORD: wordpressvolumes:
db_data:

The above file is the docker-compose file to stitch up the complete service.

varfile=Path.Combine(Directory.GetCurrentDirectory(),(TemplateString)"Resources/ComposeTests/WordPress/docker-compose.yml");using(varsvc=newBuilder().UseContainer().UseCompose().FromFile(file).RemoveOrphans().Build().Start()){varinstallPage=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);}

The above snippet is fluently configuring the docker-compose service and invokes the install page to verify that WordPress is indeed working.

It is also possible to do all the operations that a single container supports such as copy on, export, wait operations. For example:

varfile=Path.Combine(Directory.GetCurrentDirectory(),(TemplateString)"Resources/ComposeTests/WordPress/docker-compose.yml");// @formatter:offusing(newBuilder().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. varinstallPage=await"http://localhost:8000/wp-admin/install.php".Wget();Assert.IsTrue(installPage.IndexOf("https://wordpress.org/",StringComparison.Ordinal)!=-1);}

The above snippet fires up the wordpress docker compose project and checks the URLhttp://localhost:8000/wp-admin/install.php it it returns a certain value in the body (in this case "https://wordpress.org/"). If not it returns 500 and the WaitForHttp function will wait 500 milliseconds before invoking again. This works for any custom lambda as well, just use WaitFor instead. Thus it is possible to e.g. query a database before continuing inside the using scope.

Talking to Docker Daemon

For Linux and Mac users there are several options how to authenticate towards the socket. FluentDocker supports no sudo, sudo without any password (user added as NOPASSWD in /etc/sudoer), or sudo with password. The default is that FluentDocker expects to be able to talk without any sudo. The options ar global but can be changed in runtime.

SudoMechanism.None.SetSudo();// This is the defaultSudoMechanism.Password.SetSudo("<my-sudo-password>");SudoMechanism.NoPassword.SetSudo();

If you wish to turn off sudo to communicate with the docker daemon, you can follow a docker tutorial and do the last step of adding your user to docker group.

Connecting to Remote Docker Daemons

FluentDocker supports connection to remote docker daemons. The fluent API supports e.g.

newBuilder().UseHost().UseMachine().WithName("remote-daemon")

where this requires a already pre-setup entry in the docker-machine registry. It is also possible to define SSH based docker-machine registry entires to connect to remote daemon.

using(varcontainer=newBuilder().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);}

This example will create a new docker-machine registry entry named remote-daemon that uses SSH with ip address of 192.168.1.27 and the SSH user solo. If a entry is already found named remote-daemon it will just reuse this entry. Then it gets a IHostService with correct certificates and URL for the remote daemon. Thus, it is possible then to create a docker container on the remote daemon, in thus case it is the postgres image. When it disposes the container, as usual it deletes it from the remote docker. The IHostService do make sure to pick upp all necessary certificates in order to authenticate the connection.

The above example produces this docker-machine registry entry.

C:\Users\martoffi>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

In order to use UseSsh(...) a SSH tunnel with no password must been set up. In addition the user that uses the tunnel must be allowed to access the docker daemon.

Follow these tutorial how to setup the SSH tunnel and make sure the user can access the docker daemon.

  1. https://www.kevinkuszyk.com/2016/11/28/connect-your-docker-client-to-a-remote-docker-host/
  2. https://askubuntu.com/questions/192050/how-to-run-sudo-command-with-no-password

Basically create a new rsa key to use with the SSH tunnel using ssh-keygen -t rsa and then copy it to the remote host by ssh-copy-id {username}@{host}.

Edit the /etc/sudoers as specified in the second tutorial.

When this is done, you now can access the remote docker daemon by the generic driver or the fluent API specified above. To do the same thing manually as specified in the example it would look something like this.

C:\Users\martoffi>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

Now the registry entry is created, it is possible set the environment for the terminal docker.

C:\Users\martoffi>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:\Users\martoffi\.docker\machine\machines\remote-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

Run this to make docker client use the remote docker daemon.

@FOR /f "tokens=*" %i IN ('docker-machine.exe env remote-daemon') DO @%i

All commands using the docker binary will now execute on the remote docker daemon.

Hyper-V

When creating and querying, via machine, a hyper-v docker machine the process needs to be elevated since Hyper-V will not respond to API calls in standard user mode.

Misc

Health Check

It is possible to specify a health check for the docker container to report the state of the container based on such activity. The following example is using a health check that the container has exited or not. It is possible to check the configuration (make sure to force refresh) what status the health check is reporting.

using(varcontainer=Fd.UseContainer().UseImage("postgres:latest",force:true).HealthCheck("exit").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start()){varconfig=container.GetConfiguration(true);AreEqual(HealthState.Starting,config.State.Health.Status);}

ulimit

It is possible via the Fluent API and ContainerCreateParams specify ulimit to the docker container to e.g. limit the number of open files etc. For example using the Fluent API could look like this when restricting the number of open files to 2048 (both soft and hard).

using(varcontainer=Fd.UseContainer().UseImage("postgres:latest",force:true).UseUlimit(Ulimit.NoFile,2048,2048).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start()){// Do stuff}

Test Support

This repo contains three nuget packages, one for the fluent access, one for ms-test base classes and another for xunit base classes to be used while testing. For example in a unit-test it is possible to fire up a postgres container and wait when the the db has booted.

XUnit

publicclassPostgresXUnitTests:IClassFixture<PostgresTestBase>{[Fact]publicvoidTest(){// We now have a running postgres// and a valid connection string to use.}}

MSTest

using(varcontainer=newDockerBuilder().WithImage("kiasaki/alpine-postgres").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").ExposePorts("5432").WaitForPort("5432/tcp",30000/*30s*/).Build()){container.Create().Start();}

It is also possible to re-use abstract base classes, for example postgres test base to simplify and make clean unittest towards a container.

[TestClass]publicclassPostgresMsTests:PostgresTestBase{[TestMethod]publicvoidTest(){// We now have a running postgres// and a valid connection string to use.}}

The FluentDockerTestBase allows for simple overrides to do whatever custom docker backed test easily. Just create a test class and derive from the FluentDockerTestBase and override suitable methods. For example.

protectedoverrideDockerBuilderBuild(){returnnewDockerBuilder().WithImage("kiasaki/alpine-postgres").WithEnvironment($"POSTGRES_PASSWORD={PostgresPassword}").ExposePorts("5432").WaitForPort("5432/tcp",30000/*30s*/);}

This will create a builder with docker image postgres:latest and set one environment string, it will also expose the postgres db port 5432 to the host so one can connect to the db within the container. Lastly it will wait for the port 5432. This ensures that the db is running and have properly booted. If timeout, in this example set to 30 seconds, it will throw an exception and the container is stopped and removed. Note that the host port is not 5432! Use Container.GetHostPort("5432/tcp") to get the host port. The host ip can be retrieved by Container.Host property and thus shall be used when communicating with the app in the container.

If a callback is needed when the container has been successfully pulled, created, and started.

protectedoverridevoidOnContainerInitialized(){ConnectionString=string.Format(PostgresConnectionString,Container.Host,Container.GetHostPort("5432/tcp"),PostgresUser,PostgresPassword,PostgresDb);}

This example renders a proper connection string to the postgresql db within the newly spun up container. This can be used to connect using Npgsql, EF7, NHibernate, Marten or other compatible tools. This method will not be called if pulling of the image from the docker repository or it could not create/start the container.

If a before shutdown container hook is wanted override.

protectedvirtualvoidOnContainerTearDown(){// Do stuff before container is shut down.}

Note that if un-named container, if not properly disposed, the docker container will still run and must be manually removed. This is a feature not a bug since you might want several containers running in your test. The DockerContainer class manages the instance id of the container and thus only interact with it and no other container.

When creating / starting a new container it will first check the local repository if the container image is already present and will download it if not found. This may take some time and there's just a Debug Log if enabled it is possible to monitor the download process.

Miscellanious

Unhandled Exceptions

When a unhandled exception occurs and the application FailFast i.e. terminates quickly it will not invoke finally clause. Thus a failing WaitForPort inside a using statement will not dispose the container service. Therefore the container is is still running. To fix this, either have a global try...catch or inject one locally e.g.

try{using(varcontainer=newBuilder().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;}

But it this is only when application termination is done due to the FluentDockerException thrown in the WaitForPort, otherwise it will dispose the container properly and thus the try...catch is not needed.

This could also be solved using the Fd.Build functions (see Using Builder Extensions for more information).

Using Builder Extensions

The class Fd is a static class that provides convenience methods for building and running single and composed containers. To build a container just use:

varbuild=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 ofvarbuild=newBuilder().UseContainer().UseImage("postgres:9.6-alpine").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",TimeSpan.FromSeconds(30));

This can then be used to start the containers within a safeusing clause that is guaranteed to be disposed even if uncaught exception.

build.Container(svc =>{varconfig=svc.GetConfiguration();// Do stuff...});

After the Container method has been executed the container is in this case stopped and removed. This is the quivalent of

// This is equivalent oftry{using(varsvc=build.Build()){svc.Start();varconfig=svc.GetConfiguration();// Do stuff... }}catch{Log(...);throw;}

It is also possible to combine builder and running e.g. via:

Fd.Container(c =>c.UseContainer().UseImage("postgres:9.6-alpine").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",TimeSpan.FromSeconds(30)),
svc =>{varconfig=svc.GetConfiguration();// Do stuff...});

The above example will build the container, start, stop, and finally delete the container. Even if and Exception is thrown it will be Disposed. Of course it is possible to use compsed container using composite extension methods as with container.

About

Use docker, docker-compose local and remote in tests and your .NET core/full framework apps via a FluentAPI

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Quality Gate StatusBuild status

PackageNuGet
FluentDockerNuGet
Microsoft TestNuGet
XUnit TestNuGet

FluentDocker

This library enables docker and docker-compose interactions usinga Fluent API. It is supported on Linux, Windows and Mac. It also has support for the legazy docker-machine interactions.

Sample Fluent API usage

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",30000/*30s*/).Build().Start()){varconfig=container.GetConfiguration(true);Assert.AreEqual(ServiceRunningState.Running,config.State.ToServiceState());}

This fires up a postgres and waits for it to be ready. To use compose, just do it like this:

💡 NOTE: Use the AssumeComposeVersion(ComposeVersion.V2) to use the V2 behaviour, default is still V1 (to be changed to default to V2 later this year)

varfile=Path.Combine(Directory.GetCurrentDirectory(),(TemplateString)"Resources/ComposeTests/WordPress/docker-compose.yml");// @formatter:offusing(varsvc=newBuilder().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 varinstallPage=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 composeAssert.AreEqual(2,svc.Containers.Count);// We can access each individual containerAssert.AreEqual(2,svc.Images.Count);// And the images used.}

:bulb Note for Linux Users: Docker requires sudo by default and the library by default expects that executing user do not need to do sudo in order to talk to the docker daemon. More description can be found in the Talking to Docker Daemon chapter.

The fluent API builds up one or more services. Each service may be composite or singular. Therefore it is possible to e.g. fire up several docker-compose based services and manage each of them as a single service or dig in and use all underlying services on each docker-compose service. It is also possible to use services directly e.g.

varfile=Path.Combine(Directory.GetCurrentDirectory(),(TemplateString)"Resources/ComposeTests/WordPress/docker-compose.yml");using(varsvc=newDockerComposeCompositeService(DockerHost,newDockerComposeConfig{ComposeFilePath=newList<string>{file},ForceRecreate=true,RemoveOrphans=true,StopOnDispose=true})){svc.Start();// We now have a running WordPress with a MySql databasevarinstallPage=await$"http://localhost:8000/wp-admin/install.php".Wget();Assert.IsTrue(installPage.IndexOf("https://wordpress.org/",StringComparison.Ordinal)!=-1);}

The above example creates a docker-compose service from a single compose file. When the service is disposed all underlying services is automatically stopped.

The library is supported by .NET full 4.51 framework and higher, .NET standard 1.6, 2.0. It is divided into three thin layers, each layer is accessible:

  1. Docker Binaries interactions - Static commands and docker environment
  2. Services - thin service layer to manage machines, containers etc.
  3. Fluent API - API to build/discover services to be used

The Majority of the service methods are extension methods and not hardwired into the service itself, making them lightweight and customizable. Since everything is accessible it is e.g. easy to add extensions method for a service that uses the layer 1 commands to provide functionality.

Contribution

I do welcome contribution, though there is no contribution guideline as of yet, make sure to adhere to .editorconfig when doing the Pull Requests. Otherwise the build will fail. I'll update with a real guideline sooner or later this year.

Basic Usage of Commands (Layer 1)

All commands needs a DockerUri to work with. It is the Uri to the docker daemon, either locally or remote. It can be discoverable or hardcoded. Discovery of local DockerUri can be done by

varhosts=newHosts().Discover();var_docker=hosts.FirstOrDefault(x =>x.IsNative)??hosts.FirstOrDefault(x =>x.Name=="default");

The example snipped will check for native, or docker beta "native" hosts, if not choose the docker-machine "default" as host. If you're using docker-machine and no machine exists or is not started it is easy to create / start a docker-machine by e.g. "test-machine".Create(1024,20000000,1). This will create a docker machine named "test-machine" with 1GB of RAM, 20GB Disk, and use one CPU.

It is now possible to use the Uri to communicate using the commands. For example to get the version of client and server docker binaries:

varresult=_docker.Host.Version(_docker.Certificates);Debug.WriteLine(result.Data);// Will Print the Client and Server Version and API Versions respectively.

All commands return a CommandResponse such that it is possible to check success factor by response.Success. If any data associated with the command it is returned in the response.Data property.

Then it is simple as below to start and stop include delete a container using the commands. Below starts a container and do a PS on it and then deletes it.

varid=_docker.Host.Run("nginx:latest",null,_docker.Certificates).Data;varps=_docker.Host.Ps(null,_docker.Certificates).Data;_docker.Host.RemoveContainer(id,true,true,null,_docker.Certificates);

When running on windows, one can choose to run linux or windows container. Use the LinuxDaemon or WindowsDaemon to control which daemon to talk to.

_docker.LinuxDaemon();// ensures that it will talk to linux daemon, if windows daemon it will switch

Some commands returns a stream of data when e.g. events or logs is wanted using a continuous stream. Streams can be used in background tasks and support CancellationToken. Below example tails a log.

using(varlogs=_docker.Host.Logs(id,_docker.Certificates)){while(!logs.IsFinished){varline=logs.TryRead(5000);// Do a read with timeoutif(null==line){break;}Debug.WriteLine(line);}}

Utility methods exists for commands. They come in different flaviours such as networking etc. For example when reading a log to the end:

using(varlogs=_docker.Host.Logs(id,_docker.Certificates)){foreach(varlineinlogs.ReadToEnd()){Debug.WriteLine(line);}}

Using Fluent API

The highest layer of this library is the fluent API where you can define and control machines, images, and containers. For example to setup a load balancer with two nodejs servers reading from a redis server can look like this (node image is custom built if not found in the repository).

varfullPath=(TemplateString)@"${TEMP}/fluentdockertest/${RND}";varnginx=Path.Combine(fullPath,"nginx.conf");Directory.CreateDirectory(fullPath);typeof(NsResolver).ResourceExtract(fullPath,"index.js");using(varservices=newBuilder()// Define custom node image to be used.DefineImage("mariotoffia/nodetest").ReuseIfAlreadyExists().From("ubuntu").Maintainer("Mario Toffia <mario.toffia@xyz.com>").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);varep=services.Containers.First(x =>x.Name=="nginx").ToHostExposedEndpoint("80/tcp");Assert.IsNotNull(ep);varround1=$"http://{ep.Address}:{ep.Port}".Wget();Assert.AreEqual("This page has been viewed 1 times!",round1);varround2=$"http://{ep.Address}:{ep.Port}".Wget();Assert.AreEqual("This page has been viewed 2 times!",round2);}

The above example defines a Dockerfile, builds it, for the node image. It then uses vanilla redis and nginx. If you just want to use an existing Dockerfile it can be done like this.

using(varservices=newBuilder().DefineImage("mariotoffia/nodetest").ReuseIfAlreadyExists().FromFile("/tmp/Dockerfile").Build().Start()){// Container either build to reused if found in registry and started here.}

The fluent API supports from defining a docker-machine to a set of docker instances. It has built-in support for e.g. waiting for a specific port or a process within the container before Build() completes and thus can be safely be used within a using statement. If specific management on wait timeouts etc. you can always build and start the container and use extension methods to do the waiting on the container itself.

To create a container just omit the start. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build()){Assert.AreEqual(ServiceRunningState.Stopped,container.State);}

This example creates a container with postgres, configure one environment variable. Within the using statement it is possible to start the IContainerService. Thus each built container is wrapped in a IContainerService. It is also possible to use the IHostService.GetContainers(...) to obtain the created, running, and exited containers. From the IHostService it is also possible to get all the images in the local repository to create containers from.

Whe you want to run a single container do use the fluent or container service start method. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start()){varconfig=container.GetConfiguration();Assert.AreEqual(ServiceRunningState.Running,container.State);Assert.IsTrue(config.Config.Env.Any(x =>x=="POSTGRES_PASSWORD=mysecretpassword"));}

By default the container is stopped and deleted when the Dispose method is run, in order to keep the container in archve, use the KeepContainer() on the fluent API. When Dispose() is invoked it will be stopped but not deleted. It is also possible to keep it running after dispose as well.

Working with ports

It is possible to expose ports both explicit or randomly. Either way it is possible to resolve the IP (in case of machine) and the port (in case of random port) to use in code. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(40001,5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start()){varendpoint=container.ToHostExposedEndpoint("5432/tcp");Assert.AreEqual(40001,endpoint.Port);}

Here we map the container port 5432 to host port 40001 explicitly. Note the use of container.ToHostExposedEndpoint(...). This is to always resolve to a working ip and port to communicate with the docker container. It is also possible to map a random port, i.e. let Docker choose a available port. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start()){varendpoint=container.ToHostExposedEndpoint("5432/tcp");Assert.AreNotEqual(0,endpoint.Port);}

The only difference here is that only one argument is used when ExposePort(...) was used to configure the container. The same usage applies otherwise and thus is transparent for the code.

In order to know when a certain service is up and running before starting to e.g. connect to it. It is possible to wait for a specific port to be open. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",30000/*30s*/).Build().Start()){varconfig=container.GetConfiguration(true);Assert.AreEqual(ServiceRunningState.Running,config.State.ToServiceState());}

In the above example we wait for the container port 5432 to be opened within 30 seconds. If it fails, it will throw an exception and thus the container will be disposed and removed (since we dont have any keep container etc. configuration).

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",30000/*30s*/,"127.0.0.1").Build().Start()){varconfig=container.GetConfiguration(true);Assert.AreEqual(ServiceRunningState.Running,config.State.ToServiceState());}

Sometimes it is not possible to directly reach the container, by local ip and port, instead e.g. the container has an exposed port on the loopback interface (127.0.0.1) and that is the only way of reaching the container from the program. The above example forces the address to be 127.0.0.1 but still resolves the host port. By default, FluentDocker uses the network inspect on the container to determine the network configuration.

Sometime it is not sufficient to just wait for a port. Sometimes a container process is much more vital to wait for. Therefore a wait for process method exist in the fluent API as well as an extension method on the container object. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForProcess("postgres",30000/*30s*/).Build().Start()){varconfig=container.GetConfiguration(true);Assert.AreEqual(ServiceRunningState.Running,config.State.ToServiceState());}

In the above example Build() will return control when the process "postgres" have been started within the container.

Filesystem & Files

In order to make use of containers, sometimes it is necessary to mount volumes in the container onto the host or just copy from or to the container. Depending on if you're running machine or docker natively volume mapping have the constraint that it must be reachable from the virtual machine.

A normal use case is to have e.g. a webserver serving content on a docker container and the user edits files on the host file system. In such scenario it is necessary to mount a docker container volume onto the host. For example:

conststringhtml="<html><head>Hello World</head><body><h1>Hello world</h1></body></html>";varhostPath=(TemplateString)@"${TEMP}/fluentdockertest/${RND}";Directory.CreateDirectory(hostPath);using(varcontainer=newBuilder().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);varresponse=$"http://{container.ToHostExposedEndpoint("80/tcp")}/hello.html".Wget();Assert.AreEqual(html,response);}

In the above example a nginx container is started and mounts '/usr/share/nginx/html' onto a (random, in temp directory) host path. A HTML file is copied into the host path and when a HTTP get towards the nginx docker container is done, that same file is served.

Sometimes it is necessary copy files to and from a container. For example copy a configuration file, configure it and copy it back. More common scenario is to copy a configuration file to the container, just before it is started. The multi container example copies a nginx configuration file just before it is started. Thus it is possible to avoid manually creating a Dockerfile and a image for such a simple task. Instead just use e.g. an official or custom image, copy configuration and run.

using(newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start().CopyFrom("/etc/conf.d",fullPath)){varfiles=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")));}

Above example copies a directory to a host path (fullPath) from a running container. Note the use of extension method here, thus not using the fluent API (since CopyFrom is after Start()). If you want to copy files from the container just before starting use the Fluent API instead.

using(newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").CopyOnStart("/etc/conf.d",fullPath).Build().Start()){}

The below example illustrates a much more common scenario where files are copied to the container. This example makes use of the extension method instead of the fluent API version. It takes a Diff snapshot before copy and then just after the copy. In the latter the hello.html is present.

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start().WaitForProcess("postgres",30000/*30s*/).Diff(outbefore).CopyTo("/bin",fullPath)){varafter=container.Diff();Assert.IsFalse(before.Any(x =>x.Item=="/bin/hello.html"));Assert.IsTrue(after.Any(x =>x.Item=="/bin/hello.html"));}

Sometime is it useful to copy files in the IContainerService.Dispose() (just before container has stopped). Therefore a fluent API exists to ensure that it will just do that.

using(newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").CopyOnDispose("/etc/conf.d",fullPath).Build().Start()){}

In order to analyze a container, export extension method and fluent API methods exists. Most notably is the possibility to export a container when a IContainerService is disposed.

using(newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").ExportOnDispose(fullPath).Build().Start()){}

This will produce a container export (tar file) on the host (fullPath). If you rather have it exploded (un-tared) use the ExportExplodedOnDispose method instead. Of course you can export the container any time using a extension method on the container.

A useful trick when it comes to unit-testing is to export the container state when the unit test fails for some reason, therefore it exists a Fluent API that will export when a certain Lambda condition is met. For example:

varfailure=false;using(newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").ExportOnDispose(fullPath, svc =>failure).Build().Start()){failure=true;}

This snippet will export the container when the using statement is disposing the container since the failure variable is set to true and is used in the ExportOnDispose expression.

IService Hooks

All services can be extended with hooks. In the ExportOnDispose(path, lambda) installs a Hook when the service state is set to execute the lambda when the state is Removing. It is possible to install and remove hooks on the fly. If multiple hooks are registered on same service instance, with same ServiceRunningState, they will be executed in installation order.

The hooks are particulary good if you want something to be executed when a state is about to be set (or executed) on the service such as Starting. The Fluent API makes use of those in some situations such as Copy files, export, etc.

Docker Networking

FluentDocker do support all docker network commands. It can discover networks by _docker.NetworkLs() where it discovers all networks and some simple parameters defined in NetworkRow. It can also inspect to gain deeper information about the network (such as which containers is in the network and Ipam configuration) by _docker.NetworkInspect(network:"networkId").

In order to create a new network, use the _docker.NetworkCreate("name_of_network"). It is also possible to supply NetworkCreateParams where everything can be customized such as creating a overlay network och change the Ipam configuration. To delete a network, just use the _docker.NetworkRm(network:"networkId").

Note that networks are not deleted if there are any containers attached to it!

When a network is created it is possible to put one or more containers into it using the _docker.NetworkConnect("containerId","networkId"). Note that containers may be in several networks at a time, thus can proxy request between isolated networks. To disconnect a container from a network, simple do a _docker.NetworkDisconnect("containerId","networkId").

The following sample runs a container, creates a new network, and connects the running container into the network. It then disconnect the container, delete it, and delete the network.

varcmd=_docker.Run("postgres:9.6-alpine",newContainerCreateParams{PortMappings=new[]{"40001:5432"},Environment=new[]{"POSTGRES_PASSWORD=mysecretpassword"}},_certificates);varcontainer=cmd.Data;varnetwork=string.Empty;varcreated=_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 Networking

It is also possible to use a fluent builder to build new or reuse existing docker networks. Those can then be referenced while building containers. It is possible to build more than one docker network and attach a container to more than one network at a time.

using(varnw=newBuilder().UseNetwork("test-network")){using(varcontainer=newDockerBuilder().WithImage("kiasaki/alpine-postgres").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").ExposePorts("5432").UseNetwork(nw).WaitForPort("5432/tcp",30000/*30s*/).Build()){container.Create().Start();}}

The above code snippet creates a new network called test-network and then creates a container that is attached to the test-network. When the Dispose() is called on nw it will remove the network. It is also possible to do static IP container assignments within the network by UseIpV4 or UseIpV6. For example:

using(varnw=Fd.UseNetwork("unit-test-nw").UseSubnet("10.18.0.0/16").Build()){using(varcontainer=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()){varip=container.GetConfiguration().NetworkSettings.Networks["unit-test-nw"].IPAddress;Assert.AreEqual("10.18.0.22",ip);}}

The above example creates a new network unit-test-nw with ip-range 10.18.0.0/16. It is the used in the new container. The IP for the container is set to 10.18.0.22 and is static due to UseIpV4 command.

Volume Support

FluentDocker supports docker volume management both from commands and from a fluent API. Therefore it is possible to have total control on volumes used in container such if it shall be disposed, reused, what driver to use etc.

varvolume=_docker.VolumeCreate("test-volume","local",opts:{{"type","nfs"},{"o=addr","192.168.1.1,rw"},{"device",":/path/to/dir"}});varcfg=_docker.VolumeInspect(_certificates,"test-volume");_docker.VolumeRm(force:true,id:"test-volume");

The above snippet creates a new volume with name test-volume and is of NFS type. It then inspects the just created volume and lastly force delete the volume.

Fluent Volume API

It is also possible to use a fluent API to create or use volumes. They can then be used when building a container. This is especially useful when creation of volumes are special or lifetime needs to be controlled.

using(varvol=newBuilder().UseVolume("test-volume").RemoveOnDispose().Build()){using(varcontainer=newBuilder().UseContainer().UseImage("postgres:9.6-alpine").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").MountVolume(vol,"/var/lib/postgresql/data",MountType.ReadWrite).Build().Start()){varconfig=container.GetConfiguration();Assert.AreEqual(1,config.Mounts.Length);Assert.AreEqual("test-volume",config.Mounts[0].Name);}}

The above sample creates a new volume called test-volume and it is scheduled to be delete when Dispose() is invoked on the IVolumeService. The container is created and mounts the newly created volume to /var/lib/postgresql/data as read/write access mode. Since the container is within the scope of the using statement of the volume it's lifetime spans the whole container lifetime and then gets deleted.

Events

FluentDocker supports, connecting to the docker event mechanism to listen to the events it sends.

using(varevents=Fd.Native().Events()){using(varcontainer=newBuilder().UseContainer().UseImage("postgres:9.6-alpine").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",30000/*30s*/).Build().Start()){FdEvente;while((e=events.TryRead(3000))!=null){if(e.Type==EventType.Container&&e.Action==EventAction.Start)break;}}}

Event listener is global and may handle many EventAction types.

For example

  • Pull
  • Create
  • Start
  • Kill
  • Die
  • Connect
  • Disconnect
  • Stop
  • Destroy

Depending on action, the event type may differ such as ContainerKillEvent for EventAction.Kill. All events derive from FdEvent. That means all shared properties is in the base event and the explicit ones are in the derived.

For example, the ´ContainerKillEvent` contains the following properties:

publicsealedclassContainerKillActor:EventActor{/// <summary>/// The image name and label such as "alpine:latest"./// </summary>publicstringImage{get;set;}/// <summary>/// Name of the container./// </summary>publicstringName{get;set;}/// <summary>/// The signal that the container has been signalled./// </summary>publicstringSignal{get;set;}}

This event loop may be used to pick up events and drive your instantiated IService instances. Or if you need to react to e.g. a network is added or deleted.

Logging

In the full framework it uses verbose logging using the System.Diagnostics.Debugger.Log. For .net core it uses the standard Microsoft.Extensions.Logging.ILog to log. Both are using the category Ductus.FluentDocker and therefore may be configured to participate in logging or not and configure different logging destinations.

In .net core you can provide the logging segment in the application config file.

{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Ductus.FluentDocker": "None"
}
}
}

Please check the https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging/?view=aspnetcore-2.1 for more information. For full framework please check out the XML needed in the appconfig for the full framework described in https://docs.microsoft.com/en-us/dotnet/framework/wcf/diagnostics/tracing/configuring-tracing.

There's a quick way of disabling / enabling logging via (Ductus.FluentDocker.Services) Logging.Enabled() or Logging.Disabled(). This will forcefully enable / disable logging.

Custom IPEndpoint Resolvers

It is possible to override the default mechanism of FluentDocker resolves the container IP from the clients perspective in e.g. WaitForPort. This can be overridden on ContainerBuilder basis.

The below sample, overrides the default behaviour. When it returns null the default resolver kicks in.

using(varcontainer=Fd.UseContainer().UseImage("postgres:9.6-alpine").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").ExposePort(5432).UseCustomResolver((ports,portAndProto,dockerUri)=>{if(null==ports||string.IsNullOrEmpty(portAndProto))returnnull;if(!ports.TryGetValue(portAndProto,outvarendpoints))returnnull;if(null==endpoints||endpoints.Length==0)returnnull;if(CommandExtensions.IsNative())returnendpoints[0];if(CommandExtensions.IsEmulatedNative())returnCommandExtensions.IsDockerDnsAvailable()?newIPEndPoint(CommandExtensions.EmulatedNativeAddress(),endpoints[0].Port):newIPEndPoint(IPAddress.Loopback,endpoints[0].Port);if(Equals(endpoints[0].Address,IPAddress.Any)&&null!=dockerUri)returnnewIPEndPoint(IPAddress.Parse(dockerUri.Host),endpoints[0].Port);returnendpoints[0];}).WaitForPort("5432/tcp",30000/*30s*/).Build().Start()){varstate=container.GetConfiguration(true/*force*/).State.ToServiceState();Assert.AreEqual(ServiceRunningState.Running,state);}

Talking to custom docker daemon URI without docker-machine

There's limited support to use the FluentAPI to talk to a remote docker daemon without using docker-machine. This is done either by manually creating a instance of a DockerHostService or use FromUri on HostBuilder.

using(varcontainer=Fd.UseHost().FromUri(Settings.DockerUri,isWindowsHost:true).UseContainer().Build()){}

The above sample connects to a custom DockerUri from a setting and is a windows container docker daemon.

  • FromUri that uses a DockerUri to create a IHostService. This uri is arbitrary. It also support other properties (see below).
publicHostBuilderFromUri(DockerUriuri,stringname=null,boolisNative=true,boolstopWhenDisposed=false,boolisWindowsHost=false,stringcertificatePath=null){/*...*/}

It will use "sensible" defaults on all parameters. Most of the case the uri is sufficient. For example if not providing the certificatePath it will try to get it from the environment DOCKER_CERT_PATH. If not found in the environment, it will default to none.

  • UseHost that takes a instantiated IHostService implementation.

Docker Compose Support

The library support docker-compose to use existing compose files to render services and manage lifetime of such.

The following sample will do have compose file that fires up a MySql and a WordPress. Therefore the single compose service will have two container services below it. By default, it will stop the services and clean up when Dispose() is invoked. This can be overridden by KeepContainers() in the fluent configuration.

version: '3.3'services:
db:
image: mysql:5.7volumes:
- db_data:/var/lib/mysqlrestart: alwaysenvironment:
MYSQL_ROOT_PASSWORD: somewordpressMYSQL_DATABASE: wordpressMYSQL_USER: wordpressMYSQL_PASSWORD: wordpresswordpress:
depends_on:
- dbimage: wordpress:latestports:
- "8000:80"restart: alwaysenvironment:
WORDPRESS_DB_HOST: db:3306WORDPRESS_DB_USER: wordpressWORDPRESS_DB_PASSWORD: wordpressvolumes:
db_data:

The above file is the docker-compose file to stitch up the complete service.

varfile=Path.Combine(Directory.GetCurrentDirectory(),(TemplateString)"Resources/ComposeTests/WordPress/docker-compose.yml");using(varsvc=newBuilder().UseContainer().UseCompose().FromFile(file).RemoveOrphans().Build().Start()){varinstallPage=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);}

The above snippet is fluently configuring the docker-compose service and invokes the install page to verify that WordPress is indeed working.

It is also possible to do all the operations that a single container supports such as copy on, export, wait operations. For example:

varfile=Path.Combine(Directory.GetCurrentDirectory(),(TemplateString)"Resources/ComposeTests/WordPress/docker-compose.yml");// @formatter:offusing(newBuilder().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. varinstallPage=await"http://localhost:8000/wp-admin/install.php".Wget();Assert.IsTrue(installPage.IndexOf("https://wordpress.org/",StringComparison.Ordinal)!=-1);}

The above snippet fires up the wordpress docker compose project and checks the URLhttp://localhost:8000/wp-admin/install.php it it returns a certain value in the body (in this case "https://wordpress.org/"). If not it returns 500 and the WaitForHttp function will wait 500 milliseconds before invoking again. This works for any custom lambda as well, just use WaitFor instead. Thus it is possible to e.g. query a database before continuing inside the using scope.

Talking to Docker Daemon

For Linux and Mac users there are several options how to authenticate towards the socket. FluentDocker supports no sudo, sudo without any password (user added as NOPASSWD in /etc/sudoer), or sudo with password. The default is that FluentDocker expects to be able to talk without any sudo. The options ar global but can be changed in runtime.

SudoMechanism.None.SetSudo();// This is the defaultSudoMechanism.Password.SetSudo("<my-sudo-password>");SudoMechanism.NoPassword.SetSudo();

If you wish to turn off sudo to communicate with the docker daemon, you can follow a docker tutorial and do the last step of adding your user to docker group.

Connecting to Remote Docker Daemons

FluentDocker supports connection to remote docker daemons. The fluent API supports e.g.

newBuilder().UseHost().UseMachine().WithName("remote-daemon")

where this requires a already pre-setup entry in the docker-machine registry. It is also possible to define SSH based docker-machine registry entires to connect to remote daemon.

using(varcontainer=newBuilder().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);}

This example will create a new docker-machine registry entry named remote-daemon that uses SSH with ip address of 192.168.1.27 and the SSH user solo. If a entry is already found named remote-daemon it will just reuse this entry. Then it gets a IHostService with correct certificates and URL for the remote daemon. Thus, it is possible then to create a docker container on the remote daemon, in thus case it is the postgres image. When it disposes the container, as usual it deletes it from the remote docker. The IHostService do make sure to pick upp all necessary certificates in order to authenticate the connection.

The above example produces this docker-machine registry entry.

C:\Users\martoffi>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

In order to use UseSsh(...) a SSH tunnel with no password must been set up. In addition the user that uses the tunnel must be allowed to access the docker daemon.

Follow these tutorial how to setup the SSH tunnel and make sure the user can access the docker daemon.

  1. https://www.kevinkuszyk.com/2016/11/28/connect-your-docker-client-to-a-remote-docker-host/
  2. https://askubuntu.com/questions/192050/how-to-run-sudo-command-with-no-password

Basically create a new rsa key to use with the SSH tunnel using ssh-keygen -t rsa and then copy it to the remote host by ssh-copy-id {username}@{host}.

Edit the /etc/sudoers as specified in the second tutorial.

When this is done, you now can access the remote docker daemon by the generic driver or the fluent API specified above. To do the same thing manually as specified in the example it would look something like this.

C:\Users\martoffi>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

Now the registry entry is created, it is possible set the environment for the terminal docker.

C:\Users\martoffi>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:\Users\martoffi\.docker\machine\machines\remote-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

Run this to make docker client use the remote docker daemon.

@FOR /f "tokens=*" %i IN ('docker-machine.exe env remote-daemon') DO @%i

All commands using the docker binary will now execute on the remote docker daemon.

Hyper-V

When creating and querying, via machine, a hyper-v docker machine the process needs to be elevated since Hyper-V will not respond to API calls in standard user mode.

Misc

Health Check

It is possible to specify a health check for the docker container to report the state of the container based on such activity. The following example is using a health check that the container has exited or not. It is possible to check the configuration (make sure to force refresh) what status the health check is reporting.

using(varcontainer=Fd.UseContainer().UseImage("postgres:latest",force:true).HealthCheck("exit").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start()){varconfig=container.GetConfiguration(true);AreEqual(HealthState.Starting,config.State.Health.Status);}

ulimit

It is possible via the Fluent API and ContainerCreateParams specify ulimit to the docker container to e.g. limit the number of open files etc. For example using the Fluent API could look like this when restricting the number of open files to 2048 (both soft and hard).

using(varcontainer=Fd.UseContainer().UseImage("postgres:latest",force:true).UseUlimit(Ulimit.NoFile,2048,2048).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start()){// Do stuff}

Test Support

This repo contains three nuget packages, one for the fluent access, one for ms-test base classes and another for xunit base classes to be used while testing. For example in a unit-test it is possible to fire up a postgres container and wait when the the db has booted.

XUnit

publicclassPostgresXUnitTests:IClassFixture<PostgresTestBase>{[Fact]publicvoidTest(){// We now have a running postgres// and a valid connection string to use.}}

MSTest

using(varcontainer=newDockerBuilder().WithImage("kiasaki/alpine-postgres").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").ExposePorts("5432").WaitForPort("5432/tcp",30000/*30s*/).Build()){container.Create().Start();}

It is also possible to re-use abstract base classes, for example postgres test base to simplify and make clean unittest towards a container.

[TestClass]publicclassPostgresMsTests:PostgresTestBase{[TestMethod]publicvoidTest(){// We now have a running postgres// and a valid connection string to use.}}

The FluentDockerTestBase allows for simple overrides to do whatever custom docker backed test easily. Just create a test class and derive from the FluentDockerTestBase and override suitable methods. For example.

protectedoverrideDockerBuilderBuild(){returnnewDockerBuilder().WithImage("kiasaki/alpine-postgres").WithEnvironment($"POSTGRES_PASSWORD={PostgresPassword}").ExposePorts("5432").WaitForPort("5432/tcp",30000/*30s*/);}

This will create a builder with docker image postgres:latest and set one environment string, it will also expose the postgres db port 5432 to the host so one can connect to the db within the container. Lastly it will wait for the port 5432. This ensures that the db is running and have properly booted. If timeout, in this example set to 30 seconds, it will throw an exception and the container is stopped and removed. Note that the host port is not 5432! Use Container.GetHostPort("5432/tcp") to get the host port. The host ip can be retrieved by Container.Host property and thus shall be used when communicating with the app in the container.

If a callback is needed when the container has been successfully pulled, created, and started.

protectedoverridevoidOnContainerInitialized(){ConnectionString=string.Format(PostgresConnectionString,Container.Host,Container.GetHostPort("5432/tcp"),PostgresUser,PostgresPassword,PostgresDb);}

This example renders a proper connection string to the postgresql db within the newly spun up container. This can be used to connect using Npgsql, EF7, NHibernate, Marten or other compatible tools. This method will not be called if pulling of the image from the docker repository or it could not create/start the container.

If a before shutdown container hook is wanted override.

protectedvirtualvoidOnContainerTearDown(){// Do stuff before container is shut down.}

Note that if un-named container, if not properly disposed, the docker container will still run and must be manually removed. This is a feature not a bug since you might want several containers running in your test. The DockerContainer class manages the instance id of the container and thus only interact with it and no other container.

When creating / starting a new container it will first check the local repository if the container image is already present and will download it if not found. This may take some time and there's just a Debug Log if enabled it is possible to monitor the download process.

Miscellanious

Unhandled Exceptions

When a unhandled exception occurs and the application FailFast i.e. terminates quickly it will not invoke finally clause. Thus a failing WaitForPort inside a using statement will not dispose the container service. Therefore the container is is still running. To fix this, either have a global try...catch or inject one locally e.g.

try{using(varcontainer=newBuilder().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;}

But it this is only when application termination is done due to the FluentDockerException thrown in the WaitForPort, otherwise it will dispose the container properly and thus the try...catch is not needed.

This could also be solved using the Fd.Build functions (see Using Builder Extensions for more information).

Using Builder Extensions

The class Fd is a static class that provides convenience methods for building and running single and composed containers. To build a container just use:

varbuild=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 ofvarbuild=newBuilder().UseContainer().UseImage("postgres:9.6-alpine").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",TimeSpan.FromSeconds(30));

This can then be used to start the containers within a safeusing clause that is guaranteed to be disposed even if uncaught exception.

build.Container(svc =>{varconfig=svc.GetConfiguration();// Do stuff...});

After the Container method has been executed the container is in this case stopped and removed. This is the quivalent of

// This is equivalent oftry{using(varsvc=build.Build()){svc.Start();varconfig=svc.GetConfiguration();// Do stuff... }}catch{Log(...);throw;}

It is also possible to combine builder and running e.g. via:

Fd.Container(c =>c.UseContainer().UseImage("postgres:9.6-alpine").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",TimeSpan.FromSeconds(30)),
svc =>{varconfig=svc.GetConfiguration();// Do stuff...});

The above example will build the container, start, stop, and finally delete the container. Even if and Exception is thrown it will be Disposed. Of course it is possible to use compsed container using composite extension methods as with container.

About

Use docker, docker-compose local and remote in tests and your .NET core/full framework apps via a FluentAPI

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Quality Gate StatusBuild status

PackageNuGet
FluentDockerNuGet
Microsoft TestNuGet
XUnit TestNuGet

FluentDocker

This library enables docker and docker-compose interactions usinga Fluent API. It is supported on Linux, Windows and Mac. It also has support for the legazy docker-machine interactions.

Sample Fluent API usage

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",30000/*30s*/).Build().Start()){varconfig=container.GetConfiguration(true);Assert.AreEqual(ServiceRunningState.Running,config.State.ToServiceState());}

This fires up a postgres and waits for it to be ready. To use compose, just do it like this:

💡 NOTE: Use the AssumeComposeVersion(ComposeVersion.V2) to use the V2 behaviour, default is still V1 (to be changed to default to V2 later this year)

varfile=Path.Combine(Directory.GetCurrentDirectory(),(TemplateString)"Resources/ComposeTests/WordPress/docker-compose.yml");// @formatter:offusing(varsvc=newBuilder().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 varinstallPage=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 composeAssert.AreEqual(2,svc.Containers.Count);// We can access each individual containerAssert.AreEqual(2,svc.Images.Count);// And the images used.}

:bulb Note for Linux Users: Docker requires sudo by default and the library by default expects that executing user do not need to do sudo in order to talk to the docker daemon. More description can be found in the Talking to Docker Daemon chapter.

The fluent API builds up one or more services. Each service may be composite or singular. Therefore it is possible to e.g. fire up several docker-compose based services and manage each of them as a single service or dig in and use all underlying services on each docker-compose service. It is also possible to use services directly e.g.

varfile=Path.Combine(Directory.GetCurrentDirectory(),(TemplateString)"Resources/ComposeTests/WordPress/docker-compose.yml");using(varsvc=newDockerComposeCompositeService(DockerHost,newDockerComposeConfig{ComposeFilePath=newList<string>{file},ForceRecreate=true,RemoveOrphans=true,StopOnDispose=true})){svc.Start();// We now have a running WordPress with a MySql databasevarinstallPage=await$"http://localhost:8000/wp-admin/install.php".Wget();Assert.IsTrue(installPage.IndexOf("https://wordpress.org/",StringComparison.Ordinal)!=-1);}

The above example creates a docker-compose service from a single compose file. When the service is disposed all underlying services is automatically stopped.

The library is supported by .NET full 4.51 framework and higher, .NET standard 1.6, 2.0. It is divided into three thin layers, each layer is accessible:

  1. Docker Binaries interactions - Static commands and docker environment
  2. Services - thin service layer to manage machines, containers etc.
  3. Fluent API - API to build/discover services to be used

The Majority of the service methods are extension methods and not hardwired into the service itself, making them lightweight and customizable. Since everything is accessible it is e.g. easy to add extensions method for a service that uses the layer 1 commands to provide functionality.

Contribution

I do welcome contribution, though there is no contribution guideline as of yet, make sure to adhere to .editorconfig when doing the Pull Requests. Otherwise the build will fail. I'll update with a real guideline sooner or later this year.

Basic Usage of Commands (Layer 1)

All commands needs a DockerUri to work with. It is the Uri to the docker daemon, either locally or remote. It can be discoverable or hardcoded. Discovery of local DockerUri can be done by

varhosts=newHosts().Discover();var_docker=hosts.FirstOrDefault(x =>x.IsNative)??hosts.FirstOrDefault(x =>x.Name=="default");

The example snipped will check for native, or docker beta "native" hosts, if not choose the docker-machine "default" as host. If you're using docker-machine and no machine exists or is not started it is easy to create / start a docker-machine by e.g. "test-machine".Create(1024,20000000,1). This will create a docker machine named "test-machine" with 1GB of RAM, 20GB Disk, and use one CPU.

It is now possible to use the Uri to communicate using the commands. For example to get the version of client and server docker binaries:

varresult=_docker.Host.Version(_docker.Certificates);Debug.WriteLine(result.Data);// Will Print the Client and Server Version and API Versions respectively.

All commands return a CommandResponse such that it is possible to check success factor by response.Success. If any data associated with the command it is returned in the response.Data property.

Then it is simple as below to start and stop include delete a container using the commands. Below starts a container and do a PS on it and then deletes it.

varid=_docker.Host.Run("nginx:latest",null,_docker.Certificates).Data;varps=_docker.Host.Ps(null,_docker.Certificates).Data;_docker.Host.RemoveContainer(id,true,true,null,_docker.Certificates);

When running on windows, one can choose to run linux or windows container. Use the LinuxDaemon or WindowsDaemon to control which daemon to talk to.

_docker.LinuxDaemon();// ensures that it will talk to linux daemon, if windows daemon it will switch

Some commands returns a stream of data when e.g. events or logs is wanted using a continuous stream. Streams can be used in background tasks and support CancellationToken. Below example tails a log.

using(varlogs=_docker.Host.Logs(id,_docker.Certificates)){while(!logs.IsFinished){varline=logs.TryRead(5000);// Do a read with timeoutif(null==line){break;}Debug.WriteLine(line);}}

Utility methods exists for commands. They come in different flaviours such as networking etc. For example when reading a log to the end:

using(varlogs=_docker.Host.Logs(id,_docker.Certificates)){foreach(varlineinlogs.ReadToEnd()){Debug.WriteLine(line);}}

Using Fluent API

The highest layer of this library is the fluent API where you can define and control machines, images, and containers. For example to setup a load balancer with two nodejs servers reading from a redis server can look like this (node image is custom built if not found in the repository).

varfullPath=(TemplateString)@"${TEMP}/fluentdockertest/${RND}";varnginx=Path.Combine(fullPath,"nginx.conf");Directory.CreateDirectory(fullPath);typeof(NsResolver).ResourceExtract(fullPath,"index.js");using(varservices=newBuilder()// Define custom node image to be used.DefineImage("mariotoffia/nodetest").ReuseIfAlreadyExists().From("ubuntu").Maintainer("Mario Toffia <mario.toffia@xyz.com>").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);varep=services.Containers.First(x =>x.Name=="nginx").ToHostExposedEndpoint("80/tcp");Assert.IsNotNull(ep);varround1=$"http://{ep.Address}:{ep.Port}".Wget();Assert.AreEqual("This page has been viewed 1 times!",round1);varround2=$"http://{ep.Address}:{ep.Port}".Wget();Assert.AreEqual("This page has been viewed 2 times!",round2);}

The above example defines a Dockerfile, builds it, for the node image. It then uses vanilla redis and nginx. If you just want to use an existing Dockerfile it can be done like this.

using(varservices=newBuilder().DefineImage("mariotoffia/nodetest").ReuseIfAlreadyExists().FromFile("/tmp/Dockerfile").Build().Start()){// Container either build to reused if found in registry and started here.}

The fluent API supports from defining a docker-machine to a set of docker instances. It has built-in support for e.g. waiting for a specific port or a process within the container before Build() completes and thus can be safely be used within a using statement. If specific management on wait timeouts etc. you can always build and start the container and use extension methods to do the waiting on the container itself.

To create a container just omit the start. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build()){Assert.AreEqual(ServiceRunningState.Stopped,container.State);}

This example creates a container with postgres, configure one environment variable. Within the using statement it is possible to start the IContainerService. Thus each built container is wrapped in a IContainerService. It is also possible to use the IHostService.GetContainers(...) to obtain the created, running, and exited containers. From the IHostService it is also possible to get all the images in the local repository to create containers from.

Whe you want to run a single container do use the fluent or container service start method. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start()){varconfig=container.GetConfiguration();Assert.AreEqual(ServiceRunningState.Running,container.State);Assert.IsTrue(config.Config.Env.Any(x =>x=="POSTGRES_PASSWORD=mysecretpassword"));}

By default the container is stopped and deleted when the Dispose method is run, in order to keep the container in archve, use the KeepContainer() on the fluent API. When Dispose() is invoked it will be stopped but not deleted. It is also possible to keep it running after dispose as well.

Working with ports

It is possible to expose ports both explicit or randomly. Either way it is possible to resolve the IP (in case of machine) and the port (in case of random port) to use in code. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(40001,5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start()){varendpoint=container.ToHostExposedEndpoint("5432/tcp");Assert.AreEqual(40001,endpoint.Port);}

Here we map the container port 5432 to host port 40001 explicitly. Note the use of container.ToHostExposedEndpoint(...). This is to always resolve to a working ip and port to communicate with the docker container. It is also possible to map a random port, i.e. let Docker choose a available port. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start()){varendpoint=container.ToHostExposedEndpoint("5432/tcp");Assert.AreNotEqual(0,endpoint.Port);}

The only difference here is that only one argument is used when ExposePort(...) was used to configure the container. The same usage applies otherwise and thus is transparent for the code.

In order to know when a certain service is up and running before starting to e.g. connect to it. It is possible to wait for a specific port to be open. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",30000/*30s*/).Build().Start()){varconfig=container.GetConfiguration(true);Assert.AreEqual(ServiceRunningState.Running,config.State.ToServiceState());}

In the above example we wait for the container port 5432 to be opened within 30 seconds. If it fails, it will throw an exception and thus the container will be disposed and removed (since we dont have any keep container etc. configuration).

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",30000/*30s*/,"127.0.0.1").Build().Start()){varconfig=container.GetConfiguration(true);Assert.AreEqual(ServiceRunningState.Running,config.State.ToServiceState());}

Sometimes it is not possible to directly reach the container, by local ip and port, instead e.g. the container has an exposed port on the loopback interface (127.0.0.1) and that is the only way of reaching the container from the program. The above example forces the address to be 127.0.0.1 but still resolves the host port. By default, FluentDocker uses the network inspect on the container to determine the network configuration.

Sometime it is not sufficient to just wait for a port. Sometimes a container process is much more vital to wait for. Therefore a wait for process method exist in the fluent API as well as an extension method on the container object. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForProcess("postgres",30000/*30s*/).Build().Start()){varconfig=container.GetConfiguration(true);Assert.AreEqual(ServiceRunningState.Running,config.State.ToServiceState());}

In the above example Build() will return control when the process "postgres" have been started within the container.

Filesystem & Files

In order to make use of containers, sometimes it is necessary to mount volumes in the container onto the host or just copy from or to the container. Depending on if you're running machine or docker natively volume mapping have the constraint that it must be reachable from the virtual machine.

A normal use case is to have e.g. a webserver serving content on a docker container and the user edits files on the host file system. In such scenario it is necessary to mount a docker container volume onto the host. For example:

conststringhtml="<html><head>Hello World</head><body><h1>Hello world</h1></body></html>";varhostPath=(TemplateString)@"${TEMP}/fluentdockertest/${RND}";Directory.CreateDirectory(hostPath);using(varcontainer=newBuilder().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);varresponse=$"http://{container.ToHostExposedEndpoint("80/tcp")}/hello.html".Wget();Assert.AreEqual(html,response);}

In the above example a nginx container is started and mounts '/usr/share/nginx/html' onto a (random, in temp directory) host path. A HTML file is copied into the host path and when a HTTP get towards the nginx docker container is done, that same file is served.

Sometimes it is necessary copy files to and from a container. For example copy a configuration file, configure it and copy it back. More common scenario is to copy a configuration file to the container, just before it is started. The multi container example copies a nginx configuration file just before it is started. Thus it is possible to avoid manually creating a Dockerfile and a image for such a simple task. Instead just use e.g. an official or custom image, copy configuration and run.

using(newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start().CopyFrom("/etc/conf.d",fullPath)){varfiles=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")));}

Above example copies a directory to a host path (fullPath) from a running container. Note the use of extension method here, thus not using the fluent API (since CopyFrom is after Start()). If you want to copy files from the container just before starting use the Fluent API instead.

using(newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").CopyOnStart("/etc/conf.d",fullPath).Build().Start()){}

The below example illustrates a much more common scenario where files are copied to the container. This example makes use of the extension method instead of the fluent API version. It takes a Diff snapshot before copy and then just after the copy. In the latter the hello.html is present.

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start().WaitForProcess("postgres",30000/*30s*/).Diff(outbefore).CopyTo("/bin",fullPath)){varafter=container.Diff();Assert.IsFalse(before.Any(x =>x.Item=="/bin/hello.html"));Assert.IsTrue(after.Any(x =>x.Item=="/bin/hello.html"));}

Sometime is it useful to copy files in the IContainerService.Dispose() (just before container has stopped). Therefore a fluent API exists to ensure that it will just do that.

using(newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").CopyOnDispose("/etc/conf.d",fullPath).Build().Start()){}

In order to analyze a container, export extension method and fluent API methods exists. Most notably is the possibility to export a container when a IContainerService is disposed.

using(newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").ExportOnDispose(fullPath).Build().Start()){}

This will produce a container export (tar file) on the host (fullPath). If you rather have it exploded (un-tared) use the ExportExplodedOnDispose method instead. Of course you can export the container any time using a extension method on the container.

A useful trick when it comes to unit-testing is to export the container state when the unit test fails for some reason, therefore it exists a Fluent API that will export when a certain Lambda condition is met. For example:

varfailure=false;using(newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").ExportOnDispose(fullPath, svc =>failure).Build().Start()){failure=true;}

This snippet will export the container when the using statement is disposing the container since the failure variable is set to true and is used in the ExportOnDispose expression.

IService Hooks

All services can be extended with hooks. In the ExportOnDispose(path, lambda) installs a Hook when the service state is set to execute the lambda when the state is Removing. It is possible to install and remove hooks on the fly. If multiple hooks are registered on same service instance, with same ServiceRunningState, they will be executed in installation order.

The hooks are particulary good if you want something to be executed when a state is about to be set (or executed) on the service such as Starting. The Fluent API makes use of those in some situations such as Copy files, export, etc.

Docker Networking

FluentDocker do support all docker network commands. It can discover networks by _docker.NetworkLs() where it discovers all networks and some simple parameters defined in NetworkRow. It can also inspect to gain deeper information about the network (such as which containers is in the network and Ipam configuration) by _docker.NetworkInspect(network:"networkId").

In order to create a new network, use the _docker.NetworkCreate("name_of_network"). It is also possible to supply NetworkCreateParams where everything can be customized such as creating a overlay network och change the Ipam configuration. To delete a network, just use the _docker.NetworkRm(network:"networkId").

Note that networks are not deleted if there are any containers attached to it!

When a network is created it is possible to put one or more containers into it using the _docker.NetworkConnect("containerId","networkId"). Note that containers may be in several networks at a time, thus can proxy request between isolated networks. To disconnect a container from a network, simple do a _docker.NetworkDisconnect("containerId","networkId").

The following sample runs a container, creates a new network, and connects the running container into the network. It then disconnect the container, delete it, and delete the network.

varcmd=_docker.Run("postgres:9.6-alpine",newContainerCreateParams{PortMappings=new[]{"40001:5432"},Environment=new[]{"POSTGRES_PASSWORD=mysecretpassword"}},_certificates);varcontainer=cmd.Data;varnetwork=string.Empty;varcreated=_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 Networking

It is also possible to use a fluent builder to build new or reuse existing docker networks. Those can then be referenced while building containers. It is possible to build more than one docker network and attach a container to more than one network at a time.

using(varnw=newBuilder().UseNetwork("test-network")){using(varcontainer=newDockerBuilder().WithImage("kiasaki/alpine-postgres").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").ExposePorts("5432").UseNetwork(nw).WaitForPort("5432/tcp",30000/*30s*/).Build()){container.Create().Start();}}

The above code snippet creates a new network called test-network and then creates a container that is attached to the test-network. When the Dispose() is called on nw it will remove the network. It is also possible to do static IP container assignments within the network by UseIpV4 or UseIpV6. For example:

using(varnw=Fd.UseNetwork("unit-test-nw").UseSubnet("10.18.0.0/16").Build()){using(varcontainer=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()){varip=container.GetConfiguration().NetworkSettings.Networks["unit-test-nw"].IPAddress;Assert.AreEqual("10.18.0.22",ip);}}

The above example creates a new network unit-test-nw with ip-range 10.18.0.0/16. It is the used in the new container. The IP for the container is set to 10.18.0.22 and is static due to UseIpV4 command.

Volume Support

FluentDocker supports docker volume management both from commands and from a fluent API. Therefore it is possible to have total control on volumes used in container such if it shall be disposed, reused, what driver to use etc.

varvolume=_docker.VolumeCreate("test-volume","local",opts:{{"type","nfs"},{"o=addr","192.168.1.1,rw"},{"device",":/path/to/dir"}});varcfg=_docker.VolumeInspect(_certificates,"test-volume");_docker.VolumeRm(force:true,id:"test-volume");

The above snippet creates a new volume with name test-volume and is of NFS type. It then inspects the just created volume and lastly force delete the volume.

Fluent Volume API

It is also possible to use a fluent API to create or use volumes. They can then be used when building a container. This is especially useful when creation of volumes are special or lifetime needs to be controlled.

using(varvol=newBuilder().UseVolume("test-volume").RemoveOnDispose().Build()){using(varcontainer=newBuilder().UseContainer().UseImage("postgres:9.6-alpine").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").MountVolume(vol,"/var/lib/postgresql/data",MountType.ReadWrite).Build().Start()){varconfig=container.GetConfiguration();Assert.AreEqual(1,config.Mounts.Length);Assert.AreEqual("test-volume",config.Mounts[0].Name);}}

The above sample creates a new volume called test-volume and it is scheduled to be delete when Dispose() is invoked on the IVolumeService. The container is created and mounts the newly created volume to /var/lib/postgresql/data as read/write access mode. Since the container is within the scope of the using statement of the volume it's lifetime spans the whole container lifetime and then gets deleted.

Events

FluentDocker supports, connecting to the docker event mechanism to listen to the events it sends.

using(varevents=Fd.Native().Events()){using(varcontainer=newBuilder().UseContainer().UseImage("postgres:9.6-alpine").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",30000/*30s*/).Build().Start()){FdEvente;while((e=events.TryRead(3000))!=null){if(e.Type==EventType.Container&&e.Action==EventAction.Start)break;}}}

Event listener is global and may handle many EventAction types.

For example

  • Pull
  • Create
  • Start
  • Kill
  • Die
  • Connect
  • Disconnect
  • Stop
  • Destroy

Depending on action, the event type may differ such as ContainerKillEvent for EventAction.Kill. All events derive from FdEvent. That means all shared properties is in the base event and the explicit ones are in the derived.

For example, the ´ContainerKillEvent` contains the following properties:

publicsealedclassContainerKillActor:EventActor{/// <summary>/// The image name and label such as "alpine:latest"./// </summary>publicstringImage{get;set;}/// <summary>/// Name of the container./// </summary>publicstringName{get;set;}/// <summary>/// The signal that the container has been signalled./// </summary>publicstringSignal{get;set;}}

This event loop may be used to pick up events and drive your instantiated IService instances. Or if you need to react to e.g. a network is added or deleted.

Logging

In the full framework it uses verbose logging using the System.Diagnostics.Debugger.Log. For .net core it uses the standard Microsoft.Extensions.Logging.ILog to log. Both are using the category Ductus.FluentDocker and therefore may be configured to participate in logging or not and configure different logging destinations.

In .net core you can provide the logging segment in the application config file.

{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Ductus.FluentDocker": "None"
}
}
}

Please check the https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging/?view=aspnetcore-2.1 for more information. For full framework please check out the XML needed in the appconfig for the full framework described in https://docs.microsoft.com/en-us/dotnet/framework/wcf/diagnostics/tracing/configuring-tracing.

There's a quick way of disabling / enabling logging via (Ductus.FluentDocker.Services) Logging.Enabled() or Logging.Disabled(). This will forcefully enable / disable logging.

Custom IPEndpoint Resolvers

It is possible to override the default mechanism of FluentDocker resolves the container IP from the clients perspective in e.g. WaitForPort. This can be overridden on ContainerBuilder basis.

The below sample, overrides the default behaviour. When it returns null the default resolver kicks in.

using(varcontainer=Fd.UseContainer().UseImage("postgres:9.6-alpine").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").ExposePort(5432).UseCustomResolver((ports,portAndProto,dockerUri)=>{if(null==ports||string.IsNullOrEmpty(portAndProto))returnnull;if(!ports.TryGetValue(portAndProto,outvarendpoints))returnnull;if(null==endpoints||endpoints.Length==0)returnnull;if(CommandExtensions.IsNative())returnendpoints[0];if(CommandExtensions.IsEmulatedNative())returnCommandExtensions.IsDockerDnsAvailable()?newIPEndPoint(CommandExtensions.EmulatedNativeAddress(),endpoints[0].Port):newIPEndPoint(IPAddress.Loopback,endpoints[0].Port);if(Equals(endpoints[0].Address,IPAddress.Any)&&null!=dockerUri)returnnewIPEndPoint(IPAddress.Parse(dockerUri.Host),endpoints[0].Port);returnendpoints[0];}).WaitForPort("5432/tcp",30000/*30s*/).Build().Start()){varstate=container.GetConfiguration(true/*force*/).State.ToServiceState();Assert.AreEqual(ServiceRunningState.Running,state);}

Talking to custom docker daemon URI without docker-machine

There's limited support to use the FluentAPI to talk to a remote docker daemon without using docker-machine. This is done either by manually creating a instance of a DockerHostService or use FromUri on HostBuilder.

using(varcontainer=Fd.UseHost().FromUri(Settings.DockerUri,isWindowsHost:true).UseContainer().Build()){}

The above sample connects to a custom DockerUri from a setting and is a windows container docker daemon.

  • FromUri that uses a DockerUri to create a IHostService. This uri is arbitrary. It also support other properties (see below).
publicHostBuilderFromUri(DockerUriuri,stringname=null,boolisNative=true,boolstopWhenDisposed=false,boolisWindowsHost=false,stringcertificatePath=null){/*...*/}

It will use "sensible" defaults on all parameters. Most of the case the uri is sufficient. For example if not providing the certificatePath it will try to get it from the environment DOCKER_CERT_PATH. If not found in the environment, it will default to none.

  • UseHost that takes a instantiated IHostService implementation.

Docker Compose Support

The library support docker-compose to use existing compose files to render services and manage lifetime of such.

The following sample will do have compose file that fires up a MySql and a WordPress. Therefore the single compose service will have two container services below it. By default, it will stop the services and clean up when Dispose() is invoked. This can be overridden by KeepContainers() in the fluent configuration.

version: '3.3'services:
db:
image: mysql:5.7volumes:
- db_data:/var/lib/mysqlrestart: alwaysenvironment:
MYSQL_ROOT_PASSWORD: somewordpressMYSQL_DATABASE: wordpressMYSQL_USER: wordpressMYSQL_PASSWORD: wordpresswordpress:
depends_on:
- dbimage: wordpress:latestports:
- "8000:80"restart: alwaysenvironment:
WORDPRESS_DB_HOST: db:3306WORDPRESS_DB_USER: wordpressWORDPRESS_DB_PASSWORD: wordpressvolumes:
db_data:

The above file is the docker-compose file to stitch up the complete service.

varfile=Path.Combine(Directory.GetCurrentDirectory(),(TemplateString)"Resources/ComposeTests/WordPress/docker-compose.yml");using(varsvc=newBuilder().UseContainer().UseCompose().FromFile(file).RemoveOrphans().Build().Start()){varinstallPage=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);}

The above snippet is fluently configuring the docker-compose service and invokes the install page to verify that WordPress is indeed working.

It is also possible to do all the operations that a single container supports such as copy on, export, wait operations. For example:

varfile=Path.Combine(Directory.GetCurrentDirectory(),(TemplateString)"Resources/ComposeTests/WordPress/docker-compose.yml");// @formatter:offusing(newBuilder().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. varinstallPage=await"http://localhost:8000/wp-admin/install.php".Wget();Assert.IsTrue(installPage.IndexOf("https://wordpress.org/",StringComparison.Ordinal)!=-1);}

The above snippet fires up the wordpress docker compose project and checks the URLhttp://localhost:8000/wp-admin/install.php it it returns a certain value in the body (in this case "https://wordpress.org/"). If not it returns 500 and the WaitForHttp function will wait 500 milliseconds before invoking again. This works for any custom lambda as well, just use WaitFor instead. Thus it is possible to e.g. query a database before continuing inside the using scope.

Talking to Docker Daemon

For Linux and Mac users there are several options how to authenticate towards the socket. FluentDocker supports no sudo, sudo without any password (user added as NOPASSWD in /etc/sudoer), or sudo with password. The default is that FluentDocker expects to be able to talk without any sudo. The options ar global but can be changed in runtime.

SudoMechanism.None.SetSudo();// This is the defaultSudoMechanism.Password.SetSudo("<my-sudo-password>");SudoMechanism.NoPassword.SetSudo();

If you wish to turn off sudo to communicate with the docker daemon, you can follow a docker tutorial and do the last step of adding your user to docker group.

Connecting to Remote Docker Daemons

FluentDocker supports connection to remote docker daemons. The fluent API supports e.g.

newBuilder().UseHost().UseMachine().WithName("remote-daemon")

where this requires a already pre-setup entry in the docker-machine registry. It is also possible to define SSH based docker-machine registry entires to connect to remote daemon.

using(varcontainer=newBuilder().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);}

This example will create a new docker-machine registry entry named remote-daemon that uses SSH with ip address of 192.168.1.27 and the SSH user solo. If a entry is already found named remote-daemon it will just reuse this entry. Then it gets a IHostService with correct certificates and URL for the remote daemon. Thus, it is possible then to create a docker container on the remote daemon, in thus case it is the postgres image. When it disposes the container, as usual it deletes it from the remote docker. The IHostService do make sure to pick upp all necessary certificates in order to authenticate the connection.

The above example produces this docker-machine registry entry.

C:\Users\martoffi>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

In order to use UseSsh(...) a SSH tunnel with no password must been set up. In addition the user that uses the tunnel must be allowed to access the docker daemon.

Follow these tutorial how to setup the SSH tunnel and make sure the user can access the docker daemon.

  1. https://www.kevinkuszyk.com/2016/11/28/connect-your-docker-client-to-a-remote-docker-host/
  2. https://askubuntu.com/questions/192050/how-to-run-sudo-command-with-no-password

Basically create a new rsa key to use with the SSH tunnel using ssh-keygen -t rsa and then copy it to the remote host by ssh-copy-id {username}@{host}.

Edit the /etc/sudoers as specified in the second tutorial.

When this is done, you now can access the remote docker daemon by the generic driver or the fluent API specified above. To do the same thing manually as specified in the example it would look something like this.

C:\Users\martoffi>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

Now the registry entry is created, it is possible set the environment for the terminal docker.

C:\Users\martoffi>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:\Users\martoffi\.docker\machine\machines\remote-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

Run this to make docker client use the remote docker daemon.

@FOR /f "tokens=*" %i IN ('docker-machine.exe env remote-daemon') DO @%i

All commands using the docker binary will now execute on the remote docker daemon.

Hyper-V

When creating and querying, via machine, a hyper-v docker machine the process needs to be elevated since Hyper-V will not respond to API calls in standard user mode.

Misc

Health Check

It is possible to specify a health check for the docker container to report the state of the container based on such activity. The following example is using a health check that the container has exited or not. It is possible to check the configuration (make sure to force refresh) what status the health check is reporting.

using(varcontainer=Fd.UseContainer().UseImage("postgres:latest",force:true).HealthCheck("exit").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start()){varconfig=container.GetConfiguration(true);AreEqual(HealthState.Starting,config.State.Health.Status);}

ulimit

It is possible via the Fluent API and ContainerCreateParams specify ulimit to the docker container to e.g. limit the number of open files etc. For example using the Fluent API could look like this when restricting the number of open files to 2048 (both soft and hard).

using(varcontainer=Fd.UseContainer().UseImage("postgres:latest",force:true).UseUlimit(Ulimit.NoFile,2048,2048).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start()){// Do stuff}

Test Support

This repo contains three nuget packages, one for the fluent access, one for ms-test base classes and another for xunit base classes to be used while testing. For example in a unit-test it is possible to fire up a postgres container and wait when the the db has booted.

XUnit

publicclassPostgresXUnitTests:IClassFixture<PostgresTestBase>{[Fact]publicvoidTest(){// We now have a running postgres// and a valid connection string to use.}}

MSTest

using(varcontainer=newDockerBuilder().WithImage("kiasaki/alpine-postgres").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").ExposePorts("5432").WaitForPort("5432/tcp",30000/*30s*/).Build()){container.Create().Start();}

It is also possible to re-use abstract base classes, for example postgres test base to simplify and make clean unittest towards a container.

[TestClass]publicclassPostgresMsTests:PostgresTestBase{[TestMethod]publicvoidTest(){// We now have a running postgres// and a valid connection string to use.}}

The FluentDockerTestBase allows for simple overrides to do whatever custom docker backed test easily. Just create a test class and derive from the FluentDockerTestBase and override suitable methods. For example.

protectedoverrideDockerBuilderBuild(){returnnewDockerBuilder().WithImage("kiasaki/alpine-postgres").WithEnvironment($"POSTGRES_PASSWORD={PostgresPassword}").ExposePorts("5432").WaitForPort("5432/tcp",30000/*30s*/);}

This will create a builder with docker image postgres:latest and set one environment string, it will also expose the postgres db port 5432 to the host so one can connect to the db within the container. Lastly it will wait for the port 5432. This ensures that the db is running and have properly booted. If timeout, in this example set to 30 seconds, it will throw an exception and the container is stopped and removed. Note that the host port is not 5432! Use Container.GetHostPort("5432/tcp") to get the host port. The host ip can be retrieved by Container.Host property and thus shall be used when communicating with the app in the container.

If a callback is needed when the container has been successfully pulled, created, and started.

protectedoverridevoidOnContainerInitialized(){ConnectionString=string.Format(PostgresConnectionString,Container.Host,Container.GetHostPort("5432/tcp"),PostgresUser,PostgresPassword,PostgresDb);}

This example renders a proper connection string to the postgresql db within the newly spun up container. This can be used to connect using Npgsql, EF7, NHibernate, Marten or other compatible tools. This method will not be called if pulling of the image from the docker repository or it could not create/start the container.

If a before shutdown container hook is wanted override.

protectedvirtualvoidOnContainerTearDown(){// Do stuff before container is shut down.}

Note that if un-named container, if not properly disposed, the docker container will still run and must be manually removed. This is a feature not a bug since you might want several containers running in your test. The DockerContainer class manages the instance id of the container and thus only interact with it and no other container.

When creating / starting a new container it will first check the local repository if the container image is already present and will download it if not found. This may take some time and there's just a Debug Log if enabled it is possible to monitor the download process.

Miscellanious

Unhandled Exceptions

When a unhandled exception occurs and the application FailFast i.e. terminates quickly it will not invoke finally clause. Thus a failing WaitForPort inside a using statement will not dispose the container service. Therefore the container is is still running. To fix this, either have a global try...catch or inject one locally e.g.

try{using(varcontainer=newBuilder().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;}

But it this is only when application termination is done due to the FluentDockerException thrown in the WaitForPort, otherwise it will dispose the container properly and thus the try...catch is not needed.

This could also be solved using the Fd.Build functions (see Using Builder Extensions for more information).

Using Builder Extensions

The class Fd is a static class that provides convenience methods for building and running single and composed containers. To build a container just use:

varbuild=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 ofvarbuild=newBuilder().UseContainer().UseImage("postgres:9.6-alpine").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",TimeSpan.FromSeconds(30));

This can then be used to start the containers within a safeusing clause that is guaranteed to be disposed even if uncaught exception.

build.Container(svc =>{varconfig=svc.GetConfiguration();// Do stuff...});

After the Container method has been executed the container is in this case stopped and removed. This is the quivalent of

// This is equivalent oftry{using(varsvc=build.Build()){svc.Start();varconfig=svc.GetConfiguration();// Do stuff... }}catch{Log(...);throw;}

It is also possible to combine builder and running e.g. via:

Fd.Container(c =>c.UseContainer().UseImage("postgres:9.6-alpine").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",TimeSpan.FromSeconds(30)),
svc =>{varconfig=svc.GetConfiguration();// Do stuff...});

The above example will build the container, start, stop, and finally delete the container. Even if and Exception is thrown it will be Disposed. Of course it is possible to use compsed container using composite extension methods as with container.

About

Use docker, docker-compose local and remote in tests and your .NET core/full framework apps via a FluentAPI

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

Quality Gate StatusBuild status

PackageNuGet
FluentDockerNuGet
Microsoft TestNuGet
XUnit TestNuGet

FluentDocker

This library enables docker and docker-compose interactions usinga Fluent API. It is supported on Linux, Windows and Mac. It also has support for the legazy docker-machine interactions.

Sample Fluent API usage

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",30000/*30s*/).Build().Start()){varconfig=container.GetConfiguration(true);Assert.AreEqual(ServiceRunningState.Running,config.State.ToServiceState());}

This fires up a postgres and waits for it to be ready. To use compose, just do it like this:

💡 NOTE: Use the AssumeComposeVersion(ComposeVersion.V2) to use the V2 behaviour, default is still V1 (to be changed to default to V2 later this year)

varfile=Path.Combine(Directory.GetCurrentDirectory(),(TemplateString)"Resources/ComposeTests/WordPress/docker-compose.yml");// @formatter:offusing(varsvc=newBuilder().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 varinstallPage=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 composeAssert.AreEqual(2,svc.Containers.Count);// We can access each individual containerAssert.AreEqual(2,svc.Images.Count);// And the images used.}

:bulb Note for Linux Users: Docker requires sudo by default and the library by default expects that executing user do not need to do sudo in order to talk to the docker daemon. More description can be found in the Talking to Docker Daemon chapter.

The fluent API builds up one or more services. Each service may be composite or singular. Therefore it is possible to e.g. fire up several docker-compose based services and manage each of them as a single service or dig in and use all underlying services on each docker-compose service. It is also possible to use services directly e.g.

varfile=Path.Combine(Directory.GetCurrentDirectory(),(TemplateString)"Resources/ComposeTests/WordPress/docker-compose.yml");using(varsvc=newDockerComposeCompositeService(DockerHost,newDockerComposeConfig{ComposeFilePath=newList<string>{file},ForceRecreate=true,RemoveOrphans=true,StopOnDispose=true})){svc.Start();// We now have a running WordPress with a MySql databasevarinstallPage=await$"http://localhost:8000/wp-admin/install.php".Wget();Assert.IsTrue(installPage.IndexOf("https://wordpress.org/",StringComparison.Ordinal)!=-1);}

The above example creates a docker-compose service from a single compose file. When the service is disposed all underlying services is automatically stopped.

The library is supported by .NET full 4.51 framework and higher, .NET standard 1.6, 2.0. It is divided into three thin layers, each layer is accessible:

  1. Docker Binaries interactions - Static commands and docker environment
  2. Services - thin service layer to manage machines, containers etc.
  3. Fluent API - API to build/discover services to be used

The Majority of the service methods are extension methods and not hardwired into the service itself, making them lightweight and customizable. Since everything is accessible it is e.g. easy to add extensions method for a service that uses the layer 1 commands to provide functionality.

Contribution

I do welcome contribution, though there is no contribution guideline as of yet, make sure to adhere to .editorconfig when doing the Pull Requests. Otherwise the build will fail. I'll update with a real guideline sooner or later this year.

Basic Usage of Commands (Layer 1)

All commands needs a DockerUri to work with. It is the Uri to the docker daemon, either locally or remote. It can be discoverable or hardcoded. Discovery of local DockerUri can be done by

varhosts=newHosts().Discover();var_docker=hosts.FirstOrDefault(x =>x.IsNative)??hosts.FirstOrDefault(x =>x.Name=="default");

The example snipped will check for native, or docker beta "native" hosts, if not choose the docker-machine "default" as host. If you're using docker-machine and no machine exists or is not started it is easy to create / start a docker-machine by e.g. "test-machine".Create(1024,20000000,1). This will create a docker machine named "test-machine" with 1GB of RAM, 20GB Disk, and use one CPU.

It is now possible to use the Uri to communicate using the commands. For example to get the version of client and server docker binaries:

varresult=_docker.Host.Version(_docker.Certificates);Debug.WriteLine(result.Data);// Will Print the Client and Server Version and API Versions respectively.

All commands return a CommandResponse such that it is possible to check success factor by response.Success. If any data associated with the command it is returned in the response.Data property.

Then it is simple as below to start and stop include delete a container using the commands. Below starts a container and do a PS on it and then deletes it.

varid=_docker.Host.Run("nginx:latest",null,_docker.Certificates).Data;varps=_docker.Host.Ps(null,_docker.Certificates).Data;_docker.Host.RemoveContainer(id,true,true,null,_docker.Certificates);

When running on windows, one can choose to run linux or windows container. Use the LinuxDaemon or WindowsDaemon to control which daemon to talk to.

_docker.LinuxDaemon();// ensures that it will talk to linux daemon, if windows daemon it will switch

Some commands returns a stream of data when e.g. events or logs is wanted using a continuous stream. Streams can be used in background tasks and support CancellationToken. Below example tails a log.

using(varlogs=_docker.Host.Logs(id,_docker.Certificates)){while(!logs.IsFinished){varline=logs.TryRead(5000);// Do a read with timeoutif(null==line){break;}Debug.WriteLine(line);}}

Utility methods exists for commands. They come in different flaviours such as networking etc. For example when reading a log to the end:

using(varlogs=_docker.Host.Logs(id,_docker.Certificates)){foreach(varlineinlogs.ReadToEnd()){Debug.WriteLine(line);}}

Using Fluent API

The highest layer of this library is the fluent API where you can define and control machines, images, and containers. For example to setup a load balancer with two nodejs servers reading from a redis server can look like this (node image is custom built if not found in the repository).

varfullPath=(TemplateString)@"${TEMP}/fluentdockertest/${RND}";varnginx=Path.Combine(fullPath,"nginx.conf");Directory.CreateDirectory(fullPath);typeof(NsResolver).ResourceExtract(fullPath,"index.js");using(varservices=newBuilder()// Define custom node image to be used.DefineImage("mariotoffia/nodetest").ReuseIfAlreadyExists().From("ubuntu").Maintainer("Mario Toffia <mario.toffia@xyz.com>").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);varep=services.Containers.First(x =>x.Name=="nginx").ToHostExposedEndpoint("80/tcp");Assert.IsNotNull(ep);varround1=$"http://{ep.Address}:{ep.Port}".Wget();Assert.AreEqual("This page has been viewed 1 times!",round1);varround2=$"http://{ep.Address}:{ep.Port}".Wget();Assert.AreEqual("This page has been viewed 2 times!",round2);}

The above example defines a Dockerfile, builds it, for the node image. It then uses vanilla redis and nginx. If you just want to use an existing Dockerfile it can be done like this.

using(varservices=newBuilder().DefineImage("mariotoffia/nodetest").ReuseIfAlreadyExists().FromFile("/tmp/Dockerfile").Build().Start()){// Container either build to reused if found in registry and started here.}

The fluent API supports from defining a docker-machine to a set of docker instances. It has built-in support for e.g. waiting for a specific port or a process within the container before Build() completes and thus can be safely be used within a using statement. If specific management on wait timeouts etc. you can always build and start the container and use extension methods to do the waiting on the container itself.

To create a container just omit the start. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build()){Assert.AreEqual(ServiceRunningState.Stopped,container.State);}

This example creates a container with postgres, configure one environment variable. Within the using statement it is possible to start the IContainerService. Thus each built container is wrapped in a IContainerService. It is also possible to use the IHostService.GetContainers(...) to obtain the created, running, and exited containers. From the IHostService it is also possible to get all the images in the local repository to create containers from.

Whe you want to run a single container do use the fluent or container service start method. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start()){varconfig=container.GetConfiguration();Assert.AreEqual(ServiceRunningState.Running,container.State);Assert.IsTrue(config.Config.Env.Any(x =>x=="POSTGRES_PASSWORD=mysecretpassword"));}

By default the container is stopped and deleted when the Dispose method is run, in order to keep the container in archve, use the KeepContainer() on the fluent API. When Dispose() is invoked it will be stopped but not deleted. It is also possible to keep it running after dispose as well.

Working with ports

It is possible to expose ports both explicit or randomly. Either way it is possible to resolve the IP (in case of machine) and the port (in case of random port) to use in code. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(40001,5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start()){varendpoint=container.ToHostExposedEndpoint("5432/tcp");Assert.AreEqual(40001,endpoint.Port);}

Here we map the container port 5432 to host port 40001 explicitly. Note the use of container.ToHostExposedEndpoint(...). This is to always resolve to a working ip and port to communicate with the docker container. It is also possible to map a random port, i.e. let Docker choose a available port. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start()){varendpoint=container.ToHostExposedEndpoint("5432/tcp");Assert.AreNotEqual(0,endpoint.Port);}

The only difference here is that only one argument is used when ExposePort(...) was used to configure the container. The same usage applies otherwise and thus is transparent for the code.

In order to know when a certain service is up and running before starting to e.g. connect to it. It is possible to wait for a specific port to be open. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",30000/*30s*/).Build().Start()){varconfig=container.GetConfiguration(true);Assert.AreEqual(ServiceRunningState.Running,config.State.ToServiceState());}

In the above example we wait for the container port 5432 to be opened within 30 seconds. If it fails, it will throw an exception and thus the container will be disposed and removed (since we dont have any keep container etc. configuration).

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",30000/*30s*/,"127.0.0.1").Build().Start()){varconfig=container.GetConfiguration(true);Assert.AreEqual(ServiceRunningState.Running,config.State.ToServiceState());}

Sometimes it is not possible to directly reach the container, by local ip and port, instead e.g. the container has an exposed port on the loopback interface (127.0.0.1) and that is the only way of reaching the container from the program. The above example forces the address to be 127.0.0.1 but still resolves the host port. By default, FluentDocker uses the network inspect on the container to determine the network configuration.

Sometime it is not sufficient to just wait for a port. Sometimes a container process is much more vital to wait for. Therefore a wait for process method exist in the fluent API as well as an extension method on the container object. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForProcess("postgres",30000/*30s*/).Build().Start()){varconfig=container.GetConfiguration(true);Assert.AreEqual(ServiceRunningState.Running,config.State.ToServiceState());}

In the above example Build() will return control when the process "postgres" have been started within the container.

Filesystem & Files

In order to make use of containers, sometimes it is necessary to mount volumes in the container onto the host or just copy from or to the container. Depending on if you're running machine or docker natively volume mapping have the constraint that it must be reachable from the virtual machine.

A normal use case is to have e.g. a webserver serving content on a docker container and the user edits files on the host file system. In such scenario it is necessary to mount a docker container volume onto the host. For example:

conststringhtml="<html><head>Hello World</head><body><h1>Hello world</h1></body></html>";varhostPath=(TemplateString)@"${TEMP}/fluentdockertest/${RND}";Directory.CreateDirectory(hostPath);using(varcontainer=newBuilder().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);varresponse=$"http://{container.ToHostExposedEndpoint("80/tcp")}/hello.html".Wget();Assert.AreEqual(html,response);}

In the above example a nginx container is started and mounts '/usr/share/nginx/html' onto a (random, in temp directory) host path. A HTML file is copied into the host path and when a HTTP get towards the nginx docker container is done, that same file is served.

Sometimes it is necessary copy files to and from a container. For example copy a configuration file, configure it and copy it back. More common scenario is to copy a configuration file to the container, just before it is started. The multi container example copies a nginx configuration file just before it is started. Thus it is possible to avoid manually creating a Dockerfile and a image for such a simple task. Instead just use e.g. an official or custom image, copy configuration and run.

using(newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start().CopyFrom("/etc/conf.d",fullPath)){varfiles=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")));}

Above example copies a directory to a host path (fullPath) from a running container. Note the use of extension method here, thus not using the fluent API (since CopyFrom is after Start()). If you want to copy files from the container just before starting use the Fluent API instead.

using(newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").CopyOnStart("/etc/conf.d",fullPath).Build().Start()){}

The below example illustrates a much more common scenario where files are copied to the container. This example makes use of the extension method instead of the fluent API version. It takes a Diff snapshot before copy and then just after the copy. In the latter the hello.html is present.

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start().WaitForProcess("postgres",30000/*30s*/).Diff(outbefore).CopyTo("/bin",fullPath)){varafter=container.Diff();Assert.IsFalse(before.Any(x =>x.Item=="/bin/hello.html"));Assert.IsTrue(after.Any(x =>x.Item=="/bin/hello.html"));}

Sometime is it useful to copy files in the IContainerService.Dispose() (just before container has stopped). Therefore a fluent API exists to ensure that it will just do that.

using(newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").CopyOnDispose("/etc/conf.d",fullPath).Build().Start()){}

In order to analyze a container, export extension method and fluent API methods exists. Most notably is the possibility to export a container when a IContainerService is disposed.

using(newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").ExportOnDispose(fullPath).Build().Start()){}

This will produce a container export (tar file) on the host (fullPath). If you rather have it exploded (un-tared) use the ExportExplodedOnDispose method instead. Of course you can export the container any time using a extension method on the container.

A useful trick when it comes to unit-testing is to export the container state when the unit test fails for some reason, therefore it exists a Fluent API that will export when a certain Lambda condition is met. For example:

varfailure=false;using(newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").ExportOnDispose(fullPath, svc =>failure).Build().Start()){failure=true;}

This snippet will export the container when the using statement is disposing the container since the failure variable is set to true and is used in the ExportOnDispose expression.

IService Hooks

All services can be extended with hooks. In the ExportOnDispose(path, lambda) installs a Hook when the service state is set to execute the lambda when the state is Removing. It is possible to install and remove hooks on the fly. If multiple hooks are registered on same service instance, with same ServiceRunningState, they will be executed in installation order.

The hooks are particulary good if you want something to be executed when a state is about to be set (or executed) on the service such as Starting. The Fluent API makes use of those in some situations such as Copy files, export, etc.

Docker Networking

FluentDocker do support all docker network commands. It can discover networks by _docker.NetworkLs() where it discovers all networks and some simple parameters defined in NetworkRow. It can also inspect to gain deeper information about the network (such as which containers is in the network and Ipam configuration) by _docker.NetworkInspect(network:"networkId").

In order to create a new network, use the _docker.NetworkCreate("name_of_network"). It is also possible to supply NetworkCreateParams where everything can be customized such as creating a overlay network och change the Ipam configuration. To delete a network, just use the _docker.NetworkRm(network:"networkId").

Note that networks are not deleted if there are any containers attached to it!

When a network is created it is possible to put one or more containers into it using the _docker.NetworkConnect("containerId","networkId"). Note that containers may be in several networks at a time, thus can proxy request between isolated networks. To disconnect a container from a network, simple do a _docker.NetworkDisconnect("containerId","networkId").

The following sample runs a container, creates a new network, and connects the running container into the network. It then disconnect the container, delete it, and delete the network.

varcmd=_docker.Run("postgres:9.6-alpine",newContainerCreateParams{PortMappings=new[]{"40001:5432"},Environment=new[]{"POSTGRES_PASSWORD=mysecretpassword"}},_certificates);varcontainer=cmd.Data;varnetwork=string.Empty;varcreated=_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 Networking

It is also possible to use a fluent builder to build new or reuse existing docker networks. Those can then be referenced while building containers. It is possible to build more than one docker network and attach a container to more than one network at a time.

using(varnw=newBuilder().UseNetwork("test-network")){using(varcontainer=newDockerBuilder().WithImage("kiasaki/alpine-postgres").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").ExposePorts("5432").UseNetwork(nw).WaitForPort("5432/tcp",30000/*30s*/).Build()){container.Create().Start();}}

The above code snippet creates a new network called test-network and then creates a container that is attached to the test-network. When the Dispose() is called on nw it will remove the network. It is also possible to do static IP container assignments within the network by UseIpV4 or UseIpV6. For example:

using(varnw=Fd.UseNetwork("unit-test-nw").UseSubnet("10.18.0.0/16").Build()){using(varcontainer=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()){varip=container.GetConfiguration().NetworkSettings.Networks["unit-test-nw"].IPAddress;Assert.AreEqual("10.18.0.22",ip);}}

The above example creates a new network unit-test-nw with ip-range 10.18.0.0/16. It is the used in the new container. The IP for the container is set to 10.18.0.22 and is static due to UseIpV4 command.

Volume Support

FluentDocker supports docker volume management both from commands and from a fluent API. Therefore it is possible to have total control on volumes used in container such if it shall be disposed, reused, what driver to use etc.

varvolume=_docker.VolumeCreate("test-volume","local",opts:{{"type","nfs"},{"o=addr","192.168.1.1,rw"},{"device",":/path/to/dir"}});varcfg=_docker.VolumeInspect(_certificates,"test-volume");_docker.VolumeRm(force:true,id:"test-volume");

The above snippet creates a new volume with name test-volume and is of NFS type. It then inspects the just created volume and lastly force delete the volume.

Fluent Volume API

It is also possible to use a fluent API to create or use volumes. They can then be used when building a container. This is especially useful when creation of volumes are special or lifetime needs to be controlled.

using(varvol=newBuilder().UseVolume("test-volume").RemoveOnDispose().Build()){using(varcontainer=newBuilder().UseContainer().UseImage("postgres:9.6-alpine").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").MountVolume(vol,"/var/lib/postgresql/data",MountType.ReadWrite).Build().Start()){varconfig=container.GetConfiguration();Assert.AreEqual(1,config.Mounts.Length);Assert.AreEqual("test-volume",config.Mounts[0].Name);}}

The above sample creates a new volume called test-volume and it is scheduled to be delete when Dispose() is invoked on the IVolumeService. The container is created and mounts the newly created volume to /var/lib/postgresql/data as read/write access mode. Since the container is within the scope of the using statement of the volume it's lifetime spans the whole container lifetime and then gets deleted.

Events

FluentDocker supports, connecting to the docker event mechanism to listen to the events it sends.

using(varevents=Fd.Native().Events()){using(varcontainer=newBuilder().UseContainer().UseImage("postgres:9.6-alpine").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",30000/*30s*/).Build().Start()){FdEvente;while((e=events.TryRead(3000))!=null){if(e.Type==EventType.Container&&e.Action==EventAction.Start)break;}}}

Event listener is global and may handle many EventAction types.

For example

  • Pull
  • Create
  • Start
  • Kill
  • Die
  • Connect
  • Disconnect
  • Stop
  • Destroy

Depending on action, the event type may differ such as ContainerKillEvent for EventAction.Kill. All events derive from FdEvent. That means all shared properties is in the base event and the explicit ones are in the derived.

For example, the ´ContainerKillEvent` contains the following properties:

publicsealedclassContainerKillActor:EventActor{/// <summary>/// The image name and label such as "alpine:latest"./// </summary>publicstringImage{get;set;}/// <summary>/// Name of the container./// </summary>publicstringName{get;set;}/// <summary>/// The signal that the container has been signalled./// </summary>publicstringSignal{get;set;}}

This event loop may be used to pick up events and drive your instantiated IService instances. Or if you need to react to e.g. a network is added or deleted.

Logging

In the full framework it uses verbose logging using the System.Diagnostics.Debugger.Log. For .net core it uses the standard Microsoft.Extensions.Logging.ILog to log. Both are using the category Ductus.FluentDocker and therefore may be configured to participate in logging or not and configure different logging destinations.

In .net core you can provide the logging segment in the application config file.

{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Ductus.FluentDocker": "None"
}
}
}

Please check the https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging/?view=aspnetcore-2.1 for more information. For full framework please check out the XML needed in the appconfig for the full framework described in https://docs.microsoft.com/en-us/dotnet/framework/wcf/diagnostics/tracing/configuring-tracing.

There's a quick way of disabling / enabling logging via (Ductus.FluentDocker.Services) Logging.Enabled() or Logging.Disabled(). This will forcefully enable / disable logging.

Custom IPEndpoint Resolvers

It is possible to override the default mechanism of FluentDocker resolves the container IP from the clients perspective in e.g. WaitForPort. This can be overridden on ContainerBuilder basis.

The below sample, overrides the default behaviour. When it returns null the default resolver kicks in.

using(varcontainer=Fd.UseContainer().UseImage("postgres:9.6-alpine").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").ExposePort(5432).UseCustomResolver((ports,portAndProto,dockerUri)=>{if(null==ports||string.IsNullOrEmpty(portAndProto))returnnull;if(!ports.TryGetValue(portAndProto,outvarendpoints))returnnull;if(null==endpoints||endpoints.Length==0)returnnull;if(CommandExtensions.IsNative())returnendpoints[0];if(CommandExtensions.IsEmulatedNative())returnCommandExtensions.IsDockerDnsAvailable()?newIPEndPoint(CommandExtensions.EmulatedNativeAddress(),endpoints[0].Port):newIPEndPoint(IPAddress.Loopback,endpoints[0].Port);if(Equals(endpoints[0].Address,IPAddress.Any)&&null!=dockerUri)returnnewIPEndPoint(IPAddress.Parse(dockerUri.Host),endpoints[0].Port);returnendpoints[0];}).WaitForPort("5432/tcp",30000/*30s*/).Build().Start()){varstate=container.GetConfiguration(true/*force*/).State.ToServiceState();Assert.AreEqual(ServiceRunningState.Running,state);}

Talking to custom docker daemon URI without docker-machine

There's limited support to use the FluentAPI to talk to a remote docker daemon without using docker-machine. This is done either by manually creating a instance of a DockerHostService or use FromUri on HostBuilder.

using(varcontainer=Fd.UseHost().FromUri(Settings.DockerUri,isWindowsHost:true).UseContainer().Build()){}

The above sample connects to a custom DockerUri from a setting and is a windows container docker daemon.

  • FromUri that uses a DockerUri to create a IHostService. This uri is arbitrary. It also support other properties (see below).
publicHostBuilderFromUri(DockerUriuri,stringname=null,boolisNative=true,boolstopWhenDisposed=false,boolisWindowsHost=false,stringcertificatePath=null){/*...*/}

It will use "sensible" defaults on all parameters. Most of the case the uri is sufficient. For example if not providing the certificatePath it will try to get it from the environment DOCKER_CERT_PATH. If not found in the environment, it will default to none.

  • UseHost that takes a instantiated IHostService implementation.

Docker Compose Support

The library support docker-compose to use existing compose files to render services and manage lifetime of such.

The following sample will do have compose file that fires up a MySql and a WordPress. Therefore the single compose service will have two container services below it. By default, it will stop the services and clean up when Dispose() is invoked. This can be overridden by KeepContainers() in the fluent configuration.

version: '3.3'services:
db:
image: mysql:5.7volumes:
- db_data:/var/lib/mysqlrestart: alwaysenvironment:
MYSQL_ROOT_PASSWORD: somewordpressMYSQL_DATABASE: wordpressMYSQL_USER: wordpressMYSQL_PASSWORD: wordpresswordpress:
depends_on:
- dbimage: wordpress:latestports:
- "8000:80"restart: alwaysenvironment:
WORDPRESS_DB_HOST: db:3306WORDPRESS_DB_USER: wordpressWORDPRESS_DB_PASSWORD: wordpressvolumes:
db_data:

The above file is the docker-compose file to stitch up the complete service.

varfile=Path.Combine(Directory.GetCurrentDirectory(),(TemplateString)"Resources/ComposeTests/WordPress/docker-compose.yml");using(varsvc=newBuilder().UseContainer().UseCompose().FromFile(file).RemoveOrphans().Build().Start()){varinstallPage=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);}

The above snippet is fluently configuring the docker-compose service and invokes the install page to verify that WordPress is indeed working.

It is also possible to do all the operations that a single container supports such as copy on, export, wait operations. For example:

varfile=Path.Combine(Directory.GetCurrentDirectory(),(TemplateString)"Resources/ComposeTests/WordPress/docker-compose.yml");// @formatter:offusing(newBuilder().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. varinstallPage=await"http://localhost:8000/wp-admin/install.php".Wget();Assert.IsTrue(installPage.IndexOf("https://wordpress.org/",StringComparison.Ordinal)!=-1);}

The above snippet fires up the wordpress docker compose project and checks the URLhttp://localhost:8000/wp-admin/install.php it it returns a certain value in the body (in this case "https://wordpress.org/"). If not it returns 500 and the WaitForHttp function will wait 500 milliseconds before invoking again. This works for any custom lambda as well, just use WaitFor instead. Thus it is possible to e.g. query a database before continuing inside the using scope.

Talking to Docker Daemon

For Linux and Mac users there are several options how to authenticate towards the socket. FluentDocker supports no sudo, sudo without any password (user added as NOPASSWD in /etc/sudoer), or sudo with password. The default is that FluentDocker expects to be able to talk without any sudo. The options ar global but can be changed in runtime.

SudoMechanism.None.SetSudo();// This is the defaultSudoMechanism.Password.SetSudo("<my-sudo-password>");SudoMechanism.NoPassword.SetSudo();

If you wish to turn off sudo to communicate with the docker daemon, you can follow a docker tutorial and do the last step of adding your user to docker group.

Connecting to Remote Docker Daemons

FluentDocker supports connection to remote docker daemons. The fluent API supports e.g.

newBuilder().UseHost().UseMachine().WithName("remote-daemon")

where this requires a already pre-setup entry in the docker-machine registry. It is also possible to define SSH based docker-machine registry entires to connect to remote daemon.

using(varcontainer=newBuilder().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);}

This example will create a new docker-machine registry entry named remote-daemon that uses SSH with ip address of 192.168.1.27 and the SSH user solo. If a entry is already found named remote-daemon it will just reuse this entry. Then it gets a IHostService with correct certificates and URL for the remote daemon. Thus, it is possible then to create a docker container on the remote daemon, in thus case it is the postgres image. When it disposes the container, as usual it deletes it from the remote docker. The IHostService do make sure to pick upp all necessary certificates in order to authenticate the connection.

The above example produces this docker-machine registry entry.

C:\Users\martoffi>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

In order to use UseSsh(...) a SSH tunnel with no password must been set up. In addition the user that uses the tunnel must be allowed to access the docker daemon.

Follow these tutorial how to setup the SSH tunnel and make sure the user can access the docker daemon.

  1. https://www.kevinkuszyk.com/2016/11/28/connect-your-docker-client-to-a-remote-docker-host/
  2. https://askubuntu.com/questions/192050/how-to-run-sudo-command-with-no-password

Basically create a new rsa key to use with the SSH tunnel using ssh-keygen -t rsa and then copy it to the remote host by ssh-copy-id {username}@{host}.

Edit the /etc/sudoers as specified in the second tutorial.

When this is done, you now can access the remote docker daemon by the generic driver or the fluent API specified above. To do the same thing manually as specified in the example it would look something like this.

C:\Users\martoffi>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

Now the registry entry is created, it is possible set the environment for the terminal docker.

C:\Users\martoffi>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:\Users\martoffi\.docker\machine\machines\remote-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

Run this to make docker client use the remote docker daemon.

@FOR /f "tokens=*" %i IN ('docker-machine.exe env remote-daemon') DO @%i

All commands using the docker binary will now execute on the remote docker daemon.

Hyper-V

When creating and querying, via machine, a hyper-v docker machine the process needs to be elevated since Hyper-V will not respond to API calls in standard user mode.

Misc

Health Check

It is possible to specify a health check for the docker container to report the state of the container based on such activity. The following example is using a health check that the container has exited or not. It is possible to check the configuration (make sure to force refresh) what status the health check is reporting.

using(varcontainer=Fd.UseContainer().UseImage("postgres:latest",force:true).HealthCheck("exit").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start()){varconfig=container.GetConfiguration(true);AreEqual(HealthState.Starting,config.State.Health.Status);}

ulimit

It is possible via the Fluent API and ContainerCreateParams specify ulimit to the docker container to e.g. limit the number of open files etc. For example using the Fluent API could look like this when restricting the number of open files to 2048 (both soft and hard).

using(varcontainer=Fd.UseContainer().UseImage("postgres:latest",force:true).UseUlimit(Ulimit.NoFile,2048,2048).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start()){// Do stuff}

Test Support

This repo contains three nuget packages, one for the fluent access, one for ms-test base classes and another for xunit base classes to be used while testing. For example in a unit-test it is possible to fire up a postgres container and wait when the the db has booted.

XUnit

publicclassPostgresXUnitTests:IClassFixture<PostgresTestBase>{[Fact]publicvoidTest(){// We now have a running postgres// and a valid connection string to use.}}

MSTest

using(varcontainer=newDockerBuilder().WithImage("kiasaki/alpine-postgres").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").ExposePorts("5432").WaitForPort("5432/tcp",30000/*30s*/).Build()){container.Create().Start();}

It is also possible to re-use abstract base classes, for example postgres test base to simplify and make clean unittest towards a container.

[TestClass]publicclassPostgresMsTests:PostgresTestBase{[TestMethod]publicvoidTest(){// We now have a running postgres// and a valid connection string to use.}}

The FluentDockerTestBase allows for simple overrides to do whatever custom docker backed test easily. Just create a test class and derive from the FluentDockerTestBase and override suitable methods. For example.

protectedoverrideDockerBuilderBuild(){returnnewDockerBuilder().WithImage("kiasaki/alpine-postgres").WithEnvironment($"POSTGRES_PASSWORD={PostgresPassword}").ExposePorts("5432").WaitForPort("5432/tcp",30000/*30s*/);}

This will create a builder with docker image postgres:latest and set one environment string, it will also expose the postgres db port 5432 to the host so one can connect to the db within the container. Lastly it will wait for the port 5432. This ensures that the db is running and have properly booted. If timeout, in this example set to 30 seconds, it will throw an exception and the container is stopped and removed. Note that the host port is not 5432! Use Container.GetHostPort("5432/tcp") to get the host port. The host ip can be retrieved by Container.Host property and thus shall be used when communicating with the app in the container.

If a callback is needed when the container has been successfully pulled, created, and started.

protectedoverridevoidOnContainerInitialized(){ConnectionString=string.Format(PostgresConnectionString,Container.Host,Container.GetHostPort("5432/tcp"),PostgresUser,PostgresPassword,PostgresDb);}

This example renders a proper connection string to the postgresql db within the newly spun up container. This can be used to connect using Npgsql, EF7, NHibernate, Marten or other compatible tools. This method will not be called if pulling of the image from the docker repository or it could not create/start the container.

If a before shutdown container hook is wanted override.

protectedvirtualvoidOnContainerTearDown(){// Do stuff before container is shut down.}

Note that if un-named container, if not properly disposed, the docker container will still run and must be manually removed. This is a feature not a bug since you might want several containers running in your test. The DockerContainer class manages the instance id of the container and thus only interact with it and no other container.

When creating / starting a new container it will first check the local repository if the container image is already present and will download it if not found. This may take some time and there's just a Debug Log if enabled it is possible to monitor the download process.

Miscellanious

Unhandled Exceptions

When a unhandled exception occurs and the application FailFast i.e. terminates quickly it will not invoke finally clause. Thus a failing WaitForPort inside a using statement will not dispose the container service. Therefore the container is is still running. To fix this, either have a global try...catch or inject one locally e.g.

try{using(varcontainer=newBuilder().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;}

But it this is only when application termination is done due to the FluentDockerException thrown in the WaitForPort, otherwise it will dispose the container properly and thus the try...catch is not needed.

This could also be solved using the Fd.Build functions (see Using Builder Extensions for more information).

Using Builder Extensions

The class Fd is a static class that provides convenience methods for building and running single and composed containers. To build a container just use:

varbuild=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 ofvarbuild=newBuilder().UseContainer().UseImage("postgres:9.6-alpine").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",TimeSpan.FromSeconds(30));

This can then be used to start the containers within a safeusing clause that is guaranteed to be disposed even if uncaught exception.

build.Container(svc =>{varconfig=svc.GetConfiguration();// Do stuff...});

After the Container method has been executed the container is in this case stopped and removed. This is the quivalent of

// This is equivalent oftry{using(varsvc=build.Build()){svc.Start();varconfig=svc.GetConfiguration();// Do stuff... }}catch{Log(...);throw;}

It is also possible to combine builder and running e.g. via:

Fd.Container(c =>c.UseContainer().UseImage("postgres:9.6-alpine").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",TimeSpan.FromSeconds(30)),
svc =>{varconfig=svc.GetConfiguration();// Do stuff...});

The above example will build the container, start, stop, and finally delete the container. Even if and Exception is thrown it will be Disposed. Of course it is possible to use compsed container using composite extension methods as with container.

About

Use docker, docker-compose local and remote in tests and your .NET core/full framework apps via a FluentAPI

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Quality Gate StatusBuild status

PackageNuGet
FluentDockerNuGet
Microsoft TestNuGet
XUnit TestNuGet

FluentDocker

This library enables docker and docker-compose interactions usinga Fluent API. It is supported on Linux, Windows and Mac. It also has support for the legazy docker-machine interactions.

Sample Fluent API usage

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",30000/*30s*/).Build().Start()){varconfig=container.GetConfiguration(true);Assert.AreEqual(ServiceRunningState.Running,config.State.ToServiceState());}

This fires up a postgres and waits for it to be ready. To use compose, just do it like this:

💡 NOTE: Use the AssumeComposeVersion(ComposeVersion.V2) to use the V2 behaviour, default is still V1 (to be changed to default to V2 later this year)

varfile=Path.Combine(Directory.GetCurrentDirectory(),(TemplateString)"Resources/ComposeTests/WordPress/docker-compose.yml");// @formatter:offusing(varsvc=newBuilder().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 varinstallPage=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 composeAssert.AreEqual(2,svc.Containers.Count);// We can access each individual containerAssert.AreEqual(2,svc.Images.Count);// And the images used.}

:bulb Note for Linux Users: Docker requires sudo by default and the library by default expects that executing user do not need to do sudo in order to talk to the docker daemon. More description can be found in the Talking to Docker Daemon chapter.

The fluent API builds up one or more services. Each service may be composite or singular. Therefore it is possible to e.g. fire up several docker-compose based services and manage each of them as a single service or dig in and use all underlying services on each docker-compose service. It is also possible to use services directly e.g.

varfile=Path.Combine(Directory.GetCurrentDirectory(),(TemplateString)"Resources/ComposeTests/WordPress/docker-compose.yml");using(varsvc=newDockerComposeCompositeService(DockerHost,newDockerComposeConfig{ComposeFilePath=newList<string>{file},ForceRecreate=true,RemoveOrphans=true,StopOnDispose=true})){svc.Start();// We now have a running WordPress with a MySql databasevarinstallPage=await$"http://localhost:8000/wp-admin/install.php".Wget();Assert.IsTrue(installPage.IndexOf("https://wordpress.org/",StringComparison.Ordinal)!=-1);}

The above example creates a docker-compose service from a single compose file. When the service is disposed all underlying services is automatically stopped.

The library is supported by .NET full 4.51 framework and higher, .NET standard 1.6, 2.0. It is divided into three thin layers, each layer is accessible:

  1. Docker Binaries interactions - Static commands and docker environment
  2. Services - thin service layer to manage machines, containers etc.
  3. Fluent API - API to build/discover services to be used

The Majority of the service methods are extension methods and not hardwired into the service itself, making them lightweight and customizable. Since everything is accessible it is e.g. easy to add extensions method for a service that uses the layer 1 commands to provide functionality.

Contribution

I do welcome contribution, though there is no contribution guideline as of yet, make sure to adhere to .editorconfig when doing the Pull Requests. Otherwise the build will fail. I'll update with a real guideline sooner or later this year.

Basic Usage of Commands (Layer 1)

All commands needs a DockerUri to work with. It is the Uri to the docker daemon, either locally or remote. It can be discoverable or hardcoded. Discovery of local DockerUri can be done by

varhosts=newHosts().Discover();var_docker=hosts.FirstOrDefault(x =>x.IsNative)??hosts.FirstOrDefault(x =>x.Name=="default");

The example snipped will check for native, or docker beta "native" hosts, if not choose the docker-machine "default" as host. If you're using docker-machine and no machine exists or is not started it is easy to create / start a docker-machine by e.g. "test-machine".Create(1024,20000000,1). This will create a docker machine named "test-machine" with 1GB of RAM, 20GB Disk, and use one CPU.

It is now possible to use the Uri to communicate using the commands. For example to get the version of client and server docker binaries:

varresult=_docker.Host.Version(_docker.Certificates);Debug.WriteLine(result.Data);// Will Print the Client and Server Version and API Versions respectively.

All commands return a CommandResponse such that it is possible to check success factor by response.Success. If any data associated with the command it is returned in the response.Data property.

Then it is simple as below to start and stop include delete a container using the commands. Below starts a container and do a PS on it and then deletes it.

varid=_docker.Host.Run("nginx:latest",null,_docker.Certificates).Data;varps=_docker.Host.Ps(null,_docker.Certificates).Data;_docker.Host.RemoveContainer(id,true,true,null,_docker.Certificates);

When running on windows, one can choose to run linux or windows container. Use the LinuxDaemon or WindowsDaemon to control which daemon to talk to.

_docker.LinuxDaemon();// ensures that it will talk to linux daemon, if windows daemon it will switch

Some commands returns a stream of data when e.g. events or logs is wanted using a continuous stream. Streams can be used in background tasks and support CancellationToken. Below example tails a log.

using(varlogs=_docker.Host.Logs(id,_docker.Certificates)){while(!logs.IsFinished){varline=logs.TryRead(5000);// Do a read with timeoutif(null==line){break;}Debug.WriteLine(line);}}

Utility methods exists for commands. They come in different flaviours such as networking etc. For example when reading a log to the end:

using(varlogs=_docker.Host.Logs(id,_docker.Certificates)){foreach(varlineinlogs.ReadToEnd()){Debug.WriteLine(line);}}

Using Fluent API

The highest layer of this library is the fluent API where you can define and control machines, images, and containers. For example to setup a load balancer with two nodejs servers reading from a redis server can look like this (node image is custom built if not found in the repository).

varfullPath=(TemplateString)@"${TEMP}/fluentdockertest/${RND}";varnginx=Path.Combine(fullPath,"nginx.conf");Directory.CreateDirectory(fullPath);typeof(NsResolver).ResourceExtract(fullPath,"index.js");using(varservices=newBuilder()// Define custom node image to be used.DefineImage("mariotoffia/nodetest").ReuseIfAlreadyExists().From("ubuntu").Maintainer("Mario Toffia <mario.toffia@xyz.com>").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);varep=services.Containers.First(x =>x.Name=="nginx").ToHostExposedEndpoint("80/tcp");Assert.IsNotNull(ep);varround1=$"http://{ep.Address}:{ep.Port}".Wget();Assert.AreEqual("This page has been viewed 1 times!",round1);varround2=$"http://{ep.Address}:{ep.Port}".Wget();Assert.AreEqual("This page has been viewed 2 times!",round2);}

The above example defines a Dockerfile, builds it, for the node image. It then uses vanilla redis and nginx. If you just want to use an existing Dockerfile it can be done like this.

using(varservices=newBuilder().DefineImage("mariotoffia/nodetest").ReuseIfAlreadyExists().FromFile("/tmp/Dockerfile").Build().Start()){// Container either build to reused if found in registry and started here.}

The fluent API supports from defining a docker-machine to a set of docker instances. It has built-in support for e.g. waiting for a specific port or a process within the container before Build() completes and thus can be safely be used within a using statement. If specific management on wait timeouts etc. you can always build and start the container and use extension methods to do the waiting on the container itself.

To create a container just omit the start. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build()){Assert.AreEqual(ServiceRunningState.Stopped,container.State);}

This example creates a container with postgres, configure one environment variable. Within the using statement it is possible to start the IContainerService. Thus each built container is wrapped in a IContainerService. It is also possible to use the IHostService.GetContainers(...) to obtain the created, running, and exited containers. From the IHostService it is also possible to get all the images in the local repository to create containers from.

Whe you want to run a single container do use the fluent or container service start method. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start()){varconfig=container.GetConfiguration();Assert.AreEqual(ServiceRunningState.Running,container.State);Assert.IsTrue(config.Config.Env.Any(x =>x=="POSTGRES_PASSWORD=mysecretpassword"));}

By default the container is stopped and deleted when the Dispose method is run, in order to keep the container in archve, use the KeepContainer() on the fluent API. When Dispose() is invoked it will be stopped but not deleted. It is also possible to keep it running after dispose as well.

Working with ports

It is possible to expose ports both explicit or randomly. Either way it is possible to resolve the IP (in case of machine) and the port (in case of random port) to use in code. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(40001,5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start()){varendpoint=container.ToHostExposedEndpoint("5432/tcp");Assert.AreEqual(40001,endpoint.Port);}

Here we map the container port 5432 to host port 40001 explicitly. Note the use of container.ToHostExposedEndpoint(...). This is to always resolve to a working ip and port to communicate with the docker container. It is also possible to map a random port, i.e. let Docker choose a available port. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start()){varendpoint=container.ToHostExposedEndpoint("5432/tcp");Assert.AreNotEqual(0,endpoint.Port);}

The only difference here is that only one argument is used when ExposePort(...) was used to configure the container. The same usage applies otherwise and thus is transparent for the code.

In order to know when a certain service is up and running before starting to e.g. connect to it. It is possible to wait for a specific port to be open. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",30000/*30s*/).Build().Start()){varconfig=container.GetConfiguration(true);Assert.AreEqual(ServiceRunningState.Running,config.State.ToServiceState());}

In the above example we wait for the container port 5432 to be opened within 30 seconds. If it fails, it will throw an exception and thus the container will be disposed and removed (since we dont have any keep container etc. configuration).

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",30000/*30s*/,"127.0.0.1").Build().Start()){varconfig=container.GetConfiguration(true);Assert.AreEqual(ServiceRunningState.Running,config.State.ToServiceState());}

Sometimes it is not possible to directly reach the container, by local ip and port, instead e.g. the container has an exposed port on the loopback interface (127.0.0.1) and that is the only way of reaching the container from the program. The above example forces the address to be 127.0.0.1 but still resolves the host port. By default, FluentDocker uses the network inspect on the container to determine the network configuration.

Sometime it is not sufficient to just wait for a port. Sometimes a container process is much more vital to wait for. Therefore a wait for process method exist in the fluent API as well as an extension method on the container object. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForProcess("postgres",30000/*30s*/).Build().Start()){varconfig=container.GetConfiguration(true);Assert.AreEqual(ServiceRunningState.Running,config.State.ToServiceState());}

In the above example Build() will return control when the process "postgres" have been started within the container.

Filesystem & Files

In order to make use of containers, sometimes it is necessary to mount volumes in the container onto the host or just copy from or to the container. Depending on if you're running machine or docker natively volume mapping have the constraint that it must be reachable from the virtual machine.

A normal use case is to have e.g. a webserver serving content on a docker container and the user edits files on the host file system. In such scenario it is necessary to mount a docker container volume onto the host. For example:

conststringhtml="<html><head>Hello World</head><body><h1>Hello world</h1></body></html>";varhostPath=(TemplateString)@"${TEMP}/fluentdockertest/${RND}";Directory.CreateDirectory(hostPath);using(varcontainer=newBuilder().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);varresponse=$"http://{container.ToHostExposedEndpoint("80/tcp")}/hello.html".Wget();Assert.AreEqual(html,response);}

In the above example a nginx container is started and mounts '/usr/share/nginx/html' onto a (random, in temp directory) host path. A HTML file is copied into the host path and when a HTTP get towards the nginx docker container is done, that same file is served.

Sometimes it is necessary copy files to and from a container. For example copy a configuration file, configure it and copy it back. More common scenario is to copy a configuration file to the container, just before it is started. The multi container example copies a nginx configuration file just before it is started. Thus it is possible to avoid manually creating a Dockerfile and a image for such a simple task. Instead just use e.g. an official or custom image, copy configuration and run.

using(newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start().CopyFrom("/etc/conf.d",fullPath)){varfiles=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")));}

Above example copies a directory to a host path (fullPath) from a running container. Note the use of extension method here, thus not using the fluent API (since CopyFrom is after Start()). If you want to copy files from the container just before starting use the Fluent API instead.

using(newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").CopyOnStart("/etc/conf.d",fullPath).Build().Start()){}

The below example illustrates a much more common scenario where files are copied to the container. This example makes use of the extension method instead of the fluent API version. It takes a Diff snapshot before copy and then just after the copy. In the latter the hello.html is present.

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start().WaitForProcess("postgres",30000/*30s*/).Diff(outbefore).CopyTo("/bin",fullPath)){varafter=container.Diff();Assert.IsFalse(before.Any(x =>x.Item=="/bin/hello.html"));Assert.IsTrue(after.Any(x =>x.Item=="/bin/hello.html"));}

Sometime is it useful to copy files in the IContainerService.Dispose() (just before container has stopped). Therefore a fluent API exists to ensure that it will just do that.

using(newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").CopyOnDispose("/etc/conf.d",fullPath).Build().Start()){}

In order to analyze a container, export extension method and fluent API methods exists. Most notably is the possibility to export a container when a IContainerService is disposed.

using(newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").ExportOnDispose(fullPath).Build().Start()){}

This will produce a container export (tar file) on the host (fullPath). If you rather have it exploded (un-tared) use the ExportExplodedOnDispose method instead. Of course you can export the container any time using a extension method on the container.

A useful trick when it comes to unit-testing is to export the container state when the unit test fails for some reason, therefore it exists a Fluent API that will export when a certain Lambda condition is met. For example:

varfailure=false;using(newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").ExportOnDispose(fullPath, svc =>failure).Build().Start()){failure=true;}

This snippet will export the container when the using statement is disposing the container since the failure variable is set to true and is used in the ExportOnDispose expression.

IService Hooks

All services can be extended with hooks. In the ExportOnDispose(path, lambda) installs a Hook when the service state is set to execute the lambda when the state is Removing. It is possible to install and remove hooks on the fly. If multiple hooks are registered on same service instance, with same ServiceRunningState, they will be executed in installation order.

The hooks are particulary good if you want something to be executed when a state is about to be set (or executed) on the service such as Starting. The Fluent API makes use of those in some situations such as Copy files, export, etc.

Docker Networking

FluentDocker do support all docker network commands. It can discover networks by _docker.NetworkLs() where it discovers all networks and some simple parameters defined in NetworkRow. It can also inspect to gain deeper information about the network (such as which containers is in the network and Ipam configuration) by _docker.NetworkInspect(network:"networkId").

In order to create a new network, use the _docker.NetworkCreate("name_of_network"). It is also possible to supply NetworkCreateParams where everything can be customized such as creating a overlay network och change the Ipam configuration. To delete a network, just use the _docker.NetworkRm(network:"networkId").

Note that networks are not deleted if there are any containers attached to it!

When a network is created it is possible to put one or more containers into it using the _docker.NetworkConnect("containerId","networkId"). Note that containers may be in several networks at a time, thus can proxy request between isolated networks. To disconnect a container from a network, simple do a _docker.NetworkDisconnect("containerId","networkId").

The following sample runs a container, creates a new network, and connects the running container into the network. It then disconnect the container, delete it, and delete the network.

varcmd=_docker.Run("postgres:9.6-alpine",newContainerCreateParams{PortMappings=new[]{"40001:5432"},Environment=new[]{"POSTGRES_PASSWORD=mysecretpassword"}},_certificates);varcontainer=cmd.Data;varnetwork=string.Empty;varcreated=_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 Networking

It is also possible to use a fluent builder to build new or reuse existing docker networks. Those can then be referenced while building containers. It is possible to build more than one docker network and attach a container to more than one network at a time.

using(varnw=newBuilder().UseNetwork("test-network")){using(varcontainer=newDockerBuilder().WithImage("kiasaki/alpine-postgres").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").ExposePorts("5432").UseNetwork(nw).WaitForPort("5432/tcp",30000/*30s*/).Build()){container.Create().Start();}}

The above code snippet creates a new network called test-network and then creates a container that is attached to the test-network. When the Dispose() is called on nw it will remove the network. It is also possible to do static IP container assignments within the network by UseIpV4 or UseIpV6. For example:

using(varnw=Fd.UseNetwork("unit-test-nw").UseSubnet("10.18.0.0/16").Build()){using(varcontainer=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()){varip=container.GetConfiguration().NetworkSettings.Networks["unit-test-nw"].IPAddress;Assert.AreEqual("10.18.0.22",ip);}}

The above example creates a new network unit-test-nw with ip-range 10.18.0.0/16. It is the used in the new container. The IP for the container is set to 10.18.0.22 and is static due to UseIpV4 command.

Volume Support

FluentDocker supports docker volume management both from commands and from a fluent API. Therefore it is possible to have total control on volumes used in container such if it shall be disposed, reused, what driver to use etc.

varvolume=_docker.VolumeCreate("test-volume","local",opts:{{"type","nfs"},{"o=addr","192.168.1.1,rw"},{"device",":/path/to/dir"}});varcfg=_docker.VolumeInspect(_certificates,"test-volume");_docker.VolumeRm(force:true,id:"test-volume");

The above snippet creates a new volume with name test-volume and is of NFS type. It then inspects the just created volume and lastly force delete the volume.

Fluent Volume API

It is also possible to use a fluent API to create or use volumes. They can then be used when building a container. This is especially useful when creation of volumes are special or lifetime needs to be controlled.

using(varvol=newBuilder().UseVolume("test-volume").RemoveOnDispose().Build()){using(varcontainer=newBuilder().UseContainer().UseImage("postgres:9.6-alpine").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").MountVolume(vol,"/var/lib/postgresql/data",MountType.ReadWrite).Build().Start()){varconfig=container.GetConfiguration();Assert.AreEqual(1,config.Mounts.Length);Assert.AreEqual("test-volume",config.Mounts[0].Name);}}

The above sample creates a new volume called test-volume and it is scheduled to be delete when Dispose() is invoked on the IVolumeService. The container is created and mounts the newly created volume to /var/lib/postgresql/data as read/write access mode. Since the container is within the scope of the using statement of the volume it's lifetime spans the whole container lifetime and then gets deleted.

Events

FluentDocker supports, connecting to the docker event mechanism to listen to the events it sends.

using(varevents=Fd.Native().Events()){using(varcontainer=newBuilder().UseContainer().UseImage("postgres:9.6-alpine").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",30000/*30s*/).Build().Start()){FdEvente;while((e=events.TryRead(3000))!=null){if(e.Type==EventType.Container&&e.Action==EventAction.Start)break;}}}

Event listener is global and may handle many EventAction types.

For example

  • Pull
  • Create
  • Start
  • Kill
  • Die
  • Connect
  • Disconnect
  • Stop
  • Destroy

Depending on action, the event type may differ such as ContainerKillEvent for EventAction.Kill. All events derive from FdEvent. That means all shared properties is in the base event and the explicit ones are in the derived.

For example, the ´ContainerKillEvent` contains the following properties:

publicsealedclassContainerKillActor:EventActor{/// <summary>/// The image name and label such as "alpine:latest"./// </summary>publicstringImage{get;set;}/// <summary>/// Name of the container./// </summary>publicstringName{get;set;}/// <summary>/// The signal that the container has been signalled./// </summary>publicstringSignal{get;set;}}

This event loop may be used to pick up events and drive your instantiated IService instances. Or if you need to react to e.g. a network is added or deleted.

Logging

In the full framework it uses verbose logging using the System.Diagnostics.Debugger.Log. For .net core it uses the standard Microsoft.Extensions.Logging.ILog to log. Both are using the category Ductus.FluentDocker and therefore may be configured to participate in logging or not and configure different logging destinations.

In .net core you can provide the logging segment in the application config file.

{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Ductus.FluentDocker": "None"
}
}
}

Please check the https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging/?view=aspnetcore-2.1 for more information. For full framework please check out the XML needed in the appconfig for the full framework described in https://docs.microsoft.com/en-us/dotnet/framework/wcf/diagnostics/tracing/configuring-tracing.

There's a quick way of disabling / enabling logging via (Ductus.FluentDocker.Services) Logging.Enabled() or Logging.Disabled(). This will forcefully enable / disable logging.

Custom IPEndpoint Resolvers

It is possible to override the default mechanism of FluentDocker resolves the container IP from the clients perspective in e.g. WaitForPort. This can be overridden on ContainerBuilder basis.

The below sample, overrides the default behaviour. When it returns null the default resolver kicks in.

using(varcontainer=Fd.UseContainer().UseImage("postgres:9.6-alpine").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").ExposePort(5432).UseCustomResolver((ports,portAndProto,dockerUri)=>{if(null==ports||string.IsNullOrEmpty(portAndProto))returnnull;if(!ports.TryGetValue(portAndProto,outvarendpoints))returnnull;if(null==endpoints||endpoints.Length==0)returnnull;if(CommandExtensions.IsNative())returnendpoints[0];if(CommandExtensions.IsEmulatedNative())returnCommandExtensions.IsDockerDnsAvailable()?newIPEndPoint(CommandExtensions.EmulatedNativeAddress(),endpoints[0].Port):newIPEndPoint(IPAddress.Loopback,endpoints[0].Port);if(Equals(endpoints[0].Address,IPAddress.Any)&&null!=dockerUri)returnnewIPEndPoint(IPAddress.Parse(dockerUri.Host),endpoints[0].Port);returnendpoints[0];}).WaitForPort("5432/tcp",30000/*30s*/).Build().Start()){varstate=container.GetConfiguration(true/*force*/).State.ToServiceState();Assert.AreEqual(ServiceRunningState.Running,state);}

Talking to custom docker daemon URI without docker-machine

There's limited support to use the FluentAPI to talk to a remote docker daemon without using docker-machine. This is done either by manually creating a instance of a DockerHostService or use FromUri on HostBuilder.

using(varcontainer=Fd.UseHost().FromUri(Settings.DockerUri,isWindowsHost:true).UseContainer().Build()){}

The above sample connects to a custom DockerUri from a setting and is a windows container docker daemon.

  • FromUri that uses a DockerUri to create a IHostService. This uri is arbitrary. It also support other properties (see below).
publicHostBuilderFromUri(DockerUriuri,stringname=null,boolisNative=true,boolstopWhenDisposed=false,boolisWindowsHost=false,stringcertificatePath=null){/*...*/}

It will use "sensible" defaults on all parameters. Most of the case the uri is sufficient. For example if not providing the certificatePath it will try to get it from the environment DOCKER_CERT_PATH. If not found in the environment, it will default to none.

  • UseHost that takes a instantiated IHostService implementation.

Docker Compose Support

The library support docker-compose to use existing compose files to render services and manage lifetime of such.

The following sample will do have compose file that fires up a MySql and a WordPress. Therefore the single compose service will have two container services below it. By default, it will stop the services and clean up when Dispose() is invoked. This can be overridden by KeepContainers() in the fluent configuration.

version: '3.3'services:
db:
image: mysql:5.7volumes:
- db_data:/var/lib/mysqlrestart: alwaysenvironment:
MYSQL_ROOT_PASSWORD: somewordpressMYSQL_DATABASE: wordpressMYSQL_USER: wordpressMYSQL_PASSWORD: wordpresswordpress:
depends_on:
- dbimage: wordpress:latestports:
- "8000:80"restart: alwaysenvironment:
WORDPRESS_DB_HOST: db:3306WORDPRESS_DB_USER: wordpressWORDPRESS_DB_PASSWORD: wordpressvolumes:
db_data:

The above file is the docker-compose file to stitch up the complete service.

varfile=Path.Combine(Directory.GetCurrentDirectory(),(TemplateString)"Resources/ComposeTests/WordPress/docker-compose.yml");using(varsvc=newBuilder().UseContainer().UseCompose().FromFile(file).RemoveOrphans().Build().Start()){varinstallPage=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);}

The above snippet is fluently configuring the docker-compose service and invokes the install page to verify that WordPress is indeed working.

It is also possible to do all the operations that a single container supports such as copy on, export, wait operations. For example:

varfile=Path.Combine(Directory.GetCurrentDirectory(),(TemplateString)"Resources/ComposeTests/WordPress/docker-compose.yml");// @formatter:offusing(newBuilder().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. varinstallPage=await"http://localhost:8000/wp-admin/install.php".Wget();Assert.IsTrue(installPage.IndexOf("https://wordpress.org/",StringComparison.Ordinal)!=-1);}

The above snippet fires up the wordpress docker compose project and checks the URLhttp://localhost:8000/wp-admin/install.php it it returns a certain value in the body (in this case "https://wordpress.org/"). If not it returns 500 and the WaitForHttp function will wait 500 milliseconds before invoking again. This works for any custom lambda as well, just use WaitFor instead. Thus it is possible to e.g. query a database before continuing inside the using scope.

Talking to Docker Daemon

For Linux and Mac users there are several options how to authenticate towards the socket. FluentDocker supports no sudo, sudo without any password (user added as NOPASSWD in /etc/sudoer), or sudo with password. The default is that FluentDocker expects to be able to talk without any sudo. The options ar global but can be changed in runtime.

SudoMechanism.None.SetSudo();// This is the defaultSudoMechanism.Password.SetSudo("<my-sudo-password>");SudoMechanism.NoPassword.SetSudo();

If you wish to turn off sudo to communicate with the docker daemon, you can follow a docker tutorial and do the last step of adding your user to docker group.

Connecting to Remote Docker Daemons

FluentDocker supports connection to remote docker daemons. The fluent API supports e.g.

newBuilder().UseHost().UseMachine().WithName("remote-daemon")

where this requires a already pre-setup entry in the docker-machine registry. It is also possible to define SSH based docker-machine registry entires to connect to remote daemon.

using(varcontainer=newBuilder().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);}

This example will create a new docker-machine registry entry named remote-daemon that uses SSH with ip address of 192.168.1.27 and the SSH user solo. If a entry is already found named remote-daemon it will just reuse this entry. Then it gets a IHostService with correct certificates and URL for the remote daemon. Thus, it is possible then to create a docker container on the remote daemon, in thus case it is the postgres image. When it disposes the container, as usual it deletes it from the remote docker. The IHostService do make sure to pick upp all necessary certificates in order to authenticate the connection.

The above example produces this docker-machine registry entry.

C:\Users\martoffi>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

In order to use UseSsh(...) a SSH tunnel with no password must been set up. In addition the user that uses the tunnel must be allowed to access the docker daemon.

Follow these tutorial how to setup the SSH tunnel and make sure the user can access the docker daemon.

  1. https://www.kevinkuszyk.com/2016/11/28/connect-your-docker-client-to-a-remote-docker-host/
  2. https://askubuntu.com/questions/192050/how-to-run-sudo-command-with-no-password

Basically create a new rsa key to use with the SSH tunnel using ssh-keygen -t rsa and then copy it to the remote host by ssh-copy-id {username}@{host}.

Edit the /etc/sudoers as specified in the second tutorial.

When this is done, you now can access the remote docker daemon by the generic driver or the fluent API specified above. To do the same thing manually as specified in the example it would look something like this.

C:\Users\martoffi>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

Now the registry entry is created, it is possible set the environment for the terminal docker.

C:\Users\martoffi>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:\Users\martoffi\.docker\machine\machines\remote-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

Run this to make docker client use the remote docker daemon.

@FOR /f "tokens=*" %i IN ('docker-machine.exe env remote-daemon') DO @%i

All commands using the docker binary will now execute on the remote docker daemon.

Hyper-V

When creating and querying, via machine, a hyper-v docker machine the process needs to be elevated since Hyper-V will not respond to API calls in standard user mode.

Misc

Health Check

It is possible to specify a health check for the docker container to report the state of the container based on such activity. The following example is using a health check that the container has exited or not. It is possible to check the configuration (make sure to force refresh) what status the health check is reporting.

using(varcontainer=Fd.UseContainer().UseImage("postgres:latest",force:true).HealthCheck("exit").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start()){varconfig=container.GetConfiguration(true);AreEqual(HealthState.Starting,config.State.Health.Status);}

ulimit

It is possible via the Fluent API and ContainerCreateParams specify ulimit to the docker container to e.g. limit the number of open files etc. For example using the Fluent API could look like this when restricting the number of open files to 2048 (both soft and hard).

using(varcontainer=Fd.UseContainer().UseImage("postgres:latest",force:true).UseUlimit(Ulimit.NoFile,2048,2048).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start()){// Do stuff}

Test Support

This repo contains three nuget packages, one for the fluent access, one for ms-test base classes and another for xunit base classes to be used while testing. For example in a unit-test it is possible to fire up a postgres container and wait when the the db has booted.

XUnit

publicclassPostgresXUnitTests:IClassFixture<PostgresTestBase>{[Fact]publicvoidTest(){// We now have a running postgres// and a valid connection string to use.}}

MSTest

using(varcontainer=newDockerBuilder().WithImage("kiasaki/alpine-postgres").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").ExposePorts("5432").WaitForPort("5432/tcp",30000/*30s*/).Build()){container.Create().Start();}

It is also possible to re-use abstract base classes, for example postgres test base to simplify and make clean unittest towards a container.

[TestClass]publicclassPostgresMsTests:PostgresTestBase{[TestMethod]publicvoidTest(){// We now have a running postgres// and a valid connection string to use.}}

The FluentDockerTestBase allows for simple overrides to do whatever custom docker backed test easily. Just create a test class and derive from the FluentDockerTestBase and override suitable methods. For example.

protectedoverrideDockerBuilderBuild(){returnnewDockerBuilder().WithImage("kiasaki/alpine-postgres").WithEnvironment($"POSTGRES_PASSWORD={PostgresPassword}").ExposePorts("5432").WaitForPort("5432/tcp",30000/*30s*/);}

This will create a builder with docker image postgres:latest and set one environment string, it will also expose the postgres db port 5432 to the host so one can connect to the db within the container. Lastly it will wait for the port 5432. This ensures that the db is running and have properly booted. If timeout, in this example set to 30 seconds, it will throw an exception and the container is stopped and removed. Note that the host port is not 5432! Use Container.GetHostPort("5432/tcp") to get the host port. The host ip can be retrieved by Container.Host property and thus shall be used when communicating with the app in the container.

If a callback is needed when the container has been successfully pulled, created, and started.

protectedoverridevoidOnContainerInitialized(){ConnectionString=string.Format(PostgresConnectionString,Container.Host,Container.GetHostPort("5432/tcp"),PostgresUser,PostgresPassword,PostgresDb);}

This example renders a proper connection string to the postgresql db within the newly spun up container. This can be used to connect using Npgsql, EF7, NHibernate, Marten or other compatible tools. This method will not be called if pulling of the image from the docker repository or it could not create/start the container.

If a before shutdown container hook is wanted override.

protectedvirtualvoidOnContainerTearDown(){// Do stuff before container is shut down.}

Note that if un-named container, if not properly disposed, the docker container will still run and must be manually removed. This is a feature not a bug since you might want several containers running in your test. The DockerContainer class manages the instance id of the container and thus only interact with it and no other container.

When creating / starting a new container it will first check the local repository if the container image is already present and will download it if not found. This may take some time and there's just a Debug Log if enabled it is possible to monitor the download process.

Miscellanious

Unhandled Exceptions

When a unhandled exception occurs and the application FailFast i.e. terminates quickly it will not invoke finally clause. Thus a failing WaitForPort inside a using statement will not dispose the container service. Therefore the container is is still running. To fix this, either have a global try...catch or inject one locally e.g.

try{using(varcontainer=newBuilder().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;}

But it this is only when application termination is done due to the FluentDockerException thrown in the WaitForPort, otherwise it will dispose the container properly and thus the try...catch is not needed.

This could also be solved using the Fd.Build functions (see Using Builder Extensions for more information).

Using Builder Extensions

The class Fd is a static class that provides convenience methods for building and running single and composed containers. To build a container just use:

varbuild=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 ofvarbuild=newBuilder().UseContainer().UseImage("postgres:9.6-alpine").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",TimeSpan.FromSeconds(30));

This can then be used to start the containers within a safeusing clause that is guaranteed to be disposed even if uncaught exception.

build.Container(svc =>{varconfig=svc.GetConfiguration();// Do stuff...});

After the Container method has been executed the container is in this case stopped and removed. This is the quivalent of

// This is equivalent oftry{using(varsvc=build.Build()){svc.Start();varconfig=svc.GetConfiguration();// Do stuff... }}catch{Log(...);throw;}

It is also possible to combine builder and running e.g. via:

Fd.Container(c =>c.UseContainer().UseImage("postgres:9.6-alpine").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",TimeSpan.FromSeconds(30)),
svc =>{varconfig=svc.GetConfiguration();// Do stuff...});

The above example will build the container, start, stop, and finally delete the container. Even if and Exception is thrown it will be Disposed. Of course it is possible to use compsed container using composite extension methods as with container.

About

Use docker, docker-compose local and remote in tests and your .NET core/full framework apps via a FluentAPI

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Quality Gate StatusBuild status

PackageNuGet
FluentDockerNuGet
Microsoft TestNuGet
XUnit TestNuGet

FluentDocker

This library enables docker and docker-compose interactions usinga Fluent API. It is supported on Linux, Windows and Mac. It also has support for the legazy docker-machine interactions.

Sample Fluent API usage

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",30000/*30s*/).Build().Start()){varconfig=container.GetConfiguration(true);Assert.AreEqual(ServiceRunningState.Running,config.State.ToServiceState());}

This fires up a postgres and waits for it to be ready. To use compose, just do it like this:

💡 NOTE: Use the AssumeComposeVersion(ComposeVersion.V2) to use the V2 behaviour, default is still V1 (to be changed to default to V2 later this year)

varfile=Path.Combine(Directory.GetCurrentDirectory(),(TemplateString)"Resources/ComposeTests/WordPress/docker-compose.yml");// @formatter:offusing(varsvc=newBuilder().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 varinstallPage=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 composeAssert.AreEqual(2,svc.Containers.Count);// We can access each individual containerAssert.AreEqual(2,svc.Images.Count);// And the images used.}

:bulb Note for Linux Users: Docker requires sudo by default and the library by default expects that executing user do not need to do sudo in order to talk to the docker daemon. More description can be found in the Talking to Docker Daemon chapter.

The fluent API builds up one or more services. Each service may be composite or singular. Therefore it is possible to e.g. fire up several docker-compose based services and manage each of them as a single service or dig in and use all underlying services on each docker-compose service. It is also possible to use services directly e.g.

varfile=Path.Combine(Directory.GetCurrentDirectory(),(TemplateString)"Resources/ComposeTests/WordPress/docker-compose.yml");using(varsvc=newDockerComposeCompositeService(DockerHost,newDockerComposeConfig{ComposeFilePath=newList<string>{file},ForceRecreate=true,RemoveOrphans=true,StopOnDispose=true})){svc.Start();// We now have a running WordPress with a MySql databasevarinstallPage=await$"http://localhost:8000/wp-admin/install.php".Wget();Assert.IsTrue(installPage.IndexOf("https://wordpress.org/",StringComparison.Ordinal)!=-1);}

The above example creates a docker-compose service from a single compose file. When the service is disposed all underlying services is automatically stopped.

The library is supported by .NET full 4.51 framework and higher, .NET standard 1.6, 2.0. It is divided into three thin layers, each layer is accessible:

  1. Docker Binaries interactions - Static commands and docker environment
  2. Services - thin service layer to manage machines, containers etc.
  3. Fluent API - API to build/discover services to be used

The Majority of the service methods are extension methods and not hardwired into the service itself, making them lightweight and customizable. Since everything is accessible it is e.g. easy to add extensions method for a service that uses the layer 1 commands to provide functionality.

Contribution

I do welcome contribution, though there is no contribution guideline as of yet, make sure to adhere to .editorconfig when doing the Pull Requests. Otherwise the build will fail. I'll update with a real guideline sooner or later this year.

Basic Usage of Commands (Layer 1)

All commands needs a DockerUri to work with. It is the Uri to the docker daemon, either locally or remote. It can be discoverable or hardcoded. Discovery of local DockerUri can be done by

varhosts=newHosts().Discover();var_docker=hosts.FirstOrDefault(x =>x.IsNative)??hosts.FirstOrDefault(x =>x.Name=="default");

The example snipped will check for native, or docker beta "native" hosts, if not choose the docker-machine "default" as host. If you're using docker-machine and no machine exists or is not started it is easy to create / start a docker-machine by e.g. "test-machine".Create(1024,20000000,1). This will create a docker machine named "test-machine" with 1GB of RAM, 20GB Disk, and use one CPU.

It is now possible to use the Uri to communicate using the commands. For example to get the version of client and server docker binaries:

varresult=_docker.Host.Version(_docker.Certificates);Debug.WriteLine(result.Data);// Will Print the Client and Server Version and API Versions respectively.

All commands return a CommandResponse such that it is possible to check success factor by response.Success. If any data associated with the command it is returned in the response.Data property.

Then it is simple as below to start and stop include delete a container using the commands. Below starts a container and do a PS on it and then deletes it.

varid=_docker.Host.Run("nginx:latest",null,_docker.Certificates).Data;varps=_docker.Host.Ps(null,_docker.Certificates).Data;_docker.Host.RemoveContainer(id,true,true,null,_docker.Certificates);

When running on windows, one can choose to run linux or windows container. Use the LinuxDaemon or WindowsDaemon to control which daemon to talk to.

_docker.LinuxDaemon();// ensures that it will talk to linux daemon, if windows daemon it will switch

Some commands returns a stream of data when e.g. events or logs is wanted using a continuous stream. Streams can be used in background tasks and support CancellationToken. Below example tails a log.

using(varlogs=_docker.Host.Logs(id,_docker.Certificates)){while(!logs.IsFinished){varline=logs.TryRead(5000);// Do a read with timeoutif(null==line){break;}Debug.WriteLine(line);}}

Utility methods exists for commands. They come in different flaviours such as networking etc. For example when reading a log to the end:

using(varlogs=_docker.Host.Logs(id,_docker.Certificates)){foreach(varlineinlogs.ReadToEnd()){Debug.WriteLine(line);}}

Using Fluent API

The highest layer of this library is the fluent API where you can define and control machines, images, and containers. For example to setup a load balancer with two nodejs servers reading from a redis server can look like this (node image is custom built if not found in the repository).

varfullPath=(TemplateString)@"${TEMP}/fluentdockertest/${RND}";varnginx=Path.Combine(fullPath,"nginx.conf");Directory.CreateDirectory(fullPath);typeof(NsResolver).ResourceExtract(fullPath,"index.js");using(varservices=newBuilder()// Define custom node image to be used.DefineImage("mariotoffia/nodetest").ReuseIfAlreadyExists().From("ubuntu").Maintainer("Mario Toffia <mario.toffia@xyz.com>").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);varep=services.Containers.First(x =>x.Name=="nginx").ToHostExposedEndpoint("80/tcp");Assert.IsNotNull(ep);varround1=$"http://{ep.Address}:{ep.Port}".Wget();Assert.AreEqual("This page has been viewed 1 times!",round1);varround2=$"http://{ep.Address}:{ep.Port}".Wget();Assert.AreEqual("This page has been viewed 2 times!",round2);}

The above example defines a Dockerfile, builds it, for the node image. It then uses vanilla redis and nginx. If you just want to use an existing Dockerfile it can be done like this.

using(varservices=newBuilder().DefineImage("mariotoffia/nodetest").ReuseIfAlreadyExists().FromFile("/tmp/Dockerfile").Build().Start()){// Container either build to reused if found in registry and started here.}

The fluent API supports from defining a docker-machine to a set of docker instances. It has built-in support for e.g. waiting for a specific port or a process within the container before Build() completes and thus can be safely be used within a using statement. If specific management on wait timeouts etc. you can always build and start the container and use extension methods to do the waiting on the container itself.

To create a container just omit the start. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build()){Assert.AreEqual(ServiceRunningState.Stopped,container.State);}

This example creates a container with postgres, configure one environment variable. Within the using statement it is possible to start the IContainerService. Thus each built container is wrapped in a IContainerService. It is also possible to use the IHostService.GetContainers(...) to obtain the created, running, and exited containers. From the IHostService it is also possible to get all the images in the local repository to create containers from.

Whe you want to run a single container do use the fluent or container service start method. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start()){varconfig=container.GetConfiguration();Assert.AreEqual(ServiceRunningState.Running,container.State);Assert.IsTrue(config.Config.Env.Any(x =>x=="POSTGRES_PASSWORD=mysecretpassword"));}

By default the container is stopped and deleted when the Dispose method is run, in order to keep the container in archve, use the KeepContainer() on the fluent API. When Dispose() is invoked it will be stopped but not deleted. It is also possible to keep it running after dispose as well.

Working with ports

It is possible to expose ports both explicit or randomly. Either way it is possible to resolve the IP (in case of machine) and the port (in case of random port) to use in code. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(40001,5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start()){varendpoint=container.ToHostExposedEndpoint("5432/tcp");Assert.AreEqual(40001,endpoint.Port);}

Here we map the container port 5432 to host port 40001 explicitly. Note the use of container.ToHostExposedEndpoint(...). This is to always resolve to a working ip and port to communicate with the docker container. It is also possible to map a random port, i.e. let Docker choose a available port. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start()){varendpoint=container.ToHostExposedEndpoint("5432/tcp");Assert.AreNotEqual(0,endpoint.Port);}

The only difference here is that only one argument is used when ExposePort(...) was used to configure the container. The same usage applies otherwise and thus is transparent for the code.

In order to know when a certain service is up and running before starting to e.g. connect to it. It is possible to wait for a specific port to be open. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",30000/*30s*/).Build().Start()){varconfig=container.GetConfiguration(true);Assert.AreEqual(ServiceRunningState.Running,config.State.ToServiceState());}

In the above example we wait for the container port 5432 to be opened within 30 seconds. If it fails, it will throw an exception and thus the container will be disposed and removed (since we dont have any keep container etc. configuration).

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",30000/*30s*/,"127.0.0.1").Build().Start()){varconfig=container.GetConfiguration(true);Assert.AreEqual(ServiceRunningState.Running,config.State.ToServiceState());}

Sometimes it is not possible to directly reach the container, by local ip and port, instead e.g. the container has an exposed port on the loopback interface (127.0.0.1) and that is the only way of reaching the container from the program. The above example forces the address to be 127.0.0.1 but still resolves the host port. By default, FluentDocker uses the network inspect on the container to determine the network configuration.

Sometime it is not sufficient to just wait for a port. Sometimes a container process is much more vital to wait for. Therefore a wait for process method exist in the fluent API as well as an extension method on the container object. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForProcess("postgres",30000/*30s*/).Build().Start()){varconfig=container.GetConfiguration(true);Assert.AreEqual(ServiceRunningState.Running,config.State.ToServiceState());}

In the above example Build() will return control when the process "postgres" have been started within the container.

Filesystem & Files

In order to make use of containers, sometimes it is necessary to mount volumes in the container onto the host or just copy from or to the container. Depending on if you're running machine or docker natively volume mapping have the constraint that it must be reachable from the virtual machine.

A normal use case is to have e.g. a webserver serving content on a docker container and the user edits files on the host file system. In such scenario it is necessary to mount a docker container volume onto the host. For example:

conststringhtml="<html><head>Hello World</head><body><h1>Hello world</h1></body></html>";varhostPath=(TemplateString)@"${TEMP}/fluentdockertest/${RND}";Directory.CreateDirectory(hostPath);using(varcontainer=newBuilder().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);varresponse=$"http://{container.ToHostExposedEndpoint("80/tcp")}/hello.html".Wget();Assert.AreEqual(html,response);}

In the above example a nginx container is started and mounts '/usr/share/nginx/html' onto a (random, in temp directory) host path. A HTML file is copied into the host path and when a HTTP get towards the nginx docker container is done, that same file is served.

Sometimes it is necessary copy files to and from a container. For example copy a configuration file, configure it and copy it back. More common scenario is to copy a configuration file to the container, just before it is started. The multi container example copies a nginx configuration file just before it is started. Thus it is possible to avoid manually creating a Dockerfile and a image for such a simple task. Instead just use e.g. an official or custom image, copy configuration and run.

using(newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start().CopyFrom("/etc/conf.d",fullPath)){varfiles=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")));}

Above example copies a directory to a host path (fullPath) from a running container. Note the use of extension method here, thus not using the fluent API (since CopyFrom is after Start()). If you want to copy files from the container just before starting use the Fluent API instead.

using(newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").CopyOnStart("/etc/conf.d",fullPath).Build().Start()){}

The below example illustrates a much more common scenario where files are copied to the container. This example makes use of the extension method instead of the fluent API version. It takes a Diff snapshot before copy and then just after the copy. In the latter the hello.html is present.

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start().WaitForProcess("postgres",30000/*30s*/).Diff(outbefore).CopyTo("/bin",fullPath)){varafter=container.Diff();Assert.IsFalse(before.Any(x =>x.Item=="/bin/hello.html"));Assert.IsTrue(after.Any(x =>x.Item=="/bin/hello.html"));}

Sometime is it useful to copy files in the IContainerService.Dispose() (just before container has stopped). Therefore a fluent API exists to ensure that it will just do that.

using(newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").CopyOnDispose("/etc/conf.d",fullPath).Build().Start()){}

In order to analyze a container, export extension method and fluent API methods exists. Most notably is the possibility to export a container when a IContainerService is disposed.

using(newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").ExportOnDispose(fullPath).Build().Start()){}

This will produce a container export (tar file) on the host (fullPath). If you rather have it exploded (un-tared) use the ExportExplodedOnDispose method instead. Of course you can export the container any time using a extension method on the container.

A useful trick when it comes to unit-testing is to export the container state when the unit test fails for some reason, therefore it exists a Fluent API that will export when a certain Lambda condition is met. For example:

varfailure=false;using(newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").ExportOnDispose(fullPath, svc =>failure).Build().Start()){failure=true;}

This snippet will export the container when the using statement is disposing the container since the failure variable is set to true and is used in the ExportOnDispose expression.

IService Hooks

All services can be extended with hooks. In the ExportOnDispose(path, lambda) installs a Hook when the service state is set to execute the lambda when the state is Removing. It is possible to install and remove hooks on the fly. If multiple hooks are registered on same service instance, with same ServiceRunningState, they will be executed in installation order.

The hooks are particulary good if you want something to be executed when a state is about to be set (or executed) on the service such as Starting. The Fluent API makes use of those in some situations such as Copy files, export, etc.

Docker Networking

FluentDocker do support all docker network commands. It can discover networks by _docker.NetworkLs() where it discovers all networks and some simple parameters defined in NetworkRow. It can also inspect to gain deeper information about the network (such as which containers is in the network and Ipam configuration) by _docker.NetworkInspect(network:"networkId").

In order to create a new network, use the _docker.NetworkCreate("name_of_network"). It is also possible to supply NetworkCreateParams where everything can be customized such as creating a overlay network och change the Ipam configuration. To delete a network, just use the _docker.NetworkRm(network:"networkId").

Note that networks are not deleted if there are any containers attached to it!

When a network is created it is possible to put one or more containers into it using the _docker.NetworkConnect("containerId","networkId"). Note that containers may be in several networks at a time, thus can proxy request between isolated networks. To disconnect a container from a network, simple do a _docker.NetworkDisconnect("containerId","networkId").

The following sample runs a container, creates a new network, and connects the running container into the network. It then disconnect the container, delete it, and delete the network.

varcmd=_docker.Run("postgres:9.6-alpine",newContainerCreateParams{PortMappings=new[]{"40001:5432"},Environment=new[]{"POSTGRES_PASSWORD=mysecretpassword"}},_certificates);varcontainer=cmd.Data;varnetwork=string.Empty;varcreated=_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 Networking

It is also possible to use a fluent builder to build new or reuse existing docker networks. Those can then be referenced while building containers. It is possible to build more than one docker network and attach a container to more than one network at a time.

using(varnw=newBuilder().UseNetwork("test-network")){using(varcontainer=newDockerBuilder().WithImage("kiasaki/alpine-postgres").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").ExposePorts("5432").UseNetwork(nw).WaitForPort("5432/tcp",30000/*30s*/).Build()){container.Create().Start();}}

The above code snippet creates a new network called test-network and then creates a container that is attached to the test-network. When the Dispose() is called on nw it will remove the network. It is also possible to do static IP container assignments within the network by UseIpV4 or UseIpV6. For example:

using(varnw=Fd.UseNetwork("unit-test-nw").UseSubnet("10.18.0.0/16").Build()){using(varcontainer=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()){varip=container.GetConfiguration().NetworkSettings.Networks["unit-test-nw"].IPAddress;Assert.AreEqual("10.18.0.22",ip);}}

The above example creates a new network unit-test-nw with ip-range 10.18.0.0/16. It is the used in the new container. The IP for the container is set to 10.18.0.22 and is static due to UseIpV4 command.

Volume Support

FluentDocker supports docker volume management both from commands and from a fluent API. Therefore it is possible to have total control on volumes used in container such if it shall be disposed, reused, what driver to use etc.

varvolume=_docker.VolumeCreate("test-volume","local",opts:{{"type","nfs"},{"o=addr","192.168.1.1,rw"},{"device",":/path/to/dir"}});varcfg=_docker.VolumeInspect(_certificates,"test-volume");_docker.VolumeRm(force:true,id:"test-volume");

The above snippet creates a new volume with name test-volume and is of NFS type. It then inspects the just created volume and lastly force delete the volume.

Fluent Volume API

It is also possible to use a fluent API to create or use volumes. They can then be used when building a container. This is especially useful when creation of volumes are special or lifetime needs to be controlled.

using(varvol=newBuilder().UseVolume("test-volume").RemoveOnDispose().Build()){using(varcontainer=newBuilder().UseContainer().UseImage("postgres:9.6-alpine").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").MountVolume(vol,"/var/lib/postgresql/data",MountType.ReadWrite).Build().Start()){varconfig=container.GetConfiguration();Assert.AreEqual(1,config.Mounts.Length);Assert.AreEqual("test-volume",config.Mounts[0].Name);}}

The above sample creates a new volume called test-volume and it is scheduled to be delete when Dispose() is invoked on the IVolumeService. The container is created and mounts the newly created volume to /var/lib/postgresql/data as read/write access mode. Since the container is within the scope of the using statement of the volume it's lifetime spans the whole container lifetime and then gets deleted.

Events

FluentDocker supports, connecting to the docker event mechanism to listen to the events it sends.

using(varevents=Fd.Native().Events()){using(varcontainer=newBuilder().UseContainer().UseImage("postgres:9.6-alpine").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",30000/*30s*/).Build().Start()){FdEvente;while((e=events.TryRead(3000))!=null){if(e.Type==EventType.Container&&e.Action==EventAction.Start)break;}}}

Event listener is global and may handle many EventAction types.

For example

  • Pull
  • Create
  • Start
  • Kill
  • Die
  • Connect
  • Disconnect
  • Stop
  • Destroy

Depending on action, the event type may differ such as ContainerKillEvent for EventAction.Kill. All events derive from FdEvent. That means all shared properties is in the base event and the explicit ones are in the derived.

For example, the ´ContainerKillEvent` contains the following properties:

publicsealedclassContainerKillActor:EventActor{/// <summary>/// The image name and label such as "alpine:latest"./// </summary>publicstringImage{get;set;}/// <summary>/// Name of the container./// </summary>publicstringName{get;set;}/// <summary>/// The signal that the container has been signalled./// </summary>publicstringSignal{get;set;}}

This event loop may be used to pick up events and drive your instantiated IService instances. Or if you need to react to e.g. a network is added or deleted.

Logging

In the full framework it uses verbose logging using the System.Diagnostics.Debugger.Log. For .net core it uses the standard Microsoft.Extensions.Logging.ILog to log. Both are using the category Ductus.FluentDocker and therefore may be configured to participate in logging or not and configure different logging destinations.

In .net core you can provide the logging segment in the application config file.

{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Ductus.FluentDocker": "None"
}
}
}

Please check the https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging/?view=aspnetcore-2.1 for more information. For full framework please check out the XML needed in the appconfig for the full framework described in https://docs.microsoft.com/en-us/dotnet/framework/wcf/diagnostics/tracing/configuring-tracing.

There's a quick way of disabling / enabling logging via (Ductus.FluentDocker.Services) Logging.Enabled() or Logging.Disabled(). This will forcefully enable / disable logging.

Custom IPEndpoint Resolvers

It is possible to override the default mechanism of FluentDocker resolves the container IP from the clients perspective in e.g. WaitForPort. This can be overridden on ContainerBuilder basis.

The below sample, overrides the default behaviour. When it returns null the default resolver kicks in.

using(varcontainer=Fd.UseContainer().UseImage("postgres:9.6-alpine").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").ExposePort(5432).UseCustomResolver((ports,portAndProto,dockerUri)=>{if(null==ports||string.IsNullOrEmpty(portAndProto))returnnull;if(!ports.TryGetValue(portAndProto,outvarendpoints))returnnull;if(null==endpoints||endpoints.Length==0)returnnull;if(CommandExtensions.IsNative())returnendpoints[0];if(CommandExtensions.IsEmulatedNative())returnCommandExtensions.IsDockerDnsAvailable()?newIPEndPoint(CommandExtensions.EmulatedNativeAddress(),endpoints[0].Port):newIPEndPoint(IPAddress.Loopback,endpoints[0].Port);if(Equals(endpoints[0].Address,IPAddress.Any)&&null!=dockerUri)returnnewIPEndPoint(IPAddress.Parse(dockerUri.Host),endpoints[0].Port);returnendpoints[0];}).WaitForPort("5432/tcp",30000/*30s*/).Build().Start()){varstate=container.GetConfiguration(true/*force*/).State.ToServiceState();Assert.AreEqual(ServiceRunningState.Running,state);}

Talking to custom docker daemon URI without docker-machine

There's limited support to use the FluentAPI to talk to a remote docker daemon without using docker-machine. This is done either by manually creating a instance of a DockerHostService or use FromUri on HostBuilder.

using(varcontainer=Fd.UseHost().FromUri(Settings.DockerUri,isWindowsHost:true).UseContainer().Build()){}

The above sample connects to a custom DockerUri from a setting and is a windows container docker daemon.

  • FromUri that uses a DockerUri to create a IHostService. This uri is arbitrary. It also support other properties (see below).
publicHostBuilderFromUri(DockerUriuri,stringname=null,boolisNative=true,boolstopWhenDisposed=false,boolisWindowsHost=false,stringcertificatePath=null){/*...*/}

It will use "sensible" defaults on all parameters. Most of the case the uri is sufficient. For example if not providing the certificatePath it will try to get it from the environment DOCKER_CERT_PATH. If not found in the environment, it will default to none.

  • UseHost that takes a instantiated IHostService implementation.

Docker Compose Support

The library support docker-compose to use existing compose files to render services and manage lifetime of such.

The following sample will do have compose file that fires up a MySql and a WordPress. Therefore the single compose service will have two container services below it. By default, it will stop the services and clean up when Dispose() is invoked. This can be overridden by KeepContainers() in the fluent configuration.

version: '3.3'services:
db:
image: mysql:5.7volumes:
- db_data:/var/lib/mysqlrestart: alwaysenvironment:
MYSQL_ROOT_PASSWORD: somewordpressMYSQL_DATABASE: wordpressMYSQL_USER: wordpressMYSQL_PASSWORD: wordpresswordpress:
depends_on:
- dbimage: wordpress:latestports:
- "8000:80"restart: alwaysenvironment:
WORDPRESS_DB_HOST: db:3306WORDPRESS_DB_USER: wordpressWORDPRESS_DB_PASSWORD: wordpressvolumes:
db_data:

The above file is the docker-compose file to stitch up the complete service.

varfile=Path.Combine(Directory.GetCurrentDirectory(),(TemplateString)"Resources/ComposeTests/WordPress/docker-compose.yml");using(varsvc=newBuilder().UseContainer().UseCompose().FromFile(file).RemoveOrphans().Build().Start()){varinstallPage=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);}

The above snippet is fluently configuring the docker-compose service and invokes the install page to verify that WordPress is indeed working.

It is also possible to do all the operations that a single container supports such as copy on, export, wait operations. For example:

varfile=Path.Combine(Directory.GetCurrentDirectory(),(TemplateString)"Resources/ComposeTests/WordPress/docker-compose.yml");// @formatter:offusing(newBuilder().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. varinstallPage=await"http://localhost:8000/wp-admin/install.php".Wget();Assert.IsTrue(installPage.IndexOf("https://wordpress.org/",StringComparison.Ordinal)!=-1);}

The above snippet fires up the wordpress docker compose project and checks the URLhttp://localhost:8000/wp-admin/install.php it it returns a certain value in the body (in this case "https://wordpress.org/"). If not it returns 500 and the WaitForHttp function will wait 500 milliseconds before invoking again. This works for any custom lambda as well, just use WaitFor instead. Thus it is possible to e.g. query a database before continuing inside the using scope.

Talking to Docker Daemon

For Linux and Mac users there are several options how to authenticate towards the socket. FluentDocker supports no sudo, sudo without any password (user added as NOPASSWD in /etc/sudoer), or sudo with password. The default is that FluentDocker expects to be able to talk without any sudo. The options ar global but can be changed in runtime.

SudoMechanism.None.SetSudo();// This is the defaultSudoMechanism.Password.SetSudo("<my-sudo-password>");SudoMechanism.NoPassword.SetSudo();

If you wish to turn off sudo to communicate with the docker daemon, you can follow a docker tutorial and do the last step of adding your user to docker group.

Connecting to Remote Docker Daemons

FluentDocker supports connection to remote docker daemons. The fluent API supports e.g.

newBuilder().UseHost().UseMachine().WithName("remote-daemon")

where this requires a already pre-setup entry in the docker-machine registry. It is also possible to define SSH based docker-machine registry entires to connect to remote daemon.

using(varcontainer=newBuilder().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);}

This example will create a new docker-machine registry entry named remote-daemon that uses SSH with ip address of 192.168.1.27 and the SSH user solo. If a entry is already found named remote-daemon it will just reuse this entry. Then it gets a IHostService with correct certificates and URL for the remote daemon. Thus, it is possible then to create a docker container on the remote daemon, in thus case it is the postgres image. When it disposes the container, as usual it deletes it from the remote docker. The IHostService do make sure to pick upp all necessary certificates in order to authenticate the connection.

The above example produces this docker-machine registry entry.

C:\Users\martoffi>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

In order to use UseSsh(...) a SSH tunnel with no password must been set up. In addition the user that uses the tunnel must be allowed to access the docker daemon.

Follow these tutorial how to setup the SSH tunnel and make sure the user can access the docker daemon.

  1. https://www.kevinkuszyk.com/2016/11/28/connect-your-docker-client-to-a-remote-docker-host/
  2. https://askubuntu.com/questions/192050/how-to-run-sudo-command-with-no-password

Basically create a new rsa key to use with the SSH tunnel using ssh-keygen -t rsa and then copy it to the remote host by ssh-copy-id {username}@{host}.

Edit the /etc/sudoers as specified in the second tutorial.

When this is done, you now can access the remote docker daemon by the generic driver or the fluent API specified above. To do the same thing manually as specified in the example it would look something like this.

C:\Users\martoffi>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

Now the registry entry is created, it is possible set the environment for the terminal docker.

C:\Users\martoffi>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:\Users\martoffi\.docker\machine\machines\remote-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

Run this to make docker client use the remote docker daemon.

@FOR /f "tokens=*" %i IN ('docker-machine.exe env remote-daemon') DO @%i

All commands using the docker binary will now execute on the remote docker daemon.

Hyper-V

When creating and querying, via machine, a hyper-v docker machine the process needs to be elevated since Hyper-V will not respond to API calls in standard user mode.

Misc

Health Check

It is possible to specify a health check for the docker container to report the state of the container based on such activity. The following example is using a health check that the container has exited or not. It is possible to check the configuration (make sure to force refresh) what status the health check is reporting.

using(varcontainer=Fd.UseContainer().UseImage("postgres:latest",force:true).HealthCheck("exit").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start()){varconfig=container.GetConfiguration(true);AreEqual(HealthState.Starting,config.State.Health.Status);}

ulimit

It is possible via the Fluent API and ContainerCreateParams specify ulimit to the docker container to e.g. limit the number of open files etc. For example using the Fluent API could look like this when restricting the number of open files to 2048 (both soft and hard).

using(varcontainer=Fd.UseContainer().UseImage("postgres:latest",force:true).UseUlimit(Ulimit.NoFile,2048,2048).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start()){// Do stuff}

Test Support

This repo contains three nuget packages, one for the fluent access, one for ms-test base classes and another for xunit base classes to be used while testing. For example in a unit-test it is possible to fire up a postgres container and wait when the the db has booted.

XUnit

publicclassPostgresXUnitTests:IClassFixture<PostgresTestBase>{[Fact]publicvoidTest(){// We now have a running postgres// and a valid connection string to use.}}

MSTest

using(varcontainer=newDockerBuilder().WithImage("kiasaki/alpine-postgres").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").ExposePorts("5432").WaitForPort("5432/tcp",30000/*30s*/).Build()){container.Create().Start();}

It is also possible to re-use abstract base classes, for example postgres test base to simplify and make clean unittest towards a container.

[TestClass]publicclassPostgresMsTests:PostgresTestBase{[TestMethod]publicvoidTest(){// We now have a running postgres// and a valid connection string to use.}}

The FluentDockerTestBase allows for simple overrides to do whatever custom docker backed test easily. Just create a test class and derive from the FluentDockerTestBase and override suitable methods. For example.

protectedoverrideDockerBuilderBuild(){returnnewDockerBuilder().WithImage("kiasaki/alpine-postgres").WithEnvironment($"POSTGRES_PASSWORD={PostgresPassword}").ExposePorts("5432").WaitForPort("5432/tcp",30000/*30s*/);}

This will create a builder with docker image postgres:latest and set one environment string, it will also expose the postgres db port 5432 to the host so one can connect to the db within the container. Lastly it will wait for the port 5432. This ensures that the db is running and have properly booted. If timeout, in this example set to 30 seconds, it will throw an exception and the container is stopped and removed. Note that the host port is not 5432! Use Container.GetHostPort("5432/tcp") to get the host port. The host ip can be retrieved by Container.Host property and thus shall be used when communicating with the app in the container.

If a callback is needed when the container has been successfully pulled, created, and started.

protectedoverridevoidOnContainerInitialized(){ConnectionString=string.Format(PostgresConnectionString,Container.Host,Container.GetHostPort("5432/tcp"),PostgresUser,PostgresPassword,PostgresDb);}

This example renders a proper connection string to the postgresql db within the newly spun up container. This can be used to connect using Npgsql, EF7, NHibernate, Marten or other compatible tools. This method will not be called if pulling of the image from the docker repository or it could not create/start the container.

If a before shutdown container hook is wanted override.

protectedvirtualvoidOnContainerTearDown(){// Do stuff before container is shut down.}

Note that if un-named container, if not properly disposed, the docker container will still run and must be manually removed. This is a feature not a bug since you might want several containers running in your test. The DockerContainer class manages the instance id of the container and thus only interact with it and no other container.

When creating / starting a new container it will first check the local repository if the container image is already present and will download it if not found. This may take some time and there's just a Debug Log if enabled it is possible to monitor the download process.

Miscellanious

Unhandled Exceptions

When a unhandled exception occurs and the application FailFast i.e. terminates quickly it will not invoke finally clause. Thus a failing WaitForPort inside a using statement will not dispose the container service. Therefore the container is is still running. To fix this, either have a global try...catch or inject one locally e.g.

try{using(varcontainer=newBuilder().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;}

But it this is only when application termination is done due to the FluentDockerException thrown in the WaitForPort, otherwise it will dispose the container properly and thus the try...catch is not needed.

This could also be solved using the Fd.Build functions (see Using Builder Extensions for more information).

Using Builder Extensions

The class Fd is a static class that provides convenience methods for building and running single and composed containers. To build a container just use:

varbuild=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 ofvarbuild=newBuilder().UseContainer().UseImage("postgres:9.6-alpine").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",TimeSpan.FromSeconds(30));

This can then be used to start the containers within a safeusing clause that is guaranteed to be disposed even if uncaught exception.

build.Container(svc =>{varconfig=svc.GetConfiguration();// Do stuff...});

After the Container method has been executed the container is in this case stopped and removed. This is the quivalent of

// This is equivalent oftry{using(varsvc=build.Build()){svc.Start();varconfig=svc.GetConfiguration();// Do stuff... }}catch{Log(...);throw;}

It is also possible to combine builder and running e.g. via:

Fd.Container(c =>c.UseContainer().UseImage("postgres:9.6-alpine").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",TimeSpan.FromSeconds(30)),
svc =>{varconfig=svc.GetConfiguration();// Do stuff...});

The above example will build the container, start, stop, and finally delete the container. Even if and Exception is thrown it will be Disposed. Of course it is possible to use compsed container using composite extension methods as with container.

About

Use docker, docker-compose local and remote in tests and your .NET core/full framework apps via a FluentAPI

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

Quality Gate StatusBuild status

PackageNuGet
FluentDockerNuGet
Microsoft TestNuGet
XUnit TestNuGet

FluentDocker

This library enables docker and docker-compose interactions usinga Fluent API. It is supported on Linux, Windows and Mac. It also has support for the legazy docker-machine interactions.

Sample Fluent API usage

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",30000/*30s*/).Build().Start()){varconfig=container.GetConfiguration(true);Assert.AreEqual(ServiceRunningState.Running,config.State.ToServiceState());}

This fires up a postgres and waits for it to be ready. To use compose, just do it like this:

💡 NOTE: Use the AssumeComposeVersion(ComposeVersion.V2) to use the V2 behaviour, default is still V1 (to be changed to default to V2 later this year)

varfile=Path.Combine(Directory.GetCurrentDirectory(),(TemplateString)"Resources/ComposeTests/WordPress/docker-compose.yml");// @formatter:offusing(varsvc=newBuilder().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 varinstallPage=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 composeAssert.AreEqual(2,svc.Containers.Count);// We can access each individual containerAssert.AreEqual(2,svc.Images.Count);// And the images used.}

:bulb Note for Linux Users: Docker requires sudo by default and the library by default expects that executing user do not need to do sudo in order to talk to the docker daemon. More description can be found in the Talking to Docker Daemon chapter.

The fluent API builds up one or more services. Each service may be composite or singular. Therefore it is possible to e.g. fire up several docker-compose based services and manage each of them as a single service or dig in and use all underlying services on each docker-compose service. It is also possible to use services directly e.g.

varfile=Path.Combine(Directory.GetCurrentDirectory(),(TemplateString)"Resources/ComposeTests/WordPress/docker-compose.yml");using(varsvc=newDockerComposeCompositeService(DockerHost,newDockerComposeConfig{ComposeFilePath=newList<string>{file},ForceRecreate=true,RemoveOrphans=true,StopOnDispose=true})){svc.Start();// We now have a running WordPress with a MySql databasevarinstallPage=await$"http://localhost:8000/wp-admin/install.php".Wget();Assert.IsTrue(installPage.IndexOf("https://wordpress.org/",StringComparison.Ordinal)!=-1);}

The above example creates a docker-compose service from a single compose file. When the service is disposed all underlying services is automatically stopped.

The library is supported by .NET full 4.51 framework and higher, .NET standard 1.6, 2.0. It is divided into three thin layers, each layer is accessible:

  1. Docker Binaries interactions - Static commands and docker environment
  2. Services - thin service layer to manage machines, containers etc.
  3. Fluent API - API to build/discover services to be used

The Majority of the service methods are extension methods and not hardwired into the service itself, making them lightweight and customizable. Since everything is accessible it is e.g. easy to add extensions method for a service that uses the layer 1 commands to provide functionality.

Contribution

I do welcome contribution, though there is no contribution guideline as of yet, make sure to adhere to .editorconfig when doing the Pull Requests. Otherwise the build will fail. I'll update with a real guideline sooner or later this year.

Basic Usage of Commands (Layer 1)

All commands needs a DockerUri to work with. It is the Uri to the docker daemon, either locally or remote. It can be discoverable or hardcoded. Discovery of local DockerUri can be done by

varhosts=newHosts().Discover();var_docker=hosts.FirstOrDefault(x =>x.IsNative)??hosts.FirstOrDefault(x =>x.Name=="default");

The example snipped will check for native, or docker beta "native" hosts, if not choose the docker-machine "default" as host. If you're using docker-machine and no machine exists or is not started it is easy to create / start a docker-machine by e.g. "test-machine".Create(1024,20000000,1). This will create a docker machine named "test-machine" with 1GB of RAM, 20GB Disk, and use one CPU.

It is now possible to use the Uri to communicate using the commands. For example to get the version of client and server docker binaries:

varresult=_docker.Host.Version(_docker.Certificates);Debug.WriteLine(result.Data);// Will Print the Client and Server Version and API Versions respectively.

All commands return a CommandResponse such that it is possible to check success factor by response.Success. If any data associated with the command it is returned in the response.Data property.

Then it is simple as below to start and stop include delete a container using the commands. Below starts a container and do a PS on it and then deletes it.

varid=_docker.Host.Run("nginx:latest",null,_docker.Certificates).Data;varps=_docker.Host.Ps(null,_docker.Certificates).Data;_docker.Host.RemoveContainer(id,true,true,null,_docker.Certificates);

When running on windows, one can choose to run linux or windows container. Use the LinuxDaemon or WindowsDaemon to control which daemon to talk to.

_docker.LinuxDaemon();// ensures that it will talk to linux daemon, if windows daemon it will switch

Some commands returns a stream of data when e.g. events or logs is wanted using a continuous stream. Streams can be used in background tasks and support CancellationToken. Below example tails a log.

using(varlogs=_docker.Host.Logs(id,_docker.Certificates)){while(!logs.IsFinished){varline=logs.TryRead(5000);// Do a read with timeoutif(null==line){break;}Debug.WriteLine(line);}}

Utility methods exists for commands. They come in different flaviours such as networking etc. For example when reading a log to the end:

using(varlogs=_docker.Host.Logs(id,_docker.Certificates)){foreach(varlineinlogs.ReadToEnd()){Debug.WriteLine(line);}}

Using Fluent API

The highest layer of this library is the fluent API where you can define and control machines, images, and containers. For example to setup a load balancer with two nodejs servers reading from a redis server can look like this (node image is custom built if not found in the repository).

varfullPath=(TemplateString)@"${TEMP}/fluentdockertest/${RND}";varnginx=Path.Combine(fullPath,"nginx.conf");Directory.CreateDirectory(fullPath);typeof(NsResolver).ResourceExtract(fullPath,"index.js");using(varservices=newBuilder()// Define custom node image to be used.DefineImage("mariotoffia/nodetest").ReuseIfAlreadyExists().From("ubuntu").Maintainer("Mario Toffia <mario.toffia@xyz.com>").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);varep=services.Containers.First(x =>x.Name=="nginx").ToHostExposedEndpoint("80/tcp");Assert.IsNotNull(ep);varround1=$"http://{ep.Address}:{ep.Port}".Wget();Assert.AreEqual("This page has been viewed 1 times!",round1);varround2=$"http://{ep.Address}:{ep.Port}".Wget();Assert.AreEqual("This page has been viewed 2 times!",round2);}

The above example defines a Dockerfile, builds it, for the node image. It then uses vanilla redis and nginx. If you just want to use an existing Dockerfile it can be done like this.

using(varservices=newBuilder().DefineImage("mariotoffia/nodetest").ReuseIfAlreadyExists().FromFile("/tmp/Dockerfile").Build().Start()){// Container either build to reused if found in registry and started here.}

The fluent API supports from defining a docker-machine to a set of docker instances. It has built-in support for e.g. waiting for a specific port or a process within the container before Build() completes and thus can be safely be used within a using statement. If specific management on wait timeouts etc. you can always build and start the container and use extension methods to do the waiting on the container itself.

To create a container just omit the start. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build()){Assert.AreEqual(ServiceRunningState.Stopped,container.State);}

This example creates a container with postgres, configure one environment variable. Within the using statement it is possible to start the IContainerService. Thus each built container is wrapped in a IContainerService. It is also possible to use the IHostService.GetContainers(...) to obtain the created, running, and exited containers. From the IHostService it is also possible to get all the images in the local repository to create containers from.

Whe you want to run a single container do use the fluent or container service start method. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start()){varconfig=container.GetConfiguration();Assert.AreEqual(ServiceRunningState.Running,container.State);Assert.IsTrue(config.Config.Env.Any(x =>x=="POSTGRES_PASSWORD=mysecretpassword"));}

By default the container is stopped and deleted when the Dispose method is run, in order to keep the container in archve, use the KeepContainer() on the fluent API. When Dispose() is invoked it will be stopped but not deleted. It is also possible to keep it running after dispose as well.

Working with ports

It is possible to expose ports both explicit or randomly. Either way it is possible to resolve the IP (in case of machine) and the port (in case of random port) to use in code. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(40001,5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start()){varendpoint=container.ToHostExposedEndpoint("5432/tcp");Assert.AreEqual(40001,endpoint.Port);}

Here we map the container port 5432 to host port 40001 explicitly. Note the use of container.ToHostExposedEndpoint(...). This is to always resolve to a working ip and port to communicate with the docker container. It is also possible to map a random port, i.e. let Docker choose a available port. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start()){varendpoint=container.ToHostExposedEndpoint("5432/tcp");Assert.AreNotEqual(0,endpoint.Port);}

The only difference here is that only one argument is used when ExposePort(...) was used to configure the container. The same usage applies otherwise and thus is transparent for the code.

In order to know when a certain service is up and running before starting to e.g. connect to it. It is possible to wait for a specific port to be open. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",30000/*30s*/).Build().Start()){varconfig=container.GetConfiguration(true);Assert.AreEqual(ServiceRunningState.Running,config.State.ToServiceState());}

In the above example we wait for the container port 5432 to be opened within 30 seconds. If it fails, it will throw an exception and thus the container will be disposed and removed (since we dont have any keep container etc. configuration).

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",30000/*30s*/,"127.0.0.1").Build().Start()){varconfig=container.GetConfiguration(true);Assert.AreEqual(ServiceRunningState.Running,config.State.ToServiceState());}

Sometimes it is not possible to directly reach the container, by local ip and port, instead e.g. the container has an exposed port on the loopback interface (127.0.0.1) and that is the only way of reaching the container from the program. The above example forces the address to be 127.0.0.1 but still resolves the host port. By default, FluentDocker uses the network inspect on the container to determine the network configuration.

Sometime it is not sufficient to just wait for a port. Sometimes a container process is much more vital to wait for. Therefore a wait for process method exist in the fluent API as well as an extension method on the container object. For example:

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForProcess("postgres",30000/*30s*/).Build().Start()){varconfig=container.GetConfiguration(true);Assert.AreEqual(ServiceRunningState.Running,config.State.ToServiceState());}

In the above example Build() will return control when the process "postgres" have been started within the container.

Filesystem & Files

In order to make use of containers, sometimes it is necessary to mount volumes in the container onto the host or just copy from or to the container. Depending on if you're running machine or docker natively volume mapping have the constraint that it must be reachable from the virtual machine.

A normal use case is to have e.g. a webserver serving content on a docker container and the user edits files on the host file system. In such scenario it is necessary to mount a docker container volume onto the host. For example:

conststringhtml="<html><head>Hello World</head><body><h1>Hello world</h1></body></html>";varhostPath=(TemplateString)@"${TEMP}/fluentdockertest/${RND}";Directory.CreateDirectory(hostPath);using(varcontainer=newBuilder().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);varresponse=$"http://{container.ToHostExposedEndpoint("80/tcp")}/hello.html".Wget();Assert.AreEqual(html,response);}

In the above example a nginx container is started and mounts '/usr/share/nginx/html' onto a (random, in temp directory) host path. A HTML file is copied into the host path and when a HTTP get towards the nginx docker container is done, that same file is served.

Sometimes it is necessary copy files to and from a container. For example copy a configuration file, configure it and copy it back. More common scenario is to copy a configuration file to the container, just before it is started. The multi container example copies a nginx configuration file just before it is started. Thus it is possible to avoid manually creating a Dockerfile and a image for such a simple task. Instead just use e.g. an official or custom image, copy configuration and run.

using(newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start().CopyFrom("/etc/conf.d",fullPath)){varfiles=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")));}

Above example copies a directory to a host path (fullPath) from a running container. Note the use of extension method here, thus not using the fluent API (since CopyFrom is after Start()). If you want to copy files from the container just before starting use the Fluent API instead.

using(newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").CopyOnStart("/etc/conf.d",fullPath).Build().Start()){}

The below example illustrates a much more common scenario where files are copied to the container. This example makes use of the extension method instead of the fluent API version. It takes a Diff snapshot before copy and then just after the copy. In the latter the hello.html is present.

using(varcontainer=newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start().WaitForProcess("postgres",30000/*30s*/).Diff(outbefore).CopyTo("/bin",fullPath)){varafter=container.Diff();Assert.IsFalse(before.Any(x =>x.Item=="/bin/hello.html"));Assert.IsTrue(after.Any(x =>x.Item=="/bin/hello.html"));}

Sometime is it useful to copy files in the IContainerService.Dispose() (just before container has stopped). Therefore a fluent API exists to ensure that it will just do that.

using(newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").CopyOnDispose("/etc/conf.d",fullPath).Build().Start()){}

In order to analyze a container, export extension method and fluent API methods exists. Most notably is the possibility to export a container when a IContainerService is disposed.

using(newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").ExportOnDispose(fullPath).Build().Start()){}

This will produce a container export (tar file) on the host (fullPath). If you rather have it exploded (un-tared) use the ExportExplodedOnDispose method instead. Of course you can export the container any time using a extension method on the container.

A useful trick when it comes to unit-testing is to export the container state when the unit test fails for some reason, therefore it exists a Fluent API that will export when a certain Lambda condition is met. For example:

varfailure=false;using(newBuilder().UseContainer().UseImage("kiasaki/alpine-postgres").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").ExportOnDispose(fullPath, svc =>failure).Build().Start()){failure=true;}

This snippet will export the container when the using statement is disposing the container since the failure variable is set to true and is used in the ExportOnDispose expression.

IService Hooks

All services can be extended with hooks. In the ExportOnDispose(path, lambda) installs a Hook when the service state is set to execute the lambda when the state is Removing. It is possible to install and remove hooks on the fly. If multiple hooks are registered on same service instance, with same ServiceRunningState, they will be executed in installation order.

The hooks are particulary good if you want something to be executed when a state is about to be set (or executed) on the service such as Starting. The Fluent API makes use of those in some situations such as Copy files, export, etc.

Docker Networking

FluentDocker do support all docker network commands. It can discover networks by _docker.NetworkLs() where it discovers all networks and some simple parameters defined in NetworkRow. It can also inspect to gain deeper information about the network (such as which containers is in the network and Ipam configuration) by _docker.NetworkInspect(network:"networkId").

In order to create a new network, use the _docker.NetworkCreate("name_of_network"). It is also possible to supply NetworkCreateParams where everything can be customized such as creating a overlay network och change the Ipam configuration. To delete a network, just use the _docker.NetworkRm(network:"networkId").

Note that networks are not deleted if there are any containers attached to it!

When a network is created it is possible to put one or more containers into it using the _docker.NetworkConnect("containerId","networkId"). Note that containers may be in several networks at a time, thus can proxy request between isolated networks. To disconnect a container from a network, simple do a _docker.NetworkDisconnect("containerId","networkId").

The following sample runs a container, creates a new network, and connects the running container into the network. It then disconnect the container, delete it, and delete the network.

varcmd=_docker.Run("postgres:9.6-alpine",newContainerCreateParams{PortMappings=new[]{"40001:5432"},Environment=new[]{"POSTGRES_PASSWORD=mysecretpassword"}},_certificates);varcontainer=cmd.Data;varnetwork=string.Empty;varcreated=_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 Networking

It is also possible to use a fluent builder to build new or reuse existing docker networks. Those can then be referenced while building containers. It is possible to build more than one docker network and attach a container to more than one network at a time.

using(varnw=newBuilder().UseNetwork("test-network")){using(varcontainer=newDockerBuilder().WithImage("kiasaki/alpine-postgres").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").ExposePorts("5432").UseNetwork(nw).WaitForPort("5432/tcp",30000/*30s*/).Build()){container.Create().Start();}}

The above code snippet creates a new network called test-network and then creates a container that is attached to the test-network. When the Dispose() is called on nw it will remove the network. It is also possible to do static IP container assignments within the network by UseIpV4 or UseIpV6. For example:

using(varnw=Fd.UseNetwork("unit-test-nw").UseSubnet("10.18.0.0/16").Build()){using(varcontainer=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()){varip=container.GetConfiguration().NetworkSettings.Networks["unit-test-nw"].IPAddress;Assert.AreEqual("10.18.0.22",ip);}}

The above example creates a new network unit-test-nw with ip-range 10.18.0.0/16. It is the used in the new container. The IP for the container is set to 10.18.0.22 and is static due to UseIpV4 command.

Volume Support

FluentDocker supports docker volume management both from commands and from a fluent API. Therefore it is possible to have total control on volumes used in container such if it shall be disposed, reused, what driver to use etc.

varvolume=_docker.VolumeCreate("test-volume","local",opts:{{"type","nfs"},{"o=addr","192.168.1.1,rw"},{"device",":/path/to/dir"}});varcfg=_docker.VolumeInspect(_certificates,"test-volume");_docker.VolumeRm(force:true,id:"test-volume");

The above snippet creates a new volume with name test-volume and is of NFS type. It then inspects the just created volume and lastly force delete the volume.

Fluent Volume API

It is also possible to use a fluent API to create or use volumes. They can then be used when building a container. This is especially useful when creation of volumes are special or lifetime needs to be controlled.

using(varvol=newBuilder().UseVolume("test-volume").RemoveOnDispose().Build()){using(varcontainer=newBuilder().UseContainer().UseImage("postgres:9.6-alpine").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").MountVolume(vol,"/var/lib/postgresql/data",MountType.ReadWrite).Build().Start()){varconfig=container.GetConfiguration();Assert.AreEqual(1,config.Mounts.Length);Assert.AreEqual("test-volume",config.Mounts[0].Name);}}

The above sample creates a new volume called test-volume and it is scheduled to be delete when Dispose() is invoked on the IVolumeService. The container is created and mounts the newly created volume to /var/lib/postgresql/data as read/write access mode. Since the container is within the scope of the using statement of the volume it's lifetime spans the whole container lifetime and then gets deleted.

Events

FluentDocker supports, connecting to the docker event mechanism to listen to the events it sends.

using(varevents=Fd.Native().Events()){using(varcontainer=newBuilder().UseContainer().UseImage("postgres:9.6-alpine").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",30000/*30s*/).Build().Start()){FdEvente;while((e=events.TryRead(3000))!=null){if(e.Type==EventType.Container&&e.Action==EventAction.Start)break;}}}

Event listener is global and may handle many EventAction types.

For example

  • Pull
  • Create
  • Start
  • Kill
  • Die
  • Connect
  • Disconnect
  • Stop
  • Destroy

Depending on action, the event type may differ such as ContainerKillEvent for EventAction.Kill. All events derive from FdEvent. That means all shared properties is in the base event and the explicit ones are in the derived.

For example, the ´ContainerKillEvent` contains the following properties:

publicsealedclassContainerKillActor:EventActor{/// <summary>/// The image name and label such as "alpine:latest"./// </summary>publicstringImage{get;set;}/// <summary>/// Name of the container./// </summary>publicstringName{get;set;}/// <summary>/// The signal that the container has been signalled./// </summary>publicstringSignal{get;set;}}

This event loop may be used to pick up events and drive your instantiated IService instances. Or if you need to react to e.g. a network is added or deleted.

Logging

In the full framework it uses verbose logging using the System.Diagnostics.Debugger.Log. For .net core it uses the standard Microsoft.Extensions.Logging.ILog to log. Both are using the category Ductus.FluentDocker and therefore may be configured to participate in logging or not and configure different logging destinations.

In .net core you can provide the logging segment in the application config file.

{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Ductus.FluentDocker": "None"
}
}
}

Please check the https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging/?view=aspnetcore-2.1 for more information. For full framework please check out the XML needed in the appconfig for the full framework described in https://docs.microsoft.com/en-us/dotnet/framework/wcf/diagnostics/tracing/configuring-tracing.

There's a quick way of disabling / enabling logging via (Ductus.FluentDocker.Services) Logging.Enabled() or Logging.Disabled(). This will forcefully enable / disable logging.

Custom IPEndpoint Resolvers

It is possible to override the default mechanism of FluentDocker resolves the container IP from the clients perspective in e.g. WaitForPort. This can be overridden on ContainerBuilder basis.

The below sample, overrides the default behaviour. When it returns null the default resolver kicks in.

using(varcontainer=Fd.UseContainer().UseImage("postgres:9.6-alpine").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").ExposePort(5432).UseCustomResolver((ports,portAndProto,dockerUri)=>{if(null==ports||string.IsNullOrEmpty(portAndProto))returnnull;if(!ports.TryGetValue(portAndProto,outvarendpoints))returnnull;if(null==endpoints||endpoints.Length==0)returnnull;if(CommandExtensions.IsNative())returnendpoints[0];if(CommandExtensions.IsEmulatedNative())returnCommandExtensions.IsDockerDnsAvailable()?newIPEndPoint(CommandExtensions.EmulatedNativeAddress(),endpoints[0].Port):newIPEndPoint(IPAddress.Loopback,endpoints[0].Port);if(Equals(endpoints[0].Address,IPAddress.Any)&&null!=dockerUri)returnnewIPEndPoint(IPAddress.Parse(dockerUri.Host),endpoints[0].Port);returnendpoints[0];}).WaitForPort("5432/tcp",30000/*30s*/).Build().Start()){varstate=container.GetConfiguration(true/*force*/).State.ToServiceState();Assert.AreEqual(ServiceRunningState.Running,state);}

Talking to custom docker daemon URI without docker-machine

There's limited support to use the FluentAPI to talk to a remote docker daemon without using docker-machine. This is done either by manually creating a instance of a DockerHostService or use FromUri on HostBuilder.

using(varcontainer=Fd.UseHost().FromUri(Settings.DockerUri,isWindowsHost:true).UseContainer().Build()){}

The above sample connects to a custom DockerUri from a setting and is a windows container docker daemon.

  • FromUri that uses a DockerUri to create a IHostService. This uri is arbitrary. It also support other properties (see below).
publicHostBuilderFromUri(DockerUriuri,stringname=null,boolisNative=true,boolstopWhenDisposed=false,boolisWindowsHost=false,stringcertificatePath=null){/*...*/}

It will use "sensible" defaults on all parameters. Most of the case the uri is sufficient. For example if not providing the certificatePath it will try to get it from the environment DOCKER_CERT_PATH. If not found in the environment, it will default to none.

  • UseHost that takes a instantiated IHostService implementation.

Docker Compose Support

The library support docker-compose to use existing compose files to render services and manage lifetime of such.

The following sample will do have compose file that fires up a MySql and a WordPress. Therefore the single compose service will have two container services below it. By default, it will stop the services and clean up when Dispose() is invoked. This can be overridden by KeepContainers() in the fluent configuration.

version: '3.3'services:
db:
image: mysql:5.7volumes:
- db_data:/var/lib/mysqlrestart: alwaysenvironment:
MYSQL_ROOT_PASSWORD: somewordpressMYSQL_DATABASE: wordpressMYSQL_USER: wordpressMYSQL_PASSWORD: wordpresswordpress:
depends_on:
- dbimage: wordpress:latestports:
- "8000:80"restart: alwaysenvironment:
WORDPRESS_DB_HOST: db:3306WORDPRESS_DB_USER: wordpressWORDPRESS_DB_PASSWORD: wordpressvolumes:
db_data:

The above file is the docker-compose file to stitch up the complete service.

varfile=Path.Combine(Directory.GetCurrentDirectory(),(TemplateString)"Resources/ComposeTests/WordPress/docker-compose.yml");using(varsvc=newBuilder().UseContainer().UseCompose().FromFile(file).RemoveOrphans().Build().Start()){varinstallPage=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);}

The above snippet is fluently configuring the docker-compose service and invokes the install page to verify that WordPress is indeed working.

It is also possible to do all the operations that a single container supports such as copy on, export, wait operations. For example:

varfile=Path.Combine(Directory.GetCurrentDirectory(),(TemplateString)"Resources/ComposeTests/WordPress/docker-compose.yml");// @formatter:offusing(newBuilder().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. varinstallPage=await"http://localhost:8000/wp-admin/install.php".Wget();Assert.IsTrue(installPage.IndexOf("https://wordpress.org/",StringComparison.Ordinal)!=-1);}

The above snippet fires up the wordpress docker compose project and checks the URLhttp://localhost:8000/wp-admin/install.php it it returns a certain value in the body (in this case "https://wordpress.org/"). If not it returns 500 and the WaitForHttp function will wait 500 milliseconds before invoking again. This works for any custom lambda as well, just use WaitFor instead. Thus it is possible to e.g. query a database before continuing inside the using scope.

Talking to Docker Daemon

For Linux and Mac users there are several options how to authenticate towards the socket. FluentDocker supports no sudo, sudo without any password (user added as NOPASSWD in /etc/sudoer), or sudo with password. The default is that FluentDocker expects to be able to talk without any sudo. The options ar global but can be changed in runtime.

SudoMechanism.None.SetSudo();// This is the defaultSudoMechanism.Password.SetSudo("<my-sudo-password>");SudoMechanism.NoPassword.SetSudo();

If you wish to turn off sudo to communicate with the docker daemon, you can follow a docker tutorial and do the last step of adding your user to docker group.

Connecting to Remote Docker Daemons

FluentDocker supports connection to remote docker daemons. The fluent API supports e.g.

newBuilder().UseHost().UseMachine().WithName("remote-daemon")

where this requires a already pre-setup entry in the docker-machine registry. It is also possible to define SSH based docker-machine registry entires to connect to remote daemon.

using(varcontainer=newBuilder().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);}

This example will create a new docker-machine registry entry named remote-daemon that uses SSH with ip address of 192.168.1.27 and the SSH user solo. If a entry is already found named remote-daemon it will just reuse this entry. Then it gets a IHostService with correct certificates and URL for the remote daemon. Thus, it is possible then to create a docker container on the remote daemon, in thus case it is the postgres image. When it disposes the container, as usual it deletes it from the remote docker. The IHostService do make sure to pick upp all necessary certificates in order to authenticate the connection.

The above example produces this docker-machine registry entry.

C:\Users\martoffi>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

In order to use UseSsh(...) a SSH tunnel with no password must been set up. In addition the user that uses the tunnel must be allowed to access the docker daemon.

Follow these tutorial how to setup the SSH tunnel and make sure the user can access the docker daemon.

  1. https://www.kevinkuszyk.com/2016/11/28/connect-your-docker-client-to-a-remote-docker-host/
  2. https://askubuntu.com/questions/192050/how-to-run-sudo-command-with-no-password

Basically create a new rsa key to use with the SSH tunnel using ssh-keygen -t rsa and then copy it to the remote host by ssh-copy-id {username}@{host}.

Edit the /etc/sudoers as specified in the second tutorial.

When this is done, you now can access the remote docker daemon by the generic driver or the fluent API specified above. To do the same thing manually as specified in the example it would look something like this.

C:\Users\martoffi>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

Now the registry entry is created, it is possible set the environment for the terminal docker.

C:\Users\martoffi>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:\Users\martoffi\.docker\machine\machines\remote-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

Run this to make docker client use the remote docker daemon.

@FOR /f "tokens=*" %i IN ('docker-machine.exe env remote-daemon') DO @%i

All commands using the docker binary will now execute on the remote docker daemon.

Hyper-V

When creating and querying, via machine, a hyper-v docker machine the process needs to be elevated since Hyper-V will not respond to API calls in standard user mode.

Misc

Health Check

It is possible to specify a health check for the docker container to report the state of the container based on such activity. The following example is using a health check that the container has exited or not. It is possible to check the configuration (make sure to force refresh) what status the health check is reporting.

using(varcontainer=Fd.UseContainer().UseImage("postgres:latest",force:true).HealthCheck("exit").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start()){varconfig=container.GetConfiguration(true);AreEqual(HealthState.Starting,config.State.Health.Status);}

ulimit

It is possible via the Fluent API and ContainerCreateParams specify ulimit to the docker container to e.g. limit the number of open files etc. For example using the Fluent API could look like this when restricting the number of open files to 2048 (both soft and hard).

using(varcontainer=Fd.UseContainer().UseImage("postgres:latest",force:true).UseUlimit(Ulimit.NoFile,2048,2048).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").Build().Start()){// Do stuff}

Test Support

This repo contains three nuget packages, one for the fluent access, one for ms-test base classes and another for xunit base classes to be used while testing. For example in a unit-test it is possible to fire up a postgres container and wait when the the db has booted.

XUnit

publicclassPostgresXUnitTests:IClassFixture<PostgresTestBase>{[Fact]publicvoidTest(){// We now have a running postgres// and a valid connection string to use.}}

MSTest

using(varcontainer=newDockerBuilder().WithImage("kiasaki/alpine-postgres").WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").ExposePorts("5432").WaitForPort("5432/tcp",30000/*30s*/).Build()){container.Create().Start();}

It is also possible to re-use abstract base classes, for example postgres test base to simplify and make clean unittest towards a container.

[TestClass]publicclassPostgresMsTests:PostgresTestBase{[TestMethod]publicvoidTest(){// We now have a running postgres// and a valid connection string to use.}}

The FluentDockerTestBase allows for simple overrides to do whatever custom docker backed test easily. Just create a test class and derive from the FluentDockerTestBase and override suitable methods. For example.

protectedoverrideDockerBuilderBuild(){returnnewDockerBuilder().WithImage("kiasaki/alpine-postgres").WithEnvironment($"POSTGRES_PASSWORD={PostgresPassword}").ExposePorts("5432").WaitForPort("5432/tcp",30000/*30s*/);}

This will create a builder with docker image postgres:latest and set one environment string, it will also expose the postgres db port 5432 to the host so one can connect to the db within the container. Lastly it will wait for the port 5432. This ensures that the db is running and have properly booted. If timeout, in this example set to 30 seconds, it will throw an exception and the container is stopped and removed. Note that the host port is not 5432! Use Container.GetHostPort("5432/tcp") to get the host port. The host ip can be retrieved by Container.Host property and thus shall be used when communicating with the app in the container.

If a callback is needed when the container has been successfully pulled, created, and started.

protectedoverridevoidOnContainerInitialized(){ConnectionString=string.Format(PostgresConnectionString,Container.Host,Container.GetHostPort("5432/tcp"),PostgresUser,PostgresPassword,PostgresDb);}

This example renders a proper connection string to the postgresql db within the newly spun up container. This can be used to connect using Npgsql, EF7, NHibernate, Marten or other compatible tools. This method will not be called if pulling of the image from the docker repository or it could not create/start the container.

If a before shutdown container hook is wanted override.

protectedvirtualvoidOnContainerTearDown(){// Do stuff before container is shut down.}

Note that if un-named container, if not properly disposed, the docker container will still run and must be manually removed. This is a feature not a bug since you might want several containers running in your test. The DockerContainer class manages the instance id of the container and thus only interact with it and no other container.

When creating / starting a new container it will first check the local repository if the container image is already present and will download it if not found. This may take some time and there's just a Debug Log if enabled it is possible to monitor the download process.

Miscellanious

Unhandled Exceptions

When a unhandled exception occurs and the application FailFast i.e. terminates quickly it will not invoke finally clause. Thus a failing WaitForPort inside a using statement will not dispose the container service. Therefore the container is is still running. To fix this, either have a global try...catch or inject one locally e.g.

try{using(varcontainer=newBuilder().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;}

But it this is only when application termination is done due to the FluentDockerException thrown in the WaitForPort, otherwise it will dispose the container properly and thus the try...catch is not needed.

This could also be solved using the Fd.Build functions (see Using Builder Extensions for more information).

Using Builder Extensions

The class Fd is a static class that provides convenience methods for building and running single and composed containers. To build a container just use:

varbuild=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 ofvarbuild=newBuilder().UseContainer().UseImage("postgres:9.6-alpine").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",TimeSpan.FromSeconds(30));

This can then be used to start the containers within a safeusing clause that is guaranteed to be disposed even if uncaught exception.

build.Container(svc =>{varconfig=svc.GetConfiguration();// Do stuff...});

After the Container method has been executed the container is in this case stopped and removed. This is the quivalent of

// This is equivalent oftry{using(varsvc=build.Build()){svc.Start();varconfig=svc.GetConfiguration();// Do stuff... }}catch{Log(...);throw;}

It is also possible to combine builder and running e.g. via:

Fd.Container(c =>c.UseContainer().UseImage("postgres:9.6-alpine").ExposePort(5432).WithEnvironment("POSTGRES_PASSWORD=mysecretpassword").WaitForPort("5432/tcp",TimeSpan.FromSeconds(30)),
svc =>{varconfig=svc.GetConfiguration();// Do stuff...});

The above example will build the container, start, stop, and finally delete the container. Even if and Exception is thrown it will be Disposed. Of course it is possible to use compsed container using composite extension methods as with container.

About

Use docker, docker-compose local and remote in tests and your .NET core/full framework apps via a FluentAPI

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages