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
101 changes: 101 additions & 0 deletions .castor/docker-push-test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php

// This file only tests the "castor docker:push" task, against a real, throwaway
// registry started and destroyed in Docker. It is entirely self-contained: to
// drop it from a project generated from this template, just delete this file
// (and, if it was wired there, the corresponding step in .github/workflows/ci.yml).

namespace docker;

use Castor\Attribute\AsTask;

use function Castor\capture;
use function Castor\context;
use function Castor\http_client;
use function Castor\io;
use function Castor\run;
use function Castor\wait_for_http_status;

#[AsTask(description: 'Tests "docker:push" against a throwaway local registry', namespace: 'docker', name: 'test-push')]
function test_push(): void
{
io()->title('Testing docker:push against a throwaway registry');

$services = array_keys(array_filter(
get_services(),
static fn (array $config) => isset($config['build']['cache_from']),
));

if (!$services) {
throw new \RuntimeException('No service defines a "cache_from", there is nothing to test.');
}

// A dedicated network is required so that the buildx builder (which, on CI runners,
// builds inside its own container) can reach the registry by its container name,
// regardless of which buildx driver is used.
$suffix = bin2hex(random_bytes(4));
$network = "docker-starter-test-{$suffix}";
$registryName = "docker-starter-test-registry-{$suffix}";
$builderName = "docker-starter-test-builder-{$suffix}";
$namespace = 'docker-starter-test';

io()->section('Starting a throwaway registry');
run(['docker', 'network', 'create', $network]);
run([
'docker', 'run', '--detach', '--rm',
'--name', $registryName,
'--network', $network,
'--publish', '127.0.0.1::5000',
'registry:2',
]);

try {
[$host, $port] = explode(':', trim(capture(['docker', 'port', $registryName, '5000/tcp'])));
$hostRegistry = "{$host}:{$port}";

wait_for_http_status("http://{$hostRegistry}/v2/", message: 'Waiting for the throwaway registry to be available...');

io()->section('Creating a throwaway buildx builder on the same network');

// The builder runs in its own container and, unlike the Docker daemon, does not
// trust plain HTTP registries by default: it must be told explicitly.
$buildkitConfig = tempnam(sys_get_temp_dir(), 'buildkitd-');
file_put_contents($buildkitConfig, <<<TOML
[registry."{$registryName}:5000"]
http = true
TOML);

try {
run(['docker', 'buildx', 'create', '--name', $builderName, '--driver', 'docker-container', '--driver-opt', "network={$network}", '--config', $buildkitConfig]);

io()->section('Running docker:push against it');
run(['castor', 'docker:push'], context: context()->withEnvironment([
'DS_REGISTRY' => "{$registryName}:5000/{$namespace}",
'BUILDX_BUILDER' => $builderName,
]));
} finally {
run(['docker', 'buildx', 'rm', $builderName], context: context()->withAllowFailure()->withQuiet());
unlink($buildkitConfig);
}

io()->section('Checking the pushed images');
foreach ($services as $service) {
$tags = http_client()
->request('GET', "http://{$hostRegistry}/v2/{$namespace}/{$service}/tags/list")
->toArray()
;

if (!\in_array('cache', $tags['tags'] ?? [], true)) {
throw new \RuntimeException(\sprintf('Expected a "cache" tag for "%s" in the registry, got: %s', $service, json_encode($tags)));
}

io()->comment("{$service}: OK");
}
} finally {
io()->section('Removing the throwaway registry');
run(['docker', 'stop', $registryName], context: context()->withAllowFailure()->withQuiet());
run(['docker', 'network', 'rm', $network], context: context()->withAllowFailure()->withQuiet());
}

io()->success('docker:push correctly pushed the cache images to the registry.');
}
118 changes: 36 additions & 82 deletions .castor/docker.php
Original file line number Diff line number Diff line change
Expand Up @@ -371,25 +371,33 @@ function workers_stop(): void
}

