refactor start() method of the GenericContainer

This commit is contained in:
Sergei Shitikov
2024-09-29 11:11:37 +02:00
parent 1f9035f790
commit 2802dea75e
3 changed files with 156 additions and 68 deletions
+100 -68
View File
@@ -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();
}
}
+24
View File
@@ -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));
}
}
+32
View File
@@ -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;
}
}