From 655aae6a63db04456d3dd6c100808c77494706a2 Mon Sep 17 00:00:00 2001 From: Nikola Petkanski Date: Tue, 16 Jul 2024 04:39:29 +0300 Subject: [PATCH] Code reuse on working with docker networks --- src/Container/Container.php | 5 ++- src/Trait/DockerContainerAwareTrait.php | 55 +++++++++++++++++++++++-- src/Wait/WaitForHttp.php | 2 +- src/Wait/WaitForTcpPortOpen.php | 8 ++-- tests/Integration/WaitStrategyTest.php | 21 +++++++--- 5 files changed, 75 insertions(+), 16 deletions(-) diff --git a/src/Container/Container.php b/src/Container/Container.php index f8d0470..d9f392b 100644 --- a/src/Container/Container.php +++ b/src/Container/Container.php @@ -15,6 +15,7 @@ * @phpstan-type ContainerInspectSingleNetwork array * @phpstan-type ContainerInspectMultipleNetworks array}}> * @phpstan-type ContainerInspect ContainerInspectSingleNetwork|ContainerInspectMultipleNetworks + * @phpstan-type DockerNetwork array{CreatedAt: string, Driver: string, ID: string, IPv6: string, Internal: string, Labels: string, Name: string, Scope: string} */ class Container { @@ -167,7 +168,7 @@ public function run(bool $wait = true): self $this->process = new Process($params); $this->process->mustRun(); - $this->inspectedData = $this->getContainerInspect($this->id); + $this->inspectedData = self::dockerContainerInspect($this->id); Registry::add($this); @@ -255,7 +256,7 @@ public function logs(): string public function getAddress(): string { - return $this->getContainerAddress( + return self::dockerContainerAddress( containerId: $this->id, networkName: $this->network, inspectedData: $this->inspectedData diff --git a/src/Trait/DockerContainerAwareTrait.php b/src/Trait/DockerContainerAwareTrait.php index f5d2c61..274d91a 100644 --- a/src/Trait/DockerContainerAwareTrait.php +++ b/src/Trait/DockerContainerAwareTrait.php @@ -6,10 +6,12 @@ use JsonException; use Symfony\Component\Process\Process; +use Testcontainer\Container\Container; use UnexpectedValueException; /** - * @phpstan-import-type ContainerInspect from \Testcontainer\Container\Container + * @phpstan-import-type ContainerInspect from Container + * @phpstan-import-type DockerNetwork from Container */ trait DockerContainerAwareTrait { @@ -21,10 +23,10 @@ trait DockerContainerAwareTrait * * @throws JsonException */ - protected function getContainerAddress(string $containerId, ?string $networkName = null, ?array $inspectedData = null): string + private static function dockerContainerAddress(string $containerId, ?string $networkName = null, ?array $inspectedData = null): string { if (! is_array($inspectedData)) { - $inspectedData = $this->getContainerInspect($containerId); + $inspectedData = self::dockerContainerInspect($containerId); } if (is_string($networkName)) { @@ -50,7 +52,7 @@ protected function getContainerAddress(string $containerId, ?string $networkName * * @throws JsonException */ - protected function getContainerInspect(string $containerId): array + private static function dockerContainerInspect(string $containerId): array { $process = new Process(['docker', 'inspect', $containerId]); $process->mustRun(); @@ -58,4 +60,49 @@ protected function getContainerInspect(string $containerId): array /** @var ContainerInspect */ return json_decode($process->getOutput(), true, 512, JSON_THROW_ON_ERROR); } + + /** + * @param string $networkName + * @return DockerNetwork|false + * + * @throws JsonException + */ + private static function dockerNetworkFind(string $networkName): array|false + { + $process = new Process(['docker', 'network', 'ls', '--format', 'json', '--filter', 'name=' . $networkName]); + $process->mustRun(); + + $json = $process->getOutput(); + + if ($json === '') { + return false; + } + + $json = str_replace("\n", ',', $json); + $json = '['. rtrim($json, ',') .']'; + + /** @var array $output */ + $output = json_decode($json, true, 512, JSON_THROW_ON_ERROR); + + /** @var array $matchingNetworks */ + $matchingNetworks = array_filter($output, static fn (array $network) => $network['Name'] === $networkName); + + if (count($matchingNetworks) === 0) { + return false; + } + + return $matchingNetworks[0]; + } + + private static function dockerNetworkCreate(string $networkName, string $driver = 'bridge'): void + { + $process = new Process(['docker', 'network', 'create', '--driver', $driver, $networkName]); + $process->mustRun(); + } + + private static function dockerNetworkRemove(string $networkName): void + { + $process = new Process(['docker', 'network', 'rm', $networkName, '-f']); + $process->mustRun(); + } } diff --git a/src/Wait/WaitForHttp.php b/src/Wait/WaitForHttp.php index 889bc61..7de3f1e 100644 --- a/src/Wait/WaitForHttp.php +++ b/src/Wait/WaitForHttp.php @@ -58,7 +58,7 @@ public function withStatusCode(int $statusCode): self public function wait(string $id): void { - $containerAddress = $this->getContainerAddress(containerId: $id); + $containerAddress = self::dockerContainerAddress(containerId: $id); $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, sprintf('http://%s:%d%s', $containerAddress, $this->port, $this->path)); diff --git a/src/Wait/WaitForTcpPortOpen.php b/src/Wait/WaitForTcpPortOpen.php index d55473d..52e7d72 100644 --- a/src/Wait/WaitForTcpPortOpen.php +++ b/src/Wait/WaitForTcpPortOpen.php @@ -13,13 +13,13 @@ final class WaitForTcpPortOpen implements WaitInterface { use DockerContainerAwareTrait; - public function __construct(private readonly int $port) + public function __construct(private readonly int $port, private readonly ?string $network = null) { } - public static function make(int $port): self + public static function make(int $port, ?string $network = null): self { - return new self($port); + return new self($port, $network); } /** @@ -27,7 +27,7 @@ public static function make(int $port): self */ public function wait(string $id): void { - if (@fsockopen($this->getContainerAddress($id), $this->port) === false) { + if (@fsockopen(self::dockerContainerAddress(containerId: $id, networkName: $this->network), $this->port) === false) { throw new ContainerNotReadyException($id, new RuntimeException('Unable to connect to container TCP port')); } } diff --git a/tests/Integration/WaitStrategyTest.php b/tests/Integration/WaitStrategyTest.php index 455e07b..cb559db 100644 --- a/tests/Integration/WaitStrategyTest.php +++ b/tests/Integration/WaitStrategyTest.php @@ -10,6 +10,8 @@ use Symfony\Component\Process\Process; use Testcontainer\Container\Container; use Testcontainer\Exception\ContainerNotReadyException; +use Testcontainer\Registry; +use Testcontainer\Trait\DockerContainerAwareTrait; use Testcontainer\Wait\WaitForExec; use Testcontainer\Wait\WaitForHealthCheck; use Testcontainer\Wait\WaitForHttp; @@ -18,6 +20,15 @@ class WaitStrategyTest extends TestCase { + use DockerContainerAwareTrait; + + public static function tearDownAfterClass(): void + { + parent::tearDownAfterClass(); + + Registry::cleanup(); + } + public function testWaitForExec(): void { $called = false; @@ -95,17 +106,17 @@ public function testWaitForHTTP(): void /** * @dataProvider provideWaitForTcpPortOpen */ - public function testWaitForTcpPortOpen(bool $canConnect): void + public function testWaitForTcpPortOpen(bool $wait, bool $expectConnection): void { $container = Container::make('nginx:alpine'); - if ($canConnect) { + if ($wait) { $container->withWait(WaitForTcpPortOpen::make(80)); } $container->run(); - if ($canConnect) { + if ($wait || $expectConnection) { static::assertIsResource(fsockopen($container->getAddress(), 80), 'Failed to connect to container'); return; } @@ -123,8 +134,8 @@ public function testWaitForTcpPortOpen(bool $canConnect): void public function provideWaitForTcpPortOpen(): array { return [ - 'Can connect to container' => [true], - 'Cannot connect to container' => [false], + 'Can connect to container; no network in use' => [true, true], + 'Cannot connect to container; no network in use' => [false, true], ]; }