/**
* @param list<string> $subCommand
* @param list<string> $profiles
* @return array<string, string>
*/
function docker_compose(array $subCommand, ?Context $c = null, array $profiles = []): Process
function get_compose_environment(Context $c): array
{
$c ??= context();
$profiles = $profiles ?: ['default'];

$domains = [$c['root_domain'], ...$c['extra_domains']];
$domains = '`' . implode('`) || Host(`', $domains) . '`';

$c = $c->withEnvironment([
return [
'PROJECT_NAME' => $c['project_name'],
'PROJECT_ROOT_DOMAIN' => $c['root_domain'],
'PROJECT_DOMAINS' => $domains,
'USER_ID' => $c['user_id'],
'PHP_VERSION' => $c['php_version'],
'REGISTRY' => $c['registry'] ?? '',
]);
];
}

/**
* @param list<string> $subCommand
* @param list<string> $profiles
*/
function docker_compose(array $subCommand, ?Context $c = null, array $profiles = []): Process
{
$c ??= context();
$profiles = $profiles ?: ['default'];

$c = $c->withEnvironment(get_compose_environment($c));

$worktreeName = get_worktree_name();
if ($worktreeName) {
Expand Down Expand Up @@ -542,91 +550,37 @@ function push(bool $dryRun = false): void
throw new \RuntimeException('You must define a registry to push images.');
}

// Generate bake file
$targets = [];

foreach (get_services() as $service => $config) {
$cacheFrom = $config['build']['cache_from'][0] ?? null;

if (null === $cacheFrom) {
continue;
}

$cacheFrom = explode(',', $cacheFrom);
$reference = null;
$type = null;
// Only services with a cache_from can push their build cache back to the registry.
$cacheFroms = array_filter(array_map(
static fn (array $config) => $config['build']['cache_from'][0] ?? null,
get_services(),
));

if (1 === \count($cacheFrom)) {
$reference = $cacheFrom[0];
$type = 'registry';
} else {
foreach ($cacheFrom as $part) {
$from = explode('=', $part);

if (2 !== \count($from)) {
continue;
}

if ('type' === $from[0]) {
$type = $from[1];
}
$c = context()
->withEnvironment(get_compose_environment(context()))
->withWorkingDirectory(variable('root_dir') . '/infrastructure/docker')
;

if ('ref' === $from[0]) {
$reference = $from[1];
}
}
}
$command = ['docker', 'buildx', 'bake'];

$targets[] = [
'reference' => $reference,
'type' => $type,
'context' => $config['build']['context'],
'dockerfile' => $config['build']['dockerfile'] ?? 'Dockerfile',
'target' => $config['build']['target'] ?? null,
];
foreach ($c['docker_compose_files'] as $file) {
$command[] = '-f';
$command[] = $file;
}

$content = \sprintf(
<<<'EOHCL'
group "default" {
targets = [%s]
}
$command[] = '--set';
$command[] = '*.args.PHP_VERSION=' . $c['php_version'];

EOHCL,
implode(', ', array_map(static fn ($target) => \sprintf('"%s"', $target['target']), $targets))
);

foreach ($targets as $target) {
$content .= \sprintf(
<<<'EOHCL'
target "%s" {
context = "%s"
dockerfile = "%s"
cache-from = ["%s"]
cache-to = ["type=%s,ref=%s,mode=max"]
target = "%s"
args = {
PHP_VERSION = "%s"
}
}

EOHCL,
$target['target'], $target['context'], $target['dockerfile'], $target['reference'], $target['type'], $target['reference'], $target['target'], variable('php_version')
);
foreach ($cacheFroms as $service => $cacheFrom) {
$command[] = '--set';
$command[] = "{$service}.cache-to={$cacheFrom},mode=max";
}

if ($dryRun) {
io()->write($content);

return;
$command[] = '--print';
}

// write bake file in tmp file
$bakeFile = tempnam(sys_get_temp_dir(), 'bake');
file_put_contents($bakeFile, $content);

// Run bake
run(['docker', 'buildx', 'bake', '-f', $bakeFile]);
run([...$command, ...array_keys($cacheFroms)], context: $c);
}

/**
Expand Down
1 change: 1 addition & 0 deletions .castor/init.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ function init(): void
{
fs()->remove([
'.github/',
'.castor/docker-push-test.php',
'README.md',
'CHANGELOG.md',
'CONTRIBUTING.md',
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ jobs:
- name: "Run PHPUnit"
run: "castor qa:phpunit"

- name: "Test docker:push"
run: "castor docker:test-push"

- name: "Test HTTP server"
run: |
set -e
Expand Down
Loading