fix: add dev DB DNS contract

This commit is contained in:
Code Queue Review
2026-05-22 15:32:57 +00:00
parent 01c4e1b533
commit 3049546cfd
19 changed files with 407 additions and 29 deletions
+1 -1
View File
@@ -19,7 +19,7 @@ HWLAB 是硬件实验室运行面和控制面项目。本文是 agent、指挥
- 长期参考入口:[docs/reference/README.md](docs/reference/README.md)
- 文档治理规范:[docs/reference/documentation-governance.md](docs/reference/documentation-governance.md)
- 架构和 M3 主线:[docs/reference/architecture.md](docs/reference/architecture.md)
- DEV 运行态、端口k3s 边界:[docs/reference/dev-runtime-boundary.md](docs/reference/dev-runtime-boundary.md)
- DEV 运行态、端口k3s 和 DB DNS 边界:[docs/reference/dev-runtime-boundary.md](docs/reference/dev-runtime-boundary.md)
- 部署正规化、镜像发布、回滚和 Cloud Web 路径:[docs/reference/deployment-publish.md](docs/reference/deployment-publish.md)
- 指挥官协作、PR 和 runner handoff[docs/reference/commander-collaboration.md](docs/reference/commander-collaboration.md)
- M3 闭环 rollout runbook[docs/reference/m3-loop-rollout-runbook.md](docs/reference/m3-loop-rollout-runbook.md)
+7 -3
View File
@@ -18,9 +18,13 @@ for future compatibility, but PROD deployment is not part of MVP acceptance.
- `hwlab-cloud-api` declares the DEV DB env contract with
`HWLAB_CLOUD_DB_URL` from Secret reference
`hwlab-cloud-api-dev-db/database-url` and non-secret
`HWLAB_CLOUD_DB_SSL_MODE=require`. The repository records names only, never
secret values or a live DB connection string; runtime health reports redacted
env injection and DB connection result classifiers.
`HWLAB_CLOUD_DB_SSL_MODE=require`. It also declares the non-secret DB DNS
contract `cloud-api-db.hwlab-dev.svc.cluster.local:5432`, backed by
selectorless `Service/cloud-api-db` in `hwlab-dev`; see
[docs/reference/dev-runtime-boundary.md](../docs/reference/dev-runtime-boundary.md).
The repository records names only, never secret values or a live DB
connection string; runtime health reports redacted env injection and DB
connection result classifiers.
- `deploy/k8s/prod` is a disabled placeholder gate only.
- `deploy/frp` describes the D601-to-master reverse link without secrets.
- `deploy/master-edge` describes public edge ownership and health boundaries.
+4
View File
@@ -182,6 +182,10 @@
"HWLAB_CLOUD_DB_URL": "secretRef:hwlab-cloud-api-dev-db/database-url",
"HWLAB_CLOUD_DB_SSL_MODE": "require",
"HWLAB_CLOUD_DB_CONTRACT": "dev-redacted-presence-only",
"HWLAB_CLOUD_DB_SERVICE_NAME": "cloud-api-db",
"HWLAB_CLOUD_DB_SERVICE_NAMESPACE": "hwlab-dev",
"HWLAB_CLOUD_DB_HOST": "cloud-api-db.hwlab-dev.svc.cluster.local",
"HWLAB_CLOUD_DB_PORT": "5432",
"HWLAB_CODE_AGENT_PROVIDER": "openai",
"HWLAB_CODE_AGENT_MODEL": "gpt-5.5",
"OPENAI_API_KEY": "secretRef:hwlab-code-agent-provider/openai-api-key"
+16
View File
@@ -359,6 +359,22 @@
"HWLAB_CLOUD_DB_CONTRACT": {
"type": "string",
"const": "dev-redacted-presence-only"
},
"HWLAB_CLOUD_DB_SERVICE_NAME": {
"type": "string",
"const": "cloud-api-db"
},
"HWLAB_CLOUD_DB_SERVICE_NAMESPACE": {
"type": "string",
"const": "hwlab-dev"
},
"HWLAB_CLOUD_DB_HOST": {
"type": "string",
"const": "cloud-api-db.hwlab-dev.svc.cluster.local"
},
"HWLAB_CLOUD_DB_PORT": {
"type": "string",
"const": "5432"
}
}
},
+23
View File
@@ -27,6 +27,29 @@
]
}
},
{
"apiVersion": "v1",
"kind": "Service",
"metadata": {
"name": "cloud-api-db",
"namespace": "hwlab-dev",
"labels": {
"app.kubernetes.io/name": "cloud-api-db",
"hwlab.pikastech.local/component": "cloud-api-db",
"hwlab.pikastech.local/source-contract": "db-dns"
}
},
"spec": {
"type": "ClusterIP",
"ports": [
{
"name": "postgres",
"port": 5432,
"targetPort": "postgres"
}
]
}
},
{
"apiVersion": "v1",
"kind": "Service",
+16
View File
@@ -81,6 +81,22 @@
"name": "HWLAB_CLOUD_DB_CONTRACT",
"value": "dev-redacted-presence-only"
},
{
"name": "HWLAB_CLOUD_DB_SERVICE_NAME",
"value": "cloud-api-db"
},
{
"name": "HWLAB_CLOUD_DB_SERVICE_NAMESPACE",
"value": "hwlab-dev"
},
{
"name": "HWLAB_CLOUD_DB_HOST",
"value": "cloud-api-db.hwlab-dev.svc.cluster.local"
},
{
"name": "HWLAB_CLOUD_DB_PORT",
"value": "5432"
},
{
"name": "HWLAB_CODE_AGENT_PROVIDER",
"value": "openai"
+1 -1
View File
@@ -12,7 +12,7 @@
"data": {
"endpoint": "http://74.48.78.17:16667",
"cloud-api": "GET /health/live via hwlab-edge-proxy then hwlab-cloud-api:6667",
"cloud-api-db": "DEV DB config gate requires HWLAB_CLOUD_DB_URL from Secret hwlab-cloud-api-dev-db/database-url plus HWLAB_CLOUD_DB_SSL_MODE=require; health reports env presence, env injection, redacted connection attempt/result, and liveConnected without exposing secret values",
"cloud-api-db": "DEV DB config gate requires HWLAB_CLOUD_DB_URL from Secret hwlab-cloud-api-dev-db/database-url, HWLAB_CLOUD_DB_SSL_MODE=require, and stable internal DNS cloud-api-db.hwlab-dev.svc.cluster.local:5432 backed by Service cloud-api-db in hwlab-dev; health reports env presence, env injection, redacted connection attempt/result, and liveConnected without exposing secret values",
"cloud-web": "GET /health/live on hwlab-cloud-web:8080; consumes cloud-api only",
"agent": "hwlab-agent-mgr readiness gates worker template creation; hwlab-agent-worker is suspended until a session is scheduled",
"sim": "hwlab-gateway-simu:7101 and hwlab-box-simu:7201 expose /health/live for DEV smoke",
+5
View File
@@ -41,6 +41,11 @@ If both env vars are present, cloud-api attempts a short read-only TCP
connection to the DB endpoint from the injected runtime env. The health payload
keeps all endpoint details redacted and reports only:
The source-controlled DEV DNS contract for that endpoint is
`cloud-api-db.hwlab-dev.svc.cluster.local:5432`. The actual connection string
still comes from `hwlab-cloud-api-dev-db/database-url`; source records only the
non-secret host/service/namespace/port contract.
- `db.configReady`: required env names are present.
- `db.connectionAttempted`: the runtime tried the live connection path.
- `db.connectionResult`: redacted classifier such as `connected`, `refused`,
+4 -2
View File
@@ -220,8 +220,10 @@ unchanged.
`hwlab-cloud-api` has a DEV DB env contract placeholder:
`HWLAB_CLOUD_DB_URL` must come from Secret reference
`hwlab-cloud-api-dev-db/database-url`, and
`HWLAB_CLOUD_DB_SSL_MODE=require`. The local health gate reports DB runtime
config without printing the connection string.
`HWLAB_CLOUD_DB_SSL_MODE=require`. The non-secret DB DNS contract is
`cloud-api-db.hwlab-dev.svc.cluster.local:5432` via selectorless
`Service/cloud-api-db` in `hwlab-dev`. The local health gate reports DB
runtime config without printing the connection string.
No PROD resource, secret value, UniDesk runtime substitute, browser e2e,
heavyweight e2e, or force push is part of this flow.
+2
View File
@@ -35,6 +35,8 @@ Required DEV DB config names:
- `HWLAB_CLOUD_DB_URL` from Secret reference
`hwlab-cloud-api-dev-db/database-url`.
- `HWLAB_CLOUD_DB_SSL_MODE=require`.
- Stable k3s DB DNS: `cloud-api-db.hwlab-dev.svc.cluster.local:5432`,
backed by selectorless `Service/cloud-api-db` in `hwlab-dev`.
When the Secret/env is absent, the health JSON should keep
`db.connected: false`, set `db.status: "blocked"`, and list the missing env
+3 -1
View File
@@ -67,7 +67,9 @@ The preflight checks:
- `hwlab-cloud-api` declares the DEV DB env contract:
`HWLAB_CLOUD_DB_URL` from Secret reference
`hwlab-cloud-api-dev-db/database-url` and
`HWLAB_CLOUD_DB_SSL_MODE=require`.
`HWLAB_CLOUD_DB_SSL_MODE=require`. The non-secret DB DNS contract is
`cloud-api-db.hwlab-dev.svc.cluster.local:5432` via selectorless
`Service/cloud-api-db` in `hwlab-dev`.
- The local cloud-api health payload reports DB env presence/missing status with
redacted values only. This is a readiness gate, not live DB evidence.
- `deploy/frp` and `deploy/master-edge` describe the D601-to-master DEV route
+1 -1
View File
@@ -11,7 +11,7 @@ as run logs.
| --- | --- |
| Documentation governance | [documentation-governance.md](documentation-governance.md) |
| Architecture and M3 decision line | [architecture.md](architecture.md) |
| DEV runtime, ports, k3s, and environment boundary | [dev-runtime-boundary.md](dev-runtime-boundary.md) |
| DEV runtime, ports, k3s, DB DNS, and environment boundary | [dev-runtime-boundary.md](dev-runtime-boundary.md) |
| Deployment regularization, artifact publish, rollback, and Cloud Web rollout | [deployment-publish.md](deployment-publish.md) |
| Cloud Workbench default UI and UX boundary | [cloud-workbench.md](cloud-workbench.md) |
| Commander, runner, PR, and prompt handoff | [commander-collaboration.md](commander-collaboration.md) |
+30
View File
@@ -54,6 +54,36 @@ node scripts/d601-k3s-readonly-observability.mjs
The preflight must not print secret values, kubeconfig material, ConfigMap
values, or token content.
## Cloud API DB DNS Contract
`hwlab-cloud-api` reads the DEV database connection string from Secret
reference `hwlab-cloud-api-dev-db/database-url`. The Secret value is not stored
in source, not printed by reports, and not required for offline validation.
The non-secret DNS contract is source-controlled:
| Field | Value |
| --- | --- |
| Service | `cloud-api-db` |
| Namespace | `hwlab-dev` |
| Host | `cloud-api-db.hwlab-dev.svc.cluster.local` |
| Port | `5432` |
| Port name | `postgres` |
`deploy/k8s/base/services.yaml` must include a selectorless
`Service/cloud-api-db` with that port so k3s DNS has a stable handle before the
actual DB endpoints are provisioned. A selectorless Service proves only that
the name can exist in cluster DNS; it does not prove a DB endpoint, credential,
TCP connection, schema migration, durable persistence, or M4/M5 readiness.
`deploy/deploy.json`, `deploy/k8s/base/workloads.yaml`, and
`internal/cloud/db-contract.mjs` must agree on
`HWLAB_CLOUD_DB_SERVICE_NAME`, `HWLAB_CLOUD_DB_SERVICE_NAMESPACE`,
`HWLAB_CLOUD_DB_HOST`, and `HWLAB_CLOUD_DB_PORT`. `npm run validate` checks
this agreement offline. A live pass still requires `/health/live` to report
`db.ready=true`, `db.connected=true`, and `liveDbEvidence=true` with secret
values redacted.
## Runtime Substitution Ban
UniDesk services, provider-gateway, backend-core, microservice proxies, local
+15 -3
View File
@@ -16,8 +16,19 @@ export const DEV_DB_ENV_CONTRACT = Object.freeze({
secretKey: "database-url"
})
]),
dns: Object.freeze({
serviceName: "cloud-api-db",
namespace: "hwlab-dev",
host: "cloud-api-db.hwlab-dev.svc.cluster.local",
port: 5432,
portName: "postgres"
}),
nonSecretDefaults: Object.freeze({
HWLAB_CLOUD_DB_SSL_MODE: "require"
HWLAB_CLOUD_DB_SSL_MODE: "require",
HWLAB_CLOUD_DB_SERVICE_NAME: "cloud-api-db",
HWLAB_CLOUD_DB_SERVICE_NAMESPACE: "hwlab-dev",
HWLAB_CLOUD_DB_HOST: "cloud-api-db.hwlab-dev.svc.cluster.local",
HWLAB_CLOUD_DB_PORT: "5432"
}),
forbidden: Object.freeze({
prodAllowed: false,
@@ -112,6 +123,7 @@ export function buildDbEnvManifestPlaceholder() {
return {
requiredEnv: [...DEV_DB_ENV_CONTRACT.requiredEnv],
secretRefs: DEV_DB_ENV_CONTRACT.secretRefs.map((item) => ({ ...item })),
dns: { ...DEV_DB_ENV_CONTRACT.dns },
nonSecretDefaults: { ...DEV_DB_ENV_CONTRACT.nonSecretDefaults },
fixtureEvidence: {
liveDbEvidence: false,
@@ -202,7 +214,7 @@ function buildRedactionSafety(connection) {
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);
const parsed = parseDbUrlContract(env?.HWLAB_CLOUD_DB_URL);
if (!parsed.ok) {
return {
attempted: true,
@@ -226,7 +238,7 @@ async function probeDbConnection(env, options) {
});
}
function parseDbTarget(rawUrl) {
export function parseDbUrlContract(rawUrl) {
try {
const url = new URL(rawUrl);
if (!supportedDbProtocols.has(url.protocol)) {
+39
View File
@@ -3,6 +3,7 @@ import { readFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { DEV_DB_ENV_CONTRACT } from "../../internal/cloud/db-contract.mjs";
const execFileAsync = promisify(execFile);
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
@@ -135,6 +136,7 @@ function validateSource(ctx, manifest) {
const tunnelClient = servicesById.get("hwlab-tunnel-client");
const cli = servicesById.get("hwlab-cli");
expectEqual(ctx, cloudApi?.env?.HWLAB_PUBLIC_ENDPOINT, endpoints.api?.url, "$.services.hwlab-cloud-api.env.HWLAB_PUBLIC_ENDPOINT", "cloud API public endpoint env");
validateCloudApiDbDnsSource(ctx, cloudApi?.env ?? {});
expectEqual(ctx, cli?.env?.HWLAB_CLI_ENDPOINT, endpoints.api?.url, "$.services.hwlab-cli.env.HWLAB_CLI_ENDPOINT", "CLI endpoint env");
expectEqual(ctx, tunnelClient?.env?.HWLAB_FRP_PUBLIC_PORT, String(expectedPorts.api), "$.services.hwlab-tunnel-client.env.HWLAB_FRP_PUBLIC_PORT", "tunnel API public port env");
expectEqual(ctx, tunnelClient?.env?.HWLAB_FRP_WEB_PUBLIC_PORT, String(expectedPorts.frontend), "$.services.hwlab-tunnel-client.env.HWLAB_FRP_WEB_PUBLIC_PORT", "tunnel frontend public port env");
@@ -168,6 +170,15 @@ function validateSource(ctx, manifest) {
return { manifest, endpoints, servicesById, proxies, serviceMappings, healthPath: manifest.health?.path };
}
function validateCloudApiDbDnsSource(ctx, env) {
const dns = DEV_DB_ENV_CONTRACT.dns;
const expectedHost = `${dns.serviceName}.${dns.namespace}.svc.cluster.local`;
expectEqual(ctx, env.HWLAB_CLOUD_DB_SERVICE_NAME, dns.serviceName, "$.services.hwlab-cloud-api.env.HWLAB_CLOUD_DB_SERVICE_NAME", "cloud API DB service name");
expectEqual(ctx, env.HWLAB_CLOUD_DB_SERVICE_NAMESPACE, dns.namespace, "$.services.hwlab-cloud-api.env.HWLAB_CLOUD_DB_SERVICE_NAMESPACE", "cloud API DB service namespace");
expectEqual(ctx, env.HWLAB_CLOUD_DB_HOST, expectedHost, "$.services.hwlab-cloud-api.env.HWLAB_CLOUD_DB_HOST", "cloud API DB stable DNS host");
expectEqual(ctx, env.HWLAB_CLOUD_DB_PORT, String(dns.port), "$.services.hwlab-cloud-api.env.HWLAB_CLOUD_DB_PORT", "cloud API DB service port");
}
function renderPlan(source) {
const proxyPorts = [...new Set(source.proxies.map((proxy) => Number(proxy.remotePort)))].sort((a, b) => a - b);
return {
@@ -265,6 +276,8 @@ function validateK3sArtifacts(ctx, source, rendered, services, workloads, health
}
}
validateCloudApiDbDnsArtifacts(ctx, services, workloads);
expectEqual(ctx, healthContract.data?.endpoint, source.endpoints.api?.url, "deploy/k8s/dev/health-contract.yaml.data.endpoint", "health contract endpoint");
expectEqual(ctx, masterEdge.endpoint, source.endpoints.api?.url, "deploy/master-edge/health-contract.json.endpoint", "master edge endpoint");
flagLegacyPublicPort(ctx, "deploy/master-edge/health-contract.json.reverseLink.publicPort", masterEdge.reverseLink?.publicPort, expectedPorts.api, "master edge API");
@@ -272,6 +285,32 @@ function validateK3sArtifacts(ctx, source, rendered, services, workloads, health
expectEqual(ctx, masterEdge.prodAcceptance, false, "deploy/master-edge/health-contract.json.prodAcceptance", "master edge PROD acceptance");
}
function validateCloudApiDbDnsArtifacts(ctx, services, workloads) {
const dns = DEV_DB_ENV_CONTRACT.dns;
const expectedHost = `${dns.serviceName}.${dns.namespace}.svc.cluster.local`;
const service = listItems(services).find((item) => item?.metadata?.name === dns.serviceName);
expect(ctx, Boolean(service), "missing_rendered_artifact", "deploy/k8s/base/services.yaml.cloud-api-db", "cloud-api DB stable DNS Service exists");
if (service) {
const port = (service.spec?.ports ?? []).find((entry) => entry.name === dns.portName);
expectEqual(ctx, service.metadata?.namespace, dns.namespace, "deploy/k8s/base/services.yaml.cloud-api-db.metadata.namespace", "cloud-api DB Service namespace");
expectEqual(ctx, service.spec?.type, "ClusterIP", "deploy/k8s/base/services.yaml.cloud-api-db.spec.type", "cloud-api DB Service type");
expectEqual(ctx, service.spec?.selector, undefined, "deploy/k8s/base/services.yaml.cloud-api-db.spec.selector", "cloud-api DB Service is selectorless until endpoints are provisioned");
expectEqual(ctx, service.metadata?.labels?.["hwlab.pikastech.local/source-contract"], "db-dns", "deploy/k8s/base/services.yaml.cloud-api-db.metadata.labels.source-contract", "cloud-api DB Service source-contract label");
expect(ctx, Boolean(port), "missing_rendered_artifact", "deploy/k8s/base/services.yaml.cloud-api-db.spec.ports.postgres", "cloud-api DB Service postgres port exists");
if (port) {
expectEqual(ctx, port.port, dns.port, "deploy/k8s/base/services.yaml.cloud-api-db.spec.ports.postgres.port", "cloud-api DB Service port");
expectEqual(ctx, port.targetPort, dns.portName, "deploy/k8s/base/services.yaml.cloud-api-db.spec.ports.postgres.targetPort", "cloud-api DB Service targetPort");
}
}
const container = mapByServiceId(listItems(workloads)).get("hwlab-cloud-api")?.spec?.template?.spec?.containers?.[0];
const env = new Map((container?.env ?? []).map((entry) => [entry.name, entry.value]));
expectEqual(ctx, env.get("HWLAB_CLOUD_DB_SERVICE_NAME"), dns.serviceName, "deploy/k8s/base/workloads.yaml.hwlab-cloud-api.env.HWLAB_CLOUD_DB_SERVICE_NAME", "cloud API workload DB service name");
expectEqual(ctx, env.get("HWLAB_CLOUD_DB_SERVICE_NAMESPACE"), dns.namespace, "deploy/k8s/base/workloads.yaml.hwlab-cloud-api.env.HWLAB_CLOUD_DB_SERVICE_NAMESPACE", "cloud API workload DB service namespace");
expectEqual(ctx, env.get("HWLAB_CLOUD_DB_HOST"), expectedHost, "deploy/k8s/base/workloads.yaml.hwlab-cloud-api.env.HWLAB_CLOUD_DB_HOST", "cloud API workload DB host");
expectEqual(ctx, env.get("HWLAB_CLOUD_DB_PORT"), String(dns.port), "deploy/k8s/base/workloads.yaml.hwlab-cloud-api.env.HWLAB_CLOUD_DB_PORT", "cloud API workload DB port");
}
async function buildPlan() {
const ctx = { checks: 0, diagnostics: [] };
const [manifest, services, workloads, healthContract, masterEdge, frpc, frps] = await Promise.all([
+72 -7
View File
@@ -339,7 +339,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 env readiness through health output, without reading secret values.";
return "Configure and verify the DEV cloud-api DB env and stable DNS readiness through health output, without reading secret values.";
}
return blocker.summary;
}
@@ -543,7 +543,7 @@ function validateK8s(devKustomization, namespaceDoc, workloads, services, health
};
}
async function checkCloudApiDb(deploy, workloads, blockers) {
async function checkCloudApiDb(deploy, workloads, services, blockers) {
const deployEnv = deploy?.services?.find((service) => service.serviceId === "hwlab-cloud-api")?.env ?? {};
const envNames = new Set(Object.keys(deployEnv));
const workloadEnv = new Map();
@@ -586,16 +586,22 @@ async function checkCloudApiDb(deploy, workloads, blockers) {
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) {
const dnsContract = inspectCloudApiDbDnsContract(deployEnv, workloadEnv, services);
const missingDnsContract = dnsContract.missing;
if (missingManifest.length > 0 || missingK8s.length > 0 || missingSecretRefs.length > 0 || missingDnsContract.length > 0) {
addBlocker(
blockers,
"runtime_blocker",
"cloud-api-db-config",
`cloud-api DEV DB env contract is incomplete; missing ${[...missingManifest, ...missingK8s, ...missingSecretRefs].join(", ")}`
`cloud-api DEV DB env/DNS contract is incomplete; missing ${[...missingManifest, ...missingK8s, ...missingSecretRefs, ...missingDnsContract].join(", ")}`
);
}
const configReady = missingManifest.length === 0 && missingK8s.length === 0 && missingSecretRefs.length === 0;
const configReady =
missingManifest.length === 0 &&
missingK8s.length === 0 &&
missingSecretRefs.length === 0 &&
missingDnsContract.length === 0;
const health = {
status: configReady ? "degraded" : "blocked",
ready: false,
@@ -628,12 +634,71 @@ async function checkCloudApiDb(deploy, workloads, blockers) {
missingManifest,
missingK8s,
missingSecretRefs,
missingDnsContract,
dns: dnsContract,
runtimeHealth: summarizeDbContract(health),
secretMaterialRead: false,
valuesRedacted: true,
liveDbEvidence: false,
fixtureEvidence: false,
note: "This check records DB env/Secret reference presence only. It is not live DB evidence."
note: "This check records DB env, Secret reference, and stable DNS Service presence only. It is not live DB evidence."
};
}
function inspectCloudApiDbDnsContract(deployEnv, workloadEnv, services) {
const dns = DEV_DB_ENV_CONTRACT.dns;
const expectedHost = `${dns.serviceName}.${dns.namespace}.svc.cluster.local`;
const service = listItems(services).find((item) => item?.metadata?.name === dns.serviceName) ?? null;
const port = service?.spec?.ports?.find((entry) => entry.name === dns.portName) ?? null;
const actual = {
deployServiceName: deployEnv.HWLAB_CLOUD_DB_SERVICE_NAME,
deployServiceNamespace: deployEnv.HWLAB_CLOUD_DB_SERVICE_NAMESPACE,
deployHost: deployEnv.HWLAB_CLOUD_DB_HOST,
deployPort: deployEnv.HWLAB_CLOUD_DB_PORT,
workloadServiceName: workloadEnv.get("HWLAB_CLOUD_DB_SERVICE_NAME")?.value,
workloadServiceNamespace: workloadEnv.get("HWLAB_CLOUD_DB_SERVICE_NAMESPACE")?.value,
workloadHost: workloadEnv.get("HWLAB_CLOUD_DB_HOST")?.value,
workloadPort: workloadEnv.get("HWLAB_CLOUD_DB_PORT")?.value,
serviceName: service?.metadata?.name,
serviceNamespace: service?.metadata?.namespace,
serviceType: service?.spec?.type,
serviceSelector: service?.spec?.selector ?? null,
portName: port?.name,
port: port?.port,
targetPort: port?.targetPort
};
const checks = [
["deployEnv.HWLAB_CLOUD_DB_SERVICE_NAME", actual.deployServiceName, dns.serviceName],
["deployEnv.HWLAB_CLOUD_DB_SERVICE_NAMESPACE", actual.deployServiceNamespace, dns.namespace],
["deployEnv.HWLAB_CLOUD_DB_HOST", actual.deployHost, expectedHost],
["deployEnv.HWLAB_CLOUD_DB_PORT", actual.deployPort, String(dns.port)],
["workloadEnv.HWLAB_CLOUD_DB_SERVICE_NAME", actual.workloadServiceName, dns.serviceName],
["workloadEnv.HWLAB_CLOUD_DB_SERVICE_NAMESPACE", actual.workloadServiceNamespace, dns.namespace],
["workloadEnv.HWLAB_CLOUD_DB_HOST", actual.workloadHost, expectedHost],
["workloadEnv.HWLAB_CLOUD_DB_PORT", actual.workloadPort, String(dns.port)],
["Service.metadata.name", actual.serviceName, dns.serviceName],
["Service.metadata.namespace", actual.serviceNamespace, dns.namespace],
["Service.spec.type", actual.serviceType, "ClusterIP"],
["Service.spec.selector", actual.serviceSelector, null],
["Service.port.name", actual.portName, dns.portName],
["Service.port.port", actual.port, dns.port],
["Service.port.targetPort", actual.targetPort, dns.portName]
];
const missing = checks
.filter(([, actualValue, expectedValue]) => !Object.is(actualValue, expectedValue))
.map(([name, actualValue, expectedValue]) => `${name} expected ${expectedValue} got ${actualValue ?? "missing"}`);
return {
ready: missing.length === 0,
host: expectedHost,
serviceName: dns.serviceName,
namespace: dns.namespace,
port: dns.port,
selectorless: service ? service.spec?.selector === undefined : false,
secretMaterialRead: false,
liveDbEvidence: false,
missing,
actual
};
}
@@ -937,7 +1002,7 @@ export async function runDevDeployApply(argv, io = {}) {
const artifactEvidence = validateDeployAndCatalog(deploy, catalog, commitId, blockers);
const k8sManifest = validateK8s(devKustomization, namespaceDoc, workloads, services, healthContract, deploy, blockers);
const cloudApiDb = await checkCloudApiDb(deploy, workloads, blockers);
const cloudApiDb = await checkCloudApiDb(deploy, workloads, services, 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");
}
+74 -8
View File
@@ -805,8 +805,8 @@ function validateEdgeHealthReport(reporter, edgeReport) {
});
}
function validateCloudApiDbContract(reporter, deploy, workloads) {
const staticContract = inspectCloudApiDbStaticContract(deploy, workloads);
function validateCloudApiDbContract(reporter, deploy, workloads, services) {
const staticContract = inspectCloudApiDbStaticContract(deploy, workloads, services);
const healthContract = summarizeDbContract(buildDbHealthContract());
const evidence = [
{
@@ -822,14 +822,15 @@ function validateCloudApiDbContract(reporter, deploy, workloads) {
"cloud-api-db-env-contract",
"db",
"pass",
"cloud-api DEV DB manifest declares the required env names and redacted Secret reference.",
"cloud-api DEV DB manifest declares the required env names, redacted Secret reference, and stable k3s DB DNS Service.",
evidence
);
} else {
const missing = [
...staticContract.missingDeployEnv,
...staticContract.missingK8sEnv,
...staticContract.missingSecretRefs
...staticContract.missingSecretRefs,
...staticContract.missingDnsContract
];
reporter.check(
"cloud-api-db-env-contract",
@@ -842,7 +843,7 @@ function validateCloudApiDbContract(reporter, deploy, workloads) {
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."
nextTask: "Repair deploy/deploy.json, deploy/k8s/base/workloads.yaml, and deploy/k8s/base/services.yaml so they expose the required DB env names, redacted Secret reference, and stable DB Service DNS."
});
}
@@ -872,7 +873,7 @@ function validateCloudApiDbContract(reporter, deploy, workloads) {
});
}
function inspectCloudApiDbStaticContract(deploy, workloads) {
function inspectCloudApiDbStaticContract(deploy, workloads, services) {
const cloudApi = deploy?.services?.find((service) => service.serviceId === "hwlab-cloud-api") ?? {};
const deployEnv = cloudApi.env ?? {};
const workloadEnv = getWorkloadEnvByServiceId(workloads, "hwlab-cloud-api");
@@ -909,15 +910,22 @@ function inspectCloudApiDbStaticContract(deploy, workloads) {
const missingSecretRefs = fields
.filter((field) => field.secretRef && !field.secretRef.present)
.map((field) => `${field.secretRef.secretName}/${field.secretRef.secretKey}`);
const dnsContract = inspectCloudApiDbDnsContract(deployEnv, workloadEnv, services);
return {
contractVersion: DEV_DB_ENV_CONTRACT.contractVersion,
environment: DEV_DB_ENV_CONTRACT.environment,
ready: missingDeployEnv.length === 0 && missingK8sEnv.length === 0 && missingSecretRefs.length === 0,
ready:
missingDeployEnv.length === 0 &&
missingK8sEnv.length === 0 &&
missingSecretRefs.length === 0 &&
dnsContract.ready,
requiredEnv: fields,
dns: dnsContract,
missingDeployEnv,
missingK8sEnv,
missingSecretRefs,
missingDnsContract: dnsContract.missing,
secretMaterialRead: false,
valuesRedacted: true,
liveDbEvidence: false,
@@ -925,6 +933,64 @@ function inspectCloudApiDbStaticContract(deploy, workloads) {
};
}
function inspectCloudApiDbDnsContract(deployEnv, workloadEnv, services) {
const dns = DEV_DB_ENV_CONTRACT.dns;
const expectedHost = `${dns.serviceName}.${dns.namespace}.svc.cluster.local`;
const service = listManifestItems(services).find((item) => item?.metadata?.name === dns.serviceName) ?? null;
const port = service?.spec?.ports?.find((entry) => entry.name === dns.portName) ?? null;
const actual = {
deployServiceName: deployEnv.HWLAB_CLOUD_DB_SERVICE_NAME,
deployServiceNamespace: deployEnv.HWLAB_CLOUD_DB_SERVICE_NAMESPACE,
deployHost: deployEnv.HWLAB_CLOUD_DB_HOST,
deployPort: deployEnv.HWLAB_CLOUD_DB_PORT,
workloadServiceName: workloadEnv.get("HWLAB_CLOUD_DB_SERVICE_NAME")?.value,
workloadServiceNamespace: workloadEnv.get("HWLAB_CLOUD_DB_SERVICE_NAMESPACE")?.value,
workloadHost: workloadEnv.get("HWLAB_CLOUD_DB_HOST")?.value,
workloadPort: workloadEnv.get("HWLAB_CLOUD_DB_PORT")?.value,
serviceName: service?.metadata?.name,
serviceNamespace: service?.metadata?.namespace,
serviceType: service?.spec?.type,
serviceSelector: service?.spec?.selector ?? null,
portName: port?.name,
port: port?.port,
targetPort: port?.targetPort
};
const checks = [
["deployEnv.HWLAB_CLOUD_DB_SERVICE_NAME", actual.deployServiceName, dns.serviceName],
["deployEnv.HWLAB_CLOUD_DB_SERVICE_NAMESPACE", actual.deployServiceNamespace, dns.namespace],
["deployEnv.HWLAB_CLOUD_DB_HOST", actual.deployHost, expectedHost],
["deployEnv.HWLAB_CLOUD_DB_PORT", actual.deployPort, String(dns.port)],
["workloadEnv.HWLAB_CLOUD_DB_SERVICE_NAME", actual.workloadServiceName, dns.serviceName],
["workloadEnv.HWLAB_CLOUD_DB_SERVICE_NAMESPACE", actual.workloadServiceNamespace, dns.namespace],
["workloadEnv.HWLAB_CLOUD_DB_HOST", actual.workloadHost, expectedHost],
["workloadEnv.HWLAB_CLOUD_DB_PORT", actual.workloadPort, String(dns.port)],
["Service.metadata.name", actual.serviceName, dns.serviceName],
["Service.metadata.namespace", actual.serviceNamespace, dns.namespace],
["Service.spec.type", actual.serviceType, "ClusterIP"],
["Service.spec.selector", actual.serviceSelector, null],
["Service.port.name", actual.portName, dns.portName],
["Service.port.port", actual.port, dns.port],
["Service.port.targetPort", actual.targetPort, dns.portName]
];
const missing = checks
.filter(([, actualValue, expectedValue]) => !Object.is(actualValue, expectedValue))
.map(([name, actualValue, expectedValue]) => `${name} expected ${expectedValue} got ${actualValue ?? "missing"}`);
return {
ready: missing.length === 0,
host: expectedHost,
serviceName: dns.serviceName,
namespace: dns.namespace,
port: dns.port,
portName: dns.portName,
selectorless: service ? service.spec?.selector === undefined : false,
secretMaterialRead: false,
liveDbEvidence: false,
missing,
actual
};
}
function getWorkloadEnvByServiceId(workloads, serviceId) {
const envByName = new Map();
for (const item of listManifestItems(workloads)) {
@@ -1174,7 +1240,7 @@ export async function runPreflight(argv) {
);
validateRegistryCapabilities(reporter, registryCapabilities);
validateCloudApiDbContract(reporter, deploy, contracts[3]);
validateCloudApiDbContract(reporter, deploy, contracts[3], contracts[4]);
validateArtifactPublishReport(reporter, optionalReports.artifactPublish, artifactIdentity, targetShortCommit, targetCommit, args.targetRef);
validateEdgeHealthReport(reporter, optionalReports.edgeHealth);
await validateEdgeContracts(reporter, masterEdge);
+67
View File
@@ -3,6 +3,10 @@ import assert from "node:assert/strict";
import { readdir, readFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
DEV_DB_ENV_CONTRACT,
parseDbUrlContract
} from "../internal/cloud/db-contract.mjs";
import {
ENVIRONMENT_DEV,
SERVICE_IDS,
@@ -53,6 +57,14 @@ async function parseDeployManifest(relativePath) {
return doc;
}
async function parseK8sList(relativePath) {
const raw = await readFile(path.join(repoRoot, relativePath), "utf8");
const doc = JSON.parse(raw);
assert.equal(doc.kind, "List", `${relativePath} kind`);
assert.ok(Array.isArray(doc.items), `${relativePath} items`);
return doc;
}
function assertUnique(name, values) {
assert.equal(new Set(values).size, values.length, `${name} must be unique`);
}
@@ -122,6 +134,8 @@ function assertEnvelopeValidation() {
const schemas = await parseJSONSchemaFiles("protocol/schemas");
const deploySchemas = await parseJSONSchemaFiles("deploy");
const deployManifest = await parseDeployManifest("deploy/deploy.json");
const k8sServices = await parseK8sList("deploy/k8s/base/services.yaml");
const k8sWorkloads = await parseK8sList("deploy/k8s/base/workloads.yaml");
const docsByName = new Map([...schemas, ...deploySchemas].map((item) => [path.basename(item.relativePath), item.doc]));
assert.ok(schemas.length >= 13, "expected protocol schemas");
@@ -137,6 +151,7 @@ assert.equal(deployServicesById.has("hwlab-agent-worker"), true);
assert.equal(deployServicesById.has("hwlab-agent-skills"), true);
const cloudApi = deployServicesById.get("hwlab-cloud-api");
const cloudApiWorkloadEnv = getWorkloadEnv(k8sWorkloads, "hwlab-cloud-api");
assert.equal(deployManifest.health.path, "/health/live", "deploy health source path");
assert.equal(deployManifest.publicEndpoints.frontend.url, "http://74.48.78.17:16666", "deploy frontend endpoint");
assert.equal(deployManifest.publicEndpoints.api.url, "http://74.48.78.17:16667", "deploy api endpoint");
@@ -153,8 +168,60 @@ assert.equal(cloudApi.env.HWLAB_IMAGE_TAG, deployManifest.commitId.slice(0, 7),
assert.equal(cloudApi.env.HWLAB_CLOUD_DB_URL, "secretRef:hwlab-cloud-api-dev-db/database-url", "cloud-api DB URL must be a Secret reference placeholder");
assert.equal(cloudApi.env.HWLAB_CLOUD_DB_SSL_MODE, "require", "cloud-api DB SSL mode");
assert.equal(cloudApi.env.HWLAB_CLOUD_DB_CONTRACT, "dev-redacted-presence-only", "cloud-api DB contract marker");
assert.equal(cloudApi.env.HWLAB_CLOUD_DB_SERVICE_NAME, DEV_DB_ENV_CONTRACT.dns.serviceName, "cloud-api DB service name");
assert.equal(cloudApi.env.HWLAB_CLOUD_DB_SERVICE_NAMESPACE, DEV_DB_ENV_CONTRACT.dns.namespace, "cloud-api DB service namespace");
assert.equal(cloudApi.env.HWLAB_CLOUD_DB_HOST, DEV_DB_ENV_CONTRACT.dns.host, "cloud-api DB stable host");
assert.equal(cloudApi.env.HWLAB_CLOUD_DB_PORT, String(DEV_DB_ENV_CONTRACT.dns.port), "cloud-api DB stable port");
assert.equal(cloudApiWorkloadEnv.HWLAB_CLOUD_DB_SERVICE_NAME?.value, DEV_DB_ENV_CONTRACT.dns.serviceName, "cloud-api workload DB service name");
assert.equal(cloudApiWorkloadEnv.HWLAB_CLOUD_DB_SERVICE_NAMESPACE?.value, DEV_DB_ENV_CONTRACT.dns.namespace, "cloud-api workload DB service namespace");
assert.equal(cloudApiWorkloadEnv.HWLAB_CLOUD_DB_HOST?.value, DEV_DB_ENV_CONTRACT.dns.host, "cloud-api workload DB host");
assert.equal(cloudApiWorkloadEnv.HWLAB_CLOUD_DB_PORT?.value, String(DEV_DB_ENV_CONTRACT.dns.port), "cloud-api workload DB port");
assertCloudApiDbDnsContract(k8sServices, cloudApi.env);
assert.equal(cloudApi.env.HWLAB_CODE_AGENT_PROVIDER, "openai", "cloud-api Code Agent provider");
assert.equal(cloudApi.env.HWLAB_CODE_AGENT_MODEL, "gpt-5.5", "cloud-api Code Agent model");
assert.equal(cloudApi.env.OPENAI_API_KEY, "secretRef:hwlab-code-agent-provider/openai-api-key", "cloud-api Code Agent OpenAI key must be a Secret reference placeholder");
console.log(`validated ${schemas.length + deploySchemas.length} JSON files and ${SERVICE_IDS.length} service ids`);
function listItems(document) {
return document?.kind === "List" ? document.items ?? [] : [document].filter(Boolean);
}
function getWorkloadEnv(workloads, serviceId) {
for (const item of listItems(workloads)) {
const itemServiceId =
item?.metadata?.labels?.["hwlab.pikastech.local/service-id"] ||
item?.spec?.template?.metadata?.labels?.["hwlab.pikastech.local/service-id"] ||
item?.metadata?.name;
if (itemServiceId !== serviceId) {
continue;
}
const container = item?.spec?.template?.spec?.containers?.find((entry) => entry.name === serviceId);
return Object.fromEntries((container?.env ?? []).map((entry) => [entry.name, entry]));
}
return {};
}
function assertCloudApiDbDnsContract(services, env) {
const dns = DEV_DB_ENV_CONTRACT.dns;
const service = listItems(services).find((item) => item?.metadata?.name === dns.serviceName);
assert.ok(service, `deploy/k8s/base/services.yaml must define Service/${dns.serviceName}`);
assert.equal(service.metadata.namespace, dns.namespace, "cloud-api DB Service namespace");
assert.equal(service.kind, "Service", "cloud-api DB Service kind");
assert.equal(service.spec?.type, "ClusterIP", "cloud-api DB Service type");
assert.equal(service.spec?.selector, undefined, "cloud-api DB Service must be selectorless until DB endpoints are provisioned");
assert.equal(service.metadata?.labels?.["hwlab.pikastech.local/source-contract"], "db-dns", "cloud-api DB Service source-contract label");
const port = service.spec?.ports?.find((entry) => entry.name === dns.portName);
assert.ok(port, `Service/${dns.serviceName} must expose ${dns.portName}`);
assert.equal(port.port, dns.port, "cloud-api DB Service port");
assert.equal(port.targetPort, dns.portName, "cloud-api DB Service targetPort");
const expectedHost = `${dns.serviceName}.${dns.namespace}.svc.cluster.local`;
assert.equal(dns.host, expectedHost, "DB DNS contract host matches service namespace");
assert.equal(env.HWLAB_CLOUD_DB_HOST, expectedHost, "cloud-api DB host env matches Service DNS");
assert.equal(env.HWLAB_CLOUD_DB_SERVICE_NAME, service.metadata.name, "cloud-api DB service env matches Service name");
assert.equal(env.HWLAB_CLOUD_DB_SERVICE_NAMESPACE, service.metadata.namespace, "cloud-api DB namespace env matches Service namespace");
assert.equal(env.HWLAB_CLOUD_DB_PORT, String(port.port), "cloud-api DB port env matches Service port");
const secretUrl = parseDbUrlContract(`postgres://${env.HWLAB_CLOUD_DB_HOST}:${env.HWLAB_CLOUD_DB_PORT}/hwlab`);
assert.equal(secretUrl.ok, true, "cloud-api DB stable host/port must form a supported postgres URL");
}
+27 -2
View File
@@ -3,6 +3,7 @@ import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { DEV_DB_ENV_CONTRACT } 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)), "..");
@@ -13,6 +14,7 @@ const forbiddenRuntimeSubstitutes = Object.freeze([
"provider-gateway",
"microservice-proxy"
]);
const dbDnsComponent = "cloud-api-db";
async function readText(relativePath) {
return readFile(path.join(repoRoot, relativePath), "utf8");
@@ -130,6 +132,24 @@ function assertMetadataServiceId(resource, expectedServiceId, label) {
assert.equal(labels["app.kubernetes.io/name"], expectedServiceId, `${label} app name label`);
}
function assertDbDnsService(service) {
const dns = DEV_DB_ENV_CONTRACT.dns;
assert.equal(service.metadata?.name, dns.serviceName, "DB DNS Service name");
assertDevNamespace(service.metadata?.namespace, "DB DNS Service namespace");
assert.equal(service.metadata?.labels?.["app.kubernetes.io/name"], dbDnsComponent, "DB DNS app label");
assert.equal(service.metadata?.labels?.["hwlab.pikastech.local/component"], dbDnsComponent, "DB DNS component label");
assert.equal(service.metadata?.labels?.["hwlab.pikastech.local/source-contract"], "db-dns", "DB DNS source-contract label");
assert.equal(service.metadata?.labels?.[serviceIdLabel], undefined, "DB DNS Service is not a frozen HWLAB runtime service-id");
assert.equal(service.spec?.type, "ClusterIP", "DB DNS Service type");
assert.equal(service.spec?.selector, undefined, "DB DNS Service stays selectorless until endpoints are provisioned");
const ports = service.spec?.ports;
assertArray(ports, "DB DNS Service ports");
assert.equal(ports.length, 1, "DB DNS Service exposes one port");
assert.equal(ports[0].name, dns.portName, "DB DNS Service port name");
assert.equal(ports[0].port, dns.port, "DB DNS Service port");
assert.equal(ports[0].targetPort, dns.portName, "DB DNS Service targetPort");
}
function assertTemplateServiceId(workload, expectedServiceId, label) {
const templateLabels = workload.spec?.template?.metadata?.labels;
assertObject(templateLabels, `${label}.spec.template.metadata.labels`);
@@ -291,11 +311,16 @@ function assertServices(services, guard) {
assert.equal(services.kind, "List", "services kind");
assertArray(services.items, "services items");
const serviceIds = services.items.map((item) => item.metadata?.labels?.[serviceIdLabel]);
const runtimeServices = services.items.filter((item) => item.metadata?.name !== dbDnsComponent);
const serviceIds = runtimeServices.map((item) => item.metadata?.labels?.[serviceIdLabel]);
assertUnique(serviceIds, "k8s service ids");
assertSameMembers(serviceIds, guard.requiredK3sSkeleton.requiredServiceServiceIds, "k8s service ids");
for (const service of services.items) {
const dbDnsService = services.items.find((item) => item.metadata?.name === dbDnsComponent);
assert.ok(dbDnsService, "k8s services must include selectorless cloud-api-db DNS Service");
assertDbDnsService(dbDnsService);
for (const service of runtimeServices) {
const serviceId = service.metadata?.labels?.[serviceIdLabel];
assertServiceId(serviceId, `k8s service ${service.metadata?.name}`);
assertMetadataServiceId(service, serviceId, `k8s service ${serviceId}`);