} */ 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 < /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} */ 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 $artifacts * @return array{script: string, secrets: array} */ 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, '/'); } }