Skip to content
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,13 @@ jobs:
run: docker network create tiny-blocks

- name: Create Docker volume for migrations
run: docker volume create migrations
run: docker volume create test-adm-migrations

- name: Run tests
run: |
docker run --network=tiny-blocks \
-v ${PWD}:/app \
-v ${PWD}/tests/Integration/Database/Migrations:/migrations \
-v ${PWD}/tests/Integration/Database/Migrations:/test-adm-migrations \
-v /var/run/docker.sock:/var/run/docker.sock \
-w /app \
gustavofreze/php:${{ env.PHP_VERSION }} bash -c "composer tests"
8 changes: 6 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
DOCKER_RUN = docker run -u root --rm -it --network=tiny-blocks --name test-lib -v ${PWD}:/app -v ${PWD}/tests/Integration/Database/Migrations:/migrations -v /var/run/docker.sock:/var/run/docker.sock -w /app gustavofreze/php:8.3
DOCKER_RUN = docker run -u root --rm -it --network=tiny-blocks --name test-lib \
-v ${PWD}:/app \
-v ${PWD}/tests/Integration/Database/Migrations:/test-adm-migrations \
-v /var/run/docker.sock:/var/run/docker.sock \
-w /app gustavofreze/php:8.3

.PHONY: configure test unit-test test-no-coverage create-volume create-network review show-reports clean

Expand All @@ -18,7 +22,7 @@ create-network:
@docker network create tiny-blocks

create-volume:
@docker volume create migrations
@docker volume create test-adm-migrations

