diff --git a/src/Container/Container.php b/src/Container/Container.php index f8d0470..a31595e 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 = static::getContainerInspect($this->id); Registry::add($this); @@ -255,7 +256,7 @@ public function logs(): string public function getAddress(): string { - return $this->getContainerAddress( + return static::getContainerAddress( containerId: $this->id, networkName: $this->network, inspectedData: $this->inspectedData diff --git a/src/Trait/DockerContainerAwareTrait.php b/src/Trait/DockerContainerAwareTrait.php index f5d2c61..201eadc 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 getContainerAddress(string $containerId, ?string $networkName = null, ?array $inspectedData = null): string { if (! is_array($inspectedData)) { - $inspectedData = $this->getContainerInspect($containerId); + $inspectedData = static::getContainerInspect($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 getContainerInspect(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): void + { + $process = new Process(['docker', 'network', 'create', $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..7a14acf 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 = static::getContainerAddress(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..a4c87ac 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::getContainerAddress(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..828ddd7 100644 --- a/tests/Integration/WaitStrategyTest.php +++ b/tests/Integration/WaitStrategyTest.php @@ -10,6 +10,7 @@ use Symfony\Component\Process\Process; use Testcontainer\Container\Container; use Testcontainer\Exception\ContainerNotReadyException; +use Testcontainer\Trait\DockerContainerAwareTrait; use Testcontainer\Wait\WaitForExec; use Testcontainer\Wait\WaitForHealthCheck; use Testcontainer\Wait\WaitForHttp; @@ -18,6 +19,29 @@ class WaitStrategyTest extends TestCase { + use DockerContainerAwareTrait; + + private static string $testNetworkName = 'test-network'; + + public static function setUpBeforeClass(): void + { + parent::setUpBeforeClass(); + + if (self::dockerNetworkFind(self::$testNetworkName) !== false) { + // the default name is in use; bring entropy to it, not to mess with the other software + self::$testNetworkName .= bin2hex(random_bytes(12)); + } + + self::dockerNetworkCreate(self::$testNetworkName); + } + + public static function tearDownAfterClass(): void + { + parent::tearDownAfterClass(); + + self::dockerNetworkRemove(self::$testNetworkName); + } + public function testWaitForExec(): void { $called = false; @@ -95,26 +119,38 @@ public function testWaitForHTTP(): void /** * @dataProvider provideWaitForTcpPortOpen */ - public function testWaitForTcpPortOpen(bool $canConnect): void - { + public function testWaitForTcpPortOpen( + bool $wait, + bool $expectConnection, + bool $useNetwork = false + ): void { $container = Container::make('nginx:alpine'); + $networkName = self::$testNetworkName; - if ($canConnect) { + if ($wait) { $container->withWait(WaitForTcpPortOpen::make(80)); } + if ($useNetwork) { + $container->withNetwork($networkName); + } + $container->run(); - if ($canConnect) { - static::assertIsResource(fsockopen($container->getAddress(), 80), 'Failed to connect to container'); - return; - } + try { + if ($wait || $expectConnection) { + static::assertIsResource(fsockopen($container->getAddress(), 80), 'Failed to connect to container'); + return; + } - $containerId = $container->getId(); + $containerId = $container->getId(); - $this->expectExceptionObject(new ContainerNotReadyException($containerId)); + $this->expectExceptionObject(new ContainerNotReadyException($containerId)); - (new WaitForTcpPortOpen(8080))->wait($containerId); + (new WaitForTcpPortOpen(8080))->wait($containerId); + } finally { + $container->stop(); + } } /** @@ -123,8 +159,10 @@ 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, false], + 'Cannot connect to container; no network in use' => [false, true, false], + 'Can connect to container; with network in use' => [true, true, true], + 'Cannot connect to container; with network in use' => [false, true, true], ]; }