fix: tighten DB live readiness contract
This commit is contained in:
@@ -17,8 +17,9 @@ and verifies:
|
||||
- `GET /health` reports `status: "degraded"` when live DB persistence is not
|
||||
connected.
|
||||
- The DB contract reports required env names, redacted Secret refs, env
|
||||
injection, connection attempt status, connection result, and `liveConnected`
|
||||
without exposing DB URL or secret values.
|
||||
injection, connection attempt status, connection result, `liveConnected`,
|
||||
`liveDbEvidence`, and redaction/safety fields without exposing DB URL or
|
||||
secret values.
|
||||
- `gateway.session.register`, `box.resource.register`,
|
||||
`box.capability.report`, `hardware.invoke.shell`, `audit.event.query`, and
|
||||
`evidence.record.query` round-trip through the same runtime store.
|
||||
@@ -45,10 +46,15 @@ keeps all endpoint details redacted and reports only:
|
||||
- `db.connectionResult`: redacted classifier such as `connected`, `refused`,
|
||||
`timeout`, `dns_error`, `invalid_url`, or `unsupported_protocol`.
|
||||
- `db.liveConnected`: the TCP connection completed.
|
||||
- `db.liveDbEvidence`: true only when the live connection completed; env
|
||||
presence, fixtures, and disabled probes must keep this false.
|
||||
- `db.redaction` and `db.safety`: confirm values/endpoints are redacted and
|
||||
secret material was not read by the health path.
|
||||
- `db.ready`: currently aliases `db.liveConnected`.
|
||||
|
||||
Env presence alone is not treated as green. If the runtime has env injection
|
||||
but `db.connectionAttempted` is false or `db.liveConnected` is false, the gate
|
||||
must stay blocked/degraded with the reported blocker.
|
||||
but `db.connectionAttempted` is false, `db.liveConnected` is false, or
|
||||
`db.liveDbEvidence` is false, the gate must stay blocked/degraded with the
|
||||
reported blocker.
|
||||
|
||||
Fixture: `fixtures/cloud-api-runtime/minimal-runtime.json`.
|
||||
|
||||
@@ -53,6 +53,7 @@ export function buildDbHealthContract(env = process.env) {
|
||||
environment: DEV_DB_ENV_CONTRACT.environment,
|
||||
connected,
|
||||
liveConnected: connected,
|
||||
liveDbEvidence: connected,
|
||||
connectionChecked,
|
||||
connectionAttempted: connection.attempted,
|
||||
connectionResult: connection.result,
|
||||
@@ -73,12 +74,16 @@ export function buildDbHealthContract(env = process.env) {
|
||||
redacted: true
|
||||
})),
|
||||
connection,
|
||||
redaction: buildRedactionSafety(connection),
|
||||
safety: {
|
||||
devOnly: true,
|
||||
prodAllowed: DEV_DB_ENV_CONTRACT.forbidden.prodAllowed,
|
||||
secretsRead: false,
|
||||
secretMaterialRead: false,
|
||||
valuesRedacted: true,
|
||||
liveDbEvidence: false
|
||||
endpointRedacted: true,
|
||||
liveDbEvidence: connected,
|
||||
liveDbConnectedEvidence: connected
|
||||
},
|
||||
blocker: configReady ? "live DB connection has not been attempted by this health contract" : missingEnvBlocker(missingEnv),
|
||||
evidence: configReady ? "env_presence_only_no_live_db" : "env_contract_blocked"
|
||||
@@ -125,6 +130,7 @@ export function summarizeDbContract(db = buildDbHealthContract()) {
|
||||
connectionChecked: db.connectionChecked,
|
||||
connectionAttempted: db.connectionAttempted ?? db.connectionChecked,
|
||||
connectionResult: db.connectionResult ?? db.connection?.result ?? "unknown",
|
||||
liveDbEvidence: Boolean(db.liveDbEvidence ?? db.safety?.liveDbEvidence),
|
||||
requiredEnv: db.fields.map((field) => ({
|
||||
name: field.name,
|
||||
present: field.present,
|
||||
@@ -154,8 +160,8 @@ export function summarizeDbContract(db = buildDbHealthContract()) {
|
||||
})
|
||||
),
|
||||
connection: summarizeConnection(db.connection),
|
||||
redaction: summarizeRedaction(db.redaction),
|
||||
blocker: db.blocker ?? null,
|
||||
liveDbEvidence: db.safety.liveDbEvidence,
|
||||
fixtureEvidence: false
|
||||
};
|
||||
}
|
||||
@@ -184,6 +190,16 @@ function buildNotAttemptedConnection(configReady, missingEnv) {
|
||||
};
|
||||
}
|
||||
|
||||
function buildRedactionSafety(connection) {
|
||||
return {
|
||||
valuesRedacted: true,
|
||||
endpointRedacted: connection?.endpointRedacted !== false,
|
||||
valueRedacted: connection?.valueRedacted !== false,
|
||||
secretMaterialRead: false,
|
||||
secretRefsOnly: true
|
||||
};
|
||||
}
|
||||
|
||||
async function probeDbConnection(env, options) {
|
||||
const timeoutMs = normalizeTimeoutMs(options.timeoutMs ?? env?.HWLAB_CLOUD_DB_PROBE_TIMEOUT_MS);
|
||||
const parsed = parseDbTarget(env?.HWLAB_CLOUD_DB_URL);
|
||||
@@ -339,6 +355,7 @@ function applyConnectionResult(base, connection) {
|
||||
...base,
|
||||
connected: liveConnected,
|
||||
liveConnected,
|
||||
liveDbEvidence: liveConnected,
|
||||
connectionChecked: connection.attempted,
|
||||
connectionAttempted: connection.attempted,
|
||||
connectionResult: connection.result,
|
||||
@@ -346,9 +363,11 @@ function applyConnectionResult(base, connection) {
|
||||
status: liveConnected ? "ready" : "degraded",
|
||||
mode: liveConnected ? "live_connection_ready" : "live_connection_blocked",
|
||||
connection,
|
||||
redaction: buildRedactionSafety(connection),
|
||||
safety: {
|
||||
...base.safety,
|
||||
liveDbEvidence: connection.attempted,
|
||||
endpointRedacted: connection.endpointRedacted !== false,
|
||||
liveDbEvidence: liveConnected,
|
||||
liveDbConnectedEvidence: liveConnected
|
||||
},
|
||||
blocker: liveConnected ? null : blockerForConnection(connection),
|
||||
@@ -412,3 +431,13 @@ function summarizeConnection(connection) {
|
||||
errorCode: connection.errorCode ?? null
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeRedaction(redaction) {
|
||||
return {
|
||||
valuesRedacted: redaction?.valuesRedacted !== false,
|
||||
endpointRedacted: redaction?.endpointRedacted !== false,
|
||||
valueRedacted: redaction?.valueRedacted !== false,
|
||||
secretMaterialRead: redaction?.secretMaterialRead === true,
|
||||
secretRefsOnly: redaction?.secretRefsOnly !== false
|
||||
};
|
||||
}
|
||||
|
||||
@@ -60,6 +60,13 @@ test("system.health includes the redacted DEV DB env contract", async () => {
|
||||
assert.equal(response.result.status, "degraded");
|
||||
assert.equal(response.result.db.status, "blocked");
|
||||
assert.equal(response.result.db.connected, false);
|
||||
assert.equal(response.result.db.liveConnected, false);
|
||||
assert.equal(response.result.db.liveDbEvidence, false);
|
||||
assert.equal(response.result.db.connectionAttempted, false);
|
||||
assert.equal(response.result.db.connectionResult, "not_attempted_missing_env");
|
||||
assert.equal(response.result.db.redaction.valuesRedacted, true);
|
||||
assert.equal(response.result.db.redaction.secretMaterialRead, false);
|
||||
assert.equal(response.result.db.safety.liveDbEvidence, false);
|
||||
assert.equal(response.result.runtime.durable, false);
|
||||
assert.ok(response.result.db.missingEnv.includes("HWLAB_CLOUD_DB_URL"));
|
||||
assert.equal(response.result.db.secretRefs[0].secretName, "hwlab-cloud-api-dev-db");
|
||||
|
||||
@@ -28,8 +28,12 @@ test("cloud api exposes /health, /health/live, and /live probes", async () => {
|
||||
assert.equal(healthPayload.db.status, "blocked");
|
||||
assert.equal(healthPayload.db.connected, false);
|
||||
assert.equal(healthPayload.db.liveConnected, false);
|
||||
assert.equal(healthPayload.db.liveDbEvidence, false);
|
||||
assert.equal(healthPayload.db.connectionAttempted, false);
|
||||
assert.equal(healthPayload.db.connectionResult, "not_attempted_missing_env");
|
||||
assert.equal(healthPayload.db.redaction.valuesRedacted, true);
|
||||
assert.equal(healthPayload.db.redaction.secretMaterialRead, false);
|
||||
assert.equal(healthPayload.db.safety.liveDbEvidence, false);
|
||||
assert.equal(healthPayload.runtime.durable, false);
|
||||
assert.equal(healthPayload.runtime.status, "degraded");
|
||||
assert.deepEqual(healthPayload.db.missingEnv, ["HWLAB_CLOUD_DB_URL", "HWLAB_CLOUD_DB_SSL_MODE"]);
|
||||
@@ -88,12 +92,18 @@ test("cloud api health reports DB env presence and live connection classificatio
|
||||
assert.equal(payload.db.status, "ready");
|
||||
assert.equal(payload.db.connected, true);
|
||||
assert.equal(payload.db.liveConnected, true);
|
||||
assert.equal(payload.db.liveDbEvidence, true);
|
||||
assert.equal(payload.db.connectionChecked, true);
|
||||
assert.equal(payload.db.connectionAttempted, true);
|
||||
assert.equal(payload.db.connectionResult, "connected");
|
||||
assert.equal(payload.db.configReady, true);
|
||||
assert.equal(payload.db.ready, true);
|
||||
assert.equal(payload.db.evidence, "live_db_tcp_connection_ready");
|
||||
assert.equal(payload.db.safety.liveDbEvidence, true);
|
||||
assert.equal(payload.db.safety.liveDbConnectedEvidence, true);
|
||||
assert.equal(payload.db.redaction.valuesRedacted, true);
|
||||
assert.equal(payload.db.redaction.endpointRedacted, true);
|
||||
assert.equal(payload.db.redaction.secretMaterialRead, false);
|
||||
assert.deepEqual(payload.db.missingEnv, []);
|
||||
assert.equal(JSON.stringify(payload.db).includes("password"), false);
|
||||
assert.equal(JSON.stringify(payload.db).includes("127.0.0.1"), false);
|
||||
@@ -119,3 +129,52 @@ test("cloud api health reports DB env presence and live connection classificatio
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test("cloud api health does not treat DB env presence-only as live readiness", async () => {
|
||||
const originalUrl = process.env.HWLAB_CLOUD_DB_URL;
|
||||
const originalSslMode = process.env.HWLAB_CLOUD_DB_SSL_MODE;
|
||||
const originalProbeDisabled = process.env.HWLAB_CLOUD_DB_PROBE_DISABLED;
|
||||
process.env.HWLAB_CLOUD_DB_URL = "postgres://user:password@db.example.invalid:5432/hwlab";
|
||||
process.env.HWLAB_CLOUD_DB_SSL_MODE = "require";
|
||||
process.env.HWLAB_CLOUD_DB_PROBE_DISABLED = "1";
|
||||
|
||||
const server = createCloudApiServer();
|
||||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
|
||||
try {
|
||||
const { port } = server.address();
|
||||
const response = await fetch(`http://127.0.0.1:${port}/health/live`);
|
||||
const payload = await response.json();
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(payload.db.configReady, true);
|
||||
assert.equal(payload.db.connected, false);
|
||||
assert.equal(payload.db.liveConnected, false);
|
||||
assert.equal(payload.db.liveDbEvidence, false);
|
||||
assert.equal(payload.db.safety.liveDbEvidence, false);
|
||||
assert.equal(payload.db.ready, false);
|
||||
assert.equal(payload.db.connectionAttempted, false);
|
||||
assert.equal(payload.db.connectionResult, "not_attempted");
|
||||
assert.equal(payload.db.evidence, "env_presence_only_probe_disabled");
|
||||
assert.equal(JSON.stringify(payload.db).includes("password"), false);
|
||||
assert.equal(JSON.stringify(payload.db).includes("db.example.invalid"), false);
|
||||
} finally {
|
||||
if (originalUrl === undefined) {
|
||||
delete process.env.HWLAB_CLOUD_DB_URL;
|
||||
} else {
|
||||
process.env.HWLAB_CLOUD_DB_URL = originalUrl;
|
||||
}
|
||||
if (originalSslMode === undefined) {
|
||||
delete process.env.HWLAB_CLOUD_DB_SSL_MODE;
|
||||
} else {
|
||||
process.env.HWLAB_CLOUD_DB_SSL_MODE = originalSslMode;
|
||||
}
|
||||
if (originalProbeDisabled === undefined) {
|
||||
delete process.env.HWLAB_CLOUD_DB_PROBE_DISABLED;
|
||||
} else {
|
||||
process.env.HWLAB_CLOUD_DB_PROBE_DISABLED = originalProbeDisabled;
|
||||
}
|
||||
await new Promise((resolve, reject) => {
|
||||
server.close((error) => (error ? reject(error) : resolve()));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -87,6 +87,7 @@ async function run() {
|
||||
assert.equal(health.body.db.status, "blocked");
|
||||
assert.equal(health.body.db.connected, false);
|
||||
assert.equal(health.body.db.liveConnected, false);
|
||||
assert.equal(health.body.db.liveDbEvidence, false);
|
||||
assert.equal(health.body.db.connectionAttempted, false);
|
||||
assert.equal(health.body.db.connectionResult, "not_attempted_missing_env");
|
||||
assert.equal(health.body.db.ready, false);
|
||||
|
||||
@@ -154,6 +154,7 @@ async function smokeCloudApi() {
|
||||
assert.equal(health.body.status, "degraded");
|
||||
assert.equal(health.body.db.connected, false);
|
||||
assert.equal(health.body.db.liveConnected, false);
|
||||
assert.equal(health.body.db.liveDbEvidence, false);
|
||||
assert.equal(health.body.db.connectionAttempted, false);
|
||||
assert.equal(health.body.db.connectionResult, "not_attempted_missing_env");
|
||||
assert.equal(health.body.db.status, "blocked");
|
||||
|
||||
@@ -394,9 +394,9 @@ function summarizeRuntimeDbReadiness(db, secretPresence = unknownDbSecretPresenc
|
||||
connectionAttempted,
|
||||
connectionResult,
|
||||
liveConnected,
|
||||
valuesRedacted: db.safety?.valuesRedacted === true,
|
||||
valuesRedacted: db.redaction?.valuesRedacted !== false && db.safety?.valuesRedacted === true,
|
||||
secretMaterialRead: false,
|
||||
liveDbEvidence: Boolean(db.safety?.liveDbEvidence),
|
||||
liveDbEvidence: Boolean(db.liveDbEvidence ?? db.safety?.liveDbEvidence),
|
||||
blocker: blockerMessage,
|
||||
evidence: [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user