review:
@${DOCKER_RUN} composer review
Expand Down
47 changes: 43 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,42 @@ $container->withWaitBeforeRun(wait: ContainerWaitForDependency::untilReady(condi

## Usage examples

- When running the containers from the library on a host (your local machine), you need to map the volume
`/var/run/docker.sock:/var/run/docker.sock`.
This ensures that the container has access to the Docker daemon on the host machine, allowing Docker commands to be
executed within the container.


- In some cases, it may be necessary to add the `docker-cli` dependency to your PHP image.
This enables the container to interact with Docker from within the container environment.

### MySQL and Generic Containers

Before configuring and starting the MySQL container, a PHP container is set up to execute the tests and manage the
integration process.

This container runs within a Docker network and uses a volume for the database migrations.
The following commands are used to prepare the environment:

1. **Create the Docker network**:
```bash
docker network create tiny-blocks
```

2. **Create the volume for migrations**:
```bash
docker volume create test-adm-migrations
```

3. **Run the PHP container**:
```bash
docker run -u root --rm -it --network=tiny-blocks --name test-lib \
-v ${PWD}:/app \
-v ${PWD}/tests/Integration/Database/Migrations:/test-adm-migrations \
-v /var/run/docker.sock:/var/run/docker.sock \
-w /app gustavofreze/php:8.3 bash -c "composer tests"
```

The MySQL container is configured and started:

```php
Expand All @@ -179,6 +213,7 @@ $mySQLContainer = MySQLDockerContainer::from(image: 'mysql:8.1', name: 'test-dat
->withDatabase(database: 'test_adm')
->withPortMapping(portOnHost: 3306, portOnContainer: 3306)
->withRootPassword(rootPassword: 'root')
->withGrantedHosts()
->withVolumeMapping(pathOnHost: '/var/lib/mysql', pathOnContainer: '/var/lib/mysql')
->withoutAutoRemove()
->runIfNotExists();
Expand All @@ -187,7 +222,8 @@ $mySQLContainer = MySQLDockerContainer::from(image: 'mysql:8.1', name: 'test-dat
With the MySQL container started, it is possible to retrieve data, such as the address and JDBC connection URL:

```php
$jdbcUrl = $mySQLContainer->getJdbcUrl(options: 'useUnicode=yes&characterEncoding=UTF-8&allowPublicKeyRetrieval=true&useSSL=false');
$environmentVariables = $mySQLContainer->getEnvironmentVariables();
$jdbcUrl = $mySQLContainer->getJdbcUrl();
$database = $environmentVariables->getValueBy(key: 'MYSQL_DATABASE');
$username = $environmentVariables->getValueBy(key: 'MYSQL_USER');
$password = $environmentVariables->getValueBy(key: 'MYSQL_PASSWORD');
Expand All @@ -198,8 +234,8 @@ The Flyway container is configured and only starts and executes migrations after
```php
$flywayContainer = GenericDockerContainer::from(image: 'flyway/flyway:11.0.0')
->withNetwork(name: 'tiny-blocks')
->copyToContainer(pathOnHost: '/migrations', pathOnContainer: '/flyway/sql')
->withVolumeMapping(pathOnHost: '/migrations', pathOnContainer: '/flyway/sql')
->copyToContainer(pathOnHost: '/test-adm-migrations', pathOnContainer: '/flyway/sql')
->withVolumeMapping(pathOnHost: '/test-adm-migrations', pathOnContainer: '/flyway/sql')
->withWaitBeforeRun(
wait: ContainerWaitForDependency::untilReady(
condition: MySQLReady::from(
Expand All @@ -216,7 +252,10 @@ $flywayContainer = GenericDockerContainer::from(image: 'flyway/flyway:11.0.0')
->withEnvironmentVariable(key: 'FLYWAY_LOCATIONS', value: 'filesystem:/flyway/sql')
->withEnvironmentVariable(key: 'FLYWAY_CLEAN_DISABLED', value: 'false')
->withEnvironmentVariable(key: 'FLYWAY_VALIDATE_MIGRATION_NAMING', value: 'true')
->run(commands: ['-connectRetries=15', 'clean', 'migrate']);
->run(
commands: ['-connectRetries=15', 'clean', 'migrate'],
waitAfterStarted: ContainerWaitForTime::forSeconds(seconds: 5)
);
```

<div id='license'></div>
Expand Down
7 changes: 3 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
"minimum-stability": "stable",
"keywords": [
"psr",
"tests",
"docker",
"tiny-blocks",
"test-containers",
"docker-container"
],
"authors": [
Expand All @@ -23,10 +25,7 @@
"source": "https://github.com/tiny-blocks/docker-container"
},
"config": {
"sort-packages": true,
"allow-plugins": {
"infection/extension-installer": true
}
"sort-packages": true
},
"autoload": {
"psr-4": {
Expand Down
16 changes: 13 additions & 3 deletions src/Contracts/MySQL/MySQLContainerStarted.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,25 @@
*/
interface MySQLContainerStarted extends ContainerStarted
{
/**
* Default JDBC options for connecting to the MySQL container.
*/
public const array DEFAULT_JDBC_OPTIONS = [
'useSSL' => 'false',
'useUnicode' => 'yes',
'characterEncoding' => 'UTF-8',
'allowPublicKeyRetrieval' => 'true'
];

/**
* Generates and returns a JDBC URL for connecting to the MySQL container.
*
* The URL is built using the container's hostname, port, and database name,
* with optional query parameters for additional configurations.
*
* @param string|null $options A query string to append to the JDBC URL.
* Example: "useSSL=false&serverTimezone=UTC".
* @param array $options An array of key-value pairs to append to the JDBC URL.
* Defaults to {@see DEFAULT_JDBC_OPTIONS}.
* @return string The generated JDBC URL.
*/
public function getJdbcUrl(?string $options = null): string;
public function getJdbcUrl(array $options = self::DEFAULT_JDBC_OPTIONS): string;
}
16 changes: 8 additions & 8 deletions src/GenericDockerContainer.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
use TinyBlocks\DockerContainer\Internal\Commands\Options\PortOption;
use TinyBlocks\DockerContainer\Internal\Commands\Options\SimpleCommandOption;
use TinyBlocks\DockerContainer\Internal\Commands\Options\VolumeOption;
use TinyBlocks\DockerContainer\Internal\ContainerHandler;
use TinyBlocks\DockerContainer\Internal\ContainerCommandHandler;
use TinyBlocks\DockerContainer\Internal\Containers\Models\Container;
use TinyBlocks\DockerContainer\Internal\Containers\Started;
use TinyBlocks\DockerContainer\Waits\ContainerWaitAfterStarted;
Expand All @@ -34,7 +34,7 @@ class GenericDockerContainer implements DockerContainer

private bool $autoRemove = true;

private ContainerHandler $containerHandler;
private ContainerCommandHandler $commandHandler;

private ?ContainerWaitBeforeStarted $waitBeforeStarted = null;

Expand All @@ -46,7 +46,7 @@ private function __construct(private readonly Container $container)
$this->volumes = CommandOptions::createFromEmpty();
$this->environmentVariables = CommandOptions::createFromEmpty();

$this->containerHandler = new ContainerHandler(client: new DockerClient());
$this->commandHandler = new ContainerCommandHandler(client: new DockerClient());
}

public static function from(string $image, ?string $name = null): static
Expand All @@ -71,17 +71,17 @@ public function run(array $commands = [], ?ContainerWaitAfterStarted $waitAfterS
environmentVariables: $this->environmentVariables
);

$container = $this->containerHandler->run(command: $dockerRun);
$container = $this->commandHandler->run(dockerRun: $dockerRun);

$this->items->each(
actions: function (VolumeOption $volume) use ($container) {
$item = ItemToCopyOption::from(id: $container->id, volume: $volume);
$dockerCopy = DockerCopy::from(item: $item);
$this->containerHandler->execute(command: $dockerCopy);
$this->commandHandler->execute(command: $dockerCopy);
}
);

$containerStarted = new Started(container: $container, containerHandler: $this->containerHandler);
$containerStarted = new Started(container: $container, commandHandler: $this->commandHandler);
$waitAfterStarted?->waitAfter(containerStarted: $containerStarted);

return $containerStarted;
Expand All @@ -92,10 +92,10 @@ public function runIfNotExists(
?ContainerWaitAfterStarted $waitAfterStarted = null
): ContainerStarted {
$dockerList = DockerList::from(container: $this->container);
$container = $this->containerHandler->findBy(command: $dockerList);
$container = $this->commandHandler->findBy(dockerList: $dockerList);

if ($container->hasId()) {
return new Started(container: $container, containerHandler: $this->containerHandler);
return new Started(container: $container, commandHandler: $this->commandHandler);
}

return $this->run(commands: $commands, waitAfterStarted: $waitAfterStarted);
Expand Down
2 changes: 1 addition & 1 deletion src/Internal/Client/DockerClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public function execute(Command $command): ExecutionCompleted

return Execution::from(process: $process);
} catch (Throwable $exception) {
throw new DockerCommandExecutionFailed(process: $process, exception: $exception);
throw DockerCommandExecutionFailed::fromProcess(process: $process, exception: $exception);
}
}
}
4 changes: 3 additions & 1 deletion src/Internal/Client/Execution.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ private function __construct(private string $output, private bool $successful)

public static function from(Process $process): Execution
{
return new Execution(output: $process->getOutput(), successful: $process->isSuccessful());
$output = $process->isSuccessful() ? $process->getOutput() : $process->getErrorOutput();

return new Execution(output: $output, successful: $process->isSuccessful());
}

public function getOutput(): string
Expand Down
45 changes: 45 additions & 0 deletions src/Internal/CommandHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

namespace TinyBlocks\DockerContainer\Internal;

use TinyBlocks\DockerContainer\Contracts\ExecutionCompleted;
use TinyBlocks\DockerContainer\Internal\Commands\Command;
use TinyBlocks\DockerContainer\Internal\Commands\DockerList;
use TinyBlocks\DockerContainer\Internal\Commands\DockerRun;
use TinyBlocks\DockerContainer\Internal\Containers\Models\Container;
use TinyBlocks\DockerContainer\Internal\Exceptions\DockerCommandExecutionFailed;

/**
* Handles Docker command execution.
*/
interface CommandHandler
{
/**
* Executes a Docker run command.
*
* @param DockerRun $dockerRun The command to run the container.
* @return Container The created container.
* @throws DockerCommandExecutionFailed If the command execution fails.
*/
public function run(DockerRun $dockerRun): Container;

/**
* Finds a container based on the provided criteria.
*
* @param DockerList $dockerList The criteria to find the container.
* @return Container The found container or a new one if not found.
* @throws DockerCommandExecutionFailed If the command execution fails.
*/
public function findBy(DockerList $dockerList): Container;

/**
* Executes a generic Docker command.
*
* @param Command $command The command to execute.
* @return ExecutionCompleted The result of the execution.
* @throws DockerCommandExecutionFailed If the command execution fails.
*/
public function execute(Command $command): ExecutionCompleted;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
use TinyBlocks\DockerContainer\Internal\Containers\Factories\ContainerFactory;
use TinyBlocks\DockerContainer\Internal\Containers\Models\Container;
use TinyBlocks\DockerContainer\Internal\Containers\Models\ContainerId;
use TinyBlocks\DockerContainer\Internal\Exceptions\DockerCommandExecutionFailed;

final readonly class ContainerHandler
final readonly class ContainerCommandHandler implements CommandHandler
{
private ContainerFactory $containerFactory;

Expand All @@ -22,18 +23,23 @@ public function __construct(private Client $client)
$this->containerFactory = new ContainerFactory(client: $client);
}

public function run(DockerRun $command): Container
public function run(DockerRun $dockerRun): Container
{
$executionCompleted = $this->client->execute(command: $command);
$executionCompleted = $this->client->execute(command: $dockerRun);

if (!$executionCompleted->isSuccessful()) {
throw DockerCommandExecutionFailed::fromCommand(command: $dockerRun, execution: $executionCompleted);
}

$id = ContainerId::from(value: $executionCompleted->getOutput());

return $this->containerFactory->buildFrom(id: $id, container: $command->container);
return $this->containerFactory->buildFrom(id: $id, container: $dockerRun->container);
}

public function findBy(DockerList $command): Container
public function findBy(DockerList $dockerList): Container
{
$container = $command->container;
$executionCompleted = $this->client->execute(command: $command);
$container = $dockerList->container;
$executionCompleted = $this->client->execute(command: $dockerList);

$output = $executionCompleted->getOutput();

Expand Down
11 changes: 8 additions & 3 deletions src/Internal/Containers/Drivers/MySQL/MySQLStarted.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ public static function from(ContainerStarted $containerStarted): MySQLStarted
{
return new MySQLStarted(
container: $containerStarted->container,
containerHandler: $containerStarted->containerHandler
commandHandler: $containerStarted->commandHandler
);
}

public function getJdbcUrl(?string $options = null): string
public function getJdbcUrl(array $options = self::DEFAULT_JDBC_OPTIONS): string
{
$address = $this->getAddress();
$port = $address->getPorts()->firstExposedPort() ?? self::DEFAULT_MYSQL_PORT;
Expand All @@ -29,6 +29,11 @@ public function getJdbcUrl(?string $options = null): string

$baseUrl = sprintf('jdbc:mysql://%s:%d/%s', $hostname, $port, $database);

return $options ? sprintf('%s?%s', $baseUrl, ltrim($options, '?')) : $baseUrl;
if (!empty($options)) {
$queryString = http_build_query($options);
return sprintf('%s?%s', $baseUrl, $queryString);
}

return $baseUrl;
}
}
8 changes: 4 additions & 4 deletions src/Internal/Containers/Started.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@
use TinyBlocks\DockerContainer\Contracts\ContainerStarted;
use TinyBlocks\DockerContainer\Contracts\EnvironmentVariables;
use TinyBlocks\DockerContainer\Contracts\ExecutionCompleted;
use TinyBlocks\DockerContainer\Internal\CommandHandler;
use TinyBlocks\DockerContainer\Internal\Commands\DockerExecute;
use TinyBlocks\DockerContainer\Internal\Commands\DockerStop;
use TinyBlocks\DockerContainer\Internal\ContainerHandler;
use TinyBlocks\DockerContainer\Internal\Containers\Models\Container;

readonly class Started implements ContainerStarted
{
public function __construct(public Container $container, public ContainerHandler $containerHandler)
public function __construct(public Container $container, public CommandHandler $commandHandler)
{
}

Expand Down Expand Up @@ -43,13 +43,13 @@ public function stop(int $timeoutInWholeSeconds = self::DEFAULT_TIMEOUT_IN_WHOLE
{
$command = DockerStop::from(id: $this->container->id, timeoutInWholeSeconds: $timeoutInWholeSeconds);

return $this->containerHandler->execute(command: $command);
return $this->commandHandler->execute(command: $command);
}

public function executeAfterStarted(array $commands): ExecutionCompleted
{
$command = DockerExecute::from(name: $this->container->name, commandOptions: $commands);

return $this->containerHandler->execute(command: $command);
return $this->commandHandler->execute(command: $command);
}
}
Loading