diff --git a/src/Container/GenericContainer.php b/src/Container/GenericContainer.php index 9a0a266..ca0ba6f 100644 --- a/src/Container/GenericContainer.php +++ b/src/Container/GenericContainer.php @@ -232,10 +232,12 @@ class GenericContainer implements TestContainer $this->id = $containerCreateResponse?->getId() ?? ''; } catch (ContainerCreateNotFoundException) { /** @var CreateImageStream $imageCreateResponse */ - $this->dockerClient->imageCreate(null, [ + $imageCreateResponse = $this->dockerClient->imageCreate(null, [ 'fromImage' => explode(':', $this->image)[0], 'tag' => explode(':', $this->image)[1] ?? 'latest', ]); + $imageCreateResponse->wait(); + return $this->start(); } diff --git a/src/Container/MariaDBContainer.php b/src/Container/MariaDBContainer.php index 72f0aac..b806783 100644 --- a/src/Container/MariaDBContainer.php +++ b/src/Container/MariaDBContainer.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace Testcontainers\Container; -use Testcontainers\Wait\WaitForLog; +use Testcontainers\Wait\WaitForExec; /** * Left for namespace backward compatibility @@ -17,8 +17,12 @@ class MariaDBContainer extends Container { parent::__construct('mariadb:' . $version); $this->withExposedPorts(3306); - $this->withWait(new WaitForLog('ready for connections')); $this->withEnvironment('MARIADB_ROOT_PASSWORD', $mysqlRootPassword); + $this->withWait(new WaitForExec([ + "mariadb-admin", + "ping", + "-h", "127.0.0.1", + ])); } public static function make(string $version = 'latest', string $mysqlRootPassword = 'root'): self @@ -40,4 +44,4 @@ class MariaDBContainer extends Container return $this; } -} \ No newline at end of file +} diff --git a/src/Container/MySQLContainer.php b/src/Container/MySQLContainer.php index 232c912..a6e7efb 100644 --- a/src/Container/MySQLContainer.php +++ b/src/Container/MySQLContainer.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace Testcontainers\Container; -use Testcontainers\Wait\WaitForLog; +use Testcontainers\Wait\WaitForExec; /** * Left for namespace backward compatibility @@ -18,7 +18,11 @@ class MySQLContainer extends Container parent::__construct('mysql:' . $version); $this->withExposedPorts(3306); $this->withEnvironment('MYSQL_ROOT_PASSWORD', $mysqlRootPassword); - $this->withWait(new WaitForLog('ready for connections')); + $this->withWait(new WaitForExec([ + "mysqladmin", + "ping", + "-h", "127.0.0.1", + ])); } public static function make(string $version = 'latest', string $mysqlRootPassword = 'root'): self diff --git a/src/Container/StartedGenericContainer.php b/src/Container/StartedGenericContainer.php index 60d478c..ec81e71 100644 --- a/src/Container/StartedGenericContainer.php +++ b/src/Container/StartedGenericContainer.php @@ -6,10 +6,10 @@ namespace Testcontainers\Container; use Docker\API\Client; use Docker\API\Model\ContainersIdExecPostBody; -use Docker\API\Model\ContainersIdJsonGetResponse200; use Docker\API\Model\IdResponse; use Docker\API\Runtime\Client\Client as DockerRuntimeClient; use Docker\Docker; +use Psr\Http\Message\ResponseInterface; use Testcontainers\ContainerClient\DockerContainerClient; class StartedGenericContainer implements StartedTestContainer @@ -53,7 +53,7 @@ class StartedGenericContainer implements StartedTestContainer /** @var IdResponse | null $exec */ $exec = $this->dockerClient->containerExec($this->id, $execConfig); - if($exec === null || $exec->getId() === null) { + if ($exec === null || $exec->getId() === null) { throw new \RuntimeException('Failed to create exec command'); } @@ -108,17 +108,36 @@ class StartedGenericContainer implements StartedTestContainer return $this->inspect()->ports[$port]; } - //TODO: not ready yet + /** + * @throws \JsonException + */ public function getFirstMappedPort(): int { - /** @var ContainersIdJsonGetResponse200 | null $containerInspectResponse */ - $containerInspectResponse = $this->dockerClient->containerInspect($this->id); - $settings = $containerInspectResponse->getNetworkSettings(); + //For some reason, containerInspect can crash when using FETCH_OBJECT option (e.g. with OpenSearch) + //should be checked within beluga-php/docker-php client library + /** @var ResponseInterface | null $containerInspectResponse */ + $containerInspectResponse = $this->dockerClient->containerInspect($this->id, [], Docker::FETCH_RESPONSE); + if ($containerInspectResponse === null) { + throw new \RuntimeException('Failed to inspect container'); + } + + $containerInspectResponseAsArray = json_decode( + $containerInspectResponse->getBody()->getContents(), + true, + 512, + JSON_THROW_ON_ERROR + ); + + /** @var array>> $ports */ + $ports = $containerInspectResponseAsArray['NetworkSettings']['Ports'] ?? []; + + if ($ports === []) { + throw new \RuntimeException('Failed to get ports from container'); + } - $ports = (array)$settings->getPorts(); $port = array_key_first($ports); - return (int) $ports[$port][0]->getHostPort(); + return (int) $ports[$port][0]['HostPort']; } public function getName(): string diff --git a/src/ContainerClient/DockerContainerClient.php b/src/ContainerClient/DockerContainerClient.php index 84bf645..8001992 100644 --- a/src/ContainerClient/DockerContainerClient.php +++ b/src/ContainerClient/DockerContainerClient.php @@ -1,5 +1,7 @@ withExposedPorts(3306); - $this->withWait(new WaitForLog('ready for connections')); $this->withEnvironment('MARIADB_ROOT_PASSWORD', $mysqlRootPassword); + $this->withWait(new WaitForExec([ + "mariadb-admin", + "ping", + "-h", "127.0.0.1", + ])); } public function withMariaDBUser(string $username, string $password): self diff --git a/src/Modules/MySQLContainer.php b/src/Modules/MySQLContainer.php index b5daaea..00c6a90 100644 --- a/src/Modules/MySQLContainer.php +++ b/src/Modules/MySQLContainer.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace Testcontainers\Modules; use Testcontainers\Container\GenericContainer; -use Testcontainers\Wait\WaitForLog; +use Testcontainers\Wait\WaitForExec; class MySQLContainer extends GenericContainer { @@ -14,7 +14,11 @@ class MySQLContainer extends GenericContainer parent::__construct('mysql:' . $version); $this->withExposedPorts(3306); $this->withEnvironment('MYSQL_ROOT_PASSWORD', $mysqlRootPassword); - $this->withWait(new WaitForLog('ready for connections')); + $this->withWait(new WaitForExec([ + "mysqladmin", + "ping", + "-h", "127.0.0.1", + ])); } public function withMySQLUser(string $username, string $password): self diff --git a/src/Utils/PortGenerator/RandomPortGenerator.php b/src/Utils/PortGenerator/RandomPortGenerator.php index 8d3bbbc..1610a0a 100644 --- a/src/Utils/PortGenerator/RandomPortGenerator.php +++ b/src/Utils/PortGenerator/RandomPortGenerator.php @@ -27,4 +27,4 @@ class RandomPortGenerator implements PortGenerator return $port; } -} \ No newline at end of file +} diff --git a/src/Wait/BaseWaitStrategy.php b/src/Wait/BaseWaitStrategy.php index 432aedf..80ff748 100644 --- a/src/Wait/BaseWaitStrategy.php +++ b/src/Wait/BaseWaitStrategy.php @@ -8,7 +8,6 @@ use Testcontainers\Container\StartedTestContainer; abstract class BaseWaitStrategy implements WaitStrategy { - public function __construct(protected int $timeout = 10000, protected int $pollInterval = 500) { } diff --git a/src/Wait/WaitForExec.php b/src/Wait/WaitForExec.php index 044471c..ce7af35 100644 --- a/src/Wait/WaitForExec.php +++ b/src/Wait/WaitForExec.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace Testcontainers\Wait; use Closure; -use Docker\API\Model\ContainersIdExecPostBody; use Docker\API\Model\ExecIdJsonGetResponse200; use Testcontainers\Container\StartedTestContainer; use Testcontainers\Exception\ContainerWaitingTimeoutException; @@ -15,8 +14,6 @@ use Testcontainers\Exception\ContainerWaitingTimeoutException; */ class WaitForExec extends BaseWaitStrategy { - protected ContainersIdExecPostBody $execConfig; - /** * @param array $command */ diff --git a/src/Wait/WaitForHealthCheck.php b/src/Wait/WaitForHealthCheck.php index 74a13f2..9bef414 100644 --- a/src/Wait/WaitForHealthCheck.php +++ b/src/Wait/WaitForHealthCheck.php @@ -9,6 +9,7 @@ use Http\Client\Socket\Exception\TimeoutException; use Testcontainers\Container\StartedTestContainer; use Testcontainers\Exception\ContainerNotReadyException; +//TODO: not ready yet class WaitForHealthCheck extends BaseWaitStrategy { public function __construct(protected int $timeout = 5000, protected int $pollInterval = 1000) diff --git a/src/Wait/WaitForHttp.php b/src/Wait/WaitForHttp.php index 906dc8b..87a840b 100644 --- a/src/Wait/WaitForHttp.php +++ b/src/Wait/WaitForHttp.php @@ -7,6 +7,7 @@ namespace Testcontainers\Wait; use Docker\Docker; use Testcontainers\Exception\ContainerNotReadyException; +//TODO: not ready yet class WaitForHttp implements WaitStrategy { public const METHOD_GET = 'GET'; @@ -61,7 +62,7 @@ class WaitForHttp implements WaitStrategy $containerNetworks = $this->dockerClient->containerInspect($id)->getNetworkSettings()->getNetworks(); $containerAddress = null; foreach ($containerNetworks as $network) { - if($network->getNetworkID() === $id) { + if ($network->getNetworkID() === $id) { $containerAddress = $network->getIpAddress(); break; } diff --git a/src/Wait/WaitForTcpPortOpen.php b/src/Wait/WaitForTcpPortOpen.php index e6c3700..9545a88 100644 --- a/src/Wait/WaitForTcpPortOpen.php +++ b/src/Wait/WaitForTcpPortOpen.php @@ -9,6 +9,7 @@ use JsonException; use RuntimeException; use Testcontainers\Exception\ContainerNotReadyException; +//TODO: not ready yet final class WaitForTcpPortOpen implements WaitStrategy { private Docker $dockerClient; diff --git a/tests/Integration/OldTests/ContainerTest.php b/tests/Integration/OldTests/ContainerTest.php index d7147b5..ed65412 100644 --- a/tests/Integration/OldTests/ContainerTest.php +++ b/tests/Integration/OldTests/ContainerTest.php @@ -18,6 +18,7 @@ use Testcontainers\Container\RedisContainer; class ContainerTest extends TestCase { //TODO: remove after check + //To make it work, fixed port should be first implemented protected function setUp(): void { $this->markTestIncomplete(); diff --git a/tests/Integration/OldTests/WaitStrategyTest.php b/tests/Integration/OldTests/WaitStrategyTest.php index 060ca5b..9c164e9 100644 --- a/tests/Integration/OldTests/WaitStrategyTest.php +++ b/tests/Integration/OldTests/WaitStrategyTest.php @@ -54,104 +54,104 @@ class WaitStrategyTest extends TestCase $this->assertNotEmpty($version); } - public function testWaitForLog(): void - { - $container = Container::make('redis:6.2.5') - ->withWait(new WaitForLog('Ready to accept connections')); + public function testWaitForLog(): void + { + $container = Container::make('redis:6.2.5') + ->withWait(new WaitForLog('Ready to accept connections')); - $container->run(); + $container->run(); - $redis = new Client([ - 'scheme' => 'tcp', - 'host' => $container->getAddress(), - 'port' => 6379, - ]); + $redis = new Client([ + 'scheme' => 'tcp', + 'host' => $container->getAddress(), + 'port' => 6379, + ]); - $redis->set('foo', 'bar'); + $redis->set('foo', 'bar'); - $this->assertEquals('bar', $redis->get('foo')); + $this->assertEquals('bar', $redis->get('foo')); - $container->stop(); + $container->stop(); - $this->expectException(ConnectionException::class); + $this->expectException(ConnectionException::class); - $redis->get('foo'); + $redis->get('foo'); - $container->remove(); + $container->remove(); + } + + public function testWaitForHTTP(): void + { + $container = Container::make('nginx:alpine') + ->withWait(WaitForHttp::make(80)); + + $container->run(); + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, sprintf('http://%s:%d', $container->getAddress(), 80)); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + $response = (string) curl_exec($ch); + + curl_close($ch); + + $this->assertNotEmpty($response); + } + + /** + * @dataProvider provideWaitForTcpPortOpen + */ + public function testWaitForTcpPortOpen(bool $wait): void + { + $container = Container::make('nginx:alpine'); + + if ($wait) { + $container->withWait(WaitForTcpPortOpen::make(80)); } - public function testWaitForHTTP(): void - { - $container = Container::make('nginx:alpine') - ->withWait(WaitForHttp::make(80)); + $container->run(); - $container->run(); - - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, sprintf('http://%s:%d', $container->getAddress(), 80)); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - - $response = (string) curl_exec($ch); - - curl_close($ch); - - $this->assertNotEmpty($response); + if ($wait) { + static::assertIsResource(fsockopen($container->getAddress(), 80), 'Failed to connect to container'); + return; } - /** - * @dataProvider provideWaitForTcpPortOpen - */ - public function testWaitForTcpPortOpen(bool $wait): void - { - $container = Container::make('nginx:alpine'); + $containerId = $container->getId(); - if ($wait) { - $container->withWait(WaitForTcpPortOpen::make(80)); - } + $this->expectExceptionObject(new ContainerNotReadyException($containerId)); - $container->run(); + (new WaitForTcpPortOpen(8080))->wait($containerId); + } - if ($wait) { - static::assertIsResource(fsockopen($container->getAddress(), 80), 'Failed to connect to container'); - return; - } + /** + * @return array> + */ + public function provideWaitForTcpPortOpen(): array + { + return [ + 'Can connect to container' => [true], + 'Cannot connect to container' => [false], + ]; + } - $containerId = $container->getId(); + public function testWaitForHealthCheck(): void + { + $container = Container::make('nginx') + ->withHealthCheckCommand('curl --fail http://localhost') + ->withWait(new WaitForHealthCheck()); - $this->expectExceptionObject(new ContainerNotReadyException($containerId)); + $container->run(); - (new WaitForTcpPortOpen(8080))->wait($containerId); - } + $ch = curl_init(); - /** - * @return array> - */ - public function provideWaitForTcpPortOpen(): array - { - return [ - 'Can connect to container' => [true], - 'Cannot connect to container' => [false], - ]; - } + curl_setopt($ch, CURLOPT_URL, sprintf('http://%s:%d', $container->getAddress(), 80)); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - public function testWaitForHealthCheck(): void - { - $container = Container::make('nginx') - ->withHealthCheckCommand('curl --fail http://localhost') - ->withWait(new WaitForHealthCheck()); + $response = curl_exec($ch); - $container->run(); + $this->assertNotEmpty($response); + $this->assertIsString($response); - $ch = curl_init(); - - curl_setopt($ch, CURLOPT_URL, sprintf('http://%s:%d', $container->getAddress(), 80)); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - - $response = curl_exec($ch); - - $this->assertNotEmpty($response); - $this->assertIsString($response); - - $this->assertStringContainsString('Welcome to nginx!', $response); - } + $this->assertStringContainsString('Welcome to nginx!', $response); + } }