Skip to content

Commit

Permalink
Release/1.1.0 (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
gustavofreze authored Dec 2, 2024
1 parent 0c39f44 commit 745e0d6
Show file tree
Hide file tree
Showing 20 changed files with 404 additions and 94 deletions.
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

0 comments on commit 745e0d6

Please sign in to comment.