mirror of
https://github.com/stan220/testcontainers-php.git
synced 2026-09-12 11:29:36 +00:00
refactor start() method of the GenericContainer
This commit is contained in:
@@ -5,16 +5,20 @@ declare(strict_types=1);
|
|||||||
namespace Testcontainers\Container;
|
namespace Testcontainers\Container;
|
||||||
|
|
||||||
use Docker\API\Exception\ContainerCreateNotFoundException;
|
use Docker\API\Exception\ContainerCreateNotFoundException;
|
||||||
|
use Docker\API\Model\ContainerCreateResponse;
|
||||||
use Docker\API\Model\ContainersCreatePostBody;
|
use Docker\API\Model\ContainersCreatePostBody;
|
||||||
|
use Docker\API\Model\EndpointSettings;
|
||||||
use Docker\API\Model\HealthConfig;
|
use Docker\API\Model\HealthConfig;
|
||||||
use Docker\API\Model\HostConfig;
|
use Docker\API\Model\HostConfig;
|
||||||
use Docker\API\Model\Mount;
|
use Docker\API\Model\Mount;
|
||||||
|
use Docker\API\Model\NetworkingConfig;
|
||||||
use Docker\API\Model\PortBinding;
|
use Docker\API\Model\PortBinding;
|
||||||
use Docker\Docker;
|
use Docker\Docker;
|
||||||
use Docker\Stream\CreateImageStream;
|
use Docker\Stream\CreateImageStream;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
use Testcontainers\ContainerClient\DockerContainerClient;
|
use Testcontainers\ContainerClient\DockerContainerClient;
|
||||||
use Testcontainers\Utils\PortGenerator\RandomUniquePortGenerator;
|
use Testcontainers\Utils\PortGenerator\RandomUniquePortGenerator;
|
||||||
|
use Testcontainers\Utils\PortNormalizer;
|
||||||
use Testcontainers\Wait\WaitForContainer;
|
use Testcontainers\Wait\WaitForContainer;
|
||||||
use Testcontainers\Wait\WaitStrategy;
|
use Testcontainers\Wait\WaitStrategy;
|
||||||
|
|
||||||
@@ -144,37 +148,13 @@ class GenericContainer implements TestContainer
|
|||||||
$this->withExposedPorts(...$port);
|
$this->withExposedPorts(...$port);
|
||||||
} else {
|
} else {
|
||||||
// Handle single port entry, either string or int
|
// Handle single port entry, either string or int
|
||||||
$this->exposedPorts[] = $this->normalizePort($port);
|
$this->exposedPorts[] = PortNormalizer::normalizePort($port);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalize a port specification to ensure it includes a protocol.
|
|
||||||
* Defaults to 'tcp' if no protocol is specified.
|
|
||||||
*
|
|
||||||
* @param string|int $port Port to normalize.
|
|
||||||
* @return string Normalized port string.
|
|
||||||
*
|
|
||||||
* TODO: move this to a utility class
|
|
||||||
*/
|
|
||||||
private function normalizePort(string|int $port): string
|
|
||||||
{
|
|
||||||
if (is_int($port)) {
|
|
||||||
// Direct integer ports default to tcp
|
|
||||||
return "{$port}/tcp";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the port specification already includes a protocol
|
|
||||||
if (is_string($port) && !str_contains($port, '/')) {
|
|
||||||
return "{$port}/tcp";
|
|
||||||
}
|
|
||||||
|
|
||||||
return $port;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function withPrivilegedMode(bool $privileged = true): static
|
public function withPrivilegedMode(bool $privileged = true): static
|
||||||
{
|
{
|
||||||
$this->isPrivileged = $privileged;
|
$this->isPrivileged = $privileged;
|
||||||
@@ -190,54 +170,16 @@ class GenericContainer implements TestContainer
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: needs refactoring
|
|
||||||
public function start(): StartedGenericContainer
|
public function start(): StartedGenericContainer
|
||||||
{
|
{
|
||||||
|
$containerConfig = $this->createContainerConfig();
|
||||||
try {
|
try {
|
||||||
$containerCreatePostBody = new ContainersCreatePostBody();
|
/** @var ContainerCreateResponse|null $containerCreateResponse */
|
||||||
//handle withExposedPorts
|
$containerCreateResponse = $this->dockerClient->containerCreate($containerConfig);
|
||||||
if (!empty($this->exposedPorts)) {
|
|
||||||
$portGenerator = new RandomUniquePortGenerator();
|
|
||||||
$portMap = new \ArrayObject();
|
|
||||||
|
|
||||||
foreach ($this->exposedPorts as $port) {
|
|
||||||
$portBinding = new PortBinding();
|
|
||||||
$portBinding->setHostPort((string) $portGenerator->generatePort());
|
|
||||||
$portBinding->setHostIp('0.0.0.0');
|
|
||||||
$portMap[$port] = [$portBinding];
|
|
||||||
}
|
|
||||||
|
|
||||||
$hostConfig = new HostConfig();
|
|
||||||
$hostConfig->setPortBindings($portMap);
|
|
||||||
//handle withPrivilegedMode
|
|
||||||
if ($this->isPrivileged) {
|
|
||||||
$hostConfig->setPrivileged($this->isPrivileged);
|
|
||||||
}
|
|
||||||
$containerCreatePostBody->setHostConfig($hostConfig);
|
|
||||||
}
|
|
||||||
//handle withPrivilegedMode
|
|
||||||
if ($this->isPrivileged) {
|
|
||||||
$hostConfig = new HostConfig();
|
|
||||||
$hostConfig->setPrivileged($this->isPrivileged);
|
|
||||||
}
|
|
||||||
$containerCreatePostBody->setImage($this->image);
|
|
||||||
$containerCreatePostBody->setCmd($this->command);
|
|
||||||
$envs = [];
|
|
||||||
foreach ($this->env as $key => $value) {
|
|
||||||
$envs[] = $key . '=' . $value;
|
|
||||||
}
|
|
||||||
$containerCreatePostBody->setEnv($envs);
|
|
||||||
|
|
||||||
$containerCreateResponse = $this->dockerClient->containerCreate($containerCreatePostBody);
|
|
||||||
$this->id = $containerCreateResponse?->getId() ?? '';
|
$this->id = $containerCreateResponse?->getId() ?? '';
|
||||||
} catch (ContainerCreateNotFoundException) {
|
} catch (ContainerCreateNotFoundException) {
|
||||||
/** @var CreateImageStream $imageCreateResponse */
|
// If the image is not found, pull it and try again
|
||||||
$imageCreateResponse = $this->dockerClient->imageCreate(null, [
|
$this->pullImage();
|
||||||
'fromImage' => explode(':', $this->image)[0],
|
|
||||||
'tag' => explode(':', $this->image)[1] ?? 'latest',
|
|
||||||
]);
|
|
||||||
$imageCreateResponse->wait();
|
|
||||||
|
|
||||||
return $this->start();
|
return $this->start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,4 +194,94 @@ class GenericContainer implements TestContainer
|
|||||||
|
|
||||||
return $startedContainer;
|
return $startedContainer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function createContainerConfig(): ContainersCreatePostBody
|
||||||
|
{
|
||||||
|
$containerCreatePostBody = new ContainersCreatePostBody();
|
||||||
|
$containerCreatePostBody->setImage($this->image);
|
||||||
|
$containerCreatePostBody->setCmd($this->command);
|
||||||
|
|
||||||
|
$envs = array_map(static fn ($key, $value) => "$key=$value", array_keys($this->env), $this->env);
|
||||||
|
$containerCreatePostBody->setEnv($envs);
|
||||||
|
|
||||||
|
$hostConfig = $this->createHostConfig();
|
||||||
|
$containerCreatePostBody->setHostConfig($hostConfig);
|
||||||
|
|
||||||
|
if ($this->entryPoint !== null) {
|
||||||
|
$containerCreatePostBody->setEntrypoint([$this->entryPoint]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->healthConfig !== null) {
|
||||||
|
$containerCreatePostBody->setHealthcheck($this->healthConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->networkName !== null) {
|
||||||
|
$networkingConfig = new NetworkingConfig();
|
||||||
|
$endpointsConfig = new \ArrayObject([
|
||||||
|
$this->networkName => new EndpointSettings(),
|
||||||
|
]);
|
||||||
|
$networkingConfig->setEndpointsConfig($endpointsConfig);
|
||||||
|
$containerCreatePostBody->setNetworkingConfig($networkingConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $containerCreatePostBody;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createHostConfig(): ?HostConfig
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* For some reason, if some of the properties are not set, but HostConfig is returned,
|
||||||
|
* the API will throw ContainerCreateBadRequestException: bad parameter.
|
||||||
|
* Until it will be checked and fixed, we just return null if these properties are not set.
|
||||||
|
* */
|
||||||
|
if ($this->exposedPorts === [] && !$this->isPrivileged && $this->mounts === []) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$hostConfig = new HostConfig();
|
||||||
|
|
||||||
|
if ($this->exposedPorts !== []) {
|
||||||
|
$portBindings = $this->createPortBindings();
|
||||||
|
$hostConfig->setPortBindings($portBindings);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->isPrivileged) {
|
||||||
|
$hostConfig->setPrivileged(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->mounts !== []) {
|
||||||
|
$hostConfig->setMounts($this->mounts);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $hostConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return \ArrayObject<string, list<PortBinding>>
|
||||||
|
*/
|
||||||
|
protected function createPortBindings(): \ArrayObject
|
||||||
|
{
|
||||||
|
$portGenerator = new RandomUniquePortGenerator();
|
||||||
|
$portBindings = new \ArrayObject();
|
||||||
|
|
||||||
|
foreach ($this->exposedPorts as $port) {
|
||||||
|
$portBinding = new PortBinding();
|
||||||
|
$portBinding->setHostPort((string)$portGenerator->generatePort());
|
||||||
|
$portBinding->setHostIp('0.0.0.0');
|
||||||
|
$portBindings[$port] = [$portBinding];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $portBindings;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function pullImage(): void
|
||||||
|
{
|
||||||
|
[$fromImage, $tag] = explode(':', $this->image) + [1 => 'latest'];
|
||||||
|
/** @var CreateImageStream $imageCreateResponse */
|
||||||
|
$imageCreateResponse = $this->dockerClient->imageCreate(null, [
|
||||||
|
'fromImage' => $fromImage,
|
||||||
|
'tag' => $tag,
|
||||||
|
]);
|
||||||
|
$imageCreateResponse->wait();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Testcontainers\Container;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The IP protocols supported by Docker.
|
||||||
|
*/
|
||||||
|
enum InternetProtocol: string
|
||||||
|
{
|
||||||
|
case TCP = 'TCP';
|
||||||
|
case UDP = 'UDP';
|
||||||
|
|
||||||
|
public function toDockerNotation(): string
|
||||||
|
{
|
||||||
|
return strtolower($this->value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function fromDockerNotation(string $protocol): self
|
||||||
|
{
|
||||||
|
return self::from(strtoupper($protocol));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Testcontainers\Utils;
|
||||||
|
|
||||||
|
use Testcontainers\Container\InternetProtocol;
|
||||||
|
|
||||||
|
class PortNormalizer
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Normalize a port specification to ensure it includes a protocol.
|
||||||
|
* Defaults to 'tcp' if no protocol is specified.
|
||||||
|
*
|
||||||
|
* @param string|int $port Port to normalize.
|
||||||
|
* @return string Normalized port string.
|
||||||
|
*/
|
||||||
|
public static function normalizePort(string|int $port, InternetProtocol $internetProtocol = InternetProtocol::TCP): string
|
||||||
|
{
|
||||||
|
if (is_int($port)) {
|
||||||
|
// Direct integer ports default to tcp
|
||||||
|
return "{$port}/{$internetProtocol->toDockerNotation()}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the port specification already includes a protocol
|
||||||
|
if (is_string($port) && !str_contains($port, '/')) {
|
||||||
|
return "{$port}/{$internetProtocol->toDockerNotation()}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $port;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user