fix: honor Komodo health and prefer fresh telemetry across fleet status
This commit is contained in:
@@ -257,85 +257,62 @@ export type KomodoHealthResult =
|
||||
| { ok: true; state: string }
|
||||
| { ok: false; reason: string; state?: string; snippet?: string };
|
||||
|
||||
// Shape-agnostic scan for the container's running-state. Walks the entire
|
||||
// payload looking for anything that resembles a state field. Covered shapes:
|
||||
// - Docker inspect: { State: { Status: "running", Running: true, Health: { Status: "healthy" } } }
|
||||
// - Komodo GetDeployment: top-level or nested `state`, `Status`, `container_state`
|
||||
// - Komodo GetDeploymentStatus: { state: "Running" }
|
||||
// - Anything with `Running: true` at any depth
|
||||
// When it can't find any of these, callers get the raw JSON in the error so
|
||||
// we can spot which field to add here.
|
||||
const STATE_KEYS = new Set([
|
||||
'state',
|
||||
'State',
|
||||
'status',
|
||||
'Status',
|
||||
'container_state',
|
||||
'containerState',
|
||||
'deployment_state',
|
||||
'deploymentState',
|
||||
'docker_status'
|
||||
]);
|
||||
|
||||
// Komodo's container summary has { state: "running", container: { status:
|
||||
// "Up 35 seconds (health: starting)" } }. Inspect responses instead have
|
||||
// State.Health.Status (or snake/lowercase equivalents). Runtime health wins
|
||||
// over a coarse running flag. Unknown/configuration strings are not readiness.
|
||||
export function extractContainerState(payload: unknown): string | null {
|
||||
const seen = new Set<unknown>();
|
||||
let found: string | null = null;
|
||||
|
||||
const walk = (v: unknown): void => {
|
||||
if (found || v === null || typeof v !== 'object' || seen.has(v)) return;
|
||||
seen.add(v);
|
||||
if (Array.isArray(v)) {
|
||||
for (const item of v) walk(item);
|
||||
return;
|
||||
const known = new Set(['running','healthy','starting','restarting','unhealthy','stopped','exited','created','dead','paused','removing','notdeployed','unknown']);
|
||||
const stateOf = (value: unknown): string | null => {
|
||||
if (typeof value !== 'string') return null;
|
||||
const state = value.toLowerCase().trim();
|
||||
if (known.has(state)) return state;
|
||||
if (/^up\b/.test(state)) {
|
||||
const health = state.match(/\((?:health:\s*)?(starting|unhealthy|healthy)\)/)?.[1];
|
||||
return health && health !== 'healthy' ? health : 'running';
|
||||
}
|
||||
const obj = v as Record<string, unknown>;
|
||||
|
||||
// Komodo summaries expose health alongside a flat running state.
|
||||
const flatState = obj['state'] ?? obj['State'] ?? obj['status'] ?? obj['Status'];
|
||||
const flatHealth = obj['health'] ?? obj['Health'];
|
||||
const healthStatus = typeof flatHealth === 'string' ? flatHealth
|
||||
: flatHealth && typeof flatHealth === 'object'
|
||||
? ((flatHealth as Record<string, unknown>)['status'] ?? (flatHealth as Record<string, unknown>)['Status']) : null;
|
||||
if (typeof flatState === 'string' && flatState.toLowerCase() === 'running'
|
||||
&& typeof healthStatus === 'string' && healthStatus.toLowerCase() !== 'healthy') {
|
||||
found = healthStatus;
|
||||
return;
|
||||
}
|
||||
|
||||
// Docker inspect nested: { State: { Status, Running, Health: { Status } } }
|
||||
const stateObj = obj['State'];
|
||||
if (stateObj && typeof stateObj === 'object') {
|
||||
const so = stateObj as Record<string, unknown>;
|
||||
const status = so['Status'];
|
||||
const running = so['Running'];
|
||||
const health = so['Health'];
|
||||
// A running process can still be starting or unhealthy. Honor an
|
||||
// explicit Docker health check before accepting its running state.
|
||||
if ((running === true || status === 'running') && health && typeof health === 'object') {
|
||||
const hs = (health as Record<string, unknown>)['Status'];
|
||||
if (typeof hs === 'string' && hs.toLowerCase() !== 'healthy') { found = hs; return; }
|
||||
}
|
||||
if (typeof status === 'string') { found = status; return; }
|
||||
if (running === true) { found = 'running'; return; }
|
||||
if (health && typeof health === 'object') {
|
||||
const hs = (health as Record<string, unknown>)['Status'];
|
||||
if (typeof hs === 'string') { found = hs; return; }
|
||||
}
|
||||
}
|
||||
|
||||
// Flat string-valued state under any of the recognized keys.
|
||||
for (const key of STATE_KEYS) {
|
||||
const val = obj[key];
|
||||
if (typeof val === 'string' && val.length > 0) {
|
||||
found = val;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (const val of Object.values(obj)) walk(val);
|
||||
if (/^exited\b/.test(state)) return 'exited';
|
||||
if (/^restarting\b/.test(state)) return 'restarting';
|
||||
return null;
|
||||
};
|
||||
walk(payload);
|
||||
return found;
|
||||
const walk = (value: unknown): string | null => {
|
||||
if (!value || typeof value !== 'object' || seen.has(value)) return null;
|
||||
seen.add(value);
|
||||
if (Array.isArray(value)) {
|
||||
const states = value.map(walk).filter((state): state is string => !!state);
|
||||
return states.find(state => !['running','healthy'].includes(state)) ?? states[0] ?? null;
|
||||
}
|
||||
const object = value as Record<string, unknown>;
|
||||
const rawState = object.State ?? object.state;
|
||||
const state = stateOf(rawState) ?? stateOf(object.container_state ?? object.containerState ?? object.deployment_state ?? object.deploymentState);
|
||||
// A stopped process is never ready just because its previous health check passed.
|
||||
if (state && !['running','healthy','unknown'].includes(state)) return state;
|
||||
if (rawState && typeof rawState === 'object') {
|
||||
const nested = walk(rawState);
|
||||
if (nested) return nested;
|
||||
}
|
||||
for (const key of ['container','Container','containers','Containers']) {
|
||||
if (object[key] === null && state === 'running') return 'unknown';
|
||||
const nested = walk(object[key]);
|
||||
if (nested) return nested;
|
||||
}
|
||||
const status = stateOf(object.Status ?? object.status ?? object.docker_status);
|
||||
if (status && !['running','healthy'].includes(status)) return status;
|
||||
const health = object.Health ?? object.health;
|
||||
const healthState = stateOf(typeof health === 'object' && health ?
|
||||
(health as Record<string, unknown>).Status ?? (health as Record<string, unknown>).status : health);
|
||||
if (healthState && healthState !== 'healthy') return healthState;
|
||||
if (state || status) return state ?? status;
|
||||
if (object.Running === true || object.running === true) return healthState ?? 'running';
|
||||
for (const [key, item] of Object.entries(object)) {
|
||||
if (['config','Config','labels','Labels'].includes(key)) continue;
|
||||
const nested = walk(item);
|
||||
if (nested) return nested;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
return walk(payload);
|
||||
}
|
||||
|
||||
const HEALTHY_STATES = new Set(['running', 'healthy', 'ok', 'started', 'up']);
|
||||
|
||||
Reference in New Issue
Block a user