Files
keystone/app/Services/Registries/ManagedRegistryOperationScripts.php

190 lines
9.8 KiB
PHP

<?php
namespace App\Services\Registries;
use App\Models\BuildArtifact;
use App\Models\Registry;
use App\Models\Server;
class ManagedRegistryOperationScripts
{
/**
* @return array{script: string, secrets: array<string, string>}
*/
public function provision(Registry $registry): array
{
$credentials = $registry->credentials ?? [];
$host = $this->host($registry);
$storagePath = $registry->storage_path ?: (string) config('keystone.managed_registry.storage_path');
return [
'script' => implode("\n", [
'set -euo pipefail',
'storage_path='.escapeshellarg($storagePath),
'registry_host='.escapeshellarg($host),
'install -d -m 700 -o root -g root /home/keystone/registry/auth /home/keystone/registry/config',
'install -d -m 755 -o root -g root "$storage_path"',
'tmp_htpasswd=$(mktemp)',
'cleanup() { rm -f "$tmp_htpasswd"; unset build_password runtime_password; }',
'trap cleanup EXIT',
'build_password=$(printf %s '.escapeshellarg('[!build_password_base64!]').' | base64 -d)',
'runtime_password=$(printf %s '.escapeshellarg('[!runtime_password_base64!]').' | base64 -d)',
'printf %s "$build_password" | docker run -i --rm --entrypoint htpasswd httpd:2.4-alpine -Bni '.escapeshellarg((string) ($credentials['build_username'] ?? 'keystone-build')).' > "$tmp_htpasswd"',
'printf %s "$runtime_password" | docker run -i --rm --entrypoint htpasswd httpd:2.4-alpine -Bni '.escapeshellarg((string) ($credentials['runtime_username'] ?? 'keystone-runtime')).' >> "$tmp_htpasswd"',
'install -m 600 -o root -g root "$tmp_htpasswd" /home/keystone/registry/auth/htpasswd',
'cat > /home/keystone/registry/config/config.yml <<\'KEYSTONE_REGISTRY_CONFIG\'',
'version: 0.1',
'log:',
' fields:',
' service: registry',
'storage:',
' filesystem:',
' rootdirectory: /var/lib/registry',
' delete:',
' enabled: true',
'http:',
' addr: :5000',
'auth:',
' htpasswd:',
' realm: keystone-managed-registry',
' path: /auth/htpasswd',
'KEYSTONE_REGISTRY_CONFIG',
'docker rm -f keystone-managed-registry >/dev/null 2>&1 || true',
'docker run -d --name keystone-managed-registry --restart unless-stopped -p 127.0.0.1:5000:5000 -e REGISTRY_STORAGE_DELETE_ENABLED=true -v "$storage_path:/var/lib/registry" -v /home/keystone/registry/auth:/auth:ro -v /home/keystone/registry/config/config.yml:/etc/docker/registry/config.yml:ro registry:2',
'install -d -m 755 /home/keystone/gateway/Caddyfile.d',
'cat > /home/keystone/gateway/Caddyfile.d/managed-registry.caddy <<KEYSTONE_CADDY_REGISTRY',
'$registry_host {',
' reverse_proxy 127.0.0.1:5000',
'}',
'KEYSTONE_CADDY_REGISTRY',
'if test -d /home/keystone/gateway/Caddyfile.d; then cat /home/keystone/gateway/Caddyfile.d/*.caddy > /home/keystone/gateway/Caddyfile; fi',
'if docker ps --format \'{{.Names}}\' | grep -qx gateway-1; then docker exec gateway-1 caddy reload --config /etc/caddy/Caddyfile; fi',
'if docker ps --format \'{{.Names}}\' | grep -qx caddy; then docker exec caddy caddy reload --config /etc/caddy/Caddyfile; fi',
'if docker ps --format \'{{.Names}}\' | grep -Eqx \'(gateway-1|caddy)\'; then curl --fail --silent --show-error --location --head https://"$registry_host"/v2/ || test "$?" = "22"; else echo "Registry proxy reload skipped because no Caddy container is running."; fi',
]),
'secrets' => [
'build_password_base64' => base64_encode((string) ($credentials['build_password'] ?? '')),
'runtime_password_base64' => base64_encode((string) ($credentials['runtime_password'] ?? '')),
],
];
}
/**
* @return array{script: string, secrets: array<string, string>}
*/
public function smokeCheck(Registry $registry, Server $server, string $scope, ?string $imageReference = null): array
{
$credentials = $registry->credentials ?? [];
$username = (string) ($credentials[$scope.'_username'] ?? '');
$password = (string) ($credentials[$scope.'_password'] ?? '');
$host = $this->host($registry);
$repository = $imageReference ?: $host.'/keystone/smoke/server-'.$server->id.':latest';
$commands = [
'set -euo pipefail',
'registry_host='.escapeshellarg($host),
'image_ref='.escapeshellarg($repository),
'username='.escapeshellarg($username),
'password=$(printf %s '.escapeshellarg('[!registry_password_base64!]').' | base64 -d)',
'curl --fail --silent --show-error --user "$username:$password" https://"$registry_host"/v2/ >/dev/null',
'printf %s '.escapeshellarg('[!registry_password_base64!]').' | base64 -d | docker login "$registry_host" --username '.escapeshellarg($username).' --password-stdin >/dev/null',
];
if ($scope === 'build') {
$commands = [
...$commands,
'docker pull busybox:latest >/dev/null',
'docker tag busybox:latest "$image_ref"',
'docker push "$image_ref" >/dev/null',
'docker buildx imagetools inspect "$image_ref" >/dev/null',
'printf "smoke_ref=%s\n" "$image_ref"',
'unset password',
];
} else {
$commands = [
...$commands,
'docker pull "$image_ref" >/dev/null',
'unset password',
];
}
return [
'script' => implode("\n", $commands),
'secrets' => [
'registry_password_base64' => base64_encode($password),
],
];
}
/**
* @param iterable<int, BuildArtifact> $artifacts
* @return array{script: string, secrets: array<string, string>}
*/
public function maintenance(Registry $registry, iterable $artifacts): array
{
$credentials = $registry->credentials ?? [];
$host = $this->host($registry);
$deletions = [];
foreach ($artifacts as $artifact) {
$repository = $this->repositoryPath((string) $artifact->registry_ref);
if ($repository === '' || blank($artifact->image_digest)) {
continue;
}
$deletions[] = 'delete_manifest '.escapeshellarg($repository).' '.escapeshellarg((string) $artifact->image_digest).' || delete_failures=1';
}
return [
'script' => implode("\n", [
'set -euo pipefail',
'registry_host='.escapeshellarg($host),
'lock_file=/home/keystone/registry/maintenance.lock',
'exec 9>"$lock_file"',
'flock -n 9',
'username='.escapeshellarg((string) ($credentials['build_username'] ?? 'keystone-build')),
'password=$(printf %s '.escapeshellarg('[!registry_password_base64!]').' | base64 -d)',
'curl_config=$(mktemp)',
'registry_was_stopped=0',
'cleanup() { rm -f "$curl_config"; unset password auth_header; if test "$registry_was_stopped" = "1"; then docker start keystone-managed-registry >/dev/null 2>&1 || true; fi; }',
'trap cleanup EXIT',
'auth_header=$(printf "%s:%s" "$username" "$password" | base64 | tr -d "\n")',
'printf "header = \"Authorization: Basic %s\"\n" "$auth_header" > "$curl_config"',
'chmod 600 "$curl_config"',
'delete_failures=0',
'delete_manifest() {',
' repository="$1"',
' digest="$2"',
' status=$(curl --silent --show-error --output /tmp/keystone-registry-delete-response --write-out "%{http_code}" --request DELETE --config "$curl_config" --header "Accept: application/vnd.docker.distribution.manifest.v2+json" "https://$registry_host/v2/$repository/manifests/$digest" || true)',
' case "$status" in 2*|404) printf "deleted_manifest=%s@%s status=%s\n" "$repository" "$digest" "$status" ;; *) cat /tmp/keystone-registry-delete-response >&2; printf "delete_failed=%s@%s status=%s\n" "$repository" "$digest" "$status" >&2; return 1 ;; esac',
'}',
...$deletions,
'test "$delete_failures" = "0"',
'docker stop keystone-managed-registry',
'registry_was_stopped=1',
'docker run --rm -v /home/keystone/registry/config/config.yml:/etc/docker/registry/config.yml:ro -v '.escapeshellarg(($registry->storage_path ?: (string) config('keystone.managed_registry.storage_path')).':/var/lib/registry').' registry:2 garbage-collect --delete-untagged /etc/docker/registry/config.yml',
'docker start keystone-managed-registry',
'registry_was_stopped=0',
'unset password',
]),
'secrets' => [
'registry_password_base64' => base64_encode((string) ($credentials['build_password'] ?? '')),
],
];
}
private function host(Registry $registry): string
{
return rtrim((string) preg_replace('#^https?://#', '', (string) $registry->url), '/');
}
private function repositoryPath(string $reference): string
{
$withoutHost = str($reference)->after('/')->value();
$withoutTag = preg_replace('/:[^:\/]+$/', '', $withoutHost);
return trim((string) $withoutTag, '/');
}
}