mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-27 10:46:51 +00:00
fix: recognize clean one-shot completions in health gate and drift (#1691)
* fix: recognize clean one-shot completions in health gate and drift Treat exit 0 with restart policy no/absent as successful completion so init and migration jobs no longer fail post-update observation or show as service-missing, while long-running restart policies still fail closed. * fix: ignore residual health on clean one-shots and honor deploy.restart_policy Completed exit-0 jobs with no-restart intent no longer fail the health gate on leftover starting/unhealthy state, and Drift treats deploy.restart_policy with Compose precedence so any/on-failure services are not mistaken for one-shots. * fix: require explicit Compose restart no for one-shot recognition Docker inspect reports restart no for both intentional jobs and bare services that omit restart, so Health Gate and Drift now require declared restart:""no"" (or deploy.restart_policy condition none) and load Compose intent once per gate.
This commit is contained in:
@@ -8,6 +8,7 @@ import { parseEffectiveModel } from './preflight/effectiveModel';
|
||||
import { compareStackNetworks, fromDeclaredCompose } from './network/normalize';
|
||||
import { sanitizeForLog, redactSensitiveText } from '../utils/safeLog';
|
||||
import { getErrorMessage } from '../utils/errors';
|
||||
import { isCleanOneShotCompletion } from '../utils/oneShotCompletion';
|
||||
|
||||
const MAX_RENDER_ERROR = 600;
|
||||
|
||||
@@ -178,16 +179,17 @@ function networkDriftFindings(
|
||||
}
|
||||
|
||||
/**
|
||||
* Pure diff step (no Docker / FS access) so it is directly unit-testable. Only
|
||||
* running containers are compared, since a stopped container publishes no ports
|
||||
* and is not "deployed": a declared service with no running container is
|
||||
* service-missing, a running container with no matching service is
|
||||
* service-undeclared, and image / port differences are checked only for services
|
||||
* present on both sides so a missing/undeclared service is not double-reported.
|
||||
* Pure diff step (no Docker / FS access) so it is directly unit-testable.
|
||||
* Running/restarting containers drive image/port comparison. Clean one-shot
|
||||
* completions (exit 0 + explicit declared restart "no", including normalized
|
||||
* `deploy.restart_policy.condition: none`) satisfy service presence without
|
||||
* counting as hasContainers. Omitting restart does not qualify. Network
|
||||
* comparison still uses only running/restarting attachments.
|
||||
*/
|
||||
export function assembleStackDrift(input: AssembleStackDriftInput): StackDriftReport {
|
||||
const { stack, declared, containers, parseError } = input;
|
||||
const networks = input.networks ?? [];
|
||||
// Public contract: at least one running/restarting container (not "satisfied").
|
||||
const hasContainers = containers.some((c) => RUNNING_STATES.has(c.state));
|
||||
|
||||
// A parse failure means the declared model is untrustworthy: report drift
|
||||
@@ -196,30 +198,44 @@ export function assembleStackDrift(input: AssembleStackDriftInput): StackDriftRe
|
||||
return { stack, status: 'drifted', hasComposeFile: false, hasContainers, findings: [], parseError };
|
||||
}
|
||||
|
||||
const runtimeByService = new Map<string, RuntimeService>();
|
||||
for (const c of containers) {
|
||||
if (!RUNNING_STATES.has(c.state)) continue;
|
||||
const name = c.service ?? c.name;
|
||||
const agg = runtimeByService.get(name) ?? { images: new Set<string>(), ports: new Set<string>() };
|
||||
if (c.image) agg.images.add(normalizeImageRef(c.image));
|
||||
for (const p of c.ports) agg.ports.add(portKey(p.publishedPort, p.protocol));
|
||||
runtimeByService.set(name, agg);
|
||||
}
|
||||
|
||||
// Nothing running: the stack is defined on disk but not deployed. One status
|
||||
// conveys this; per-service findings would just be noise.
|
||||
if (!hasContainers) {
|
||||
return { stack, status: 'missing-runtime', hasComposeFile: true, hasContainers: false, findings: [] };
|
||||
}
|
||||
|
||||
const declaredByName = new Map<string, DeclaredService>();
|
||||
for (const svc of declared.services) declaredByName.set(svc.name, svc);
|
||||
|
||||
const runtimeByService = new Map<string, RuntimeService>();
|
||||
const oneShotSatisfied = new Set<string>();
|
||||
for (const c of containers) {
|
||||
const name = c.service ?? c.name;
|
||||
if (RUNNING_STATES.has(c.state)) {
|
||||
const agg = runtimeByService.get(name) ?? { images: new Set<string>(), ports: new Set<string>() };
|
||||
if (c.image) agg.images.add(normalizeImageRef(c.image));
|
||||
for (const p of c.ports) agg.ports.add(portKey(p.publishedPort, p.protocol));
|
||||
runtimeByService.set(name, agg);
|
||||
continue;
|
||||
}
|
||||
const declaredRestart = declaredByName.get(name)?.restart;
|
||||
if (isCleanOneShotCompletion({
|
||||
state: c.state,
|
||||
exitCode: c.exitCode,
|
||||
restartPolicy: declaredRestart,
|
||||
})) {
|
||||
oneShotSatisfied.add(name);
|
||||
}
|
||||
}
|
||||
|
||||
const servicePresent = (serviceName: string): boolean =>
|
||||
runtimeByService.has(serviceName) || oneShotSatisfied.has(serviceName);
|
||||
|
||||
// Nothing running and no declared service satisfied by a clean one-shot: the
|
||||
// stack is defined on disk but not deployed. One status conveys this.
|
||||
if (!hasContainers && !declared.services.some((svc) => servicePresent(svc.name))) {
|
||||
return { stack, status: 'missing-runtime', hasComposeFile: true, hasContainers: false, findings: [] };
|
||||
}
|
||||
|
||||
const findings: StackDriftFinding[] = [];
|
||||
|
||||
// Declared service with no running container.
|
||||
// Declared service with no running container and no clean one-shot completion.
|
||||
for (const svc of declared.services) {
|
||||
if (!runtimeByService.has(svc.name)) {
|
||||
if (!servicePresent(svc.name)) {
|
||||
findings.push({
|
||||
kind: 'service-missing',
|
||||
service: svc.name,
|
||||
@@ -239,7 +255,7 @@ export function assembleStackDrift(input: AssembleStackDriftInput): StackDriftRe
|
||||
}
|
||||
}
|
||||
|
||||
// Image / port divergence for services present on both sides.
|
||||
// Image / port divergence for services present on both sides (running only).
|
||||
for (const [name, svc] of declaredByName) {
|
||||
const runtime = runtimeByService.get(name);
|
||||
if (!runtime) continue;
|
||||
|
||||
Reference in New Issue
Block a user