feat(docker): pull-check + comparaison déterministe par stack (tâche 2 SJ-5)

- 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>
This commit is contained in:
2026-06-05 21:02:38 +02:00
parent 2af8e74079
commit b1c81ba518
7 changed files with 554 additions and 5 deletions
@@ -0,0 +1,57 @@
# Tâche 2 — SJ-5 : Docker pull-check + comparaison déterministe
> Statut : **implémenté** (2026-06-05). tsc 0 erreur · 85 tests · build OK.
> Réf. design : `docs/design/tache2/20-docker.md §4.3`, `40-contrats-json.md §3/§6`, `80-sous-jalons.md` SJ-5.
## Périmètre livré
Télécharger les images candidates d'un stack Compose **sans démarrer de conteneur**
(`docker compose pull`), comparer avant/après par image ID + repo digest + label OCI,
et persister l'état des services — **sans toucher au flux jalon 1** et sans déclencher
de pull automatique (action manuelle par stack, non incluse dans `refreshMachine`).
## Composants
- **Template** `templates/docker/pull-check.sh.tpl` — délimiteurs Mustache `<% %>`
(`<%stackDir%>`), Go-templates `{{.Id}}` / `{{join .RepoDigests ","}}` préservés.
Sections `===SU:DOCKER_INSPECT_BEFORE/PULL/INSPECT_AFTER===` + `===SU:EXIT=N===`.
- **`server/services/dockerPull.ts`** :
- `parseDockerPullCheck(raw)` — lit BEFORE/AFTER (id, digest, version), code de sortie,
et extrait les erreurs de pull **nettoyées de tout secret** (URLs, token/bearer/password).
- `buildDockerPullResult(stackName, raw)` — comparaison déterministe → `services`
(`up_to_date | updates_available | error` par image) + `changes` (`operation:"pulled"`
uniquement pour les images modifiées) + `status` global + `errors`.
- `dockerDedupKey(image, fromDigest, toDigest, fromId?, toId?)` — empreinte fonctionnelle
(digests prioritaires, fallback image IDs), conforme `40 §6`.
- `pullCheckStack(machineId, stackId, onData?)` — orchestration SSH + upsert des services
dans `docker_stack_services` (par `stackId + serviceName`), maj `lastUpdateAt` du stack
et `lastPullCheckAt` des settings. **Refuse un stack non `enabled`.**
- **`server/services/dockerPull.test.ts`** — 7 cas (parse, nettoyage secret registry,
classement up_to_date/updates_available, change unique, status global, dédup).
- **Wiring** :
- `runAction(machineId, action, opts?: { stackId })` — branche dédiée `docker_pull_check`
(archivage report/log, `ExecutionResult.docker.pull.changes` + `dedupKey`, event).
- Chemin générique : injection `stackDir` quand `stackId` fourni → **corrige aussi
`docker_inspect_current`** (SJ-4 le déclarait sans orchestration par stack).
- `POST /:id/actions` — allowlist élargie aux actions Docker passives/non-applicatives
(`docker_scan`, `docker_inspect_current`, `docker_pull_check`) ; `stackId` requis pour
les actions par-stack. **Destructives (apply/down/prune agressif) toujours hors API**
jusqu'au socle `action_requests` (SJ-6).
- **`shared/types.ts`** : `DockerImageChange.dedupKey?` (additif, pour mutualisation Hermes).
## Pas de migration
Le schéma SJ-4 (`docker_stack_services` avec `current/candidate_image_id|digest`,
`version_label`, `status` ; `docker_settings.last_pull_check_at`) couvrait déjà SJ-5.
## Sécurité
- `docker compose pull` sans `up` → aucun conteneur recréé (pré-check pur applicatif).
- Erreurs registry (`registry_auth_failed` / `pull_failed`) **nettoyées** : ni URL, ni token,
ni mot de passe ne remontent vers UI/MCP (test dédié).
- Credentials registry (`~/.docker/config.json`) jamais lus ni renvoyés.
## Reste pour SJ-6
`docker_compose_apply` (up -d --remove-orphans), `docker_prune_images`, `docker_compose_down`,
table `docker_image_events`, et validation UI explicite via `action_requests`.