registry, verdaccio, portainer: full nox modules (ADR 0044/0046)
registry (docker v2): catalog/tags/delete tools, emits image.pushed (a build's image is now pullable). verdaccio (npm): list/info tools, emits package.published. portainer: endpoints/stacks/containers tools — tools-only, since its only events are the underlying containers' lifecycle, which the host owns. Typecheck; manifests parse.
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
// The Docker Registry v2 client — registry's own code, living in the module (novox/hq ADR 0044).
|
||||
// Ported from the shared hal sdk, where a change to the registry API rebuilt everything; here it
|
||||
// rebuilds only registry. Both this module's tools and its events entrypoint import it.
|
||||
|
||||
export interface RegistryImage {
|
||||
repo: string;
|
||||
tag: string;
|
||||
digest: string;
|
||||
}
|
||||
|
||||
export class RegistryClient {
|
||||
readonly baseUrl: string;
|
||||
|
||||
// Auth is optional: a mesh-internal registry often runs open on the node, so a Basic header is
|
||||
// sent only when credentials were configured — an empty one would look like a failed login.
|
||||
constructor(
|
||||
url: string,
|
||||
private readonly authHeader?: string,
|
||||
) {
|
||||
this.baseUrl = url.replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Build from the module's resolved environment. The URL is MESH_REGISTRY_URL (or the local
|
||||
* registry port), and credentials — if the registry requires them — are MESH_REGISTRY_USER and
|
||||
* MESH_REGISTRY_PASSWORD. Throws when no URL is configured, so a misconfigured module exposes
|
||||
* nothing rather than talking to the wrong place.
|
||||
*/
|
||||
static fromEnv(env: NodeJS.ProcessEnv = process.env): RegistryClient {
|
||||
const url = env.MESH_REGISTRY_URL ?? `http://127.0.0.1:${env.REGISTRY_PORT ?? "5000"}`;
|
||||
if (!url) throw new Error("no registry URL — set MESH_REGISTRY_URL");
|
||||
const user = env.MESH_REGISTRY_USER;
|
||||
const password = env.MESH_REGISTRY_PASSWORD;
|
||||
const authHeader =
|
||||
user && password ? `Basic ${Buffer.from(`${user}:${password}`).toString("base64")}` : undefined;
|
||||
return new RegistryClient(url, authHeader);
|
||||
}
|
||||
|
||||
private headers(extra: Record<string, string> = {}): Record<string, string> {
|
||||
return { ...(this.authHeader ? { Authorization: this.authHeader } : {}), ...extra };
|
||||
}
|
||||
|
||||
private async getJson<T>(path: string): Promise<T> {
|
||||
const res = await fetch(`${this.baseUrl}${path}`, { headers: this.headers() });
|
||||
if (!res.ok) throw new Error(`Registry ${path}: ${res.status} ${await res.text()}`);
|
||||
return res.json() as Promise<T>;
|
||||
}
|
||||
|
||||
/** The catalog — every repository the registry holds. */
|
||||
async listRepositories(): Promise<string[]> {
|
||||
const data = await this.getJson<{ repositories: string[] | null }>("/v2/_catalog");
|
||||
return data.repositories ?? [];
|
||||
}
|
||||
|
||||
/** The tags of one repository. */
|
||||
async listTags(repo: string): Promise<string[]> {
|
||||
const data = await this.getJson<{ tags: string[] | null }>(`/v2/${repo}/tags/list`);
|
||||
return data.tags ?? [];
|
||||
}
|
||||
|
||||
/** The content digest of a repo:tag — the stable identity a tag currently points at. */
|
||||
async getManifestDigest(repo: string, tag: string): Promise<string> {
|
||||
const res = await fetch(`${this.baseUrl}/v2/${repo}/manifests/${tag}`, {
|
||||
method: "HEAD",
|
||||
headers: this.headers({ Accept: "application/vnd.docker.distribution.manifest.v2+json" }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`Registry manifest ${repo}:${tag}: ${res.status} ${await res.text()}`);
|
||||
const digest = res.headers.get("docker-content-digest");
|
||||
if (!digest) throw new Error(`no Docker-Content-Digest for ${repo}:${tag}`);
|
||||
return digest;
|
||||
}
|
||||
|
||||
/** Delete a manifest by digest. Garbage collection reclaims the storage later. */
|
||||
async deleteManifest(repo: string, digest: string): Promise<void> {
|
||||
const res = await fetch(`${this.baseUrl}/v2/${repo}/manifests/${digest}`, {
|
||||
method: "DELETE",
|
||||
headers: this.headers({ Accept: "application/vnd.docker.distribution.manifest.v2+json" }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`Registry delete ${repo}@${digest}: ${res.status} ${await res.text()}`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user