diff --git a/CHANGELOG.md b/CHANGELOG.md index 321357e6..fe3be47b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * Add `guard_min_version()` function to ensure a minimum version of Castor is used * Edited the duration of update check from `60 days` to `24 hours` * Add `wait_for_http_response()` function for a more generic response check +* Add `wait_for_docker_container()` function to wait for a docker container to be ready * [BC Break] Remove `callable $responseChecker` parameter from `wait_for_http_status()` ## 0.11.1 (2024-01-11) diff --git a/doc/06-reference.md b/doc/06-reference.md index f651a693..7d4bf590 100644 --- a/doc/06-reference.md +++ b/doc/06-reference.md @@ -38,10 +38,11 @@ Castor provides the following built-in functions: - [`ssh_upload`](going-further/ssh.md#the-ssh_upload-function) - [`variable`](05-context.md#the-variable-function) - [`wait_for`](going-further/wait-for.md#the-wait_for-function) +- [`wait_for_url`](going-further/wait-for.md#the-wait_for_url-function) - [`wait_for_http_response`](going-further/wait-for.md#the-wait_for_http_response-function) - [`wait_for_http_status`](going-further/wait-for.md#the-wait_for_http_status-function) - [`wait_for_port`](going-further/wait-for.md#the-wait_for_port-function) -- [`wait_for_url`](going-further/wait-for.md#the-wait_for_url-function) +- [`wait_for_docker_container`](going-further/wait-for.md#the-wait_for_docker_container-function) - [`watch`](going-further/watch.md) - [`with`](going-further/advanced-context.md#the-with-function) diff --git a/doc/going-further/wait-for.md b/doc/going-further/wait-for.md index 671bc0c9..988acc2e 100644 --- a/doc/going-further/wait-for.md +++ b/doc/going-further/wait-for.md @@ -127,3 +127,28 @@ wait_for_http_status( message: 'Waiting for https://example.com/api to return HTTP 200', ); ``` + +### The `wait_for_docker_container()` function + +The `wait_for_docker_container()` function waits for a Docker container to be +ready. It checks if the container is running and if the specified port is +accessible within the specified timeout. +It can also wait for a specific check to be successful, by providing a +`$check` callback function. + + +Example: + +```php +wait_for_docker_container( + container: 'mysql-container', + containerChecker: function ($containerId) { + return run("docker exec $containerId mysql -uroot -proot -e 'SELECT 1'", allowFailure: true)->isSuccessful(); + }, + portsToCheck: [3306] + timeout: 30, + quiet: false, + intervalMs: 100, + message: 'Waiting for my-container to be ready...', +); +``` diff --git a/examples/wait_for.php b/examples/wait_for.php index a58b8af2..13eee05a 100644 --- a/examples/wait_for.php +++ b/examples/wait_for.php @@ -7,8 +7,11 @@ use Castor\Exception\WaitFor\TimeoutReachedException; use Symfony\Contracts\HttpClient\ResponseInterface; +use function Castor\capture; use function Castor\io; +use function Castor\run; use function Castor\wait_for; +use function Castor\wait_for_docker_container; use function Castor\wait_for_http_response; use function Castor\wait_for_http_status; use function Castor\wait_for_port; @@ -91,3 +94,23 @@ function custom_wait_for_task(string $thing = 'foobar'): void io()->error('My custom check failed. (timeout reached)'); } } + +#[AsTask(description: 'Wait for docker container to be ready')] +function wait_for_docker_container_task(): void +{ + try { + run('docker run -d --rm --name helloworld alpine sh -c "echo hello world ; sleep 10"', quiet: true); + wait_for_docker_container( + containerName: 'helloworld', + timeout: 5, + containerChecker: function ($containerId): bool { + // Check some things (logs, command result, etc.) + $output = capture("docker logs {$containerId}", allowFailure: true); + + return u($output)->containsAny(['hello world']); + }, + ); + } catch (TimeoutReachedException) { + io()->error('Docker container is not available. (timeout reached)'); + } +} diff --git a/src/Exception/ExecutableNotFoundException.php b/src/Exception/ExecutableNotFoundException.php new file mode 100644 index 00000000..3027e0d8 --- /dev/null +++ b/src/Exception/ExecutableNotFoundException.php @@ -0,0 +1,12 @@ + $portsToCheck + * + * @throws TimeoutReachedException + */ + public function waitForDockerContainer( + SymfonyStyle $io, + string $containerName, + int $timeout = 10, + bool $quiet = false, + int $intervalMs = 100, + string $message = null, + callable $containerChecker = null, + array $portsToCheck = [], + ): void { + if (null === (new ExecutableFinder())->find('docker')) { + throw new ExecutableNotFoundException('docker'); + } + $this->waitFor( + io: $io, + callback: function () use ($timeout, $io, $portsToCheck, $containerChecker, $containerName) { + $containerId = capture("docker ps -a -q --filter name={$containerName}", allowFailure: true); + $isContainerExist = (bool) $containerId; + $isContainerRunning = (bool) capture("docker inspect -f '{{.State.Running}}' {$containerId}", allowFailure: true); + + if (false === $isContainerExist) { + throw new DockerContainerStateException($containerName, 'not exist'); + } + + if (false === $isContainerRunning) { + throw new DockerContainerStateException($containerName, 'not running'); + } + + foreach ($portsToCheck as $port) { + $this->waitForPort(io: $io, port: $port, timeout: $timeout, quiet: true); + } + + if (null !== $containerChecker) { + try { + $this->waitFor( + io: $io, + callback: function () use ($containerChecker, $containerId) { + return $containerChecker($containerId); + }, + timeout: $timeout, + quiet: true, + ); + } catch (TimeoutReachedException) { + return false; + } + } + + return true; + }, + timeout: $timeout, + quiet: $quiet, + intervalMs: $intervalMs, + message: sprintf($message ?? 'Waiting for docker container "%s" to be available...', $containerName), + ); + } } diff --git a/src/functions.php b/src/functions.php index bb5d0efe..5fb17335 100644 --- a/src/functions.php +++ b/src/functions.php @@ -971,6 +971,28 @@ function wait_for_http_response( ); } +/** + * @throws TimeoutReachedException + */ +function wait_for_docker_container( + string $containerName, + int $timeout = 10, + bool $quiet = false, + int $intervalMs = 100, + string $message = null, + callable $containerChecker = null, +): void { + GlobalHelper::getApplication()->waitForHelper->waitForDockerContainer( + io: io(), + containerName: $containerName, + timeout: $timeout, + quiet: $quiet, + intervalMs: $intervalMs, + message: $message, + containerChecker: $containerChecker, + ); +} + function guard_min_version(string $minVersion): void { $currentVersion = GlobalHelper::getApplication()->getVersion(); diff --git a/tests/Examples/Generated/ListTest.php.output.txt b/tests/Examples/Generated/ListTest.php.output.txt index 68be7759..25c7b1d3 100644 --- a/tests/Examples/Generated/ListTest.php.output.txt +++ b/tests/Examples/Generated/ListTest.php.output.txt @@ -59,6 +59,7 @@ ssh:upload Uploads a file version-guard:min-version-check Check if the minimum castor version requirement is met version-guard:min-version-check-fail Check if the minimum castor version requirement is met (fail) wait-for:custom-wait-for-task Use custom wait for, to check anything +wait-for:wait-for-docker-container-task Wait for docker container to be ready wait-for:wait-for-port-task Wait for a service available on a port wait-for:wait-for-url-task Wait for an URL to be available wait-for:wait-for-url-with-specific-response-content-and-status Wait for an URL to respond with a "200" status code and a specific content diff --git a/tests/Examples/Generated/VersionGuardMinVersionCheckFailTest.php.err.txt b/tests/Examples/Generated/VersionGuardMinVersionCheckFailTest.php.err.txt index 8e4a461d..925d5884 100644 --- a/tests/Examples/Generated/VersionGuardMinVersionCheckFailTest.php.err.txt +++ b/tests/Examples/Generated/VersionGuardMinVersionCheckFailTest.php.err.txt @@ -1,5 +1,5 @@ -In functions.php line 980: +In functions.php line 1002: Castor requires at least version v999.0.0, you are using v0.11.1. Please consider upgrading. diff --git a/tests/Examples/Generated/WaitForWaitForDockerContainerTaskTest.php b/tests/Examples/Generated/WaitForWaitForDockerContainerTaskTest.php new file mode 100644 index 00000000..2838ecee --- /dev/null +++ b/tests/Examples/Generated/WaitForWaitForDockerContainerTaskTest.php @@ -0,0 +1,22 @@ +runTask(['wait-for:wait-for-docker-container-task']); + + $this->assertSame(0, $process->getExitCode()); + $this->assertStringEqualsFile(__FILE__ . '.output.txt', $process->getOutput()); + if (file_exists(__FILE__ . '.err.txt')) { + $this->assertStringEqualsFile(__FILE__ . '.err.txt', $process->getErrorOutput()); + } else { + $this->assertSame('', $process->getErrorOutput()); + } + } +} diff --git a/tests/Examples/Generated/WaitForWaitForDockerContainerTaskTest.php.output.txt b/tests/Examples/Generated/WaitForWaitForDockerContainerTaskTest.php.output.txt new file mode 100644 index 00000000..de2a02e5 --- /dev/null +++ b/tests/Examples/Generated/WaitForWaitForDockerContainerTaskTest.php.output.txt @@ -0,0 +1,2 @@ +Waiting for docker container "helloworld" to be available... OK +