added beluga-php/docker-php client and some basic updates to the base Container class

This commit is contained in:
Sergei Shitikov
2024-08-22 16:41:51 +02:00
parent 68a2d6d47c
commit c82e974ab9
8 changed files with 154 additions and 133 deletions
+4 -2
View File
@@ -15,7 +15,8 @@
], ],
"require": { "require": {
"php": ">= 8.1", "php": ">= 8.1",
"symfony/process": "^5.0|^6.0|^7.0" "beluga-php/docker-php": "^1.45",
"symfony/http-client": "^7.1"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^9.5", "phpunit/phpunit": "^9.5",
@@ -44,7 +45,8 @@
}, },
"config": { "config": {
"allow-plugins": { "allow-plugins": {
"phpstan/extension-installer": true "phpstan/extension-installer": true,
"php-http/discovery": true
} }
} }
} }
+93 -95
View File
@@ -4,11 +4,17 @@ declare(strict_types=1);
namespace Testcontainers\Container; namespace Testcontainers\Container;
use Symfony\Component\Process\Process; use Docker\API\Model\ContainersCreatePostBody;
use Docker\API\Model\ContainersIdExecPostBody;
use Docker\API\Model\EndpointSettings;
use Docker\API\Model\HealthConfig;
use Docker\API\Model\Mount;
use Docker\API\Model\NetworkingConfig;
use Docker\API\Model\Port;
use Docker\Docker;
use Psr\Http\Message\ResponseInterface;
use Testcontainers\Exception\ContainerNotReadyException; use Testcontainers\Exception\ContainerNotReadyException;
use Testcontainers\Registry; use Testcontainers\Registry;
use Testcontainers\Trait\DockerContainerAwareTrait;
use Testcontainers\Wait\WaitForNothing;
use Testcontainers\Wait\WaitInterface; use Testcontainers\Wait\WaitInterface;
/** /**
@@ -19,43 +25,44 @@ use Testcontainers\Wait\WaitInterface;
*/ */
class Container class Container
{ {
use DockerContainerAwareTrait; protected Docker $dockerClient;
private string $id; protected ContainersCreatePostBody $containerConfig;
private ?string $entryPoint = null; protected string $image;
protected string $containerName;
protected string $id;
protected ?string $entryPoint = null;
protected ?HealthConfig $healthConfig = null;
/** /**
* @var array<string, string> * @var array<string, string>
*/ */
private array $env = []; protected array $env = [];
private Process $process; protected WaitInterface $wait;
private WaitInterface $wait;
private bool $privileged = false; protected bool $privileged = false;
private ?string $network = null; protected ?string $networkName = null;
private ?string $healthCheckCommand = null;
private int $healthCheckIntervalInMS;
/** /**
* @var ContainerInspect * @var array<Mount>
*/ */
private array $inspectedData; protected array $mounts = [];
/** /**
* @var array<string> * @var array<Port>
*/ */
private array $mounts = []; protected array $ports = [];
/** protected function __construct(string $image)
* @var array<string>
*/
private array $ports = [];
protected function __construct(private string $image)
{ {
$this->wait = new WaitForNothing(); $this->image = $image;
$this->dockerClient = Docker::create();
} }
public static function make(string $image): self public static function make(string $image): self
@@ -98,24 +105,24 @@ class Container
public function withHealthCheckCommand(string $command, int $healthCheckIntervalInMS = 1000): self public function withHealthCheckCommand(string $command, int $healthCheckIntervalInMS = 1000): self
{ {
$this->healthCheckCommand = $command; $this->healthConfig = new HealthConfig([
$this->healthCheckIntervalInMS = $healthCheckIntervalInMS; 'Test' => ['CMD', $command],
'Interval' => $healthCheckIntervalInMS,
]);
return $this; return $this;
} }
public function withMount(string $localPath, string $containerPath): self public function withMount(string $localPath, string $containerPath): self
{ {
$this->mounts[] = '-v'; $this->mounts[] = new Mount(['type' => 'bind', 'source' => $localPath, 'target' => $containerPath]);
$this->mounts[] = sprintf('%s:%s', $localPath, $containerPath);
return $this; return $this;
} }
public function withPort(string $localPort, string $containerPort): self public function withPort(string $localPort, string $containerPort): self
{ {
$this->ports[] = '-p'; $this->ports[] = new Port(['privatePort' => (int) $containerPort, 'publicPort' => (int) $localPort]);
$this->ports[] = sprintf('%s:%s', $localPort, $containerPort);
return $this; return $this;
} }
@@ -127,60 +134,52 @@ class Container
return $this; return $this;
} }
public function withNetwork(string $network): self public function withNetwork(string $networkName): self
{ {
$this->network = $network; $this->networkName = $networkName;
return $this; return $this;
} }
public function run(bool $wait = true): self public function run(bool $wait = true): self
{ {
$this->id = uniqid('testcontainer', true); $this->containerName = uniqid('testcontainer', true);
$params = [ $this->containerConfig = new ContainersCreatePostBody();
'docker', $this->containerConfig->setImage($this->image);
'run',
'--rm',
'--detach',
'--name',
$this->id,
...$this->mounts,
...$this->ports,
];
$envs = [];
foreach ($this->env as $name => $value) { foreach ($this->env as $name => $value) {
$params[] = '--env'; $envs[] = $name . '=' . $value;
$params[] = $name . '=' . $value;
} }
if ($this->healthCheckCommand !== null) { $this->containerConfig->setEnv($envs);
$params[] = '--health-cmd';
$params[] = $this->healthCheckCommand; if ($this->healthConfig !== null) {
$params[] = '--health-interval'; $this->containerConfig->setHealthcheck($this->healthConfig);
$params[] = $this->healthCheckIntervalInMS . 'ms';
} }
if ($this->network !== null) { if ($this->networkName !== null) {
$params[] = '--network'; $this->containerConfig->setNetworkingConfig(new NetworkingConfig([
$params[] = $this->network; 'endpointsConfig' => [
$this->networkName => new EndpointSettings([
'aliases' => [$this->containerName],
'networkID' => $this->networkName,
]),
]]));
} }
if ($this->entryPoint !== null) { if ($this->entryPoint !== null) {
$params[] = '--entrypoint'; $this->containerConfig->setEntrypoint([$this->entryPoint]);
$params[] = $this->entryPoint;
} }
if ($this->privileged) { if ($this->privileged) {
$params[] = '--privileged'; //TODO: Implement privileged mode
} }
$params[] = $this->image; $containerCreateResponse = $this->dockerClient->containerCreate($this->containerConfig, ['name' => $this->containerName]);
$this->process = new Process($params); $this->id = $containerCreateResponse->getId();
$this->process->mustRun();
$this->inspectedData = self::dockerContainerInspect($this->id);
Registry::add($this); Registry::add($this);
@@ -193,46 +192,45 @@ class Container
public function wait(int $wait = 100): self public function wait(int $wait = 100): self
{ {
for ($i = 0; $i < $wait; $i++) { usleep(500000);
try { return $this;
$this->wait->wait($this->id);
return $this;
} catch (ContainerNotReadyException $e) {
usleep(500000);
}
}
throw new ContainerNotReadyException($this->id); // for ($i = 0; $i < $wait; $i++) {
// try {
// $this->dockerClient->containerWait($this->id);
// return $this;
// } catch (ContainerNotReadyException $e) {
// usleep(500000);
// }
// }
//
// throw new ContainerNotReadyException($this->id);
} }
public function stop(): self public function stop(): self
{ {
$stop = new Process(['docker', 'stop', $this->id]); $this->dockerClient->containerStop($this->id);
$stop->mustRun();
return $this; return $this;
} }
public function start(): self public function start(): self
{ {
$start = new Process(['docker', 'start', $this->id]); $this->dockerClient->containerStart($this->id);
$start->mustRun();
return $this; return $this;
} }
public function restart(): self public function restart(): self
{ {
$restart = new Process(['docker', 'restart', $this->id]); $this->dockerClient->containerRestart($this->id);
$restart->mustRun();
return $this; return $this;
} }
public function remove(): self public function remove(): self
{ {
$remove = new Process(['docker', 'rm', '-f', $this->id]); $this->dockerClient->containerDelete($this->id);
$remove->mustRun();
Registry::remove($this); Registry::remove($this);
@@ -241,37 +239,37 @@ class Container
public function kill(): self public function kill(): self
{ {
$kill = new Process(['docker', 'kill', $this->id]); $this->dockerClient->containerKill($this->id);
$kill->mustRun();
return $this; return $this;
} }
/** /**
* @param array<string> $command * @param array<string> $commandAsArray
*/ */
public function execute(array $command): Process public function execute(array $commandAsArray): ResponseInterface
{ {
$process = new Process(['docker', 'exec', $this->id, ...$command]); $command = new ContainersIdExecPostBody();
$process->mustRun(); $command->setCmd($commandAsArray);
return $this->dockerClient->containerExec($this->id, $command);
return $process;
} }
public function logs(): string public function logs(): string
{ {
$logs = new Process(['docker', 'logs', $this->id]); return $this->dockerClient->containerLogs($this->id)?->getBody()?->getContents() ?? '';
$logs->mustRun();
return $logs->getOutput();
} }
public function getAddress(): string public function getAddress(): string
{ {
return self::dockerContainerAddress( $containerNetworks = $this->dockerClient->containerInspect($this->id)
containerId: $this->id, ->getNetworkSettings()->getNetworks();
networkName: $this->network, $containerAddress = '';
inspectedData: $this->inspectedData foreach ($containerNetworks as $network) {
); if($network->getNetworkID() === $this->id) {
$containerAddress = $network->getIpAddress();
break;
}
}
return $containerAddress;
} }
} }
+17 -13
View File
@@ -5,31 +5,35 @@ declare(strict_types=1);
namespace Testcontainers\Wait; namespace Testcontainers\Wait;
use Closure; use Closure;
use Symfony\Component\Process\Process; use Docker\API\Model\ContainersIdExecPostBody;
use Docker\API\Model\ExecIdStartPostBody;
use Docker\Docker;
use Testcontainers\Exception\ContainerNotReadyException; use Testcontainers\Exception\ContainerNotReadyException;
class WaitForExec implements WaitInterface class WaitForExec implements WaitInterface
{ {
protected Docker $dockerClient;
protected ContainersIdExecPostBody $execConfig;
/** /**
* @param array<string> $command * @param array<string> $command
*/ */
public function __construct(private array $command, private ?Closure $checkFunction = null) public function __construct(private array $command, private ?Closure $checkFunction = null)
{ {
$this->dockerClient = Docker::create();
$execConfig = new ContainersIdExecPostBody();
$execConfig->setTty(true);
$execConfig->setAttachStdout(true);
$execConfig->setAttachStderr(true);
$execConfig->setCmd($this->command);
} }
public function wait(string $id): void public function wait(string $id): void
{ {
$process = new Process(['docker', 'exec', $id, ...$this->command]); $execid = $this->dockerClient->containerExec($id, $this->execConfig)->getId() ?? '';
$execStartConfig = new ExecIdStartPostBody();
try { $execStartConfig->setDetach(false);
$process->mustRun(); $this->dockerClient->execStart($execid, $execStartConfig);
} catch (\Exception $e) {
throw new ContainerNotReadyException($id, $e);
}
if ($this->checkFunction !== null) {
$func = $this->checkFunction;
$func($process);
}
} }
} }
+10 -12
View File
@@ -4,24 +4,22 @@ declare(strict_types=1);
namespace Testcontainers\Wait; namespace Testcontainers\Wait;
use RuntimeException; use Docker\Docker;
use Symfony\Component\Process\Process;
use Testcontainers\Exception\ContainerNotReadyException; use Testcontainers\Exception\ContainerNotReadyException;
class WaitForHealthCheck implements WaitInterface class WaitForHealthCheck implements WaitInterface
{ {
protected Docker $dockerClient;
public function __construct()
{
$this->dockerClient = Docker::create();
}
public function wait(string $id): void public function wait(string $id): void
{ {
$process = new Process(['docker', 'inspect', '--format', '{{json .State.Health.Status}}', $id]); $containerInspect = $this->dockerClient->containerInspect($id);
$process->mustRun(); $containerInspect->getBody()->getContents();
dd($containerInspect->getStatusCode());
$status = json_decode($process->getOutput(), true, 512, JSON_THROW_ON_ERROR);
if (!is_string($status)) {
throw new ContainerNotReadyException($id, new RuntimeException('Invalid json output'));
}
$status = trim($status, '"');
if ($status !== 'healthy') { if ($status !== 'healthy') {
throw new ContainerNotReadyException($id); throw new ContainerNotReadyException($id);
+11 -4
View File
@@ -4,13 +4,11 @@ declare(strict_types=1);
namespace Testcontainers\Wait; namespace Testcontainers\Wait;
use Docker\Docker;
use Testcontainers\Exception\ContainerNotReadyException; use Testcontainers\Exception\ContainerNotReadyException;
use Testcontainers\Trait\DockerContainerAwareTrait;
class WaitForHttp implements WaitInterface class WaitForHttp implements WaitInterface
{ {
use DockerContainerAwareTrait;
public const METHOD_GET = 'GET'; public const METHOD_GET = 'GET';
public const METHOD_POST = 'POST'; public const METHOD_POST = 'POST';
public const METHOD_PUT = 'PUT'; public const METHOD_PUT = 'PUT';
@@ -22,9 +20,11 @@ class WaitForHttp implements WaitInterface
private string $method = 'GET'; private string $method = 'GET';
private string $path = '/'; private string $path = '/';
private int $statusCode = 200; private int $statusCode = 200;
private Docker $dockerClient;
public function __construct(private int $port) public function __construct(private int $port)
{ {
$this->dockerClient = Docker::create();
} }
public static function make(int $port): self public static function make(int $port): self
@@ -58,7 +58,14 @@ class WaitForHttp implements WaitInterface
public function wait(string $id): void public function wait(string $id): void
{ {
$containerAddress = self::dockerContainerAddress(containerId: $id); $containerNetworks = $this->dockerClient->containerInspect($id)->getNetworkSettings()->getNetworks();
$containerAddress = null;
foreach ($containerNetworks as $network) {
if($network->getNetworkID() === $id) {
$containerAddress = $network->getIpAddress();
break;
}
}
$ch = curl_init(); $ch = curl_init();
curl_setopt($ch, CURLOPT_URL, sprintf('http://%s:%d%s', $containerAddress, $this->port, $this->path)); curl_setopt($ch, CURLOPT_URL, sprintf('http://%s:%d%s', $containerAddress, $this->port, $this->path));
+6 -3
View File
@@ -4,21 +4,24 @@ declare(strict_types=1);
namespace Testcontainers\Wait; namespace Testcontainers\Wait;
use Docker\Docker;
use Symfony\Component\Process\Process; use Symfony\Component\Process\Process;
use Testcontainers\Exception\ContainerNotReadyException; use Testcontainers\Exception\ContainerNotReadyException;
class WaitForLog implements WaitInterface class WaitForLog implements WaitInterface
{ {
protected Docker $dockerClient;
public function __construct(private string $message, private bool $enableRegex = false) public function __construct(private string $message, private bool $enableRegex = false)
{ {
$this->dockerClient = Docker::create();
} }
public function wait(string $id): void public function wait(string $id): void
{ {
$process = new Process(['docker', 'logs', $id]); $logs = $this->dockerClient->containerLogs($id);
$process->mustRun();
$output = $process->getOutput() . PHP_EOL . $process->getErrorOutput(); $output = $logs->getBody()->getContents();
if ($this->enableRegex) { if ($this->enableRegex) {
if (!preg_match($this->message, $output)) { if (!preg_match($this->message, $output)) {
+13 -3
View File
@@ -4,17 +4,18 @@ declare(strict_types=1);
namespace Testcontainers\Wait; namespace Testcontainers\Wait;
use Docker\Docker;
use JsonException; use JsonException;
use RuntimeException; use RuntimeException;
use Testcontainers\Exception\ContainerNotReadyException; use Testcontainers\Exception\ContainerNotReadyException;
use Testcontainers\Trait\DockerContainerAwareTrait;
final class WaitForTcpPortOpen implements WaitInterface final class WaitForTcpPortOpen implements WaitInterface
{ {
use DockerContainerAwareTrait; private Docker $dockerClient;
public function __construct(private readonly int $port, private readonly ?string $network = null) public function __construct(private readonly int $port, private readonly ?string $network = null)
{ {
$this->dockerClient = Docker::create();
} }
public static function make(int $port, ?string $network = null): self public static function make(int $port, ?string $network = null): self
@@ -27,7 +28,16 @@ final class WaitForTcpPortOpen implements WaitInterface
*/ */
public function wait(string $id): void public function wait(string $id): void
{ {
if (@fsockopen(self::dockerContainerAddress(containerId: $id, networkName: $this->network), $this->port) === false) { $containerInspectResult = $this->dockerClient->containerInspect($id);
$dockerContainerNetworks = $containerInspectResult->getNetworkSettings()->getNetworks();
$dockerContainerAddress = '';
foreach ($dockerContainerNetworks as $network) {
if ($network->getNetworkID() === $this->network) {
$dockerContainerAddress = $network->getIPAddress();
break;
}
}
if (@fsockopen($dockerContainerAddress, $this->port) === false) {
throw new ContainerNotReadyException($id, new RuntimeException('Unable to connect to container TCP port')); throw new ContainerNotReadyException($id, new RuntimeException('Unable to connect to container TCP port'));
} }
} }
-1
View File
@@ -20,7 +20,6 @@ use Testcontainers\Wait\WaitForTcpPortOpen;
class WaitStrategyTest extends TestCase class WaitStrategyTest extends TestCase
{ {
use DockerContainerAwareTrait;
public static function tearDownAfterClass(): void public static function tearDownAfterClass(): void
{ {