mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-28 19:27:41 +00:00
fix(compose-doctor): resolve effective healthcheck coverage (#1713)
* fix(compose-doctor): resolve effective healthcheck coverage Compose Doctor now classifies healthcheck coverage from the Compose model, running containers, and local images so image-provided HEALTHCHECKs are not false positives. Update Guard shares the same presence helper so test NONE is not treated as active. * fix(compose-doctor): fix healthcheck project label and empty compose HC Use the Compose project name for runtime container listing so stacks whose name: differs from the directory still get runtime evidence. Treat empty or timing-only healthcheck objects as absent rather than active. * fix(compose-doctor): treat inherited healthcheck as All Clear note Inherited image healthchecks no longer block All Clear; they surface under a notes section and cannot be acknowledged.
This commit is contained in:
@@ -11,10 +11,15 @@ import type { EffService, EffectiveModel } from '../services/preflight/effective
|
||||
import type { PreflightContext, PreflightFinding } from '../services/preflight/types';
|
||||
|
||||
function svc(over: Partial<EffService> = {}): EffService {
|
||||
const hasHealthcheck = over.hasHealthcheck ?? true;
|
||||
const composeHealthcheck = over.composeHealthcheck ?? (hasHealthcheck ? 'active' : 'absent');
|
||||
return {
|
||||
name: 'web', image: 'nginx:1.27', ports: [], binds: [], namedVolumes: [], storageMounts: [],
|
||||
privileged: false, hasHealthcheck: true, restart: 'unless-stopped', envKeys: [],
|
||||
networks: [], extraHosts: [], labelKeys: [], ...over,
|
||||
privileged: false, restart: 'unless-stopped', envKeys: [],
|
||||
networks: [], extraHosts: [], labelKeys: [],
|
||||
...over,
|
||||
hasHealthcheck,
|
||||
composeHealthcheck,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -33,7 +38,9 @@ function ctx(over: Partial<PreflightContext> = {}): PreflightContext {
|
||||
existingContainers: [], nodeStateAvailable: true, bindChecks: [],
|
||||
stackIntent: null, serviceIntents: {}, accessUrlPorts: new Set(), hasAccessUrls: false,
|
||||
exposureAvailable: true,
|
||||
isSelfStack: false, ...over,
|
||||
isSelfStack: false,
|
||||
healthchecks: {},
|
||||
...over,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -217,10 +224,56 @@ describe('hygiene rules', () => {
|
||||
expect(restartFindings[0].remediation).toMatch(/one-shot|init jobs/i);
|
||||
expect(restartFindings[0].remediation).toMatch(/restart: "no"/);
|
||||
expect(restartFindings[0].remediation).toMatch(/unless-stopped/);
|
||||
expect(ids(runRules(ctx({ model: bare })), 'no-healthcheck')).toHaveLength(1);
|
||||
expect(ids(runRules(ctx({
|
||||
model: bare,
|
||||
healthchecks: { web: { state: 'absent', origin: 'local-image', consistentReplicas: null } },
|
||||
})), 'no-healthcheck')).toHaveLength(1);
|
||||
const withDeployRestart = model([svc({ restart: undefined, deploy: { restart_policy: { condition: 'any' } }})]);
|
||||
expect(ids(runRules(ctx({ model: withDeployRestart })), 'no-restart-policy')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('emits the healthcheck evidence rule family', () => {
|
||||
const bare = model([svc({ hasHealthcheck: false })]);
|
||||
expect(ids(runRules(ctx({
|
||||
model: bare,
|
||||
healthchecks: { web: { state: 'explicitly-disabled', origin: 'compose', consistentReplicas: null } },
|
||||
})), 'healthcheck-disabled')).toHaveLength(1);
|
||||
expect(ids(runRules(ctx({
|
||||
model: bare,
|
||||
healthchecks: { web: { state: 'runtime-inherited', origin: 'runtime', consistentReplicas: true } },
|
||||
})), 'healthcheck-inherited')[0]).toMatchObject({
|
||||
severity: 'info',
|
||||
title: 'Healthcheck inherited from image',
|
||||
});
|
||||
expect(ids(runRules(ctx({
|
||||
model: bare,
|
||||
healthchecks: { web: { state: 'local-image-inherited', origin: 'local-image', consistentReplicas: null } },
|
||||
})), 'healthcheck-inherited')[0].remediation).toMatch(/Optionally declare/);
|
||||
expect(ids(runRules(ctx({
|
||||
model: bare,
|
||||
healthchecks: { web: { state: 'unverifiable', origin: 'none', consistentReplicas: null } },
|
||||
})), 'healthcheck-unverifiable')[0].severity).toBe('info');
|
||||
expect(ids(runRules(ctx({
|
||||
model: bare,
|
||||
healthchecks: { web: { state: 'inconsistent-replicas', origin: 'runtime', consistentReplicas: false } },
|
||||
})), 'healthcheck-inconsistent')).toHaveLength(1);
|
||||
expect(ids(runRules(ctx({
|
||||
model: bare,
|
||||
healthchecks: { web: { state: 'compose-declared', origin: 'compose', consistentReplicas: null } },
|
||||
})), 'no-healthcheck')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('never embeds healthcheck Test command text in findings', () => {
|
||||
const bare = model([svc({ hasHealthcheck: false })]);
|
||||
const findings = runRules(ctx({
|
||||
model: bare,
|
||||
healthchecks: { web: { state: 'runtime-inherited', origin: 'runtime', consistentReplicas: true } },
|
||||
}));
|
||||
const blob = findings.map(f => `${f.title}\n${f.message}\n${f.remediation ?? ''}`).join('\n');
|
||||
expect(blob).not.toMatch(/\bCMD\b/);
|
||||
expect(blob).not.toMatch(/CMD-SHELL/);
|
||||
expect(blob).not.toContain('secret-token');
|
||||
});
|
||||
it('flags swarm-only deploy fields but not honored ones', () => {
|
||||
expect(ids(runRules(ctx({ model: model([svc({ deploy: { placement: {} }})]) })), 'deploy-swarm-only')).toHaveLength(1);
|
||||
expect(ids(runRules(ctx({ model: model([svc({ deploy: { replicas: 3 }})]) })), 'deploy-swarm-only')).toHaveLength(0);
|
||||
@@ -491,7 +544,9 @@ describe('rule registry completeness', () => {
|
||||
const EXPECTED_RULE_IDS = [
|
||||
'render-failed', 'env-unset', 'env-literal-dollar', 'env-file-missing', 'port-conflict-node', 'port-conflict-internal', 'port-exposed-all-interfaces',
|
||||
'bind-path-missing', 'bind-path-permission', 'docker-socket-mount', 'privileged', 'network-mode-host',
|
||||
'uid-gid-risk', 'image-latest', 'no-restart-policy', 'no-healthcheck', 'deploy-swarm-only',
|
||||
'uid-gid-risk', 'image-latest', 'no-restart-policy', 'no-healthcheck',
|
||||
'healthcheck-disabled', 'healthcheck-inherited', 'healthcheck-unverifiable', 'healthcheck-inconsistent',
|
||||
'deploy-swarm-only',
|
||||
'node-state-unavailable',
|
||||
'external-network-missing', 'external-volume-missing', 'new-network', 'new-volume', 'anonymous-volume',
|
||||
'container-name-internal-dup', 'container-name-collision',
|
||||
|
||||
Reference in New Issue
Block a user