b1c81ba518
- template docker/pull-check.sh.tpl (pull sans up, inspect before/after) - dockerPull: parseDockerPullCheck + buildDockerPullResult (TDD) — compare image id/digest/label OCI → services up_to_date|updates_available|error, changes operation=pulled ; erreurs registry nettoyées (URL/token/password) - dockerDedupKey (digests prioritaires, fallback image ids) + DockerImageChange.dedupKey - pullCheckStack: SSH + upsert docker_stack_services, refuse stack non enabled, refresh Docker séparé (hors refreshMachine, pas de pull auto) - execute: runAction(opts.stackId), branche docker_pull_check, injection stackDir (corrige docker_inspect_current) ; route: allowlist Docker passifs + pull_check, destructives toujours hors API jusqu'à action_requests (SJ-6) Pas de migration (schéma SJ-4 suffisant). tsc 0 erreur · 85 tests · build OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
106 lines
4.0 KiB
TypeScript
106 lines
4.0 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { parseDockerPullCheck, buildDockerPullResult, dockerDedupKey } from "./dockerPull.js";
|
|
|
|
// Stack à 3 images : nginx inchangé, app mis à jour, redis en échec d'auth registry.
|
|
const RAW = [
|
|
"===SU:DOCKER_INSPECT_BEFORE===",
|
|
"BEFORE\tnginx:latest\tsha256:aaa\tnginx@sha256:d1",
|
|
"BEFORE\tmyorg/app:latest\tsha256:bbb\tmyorg/app@sha256:d2",
|
|
"BEFORE\tredis:7\tsha256:ccc\tredis@sha256:d3",
|
|
"===SU:DOCKER_PULL===",
|
|
"nginx Pulling ",
|
|
"nginx Pull complete ",
|
|
"myorg/app Pulling ",
|
|
"myorg/app Downloaded newer image ",
|
|
"redis Pulling ",
|
|
'redis Error response from daemon: Head "https://registry-1.docker.io/v2/library/redis/manifests/7": unauthorized: incorrect username or password for token=AbCdEf123',
|
|
"===SU:DOCKER_INSPECT_AFTER===",
|
|
"AFTER\tnginx:latest\tsha256:aaa\tnginx@sha256:d1\t1.25.3",
|
|
"AFTER\tmyorg/app:latest\tsha256:zzz\tmyorg/app@sha256:d9\t2.4.0",
|
|
"AFTER\tredis:7\tsha256:ccc\tredis@sha256:d3\t7.2.0",
|
|
"===SU:EXIT=18===",
|
|
].join("\n");
|
|
|
|
describe("parseDockerPullCheck", () => {
|
|
it("lit les sections BEFORE / AFTER et le code de sortie", () => {
|
|
const p = parseDockerPullCheck(RAW);
|
|
expect(p.exitCode).toBe(18);
|
|
expect(p.before["myorg/app:latest"]).toEqual({ id: "sha256:bbb", digest: "myorg/app@sha256:d2" });
|
|
expect(p.after["myorg/app:latest"]).toEqual({
|
|
id: "sha256:zzz",
|
|
digest: "myorg/app@sha256:d9",
|
|
version: "2.4.0",
|
|
});
|
|
});
|
|
|
|
it("détecte l'erreur d'authentification registry et la nettoie (pas d'URL ni de token)", () => {
|
|
const p = parseDockerPullCheck(RAW);
|
|
expect(p.pullErrors.length).toBeGreaterThan(0);
|
|
const auth = p.pullErrors.find((e) => e.kind === "registry_auth_failed");
|
|
expect(auth).toBeTruthy();
|
|
expect(auth!.message).not.toContain("registry-1.docker.io");
|
|
expect(auth!.message).not.toContain("AbCdEf123");
|
|
expect(auth!.message.toLowerCase()).toContain("unauthorized");
|
|
});
|
|
});
|
|
|
|
describe("buildDockerPullResult", () => {
|
|
it("classe up_to_date / updates_available par image et n'émet de change que pour les modifiées", () => {
|
|
const r = buildDockerPullResult("media", RAW);
|
|
const byImage = new Map(r.services.map((s) => [s.image, s]));
|
|
|
|
expect(byImage.get("nginx:latest")!.status).toBe("up_to_date");
|
|
const app = byImage.get("myorg/app:latest")!;
|
|
expect(app.status).toBe("updates_available");
|
|
expect(app.currentImageId).toBe("sha256:bbb");
|
|
expect(app.candidateImageId).toBe("sha256:zzz");
|
|
expect(app.candidateVersion).toBe("2.4.0");
|
|
|
|
// Un seul change "pulled" (app). nginx inchangé, redis id inchangé.
|
|
expect(r.changes).toHaveLength(1);
|
|
expect(r.changes[0]).toMatchObject({
|
|
stack: "media",
|
|
imageRef: "myorg/app:latest",
|
|
fromImageId: "sha256:bbb",
|
|
toImageId: "sha256:zzz",
|
|
operation: "pulled",
|
|
});
|
|
});
|
|
|
|
it("status global = error quand le pull renvoie un code non nul / une erreur", () => {
|
|
const r = buildDockerPullResult("media", RAW);
|
|
expect(r.status).toBe("error");
|
|
expect(r.errors.some((e) => e.source === "docker")).toBe(true);
|
|
});
|
|
|
|
it("status = updates_available sans erreur quand tout réussit", () => {
|
|
const ok = [
|
|
"===SU:DOCKER_INSPECT_BEFORE===",
|
|
"BEFORE\tapp:latest\tsha256:old\tapp@sha256:o",
|
|
"===SU:DOCKER_PULL===",
|
|
"app Pulling ",
|
|
"app Downloaded newer image ",
|
|
"===SU:DOCKER_INSPECT_AFTER===",
|
|
"AFTER\tapp:latest\tsha256:new\tapp@sha256:n\t3.0",
|
|
"===SU:EXIT=0===",
|
|
].join("\n");
|
|
const r = buildDockerPullResult("s", ok);
|
|
expect(r.status).toBe("updates_available");
|
|
expect(r.errors).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe("dockerDedupKey", () => {
|
|
it("utilise les digests en priorité", () => {
|
|
expect(dockerDedupKey("app:latest", "app@sha256:d2", "app@sha256:d9")).toBe(
|
|
"app:latest|app@sha256:d2|app@sha256:d9",
|
|
);
|
|
});
|
|
|
|
it("retombe sur les image IDs quand les digests manquent", () => {
|
|
expect(dockerDedupKey("app:latest", null, null, "sha256:bbb", "sha256:zzz")).toBe(
|
|
"app:latest|sha256:bbb|sha256:zzz",
|
|
);
|
|
});
|
|
});
|