190 lines
9.8 KiB
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, '/');
|
|
}
|
|
}
|