Skip to content

Commit

Permalink
Code reuse on working with docker networks
Browse files Browse the repository at this point in the history
  • Loading branch information
dinamic committed Jul 16, 2024
1 parent ed18c0b commit 1fa2c16
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 23 deletions.
5 changes: 3 additions & 2 deletions src/Container/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
* @phpstan-type ContainerInspectSingleNetwork array<int, array{'NetworkSettings': array{'IPAddress': string}}>
* @phpstan-type ContainerInspectMultipleNetworks array<int, array{'NetworkSettings': array{'Networks': array<string, array{'IPAddress': string}>}}>
* @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
{
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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
Expand Down
55 changes: 51 additions & 4 deletions src/Trait/DockerContainerAwareTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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)) {
Expand All @@ -50,12 +52,57 @@ 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();

/** @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<int, DockerNetwork> $output */
$output = json_decode($json, true, 512, JSON_THROW_ON_ERROR);

/** @var array<int, DockerNetwork> $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();
}
}
2 changes: 1 addition & 1 deletion src/Wait/WaitForHttp.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
8 changes: 4 additions & 4 deletions src/Wait/WaitForTcpPortOpen.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,21 @@ 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);
}

/**
* @throws JsonException
*/
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'));
}
}
Expand Down
62 changes: 50 additions & 12 deletions tests/Integration/WaitStrategyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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');

if ($canConnect) {
if ($wait) {
$container->withWait(WaitForTcpPortOpen::make(80));
}

if ($useNetwork) {
$container->withNetwork(self::$testNetworkName);
}

$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();
$container->remove();
}
}

/**
Expand All @@ -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],
];
}

Expand Down

0 comments on commit 1fa2c16

Please sign in to comment.