357a68bb95
Refs #49 Merged by commander after reviewing Code Queue task codex_1779422762669_1. Adds redacted DB readiness layering and refreshes DEV endpoint/report evidence. Follow-up: deploy/verify the new readiness probe in DEV via standard image CI/CD.
415 lines
12 KiB
JavaScript
415 lines
12 KiB
JavaScript
import net from "node:net";
|
|
|
|
import { ENVIRONMENT_DEV } from "../protocol/index.mjs";
|
|
|
|
export const DEV_DB_ENV_CONTRACT = Object.freeze({
|
|
contractVersion: "v1",
|
|
environment: ENVIRONMENT_DEV,
|
|
requiredEnv: Object.freeze([
|
|
"HWLAB_CLOUD_DB_URL",
|
|
"HWLAB_CLOUD_DB_SSL_MODE"
|
|
]),
|
|
secretRefs: Object.freeze([
|
|
Object.freeze({
|
|
env: "HWLAB_CLOUD_DB_URL",
|
|
secretName: "hwlab-cloud-api-dev-db",
|
|
secretKey: "database-url"
|
|
})
|
|
]),
|
|
nonSecretDefaults: Object.freeze({
|
|
HWLAB_CLOUD_DB_SSL_MODE: "require"
|
|
}),
|
|
forbidden: Object.freeze({
|
|
prodAllowed: false,
|
|
secretPlaintextAllowed: false,
|
|
liveDbEvidenceFromFixtures: false
|
|
})
|
|
});
|
|
|
|
const secretEnvNames = new Set(DEV_DB_ENV_CONTRACT.secretRefs.map((item) => item.env));
|
|
const supportedDbProtocols = new Set(["postgres:", "postgresql:"]);
|
|
const defaultProbeTimeoutMs = 1200;
|
|
|
|
export function buildDbHealthContract(env = process.env) {
|
|
const fields = DEV_DB_ENV_CONTRACT.requiredEnv.map((name) => {
|
|
const present = hasEnvValue(env, name);
|
|
return {
|
|
name,
|
|
present,
|
|
redacted: secretEnvNames.has(name),
|
|
source: secretEnvNames.has(name) ? "k8s-secret-ref" : "runtime-env",
|
|
required: true
|
|
};
|
|
});
|
|
const missingEnv = fields.filter((field) => !field.present).map((field) => field.name);
|
|
const configReady = missingEnv.length === 0;
|
|
const connection = buildNotAttemptedConnection(configReady, missingEnv);
|
|
const connected = false;
|
|
const connectionChecked = false;
|
|
const status = configReady ? "degraded" : "blocked";
|
|
|
|
return {
|
|
contractVersion: DEV_DB_ENV_CONTRACT.contractVersion,
|
|
environment: DEV_DB_ENV_CONTRACT.environment,
|
|
connected,
|
|
liveConnected: connected,
|
|
connectionChecked,
|
|
connectionAttempted: connection.attempted,
|
|
connectionResult: connection.result,
|
|
configReady,
|
|
ready: connected,
|
|
status,
|
|
mode: configReady ? "configured_without_live_connection_attempt" : "not_configured",
|
|
fields,
|
|
missingEnv,
|
|
secretRefs: DEV_DB_ENV_CONTRACT.secretRefs.map(({ env: envName, secretName, secretKey }) => ({
|
|
env: envName,
|
|
secretName,
|
|
secretKey,
|
|
present: hasEnvValue(env, envName),
|
|
envInjected: hasEnvValue(env, envName),
|
|
secretPresent: "not_observed_by_runtime",
|
|
secretKeyPresent: "not_observed_by_runtime",
|
|
redacted: true
|
|
})),
|
|
connection,
|
|
safety: {
|
|
devOnly: true,
|
|
prodAllowed: DEV_DB_ENV_CONTRACT.forbidden.prodAllowed,
|
|
secretsRead: false,
|
|
valuesRedacted: true,
|
|
liveDbEvidence: false
|
|
},
|
|
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"
|
|
};
|
|
}
|
|
|
|
export async function buildDbRuntimeReadiness(env = process.env, options = {}) {
|
|
const base = buildDbHealthContract(env);
|
|
if (!base.configReady) {
|
|
return base;
|
|
}
|
|
|
|
if (probeDisabled(env, options)) {
|
|
return {
|
|
...base,
|
|
blocker: "live DB probe is disabled for this runtime",
|
|
evidence: "env_presence_only_probe_disabled"
|
|
};
|
|
}
|
|
|
|
const connection = await probeDbConnection(env, options);
|
|
return applyConnectionResult(base, connection);
|
|
}
|
|
|
|
export function buildDbEnvManifestPlaceholder() {
|
|
return {
|
|
requiredEnv: [...DEV_DB_ENV_CONTRACT.requiredEnv],
|
|
secretRefs: DEV_DB_ENV_CONTRACT.secretRefs.map((item) => ({ ...item })),
|
|
nonSecretDefaults: { ...DEV_DB_ENV_CONTRACT.nonSecretDefaults },
|
|
fixtureEvidence: {
|
|
liveDbEvidence: false,
|
|
summary: "This manifest contract records only env and Secret reference names. It is not live DB evidence."
|
|
}
|
|
};
|
|
}
|
|
|
|
export function summarizeDbContract(db = buildDbHealthContract()) {
|
|
return {
|
|
status: db.status,
|
|
ready: db.ready,
|
|
configReady: db.configReady,
|
|
connected: db.connected,
|
|
liveConnected: db.liveConnected ?? db.connected,
|
|
connectionChecked: db.connectionChecked,
|
|
connectionAttempted: db.connectionAttempted ?? db.connectionChecked,
|
|
connectionResult: db.connectionResult ?? db.connection?.result ?? "unknown",
|
|
requiredEnv: db.fields.map((field) => ({
|
|
name: field.name,
|
|
present: field.present,
|
|
redacted: field.redacted,
|
|
source: field.source
|
|
})),
|
|
missingEnv: [...db.missingEnv],
|
|
secretRefs: db.secretRefs.map(
|
|
({
|
|
env: envName,
|
|
secretName,
|
|
secretKey,
|
|
present,
|
|
envInjected,
|
|
secretPresent,
|
|
secretKeyPresent,
|
|
redacted
|
|
}) => ({
|
|
env: envName,
|
|
secretName,
|
|
secretKey,
|
|
present,
|
|
envInjected: envInjected ?? present,
|
|
secretPresent: secretPresent ?? "not_observed",
|
|
secretKeyPresent: secretKeyPresent ?? "not_observed",
|
|
redacted
|
|
})
|
|
),
|
|
connection: summarizeConnection(db.connection),
|
|
blocker: db.blocker ?? null,
|
|
liveDbEvidence: db.safety.liveDbEvidence,
|
|
fixtureEvidence: false
|
|
};
|
|
}
|
|
|
|
function hasEnvValue(env, name) {
|
|
return typeof env?.[name] === "string" && env[name].trim().length > 0;
|
|
}
|
|
|
|
function probeDisabled(env, options) {
|
|
return options.probe === false || env?.HWLAB_CLOUD_DB_PROBE_DISABLED === "1";
|
|
}
|
|
|
|
function buildNotAttemptedConnection(configReady, missingEnv) {
|
|
return {
|
|
attempted: false,
|
|
networkAttempted: false,
|
|
result: configReady ? "not_attempted" : "not_attempted_missing_env",
|
|
classification: configReady ? "configured_no_live_attempt" : "missing_runtime_env",
|
|
probeType: "tcp-connect",
|
|
endpointRedacted: true,
|
|
valueRedacted: true,
|
|
timeoutMs: null,
|
|
durationMs: 0,
|
|
errorCode: null,
|
|
missingEnv: [...missingEnv]
|
|
};
|
|
}
|
|
|
|
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);
|
|
if (!parsed.ok) {
|
|
return {
|
|
attempted: true,
|
|
networkAttempted: false,
|
|
result: parsed.result,
|
|
classification: parsed.classification,
|
|
probeType: "tcp-connect",
|
|
endpointRedacted: true,
|
|
valueRedacted: true,
|
|
timeoutMs,
|
|
durationMs: 0,
|
|
errorCode: parsed.errorCode,
|
|
missingEnv: []
|
|
};
|
|
}
|
|
|
|
return tcpConnect({
|
|
host: parsed.host,
|
|
port: parsed.port,
|
|
timeoutMs
|
|
});
|
|
}
|
|
|
|
function parseDbTarget(rawUrl) {
|
|
try {
|
|
const url = new URL(rawUrl);
|
|
if (!supportedDbProtocols.has(url.protocol)) {
|
|
return {
|
|
ok: false,
|
|
result: "unsupported_protocol",
|
|
classification: "db_url_unsupported_protocol",
|
|
errorCode: "UNSUPPORTED_PROTOCOL"
|
|
};
|
|
}
|
|
if (!url.hostname) {
|
|
return {
|
|
ok: false,
|
|
result: "invalid_url",
|
|
classification: "db_url_missing_host",
|
|
errorCode: "MISSING_HOST"
|
|
};
|
|
}
|
|
const port = url.port ? Number.parseInt(url.port, 10) : 5432;
|
|
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
return {
|
|
ok: false,
|
|
result: "invalid_url",
|
|
classification: "db_url_invalid_port",
|
|
errorCode: "INVALID_PORT"
|
|
};
|
|
}
|
|
return {
|
|
ok: true,
|
|
host: url.hostname,
|
|
port
|
|
};
|
|
} catch {
|
|
return {
|
|
ok: false,
|
|
result: "invalid_url",
|
|
classification: "db_url_parse_error",
|
|
errorCode: "INVALID_URL"
|
|
};
|
|
}
|
|
}
|
|
|
|
function tcpConnect({ host, port, timeoutMs }) {
|
|
return new Promise((resolve) => {
|
|
const startedAt = Date.now();
|
|
const socket = net.connect({ host, port, timeout: timeoutMs });
|
|
let settled = false;
|
|
|
|
function finish(result) {
|
|
if (settled) return;
|
|
settled = true;
|
|
socket.destroy();
|
|
resolve({
|
|
attempted: true,
|
|
networkAttempted: true,
|
|
probeType: "tcp-connect",
|
|
endpointRedacted: true,
|
|
valueRedacted: true,
|
|
timeoutMs,
|
|
durationMs: Date.now() - startedAt,
|
|
missingEnv: [],
|
|
...result
|
|
});
|
|
}
|
|
|
|
socket.on("connect", () => {
|
|
finish({
|
|
result: "connected",
|
|
classification: "tcp_connected",
|
|
errorCode: null
|
|
});
|
|
});
|
|
socket.on("timeout", () => {
|
|
finish({
|
|
result: "timeout",
|
|
classification: "tcp_timeout",
|
|
errorCode: "ETIMEDOUT"
|
|
});
|
|
});
|
|
socket.on("error", (error) => {
|
|
finish(classifySocketError(error));
|
|
});
|
|
});
|
|
}
|
|
|
|
function classifySocketError(error) {
|
|
const code = typeof error?.code === "string" ? error.code : "UNKNOWN";
|
|
if (code === "ECONNREFUSED") {
|
|
return {
|
|
result: "refused",
|
|
classification: "tcp_refused",
|
|
errorCode: code
|
|
};
|
|
}
|
|
if (code === "ENOTFOUND" || code === "EAI_AGAIN") {
|
|
return {
|
|
result: "dns_error",
|
|
classification: "dns_resolution_failed",
|
|
errorCode: code
|
|
};
|
|
}
|
|
if (code === "ETIMEDOUT") {
|
|
return {
|
|
result: "timeout",
|
|
classification: "tcp_timeout",
|
|
errorCode: code
|
|
};
|
|
}
|
|
if (code === "EHOSTUNREACH" || code === "ENETUNREACH") {
|
|
return {
|
|
result: "network_unreachable",
|
|
classification: "network_unreachable",
|
|
errorCode: code
|
|
};
|
|
}
|
|
return {
|
|
result: "error",
|
|
classification: "tcp_error",
|
|
errorCode: code
|
|
};
|
|
}
|
|
|
|
function applyConnectionResult(base, connection) {
|
|
const liveConnected = connection.result === "connected";
|
|
return {
|
|
...base,
|
|
connected: liveConnected,
|
|
liveConnected,
|
|
connectionChecked: connection.attempted,
|
|
connectionAttempted: connection.attempted,
|
|
connectionResult: connection.result,
|
|
ready: liveConnected,
|
|
status: liveConnected ? "ready" : "degraded",
|
|
mode: liveConnected ? "live_connection_ready" : "live_connection_blocked",
|
|
connection,
|
|
safety: {
|
|
...base.safety,
|
|
liveDbEvidence: connection.attempted,
|
|
liveDbConnectedEvidence: liveConnected
|
|
},
|
|
blocker: liveConnected ? null : blockerForConnection(connection),
|
|
evidence: liveConnected ? "live_db_tcp_connection_ready" : "live_db_tcp_connection_blocked"
|
|
};
|
|
}
|
|
|
|
function blockerForConnection(connection) {
|
|
if (connection.result === "invalid_url") {
|
|
return "DB URL is injected but cannot be parsed as a supported postgres URL";
|
|
}
|
|
if (connection.result === "unsupported_protocol") {
|
|
return "DB URL is injected but does not use a supported postgres protocol";
|
|
}
|
|
if (connection.result === "refused") {
|
|
return "DB endpoint refused a TCP connection from the cloud-api runtime";
|
|
}
|
|
if (connection.result === "timeout") {
|
|
return "DB endpoint TCP connection timed out from the cloud-api runtime";
|
|
}
|
|
if (connection.result === "dns_error") {
|
|
return "DB endpoint DNS resolution failed from the cloud-api runtime";
|
|
}
|
|
if (connection.result === "network_unreachable") {
|
|
return "DB endpoint network is unreachable from the cloud-api runtime";
|
|
}
|
|
return "DB endpoint TCP connection did not complete from the cloud-api runtime";
|
|
}
|
|
|
|
function missingEnvBlocker(missingEnv) {
|
|
return `DB runtime config is missing ${missingEnv.join(", ")}`;
|
|
}
|
|
|
|
function normalizeTimeoutMs(value) {
|
|
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
return defaultProbeTimeoutMs;
|
|
}
|
|
return Math.min(parsed, 5000);
|
|
}
|
|
|
|
function summarizeConnection(connection) {
|
|
if (!connection) {
|
|
return {
|
|
attempted: false,
|
|
result: "unknown",
|
|
endpointRedacted: true,
|
|
valueRedacted: true
|
|
};
|
|
}
|
|
return {
|
|
attempted: Boolean(connection.attempted),
|
|
networkAttempted: Boolean(connection.networkAttempted),
|
|
result: connection.result,
|
|
classification: connection.classification,
|
|
probeType: connection.probeType,
|
|
endpointRedacted: true,
|
|
valueRedacted: true,
|
|
timeoutMs: connection.timeoutMs,
|
|
durationMs: connection.durationMs,
|
|
errorCode: connection.errorCode ?? null
|
|
};
|
|
}
|