mirror of
https://github.com/stan220/testcontainers-php.git
synced 2026-09-08 17:29:29 +00:00
refactor start() method of the GenericContainer
This commit is contained in:
@@ -5,16 +5,20 @@ declare(strict_types=1);
|
||||
namespace Testcontainers\Container;
|
||||
|
||||
use Docker\API\Exception\ContainerCreateNotFoundException;
|
||||
use Docker\API\Model\ContainerCreateResponse;
|
||||
use Docker\API\Model\ContainersCreatePostBody;
|
||||
use Docker\API\Model\EndpointSettings;
|
||||
use Docker\API\Model\HealthConfig;
|
||||
use Docker\API\Model\HostConfig;
|
||||
use Docker\API\Model\Mount;
|
||||
use Docker\API\Model\NetworkingConfig;
|
||||
use Docker\API\Model\PortBinding;
|
||||
use Docker\Docker;
|
||||
use Docker\Stream\CreateImageStream;
|
||||
use InvalidArgumentException;
|
||||
use Testcontainers\ContainerClient\DockerContainerClient;
|
||||
use Testcontainers\Utils\PortGenerator\RandomUniquePortGenerator;
|
||||
use Testcontainers\Utils\PortNormalizer;
|
||||
use Testcontainers\Wait\WaitForContainer;
|
||||
use Testcontainers\Wait\WaitStrategy;
|
||||
|
||||
@@ -144,37 +148,13 @@ class GenericContainer implements TestContainer
|
||||
$this->withExposedPorts(...$port);
|
||||
} else {
|
||||
// Handle single port entry, either string or int
|
||||
$this->exposedPorts[] = $this->normalizePort($port);
|
||||
$this->exposedPorts[] = PortNormalizer::normalizePort($port);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
$this->isPrivileged = $privileged;
|
||||
@@ -190,54 +170,16 @@ class GenericContainer implements TestContainer
|
||||
return $this;
|
||||
}
|
||||
|
||||
//TODO: needs refactoring
|
||||
public function start(): StartedGenericContainer
|
||||
{
|
||||
$containerConfig = $this->createContainerConfig();
|
||||
try {
|
||||
$containerCreatePostBody = new ContainersCreatePostBody();
|
||||
//handle withExposedPorts
|
||||
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);
|
||||
/** @var ContainerCreateResponse|null $containerCreateResponse */
|
||||
$containerCreateResponse = $this->dockerClient->containerCreate($containerConfig);
|
||||
$this->id = $containerCreateResponse?->getId() ?? '';
|
||||
} catch (ContainerCreateNotFoundException) {
|
||||
/** @var CreateImageStream $imageCreateResponse */
|
||||
$imageCreateResponse = $this->dockerClient->imageCreate(null, [
|
||||
'fromImage' => explode(':', $this->image)[0],
|
||||
'tag' => explode(':', $this->image)[1] ?? 'latest',
|
||||
]);
|
||||
$imageCreateResponse->wait();
|
||||
|
||||
// If the image is not found, pull it and try again
|
||||
$this->pullImage();
|
||||
return $this->start();
|
||||
}
|
||||
|
||||
@@ -252,4 +194,94 @@ class GenericContainer implements TestContainer
|
||||
|
||||
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