From 1787eb8510f4d8b03f88a91308629b3b0803ec82 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Sun, 23 Nov 2025 13:31:08 +0530 Subject: [PATCH] feat-v2-containers --- .gitignore | 3 +- README.md | 58 +- composer.json | 10 +- docker-compose.yml | 6 +- src/Containers/Adapter.php | 68 +++ .../Adapter/DockerAPI.php | 52 +- src/Containers/Adapter/DockerCLI.php | 513 ++++++++++++++++++ src/Containers/Container.php | 17 + src/Containers/Containers.php | 162 ++++++ .../Exception/ContainersAuthException.php | 7 + .../ContainersDuplicateException.php | 7 + .../Exception/ContainersException.php | 7 + .../Exception/ContainersNotFoundException.php | 7 + .../Exception/ContainersTimeoutException.php | 7 + src/Containers/Mount.php | 8 + src/Containers/Mount/Bind.php | 16 + src/Containers/Mount/Tmpfs.php | 15 + src/Containers/Mount/Volume.php | 16 + src/Containers/Network.php | 14 + src/Containers/Networks.php | 67 +++ src/Containers/RestartPolicy.php | 11 + src/Containers/Usage.php | 19 + src/Orchestration/Adapter.php | 183 ------- src/Orchestration/Adapter/DockerCLI.php | 452 --------------- src/Orchestration/Container.php | 120 ---- src/Orchestration/Container/Stats.php | 89 --- src/Orchestration/Exception/Orchestration.php | 7 - src/Orchestration/Exception/Timeout.php | 7 - src/Orchestration/Network.php | 82 --- src/Orchestration/Orchestration.php | 255 --------- tests/Containers/Adapter/DockerAPITest.php | 34 ++ tests/Containers/Adapter/DockerCLITest.php | 34 ++ tests/{Orchestration => Containers}/Base.php | 181 +++--- .../Resources/.gitignore | 0 .../Resources/php/index.php | 0 .../Resources/php/logs.sh | 0 .../Resources/testfile.txt | 0 .../Resources/timeout/index.php | 0 tests/Orchestration/Adapter/DockerAPITest.php | 34 -- tests/Orchestration/Adapter/DockerCLITest.php | 34 -- 40 files changed, 1167 insertions(+), 1435 deletions(-) create mode 100644 src/Containers/Adapter.php rename src/{Orchestration => Containers}/Adapter/DockerAPI.php (90%) create mode 100644 src/Containers/Adapter/DockerCLI.php create mode 100644 src/Containers/Container.php create mode 100644 src/Containers/Containers.php create mode 100644 src/Containers/Exception/ContainersAuthException.php create mode 100644 src/Containers/Exception/ContainersDuplicateException.php create mode 100644 src/Containers/Exception/ContainersException.php create mode 100644 src/Containers/Exception/ContainersNotFoundException.php create mode 100644 src/Containers/Exception/ContainersTimeoutException.php create mode 100644 src/Containers/Mount.php create mode 100644 src/Containers/Mount/Bind.php create mode 100644 src/Containers/Mount/Tmpfs.php create mode 100644 src/Containers/Mount/Volume.php create mode 100644 src/Containers/Network.php create mode 100644 src/Containers/Networks.php create mode 100644 src/Containers/RestartPolicy.php create mode 100644 src/Containers/Usage.php delete mode 100644 src/Orchestration/Adapter.php delete mode 100644 src/Orchestration/Adapter/DockerCLI.php delete mode 100644 src/Orchestration/Container.php delete mode 100644 src/Orchestration/Container/Stats.php delete mode 100644 src/Orchestration/Exception/Orchestration.php delete mode 100644 src/Orchestration/Exception/Timeout.php delete mode 100644 src/Orchestration/Network.php delete mode 100644 src/Orchestration/Orchestration.php create mode 100644 tests/Containers/Adapter/DockerAPITest.php create mode 100644 tests/Containers/Adapter/DockerCLITest.php rename tests/{Orchestration => Containers}/Base.php (70%) rename tests/{Orchestration => Containers}/Resources/.gitignore (100%) rename tests/{Orchestration => Containers}/Resources/php/index.php (100%) rename tests/{Orchestration => Containers}/Resources/php/logs.sh (100%) rename tests/{Orchestration => Containers}/Resources/testfile.txt (100%) rename tests/{Orchestration => Containers}/Resources/timeout/index.php (100%) delete mode 100644 tests/Orchestration/Adapter/DockerAPITest.php delete mode 100644 tests/Orchestration/Adapter/DockerCLITest.php diff --git a/.gitignore b/.gitignore index 3e4071f..48e8138 100755 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /vendor/ /.idea/ .phpunit.result.cache -*.phpdisabled \ No newline at end of file +*.phpdisabled +.DS_Store diff --git a/README.md b/README.md index fca9c5e..5e76460 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# Utopia Orchestration +# Utopia Containers -[![Build Status](https://app.travis-ci.com/utopia-php/orchestration.svg?branch=main)](https://app.travis-ci.com/github/utopia-php/orchestration) -![Total Downloads](https://img.shields.io/packagist/dt/utopia-php/orchestration.svg) +[![Build Status](https://app.travis-ci.com/utopia-php/containers.svg?branch=main)](https://app.travis-ci.com/github/utopia-php/containers) +![Total Downloads](https://img.shields.io/packagist/dt/utopia-php/containers.svg) [![Discord](https://img.shields.io/discord/564160730845151244?label=discord)](https://appwrite.io/discord) -Utopia framework orchestration library is simple and lite library for abstracting the interaction with multiple container orchestrators. This library is aiming to be as simple and easy to learn and use. This library is maintained by the [Appwrite team](https://appwrite.io). +Utopia framework containers library is simple and lite library for abstracting the interaction with multiple container orchestrators. This library is aiming to be as simple and easy to learn and use. This library is maintained by the [Appwrite team](https://appwrite.io). Although this library is part of the [Utopia Framework](https://github.com/utopia-php/framework) project it is dependency free and can be used as standalone with any other PHP project or framework. @@ -12,7 +12,7 @@ Although this library is part of the [Utopia Framework](https://github.com/utopi Install using composer: ```bash -composer require utopia-php/orchestration +composer require utopia-php/containers ``` ### Example @@ -21,26 +21,26 @@ composer require utopia-php/orchestration require_once 'vendor/autoload.php'; -use Utopia\Orchestration\Orchestration; -use Utopia\Orchestration\Adapter\DockerCLI; +use Utopia\Containers\Containers; +use Utopia\Containers\Adapter\DockerCLI; -// Initialise Orchestration with Docker CLI adapter. -$orchestration = new Orchestration(new DockerCLI()); +// Initialise Containers with Docker CLI adapter. +$containers = new Containers(new DockerCLI()); // Pull the image. -$orchestration->pull('ubuntu:latest'); +$containers->pull('ubuntu:latest'); // Launch a ubuntu container that doesn't end using the tail command. -$containerID = $orchestration->run('ubuntu:latest', 'testContainer', ['tail', '-f', '/dev/null']); +$containerID = $containers->run('ubuntu:latest', 'testContainer', ['tail', '-f', '/dev/null']); $stderr = ''; $stdout = ''; // Execute a hello world command in the container -$orchestration->execute($containerID, ['echo', 'Hello World!'], $stdout, $stderr); +$containers->execute($containerID, ['echo', 'Hello World!'], $stdout, $stderr); // Remove the container forcefully since it's still running. -$orchestration->remove($containerID, true); +$containers->remove($containerID, true); ``` ## Usage @@ -53,30 +53,30 @@ There are currently two orchestrator adapters available and each of them has sli Directly communicates to the Docker Daemon using the Docker UNIX socket. ```php - use Utopia\Orchestration\Orchestration; - use Utopia\Orchestration\Adapter\DockerAPI; + use Utopia\Containers\Containers; + use Utopia\Containers\Adapter\DockerAPI; - $orchestration = new Orchestration(new DockerAPI($username, $password, $email)); + $containers = new Containers(new DockerAPI($username, $password, $email)); ``` $username, $password and $email are optional and are only used to pull private images from Docker Hub. - ### DockerCLI Uses the Docker CLI to communicate to the Docker Daemon. ```php - use Utopia\Orchestration\Orchestration; - use Utopia\Orchestration\Adapter\DockerCLI; + use Utopia\Containers\Containers; + use Utopia\Containers\Adapter\DockerCLI; - $orchestration = new Orchestration(new DockerCLI($username, $password)); + $containers = new Containers(new DockerCLI($username, $password)); ``` $username and $password are optional and are only used to pull private images from Docker Hub. -Once you have initialised your Orchestration object the following methods can be used: +Once you have initialised your Containers object the following methods can be used: - ### Pulling an image This method pulls the image requested from the orchestrators registry. It will return a boolean value indicating if the image was pulled successfully. ```php - $orchestration->pull('image:tag'); + $containers->pull('image:tag'); ```
@@ -96,7 +96,7 @@ Once you have initialised your Orchestration object the following methods can be This method creates and runs a new container. On success, it will return a string containing the container ID. On failure, it will throw an exception. ```php - $orchestration->run( + $containers->run( 'image:tag', 'name', ['echo', 'hello world!'], @@ -219,7 +219,7 @@ Once you have initialised your Orchestration object the following methods can be This method removes a container and returns a boolean value indicating if the container was removed successfully. ```php - $orchestration->remove('container_id', true); + $containers->remove('container_id', true); ```
@@ -243,7 +243,7 @@ Once you have initialised your Orchestration object the following methods can be This method returns an array of containers. ```php - $orchestration->list(['label' => 'value']); + $containers->list(['label' => 'value']); ```
@@ -263,7 +263,7 @@ Once you have initialised your Orchestration object the following methods can be This method returns an array of networks. ```php - $orchestration->listNetworks(); + $containers->listNetworks(); ```
@@ -281,7 +281,7 @@ Once you have initialised your Orchestration object the following methods can be This method creates a new network and returns a boolean value indicating if the network was created successfully. ```php - $orchestration->createNetwork('name', false); + $containers->createNetwork('name', false); ```
@@ -305,7 +305,7 @@ Once you have initialised your Orchestration object the following methods can be This method removes a network and returns a boolean value indicating if the network was removed successfully. ```php - $orchestration->removeNetwork('network_id'); + $containers->removeNetwork('network_id'); ```
@@ -325,7 +325,7 @@ Once you have initialised your Orchestration object the following methods can be This method connects a container to a network and returns a boolean value indicating if the connection was successful. ```php - $orchestration->connect('container_id', 'network_id'); + $containers->connect('container_id', 'network_id'); ```
@@ -349,7 +349,7 @@ Once you have initialised your Orchestration object the following methods can be This method disconnects a container from a network and returns a boolean value indicating if the removal was successful. ```php - $orchestration->disconnect('container_id', 'network_id', false); + $containers->disconnect('container_id', 'network_id', false); ```
diff --git a/composer.json b/composer.json index 4f6d81f..0cf7110 100755 --- a/composer.json +++ b/composer.json @@ -1,8 +1,8 @@ { - "name": "utopia-php/orchestration", - "description": "Lite & fast micro PHP abstraction library for container orchestration", + "name": "utopia-php/containers", + "description": "Lite & fast micro PHP abstraction library for container containers", "type": "library", - "keywords": ["php","framework", "upf", "utopia", "orchestration", "docker", "swarm", "kubernetes"], + "keywords": ["php","framework", "upf", "utopia", "containers", "docker", "swarm", "kubernetes"], "license": "MIT", "minimum-stability": "stable", "scripts": { @@ -12,10 +12,10 @@ "format": "./vendor/bin/pint" }, "autoload": { - "psr-4": {"Utopia\\Orchestration\\": "src/Orchestration"} + "psr-4": {"Utopia\\Containers\\": "src/Containers"} }, "autoload-dev": { - "psr-4": {"Utopia\\Tests\\": "tests/Orchestration"} + "psr-4": {"Utopia\\Tests\\": "tests/Containers"} }, "require": { "php": ">=8.0", diff --git a/docker-compose.yml b/docker-compose.yml index 9123116..c2b2586 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,10 +3,10 @@ version: '3.8' services: tests: container_name: tests - build: + build: context: . networks: - - orchestration + - containers environment: HOST_DIR: "$PWD" # Nessessary to mount test resources to child containers volumes: @@ -14,4 +14,4 @@ services: - /var/run/docker.sock:/var/run/docker.sock networks: - orchestration: \ No newline at end of file + containers: diff --git a/src/Containers/Adapter.php b/src/Containers/Adapter.php new file mode 100644 index 0000000..9d0f3ff --- /dev/null +++ b/src/Containers/Adapter.php @@ -0,0 +1,68 @@ + + */ + abstract public function listNetworks(): array; + + abstract public function getUsage(string $container): ?Usage; + + /** + * @param array $filters + * @return array Map of container ids to usage stats + */ + abstract public function listUsage(array $filters): array; + + abstract public function pull(string $image): void; + + /** + * @param array $filters + * @return Container[] + */ + abstract public function list(array $filters): array; + + abstract public function run( + string $name, + string $image, + string $hostname, + array $command, + array $entrypoint, + ?string $workdir, + array $environment, + array $mounts, + array $labels, + ?string $network, + ?float $cpus, + ?int $memory, + ?int $swap, + RestartPolicy $restart, + bool $remove, + ): string; + + /** + * @param string[] $command + * @param array $vars + */ + abstract public function execute(string $name, array $command, string &$output, array $vars, int $timeout): bool; + + /** + * Remove Container + */ + abstract public function remove(string $name, bool $force): bool; +} diff --git a/src/Orchestration/Adapter/DockerAPI.php b/src/Containers/Adapter/DockerAPI.php similarity index 90% rename from src/Orchestration/Adapter/DockerAPI.php rename to src/Containers/Adapter/DockerAPI.php index 0622719..8cde838 100644 --- a/src/Orchestration/Adapter/DockerAPI.php +++ b/src/Containers/Adapter/DockerAPI.php @@ -1,14 +1,14 @@ call('http://localhost/networks/'.$name, 'DELETE'); if ($result['code'] === 404) { - throw new Orchestration('Network with name "'.$name.'" does not exist: '.$result['response']); + throw new Containers('Network with name "'.$name.'" does not exist: '.$result['response']); } elseif ($result['code'] !== 204) { - throw new Orchestration('Error removing network: '.$result['response']); + throw new Containers('Error removing network: '.$result['response']); } return true; @@ -253,7 +253,7 @@ public function networkConnect(string $container, string $network): bool ]); if ($result['code'] !== 200) { - throw new Orchestration('Error attaching network: '.$result['response']); + throw new Containers('Error attaching network: '.$result['response']); } return $result['code'] === 200; @@ -275,7 +275,7 @@ public function networkDisconnect(string $container, string $network, bool $forc ]); if ($result['code'] !== 200) { - throw new Orchestration('Error detatching network: '.$result['response']); + throw new Containers('Error detatching network: '.$result['response']); } return $result['code'] === 200; @@ -392,7 +392,7 @@ public function listNetworks(): array $list = []; if ($result['code'] !== 200) { - throw new Orchestration($result['response']); + throw new Containers($result['response']); } foreach (\json_decode($result['response'], true) as $value) { @@ -453,7 +453,7 @@ public function list(array $filters = []): array $list = []; if ($result['code'] !== 200) { - throw new Orchestration($result['response']); + throw new Containers($result['response']); } foreach (\json_decode($result['response'], true) as $value) { @@ -500,7 +500,7 @@ public function run( ): string { $result = $this->call('http://localhost/images/'.$image.'/json', 'GET'); if ($result['code'] === 404 && ! $this->pull($image)) { - throw new Orchestration('Missing image "'.$image.'" and failed to pull it.'); + throw new Containers('Missing image "'.$image.'" and failed to pull it.'); } $parsedVariables = []; @@ -551,11 +551,11 @@ public function run( ]); if ($result['code'] === 404) { - throw new Orchestration('Container image "'.$image.'" not found.'); + throw new Containers('Container image "'.$image.'" not found.'); } elseif ($result['code'] === 409) { - throw new Orchestration('Container with name "'.$name.'" already exists.'); + throw new Containers('Container with name "'.$name.'" already exists.'); } elseif ($result['code'] !== 201) { - throw new Orchestration('Failed to create function environment: '.$result['response'].' Response Code: '.$result['code']); + throw new Containers('Failed to create function environment: '.$result['response'].' Response Code: '.$result['code']); } $parsedResponse = json_decode($result['response'], true); @@ -564,7 +564,7 @@ public function run( // Run Created Container $startResult = $this->call('http://localhost/containers/'.$containerId.'/start', 'POST', '{}'); if ($startResult['code'] !== 204) { - throw new Orchestration('Failed to start container: '.$startResult['response']); + throw new Containers('Failed to start container: '.$startResult['response']); } return $containerId; @@ -605,7 +605,7 @@ public function execute( ], $timeout); if ($result['code'] !== 201) { - throw new Orchestration('Failed to create execute command: '.$result['response'].' Response Code: '.$result['code']); + throw new Containers('Failed to create execute command: '.$result['response'].' Response Code: '.$result['code']); } $parsedResponse = json_decode($result['response'], true); @@ -615,18 +615,18 @@ public function execute( $output = $result['stdout'].$result['stderr']; if ($result['code'] !== 200) { - throw new Orchestration('Failed to create execute command: '.$result['response'].' Response Code: '.$result['code']); + throw new Containers('Failed to create execute command: '.$result['response'].' Response Code: '.$result['code']); } $result = $this->call('http://localhost/exec/'.$parsedResponse['Id'].'/json', 'GET'); if ($result['code'] !== 200) { - throw new Orchestration('Failed to inspect status of execute command: '.$result['response'].' Response Code: '.$result['code']); + throw new Containers('Failed to inspect status of execute command: '.$result['response'].' Response Code: '.$result['code']); } $parsedResponse = json_decode($result['response'], true); if ($parsedResponse['Running'] === true || $parsedResponse['ExitCode'] !== 0) { - throw new Orchestration('Failed to execute command. Exit code: '.$parsedResponse['ExitCode']); + throw new Containers('Failed to execute command. Exit code: '.$parsedResponse['ExitCode']); } return true; @@ -640,7 +640,7 @@ public function remove(string $name, bool $force = false): bool $result = $this->call('http://localhost/containers/'.$name.($force ? '?force=true' : ''), 'DELETE'); if ($result['code'] !== 204) { - throw new Orchestration('Failed to remove container: '.$result['response'].' Response Code: '.$result['code']); + throw new Containers('Failed to remove container: '.$result['response'].' Response Code: '.$result['code']); } else { return true; } diff --git a/src/Containers/Adapter/DockerCLI.php b/src/Containers/Adapter/DockerCLI.php new file mode 100644 index 0000000..9c4751a --- /dev/null +++ b/src/Containers/Adapter/DockerCLI.php @@ -0,0 +1,513 @@ +login($username, $password); + } + + private function login(string $username, string $password): void + { + $output = ''; + + $result = Console::execute([ + 'docker', 'login', '--username', $username, '--password-stdin' + ], $password, $output); + + if ($result !== 0) { + throw new ContainersAuthException("Failed to login to Docker registry, command return $result, output: $output"); + } + } + + public function createNetwork(string $id, bool $internal): void + { + $output = ''; + + $result = Console::execute([ + 'docker', 'network', 'create', $id, ($internal ? '--internal' : '') + ], '', $output); + + if ($result !== 0) { + throw new ContainersException("Failed to create Docker network, command return $result, output: $output"); + } + } + + public function removeNetwork(string $name): void + { + $output = ''; + + $result = Console::execute([ + 'docker', 'network', 'rm', $name + ], '', $output); + + if ($result !== 0) { + throw new ContainersException("Failed to remove Docker network, command return $result, output: $output"); + } + } + + public function connect(string $container, string $network): void + { + $output = ''; + + $result = Console::execute([ + 'docker', 'network', 'connect', $network, $container + ], '', $output); + + if ($result !== 0) { + throw new ContainersException("Failed to connect Docker container to network, command return $result, output: $output"); + } + } + + public function disconnect(string $container, string $network, bool $force) + { + $output = ''; + + $command = ['docker', 'network', 'disconnect', $network, $container]; + if ($force) { + $command[] = '--force'; + } + + $result = Console::execute($command, '', $output); + + if ($result !== 0) { + throw new ContainersException("Failed to disconnect Docker container from network, command return $result, output: $output"); + } + } + + /** + * Check if a network exists + */ + public function networkExists(string $name): bool + { + $output = ''; + + $result = Console::execute([ + 'docker', 'network', 'inspect', $name, '--format', '{{.Name}}' + ], '', $output); + + return $result === 0 && trim($output) === $name; + } + + public function getUsage(string $container): array + { + $output = ''; + + $result = Console::execute([ + 'docker', 'container', 'stats', $container, '--no-stream', '--format', '{{.CPUPerc}}&{{.MemPerc}}&{{.BlockIO}}&{{.MemUsage}}&{{.NetIO}}' + ], '', $output); + + if ($result !== 0) { + throw new ContainersException("Failed to get usage stats for Docker container, command return $result, output: $output"); + } + + $stats = explode('&', $output); + + throw new \Exception('getUsage'); + } + + public function listUsage(array $filters): array + { + // List ahead of time, since docker stats does not allow filtering + $containerIds = []; + + + $output = ''; + + if (\count($containerIds) <= 0 && \count($filters) > 0) { + return []; // No containers found + } + + $stats = []; + + $containersString = ''; + + foreach ($containerIds as $containerId) { + $containersString .= ' '.$containerId; + } + + $result = Console::execute([ + 'docker', 'stats', '--no-trunc', + '--format', 'id={{.ID}}&name={{.Name}}&cpu={{.CPUPerc}}&memory={{.MemPerc}}&diskIO={{.BlockIO}}&memoryIO={{.MemUsage}}&networkIO={{.NetIO}}', + '--no-stream' + ], '', $output); + + if ($result !== 0) { + return []; + } + + $lines = \explode("\n", $output); + + foreach ($lines as $line) { + if (empty($line)) { + continue; + } + + $stat = []; + \parse_str($line, $stat); + + $stats[$stat['id']] = new Usage( + cpuUsage: \floatval(\rtrim($stat['cpu'], '%')) / 100, // Remove percentage symbol, parse to number, convert to percentage + memoryUsage: empty($stat['memory']) ? 0 : \floatval(\rtrim($stat['memory'], '%')), // Remove percentage symbol and parse to number. Value is empty on Windows + diskIO: $this->parseIOStats($stat['diskIO']), + memoryIO: $this->parseIOStats($stat['memoryIO']), + networkIO: $this->parseIOStats($stat['networkIO']), + ); + } + + return $stats; + } + + /** + * Use this method to parse string format into numeric in&out stats. + * CLI IO stats in verbose format: "2.133MiB / 62.8GiB" + * Output after parsing: [ "in" => 2133000, "out" => 62800000000 ] + * + * @return array + */ + private function parseIOStats(string $stats) + { + $stats = \strtolower($stats); + $units = [ + 'b' => 1, + 'kb' => 1000, + 'mb' => 1000000, + 'gb' => 1000000000, + 'tb' => 1000000000000, + 'kib' => 1024, + 'mib' => 1048576, + 'gib' => 1073741824, + 'tib' => 1099511627776, + ]; + + [$inStr, $outStr] = \explode(' / ', $stats); + + $inUnit = null; + $outUnit = null; + + foreach ($units as $unit => $value) { + if (\str_ends_with($inStr, $unit)) { + $inUnit = $unit; + } + if (\str_ends_with($outStr, $unit)) { + $outUnit = $unit; + } + } + + $inMultiply = $inUnit === null ? 1 : $units[$inUnit]; + $outMultiply = $outUnit === null ? 1 : $units[$outUnit]; + + $inValue = \floatval(\rtrim($inStr, $inUnit)); + $outValue = \floatval(\rtrim($outStr, $outUnit)); + + $response = [ + 'in' => $inValue * $inMultiply, + 'out' => $outValue * $outMultiply, + ]; + + return $response; + } + + /** + * List Networks + * + * @return Network[] + */ + public function listNetworks(): array + { + $output = ''; + + $result = Console::execute([ + 'docker', 'network', 'ls', '--format', '"id={{.ID}}&name={{.Name}}&driver={{.Driver}}&scope={{.Scope}}"' + ], '', $output); + + if ($result !== 0) { + throw new ContainersException("Failed to list networks, command returned $result, output: $output"); + } + + $list = []; + $stdoutArray = \explode("\n", $output); + + foreach ($stdoutArray as $value) { + $network = []; + + \parse_str($value, $network); + + if (isset($network['name'])) { + $parsedNetwork = new Network($network['name'], $network['id'], $network['driver'], $network['scope']); + + array_push($list, $parsedNetwork); + } + } + + return $list; + } + + public function pull(string $image): void + { + $output = ''; + + $result = Console::execute([ + 'docker', 'pull', $image + ], '', $output); + + if ($result !== 0) { + throw new ContainersException("Failed to pull image $image, command returned $result, output: $output"); + } + } + + /** + * List Containers + * + * @param array $filters + * @return Container[] + */ + public function list(array $filters = []): array + { + $output = ''; + + $filterString = ''; + + foreach ($filters as $key => $value) { + $filterString = $filterString.' --filter "'.$key.'='.$value.'"'; + } + + $result = Console::execute([ + 'docker', 'ps', '--all', '--no-trunc', '--format', '"id={{.ID}}&name={{.Names}}&status={{.Status}}&labels={{.Labels}}"'.$filterString + ], '', $output); + + if ($result !== 0 && $result !== -1) { + throw new ContainersException("Failed to list containers, command returned $result, output: $output"); + } + + $list = []; + $stdoutArray = \explode("\n", $output); + + foreach ($stdoutArray as $value) { + $container = []; + + \parse_str($value, $container); + + if (isset($container['name'])) { + $labelsParsed = []; + + foreach (\explode(',', $container['labels']) as $value) { + $value = \explode('=', $value); + + if (isset($value[0]) && isset($value[1])) { + $labelsParsed[$value[0]] = $value[1]; + } + } + + $parsedContainer = new Container($container['name'], $container['id'], $container['status'], $labelsParsed); + + array_push($list, $parsedContainer); + } + } + + return $list; + } + + /** + * Run container + * + * Creates and runs a new container. On success it will return a string containing the container ID. + * On fail it will throw an exception. + * + * @param string[] $command + * @param string[] $entrypoint + * @param array $environment + * @param Mount[] $mounts + * @param array $labels + */ + public function run( + string $name, + string $image, + string $hostname, + array $command, + array $entrypoint, + ?string $workdir, + array $environment, + array $mounts, + array $labels, + ?string $network, + ?float $cpus, + ?int $memory, + ?int $swap, + RestartPolicy $restart, + bool $remove, + ): string { + $output = ''; + + $cmd = ['docker', 'run', '-d']; + + if ($remove) { + $cmd[] = '--rm'; + } + + if ($network !== null) { + $cmd[] = "--network={$network}"; + } + + if (!empty($entrypoint)) { + foreach ($entrypoint as $entry) { + $cmd[] = '--entrypoint'; + $cmd[] = $entry; + } + } + + if ($cpus !== null) { + $cmd[] = "--cpus={$cpus}"; + } + + if ($memory !== null) { + $cmd[] = "--memory={$memory}m"; + } + + if ($swap !== null) { + $cmd[] = "--memory-swap={$swap}m"; + } + + $cmd[] = "--restart={$restart->value}"; + $cmd[] = "--name={$name}"; + + foreach ($mounts as $mount) { + if ($mount instanceof Bind) { + $permission = $mount->isReadOnly() ? 'ro' : 'rw'; + $cmd[] = '--volume'; + $cmd[] = "{$mount->hostPath}:{$mount->containerPath}:{$permission}"; + } elseif ($mount instanceof Volume) { + $permission = $mount->isReadOnly() ? 'ro' : 'rw'; + $cmd[] = '--volume'; + $cmd[] = "{$mount->volumeName}:{$mount->containerPath}:{$permission}"; + } elseif ($mount instanceof Tmpfs) { + $tmpfsSpec = $mount->containerPath; + if ($mount->sizeBytes !== null) { + $tmpfsSpec = "{$tmpfsSpec}:size={$mount->sizeBytes}"; + } + $cmd[] = '--tmpfs'; + $cmd[] = $tmpfsSpec; + } + } + + foreach ($labels as $key => $value) { + $cmd[] = '--label'; + $cmd[] = "{$key}={$value}"; + } + + if ($workdir !== null) { + $cmd[] = '--workdir'; + $cmd[] = $workdir; + } + + if (!empty($hostname)) { + $cmd[] = '--hostname'; + $cmd[] = $hostname; + } + + foreach ($environment as $key => $value) { + $cmd[] = '--env'; + $cmd[] = "{$key}={$value}"; + } + + $cmd[] = $image; + + foreach ($command as $arg) { + $cmd[] = $arg; + } + + $result = Console::execute($cmd, '', $output, 30); + if ($result !== 0) { + throw new ContainersException("Failed to create container, command returned {$result}, output: {$output}"); + } + + // Use first line only, CLI can add warnings or other messages + $output = explode("\n", $output)[0]; + + return rtrim($output); + } + + /** + * Execute Container + * + * @param string[] $command + * @param array $vars + */ + public function execute( + string $name, + array $command, + string &$output, + array $vars, + int $timeout + ): bool { + foreach ($command as $key => $value) { + if (str_contains($value, ' ')) { + $command[$key] = "'".$value."'"; + } + } + + $parsedVariables = []; + + foreach ($vars as $key => $value) { + $value = \escapeshellarg((empty($value)) ? '' : $value); + $parsedVariables[$key] = "--env {$key}={$value}"; + } + + $vars = $parsedVariables; + + $result = Console::execute('docker exec '.\implode(' ', $vars)." {$name} ".implode(' ', $command), '', $output, $timeout); + + if ($result !== 0) { + if ($result == 124) { + throw new ContainersTimeoutException('Command timed out'); + } else { + throw new ContainersException("Docker Error: {$output}"); + } + } + + return true; + } + + /** + * Remove Container + */ + public function remove(string $name, bool $force = false): bool + { + $output = ''; + + $result = Console::execute('docker rm '.($force ? '--force' : '')." {$name}", '', $output); + + if (! \str_starts_with($output, $name) || \str_contains($output, 'No such container')) { + throw new ContainersException("Docker Error: {$output}"); + } + + return ! $result; + } +} diff --git a/src/Containers/Container.php b/src/Containers/Container.php new file mode 100644 index 0000000..ba9a648 --- /dev/null +++ b/src/Containers/Container.php @@ -0,0 +1,17 @@ + $labels + */ + public function __construct( + public string $name = '', + public string $id = '', + public string $status = '', + public array $labels = [] + ) { + } +} diff --git a/src/Containers/Containers.php b/src/Containers/Containers.php new file mode 100644 index 0000000..9f4c226 --- /dev/null +++ b/src/Containers/Containers.php @@ -0,0 +1,162 @@ +networks = new Networks($adapter); + } + + /** + * Pull container image + * + * @param string $image + * @return void + */ + public function pull(string $image): void + { + $this->adapter->pull($image); + } + + /** + * List containers + * + * @param array $filters + * @return Container[] + */ + public function list(array $filters = []): array + { + return $this->adapter->list($filters); + } + + /** + * + * Remove Container + * + * @param string $name Container ID + * @param bool $force Force removal + * @return bool + */ + public function remove(string $name, bool $force = false): bool + { + return $this->adapter->remove($name, $force); + } + + /** + * Create container + * + * Creates and runs a new container. On success it will return a string containing the container name. + * On fail it will throw an exception. + * + * @param string $name Assign the specified name to the container. + * @param string $image The image to use for the container. + * @param string $hostname The hostname to use for the container, as a valid RFC 1123 hostname. + * @param string[] $command Command to run as array of strings. + * @param string[] $entrypoint The entry point for the container as an array of strings. + * @param ?string $workdir The working directory for the container. + * @param array $environment Environment variables to set in the container. + * @param Mount[] $mounts Mounts to apply to the container. + * @param array $labels Labels to apply to the container. + * @param ?string $network The network to connect the container to. + * @param ?float $cpus CPU time the container is allowed to use. 1 is entire CPU, 0.5 is half CPU. Default is no limit. + * @param ?int $memory Memory limit in MB. Default is no limit. + * @param ?int $swap Swap limit in MB. Default is no limit. + * @param RestartPolicy $restart Restart policy for the container. + * @param bool $remove Automatically remove the container when it exits. + */ + public function run( + string $name, + string $image, + string $hostname, + array $command = [], + array $entrypoint = [], + ?string $workdir = null, + array $environment = [], + array $mounts = [], + array $labels = [], + ?string $network = null, + ?float $cpus = null, + ?int $memory = null, + ?int $swap = null, + RestartPolicy $restart = RestartPolicy::No, + bool $remove = false, + ): string { + if (empty($name)) { + throw new \InvalidArgumentException('Container name cannot be empty'); + } + if (!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9_.-]+$/i', $name)) { + throw new \InvalidArgumentException('Container name must start with a letter or number and can contain letters, numbers, dots, underscores, and dashes'); + } + if (empty($image)) { + throw new \InvalidArgumentException('Image name cannot be empty'); + } + + // Filter environment variables to valid characters. + // TODO: (@loks0n) Strongly feel we should consider removing invalid keys instead of fixing them. + $environment = array_map(fn ($var) => preg_replace('/[^A-Za-z0-9\_\.\-]/', '', $var), $environment); + $environment = array_filter($environment, fn($var) => !empty($var)); + + return $this->adapter->run( + $name, + $image, + $hostname, + $command, + $entrypoint, + $workdir, + $environment, + $mounts, + $labels, + $network, + $cpus, + $memory, + $swap, + $restart, + $remove, + ); + } + + /** + * Execute command inside an existing container + * + * @param string[] $command + * @param array $vars + */ + public function execute( + string $container, + array $command, + string &$output, + array $vars = [], + int $timeout = -1 + ): bool { + return $this->adapter->execute($container, $command, $output, $vars, $timeout); + } + + // Usage + /** + * Get usage stats of container + * + * @return ?Usage + */ + public function getUsage(string $container): ?Usage + { + return $this->adapter->getUsage($container); + } + + /** + * Get usage stats of containers + * + * @param array $filters + * @return array Map of container ids to usage stats + */ + public function listUsage(array $filters = []): array + { + return $this->adapter->listUsage($filters); + } +} diff --git a/src/Containers/Exception/ContainersAuthException.php b/src/Containers/Exception/ContainersAuthException.php new file mode 100644 index 0000000..b2f82e9 --- /dev/null +++ b/src/Containers/Exception/ContainersAuthException.php @@ -0,0 +1,7 @@ +containerPath; } + public function isReadOnly(): bool { return $this->readOnly; } +} diff --git a/src/Containers/Mount/Tmpfs.php b/src/Containers/Mount/Tmpfs.php new file mode 100644 index 0000000..8d9e95b --- /dev/null +++ b/src/Containers/Mount/Tmpfs.php @@ -0,0 +1,15 @@ +containerPath; } + public function isReadOnly(): bool { return false; } +} diff --git a/src/Containers/Mount/Volume.php b/src/Containers/Mount/Volume.php new file mode 100644 index 0000000..5d5ff6f --- /dev/null +++ b/src/Containers/Mount/Volume.php @@ -0,0 +1,16 @@ +containerPath; } + public function isReadOnly(): bool { return $this->readOnly; } +} diff --git a/src/Containers/Network.php b/src/Containers/Network.php new file mode 100644 index 0000000..f958860 --- /dev/null +++ b/src/Containers/Network.php @@ -0,0 +1,14 @@ +adapter->createNetwork($name, $internal); + } + + /** + * Remove network + * + */ + public function remove(string $name) + { + $this->adapter->removeNetwork($name); + } + + /** + * List Networks + * + * @return Network[] + */ + public function list(): array + { + return $this->adapter->listNetworks(); + } + + /** + * Connect a container to a network + * + * @param string $container Container ID + * @param string $network Network ID + * @return void + */ + public function connect(string $container, string $network): void + { + $this->adapter->connect($container, $network); + } + + /** + * Disconnect a container from a network + */ + public function disconnect(string $container, string $network, bool $force = false): bool + { + return $this->adapter->disconnect($container, $network, $force); + } + + /** + * Check if a network exists + */ + public function exists(string $name): bool + { + return $this->adapter->networkExists($name); + } + +} diff --git a/src/Containers/RestartPolicy.php b/src/Containers/RestartPolicy.php new file mode 100644 index 0000000..e87ac9d --- /dev/null +++ b/src/Containers/RestartPolicy.php @@ -0,0 +1,11 @@ + $diskIO + * @param array $memoryIO + * @param array $networkIO + */ + public function __construct( + public float $cpuUsage, + public float $memoryUsage, + public array $diskIO, + public array $memoryIO, + public array $networkIO) + {} +} diff --git a/src/Orchestration/Adapter.php b/src/Orchestration/Adapter.php deleted file mode 100644 index c4cfd56..0000000 --- a/src/Orchestration/Adapter.php +++ /dev/null @@ -1,183 +0,0 @@ - $filters - * @return array<\Utopia\Orchestration\Container\Stats> - */ - abstract public function getStats(?string $container = null, array $filters = []): array; - - /** - * Pull Image - */ - abstract public function pull(string $image): bool; - - /** - * List Containers - * - * @param array $filters - * @return Container[] - */ - abstract public function list(array $filters = []): array; - - /** - * Run Container - * - * Creates and runs a new container, On success it will return a string containing the container ID. - * On fail it will throw an exception. - * - * @param string[] $command - * @param string[] $volumes - * @param array $vars - * @param array $labels - */ - abstract public function run( - string $image, - string $name, - array $command = [], - string $entrypoint = '', - string $workdir = '', - array $volumes = [], - array $vars = [], - string $mountFolder = '', - array $labels = [], - string $hostname = '', - bool $remove = false, - string $network = '', - string $restart = self::RESTART_NO - ): string; - - /** - * Execute Container - * - * @param string[] $command - * @param array $vars - */ - abstract public function execute(string $name, array $command, string &$output, array $vars = [], int $timeout = -1): bool; - - /** - * Remove Container - */ - abstract public function remove(string $name, bool $force): bool; - - /** - * Set containers namespace - * - * @return $this - */ - public function setNamespace(string $namespace): self - { - $this->namespace = $namespace; - - return $this; - } - - /** - * Set max allowed CPU Quota per container - * - * @return $this - */ - public function setCpus(float $cores): self - { - $this->cpus = $cores; - - return $this; - } - - /** - * Set max allowed memory in mb per container - * - * @return $this - */ - public function setMemory(int $mb): self - { - $this->memory = $mb; - - return $this; - } - - /** - * Set max allowed swap memory in mb per container - * - * @return $this - */ - public function setSwap(int $mb): self - { - $this->swap = $mb; - - return $this; - } -} diff --git a/src/Orchestration/Adapter/DockerCLI.php b/src/Orchestration/Adapter/DockerCLI.php deleted file mode 100644 index c755eb5..0000000 --- a/src/Orchestration/Adapter/DockerCLI.php +++ /dev/null @@ -1,452 +0,0 @@ - $filters - * @return array - */ - public function getStats(?string $container = null, array $filters = []): array - { - // List ahead of time, since docker stats does not allow filtering - $containerIds = []; - - if ($container === null) { - $containers = $this->list($filters); - $containerIds = \array_map(fn ($c) => $c->getId(), $containers); - } else { - $containerIds[] = $container; - } - - $output = ''; - - if (\count($containerIds) <= 0 && \count($filters) > 0) { - return []; // No containers found - } - - $stats = []; - - $containersString = ''; - - foreach ($containerIds as $containerId) { - $containersString .= ' '.$containerId; - } - - $result = Console::execute('docker stats --no-trunc --format "id={{.ID}}&name={{.Name}}&cpu={{.CPUPerc}}&memory={{.MemPerc}}&diskIO={{.BlockIO}}&memoryIO={{.MemUsage}}&networkIO={{.NetIO}}" --no-stream'.$containersString, '', $output); - - if ($result !== 0) { - return []; - } - - $lines = \explode("\n", $output); - - foreach ($lines as $line) { - if (empty($line)) { - continue; - } - - $stat = []; - \parse_str($line, $stat); - - $stats[] = new Stats( - containerId: $stat['id'], - containerName: $stat['name'], - cpuUsage: \floatval(\rtrim($stat['cpu'], '%')) / 100, // Remove percentage symbol, parse to number, convert to percentage - memoryUsage: empty($stat['memory']) ? 0 : \floatval(\rtrim($stat['memory'], '%')), // Remove percentage symbol and parse to number. Value is empty on Windows - diskIO: $this->parseIOStats($stat['diskIO']), - memoryIO: $this->parseIOStats($stat['memoryIO']), - networkIO: $this->parseIOStats($stat['networkIO']), - ); - } - - return $stats; - } - - /** - * Use this method to parse string format into numeric in&out stats. - * CLI IO stats in verbose format: "2.133MiB / 62.8GiB" - * Output after parsing: [ "in" => 2133000, "out" => 62800000000 ] - * - * @return array - */ - private function parseIOStats(string $stats) - { - $stats = \strtolower($stats); - $units = [ - 'b' => 1, - 'kb' => 1000, - 'mb' => 1000000, - 'gb' => 1000000000, - 'tb' => 1000000000000, - 'kib' => 1024, - 'mib' => 1048576, - 'gib' => 1073741824, - 'tib' => 1099511627776, - ]; - - [$inStr, $outStr] = \explode(' / ', $stats); - - $inUnit = null; - $outUnit = null; - - foreach ($units as $unit => $value) { - if (\str_ends_with($inStr, $unit)) { - $inUnit = $unit; - } - if (\str_ends_with($outStr, $unit)) { - $outUnit = $unit; - } - } - - $inMultiply = $inUnit === null ? 1 : $units[$inUnit]; - $outMultiply = $outUnit === null ? 1 : $units[$outUnit]; - - $inValue = \floatval(\rtrim($inStr, $inUnit)); - $outValue = \floatval(\rtrim($outStr, $outUnit)); - - $response = [ - 'in' => $inValue * $inMultiply, - 'out' => $outValue * $outMultiply, - ]; - - return $response; - } - - /** - * List Networks - * - * @return Network[] - */ - public function listNetworks(): array - { - $output = ''; - - $result = Console::execute('docker network ls --format "id={{.ID}}&name={{.Name}}&driver={{.Driver}}&scope={{.Scope}}"', '', $output); - - if ($result !== 0) { - throw new Orchestration("Docker Error: {$output}"); - } - - $list = []; - $stdoutArray = \explode("\n", $output); - - foreach ($stdoutArray as $value) { - $network = []; - - \parse_str($value, $network); - - if (isset($network['name'])) { - $parsedNetwork = new Network($network['name'], $network['id'], $network['driver'], $network['scope']); - - array_push($list, $parsedNetwork); - } - } - - return $list; - } - - /** - * Pull Image - */ - public function pull(string $image): bool - { - $output = ''; - - $result = Console::execute('docker pull '.$image, '', $output); - - return $result === 0; - } - - /** - * List Containers - * - * @param array $filters - * @return Container[] - */ - public function list(array $filters = []): array - { - $output = ''; - - $filterString = ''; - - foreach ($filters as $key => $value) { - $filterString = $filterString.' --filter "'.$key.'='.$value.'"'; - } - - $result = Console::execute('docker ps --all --no-trunc --format "id={{.ID}}&name={{.Names}}&status={{.Status}}&labels={{.Labels}}"'.$filterString, '', $output); - - if ($result !== 0 && $result !== -1) { - throw new Orchestration("Docker Error: {$output}"); - } - - $list = []; - $stdoutArray = \explode("\n", $output); - - foreach ($stdoutArray as $value) { - $container = []; - - \parse_str($value, $container); - - if (isset($container['name'])) { - $labelsParsed = []; - - foreach (\explode(',', $container['labels']) as $value) { - $value = \explode('=', $value); - - if (isset($value[0]) && isset($value[1])) { - $labelsParsed[$value[0]] = $value[1]; - } - } - - $parsedContainer = new Container($container['name'], $container['id'], $container['status'], $labelsParsed); - - array_push($list, $parsedContainer); - } - } - - return $list; - } - - /** - * Run Container - * - * Creates and runs a new container, On success it will return a string containing the container ID. - * On fail it will throw an exception. - * - * @param string[] $command - * @param string[] $volumes - * @param array $vars - * @param array $labels - */ - public function run( - string $image, - string $name, - array $command = [], - string $entrypoint = '', - string $workdir = '', - array $volumes = [], - array $vars = [], - string $mountFolder = '', - array $labels = [], - string $hostname = '', - bool $remove = false, - string $network = '', - string $restart = self::RESTART_NO - ): string { - $output = ''; - - foreach ($command as $key => $value) { - if (str_contains($value, ' ')) { - $command[$key] = "'".$value."'"; - } - } - - $labelString = ''; - - foreach ($labels as $labelKey => $label) { - // sanitize label - $label = str_replace("'", '', $label); - - if (str_contains($label, ' ')) { - $label = "'".$label."'"; - } - - $labelString = $labelString.' --label '.$labelKey.'='.$label; - } - - $parsedVariables = []; - - foreach ($vars as $key => $value) { - $key = $this->filterEnvKey($key); - - $value = \escapeshellarg((empty($value)) ? '' : $value); - $parsedVariables[$key] = "--env {$key}={$value}"; - } - - $volumeString = ''; - foreach ($volumes as $volume) { - $volumeString = $volumeString.'--volume '.$volume.' '; - } - - $vars = $parsedVariables; - - $time = time(); - - $result = Console::execute('docker run'. - ' -d'. - ($remove ? ' --rm' : ''). - (empty($network) ? '' : " --network=\"{$network}\""). - (empty($entrypoint) ? '' : " --entrypoint=\"{$entrypoint}\""). - (empty($this->cpus) ? '' : (' --cpus='.$this->cpus)). - (empty($this->memory) ? '' : (' --memory='.$this->memory.'m')). - (empty($this->swap) ? '' : (' --memory-swap='.$this->swap.'m')). - " --restart={$restart}". - " --name={$name}". - " --label {$this->namespace}-type=runtime". - " --label {$this->namespace}-created={$time}". - (empty($mountFolder) ? '' : " --volume {$mountFolder}:/tmp:rw"). - (empty($volumeString) ? '' : ' '.$volumeString). - (empty($labelString) ? '' : ' '.$labelString). - (empty($workdir) ? '' : " --workdir {$workdir}"). - (empty($hostname) ? '' : " --hostname {$hostname}"). - (empty($vars) ? '' : ' '.\implode(' ', $vars)). - " {$image}". - (empty($command) ? '' : ' '.implode(' ', $command)), '', $output, 30); - - if ($result !== 0) { - throw new Orchestration("Docker Error: {$output}"); - } - - // Use first line only, CLI can add warnings or other messages - $output = \explode("\n", $output)[0]; - - return rtrim($output); - } - - /** - * Execute Container - * - * @param string[] $command - * @param array $vars - */ - public function execute( - string $name, - array $command, - string &$output = '', - array $vars = [], - int $timeout = -1 - ): bool { - foreach ($command as $key => $value) { - if (str_contains($value, ' ')) { - $command[$key] = "'".$value."'"; - } - } - - $parsedVariables = []; - - foreach ($vars as $key => $value) { - $key = $this->filterEnvKey($key); - - $value = \escapeshellarg((empty($value)) ? '' : $value); - $parsedVariables[$key] = "--env {$key}={$value}"; - } - - $vars = $parsedVariables; - - $result = Console::execute('docker exec '.\implode(' ', $vars)." {$name} ".implode(' ', $command), '', $output, $timeout); - - if ($result !== 0) { - if ($result == 124) { - throw new Timeout('Command timed out'); - } else { - throw new Orchestration("Docker Error: {$output}"); - } - } - - return true; - } - - /** - * Remove Container - */ - public function remove(string $name, bool $force = false): bool - { - $output = ''; - - $result = Console::execute('docker rm '.($force ? '--force' : '')." {$name}", '', $output); - - if (! \str_starts_with($output, $name) || \str_contains($output, 'No such container')) { - throw new Orchestration("Docker Error: {$output}"); - } - - return ! $result; - } -} diff --git a/src/Orchestration/Container.php b/src/Orchestration/Container.php deleted file mode 100644 index 7d7ce64..0000000 --- a/src/Orchestration/Container.php +++ /dev/null @@ -1,120 +0,0 @@ - $labels - */ - public function __construct(string $name = '', string $id = '', string $status = '', array $labels = []) - { - $this->name = $name; - $this->id = $id; - $this->status = $status; - $this->labels = $labels; - } - - /** - * @var string - */ - protected $name = ''; - - /** - * @var string - */ - protected $id = ''; - - /** - * @var string - */ - protected $status = ''; - - /** - * @var array - */ - protected $labels = []; - - /** - * Get the container's name - */ - public function getName(): string - { - return $this->name; - } - - /** - * Get the container's ID - */ - public function getId(): string - { - return $this->id; - } - - /** - * Get the container's status - */ - public function getStatus(): string - { - return $this->status; - } - - /** - * Get the container's labels - * - * @return array - */ - public function getLabels(): array - { - return $this->labels; - } - - /** - * Set the container's name - * - * @return $this - */ - public function setName(string $name): self - { - $this->name = $name; - - return $this; - } - - /** - * Set the container's id - * - * @return $this - */ - public function setId(string $id): self - { - $this->id = $id; - - return $this; - } - - /** - * Set the container's status - * - * @return $this - */ - public function setStatus(string $status): self - { - $this->status = $status; - - return $this; - } - - /** - * Set the container's labels - * - * @param array $labels - * @return $this - */ - public function setLabels(array $labels): self - { - $this->labels = $labels; - - return $this; - } -} diff --git a/src/Orchestration/Container/Stats.php b/src/Orchestration/Container/Stats.php deleted file mode 100644 index b63701b..0000000 --- a/src/Orchestration/Container/Stats.php +++ /dev/null @@ -1,89 +0,0 @@ - - */ - protected array $diskIO; - - /** - * @var array - */ - protected array $memoryIO; - - /** - * @var array - */ - protected array $networkIO; - - /** - * @param array $diskIO - * @param array $memoryIO - * @param array $networkIO - */ - public function __construct(string $containerId, string $containerName, float $cpuUsage, float $memoryUsage, array $diskIO, array $memoryIO, array $networkIO) - { - $this->containerId = $containerId; - $this->containerName = $containerName; - $this->cpuUsage = $cpuUsage; - $this->memoryUsage = $memoryUsage; - $this->diskIO = $diskIO; - $this->memoryIO = $memoryIO; - $this->networkIO = $networkIO; - } - - public function getContainerId(): string - { - return $this->containerId; - } - - public function getContainerName(): string - { - return $this->containerName; - } - - public function getCpuUsage(): float - { - return $this->cpuUsage; - } - - public function getMemoryUsage(): float - { - return $this->memoryUsage; - } - - /** - * @return array - */ - public function getMemoryIO(): array - { - return $this->memoryIO; - } - - /** - * @return array - */ - public function getDiskIO(): array - { - return $this->diskIO; - } - - /** - * @return array - */ - public function getNetworkIO(): array - { - return $this->networkIO; - } -} diff --git a/src/Orchestration/Exception/Orchestration.php b/src/Orchestration/Exception/Orchestration.php deleted file mode 100644 index 08b8349..0000000 --- a/src/Orchestration/Exception/Orchestration.php +++ /dev/null @@ -1,7 +0,0 @@ -name = $name; - $this->id = $id; - $this->driver = $driver; - $this->scope = $scope; - } - - public function getName(): string - { - return $this->name; - } - - public function getId(): string - { - return $this->id; - } - - public function getDriver(): string - { - return $this->driver; - } - - public function getScope(): string - { - return $this->scope; - } - - public function setName(string $name): Network - { - $this->name = $name; - - return $this; - } - - public function setId(string $id): Network - { - $this->id = $id; - - return $this; - } - - public function setDriver(string $driver): Network - { - $this->driver = $driver; - - return $this; - } - - public function setScope(string $scope): Network - { - $this->scope = $scope; - - return $this; - } -} diff --git a/src/Orchestration/Orchestration.php b/src/Orchestration/Orchestration.php deleted file mode 100644 index 6e366b7..0000000 --- a/src/Orchestration/Orchestration.php +++ /dev/null @@ -1,255 +0,0 @@ -adapter = $adapter; - } - - /** - * Command Line String into Array - * - * This function will convert a string containing a command into an array of arguments. - * It will go through the string and find all instances of spaces, and will split the string - * however if it detects a apostrophe comes after the space it will find the next apostrophe and split the entire thing - * and add it to the array. This is so arguments with spaces in them can be passed such as scripts for sh or bash. - * - * If there are no spaces detected in the first place it will just return the string as an array. - * - * @return (false|string)[] - */ - public function parseCommandString(string $command): array - { - $currentPos = 0; - $commandProcessed = []; - - if (strpos($command, ' ', $currentPos) === false) { - return [$command]; - } - - while (true) { - if (strpos($command, ' ', $currentPos) !== false) { - $place = (int) strpos($command, ' ', $currentPos); - - if ($command[$place + 1] !== "'") { - array_push($commandProcessed, substr($command, $currentPos, $place - $currentPos)); - $place = $place + 1; - } else { - array_push($commandProcessed, substr($command, $currentPos, $place - $currentPos)); - - $closingString = strpos($command, "'", $place + 2); - - if ($closingString == false) { - throw new Exception("Invalid Command given, are you missing an `'` at the end?"); - } - - array_push($commandProcessed, substr($command, $place + 1, $closingString)); - $place = $closingString + 1; - } - - if (strpos($command, ' ', $place) === false) { - if (! empty(substr($command, $place, strlen($command) - $currentPos))) { - array_push($commandProcessed, substr($command, $place, strlen($command) - $currentPos)); - } - } - - $currentPos = $place; - } else { - break; - } - } - - return $commandProcessed; - } - - /** - * Create Network - */ - public function createNetwork(string $name, bool $internal = false): bool - { - return $this->adapter->createNetwork($name, $internal); - } - - /** - * Remove Network - */ - public function removeNetwork(string $name): bool - { - return $this->adapter->removeNetwork($name); - } - - /** - * List Networks - * - * @return Network[] - */ - public function listNetworks(): array - { - return $this->adapter->listNetworks(); - } - - /** - * Connect a container to a network - */ - public function networkConnect(string $container, string $network): bool - { - return $this->adapter->networkConnect($container, $network); - } - - /** - * Get usage stats of containers - * - * @param array $filters - * @return array<\Utopia\Orchestration\Container\Stats> - */ - public function getStats(?string $container = null, array $filters = []): array - { - return $this->adapter->getStats($container, $filters); - } - - /** - * Disconnect a container from a network - */ - public function networkDisconnect(string $container, string $network, bool $force = false): bool - { - return $this->adapter->networkDisconnect($container, $network, $force); - } - - /** - * Check if a network exists - */ - public function networkExists(string $name): bool - { - return $this->adapter->networkExists($name); - } - - /** - * Pull Image - */ - public function pull(string $image): bool - { - return $this->adapter->pull($image); - } - - /** - * List Containers - * - * @param array $filters - * @return Container[] - */ - public function list(array $filters = []): array - { - return $this->adapter->list($filters); - } - - /** - * Run Container - * - * Creates and runs a new container, On success it will return a string containing the container ID. - * On fail it will throw an exception. - * - * @param string[] $command - * @param string[] $volumes - * @param array $labels - * @param array $vars - */ - public function run( - string $image, - string $name, - array $command = [], - string $entrypoint = '', - string $workdir = '', - array $volumes = [], - array $vars = [], - string $mountFolder = '', - array $labels = [], - string $hostname = '', - bool $remove = false, - string $network = '', - string $restart = Adapter::RESTART_NO - ): string { - return $this->adapter->run($image, $name, $command, $entrypoint, $workdir, $volumes, $vars, $mountFolder, $labels, $hostname, $remove, $network, $restart); - } - - /** - * Execute Container - * - * @param string[] $command - * @param array $vars - */ - public function execute( - string $name, - array $command, - string &$output, - array $vars = [], - int $timeout = -1 - ): bool { - return $this->adapter->execute($name, $command, $output, $vars, $timeout); - } - - /** - * Remove Container - */ - public function remove(string $name, bool $force = false): bool - { - return $this->adapter->remove($name, $force); - } - - /** - * Set containers namespace - * - * @return $this - */ - public function setNamespace(string $namespace): self - { - $this->adapter->setNamespace($namespace); - - return $this; - } - - /** - * Set max allowed CPU Quota per container - * - * @return $this - */ - public function setCpus(float $cores): self - { - $this->adapter->setCpus($cores); - - return $this; - } - - /** - * Set max allowed memory in mb per container - * - * @return $this - */ - public function setMemory(int $mb): self - { - $this->adapter->setMemory($mb); - - return $this; - } - - /** - * Set max allowed swap memory in mb per container - * - * @return $this - */ - public function setSwap(int $mb): self - { - $this->adapter->setSwap($mb); - - return $this; - } -} diff --git a/tests/Containers/Adapter/DockerAPITest.php b/tests/Containers/Adapter/DockerAPITest.php new file mode 100644 index 0000000..8607f64 --- /dev/null +++ b/tests/Containers/Adapter/DockerAPITest.php @@ -0,0 +1,34 @@ +pull('appwrite/runtime-for-php:8.0'); + $response = static::getContainers()->pull('appwrite/runtime-for-php:8.0'); $this->assertEquals(true, $response); // Used later for CPU usage test - $response = static::getOrchestration()->pull('containerstack/alpine-stress:latest'); + $response = static::getContainers()->pull('containerstack/alpine-stress:latest'); $this->assertEquals(true, $response); /** * Test for Failure */ - $response = static::getOrchestration()->pull('appwrite/tXDytMhecKCuz5B4PlITXL1yKhZXDP'); // Pull non-existent Container + $response = static::getContainers()->pull('appwrite/tXDytMhecKCuz5B4PlITXL1yKhZXDP'); // Pull non-existent Container $this->assertEquals(false, $response); } @@ -56,7 +56,7 @@ public function testPullImage(): void */ public function testCreateContainer(): void { - $response = static::getOrchestration()->run( + $response = static::getContainers()->run( 'appwrite/runtime-for-php:8.0', 'TestContainer', [ @@ -67,16 +67,16 @@ public function testCreateContainer(): void '', '/usr/local/src/', [ - \getenv('HOST_DIR').'/tests/Orchestration/Resources:/test:rw', + \getenv('HOST_DIR').'/tests/Containers/Resources:/test:rw', ], [], - \getenv('HOST_DIR').'/tests/Orchestration/Resources' + \getenv('HOST_DIR').'/tests/Containers/Resources' ); $this->assertNotEmpty($response); // "Always" Restart policy test - $response = static::getOrchestration()->run( + $response = static::getContainers()->run( 'appwrite/runtime-for-php:8.0', 'TestContainerWithRestart', [ @@ -87,10 +87,10 @@ public function testCreateContainer(): void '', '/usr/local/src/', [ - \getenv('HOST_DIR').'/tests/Orchestration/Resources:/test:rw', + \getenv('HOST_DIR').'/tests/Containers/Resources:/test:rw', ], [], - \getenv('HOST_DIR').'/tests/Orchestration/Resources', + \getenv('HOST_DIR').'/tests/Containers/Resources', restart: DockerAPI::RESTART_ALWAYS ); @@ -104,11 +104,11 @@ public function testCreateContainer(): void $occurances = \substr_count($output, 'Custom start'); $this->assertGreaterThanOrEqual(2, $occurances); // 2 logs mean it restarted at least once - $response = static::getOrchestration()->remove('TestContainerWithRestart', true); + $response = static::getContainers()->remove('TestContainerWithRestart', true); $this->assertEquals(true, $response); // "No" Restart policy test - $response = static::getOrchestration()->run( + $response = static::getContainers()->run( 'appwrite/runtime-for-php:8.0', 'TestContainerWithoutRestart', [ @@ -119,10 +119,10 @@ public function testCreateContainer(): void '', '/usr/local/src/', [ - \getenv('HOST_DIR').'/tests/Orchestration/Resources:/test:rw', + \getenv('HOST_DIR').'/tests/Containers/Resources:/test:rw', ], [], - \getenv('HOST_DIR').'/tests/Orchestration/Resources', + \getenv('HOST_DIR').'/tests/Containers/Resources', restart: DockerAPI::RESTART_NO ); @@ -136,7 +136,7 @@ public function testCreateContainer(): void $occurances = \substr_count($output, 'Custom start'); $this->assertEquals(1, $occurances); - $response = static::getOrchestration()->remove('TestContainerWithoutRestart', true); + $response = static::getContainers()->remove('TestContainerWithoutRestart', true); $this->assertEquals(true, $response); /** @@ -144,7 +144,7 @@ public function testCreateContainer(): void */ $this->expectException(\Exception::class); - $response = static::getOrchestration()->run( + $response = static::getContainers()->run( 'appwrite/txdytmheckcuz5b4plitxl1ykhzxdh', // Non-Existent Image 'TestContainer', [ @@ -156,7 +156,7 @@ public function testCreateContainer(): void '/usr/local/src/', [], [], - \getenv('HOST_DIR').'/tests/Orchestration/Resources', + \getenv('HOST_DIR').'/tests/Containers/Resources', ); /** @@ -164,7 +164,7 @@ public function testCreateContainer(): void */ $this->expectException(\Exception::class); - $response = static::getOrchestration()->run( + $response = static::getContainers()->run( 'appwrite/runtime-for-php:8.0', 'TestContainerBadBuild', [ @@ -176,7 +176,7 @@ public function testCreateContainer(): void '/usr/local/src/', [], [], - \getenv('HOST_DIR').'/tests/Orchestration/Resources', + \getenv('HOST_DIR').'/tests/Containers/Resources', ); } @@ -187,7 +187,7 @@ public function testCreateContainer(): void */ public function testCreateNetwork(): void { - $response = static::getOrchestration()->createNetwork('TestNetwork'); + $response = static::getContainers()->createNetwork('TestNetwork'); $this->assertEquals(true, $response); } @@ -197,7 +197,7 @@ public function testCreateNetwork(): void */ public function testListNetworks(): void { - $response = static::getOrchestration()->listNetworks(); + $response = static::getContainers()->listNetworks(); $foundNetwork = false; @@ -215,7 +215,7 @@ public function testListNetworks(): void */ public function testNetworkConnect(): void { - $response = static::getOrchestration()->run( + $response = static::getContainers()->run( 'appwrite/runtime-for-php:8.0', 'TestContainerRM', [ @@ -229,7 +229,7 @@ public function testNetworkConnect(): void [ 'teasdsa' => '', ], - \getenv('HOST_DIR').'/tests/Orchestration/Resources', + \getenv('HOST_DIR').'/tests/Containers/Resources', [ 'test2' => 'Hello World!', ], @@ -242,7 +242,7 @@ public function testNetworkConnect(): void sleep(1); // wait for container - $response = static::getOrchestration()->networkConnect('TestContainer', 'TestNetwork'); + $response = static::getContainers()->networkConnect('TestContainer', 'TestNetwork'); $this->assertEquals(true, $response); } @@ -252,7 +252,7 @@ public function testNetworkConnect(): void */ public function testNetworkDisconnect(): void { - $response = static::getOrchestration()->networkDisconnect('TestContainer', 'TestNetwork', true); + $response = static::getContainers()->networkDisconnect('TestContainer', 'TestNetwork', true); $this->assertEquals(true, $response); } @@ -262,7 +262,7 @@ public function testNetworkDisconnect(): void */ public function testRemoveNetwork(): void { - $response = static::getOrchestration()->removeNetwork('TestNetwork'); + $response = static::getContainers()->removeNetwork('TestNetwork'); $this->assertEquals(true, $response); } @@ -279,7 +279,7 @@ public function testExecContainer(): void $threwException = false; try { - static::getOrchestration()->execute( + static::getContainers()->execute( '60clotVWpufbEpy33zJLcoYHrUTqWaD1FV0FZWsw', // Non-Existent Container [ 'php', @@ -299,7 +299,7 @@ public function testExecContainer(): void $threwException = false; try { - static::getOrchestration()->execute( + static::getContainers()->execute( 'TestContainer', [ 'php', @@ -321,7 +321,7 @@ public function testExecContainer(): void */ $output = ''; - static::getOrchestration()->execute( + static::getContainers()->execute( 'TestContainer', [ 'php', @@ -340,7 +340,7 @@ public function testExecContainer(): void */ $output = ''; - static::getOrchestration()->execute( + static::getContainers()->execute( 'TestContainer', [ 'sh', @@ -366,7 +366,7 @@ public function testCheckVolume(): void { $output = ''; - static::getOrchestration()->execute( + static::getContainers()->execute( 'TestContainer', [ 'cat', @@ -384,7 +384,7 @@ public function testCheckVolume(): void public function testTimeoutContainer(): void { // Create container - $response = static::getOrchestration()->run( + $response = static::getContainers()->run( 'appwrite/runtime-for-php:8.0', 'TestContainerTimeout', [ @@ -398,7 +398,7 @@ public function testTimeoutContainer(): void [ 'teasdsa' => '', ], - \getenv('HOST_DIR').'/tests/Orchestration/Resources', + \getenv('HOST_DIR').'/tests/Containers/Resources', [ 'test2' => 'Hello World!', ] @@ -414,7 +414,7 @@ public function testTimeoutContainer(): void $output = ''; $threwException = false; try { - $response = static::getOrchestration()->execute( + $response = static::getContainers()->execute( 'TestContainerTimeout', [ 'php', @@ -434,7 +434,7 @@ public function testTimeoutContainer(): void */ $output = ''; - $response = static::getOrchestration()->execute( + $response = static::getContainers()->execute( 'TestContainerTimeout', [ 'php', @@ -452,7 +452,7 @@ public function testTimeoutContainer(): void */ $output = ''; - $response = static::getOrchestration()->execute( + $response = static::getContainers()->execute( 'TestContainerTimeout', [ 'sh', @@ -473,7 +473,7 @@ public function testTimeoutContainer(): void */ public function testListContainers(): void { - $response = static::getOrchestration()->list(); + $response = static::getContainers()->list(); $foundContainer = false; @@ -491,7 +491,7 @@ public function testListContainers(): void */ public function testListFilters(): void { - $response = $this->getOrchestration()->list(['id' => self::$containerID]); + $response = $this->getContainers()->list(['id' => self::$containerID]); $this->assertEquals(self::$containerID, $response[0]->getId()); } @@ -504,11 +504,11 @@ public function testRemoveContainer(): void /** * Test for Success */ - $response = static::getOrchestration()->remove('TestContainer', true); + $response = static::getContainers()->remove('TestContainer', true); $this->assertEquals(true, $response); - $response = static::getOrchestration()->remove('TestContainerTimeout', true); + $response = static::getContainers()->remove('TestContainerTimeout', true); $this->assertEquals(true, $response); /** @@ -516,42 +516,7 @@ public function testRemoveContainer(): void */ $this->expectException(\Exception::class); - $response = static::getOrchestration()->remove('TestContainer', true); - } - - public function testParseCLICommand(): void - { - /** - * Test for success - */ - $test = static::getOrchestration()->parseCommandString("sh -c 'mv /tmp/code.tar.gz /usr/local/src/code.tar.gz && tar -zxf /usr/local/src/code.tar.gz --strip 1 && rm /usr/local/src/code.tar.gz && tail -f /dev/null'"); - - $this->assertEquals([ - 'sh', - '-c', - "'mv /tmp/code.tar.gz /usr/local/src/code.tar.gz && tar -zxf /usr/local/src/code.tar.gz --strip 1 && rm /usr/local/src/code.tar.gz && tail -f /dev/null'", - ], $test); - - $test = static::getOrchestration()->parseCommandString('sudo apt-get update'); - - $this->assertEquals([ - 'sudo', - 'apt-get', - 'update', - ], $test); - - $test = static::getOrchestration()->parseCommandString('test'); - - $this->assertEquals([ - 'test', - ], $test); - - /** - * Test for failure - */ - $this->expectException(\Exception::class); - - $test = static::getOrchestration()->parseCommandString("sh -c 'mv /tmp/code.tar.gz /usr/local/src/code.tar.gz && tar -zxf /usr/local/src/code.tar.gz --strip 1 && rm /usr/local/src/code.tar.gz && tail -f /dev/null"); + $response = static::getContainers()->remove('TestContainer', true); } public function testRunRemove(): void @@ -559,7 +524,7 @@ public function testRunRemove(): void /** * Test for success */ - $response = static::getOrchestration()->run( + $response = static::getContainers()->run( 'appwrite/runtime-for-php:8.0', 'TestContainerRM', [ @@ -573,7 +538,7 @@ public function testRunRemove(): void [ 'teasdsa' => '', ], - \getenv('HOST_DIR').'/tests/Orchestration/Resources', + \getenv('HOST_DIR').'/tests/Containers/Resources', [ 'test2' => 'Hello World!', ], @@ -586,7 +551,7 @@ public function testRunRemove(): void sleep(1); // Check if container exists - $statusResponse = static::getOrchestration()->list(['id' => $response]); + $statusResponse = static::getContainers()->list(['id' => $response]); $this->assertEquals(0, count($statusResponse)); } @@ -599,14 +564,14 @@ public function testUsageStats(): void /** * Test for Success */ - $stats = static::getOrchestration()->getStats(); + $stats = static::getContainers()->getStats(); // 1 expected due to container running tests $this->assertCount(1, $stats, 'Container(s) still running: '.\json_encode($stats, JSON_PRETTY_PRINT)); // This allows CPU-heavy load check - static::getOrchestration()->setCpus(1); + static::getContainers()->setCpus(1); - $containerId1 = static::getOrchestration()->run( + $containerId1 = static::getContainers()->run( 'containerstack/alpine-stress', // https://github.com/containerstack/alpine-stress 'UsageStats1', [ @@ -615,13 +580,13 @@ public function testUsageStats(): void 'apk update && apk add screen && tail -f /dev/null', ], workdir: '/usr/local/src/', - mountFolder: \getenv('HOST_DIR').'/tests/Orchestration/Resources', + mountFolder: \getenv('HOST_DIR').'/tests/Containers/Resources', labels: ['utopia-container-type' => 'stats'] ); $this->assertNotEmpty($containerId1); - $containerId2 = static::getOrchestration()->run( + $containerId2 = static::getContainers()->run( 'containerstack/alpine-stress', 'UsageStats2', [ @@ -630,7 +595,7 @@ public function testUsageStats(): void 'apk update && apk add screen && tail -f /dev/null', ], workdir: '/usr/local/src/', - mountFolder: \getenv('HOST_DIR').'/tests/Orchestration/Resources', + mountFolder: \getenv('HOST_DIR').'/tests/Containers/Resources', ); $this->assertNotEmpty($containerId2); @@ -638,14 +603,14 @@ public function testUsageStats(): void // This allows CPU-heavy load check $output = ''; - static::getOrchestration()->execute($containerId1, ['screen', '-d', '-m', "'stress --cpu 1 --timeout 5'"], $output); // Run in screen so it's background task - static::getOrchestration()->execute($containerId2, ['screen', '-d', '-m', "'stress --cpu 1 --timeout 5'"], $output); + static::getContainers()->execute($containerId1, ['screen', '-d', '-m', "'stress --cpu 1 --timeout 5'"], $output); // Run in screen so it's background task + static::getContainers()->execute($containerId2, ['screen', '-d', '-m', "'stress --cpu 1 --timeout 5'"], $output); // Set CPU stress-test start \sleep(1); // Fetch stats, should include high CPU usage - $stats = static::getOrchestration()->getStats(); + $stats = static::getContainers()->getStats(); $this->assertCount(2 + 1, $stats); // +1 due to container running tests @@ -675,11 +640,11 @@ public function testUsageStats(): void $this->assertIsNumeric($stats[0]->getNetworkIO()['out']); $this->assertGreaterThanOrEqual(0, $stats[0]->getNetworkIO()['out']); - $stats1 = static::getOrchestration()->getStats($containerId1); - $stats2 = static::getOrchestration()->getStats($containerId2); + $stats1 = static::getContainers()->getStats($containerId1); + $stats2 = static::getContainers()->getStats($containerId2); - $statsName1 = static::getOrchestration()->getStats('UsageStats1'); - $statsName2 = static::getOrchestration()->getStats('UsageStats2'); + $statsName1 = static::getContainers()->getStats('UsageStats1'); + $statsName2 = static::getContainers()->getStats('UsageStats2'); $this->assertEquals($statsName1[0]->getContainerId(), $stats1[0]->getContainerId()); $this->assertEquals($statsName1[0]->getContainerName(), $stats1[0]->getContainerName()); @@ -694,25 +659,25 @@ public function testUsageStats(): void $this->assertGreaterThanOrEqual(0, $stats[0]->getCpuUsage()); $this->assertGreaterThanOrEqual(0, $stats[1]->getCpuUsage()); - $statsFiltered = static::getOrchestration()->getStats(filters: ['label' => 'utopia-container-type=stats']); + $statsFiltered = static::getContainers()->getStats(filters: ['label' => 'utopia-container-type=stats']); $this->assertCount(1, $statsFiltered); $this->assertEquals($containerId1, $statsFiltered[0]->getContainerId()); - $statsFiltered = static::getOrchestration()->getStats(filters: ['label' => 'utopia-container-type=non-existing-type']); + $statsFiltered = static::getContainers()->getStats(filters: ['label' => 'utopia-container-type=non-existing-type']); $this->assertCount(0, $statsFiltered); - $response = static::getOrchestration()->remove('UsageStats1', true); + $response = static::getContainers()->remove('UsageStats1', true); $this->assertEquals(true, $response); - $response = static::getOrchestration()->remove('UsageStats2', true); + $response = static::getContainers()->remove('UsageStats2', true); $this->assertEquals(true, $response); /** * Test for Failure */ - $stats = static::getOrchestration()->getStats('IDontExist'); + $stats = static::getContainers()->getStats('IDontExist'); $this->assertCount(0, $stats); } @@ -721,18 +686,18 @@ public function testNetworkExists(): void $networkName = 'test_network_'.uniqid(); // Test non-existent network - $this->assertFalse(static::getOrchestration()->networkExists($networkName)); + $this->assertFalse(static::getContainers()->networkExists($networkName)); // Create network and test it exists - $response = static::getOrchestration()->createNetwork($networkName); + $response = static::getContainers()->createNetwork($networkName); $this->assertTrue($response); - $this->assertTrue(static::getOrchestration()->networkExists($networkName)); + $this->assertTrue(static::getContainers()->networkExists($networkName)); // Remove network - $response = static::getOrchestration()->removeNetwork($networkName); + $response = static::getContainers()->removeNetwork($networkName); $this->assertTrue($response); // Test removed network - $this->assertFalse(static::getOrchestration()->networkExists($networkName)); + $this->assertFalse(static::getContainers()->networkExists($networkName)); } } diff --git a/tests/Orchestration/Resources/.gitignore b/tests/Containers/Resources/.gitignore similarity index 100% rename from tests/Orchestration/Resources/.gitignore rename to tests/Containers/Resources/.gitignore diff --git a/tests/Orchestration/Resources/php/index.php b/tests/Containers/Resources/php/index.php similarity index 100% rename from tests/Orchestration/Resources/php/index.php rename to tests/Containers/Resources/php/index.php diff --git a/tests/Orchestration/Resources/php/logs.sh b/tests/Containers/Resources/php/logs.sh similarity index 100% rename from tests/Orchestration/Resources/php/logs.sh rename to tests/Containers/Resources/php/logs.sh diff --git a/tests/Orchestration/Resources/testfile.txt b/tests/Containers/Resources/testfile.txt similarity index 100% rename from tests/Orchestration/Resources/testfile.txt rename to tests/Containers/Resources/testfile.txt diff --git a/tests/Orchestration/Resources/timeout/index.php b/tests/Containers/Resources/timeout/index.php similarity index 100% rename from tests/Orchestration/Resources/timeout/index.php rename to tests/Containers/Resources/timeout/index.php diff --git a/tests/Orchestration/Adapter/DockerAPITest.php b/tests/Orchestration/Adapter/DockerAPITest.php deleted file mode 100644 index c10ecd3..0000000 --- a/tests/Orchestration/Adapter/DockerAPITest.php +++ /dev/null @@ -1,34 +0,0 @@ -