Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add wait_for_docker_container helper #235

Merged
merged 1 commit into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion doc/06-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
25 changes: 25 additions & 0 deletions doc/going-further/wait-for.md
Original file line number Diff line number Diff line change
Expand Up @@ -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...',
);
```
23 changes: 23 additions & 0 deletions examples/wait_for.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is OK for CI and local ? quiet is used to prevent showing the container ID that is random on each test generation run

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)');
}
}
12 changes: 12 additions & 0 deletions src/Exception/ExecutableNotFoundException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace Castor\Exception;

class ExecutableNotFoundException extends \RuntimeException
{
public function __construct(
readonly string $executableName,
) {
parent::__construct("Executable {$executableName} not found. Please install it to use this feature.");
}
}
13 changes: 13 additions & 0 deletions src/Exception/WaitFor/DockerContainerStateException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Castor\Exception\WaitFor;

class DockerContainerStateException extends \RuntimeException
{
public function __construct(
readonly string $containerName,
readonly string $state,
) {
parent::__construct("Container {$containerName} is in \"{$state}\" state. Please start it to use this feature.");
}
}
64 changes: 64 additions & 0 deletions src/WaitForHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

namespace Castor;

use Castor\Exception\ExecutableNotFoundException;
use Castor\Exception\WaitFor\DockerContainerStateException;
use Castor\Exception\WaitFor\ExitedBeforeTimeoutException;
use Castor\Exception\WaitFor\TimeoutReachedException;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Process\ExecutableFinder;
use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
Expand Down Expand Up @@ -210,4 +213,65 @@ public function waitForHttpResponse(
message: $message ?? "Waiting for URL \"{$url}\" to return HTTP response...",
);
}

/**
* @param array<int> $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),
);
}
}
22 changes: 22 additions & 0 deletions src/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
1 change: 1 addition & 0 deletions tests/Examples/Generated/ListTest.php.output.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
22 changes: 22 additions & 0 deletions tests/Examples/Generated/WaitForWaitForDockerContainerTaskTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace Castor\Tests\Examples;

use Castor\Tests\TaskTestCase;

class WaitForWaitForDockerContainerTaskTest extends TaskTestCase
{
// wait-for:wait-for-docker-container-task
public function test(): void
{
$process = $this->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());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Waiting for docker container "helloworld" to be available... OK