Pacote | NuGet |
---|---|
FluenteDocker | |
Teste da Microsoft | |
Teste XUnit |
Esta biblioteca permite interações docker
e docker-compose
usando uma API Fluent . É compatível com Linux, Windows e Mac. Ele também tem suporte para interações legadas entre docker-machine
.
Exemplo de uso da API Fluent
using (
var container =
new Builder ( ) . UseContainer ( )
. UseImage ( "kiasaki/alpine-postgres" )
. ExposePort ( 5432 )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. WaitForPort ( "5432/tcp" , 30000 /*30s*/ )
. Build ( )
. Start ( ) )
{
var config = container . GetConfiguration ( true ) ;
Assert . AreEqual ( ServiceRunningState . Running , config . State . ToServiceState ( ) ) ;
}
Isso inicia um postgres e espera que esteja pronto. Para usar o compose, basta fazer assim:
NOTA: Use AssumeComposeVersion(ComposeVersion.V2) para usar o comportamento V2, o padrão ainda é V1 (a ser alterado para o padrão V2 ainda este ano)
var file = Path . Combine ( Directory . GetCurrentDirectory ( ) ,
( TemplateString ) "Resources/ComposeTests/WordPress/docker-compose.yml" ) ;
// @formatter:off
using ( var svc = new Builder ( )
. UseContainer ( )
. UseCompose ( )
. FromFile ( file )
. RemoveOrphans ( )
. WaitForHttp ( "wordpress" , "http://localhost:8000/wp-admin/install.php" )
. Build ( ) . Start ( ) )
// @formatter:on
{
// We now have a running WordPress with a MySql database
var installPage = await "http://localhost:8000/wp-admin/install.php" . Wget ( ) ;
Assert . IsTrue ( installPage . IndexOf ( "https://wordpress.org/" , StringComparison . Ordinal ) != - 1 ) ;
Assert . AreEqual ( 1 , svc . Hosts . Count ) ; // The host used by compose
Assert . AreEqual ( 2 , svc . Containers . Count ) ; // We can access each individual container
Assert . AreEqual ( 2 , svc . Images . Count ) ; // And the images used.
}
:bulb Nota para usuários de Linux: o Docker requer sudo por padrão e a biblioteca por padrão espera que o usuário em execução não precise fazer sudo para se comunicar com o daemon do docker. Mais descrições podem ser encontradas no capítulo Talking to Docker Daemon .
A API fluente cria um ou mais serviços. Cada serviço pode ser composto ou singular. Portanto, é possível, por exemplo, iniciar vários serviços baseados no docker-compose e gerenciar cada um deles como um único serviço ou explorar e usar todos os serviços subjacentes em cada serviço do docker-compose . Também é possível usar serviços diretamente, por exemplo
var file = Path . Combine ( Directory . GetCurrentDirectory ( ) ,
( TemplateString ) "Resources/ComposeTests/WordPress/docker-compose.yml" ) ;
using ( var svc = new DockerComposeCompositeService ( DockerHost , new DockerComposeConfig
{
ComposeFilePath = new List < string > { file } , ForceRecreate = true , RemoveOrphans = true ,
StopOnDispose = true
} ) )
{
svc . Start ( ) ;
// We now have a running WordPress with a MySql database
var installPage = await $ "http://localhost:8000/wp-admin/install.php" . Wget ( ) ;
Assert . IsTrue ( installPage . IndexOf ( "https://wordpress.org/" , StringComparison . Ordinal ) != - 1 ) ;
}
O exemplo acima cria um serviço docker-compose a partir de um único arquivo de composição. Quando o serviço é descartado, todos os serviços subjacentes são automaticamente interrompidos.
A biblioteca é suportada pela estrutura .NET full 4.51 e superior, padrão .NET 1.6, 2.0. Está dividido em três camadas finas, cada camada é acessível:
A maioria dos métodos de serviço são métodos de extensão e não estão conectados ao próprio serviço, o que os torna leves e personalizáveis. Como tudo é acessível, é fácil, por exemplo, adicionar o método de extensões para um serviço que usa os comandos da camada 1 para fornecer funcionalidade.
Agradeço contribuições, embora não haja nenhuma diretriz de contribuição ainda, certifique-se de aderir ao .editorconfig ao fazer solicitações pull. Caso contrário, a compilação falhará. Atualizarei com uma diretriz real mais cedo ou mais tarde este ano.
Todos os comandos precisam de um DockerUri
para funcionar. É o Uri do daemon do docker, local ou remoto. Pode ser detectável ou codificado. A descoberta do DockerUri
local pode ser feita por
var hosts = new Hosts ( ) . Discover ( ) ;
var _docker = hosts . FirstOrDefault ( x => x . IsNative ) ?? hosts . FirstOrDefault ( x => x . Name == "default" ) ;
O exemplo recortado verificará hosts nativos ou docker beta "nativos", caso contrário, escolha docker-machine "default" como host. Se você estiver usando docker-machine e nenhuma máquina existir ou não for iniciada, é fácil criar/iniciar uma docker-machine, por exemplo, "test-machine".Create(1024,20000000,1)
. Isso criará uma máquina docker chamada "máquina de teste" com 1 GB de RAM, 20 GB de disco e usará uma CPU.
Agora é possível utilizar o Uri para se comunicar através dos comandos. Por exemplo, para obter a versão dos binários do docker do cliente e do servidor:
var result = _docker . Host . Version ( _docker . Certificates ) ;
Debug . WriteLine ( result . Data ) ; // Will Print the Client and Server Version and API Versions respectively.
Todos os comandos retornam um CommandResponse tal que é possível verificar o fator de sucesso por response.Success
. Se houver algum dado associado ao comando, ele será retornado na propriedade response.Data
.
Então é simples como abaixo iniciar e parar, incluindo excluir um contêiner usando os comandos. Abaixo inicia um container e faz um PS nele e depois exclui.
var id = _docker . Host . Run ( "nginx:latest" , null , _docker . Certificates ) . Data ;
var ps = _docker . Host . Ps ( null , _docker . Certificates ) . Data ;
_docker . Host . RemoveContainer ( id , true , true , null , _docker . Certificates ) ;
Ao executar no Windows, pode-se optar por executar o Linux ou o contêiner do Windows. Use LinuxDaemon
ou WindowsDaemon
para controlar com qual daemon conversar.
_docker . LinuxDaemon ( ) ; // ensures that it will talk to linux daemon, if windows daemon it will switch
Alguns comandos retornam um fluxo de dados quando, por exemplo, eventos ou logs são desejados usando um fluxo contínuo. Streams podem ser usados em tarefas em segundo plano e oferecem suporte CancellationToken
. O exemplo abaixo segue um log.
using ( var logs = _docker . Host . Logs ( id , _docker . Certificates ) )
{
while ( ! logs . IsFinished )
{
var line = logs . TryRead ( 5000 ) ; // Do a read with timeout
if ( null == line )
{
break ;
}
Debug . WriteLine ( line ) ;
}
}
Existem métodos utilitários para comandos. Eles vêm em diferentes sabores, como rede, etc. Por exemplo, ao ler um log até o fim:
using ( var logs = _docker . Host . Logs ( id , _docker . Certificates ) )
{
foreach ( var line in logs . ReadToEnd ( ) )
{
Debug . WriteLine ( line ) ;
}
}
A camada mais alta desta biblioteca é a API fluente onde você pode definir e controlar máquinas, imagens e contêineres. Por exemplo, configurar um balanceador de carga com dois servidores nodejs lendo de um servidor redis pode ser assim (a imagem do nó é construída de forma personalizada se não for encontrada no repositório).
var fullPath = ( TemplateString ) @"${TEMP}/fluentdockertest/${RND}" ;
var nginx = Path . Combine ( fullPath , "nginx.conf" ) ;
Directory . CreateDirectory ( fullPath ) ;
typeof ( NsResolver ) . ResourceExtract ( fullPath , "index.js" ) ;
using ( var services = new Builder ( )
// Define custom node image to be used
. DefineImage ( "mariotoffia/nodetest" ) . ReuseIfAlreadyExists ( )
. From ( "ubuntu" )
. Maintainer ( "Mario Toffia <[email protected]>" )
. Run ( "apt-get update &&" ,
"apt-get -y install curl &&" ,
"curl -sL https://deb.nodesource.com/setup | sudo bash - &&" ,
"apt-get -y install python build-essential nodejs" )
. Run ( "npm install -g nodemon" )
. Add ( "emb:Ductus.FluentDockerTest/Ductus.FluentDockerTest.MultiContainerTestFiles/package.txt" ,
"/tmp/package.json" )
. Run ( "cd /tmp && npm install" )
. Run ( "mkdir -p /src && cp -a /tmp/node_modules /src/" )
. UseWorkDir ( "/src" )
. Add ( "index.js" , "/src" )
. ExposePorts ( 8080 )
. Command ( "nodemon" , "/src/index.js" ) . Builder ( )
// Redis Db Backend
. UseContainer ( ) . WithName ( "redis" ) . UseImage ( "redis" ) . Builder ( )
// Node server 1 & 2
. UseContainer ( ) . WithName ( "node1" ) . UseImage ( "mariotoffia/nodetest" ) . Link ( "redis" ) . Builder ( )
. UseContainer ( ) . WithName ( "node2" ) . UseImage ( "mariotoffia/nodetest" ) . Link ( "redis" ) . Builder ( )
// Nginx as load balancer
. UseContainer ( ) . WithName ( "nginx" ) . UseImage ( "nginx" ) . Link ( "node1" , "node2" )
. CopyOnStart ( nginx , "/etc/nginx/nginx.conf" )
. ExposePort ( 80 ) . Builder ( )
. Build ( ) . Start ( ) )
{
Assert . AreEqual ( 4 , services . Containers . Count ) ;
var ep = services . Containers . First ( x => x . Name == "nginx" ) . ToHostExposedEndpoint ( "80/tcp" ) ;
Assert . IsNotNull ( ep ) ;
var round1 = $ "http:// { ep . Address } : { ep . Port } " . Wget ( ) ;
Assert . AreEqual ( "This page has been viewed 1 times!" , round1 ) ;
var round2 = $ "http:// { ep . Address } : { ep . Port } " . Wget ( ) ;
Assert . AreEqual ( "This page has been viewed 2 times!" , round2 ) ;
}
O exemplo acima define um Dockerfile e o constrói para a imagem do nó. Em seguida, ele usa vanilla redis e nginx. Se você quiser apenas usar um Dockerfile existente, isso pode ser feito assim.
using ( var services = new Builder ( )
. DefineImage ( "mariotoffia/nodetest" ) . ReuseIfAlreadyExists ( )
. FromFile ( "/tmp/Dockerfile" )
. Build ( ) . Start ( ) )
{
// Container either build to reused if found in registry and started here.
}
A API fluente suporta desde a definição de uma máquina docker até um conjunto de instâncias do docker. Ele possui suporte integrado para, por exemplo, esperar por uma porta específica ou um processo dentro do contêiner antes que Build()
seja concluído e, portanto, pode ser usado com segurança em uma instrução using. Se houver gerenciamento específico em tempos limite de espera, etc., você sempre poderá construir e iniciar o contêiner e usar métodos de extensão para fazer a espera no próprio contêiner.
Para criar um contêiner basta omitir o início. Por exemplo:
using (
var container =
new Builder ( ) . UseContainer ( )
. UseImage ( "kiasaki/alpine-postgres" )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. Build ( ) )
{
Assert . AreEqual ( ServiceRunningState . Stopped , container . State ) ;
}
Este exemplo cria um contêiner com postgres, configura uma variável de ambiente. Dentro da instrução using é possível iniciar o IContainerService
. Assim, cada contêiner construído é encapsulado em um IContainerService
. Também é possível usar o IHostService.GetContainers(...)
para obter os contêineres criados, em execução e encerrados. A partir do IHostService
também é possível obter todas as imagens do repositório local para criar contêineres.
Quando você deseja executar um único contêiner, use o método de início de serviço fluente ou de contêiner. Por exemplo:
using (
var container =
new Builder ( ) . UseContainer ( )
. UseImage ( "kiasaki/alpine-postgres" )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. Build ( )
. Start ( ) )
{
var config = container . GetConfiguration ( ) ;
Assert . AreEqual ( ServiceRunningState . Running , container . State ) ;
Assert . IsTrue ( config . Config . Env . Any ( x => x == "POSTGRES_PASSWORD=mysecretpassword" ) ) ;
}
Por padrão, o contêiner é interrompido e excluído quando o método Dispose é executado, para manter o contêiner em arquivo, use KeepContainer()
na API fluente. Quando Dispose()
for invocado, ele será interrompido, mas não excluído. Também é possível mantê-lo funcionando após o descarte.
É possível expor portas de forma explícita ou aleatória. De qualquer forma é possível resolver o IP (no caso de máquina) e a porta (no caso de porta aleatória) a utilizar no código. Por exemplo:
using (
var container =
new Builder ( ) . UseContainer ( )
. UseImage ( "kiasaki/alpine-postgres" )
. ExposePort ( 40001 , 5432 )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. Build ( )
. Start ( ) )
{
var endpoint = container . ToHostExposedEndpoint ( "5432/tcp" ) ;
Assert . AreEqual ( 40001 , endpoint . Port ) ;
}
Aqui mapeamos explicitamente a porta do contêiner 5432 para a porta host 40001. Observe o uso de container.ToHostExposedEndpoint(...)
. Isso sempre resolve um IP e uma porta em funcionamento para se comunicar com o contêiner do Docker. Também é possível mapear uma porta aleatória, ou seja, deixar o Docker escolher uma porta disponível. Por exemplo:
using (
var container =
new Builder ( ) . UseContainer ( )
. UseImage ( "kiasaki/alpine-postgres" )
. ExposePort ( 5432 )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. Build ( )
. Start ( ) )
{
var endpoint = container . ToHostExposedEndpoint ( "5432/tcp" ) ;
Assert . AreNotEqual ( 0 , endpoint . Port ) ;
}
A única diferença aqui é que apenas um argumento é usado quando ExposePort(...)
foi usado para configurar o contêiner. O mesmo uso se aplica caso contrário e, portanto, é transparente para o código.
Para saber quando um determinado serviço está instalado e funcionando antes de começar, por exemplo, conectar-se a ele. É possível esperar que uma porta específica seja aberta. Por exemplo:
using (
var container =
new Builder ( ) . UseContainer ( )
. UseImage ( "kiasaki/alpine-postgres" )
. ExposePort ( 5432 )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. WaitForPort ( "5432/tcp" , 30000 /*30s*/ )
. Build ( )
. Start ( ) )
{
var config = container . GetConfiguration ( true ) ;
Assert . AreEqual ( ServiceRunningState . Running , config . State . ToServiceState ( ) ) ;
}
No exemplo acima, esperamos que a porta do contêiner 5432 seja aberta em 30 segundos. Se falhar, lançará uma exceção e assim o container será descartado e removido (já que não temos nenhuma configuração de keep container etc.).
using (
var container =
new Builder ( ) . UseContainer ( )
. UseImage ( "kiasaki/alpine-postgres" )
. ExposePort ( 5432 )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. WaitForPort ( "5432/tcp" , 30000 /*30s*/ , "127.0.0.1" )
. Build ( )
. Start ( ) )
{
var config = container . GetConfiguration ( true ) ;
Assert . AreEqual ( ServiceRunningState . Running , config . State . ToServiceState ( ) ) ;
}
Às vezes não é possível acessar diretamente o contêiner, por IP e porta locais; em vez disso, por exemplo, o contêiner possui uma porta exposta na interface de loopback ( 127.0.0.1 ) e essa é a única maneira de acessar o contêiner a partir do programa. O exemplo acima força o endereço a ser 127.0.0.1 , mas ainda resolve a porta do host. Por padrão, o FluentDocker usa a inspeção de rede no contêiner para determinar a configuração da rede.
Às vezes não é suficiente apenas esperar por um porto. Às vezes, é muito mais vital esperar por um processo de contêiner. Portanto, existe um método de espera por processo na API fluente, bem como um método de extensão no objeto contêiner. Por exemplo:
using (
var container =
new Builder ( ) . UseContainer ( )
. UseImage ( "kiasaki/alpine-postgres" )
. ExposePort ( 5432 )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. WaitForProcess ( "postgres" , 30000 /*30s*/ )
. Build ( )
. Start ( ) )
{
var config = container . GetConfiguration ( true ) ;
Assert . AreEqual ( ServiceRunningState . Running , config . State . ToServiceState ( ) ) ;
}
No exemplo acima, Build()
retornará o controle quando o processo "postgres" for iniciado dentro do contêiner.
Para fazer uso de containers, às vezes é necessário montar volumes do container no host ou apenas copiar de ou para o container. Dependendo se você está executando o mapeamento de volume nativo da máquina ou do docker, há a restrição de que ele deve ser acessível a partir da máquina virtual.
Um caso de uso normal é ter, por exemplo, um servidor web servindo conteúdo em um contêiner docker e o usuário edita arquivos no sistema de arquivos host. Nesse cenário, é necessário montar um volume de contêiner docker no host. Por exemplo:
const string html = "<html><head>Hello World</head><body><h1>Hello world</h1></body></html>" ;
var hostPath = ( TemplateString ) @"${TEMP}/fluentdockertest/${RND}" ;
Directory . CreateDirectory ( hostPath ) ;
using (
var container =
new Builder ( ) . UseContainer ( )
. UseImage ( "nginx:latest" )
. ExposePort ( 80 )
. Mount ( hostPath , "/usr/share/nginx/html" , MountType . ReadOnly )
. Build ( )
. Start ( )
. WaitForPort ( "80/tcp" , 30000 /*30s*/ ) )
{
File . WriteAllText ( Path . Combine ( hostPath , "hello.html" ) , html ) ;
var response = $ "http:// { container . ToHostExposedEndpoint ( "80/tcp" ) } /hello.html" . Wget ( ) ;
Assert . AreEqual ( html , response ) ;
}
No exemplo acima, um contêiner nginx é iniciado e montado '/usr/share/nginx/html' em um caminho de host (aleatório, no diretório temporário). Um arquivo HTML é copiado para o caminho do host e quando um HTTP chega ao contêiner do docker nginx é feito, esse mesmo arquivo é servido.
Às vezes é necessário copiar arquivos de e para um contêiner. Por exemplo, copie um arquivo de configuração, configure-o e copie-o novamente. O cenário mais comum é copiar um arquivo de configuração para o contêiner, pouco antes de ele ser iniciado. O exemplo de vários contêineres copia um arquivo de configuração nginx antes de ser iniciado. Assim é possível evitar a criação manual de um Dockerfile e de uma imagem para uma tarefa tão simples. Em vez disso, basta usar, por exemplo, uma imagem oficial ou personalizada, copiar a configuração e executar.
using ( new Builder ( ) . UseContainer ( )
. UseImage ( "kiasaki/alpine-postgres" )
. ExposePort ( 5432 )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. Build ( )
. Start ( )
. CopyFrom ( "/etc/conf.d" , fullPath ) )
{
var files = Directory . EnumerateFiles ( Path . Combine ( fullPath , "conf.d" ) ) . ToArray ( ) ;
Assert . IsTrue ( files . Any ( x => x . EndsWith ( "pg-restore" ) ) ) ;
Assert . IsTrue ( files . Any ( x => x . EndsWith ( "postgresql" ) ) ) ;
}
O exemplo acima copia um diretório para um caminho de host (fullPath) de um contêiner em execução. Observe o uso do método de extensão aqui, portanto não usando a API fluente (já que CopyFrom vem depois de Start()). Se você deseja copiar arquivos do contêiner antes de começar, use a API Fluent.
using ( new Builder ( ) . UseContainer ( )
. UseImage ( "kiasaki/alpine-postgres" )
. ExposePort ( 5432 )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. CopyOnStart ( "/etc/conf.d" , fullPath )
. Build ( )
. Start ( ) )
{
}
O exemplo abaixo ilustra um cenário muito mais comum em que os arquivos são copiados para o contêiner. Este exemplo utiliza o método de extensão em vez da versão API fluente. É necessário um instantâneo Diff antes da cópia e logo após a cópia. Neste último o hello.html está presente.
using (
var container =
new Builder ( ) . UseContainer ( )
. UseImage ( "kiasaki/alpine-postgres" )
. ExposePort ( 5432 )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. Build ( )
. Start ( )
. WaitForProcess ( "postgres" , 30000 /*30s*/ )
. Diff ( out before )
. CopyTo ( "/bin" , fullPath ) )
{
var after = container . Diff ( ) ;
Assert . IsFalse ( before . Any ( x => x . Item == "/bin/hello.html" ) ) ;
Assert . IsTrue ( after . Any ( x => x . Item == "/bin/hello.html" ) ) ;
}
Às vezes é útil copiar arquivos no IContainerService.Dispose()
(pouco antes do contêiner parar). Portanto, existe uma API fluente para garantir que ela fará isso.
using ( new Builder ( ) . UseContainer ( )
. UseImage ( "kiasaki/alpine-postgres" )
. ExposePort ( 5432 )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. CopyOnDispose ( "/etc/conf.d" , fullPath )
. Build ( )
. Start ( ) )
{
}
Para analisar um contêiner, existem métodos de extensão de exportação e métodos de API fluentes. O mais notável é a possibilidade de exportar um contêiner quando um IContainerService
for descartado.
using ( new Builder ( ) . UseContainer ( )
. UseImage ( "kiasaki/alpine-postgres" )
. ExposePort ( 5432 )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. ExportOnDispose ( fullPath )
. Build ( )
. Start ( ) )
{
}
Isso produzirá uma exportação de contêiner (arquivo tar) no host (fullPath). Se você preferir explodi-lo (destarar), use o método ExportExplodedOnDispose
. Claro que você pode exportar o contêiner a qualquer momento usando um método de extensão no contêiner.
Um truque útil quando se trata de teste de unidade é exportar o estado do contêiner quando o teste de unidade falhar por algum motivo, portanto existe uma API Fluent que exportará quando uma determinada condição do Lambda for atendida. Por exemplo:
var failure = false ;
using ( new Builder ( ) . UseContainer ( )
. UseImage ( "kiasaki/alpine-postgres" )
. ExposePort ( 5432 )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. ExportOnDispose ( fullPath , svc => failure )
. Build ( )
. Start ( ) )
{
failure = true ;
}
Este trecho exportará o contêiner quando a instrução using estiver descartando o contêiner, pois a variável de falha está definida como verdadeira e é usada na expressão ExportOnDispose
.
Todos os serviços podem ser estendidos com ganchos. No ExportOnDispose(path, lambda)
instala um Hook quando o estado do serviço está definido para executar o lambda quando o estado é Removing
. É possível instalar e remover ganchos rapidamente. Se vários ganchos forem registrados na mesma instância de serviço, com o mesmo ServiceRunningState
, eles serão executados na ordem de instalação.
Os ganchos são particularmente bons se você deseja que algo seja executado quando um estado estiver prestes a ser definido (ou executado) no serviço, como Starting
. A API Fluent faz uso deles em algumas situações, como copiar arquivos, exportar, etc.
FluentDocker oferece suporte a todos os comandos de rede do docker. Ele pode descobrir redes por _docker.NetworkLs()
onde descobre todas as redes e alguns parâmetros simples definidos em NetworkRow
. Ele também pode inspecionar para obter informações mais profundas sobre a rede (como quais contêineres estão na rede e configuração do Ipam) por _docker.NetworkInspect(network:"networkId")
.
Para criar uma nova rede, use _docker.NetworkCreate("name_of_network")
. Também é possível fornecer NetworkCreateParams
onde tudo pode ser customizado como criar uma rede overlay e alterar a configuração do Ipam. Para excluir uma rede, basta usar _docker.NetworkRm(network:"networkId")
.
Observe que as redes não são excluídas se houver algum contêiner anexado a elas!
Quando uma rede é criada, é possível colocar um ou mais contêineres nela usando _docker.NetworkConnect("containerId","networkId")
. Observe que os contêineres podem estar em várias redes ao mesmo tempo, portanto, podem fazer solicitações de proxy entre redes isoladas. Para desconectar um contêiner de uma rede, basta fazer um _docker.NetworkDisconnect("containerId","networkId")
.
O exemplo a seguir executa um contêiner, cria uma nova rede e conecta o contêiner em execução à rede. Em seguida, ele desconecta o contêiner, exclui-o e exclui a rede.
var cmd = _docker . Run ( "postgres:9.6-alpine" , new ContainerCreateParams
{
PortMappings = new [ ] { "40001:5432" } ,
Environment = new [ ] { "POSTGRES_PASSWORD=mysecretpassword" }
} , _certificates ) ;
var container = cmd . Data ;
var network = string . Empty ;
var created = _docker . NetworkCreate ( "test-network" ) ;
if ( created . Success )
network = created . Data [ 0 ] ;
_docker . NetworkConnect ( container , network ) ;
// Container is now running and has address in the newly created 'test-network'
_docker . NetworkDisconnect ( container , id , true /*force*/ ) ;
_docker . RemoveContainer ( container , true , true ) ;
// Now it is possible to delete the network since it has been disconnected from the network
_docker . NetworkRm ( network : network ) ;
Também é possível usar um construtor fluente para construir redes docker novas ou reutilizar redes docker existentes. Eles podem então ser referenciados durante a construção de contêineres . É possível construir mais de uma rede docker e anexar um contêiner a mais de uma rede ao mesmo tempo.
using ( var nw = new Builder ( ) . UseNetwork ( "test-network" ) )
{
using (
var container =
new DockerBuilder ( )
. WithImage ( "kiasaki/alpine-postgres" )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. ExposePorts ( "5432" )
. UseNetwork ( nw )
. WaitForPort ( "5432/tcp" , 30000 /*30s*/ )
. Build ( ) )
{
container . Create ( ) . Start ( ) ;
}
}
O trecho de código acima cria uma nova rede chamada test-network e, em seguida, cria um contêiner anexado à test-network . Quando Dispose()
for chamado nw, ele removerá a rede. Também é possível fazer atribuições de contêineres IP estáticos dentro da rede por UseIpV4
ou UseIpV6
. Por exemplo:
using ( var nw = Fd . UseNetwork ( "unit-test-nw" )
. UseSubnet ( "10.18.0.0/16" ) . Build ( ) )
{
using (
var container =
Fd . UseContainer ( )
. UseImage ( "postgres:9.6-alpine" )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. ExposePort ( 5432 )
. UseNetwork ( nw )
. UseIpV4 ( "10.18.0.22" )
. WaitForPort ( "5432/tcp" , 30000 /*30s*/ )
. Build ( )
. Start ( ) )
{
var ip = container . GetConfiguration ( ) . NetworkSettings . Networks [ "unit-test-nw" ] . IPAddress ;
Assert . AreEqual ( "10.18.0.22" , ip ) ;
}
}
O exemplo acima cria uma nova rede unit-test-nw com ip-range 10.18.0.0/16 . É o usado no novo container. O IP do contêiner está definido como 10.18.0.22 e é estático devido ao comando UseIpV4
.
FluentDocker oferece suporte ao gerenciamento de volume do docker tanto a partir de comandos quanto de uma API fluente. Assim é possível ter total controle sobre os volumes utilizados no container como se deve ser descartado, reutilizado, qual driver utilizar etc.
var volume = _docker . VolumeCreate ( "test-volume" , "local" , opts : {
{ "type" , "nfs" } ,
{ "o=addr" , "192.168.1.1,rw" } ,
{ "device" , ":/path/to/dir" }
} ) ;
var cfg = _docker . VolumeInspect ( _certificates , "test-volume" ) ;
_docker . VolumeRm ( force : true , id : "test-volume" ) ;
O trecho acima cria um novo volume com o nome test-volume e é do tipo NFS . Em seguida, ele inspeciona o volume recém-criado e, por último, força a exclusão do volume.
Também é possível usar uma API fluente para criar ou usar volumes. Eles podem então ser usados na construção de um contêiner. Isto é especialmente útil quando a criação de volumes é especial ou quando a vida útil precisa ser controlada.
using ( var vol = new Builder ( ) . UseVolume ( "test-volume" ) . RemoveOnDispose ( ) . Build ( ) )
{
using (
var container =
new Builder ( ) . UseContainer ( )
. UseImage ( "postgres:9.6-alpine" )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. MountVolume ( vol , "/var/lib/postgresql/data" , MountType . ReadWrite )
. Build ( )
. Start ( ) )
{
var config = container . GetConfiguration ( ) ;
Assert . AreEqual ( 1 , config . Mounts . Length ) ;
Assert . AreEqual ( "test-volume" , config . Mounts [ 0 ] . Name ) ;
}
}
O exemplo acima cria um novo volume chamado test-volume e está programado para ser excluído quando Dispose()
for invocado no IVolumeService
. O contêiner é criado e monta o volume recém-criado em /var/lib/postgresql/data como modo de acesso de leitura/gravação . Como o contêiner está dentro do escopo da instrução using
do volume, seu tempo de vida abrange todo o tempo de vida do contêiner e depois é excluído.
FluentDocker suporta conexão com o mecanismo de eventos do docker para ouvir os eventos que ele envia.
using ( var events = Fd . Native ( ) . Events ( ) )
{
using (
var container =
new Builder ( ) . UseContainer ( )
. UseImage ( "postgres:9.6-alpine" )
. ExposePort ( 5432 )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. WaitForPort ( "5432/tcp" , 30000 /*30s*/ )
. Build ( )
. Start ( ) )
{
FdEvent e ;
while ( ( e = events . TryRead ( 3000 ) ) != null )
{
if ( e . Type == EventType . Container && e . Action == EventAction . Start )
break ;
}
}
}
O ouvinte de eventos é global e pode lidar com muitos tipos de EventAction
.
Por exemplo
Dependendo da ação, o tipo de evento pode ser diferente, como ContainerKillEvent
para EventAction.Kill
. Todos os eventos derivam de FdEvent
. Isso significa que todas as propriedades compartilhadas estão no evento base e as explícitas estão no derivado.
Por exemplo, o ´ContainerKillEvent` contém as seguintes propriedades:
public sealed class ContainerKillActor : EventActor
{
/// <summary>
/// The image name and label such as "alpine:latest".
/// </summary>
public string Image { get ; set ; }
/// <summary>
/// Name of the container.
/// </summary>
public string Name { get ; set ; }
/// <summary>
/// The signal that the container has been signalled.
/// </summary>
public string Signal { get ; set ; }
}
Este loop de eventos pode ser usado para capturar eventos e conduzir suas instâncias IService
instanciadas. Ou se você precisar reagir, por exemplo, se uma rede for adicionada ou excluída.
Na estrutura completa, ele usa log detalhado usando System.Diagnostics.Debugger.Log
. Para o núcleo .net, ele usa o padrão Microsoft.Extensions.Logging.ILog
para registrar. Ambos estão usando a categoria Ductus.FluentDocker e, portanto, podem ser configurados para participar ou não do log e configurar diferentes destinos de log.
No .net core você pode fornecer o segmento de log no arquivo de configuração do aplicativo.
{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Ductus.FluentDocker": "None"
}
}
}
Verifique https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging/?view=aspnetcore-2.1 para obter mais informações. Para a estrutura completa, verifique o XML necessário no appconfig para a estrutura completa descrita em https://docs.microsoft.com/en-us/dotnet/framework/wcf/diagnostics/tracing/configurando-tracing.
Existe uma maneira rápida de desabilitar/habilitar o log via ( Ductus.FluentDocker.Services
) Logging.Enabled()
ou Logging.Disabled()
. Isso ativará/desativará o registro à força.
É possível substituir o mecanismo padrão do FluentDocker para resolver o IP do contêiner da perspectiva do cliente, por exemplo, WaitForPort
. Isso pode ser substituído com base no ContainerBuilder
.
O exemplo abaixo substitui o comportamento padrão . Quando retorna null
o resolvedor padrão entra em ação.
using (
var container =
Fd . UseContainer ( )
. UseImage ( "postgres:9.6-alpine" )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. ExposePort ( 5432 )
. UseCustomResolver ( (
ports , portAndProto , dockerUri ) =>
{
if ( null == ports || string . IsNullOrEmpty ( portAndProto ) )
return null ;
if ( ! ports . TryGetValue ( portAndProto , out var endpoints ) )
return null ;
if ( null == endpoints || endpoints . Length == 0 )
return null ;
if ( CommandExtensions . IsNative ( ) )
return endpoints [ 0 ] ;
if ( CommandExtensions . IsEmulatedNative ( ) )
return CommandExtensions . IsDockerDnsAvailable ( )
? new IPEndPoint ( CommandExtensions . EmulatedNativeAddress ( ) , endpoints [ 0 ] . Port )
: new IPEndPoint ( IPAddress . Loopback , endpoints [ 0 ] . Port ) ;
if ( Equals ( endpoints [ 0 ] . Address , IPAddress . Any ) && null != dockerUri )
return new IPEndPoint ( IPAddress . Parse ( dockerUri . Host ) , endpoints [ 0 ] . Port ) ;
return endpoints [ 0 ] ;
} )
. WaitForPort ( "5432/tcp" , 30000 /*30s*/ )
. Build ( )
. Start ( ) )
{
var state = container . GetConfiguration ( true /*force*/ ) . State . ToServiceState ( ) ;
Assert . AreEqual ( ServiceRunningState . Running , state ) ;
}
Há suporte limitado para usar o FluentAPI para se comunicar com um daemon docker remoto sem usar o docker-machine. Isso é feito criando manualmente uma instância de DockerHostService
ou usando FromUri
em HostBuilder
.
using ( var container = Fd . UseHost ( ) .
FromUri ( Settings . DockerUri , isWindowsHost : true ) .
UseContainer ( ) .
Build ( ) )
{
}
O exemplo acima se conecta a um DockerUri
personalizado a partir de uma configuração e é um daemon docker de contêiner do Windows.
FromUri
que usa um DockerUri
para criar um IHostService
. Este uri é arbitrário. Ele também oferece suporte a outras propriedades ( veja abaixo ). public HostBuilder FromUri (
DockerUri uri ,
string name = null ,
bool isNative = true ,
bool stopWhenDisposed = false ,
bool isWindowsHost = false ,
string certificatePath = null ) { /*...*/ }
Ele usará padrões "sensatos" em todos os parâmetros. Na maioria dos casos, o uri é suficiente. Por exemplo, se não fornecer o CertificatePath, ele tentará obtê-lo no ambiente DOCKER_CERT_PATH . Se não for encontrado no ambiente, o padrão será nenhum.
UseHost
que usa uma implementação IHostService
instanciada. A biblioteca suporta docker-compose para usar arquivos de composição existentes para renderizar serviços e gerenciar a vida útil deles.
O exemplo a seguir terá um arquivo de composição que inicia um MySql e um WordPress . Portanto, o serviço de composição única terá dois serviços de contêiner abaixo dele. Por padrão, ele interromperá os serviços e fará a limpeza quando Dispose()
for invocado. Isso pode ser substituído por KeepContainers()
na configuração fluente .
version : ' 3.3 '
services :
db :
image : mysql:5.7
volumes :
- db_data:/var/lib/mysql
restart : always
environment :
MYSQL_ROOT_PASSWORD : somewordpress
MYSQL_DATABASE : wordpress
MYSQL_USER : wordpress
MYSQL_PASSWORD : wordpress
wordpress :
depends_on :
- db
image : wordpress:latest
ports :
- " 8000:80 "
restart : always
environment :
WORDPRESS_DB_HOST : db:3306
WORDPRESS_DB_USER : wordpress
WORDPRESS_DB_PASSWORD : wordpress
volumes :
db_data :
O arquivo acima é o arquivo docker-compose para montar o serviço completo.
var file = Path . Combine ( Directory . GetCurrentDirectory ( ) ,
( TemplateString ) "Resources/ComposeTests/WordPress/docker-compose.yml" ) ;
using ( var svc = new Builder ( )
. UseContainer ( )
. UseCompose ( )
. FromFile ( file )
. RemoveOrphans ( )
. Build ( ) . Start ( ) )
{
var installPage = await "http://localhost:8000/wp-admin/install.php" . Wget ( ) ;
Assert . IsTrue ( installPage . IndexOf ( "https://wordpress.org/" , StringComparison . Ordinal ) != - 1 ) ;
Assert . AreEqual ( 1 , svc . Hosts . Count ) ;
Assert . AreEqual ( 2 , svc . Containers . Count ) ;
Assert . AreEqual ( 2 , svc . Images . Count ) ;
Assert . AreEqual ( 5 , svc . Services . Count ) ;
}
O trecho acima configura fluentemente o serviço docker-compose e invoca a página de instalação para verificar se o WordPress está realmente funcionando.
Também é possível realizar todas as operações que um único contêiner suporta, como operações de cópia, exportação e espera. Por exemplo:
var file = Path . Combine ( Directory . GetCurrentDirectory ( ) ,
( TemplateString ) "Resources/ComposeTests/WordPress/docker-compose.yml" ) ;
// @formatter:off
using ( new Builder ( )
. UseContainer ( )
. UseCompose ( )
. FromFile ( file )
. RemoveOrphans ( )
. WaitForHttp ( "wordpress" , "http://localhost:8000/wp-admin/install.php" , continuation : ( resp , cnt ) =>
resp . Body . IndexOf ( "https://wordpress.org/" , StringComparison . Ordinal ) != - 1 ? 0 : 500 )
. Build ( ) . Start ( ) )
// @formatter:on
{
// Since we have waited - this shall now always work.
var installPage = await "http://localhost:8000/wp-admin/install.php" . Wget ( ) ;
Assert . IsTrue ( installPage . IndexOf ( "https://wordpress.org/" , StringComparison . Ordinal ) != - 1 ) ;
}
O trecho acima inicia o projeto wordpress docker compose e verifica a URL http://localhost:8000/wp-admin/install.php, ele retorna um determinado valor no corpo (neste caso "https://wordpress.org /"). Caso contrário, ele retornará 500 e a função WaitForHttp
aguardará 500 milissegundos antes de invocar novamente. Isso também funciona para qualquer lambda personalizado, basta usar WaitFor
. Assim é possível, por exemplo, consultar um banco de dados antes de continuar dentro do escopo de uso.
Para usuários de Linux e Mac, existem várias opções de autenticação no soquete. FluentDocker não suporta sudo , sudo sem senha (usuário adicionado como NOPASSWD em /etc/sudoer) ou sudo com senha. O padrão é que o FluentDocker espera poder conversar sem qualquer sudo . As opções são globais, mas podem ser alteradas em tempo de execução.
SudoMechanism . None . SetSudo ( ) ; // This is the default
SudoMechanism . Password . SetSudo ( "<my-sudo-password>" ) ;
SudoMechanism . NoPassword . SetSudo ( ) ;
Se desejar desligar o sudo para se comunicar com o daemon do docker, você pode seguir um tutorial do docker e executar a última etapa de adicionar seu usuário ao grupo do docker.
FluentDocker suporta conexão com daemons docker remotos. A API fluente suporta, por exemplo
new Builder ( ) . UseHost ( ) . UseMachine ( ) . WithName ( "remote-daemon" )
onde isso requer uma entrada já pré-configurada no registro docker-machine . Também é possível definir inteiros de registro da máquina docker baseados em SSH para conectar-se ao daemon remoto.
using (
var container =
new Builder ( ) . UseHost ( )
. UseSsh ( "192.168.1.27" ) . WithName ( "remote-daemon" )
. WithSshUser ( "solo" ) . WithSshKeyPath ( "${E_LOCALAPPDATA}/lxss/home/martoffi/.ssh/id_rsa" )
. UseContainer ( )
. UseImage ( "postgres:9.6-alpine" )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. Build ( ) )
{
Assert . AreEqual ( ServiceRunningState . Stopped , container . State ) ;
}
Este exemplo criará uma nova entrada de registro docker-machine chamada remote-daemon que usa SSH com endereço IP 192.168.1.27 e o usuário SSH solo . Se já for encontrada uma entrada chamada remote-daemon, ela apenas reutilizará esta entrada. Em seguida, ele obtém um IHostService com certificados e URL corretos para o daemon remoto. Assim, é possível então criar um contêiner docker no daemon remoto, neste caso é a imagem postgres . Ao descartar o contêiner, como de costume, ele o exclui da janela de encaixe remota. O IHostService certifique-se de coletar todos os certificados necessários para autenticar a conexão.
O exemplo acima produz esta entrada de registro docker-machine .
C:Usersmartoffi>docker-machine ls
NAME ACTIVE DRIVER STATE URL SWARM DOCKER ERRORS
remote-daemon * generic Running tcp://192.168.1.27:2376 v18.06.1-ce
Para usar UseSsh(...)
um túnel SSH sem senha deve ser configurado. Além disso, o usuário que usa o túnel deve ter permissão para acessar o daemon do docker.
Siga este tutorial sobre como configurar o túnel SSH e certifique-se de que o usuário possa acessar o daemon do docker.
Basicamente, crie uma nova chave rsa para usar com o túnel SSH usando ssh-keygen -t rsa
e copie-a para o host remoto por ssh-copy-id {username}@{host}
.
Edite o /etc/sudoers conforme especificado no segundo tutorial.
Quando isso for feito, agora você poderá acessar o daemon do docker remoto pelo driver genérico ou pela API fluente especificada acima. Para fazer manualmente a mesma coisa especificada no exemplo, seria algo assim.
C:Usersmartoffi>docker-machine.exe create --driver generic --generic-ip-address=192.168.1.27 --generic-ssh-key="%localappdata%/lxss/home/martoffi/.ssh/id_rsa" --generic-ssh-user=solo remote-daemon
Running pre-create checks...
Creating machine...
(remote-daemon) Importing SSH key...
Waiting for machine to be running, this may take a few minutes...
Detecting operating system of created instance...
Waiting for SSH to be available...
Detecting the provisioner...
Provisioning with ubuntu(systemd)...
Installing Docker...
Copying certs to the local machine directory...
Copying certs to the remote machine...
Setting Docker configuration on the remote daemon...
Checking connection to Docker...
Docker is up and running!
To see how to connect your Docker Client to the Docker Engine running on this virtual machine, run: docker-machine.exe env remote-daemon
Agora que a entrada do registro foi criada, é possível definir o ambiente para o docker do terminal.
C:Usersmartoffi>docker-machine.exe env remote-daemon
SET DOCKER_TLS_VERIFY=1
SET DOCKER_HOST=tcp://192.168.1.24:2376
SET DOCKER_CERT_PATH=C:Usersmartoffi.dockermachinemachinesremote-daemon
SET DOCKER_MACHINE_NAME=remote-daemon
SET COMPOSE_CONVERT_WINDOWS_PATHS=true
REM Run this command to configure your shell:
REM @FOR /f "tokens=*" %i IN ('docker-machine.exe env remote-daemon') DO @%i
Execute isto para fazer com que o cliente docker use o daemon docker remoto.
@FOR /f "tokens=*" %i IN ('docker-machine.exe env remote-daemon') DO @%i
Todos os comandos que usam o binário docker
agora serão executados no daemon docker remoto.
Ao criar e consultar, via máquina, uma máquina docker hyper-v, o processo precisa ser elevado, pois o Hyper-V não responderá às chamadas de API no modo de usuário padrão.
É possível especificar uma verificação de integridade para o contêiner docker para relatar o estado do contêiner com base em tal atividade. O exemplo a seguir usa uma verificação de integridade de que o contêiner foi encerrado ou não. É possível verificar a configuração (certifique-se de forçar a atualização) qual status a verificação de integridade está relatando.
using (
var container =
Fd . UseContainer ( )
. UseImage ( "postgres:latest" , force : true )
. HealthCheck ( "exit" )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. Build ( )
. Start ( ) )
{
var config = container . GetConfiguration ( true ) ;
AreEqual ( HealthState . Starting , config . State . Health . Status ) ;
}
É possível, por meio da API Fluent e ContainerCreateParams
especificar ulimit para o contêiner docker para, por exemplo, limitar o número de arquivos abertos, etc. Por exemplo, usar a API Fluent pode ser assim ao restringir o número de arquivos abertos a 2.048 (soft e hard) .
using (
var container =
Fd . UseContainer ( )
. UseImage ( "postgres:latest" , force : true )
. UseUlimit ( Ulimit . NoFile , 2048 , 2048 )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. Build ( )
. Start ( ) )
{
// Do stuff
}
Este repositório contém três pacotes nuget, um para acesso fluente, um para classes base ms-test e outro para classes base xunit a serem usadas durante o teste. Por exemplo, em um teste de unidade, é possível iniciar um contêiner postgres e aguardar a inicialização do banco de dados.
public class PostgresXUnitTests : IClassFixture < PostgresTestBase >
{
[ Fact ]
public void Test ( )
{
// We now have a running postgres
// and a valid connection string to use.
}
}
using (
var container =
new DockerBuilder ( )
. WithImage ( "kiasaki/alpine-postgres" )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. ExposePorts ( "5432" )
. WaitForPort ( "5432/tcp" , 30000 /*30s*/ )
. Build ( ) )
{
container . Create ( ) . Start ( ) ;
}
Também é possível reutilizar classes base abstratas, por exemplo, base de teste postgres para simplificar e tornar o unittest limpo em um contêiner.
[ TestClass ]
public class PostgresMsTests : PostgresTestBase
{
[ TestMethod ]
public void Test ( )
{
// We now have a running postgres
// and a valid connection string to use.
}
}
O FluentDockerTestBase
permite substituições simples para fazer facilmente qualquer teste personalizado apoiado pelo docker. Basta criar uma classe de teste e derivar do FluentDockerTestBase
e substituir os métodos adequados. Por exemplo.
protected override DockerBuilder Build ( )
{
return new DockerBuilder ( )
. WithImage ( "kiasaki/alpine-postgres" )
. WithEnvironment ( $ "POSTGRES_PASSWORD= { PostgresPassword } " )
. ExposePorts ( "5432" )
. WaitForPort ( "5432/tcp" , 30000 /*30s*/ ) ;
}
Isso criará um construtor com docker image postgres:latest e definirá uma string de ambiente, mas também exporá a porta db 5432 do postgres ao host para que seja possível conectar-se ao banco de dados dentro do contêiner. Por último, ele aguardará a porta 5432. Isso garante que o banco de dados esteja em execução e inicializado corretamente. Se o tempo limite, neste exemplo definido como 30 segundos, lançará uma exceção e o contêiner será interrompido e removido. Observe que a porta do host não é 5432! Use Container.GetHostPort("5432/tcp")
para obter a porta do host. O IP do host pode ser recuperado pela propriedade Container.Host
e, portanto, deve ser usado na comunicação com o aplicativo no contêiner.
Se um retorno de chamada for necessário quando o contêiner tiver sido extraído, criado e iniciado com êxito.
protected override void OnContainerInitialized ( )
{
ConnectionString = string . Format ( PostgresConnectionString , Container . Host ,
Container . GetHostPort ( "5432/tcp" ) , PostgresUser ,
PostgresPassword , PostgresDb ) ;
}
Este exemplo renderiza uma string de conexão adequada para o banco de dados postgresql dentro do contêiner recém-criado. Isso pode ser usado para conectar usando Npgsql, EF7, NHibernate, Marten ou outras ferramentas compatíveis. Este método não será chamado se extrair a imagem do repositório docker ou não for possível criar/iniciar o contêiner.
Se for necessário substituir um gancho de contêiner antes do desligamento.
protected virtual void OnContainerTearDown ( )
{
// Do stuff before container is shut down.
}
Observe que se o contêiner não tiver nome, se não for descartado corretamente, o contêiner docker ainda será executado e deverá ser removido manualmente. Este é um recurso e não um bug, pois você pode querer vários contêineres em execução em seu teste. A classe DockerContainer
gerencia o ID da instância do contêiner e, portanto, interage apenas com ele e com nenhum outro contêiner.
Ao criar/iniciar um novo container ele irá primeiro verificar no repositório local se a imagem do container já está presente e irá baixá-la se não for encontrada. Isso pode levar algum tempo e há apenas um log de depuração, se habilitado é possível monitorar o processo de download.
Quando ocorre uma exceção não tratada e o aplicativo FailFast, ou seja, termina rapidamente, ele não invocará a cláusula finally
. Portanto, uma falha WaitForPort
dentro de uma instrução using
não descartará o serviço de contêiner. Portanto, o contêiner ainda está em execução. Para corrigir isso, faça um try...catch global ou injete um localmente, por exemplo
try
{
using ( var container =
new Builder ( ) . UseContainer ( )
. UseImage ( "postgres:9.6-alpine" )
. ExposePort ( 5432 )
. WithEnvironment ( "POSTGRES_PASSWORD=postgres" )
. WaitForPort ( "5777/tcp" , 10000 ) // Fail here since 5777 is not valid port
. Build ( ) )
{
container . Start ( ) ; // FluentDockerException is thrown here since WaitForPort is executed
}
} catch { throw ; }
Mas isso ocorre apenas quando o encerramento do aplicativo é feito devido ao FluentDockerException
lançado no WaitForPort
, caso contrário, ele descartará o contêiner corretamente e, portanto, o try...catch
não será necessário.
Isso também pode ser resolvido usando as funções Fd.Build
( consulte Usando extensões do Builder para obter mais informações).
A classe Fd
é uma classe estática que fornece métodos convenientes para construir e executar contêineres únicos e compostos. Para construir um contêiner basta usar:
var build = Fd . Build ( c => c . UseContainer ( )
. UseImage ( "postgres:9.6-alpine" )
. ExposePort ( 5432 )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. WaitForPort ( "5432/tcp" , TimeSpan . FromSeconds ( 30 ) ) ) ;
// This is the equivalent of
var build = new Builder ( ) . UseContainer ( )
. UseImage ( "postgres:9.6-alpine" )
. ExposePort ( 5432 )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. WaitForPort ( "5432/tcp" , TimeSpan . FromSeconds ( 30 ) ) ;
Isso pode então ser usado para iniciar os contêineres dentro de uma cláusula using
segura que tem garantia de ser descartada mesmo que a exceção não seja detectada.
build . Container ( svc =>
{
var config = svc . GetConfiguration ( ) ;
// Do stuff...
} ) ;
Após a execução do método Container
, o contêiner é, neste caso, interrompido e removido. Este é o equivalente de
// This is equivalent of
try
{
using ( var svc = build . Build ( ) )
{
svc . Start ( ) ;
var config = svc . GetConfiguration ( ) ;
// Do stuff...
}
}
catch
{
Log ( .. . ) ;
throw ;
}
Também é possível combinar construtor e execução, por exemplo, através de:
Fd . Container ( c => c . UseContainer ( )
. UseImage ( "postgres:9.6-alpine" )
. ExposePort ( 5432 )
. WithEnvironment ( "POSTGRES_PASSWORD=mysecretpassword" )
. WaitForPort ( "5432/tcp" , TimeSpan . FromSeconds ( 30 ) ) ,
svc =>
{
var config = svc . GetConfiguration ( ) ;
// Do stuff...
} ) ;
O exemplo acima irá construir o contêiner, iniciar, parar e finalmente excluir o contêiner. Mesmo se e Exception
for lançada, ela será Disposed
. É claro que é possível usar o contêiner compilado usando métodos de extensão composite
como acontece com container
.