feat: add dev db health contract

This commit is contained in:
HWLAB Code Queue
2026-05-21 17:53:36 +00:00
parent 36bf2c42ed
commit 1a2efd4915
27 changed files with 928 additions and 265 deletions
+76 -13
View File
@@ -6,6 +6,10 @@ import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { spawn } from "node:child_process";
import {
DEV_DB_ENV_CONTRACT,
summarizeDbContract
} from "../../internal/cloud/db-contract.mjs";
import { DEV_ENDPOINT, ENVIRONMENT_DEV, SERVICE_IDS } from "../../internal/protocol/index.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
@@ -207,7 +211,7 @@ function blockerHint(blocker) {
return "Restore the public DEV health path and confirm it identifies HWLAB dev, not a substitute runtime.";
}
if (blocker.scope === "dev-health-db" || blocker.scope === "cloud-api-db" || blocker.scope === "cloud-api-db-config") {
return "Configure and verify the DEV cloud-api DB connection through health output, without reading secret values.";
return "Configure and verify the DEV cloud-api DB env readiness through health output, without reading secret values.";
}
return blocker.summary;
}
@@ -411,29 +415,87 @@ function validateK8s(devKustomization, namespaceDoc, workloads, services, health
}
async function checkCloudApiDb(deploy, workloads, blockers) {
const envNames = new Set(Object.keys(deploy?.services?.find((service) => service.serviceId === "hwlab-cloud-api")?.env ?? {}));
const deployEnv = deploy?.services?.find((service) => service.serviceId === "hwlab-cloud-api")?.env ?? {};
const envNames = new Set(Object.keys(deployEnv));
const workloadEnv = new Map();
for (const item of listItems(workloads)) {
for (const container of containersFor(item)) {
if (serviceIdFor(item, container) === "hwlab-cloud-api") {
for (const env of container.env ?? []) envNames.add(env.name);
for (const env of container.env ?? []) {
envNames.add(env.name);
workloadEnv.set(env.name, env);
}
}
}
}
const dbEnvNames = [...envNames].filter((name) => /DATABASE|POSTGRES|MYSQL|SQLITE|MONGO|_DB(_|$)/u.test(name));
if (dbEnvNames.length === 0) {
addBlocker(blockers, "runtime_blocker", "cloud-api-db-config", "cloud-api has no DEV DB connection env configured");
const requiredEnv = DEV_DB_ENV_CONTRACT.requiredEnv.map((name) => {
const secretRef = DEV_DB_ENV_CONTRACT.secretRefs.find((ref) => ref.env === name) ?? null;
const workloadEntry = workloadEnv.get(name) ?? null;
const secretRefPresent = secretRef
? deployEnv[name] === `secretRef:${secretRef.secretName}/${secretRef.secretKey}` &&
workloadEntry?.valueFrom?.secretKeyRef?.name === secretRef.secretName &&
workloadEntry?.valueFrom?.secretKeyRef?.key === secretRef.secretKey
: true;
return {
name,
manifestPresent: Object.hasOwn(deployEnv, name),
k8sPresent: workloadEnv.has(name),
redacted: Boolean(secretRef),
source: secretRef ? "k8s-secret-ref" : "runtime-env",
secretRef: secretRef
? {
secretName: secretRef.secretName,
secretKey: secretRef.secretKey,
present: secretRefPresent,
redacted: true
}
: null
};
});
const missingManifest = requiredEnv.filter((env) => !env.manifestPresent).map((env) => env.name);
const missingK8s = requiredEnv.filter((env) => !env.k8sPresent).map((env) => env.name);
const missingSecretRefs = requiredEnv
.filter((env) => env.secretRef && !env.secretRef.present)
.map((env) => `${env.secretRef.secretName}/${env.secretRef.secretKey}`);
if (missingManifest.length > 0 || missingK8s.length > 0 || missingSecretRefs.length > 0) {
addBlocker(
blockers,
"runtime_blocker",
"cloud-api-db-config",
`cloud-api DEV DB env contract is incomplete; missing ${[...missingManifest, ...missingK8s, ...missingSecretRefs].join(", ")}`
);
}
let health = null;
try {
const modulePath = pathToFileURL(path.join(repoRoot, "internal/cloud/server.mjs")).href;
const { buildHealthPayload } = await import(modulePath);
const health = buildHealthPayload();
if (health.db?.connected !== true) {
addBlocker(blockers, "runtime_blocker", "cloud-api-db", "cloud-api health reports DB not connected in this artifact");
health = buildHealthPayload();
if (health.db?.ready !== true) {
addBlocker(
blockers,
"runtime_blocker",
"cloud-api-db",
`cloud-api health reports DB config blocked; missing ${health.db?.missingEnv?.join(", ") ?? "unknown DB env"}`
);
}
} catch (error) {
addBlocker(blockers, "observability_blocker", "cloud-api-health", `Cannot inspect cloud-api health payload: ${error.message}`);
}
return {
status: missingManifest.length === 0 && missingK8s.length === 0 && missingSecretRefs.length === 0 ? "contract-ready" : "contract-blocked",
requiredEnv,
missingManifest,
missingK8s,
missingSecretRefs,
runtimeHealth: health?.db ? summarizeDbContract(health.db) : null,
secretMaterialRead: false,
valuesRedacted: true,
liveDbEvidence: false,
fixtureEvidence: false,
note: "This check records DB env/Secret reference presence only. It is not live DB evidence."
};
}
async function probeDevEndpoint(blockers) {
@@ -451,8 +513,8 @@ async function probeDevEndpoint(blockers) {
if (json?.serviceId !== "hwlab-cloud-api" || json?.environment !== ENVIRONMENT_DEV) {
addBlocker(blockers, "runtime_blocker", "dev-health-identity", "DEV health did not identify hwlab-cloud-api in dev");
}
if (json?.db?.connected !== true) {
addBlocker(blockers, "runtime_blocker", "dev-health-db", "DEV health did not confirm cloud-api DB connectivity");
if (json?.db?.ready !== true && json?.db?.configReady !== true) {
addBlocker(blockers, "runtime_blocker", "dev-health-db", "DEV health did not confirm cloud-api DB config readiness");
}
return {
status: response.statusCode >= 200 && response.statusCode < 300 ? "pass" : "blocked",
@@ -551,7 +613,7 @@ export async function runDevDeployApply(argv, io = {}) {
const artifactEvidence = validateDeployAndCatalog(deploy, catalog, commitId, blockers);
const k8sManifest = validateK8s(devKustomization, namespaceDoc, workloads, services, healthContract, deploy, blockers);
await checkCloudApiDb(deploy, workloads, blockers);
const cloudApiDb = await checkCloudApiDb(deploy, workloads, blockers);
if (!(await readText("internal/cloud/server.mjs")).includes('url.pathname === "/health/live"')) {
addBlocker(blockers, "contract_blocker", "cloud-api-health-path", "cloud-api does not implement the k8s /health/live path");
}
@@ -622,7 +684,7 @@ export async function runDevDeployApply(argv, io = {}) {
requirements: [
"DEV artifact catalog must contain CI publish evidence and registry digests",
"kubectl must be available for D601 hwlab-dev and must not target PROD",
"hwlab-cloud-api /health/live must report serviceId, dev environment, and DB connectivity",
"hwlab-cloud-api /health/live must report serviceId, dev environment, and DB env readiness without exposing secret values",
"pods, services, configmaps, and image commit tags must be observable in hwlab-dev"
],
summary: status === "blocked" ? "One or more required DEV deploy preconditions are blocked." : "DEV deploy preconditions are satisfied."
@@ -646,6 +708,7 @@ export async function runDevDeployApply(argv, io = {}) {
rollbackHint: buildRollbackHint(workloads),
remainingBlockers,
artifactEvidence,
cloudApiDb,
k8sManifest,
clusterObservation,
liveProbe
+45 -1
View File
@@ -5,6 +5,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { summarizeDbContract } from "../../internal/cloud/db-contract.mjs";
import { DEV_ENDPOINT, ENVIRONMENT_DEV } from "../../internal/protocol/index.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
@@ -235,11 +236,54 @@ async function inspectContracts() {
namespace: deploy.namespace,
serviceCount: deploy.services?.length ?? 0,
edgeProxy: deploy.services?.find((service) => service.serviceId === "hwlab-edge-proxy") ?? null,
tunnelClient: deploy.services?.find((service) => service.serviceId === "hwlab-tunnel-client") ?? null
tunnelClient: deploy.services?.find((service) => service.serviceId === "hwlab-tunnel-client") ?? null,
cloudApiDb: inspectCloudApiDbContract(deploy)
}
};
}
function inspectCloudApiDbContract(deploy) {
const env = deploy.services?.find((service) => service.serviceId === "hwlab-cloud-api")?.env ?? {};
const db = {
status: env.HWLAB_CLOUD_DB_URL && env.HWLAB_CLOUD_DB_SSL_MODE ? "ready" : "blocked",
ready: Boolean(env.HWLAB_CLOUD_DB_URL && env.HWLAB_CLOUD_DB_SSL_MODE),
configReady: Boolean(env.HWLAB_CLOUD_DB_URL && env.HWLAB_CLOUD_DB_SSL_MODE),
connected: false,
connectionChecked: false,
fields: [
{
name: "HWLAB_CLOUD_DB_URL",
present: Boolean(env.HWLAB_CLOUD_DB_URL),
redacted: true
},
{
name: "HWLAB_CLOUD_DB_SSL_MODE",
present: Boolean(env.HWLAB_CLOUD_DB_SSL_MODE),
redacted: false
}
],
missingEnv: ["HWLAB_CLOUD_DB_URL", "HWLAB_CLOUD_DB_SSL_MODE"].filter((name) => !env[name]),
secretRefs: [
{
env: "HWLAB_CLOUD_DB_URL",
secretName: "hwlab-cloud-api-dev-db",
secretKey: "database-url",
present: env.HWLAB_CLOUD_DB_URL === "secretRef:hwlab-cloud-api-dev-db/database-url",
redacted: true
}
],
safety: {
liveDbEvidence: false
}
};
return {
...summarizeDbContract(db),
secretMaterialRead: false,
valuesRedacted: true,
note: "Manifest-only DB env placeholder; this is not live DB evidence."
};
}
async function inspectKubernetes() {
const hasKubectl = await commandExists("kubectl");
const result = {
+153 -1
View File
@@ -5,13 +5,18 @@ import path from "node:path";
import { promisify } from "node:util";
import { fileURLToPath } from "node:url";
import {
DEV_DB_ENV_CONTRACT,
buildDbHealthContract,
summarizeDbContract
} from "../../internal/cloud/db-contract.mjs";
import { DEV_ENDPOINT, ENVIRONMENT_DEV, SERVICE_IDS } from "../../internal/protocol/index.mjs";
const execFileAsync = promisify(execFile);
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const defaultReportPath = "reports/dev-gate/dev-preflight-report.json";
const issue = "pikasTech/HWLAB#34";
const supports = ["#7", "#12", "#22", "#23", "#29", "#30", "#31"].map(
const supports = ["#7", "#12", "#14", "#22", "#23", "#29", "#30", "#31", "#33", "#39", "#49"].map(
(id) => `pikasTech/HWLAB${id}`
);
const forbiddenActions = [
@@ -437,6 +442,152 @@ function validateEdgeHealthReport(reporter, edgeReport) {
});
}
function validateCloudApiDbContract(reporter, deploy, workloads) {
const staticContract = inspectCloudApiDbStaticContract(deploy, workloads);
const healthContract = summarizeDbContract(buildDbHealthContract());
const evidence = [
{
staticContract,
healthContract,
fixtureEvidence: false,
liveDbEvidence: false
}
];
if (staticContract.ready) {
reporter.check(
"cloud-api-db-env-contract",
"db",
"pass",
"cloud-api DEV DB manifest declares the required env names and redacted Secret reference.",
evidence
);
} else {
const missing = [
...staticContract.missingDeployEnv,
...staticContract.missingK8sEnv,
...staticContract.missingSecretRefs
];
reporter.check(
"cloud-api-db-env-contract",
"db",
"blocked",
`cloud-api DEV DB manifest is missing ${missing.join(", ")}.`,
evidence
);
reporter.block({
type: "contract_blocker",
scope: "cloud-api-db-env-contract",
summary: `cloud-api DEV DB contract is incomplete; missing ${missing.join(", ")}.`,
nextTask: "Repair deploy/deploy.json and deploy/k8s/base/workloads.yaml so they expose only the required DB env names and redacted Secret reference."
});
}
if (healthContract.ready) {
reporter.check(
"cloud-api-db-health-gate",
"db",
"pass",
"cloud-api DB health gate reports required env presence with redacted values.",
evidence
);
return;
}
reporter.check(
"cloud-api-db-health-gate",
"db",
"blocked",
`cloud-api DB health gate is missing ${healthContract.missingEnv.join(", ")}.`,
evidence
);
reporter.block({
type: "runtime_blocker",
scope: "cloud-api-db-health-gate",
summary: `cloud-api DB runtime env is not ready; missing ${healthContract.missingEnv.join(", ")}.`,
nextTask: "Configure DEV hwlab-cloud-api with Secret hwlab-cloud-api-dev-db/database-url and HWLAB_CLOUD_DB_SSL_MODE=require, then rerun health/preflight without printing the secret value."
});
}
function inspectCloudApiDbStaticContract(deploy, workloads) {
const cloudApi = deploy?.services?.find((service) => service.serviceId === "hwlab-cloud-api") ?? {};
const deployEnv = cloudApi.env ?? {};
const workloadEnv = getWorkloadEnvByServiceId(workloads, "hwlab-cloud-api");
const secretEnvNames = new Set(DEV_DB_ENV_CONTRACT.secretRefs.map((ref) => ref.env));
const fields = DEV_DB_ENV_CONTRACT.requiredEnv.map((name) => {
const secretRef = DEV_DB_ENV_CONTRACT.secretRefs.find((ref) => ref.env === name) ?? null;
const workloadEntry = workloadEnv.get(name) ?? null;
const manifestPresent = Object.hasOwn(deployEnv, name);
const k8sPresent = workloadEnv.has(name);
const secretRefMatches = secretRef
? deployEnv[name] === `secretRef:${secretRef.secretName}/${secretRef.secretKey}` &&
workloadEntry?.valueFrom?.secretKeyRef?.name === secretRef.secretName &&
workloadEntry?.valueFrom?.secretKeyRef?.key === secretRef.secretKey
: true;
return {
name,
manifestPresent,
k8sPresent,
redacted: secretEnvNames.has(name),
source: secretEnvNames.has(name) ? "k8s-secret-ref" : "runtime-env",
secretRef: secretRef
? {
secretName: secretRef.secretName,
secretKey: secretRef.secretKey,
present: secretRefMatches,
redacted: true
}
: null
};
});
const missingDeployEnv = fields.filter((field) => !field.manifestPresent).map((field) => field.name);
const missingK8sEnv = fields.filter((field) => !field.k8sPresent).map((field) => field.name);
const missingSecretRefs = fields
.filter((field) => field.secretRef && !field.secretRef.present)
.map((field) => `${field.secretRef.secretName}/${field.secretRef.secretKey}`);
return {
contractVersion: DEV_DB_ENV_CONTRACT.contractVersion,
environment: DEV_DB_ENV_CONTRACT.environment,
ready: missingDeployEnv.length === 0 && missingK8sEnv.length === 0 && missingSecretRefs.length === 0,
requiredEnv: fields,
missingDeployEnv,
missingK8sEnv,
missingSecretRefs,
secretMaterialRead: false,
valuesRedacted: true,
liveDbEvidence: false,
fixtureEvidence: false
};
}
function getWorkloadEnvByServiceId(workloads, serviceId) {
const envByName = new Map();
for (const item of listManifestItems(workloads)) {
for (const container of item?.spec?.template?.spec?.containers ?? []) {
const itemServiceId =
item?.metadata?.labels?.["hwlab.pikastech.local/service-id"] ||
item?.spec?.template?.metadata?.labels?.["hwlab.pikastech.local/service-id"] ||
container?.name;
if (itemServiceId !== serviceId) {
continue;
}
for (const env of container.env ?? []) {
envByName.set(env.name, env);
}
}
}
return envByName;
}
function listManifestItems(document) {
if (!document) {
return [];
}
return document.kind === "List" ? document.items ?? [] : [document];
}
async function validateEdgeContracts(reporter, masterEdge) {
try {
const [frpc, frps] = await Promise.all([
@@ -634,6 +785,7 @@ export async function runPreflight(argv) {
args.targetRef
);
validateCloudApiDbContract(reporter, deploy, contracts[3]);
validateArtifactPublishReport(reporter, optionalReports.artifactPublish, targetShortCommit, targetCommit, args.targetRef);
validateEdgeHealthReport(reporter, optionalReports.edgeHealth);
await validateEdgeContracts(reporter, masterEdge);