Add managed registry provisioning, pruning, and readiness tracking
This commit is contained in:
@@ -4,11 +4,15 @@ namespace App\Actions\Environments;
|
||||
|
||||
use App\Enums\BuildArtifactStatus;
|
||||
use App\Enums\BuildStrategy;
|
||||
use App\Enums\RegistryType;
|
||||
use App\Models\BuildArtifact;
|
||||
use App\Models\Operation;
|
||||
use App\Models\Registry;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Services\Operations\RemoteCommandRunner;
|
||||
use App\Services\Registries\RegistryDockerAuthScript;
|
||||
use App\Services\Registries\RegistryResolver;
|
||||
use RuntimeException;
|
||||
|
||||
class BuildApplicationArtifact
|
||||
@@ -61,6 +65,18 @@ class BuildApplicationArtifact
|
||||
|
||||
private function buildServer(BuildArtifact $artifact): Server
|
||||
{
|
||||
$buildServerId = (int) ($artifact->metadata['build_server_id'] ?? 0);
|
||||
|
||||
if ($buildServerId > 0) {
|
||||
$server = Server::find($buildServerId);
|
||||
|
||||
if ($server instanceof Server && $server->build_enabled) {
|
||||
return $server;
|
||||
}
|
||||
|
||||
throw new RuntimeException('Configured build server is missing or not build-enabled.');
|
||||
}
|
||||
|
||||
if ($artifact->builtByService instanceof Service) {
|
||||
$server = $artifact->builtByService->replicas->first()?->server ?: $artifact->builtByService->server;
|
||||
|
||||
@@ -70,7 +86,7 @@ class BuildApplicationArtifact
|
||||
}
|
||||
|
||||
if (($artifact->metadata['build_strategy'] ?? null) === BuildStrategy::DEDICATED_BUILDER->value) {
|
||||
throw new RuntimeException('Dedicated builder strategy requires a builder service.');
|
||||
throw new RuntimeException('Dedicated builder strategy requires a builder service or build-enabled server.');
|
||||
}
|
||||
|
||||
$services = $artifact->environment->services()
|
||||
@@ -107,9 +123,13 @@ class BuildApplicationArtifact
|
||||
|
||||
$operationDirectory = '/home/keystone/operations/build-'.$artifact->id.'-'.str()->random(8);
|
||||
$imageReference = $artifact->registry_ref ?: $artifact->image_tag;
|
||||
$pushCommand = $strategy === BuildStrategy::DEDICATED_BUILDER && $artifact->registry_ref
|
||||
? "\ndocker push ".escapeshellarg($imageReference)
|
||||
: '';
|
||||
$publishCommands = $artifact->registry_ref && $strategy !== BuildStrategy::EXTERNAL_REGISTRY
|
||||
? [
|
||||
...$this->pushDigestCommands($imageReference),
|
||||
]
|
||||
: [
|
||||
'digest=$(docker image inspect --format '.escapeshellarg('{{if .RepoDigests}}{{index .RepoDigests 0}}{{else}}{{.Id}}{{end}}').' '.escapeshellarg($imageReference).')',
|
||||
];
|
||||
|
||||
return implode("\n", [
|
||||
'set -euo pipefail',
|
||||
@@ -126,25 +146,91 @@ class BuildApplicationArtifact
|
||||
'git clone --depth 1 --branch '.escapeshellarg($artifact->environment->branch).' '.escapeshellarg($application->repository_url).' "$source_dir"',
|
||||
$this->writeFileCommand('$source_dir/Dockerfile.keystone', $this->dockerfile($artifact)),
|
||||
'cd "$source_dir"',
|
||||
'docker build --file Dockerfile.keystone --tag '.escapeshellarg($imageReference).' .'.$pushCommand,
|
||||
'digest=$(docker image inspect --format '.escapeshellarg('{{if .RepoDigests}}{{index .RepoDigests 0}}{{else}}{{.Id}}{{end}}').' '.escapeshellarg($imageReference).')',
|
||||
...$this->registryMaintenanceLockCommands($artifact),
|
||||
...$this->buildAuthCommands($artifact),
|
||||
'docker build --file Dockerfile.keystone --tag '.escapeshellarg($imageReference).' .',
|
||||
...$publishCommands,
|
||||
'printf "image_digest=%s\n" "$digest"',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function registryMaintenanceLockCommands(BuildArtifact $artifact): array
|
||||
{
|
||||
if (($artifact->metadata['registry_type'] ?? null) !== RegistryType::MANAGED->value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
'install -d -m 700 -o root -g root /home/keystone/registry',
|
||||
'exec 9>/home/keystone/registry/maintenance.lock',
|
||||
'flock 9',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function buildAuthCommands(BuildArtifact $artifact): array
|
||||
{
|
||||
if (($artifact->metadata['registry_type'] ?? null) !== RegistryType::MANAGED->value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$registry = app(RegistryResolver::class)->buildRegistryFor($artifact->environment->application->organisation);
|
||||
|
||||
if (! $registry instanceof Registry || $registry->type !== RegistryType::MANAGED || ! $registry->credentials) {
|
||||
throw new RuntimeException('Managed registry build credentials are not configured.');
|
||||
}
|
||||
|
||||
$auth = app(RegistryDockerAuthScript::class)->forBuild($registry, 'root');
|
||||
$script = $auth['script'];
|
||||
|
||||
foreach ($auth['secrets'] as $key => $value) {
|
||||
$script = str_replace("[!{$key}!]", $value, $script);
|
||||
}
|
||||
|
||||
return [$script];
|
||||
}
|
||||
|
||||
private function manifestDigestScript(BuildArtifact $artifact): string
|
||||
{
|
||||
$imageReference = $artifact->registry_ref ?: $artifact->image_tag;
|
||||
|
||||
return implode("\n", [
|
||||
'set -euo pipefail',
|
||||
'manifest=$(docker manifest inspect '.escapeshellarg($imageReference).')',
|
||||
'digest=$(printf "%s" "$manifest" | sed -n '.escapeshellarg('s/.*"digest": "\(sha256:[^"]*\)".*/\1/p').' | head -n 1)',
|
||||
'test -n "$digest"',
|
||||
...$this->manifestDigestCommands($imageReference),
|
||||
'printf "image_digest=%s\n" "$digest"',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function manifestDigestCommands(string $imageReference): array
|
||||
{
|
||||
return [
|
||||
'inspect_output=$(docker buildx imagetools inspect '.escapeshellarg($imageReference).')',
|
||||
'digest=$(printf "%s\n" "$inspect_output" | sed -n '.escapeshellarg('s/^Digest:[[:space:]]*\(sha256:[^[:space:]]*\).*/\1/p').' | head -n 1)',
|
||||
'test -n "$digest"',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function pushDigestCommands(string $imageReference): array
|
||||
{
|
||||
return [
|
||||
'push_output=$(docker push '.escapeshellarg($imageReference).')',
|
||||
'printf "%s\n" "$push_output"',
|
||||
'digest=$(printf "%s\n" "$push_output" | sed -n '.escapeshellarg('s/.*digest: \(sha256:[^[:space:]]*\).*/\1/p').' | tail -n 1)',
|
||||
'test -n "$digest"',
|
||||
];
|
||||
}
|
||||
|
||||
private function dockerfile(BuildArtifact $artifact): string
|
||||
{
|
||||
$service = $artifact->environment->services()
|
||||
@@ -176,7 +262,7 @@ DOCKERFILE;
|
||||
private function digestFromOutput(string $output): string
|
||||
{
|
||||
if (preg_match('/image_digest=(?<digest>\S+)/', $output, $matches)) {
|
||||
return $this->digestFromOutput($matches['digest']);
|
||||
$output = $matches['digest'];
|
||||
}
|
||||
|
||||
if (str_contains($output, '@')) {
|
||||
|
||||
Reference in New Issue
Block a user