*/ public function markPrunable(Registry $registry): Collection { if ($registry->type !== RegistryType::MANAGED) { return collect(); } $keep = max(1, (int) $registry->retention_successful_artifacts); $updated = collect(); Environment::query() ->whereHas('buildArtifacts', fn ($query) => $query ->where('status', BuildArtifactStatus::AVAILABLE) ->where('registry_ref', 'like', rtrim((string) $registry->url, '/').'/%')) ->with(['services', 'buildArtifacts' => fn ($query) => $query ->where('status', BuildArtifactStatus::AVAILABLE) ->where('registry_ref', 'like', rtrim((string) $registry->url, '/').'/%') ->latest()]) ->each(function (Environment $environment) use ($keep, $updated): void { $activeDigests = $environment->services ->flatMap(fn ($service): array => [ $service->available_image_digest, $service->current_image_digest, ]) ->filter() ->all(); $environment->buildArtifacts ->skip($keep) ->filter(fn (BuildArtifact $artifact): bool => ! in_array($artifact->image_digest, $activeDigests, true)) ->each(function (BuildArtifact $artifact) use ($updated): void { $metadata = $artifact->metadata ?? []; $artifact->update([ 'status' => BuildArtifactStatus::PRUNABLE, 'metadata' => [ ...$metadata, 'prunable_at' => now()->toIso8601String(), 'prune_command' => $this->deleteManifestCommand($artifact), ], ]); $updated->push($artifact->refresh()); }); }); return $updated; } public function deleteManifestCommand(BuildArtifact $artifact): string { $reference = (string) ($artifact->registry_ref ?? ''); $digest = (string) $artifact->image_digest; return 'curl --fail --silent --show-error --request DELETE '.escapeshellarg('https://'.$this->manifestPath($reference, $digest)); } private function manifestPath(string $reference, string $digest): string { $hostAndPath = preg_replace('/:[^:\/]+$/', '', $reference); $path = str($hostAndPath)->after('/')->value(); return $path === '' ? 'v2/' : str($reference)->before('/')->value().'/v2/'.$path.'/manifests/'.$digest; } }