mirror of
https://github.com/stan220/testcontainers-php.git
synced 2026-09-08 17:29:29 +00:00
Merge pull request #12 from dinamic/feature/wait-for-port
Support for waiting on tcp port to open
This commit is contained in:
+18
-13
@@ -7,14 +7,20 @@ namespace Testcontainer\Container;
|
||||
use Symfony\Component\Process\Process;
|
||||
use Testcontainer\Exception\ContainerNotReadyException;
|
||||
use Testcontainer\Registry;
|
||||
use Testcontainer\Trait\DockerContainerAwareTrait;
|
||||
use Testcontainer\Wait\WaitForNothing;
|
||||
use Testcontainer\Wait\WaitInterface;
|
||||
|
||||
/**
|
||||
* @phpstan-type ContainerInspect array{0: array{NetworkSettings: array{IPAddress: string}}}
|
||||
* @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
|
||||
{
|
||||
use DockerContainerAwareTrait;
|
||||
|
||||
private string $id;
|
||||
|
||||
private ?string $entryPoint = null;
|
||||
@@ -57,6 +63,11 @@ class Container
|
||||
return new Container($image);
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function withEntryPoint(string $entryPoint): self
|
||||
{
|
||||
$this->entryPoint = $entryPoint;
|
||||
@@ -169,13 +180,7 @@ class Container
|
||||
$this->process = new Process($params);
|
||||
$this->process->mustRun();
|
||||
|
||||
$inspect = new Process(['docker', 'inspect', $this->id]);
|
||||
$inspect->mustRun();
|
||||
|
||||
/** @var ContainerInspect $inspectedData */
|
||||
$inspectedData = json_decode($inspect->getOutput(), true, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
$this->inspectedData = $inspectedData;
|
||||
$this->inspectedData = self::dockerContainerInspect($this->id);
|
||||
|
||||
Registry::add($this);
|
||||
|
||||
@@ -263,10 +268,10 @@ class Container
|
||||
|
||||
public function getAddress(): string
|
||||
{
|
||||
if ($this->network !== null && !empty($this->inspectedData[0]['NetworkSettings']['Networks'][$this->network]['IPAddress'])) {
|
||||
return $this->inspectedData[0]['NetworkSettings']['Networks'][$this->network]['IPAddress'];
|
||||
}
|
||||
|
||||
return $this->inspectedData[0]['NetworkSettings']['IPAddress'];
|
||||
return self::dockerContainerAddress(
|
||||
containerId: $this->id,
|
||||
networkName: $this->network,
|
||||
inspectedData: $this->inspectedData
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Testcontainer\Trait;
|
||||
|
||||
use JsonException;
|
||||
use Symfony\Component\Process\Process;
|
||||
use Testcontainer\Container\Container;
|
||||
use UnexpectedValueException;
|
||||
|
||||
/**
|
||||
* @phpstan-import-type ContainerInspect from Container
|
||||
* @phpstan-import-type DockerNetwork from Container
|
||||
*/
|
||||
trait DockerContainerAwareTrait
|
||||
{
|
||||
/**
|
||||
* @param string $containerId
|
||||
* @param string|null $networkName
|
||||
* @param ContainerInspect|null $inspectedData
|
||||
* @return string
|
||||
*
|
||||
* @throws JsonException
|
||||
*/
|
||||
private static function dockerContainerAddress(string $containerId, ?string $networkName = null, ?array $inspectedData = null): string
|
||||
{
|
||||
if (! is_array($inspectedData)) {
|
||||
$inspectedData = self::dockerContainerInspect($containerId);
|
||||
}
|
||||
|
||||
if (is_string($networkName)) {
|
||||
$containerAddress = $inspectedData[0]['NetworkSettings']['Networks'][$networkName]['IPAddress'] ?? null;
|
||||
|
||||
if (is_string($containerAddress)) {
|
||||
return $containerAddress;
|
||||
}
|
||||
}
|
||||
|
||||
$containerAddress = $inspectedData[0]['NetworkSettings']['IPAddress'] ?? null;
|
||||
|
||||
if (is_string($containerAddress)) {
|
||||
return $containerAddress;
|
||||
}
|
||||
|
||||
throw new UnexpectedValueException('Unable to find container IP address');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $containerId
|
||||
* @return ContainerInspect
|
||||
*
|
||||
* @throws JsonException
|
||||
*/
|
||||
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, 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();
|
||||
}
|
||||
}
|
||||
@@ -4,14 +4,13 @@ declare(strict_types=1);
|
||||
|
||||
namespace Testcontainer\Wait;
|
||||
|
||||
use Symfony\Component\Process\Process;
|
||||
use Testcontainer\Exception\ContainerNotReadyException;
|
||||
use Testcontainer\Trait\DockerContainerAwareTrait;
|
||||
|
||||
/**
|
||||
* @phpstan-import-type ContainerInspect from \Testcontainer\Container\Container
|
||||
*/
|
||||
class WaitForHttp implements WaitInterface
|
||||
{
|
||||
use DockerContainerAwareTrait;
|
||||
|
||||
public const METHOD_GET = 'GET';
|
||||
public const METHOD_POST = 'POST';
|
||||
public const METHOD_PUT = 'PUT';
|
||||
@@ -59,16 +58,10 @@ class WaitForHttp implements WaitInterface
|
||||
|
||||
public function wait(string $id): void
|
||||
{
|
||||
$process = new Process(['docker', 'inspect', $id]);
|
||||
$process->mustRun();
|
||||
|
||||
/** @var ContainerInspect $data */
|
||||
$data = json_decode($process->getOutput(), true);
|
||||
|
||||
$ip = $data[0]['NetworkSettings']['IPAddress'];
|
||||
$containerAddress = self::dockerContainerAddress(containerId: $id);
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, sprintf('http://%s:%d%s', $ip, $this->port, $this->path));
|
||||
curl_setopt($ch, CURLOPT_URL, sprintf('http://%s:%d%s', $containerAddress, $this->port, $this->path));
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $this->method);
|
||||
curl_setopt($ch, CURLOPT_HEADER, true);
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Testcontainer\Wait;
|
||||
|
||||
use JsonException;
|
||||
use RuntimeException;
|
||||
use Testcontainer\Exception\ContainerNotReadyException;
|
||||
use Testcontainer\Trait\DockerContainerAwareTrait;
|
||||
|
||||
final class WaitForTcpPortOpen implements WaitInterface
|
||||
{
|
||||
use DockerContainerAwareTrait;
|
||||
|
||||
public function __construct(private readonly int $port, private readonly ?string $network = null)
|
||||
{
|
||||
}
|
||||
|
||||
public static function make(int $port, ?string $network = null): self
|
||||
{
|
||||
return new self($port, $network);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws JsonException
|
||||
*/
|
||||
public function wait(string $id): void
|
||||
{
|
||||
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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,13 +9,26 @@ use Predis\Client;
|
||||
use Predis\Connection\ConnectionException;
|
||||
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;
|
||||
use Testcontainer\Wait\WaitForLog;
|
||||
use Testcontainer\Wait\WaitForTcpPortOpen;
|
||||
|
||||
class WaitStrategyTest extends TestCase
|
||||
{
|
||||
use DockerContainerAwareTrait;
|
||||
|
||||
public static function tearDownAfterClass(): void
|
||||
{
|
||||
parent::tearDownAfterClass();
|
||||
|
||||
Registry::cleanup();
|
||||
}
|
||||
|
||||
public function testWaitForExec(): void
|
||||
{
|
||||
$called = false;
|
||||
@@ -90,6 +103,42 @@ class WaitStrategyTest extends TestCase
|
||||
$this->assertNotEmpty($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideWaitForTcpPortOpen
|
||||
*/
|
||||
public function testWaitForTcpPortOpen(bool $wait): void
|
||||
{
|
||||
$container = Container::make('nginx:alpine');
|
||||
|
||||
if ($wait) {
|
||||
$container->withWait(WaitForTcpPortOpen::make(80));
|
||||
}
|
||||
|
||||
$container->run();
|
||||
|
||||
if ($wait) {
|
||||
static::assertIsResource(fsockopen($container->getAddress(), 80), 'Failed to connect to container');
|
||||
return;
|
||||
}
|
||||
|
||||
$containerId = $container->getId();
|
||||
|
||||
$this->expectExceptionObject(new ContainerNotReadyException($containerId));
|
||||
|
||||
(new WaitForTcpPortOpen(8080))->wait($containerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array<bool>>
|
||||
*/
|
||||
public function provideWaitForTcpPortOpen(): array
|
||||
{
|
||||
return [
|
||||
'Can connect to container' => [true],
|
||||
'Cannot connect to container' => [false],
|
||||
];
|
||||
}
|
||||
|
||||
public function testWaitForHealthCheck(): void
|
||||
{
|
||||
$container = Container::make('nginx')
|
||||
|
||||
Reference in New Issue
Block a user