fix: preserve secret-url DB readiness authority
This commit is contained in:
+4
-3
@@ -18,9 +18,10 @@ 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`. 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
|
||||
`HWLAB_CLOUD_DB_SSL_MODE=require`. Runtime DB readiness dials the redacted
|
||||
host parsed from that Secret URL. `cloud-api-db` is only an optional desired
|
||||
alias until Service plus Endpoint/EndpointSlice ownership and rollout/apply
|
||||
contract exist; 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
|
||||
|
||||
@@ -196,10 +196,6 @@
|
||||
"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_CLOUD_RUNTIME_ADAPTER": "postgres",
|
||||
"HWLAB_CLOUD_RUNTIME_DURABLE": "true",
|
||||
"HWLAB_CODE_AGENT_PROVIDER": "openai",
|
||||
|
||||
@@ -371,22 +371,6 @@
|
||||
"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"
|
||||
},
|
||||
"HWLAB_CLOUD_RUNTIME_ADAPTER": {
|
||||
"type": "string",
|
||||
"const": "postgres"
|
||||
|
||||
@@ -27,29 +27,6 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"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",
|
||||
|
||||
@@ -81,22 +81,6 @@
|
||||
"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_CLOUD_RUNTIME_ADAPTER",
|
||||
"value": "postgres"
|
||||
|
||||
@@ -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, 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-api-db": "DEV DB config gate requires HWLAB_CLOUD_DB_URL from Secret hwlab-cloud-api-dev-db/database-url and HWLAB_CLOUD_DB_SSL_MODE=require. Runtime readiness dials the redacted host parsed from the Secret URL and reports endpointSource=secret-url-host. cloud-api-db is an optional desired alias only until this repo owns Service plus Endpoint or EndpointSlice manifests and rollout/apply contract; 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",
|
||||
|
||||
@@ -61,14 +61,20 @@ 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.
|
||||
The runtime DB readiness authority is the host parsed from the injected
|
||||
`HWLAB_CLOUD_DB_URL` Secret value. Health reports this as
|
||||
`db.endpointSource: "secret-url-host"` without printing the host, username,
|
||||
password, or full URL. The `cloud-api-db` name is only an optional future alias;
|
||||
it is not the TCP probe target and a missing alias must not reverse-fail live DB
|
||||
success from the Secret URL host.
|
||||
`.invalid` hosts, including `hwlab-dev-db.invalid`, are forbidden as DEV
|
||||
runtime targets and may be used only as negative fixtures.
|
||||
|
||||
- `db.configReady`: required env names are present.
|
||||
- `db.endpointSource`: `secret-url-host`, the authoritative redacted TCP dial
|
||||
target parsed from `HWLAB_CLOUD_DB_URL`.
|
||||
- `db.optionalPublicDnsAlias`: diagnostic metadata for the non-authoritative
|
||||
`cloud-api-db` alias; `requiredForReadiness` and `usedForProbe` stay false.
|
||||
- `db.connectionAttempted`: the runtime tried the live connection path.
|
||||
- `db.connectionResult`: redacted classifier such as `connected`, `refused`,
|
||||
`timeout`, `dns_error`, `invalid_url`, or `unsupported_protocol`.
|
||||
|
||||
@@ -228,10 +228,11 @@ 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 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.
|
||||
`HWLAB_CLOUD_DB_SSL_MODE=require`. Runtime readiness dials the redacted host
|
||||
parsed from that Secret URL. `cloud-api-db` remains an optional desired alias,
|
||||
not a readiness gate or TCP probe target, until this repo owns its
|
||||
Service/Endpoint rollout contract. 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.
|
||||
|
||||
+13
-6
@@ -26,17 +26,23 @@ forwarding checks.
|
||||
`db` in the cloud-api health payload is a redacted configuration gate. It
|
||||
reports required env names, presence, missing names, and Secret reference names
|
||||
only. Runtime health additionally reports redacted DB readiness layers:
|
||||
`configReady`, `envInjected`, `connectionAttempted`, `connectionResult`, and
|
||||
`liveConnected`. It must not expose a connection string, token, password, DB
|
||||
host, username, or claim fixture output as live DB evidence.
|
||||
`configReady`, `envInjected`, `endpointSource`, `connectionAttempted`,
|
||||
`connectionResult`, and `liveConnected`. It must not expose a connection
|
||||
string, token, password, DB host, username, or claim fixture output as live DB
|
||||
evidence.
|
||||
|
||||
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`.
|
||||
- Runtime TCP dial target: the redacted host parsed from
|
||||
`HWLAB_CLOUD_DB_URL`, reported as `endpointSource="secret-url-host"`.
|
||||
|
||||
`cloud-api-db` is only an optional desired alias. Until this repo owns the
|
||||
Service plus Endpoint or EndpointSlice manifests and rollout/apply contract, it
|
||||
is not a readiness authority, is not the TCP probe target, and a missing alias
|
||||
must not reverse-fail live DB success from the Secret URL host.
|
||||
|
||||
When the Secret/env is absent, the health JSON should keep
|
||||
`db.connected: false`, set `db.status: "blocked"`, and list the missing env
|
||||
@@ -46,7 +52,8 @@ present but no live DB probe is attempted, the gate remains blocked with
|
||||
`liveConnected` is false, the gate remains blocked/degraded with the redacted
|
||||
connection classifier. `connectionResult: "dns_error"` with
|
||||
`classification: "dns_resolution_failed"` means DB endpoint DNS failed from the
|
||||
cloud-api runtime. Env presence alone is not live DB evidence.
|
||||
cloud-api runtime for the Secret URL host. Env presence alone is not live DB
|
||||
evidence.
|
||||
|
||||
The Code Agent provider Secret for #143 is a separate blocker. The edge-health
|
||||
report may record only key-presence evidence for
|
||||
|
||||
@@ -67,9 +67,10 @@ 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`. 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`.
|
||||
`HWLAB_CLOUD_DB_SSL_MODE=require`. Runtime readiness dials the redacted host
|
||||
parsed from that Secret URL. `cloud-api-db` is an optional desired alias only
|
||||
until this repo owns Service plus Endpoint or EndpointSlice manifests and
|
||||
rollout/apply contract.
|
||||
- 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
|
||||
@@ -114,6 +115,7 @@ rerunning the preflight.
|
||||
The DB gate has two common blocked scopes:
|
||||
|
||||
- `cloud-api-db-env-contract`: manifest/Kubernetes placeholders are incomplete.
|
||||
The optional `cloud-api-db` alias is not part of this readiness gate.
|
||||
- `cloud-api-db-health-gate`: runtime env presence is missing. The next task is
|
||||
to configure the DEV Secret/env names, not to print or commit any secret
|
||||
value.
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
| 用户反馈分流规则 | [user-feedback-triage.md](user-feedback-triage.md) |
|
||||
| 文档治理与 docs-spec 入库规则 | [documentation-governance.md](documentation-governance.md) |
|
||||
| 架构和 M3 上位约束 | [architecture.md](architecture.md) |
|
||||
| DEV 运行态、端口、k3s、DB DNS 和环境边界 | [dev-runtime-boundary.md](dev-runtime-boundary.md) |
|
||||
| DEV 运行态、端口、k3s、DB readiness 和环境边界 | [dev-runtime-boundary.md](dev-runtime-boundary.md) |
|
||||
| 部署正规化、artifact 发布、回滚和 Cloud Web rollout | [deployment-publish.md](deployment-publish.md) |
|
||||
| Cloud Workbench 默认界面和 UX 边界 | [cloud-workbench.md](cloud-workbench.md) |
|
||||
| Code Agent chat 同源通道 readiness 与真实回复判定 | [code-agent-chat-readiness.md](code-agent-chat-readiness.md) |
|
||||
|
||||
@@ -54,35 +54,49 @@ 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
|
||||
## Cloud API DB Readiness Authority
|
||||
|
||||
`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:
|
||||
Runtime DB readiness uses the redacted host parsed from the injected
|
||||
`HWLAB_CLOUD_DB_URL` Secret value:
|
||||
|
||||
| Field | Value |
|
||||
| --- | --- |
|
||||
| Authority source | `secret-url-host` |
|
||||
| Authority env | `HWLAB_CLOUD_DB_URL` |
|
||||
| Required for readiness | `true` |
|
||||
| Probe target | redacted host and port parsed from the Secret URL |
|
||||
|
||||
Health output may report `endpointSource: secret-url-host`, DNS/TCP result,
|
||||
`liveConnected`, and `liveDbEvidence`, but must keep the DB endpoint and Secret
|
||||
value redacted.
|
||||
|
||||
The non-secret `cloud-api-db` name is an optional desired alias:
|
||||
|
||||
| Field | Value |
|
||||
| --- | --- |
|
||||
| Source | `optional-public-dns-alias` |
|
||||
| Service | `cloud-api-db` |
|
||||
| Namespace | `hwlab-dev` |
|
||||
| Host | `cloud-api-db.hwlab-dev.svc.cluster.local` |
|
||||
| Port | `5432` |
|
||||
| Port name | `postgres` |
|
||||
| Required for readiness | `false` |
|
||||
| Used for probe | `false` |
|
||||
|
||||
`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.
|
||||
Missing `Service/cloud-api-db`, Endpoint, EndpointSlice, or alias env vars must
|
||||
not reverse-fail live DB success from the Secret URL host. A future PR may make
|
||||
that alias operational only if it also owns the Service plus Endpoint or
|
||||
EndpointSlice manifests and the rollout/apply contract.
|
||||
|
||||
`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.
|
||||
Source validation checks `HWLAB_CLOUD_DB_URL`, `HWLAB_CLOUD_DB_SSL_MODE`,
|
||||
`endpointSource=secret-url-host`, and `liveDbEvidence=true` with secret values
|
||||
redacted. It must not require `HWLAB_CLOUD_DB_SERVICE_NAME`,
|
||||
`HWLAB_CLOUD_DB_SERVICE_NAMESPACE`, `HWLAB_CLOUD_DB_HOST`, or
|
||||
`HWLAB_CLOUD_DB_PORT` as current readiness inputs.
|
||||
|
||||
The DB readiness contract is layered:
|
||||
|
||||
|
||||
@@ -16,12 +16,22 @@ export const DEV_DB_ENV_CONTRACT = Object.freeze({
|
||||
secretKey: "database-url"
|
||||
})
|
||||
]),
|
||||
endpointAuthority: Object.freeze({
|
||||
source: "secret-url-host",
|
||||
env: "HWLAB_CLOUD_DB_URL",
|
||||
requiredForReadiness: true,
|
||||
valueRedacted: true,
|
||||
endpointRedacted: true
|
||||
}),
|
||||
dns: Object.freeze({
|
||||
source: "optional-public-dns-alias",
|
||||
serviceName: "cloud-api-db",
|
||||
namespace: "hwlab-dev",
|
||||
host: "cloud-api-db.hwlab-dev.svc.cluster.local",
|
||||
port: 5432,
|
||||
portName: "postgres"
|
||||
portName: "postgres",
|
||||
requiredForReadiness: false,
|
||||
usedForProbe: false
|
||||
}),
|
||||
readinessLayers: Object.freeze([
|
||||
"dns",
|
||||
@@ -30,11 +40,7 @@ export const DEV_DB_ENV_CONTRACT = Object.freeze({
|
||||
"schema"
|
||||
]),
|
||||
nonSecretDefaults: Object.freeze({
|
||||
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"
|
||||
HWLAB_CLOUD_DB_SSL_MODE: "require"
|
||||
}),
|
||||
forbidden: Object.freeze({
|
||||
prodAllowed: false,
|
||||
@@ -68,6 +74,7 @@ export function buildDbHealthContract(env = process.env) {
|
||||
const connected = false;
|
||||
const connectionChecked = false;
|
||||
const status = configReady ? "degraded" : "blocked";
|
||||
const endpoint = buildEndpointDiagnostics(env);
|
||||
|
||||
return {
|
||||
contractVersion: DEV_DB_ENV_CONTRACT.contractVersion,
|
||||
@@ -84,6 +91,9 @@ export function buildDbHealthContract(env = process.env) {
|
||||
mode: configReady ? "configured_without_live_connection_attempt" : "not_configured",
|
||||
fields,
|
||||
missingEnv,
|
||||
endpoint,
|
||||
endpointSource: endpoint.authoritative.source,
|
||||
optionalPublicDnsAlias: endpoint.optionalPublicDnsAlias,
|
||||
secretRefs: DEV_DB_ENV_CONTRACT.secretRefs.map(({ env: envName, secretName, secretKey }) => ({
|
||||
env: envName,
|
||||
secretName,
|
||||
@@ -134,6 +144,7 @@ export function buildDbEnvManifestPlaceholder() {
|
||||
return {
|
||||
requiredEnv: [...DEV_DB_ENV_CONTRACT.requiredEnv],
|
||||
secretRefs: DEV_DB_ENV_CONTRACT.secretRefs.map((item) => ({ ...item })),
|
||||
endpointAuthority: { ...DEV_DB_ENV_CONTRACT.endpointAuthority },
|
||||
dns: { ...DEV_DB_ENV_CONTRACT.dns },
|
||||
readinessLayers: [...DEV_DB_ENV_CONTRACT.readinessLayers],
|
||||
forbiddenRuntimeHosts: [...DEV_DB_ENV_CONTRACT.forbidden.invalidRuntimeHosts],
|
||||
@@ -184,6 +195,8 @@ export function summarizeDbContract(db = buildDbHealthContract()) {
|
||||
redacted
|
||||
})
|
||||
),
|
||||
endpointSource: db.endpointSource ?? db.endpoint?.authoritative?.source ?? db.connection?.endpointSource ?? DEV_DB_ENV_CONTRACT.endpointAuthority.source,
|
||||
optionalPublicDnsAlias: summarizeOptionalPublicDnsAlias(db.optionalPublicDnsAlias ?? db.endpoint?.optionalPublicDnsAlias),
|
||||
connection: summarizeConnection(db.connection),
|
||||
redaction: summarizeRedaction(db.redaction),
|
||||
readinessLayers: buildReadinessLayers(db.connection),
|
||||
@@ -206,6 +219,7 @@ function buildNotAttemptedConnection(configReady, missingEnv) {
|
||||
networkAttempted: false,
|
||||
result: configReady ? "not_attempted" : "not_attempted_missing_env",
|
||||
classification: configReady ? "configured_no_live_attempt" : "missing_runtime_env",
|
||||
endpointSource: DEV_DB_ENV_CONTRACT.endpointAuthority.source,
|
||||
probeType: "tcp-connect",
|
||||
endpointRedacted: true,
|
||||
valueRedacted: true,
|
||||
@@ -235,6 +249,7 @@ async function probeDbConnection(env, options) {
|
||||
networkAttempted: false,
|
||||
result: parsed.result,
|
||||
classification: parsed.classification,
|
||||
endpointSource: DEV_DB_ENV_CONTRACT.endpointAuthority.source,
|
||||
probeType: "tcp-connect",
|
||||
endpointRedacted: true,
|
||||
valueRedacted: true,
|
||||
@@ -248,7 +263,8 @@ async function probeDbConnection(env, options) {
|
||||
return tcpConnect({
|
||||
host: parsed.host,
|
||||
port: parsed.port,
|
||||
timeoutMs
|
||||
timeoutMs,
|
||||
endpointSource: DEV_DB_ENV_CONTRACT.endpointAuthority.source
|
||||
});
|
||||
}
|
||||
|
||||
@@ -303,7 +319,7 @@ export function parseDbUrlContract(rawUrl) {
|
||||
}
|
||||
}
|
||||
|
||||
function tcpConnect({ host, port, timeoutMs }) {
|
||||
function tcpConnect({ host, port, timeoutMs, endpointSource }) {
|
||||
return new Promise((resolve) => {
|
||||
const startedAt = Date.now();
|
||||
const socket = net.connect({ host, port, timeout: timeoutMs });
|
||||
@@ -316,6 +332,7 @@ function tcpConnect({ host, port, timeoutMs }) {
|
||||
resolve({
|
||||
attempted: true,
|
||||
networkAttempted: true,
|
||||
endpointSource,
|
||||
probeType: "tcp-connect",
|
||||
endpointRedacted: true,
|
||||
valueRedacted: true,
|
||||
@@ -397,6 +414,9 @@ function applyConnectionResult(base, connection) {
|
||||
status: liveConnected ? "ready" : "degraded",
|
||||
mode: liveConnected ? "live_connection_ready" : "live_connection_blocked",
|
||||
connection,
|
||||
endpoint: base.endpoint,
|
||||
endpointSource: connection.endpointSource ?? base.endpointSource,
|
||||
optionalPublicDnsAlias: base.optionalPublicDnsAlias,
|
||||
redaction: buildRedactionSafety(connection),
|
||||
safety: {
|
||||
...base.safety,
|
||||
@@ -493,6 +513,7 @@ function summarizeConnection(connection) {
|
||||
networkAttempted: Boolean(connection.networkAttempted),
|
||||
result: connection.result,
|
||||
classification: connection.classification,
|
||||
endpointSource: connection.endpointSource ?? DEV_DB_ENV_CONTRACT.endpointAuthority.source,
|
||||
probeType: connection.probeType,
|
||||
endpointRedacted: true,
|
||||
valueRedacted: true,
|
||||
@@ -502,6 +523,60 @@ function summarizeConnection(connection) {
|
||||
};
|
||||
}
|
||||
|
||||
function buildEndpointDiagnostics(env) {
|
||||
const authority = DEV_DB_ENV_CONTRACT.endpointAuthority;
|
||||
return {
|
||||
authoritative: {
|
||||
source: authority.source,
|
||||
env: authority.env,
|
||||
requiredForReadiness: authority.requiredForReadiness,
|
||||
usedForProbe: true,
|
||||
endpointRedacted: true,
|
||||
valueRedacted: true
|
||||
},
|
||||
optionalPublicDnsAlias: buildOptionalPublicDnsAlias(env)
|
||||
};
|
||||
}
|
||||
|
||||
function buildOptionalPublicDnsAlias(env) {
|
||||
const alias = DEV_DB_ENV_CONTRACT.dns;
|
||||
const serviceNamePresent = env?.HWLAB_CLOUD_DB_SERVICE_NAME === alias.serviceName;
|
||||
const namespacePresent = env?.HWLAB_CLOUD_DB_SERVICE_NAMESPACE === alias.namespace;
|
||||
const hostPresent = env?.HWLAB_CLOUD_DB_HOST === alias.host;
|
||||
const portPresent = env?.HWLAB_CLOUD_DB_PORT === String(alias.port);
|
||||
const envPresent = serviceNamePresent || namespacePresent || hostPresent || portPresent;
|
||||
return {
|
||||
source: alias.source,
|
||||
serviceName: alias.serviceName,
|
||||
namespace: alias.namespace,
|
||||
port: alias.port,
|
||||
portName: alias.portName,
|
||||
requiredForReadiness: false,
|
||||
usedForProbe: false,
|
||||
envPresent,
|
||||
readyGate: "not_readiness_authority",
|
||||
note: envPresent
|
||||
? "optional alias env observed; live DB readiness still follows the redacted Secret URL host"
|
||||
: "optional alias is not configured and is not required for live DB readiness"
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeOptionalPublicDnsAlias(alias) {
|
||||
const fallback = buildOptionalPublicDnsAlias({});
|
||||
const value = alias ?? fallback;
|
||||
return {
|
||||
source: value.source ?? fallback.source,
|
||||
serviceName: value.serviceName ?? fallback.serviceName,
|
||||
namespace: value.namespace ?? fallback.namespace,
|
||||
port: value.port ?? fallback.port,
|
||||
portName: value.portName ?? fallback.portName,
|
||||
requiredForReadiness: value.requiredForReadiness === true ? true : false,
|
||||
usedForProbe: value.usedForProbe === true ? true : false,
|
||||
envPresent: value.envPresent === true,
|
||||
readyGate: value.readyGate ?? "not_readiness_authority"
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeRedaction(redaction) {
|
||||
return {
|
||||
valuesRedacted: redaction?.valuesRedacted !== false,
|
||||
|
||||
@@ -64,6 +64,10 @@ test("system.health includes the redacted DEV DB env contract", async () => {
|
||||
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.endpointSource, "secret-url-host");
|
||||
assert.equal(response.result.db.optionalPublicDnsAlias.source, "optional-public-dns-alias");
|
||||
assert.equal(response.result.db.optionalPublicDnsAlias.requiredForReadiness, false);
|
||||
assert.equal(response.result.db.optionalPublicDnsAlias.usedForProbe, false);
|
||||
assert.equal(response.result.db.redaction.valuesRedacted, true);
|
||||
assert.equal(response.result.db.redaction.secretMaterialRead, false);
|
||||
assert.equal(response.result.db.safety.liveDbEvidence, false);
|
||||
|
||||
@@ -38,6 +38,13 @@ test("cloud api exposes /health, /health/live, and /live probes", async () => {
|
||||
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.endpointSource, "secret-url-host");
|
||||
assert.equal(healthPayload.db.endpoint.authoritative.source, "secret-url-host");
|
||||
assert.equal(healthPayload.db.endpoint.authoritative.env, "HWLAB_CLOUD_DB_URL");
|
||||
assert.equal(healthPayload.db.endpoint.authoritative.usedForProbe, true);
|
||||
assert.equal(healthPayload.db.optionalPublicDnsAlias.source, "optional-public-dns-alias");
|
||||
assert.equal(healthPayload.db.optionalPublicDnsAlias.requiredForReadiness, false);
|
||||
assert.equal(healthPayload.db.optionalPublicDnsAlias.usedForProbe, false);
|
||||
assert.equal(healthPayload.db.redaction.valuesRedacted, true);
|
||||
assert.equal(healthPayload.db.redaction.secretMaterialRead, false);
|
||||
assert.equal(healthPayload.db.safety.liveDbEvidence, false);
|
||||
@@ -94,11 +101,21 @@ test("cloud api exposes /health, /health/live, and /live probes", async () => {
|
||||
test("cloud api health reports DB env presence and live connection classification without leaking values", async () => {
|
||||
const originalUrl = process.env.HWLAB_CLOUD_DB_URL;
|
||||
const originalSslMode = process.env.HWLAB_CLOUD_DB_SSL_MODE;
|
||||
const originalAliasEnv = {
|
||||
HWLAB_CLOUD_DB_SERVICE_NAME: process.env.HWLAB_CLOUD_DB_SERVICE_NAME,
|
||||
HWLAB_CLOUD_DB_SERVICE_NAMESPACE: process.env.HWLAB_CLOUD_DB_SERVICE_NAMESPACE,
|
||||
HWLAB_CLOUD_DB_HOST: process.env.HWLAB_CLOUD_DB_HOST,
|
||||
HWLAB_CLOUD_DB_PORT: process.env.HWLAB_CLOUD_DB_PORT
|
||||
};
|
||||
const fakeDb = createTcpServer((socket) => socket.end());
|
||||
await new Promise((resolve) => fakeDb.listen(0, "127.0.0.1", resolve));
|
||||
const dbPort = fakeDb.address().port;
|
||||
process.env.HWLAB_CLOUD_DB_URL = `postgres://hwlab_test@127.0.0.1:${dbPort}/hwlab`;
|
||||
process.env.HWLAB_CLOUD_DB_SSL_MODE = "require";
|
||||
delete process.env.HWLAB_CLOUD_DB_SERVICE_NAME;
|
||||
delete process.env.HWLAB_CLOUD_DB_SERVICE_NAMESPACE;
|
||||
delete process.env.HWLAB_CLOUD_DB_HOST;
|
||||
delete process.env.HWLAB_CLOUD_DB_PORT;
|
||||
|
||||
const server = createCloudApiServer();
|
||||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
@@ -115,6 +132,14 @@ test("cloud api health reports DB env presence and live connection classificatio
|
||||
assert.equal(payload.db.connectionChecked, true);
|
||||
assert.equal(payload.db.connectionAttempted, true);
|
||||
assert.equal(payload.db.connectionResult, "connected");
|
||||
assert.equal(payload.db.endpointSource, "secret-url-host");
|
||||
assert.equal(payload.db.connection.endpointSource, "secret-url-host");
|
||||
assert.equal(payload.db.endpoint.authoritative.source, "secret-url-host");
|
||||
assert.equal(payload.db.endpoint.authoritative.env, "HWLAB_CLOUD_DB_URL");
|
||||
assert.equal(payload.db.optionalPublicDnsAlias.source, "optional-public-dns-alias");
|
||||
assert.equal(payload.db.optionalPublicDnsAlias.requiredForReadiness, false);
|
||||
assert.equal(payload.db.optionalPublicDnsAlias.usedForProbe, false);
|
||||
assert.equal(payload.db.optionalPublicDnsAlias.envPresent, false);
|
||||
assert.equal(payload.db.configReady, true);
|
||||
assert.equal(payload.db.ready, true);
|
||||
assert.equal(payload.db.evidence, "live_db_tcp_connection_ready");
|
||||
@@ -131,6 +156,7 @@ test("cloud api health reports DB env presence and live connection classificatio
|
||||
assert.equal(JSON.stringify(payload.db).includes("password"), false);
|
||||
assert.equal(JSON.stringify(payload.db).includes("127.0.0.1"), false);
|
||||
assert.equal(JSON.stringify(payload.db).includes(String(dbPort)), false);
|
||||
assert.equal(JSON.stringify(payload.db).includes("cloud-api-db.hwlab-dev.svc.cluster.local"), false);
|
||||
assert.equal(payload.db.fields.find((field) => field.name === "HWLAB_CLOUD_DB_URL").redacted, true);
|
||||
assert.equal(payload.db.connection.endpointRedacted, true);
|
||||
} finally {
|
||||
@@ -144,6 +170,13 @@ test("cloud api health reports DB env presence and live connection classificatio
|
||||
} else {
|
||||
process.env.HWLAB_CLOUD_DB_SSL_MODE = originalSslMode;
|
||||
}
|
||||
for (const [name, value] of Object.entries(originalAliasEnv)) {
|
||||
if (value === undefined) {
|
||||
delete process.env[name];
|
||||
} else {
|
||||
process.env[name] = value;
|
||||
}
|
||||
}
|
||||
await new Promise((resolve, reject) => {
|
||||
server.close((error) => (error ? reject(error) : resolve()));
|
||||
});
|
||||
@@ -253,6 +286,8 @@ test("cloud api health does not treat DB env presence-only as live readiness", a
|
||||
assert.equal(payload.db.connectionAttempted, true);
|
||||
assert.equal(payload.db.connectionResult, "forbidden_runtime_host");
|
||||
assert.equal(payload.db.evidence, "live_db_tcp_connection_blocked");
|
||||
assert.equal(payload.db.endpointSource, "secret-url-host");
|
||||
assert.equal(payload.db.connection.endpointSource, "secret-url-host");
|
||||
assert.equal(payload.db.connection.networkAttempted, false);
|
||||
assert.equal(payload.db.connection.classification, "db_url_forbidden_invalid_host");
|
||||
assert.equal(payload.db.readinessLayers.dns.status, "not_proven");
|
||||
|
||||
@@ -90,6 +90,10 @@ async function run() {
|
||||
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.endpointSource, "secret-url-host");
|
||||
assert.equal(health.body.db.optionalPublicDnsAlias.source, "optional-public-dns-alias");
|
||||
assert.equal(health.body.db.optionalPublicDnsAlias.requiredForReadiness, false);
|
||||
assert.equal(health.body.db.optionalPublicDnsAlias.usedForProbe, false);
|
||||
assert.equal(health.body.db.ready, false);
|
||||
assert.deepEqual(health.body.db.missingEnv, ["HWLAB_CLOUD_DB_URL", "HWLAB_CLOUD_DB_SSL_MODE"]);
|
||||
assert.equal(health.body.runtime.adapter, "memory");
|
||||
|
||||
@@ -145,7 +145,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 ?? {});
|
||||
validateCloudApiDbSource(ctx, cloudApi?.env ?? {});
|
||||
validateCloudApiCodeAgentSource(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");
|
||||
@@ -189,13 +189,23 @@ 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 validateCloudApiDbSource(ctx, env) {
|
||||
const alias = DEV_DB_ENV_CONTRACT.dns;
|
||||
expectEqual(ctx, env.HWLAB_CLOUD_DB_URL, "secretRef:hwlab-cloud-api-dev-db/database-url", "$.services.hwlab-cloud-api.env.HWLAB_CLOUD_DB_URL", "cloud API DB URL Secret reference");
|
||||
expectEqual(ctx, env.HWLAB_CLOUD_DB_SSL_MODE, "require", "$.services.hwlab-cloud-api.env.HWLAB_CLOUD_DB_SSL_MODE", "cloud API DB SSL mode");
|
||||
expectEqual(ctx, env.HWLAB_CLOUD_DB_CONTRACT, "dev-redacted-presence-only", "$.services.hwlab-cloud-api.env.HWLAB_CLOUD_DB_CONTRACT", "cloud API DB redacted contract marker");
|
||||
expectEqual(ctx, DEV_DB_ENV_CONTRACT.endpointAuthority.source, "secret-url-host", "internal/cloud/db-contract.mjs.endpointAuthority.source", "cloud API DB runtime authority source");
|
||||
expectEqual(ctx, DEV_DB_ENV_CONTRACT.endpointAuthority.env, "HWLAB_CLOUD_DB_URL", "internal/cloud/db-contract.mjs.endpointAuthority.env", "cloud API DB runtime authority env");
|
||||
expectEqual(ctx, alias.requiredForReadiness, false, "internal/cloud/db-contract.mjs.dns.requiredForReadiness", "cloud API DB DNS alias readiness requirement");
|
||||
expectEqual(ctx, alias.usedForProbe, false, "internal/cloud/db-contract.mjs.dns.usedForProbe", "cloud API DB DNS alias probe source");
|
||||
for (const name of [
|
||||
"HWLAB_CLOUD_DB_SERVICE_NAME",
|
||||
"HWLAB_CLOUD_DB_SERVICE_NAMESPACE",
|
||||
"HWLAB_CLOUD_DB_HOST",
|
||||
"HWLAB_CLOUD_DB_PORT"
|
||||
]) {
|
||||
expectEqual(ctx, env[name], undefined, `$.services.hwlab-cloud-api.env.${name}`, `${name} is optional and not a readiness hard requirement`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateCloudApiCodeAgentSource(ctx, env) {
|
||||
@@ -317,7 +327,7 @@ function validateK3sArtifacts(ctx, source, rendered, services, workloads, health
|
||||
}
|
||||
}
|
||||
|
||||
validateCloudApiDbDnsArtifacts(ctx, services, workloads);
|
||||
validateCloudApiDbOptionalAliasArtifacts(ctx, services, workloads);
|
||||
validateCloudApiCodeAgentArtifacts(ctx, workloads);
|
||||
|
||||
expectEqual(ctx, healthContract.data?.endpoint, source.endpoints.api?.url, "deploy/k8s/dev/health-contract.yaml.data.endpoint", "health contract endpoint");
|
||||
@@ -327,30 +337,20 @@ 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");
|
||||
}
|
||||
}
|
||||
|
||||
function validateCloudApiDbOptionalAliasArtifacts(ctx, services, workloads) {
|
||||
const alias = DEV_DB_ENV_CONTRACT.dns;
|
||||
const service = listItems(services).find((item) => item?.metadata?.name === alias.serviceName);
|
||||
expectEqual(ctx, service, undefined, "deploy/k8s/base/services.yaml.cloud-api-db", "cloud-api-db remains optional until Service/Endpoint ownership and rollout contract exist");
|
||||
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");
|
||||
for (const name of [
|
||||
"HWLAB_CLOUD_DB_SERVICE_NAME",
|
||||
"HWLAB_CLOUD_DB_SERVICE_NAMESPACE",
|
||||
"HWLAB_CLOUD_DB_HOST",
|
||||
"HWLAB_CLOUD_DB_PORT"
|
||||
]) {
|
||||
expectEqual(ctx, env.get(name), undefined, `deploy/k8s/base/workloads.yaml.hwlab-cloud-api.env.${name}`, `${name} is optional and not injected as readiness authority`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateCloudApiCodeAgentArtifacts(ctx, workloads) {
|
||||
|
||||
@@ -502,7 +502,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 and stable DNS readiness through health output, without reading secret values.";
|
||||
return "Configure and verify the DEV cloud-api DB Secret URL host readiness through health output, without reading secret values.";
|
||||
}
|
||||
return blocker.summary;
|
||||
}
|
||||
@@ -749,14 +749,14 @@ async function checkCloudApiDb(deploy, workloads, services, blockers) {
|
||||
const missingSecretRefs = requiredEnv
|
||||
.filter((env) => env.secretRef && !env.secretRef.present)
|
||||
.map((env) => `${env.secretRef.secretName}/${env.secretRef.secretKey}`);
|
||||
const dnsContract = inspectCloudApiDbDnsContract(deployEnv, workloadEnv, services);
|
||||
const dnsContract = inspectCloudApiDbOptionalAliasContract(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/DNS contract is incomplete; missing ${[...missingManifest, ...missingK8s, ...missingSecretRefs, ...missingDnsContract].join(", ")}`
|
||||
`cloud-api DEV DB env/authority contract is incomplete; missing ${[...missingManifest, ...missingK8s, ...missingSecretRefs, ...missingDnsContract].join(", ")}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -804,15 +804,13 @@ async function checkCloudApiDb(deploy, workloads, services, blockers) {
|
||||
valuesRedacted: true,
|
||||
liveDbEvidence: false,
|
||||
fixtureEvidence: false,
|
||||
note: "This check records DB env, Secret reference, and stable DNS Service presence only. It is not live DB evidence."
|
||||
note: "This check records DB env, Secret reference, and Secret URL host authority only. Optional cloud-api-db alias presence is diagnostic and 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;
|
||||
function inspectCloudApiDbOptionalAliasContract(deployEnv, workloadEnv, services) {
|
||||
const alias = DEV_DB_ENV_CONTRACT.dns;
|
||||
const service = listItems(services).find((item) => item?.metadata?.name === alias.serviceName) ?? null;
|
||||
const actual = {
|
||||
deployServiceName: deployEnv.HWLAB_CLOUD_DB_SERVICE_NAME,
|
||||
deployServiceNamespace: deployEnv.HWLAB_CLOUD_DB_SERVICE_NAMESPACE,
|
||||
@@ -822,42 +820,25 @@ function inspectCloudApiDbDnsContract(deployEnv, workloadEnv, services) {
|
||||
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
|
||||
serviceName: service?.metadata?.name
|
||||
};
|
||||
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"}`);
|
||||
const missing = [];
|
||||
for (const [name, value] of Object.entries(actual)) {
|
||||
if (value !== undefined) {
|
||||
missing.push(`${name} must be absent because ${alias.serviceName} is not the readiness authority`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ready: missing.length === 0,
|
||||
host: expectedHost,
|
||||
serviceName: dns.serviceName,
|
||||
namespace: dns.namespace,
|
||||
port: dns.port,
|
||||
selectorless: service ? service.spec?.selector === undefined : false,
|
||||
source: alias.source,
|
||||
serviceName: alias.serviceName,
|
||||
namespace: alias.namespace,
|
||||
port: alias.port,
|
||||
portName: alias.portName,
|
||||
requiredForReadiness: alias.requiredForReadiness,
|
||||
usedForProbe: alias.usedForProbe,
|
||||
servicePresent: Boolean(service),
|
||||
secretMaterialRead: false,
|
||||
liveDbEvidence: false,
|
||||
missing,
|
||||
|
||||
@@ -305,6 +305,7 @@ function inspectCloudApiDbContract(deploy) {
|
||||
networkAttempted: false,
|
||||
result: "manifest_only_not_attempted",
|
||||
classification: "manifest_only",
|
||||
endpointSource: DEV_DB_ENV_CONTRACT.endpointAuthority.source,
|
||||
probeType: "tcp-connect",
|
||||
endpointRedacted: true,
|
||||
valueRedacted: true,
|
||||
@@ -321,6 +322,28 @@ function inspectCloudApiDbContract(deploy) {
|
||||
connectionChecked: false,
|
||||
connectionAttempted: false,
|
||||
connectionResult: connection.result,
|
||||
endpointSource: DEV_DB_ENV_CONTRACT.endpointAuthority.source,
|
||||
endpoint: {
|
||||
authoritative: {
|
||||
source: DEV_DB_ENV_CONTRACT.endpointAuthority.source,
|
||||
env: DEV_DB_ENV_CONTRACT.endpointAuthority.env,
|
||||
requiredForReadiness: true,
|
||||
usedForProbe: true,
|
||||
endpointRedacted: true,
|
||||
valueRedacted: true
|
||||
}
|
||||
},
|
||||
optionalPublicDnsAlias: {
|
||||
source: DEV_DB_ENV_CONTRACT.dns.source,
|
||||
serviceName: DEV_DB_ENV_CONTRACT.dns.serviceName,
|
||||
namespace: DEV_DB_ENV_CONTRACT.dns.namespace,
|
||||
port: DEV_DB_ENV_CONTRACT.dns.port,
|
||||
portName: DEV_DB_ENV_CONTRACT.dns.portName,
|
||||
requiredForReadiness: false,
|
||||
usedForProbe: false,
|
||||
envPresent: false,
|
||||
readyGate: "not_readiness_authority"
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: "HWLAB_CLOUD_DB_URL",
|
||||
@@ -475,9 +498,25 @@ function summarizeRuntimeDbReadiness(db, secretPresence = unknownDbSecretPresenc
|
||||
const connectionResult = db.connectionResult ?? db.connection?.result ?? (connectionAttempted ? "unknown" : "not_attempted");
|
||||
const connectionClassification =
|
||||
db.connection?.classification ?? classificationForRuntimeDb(db, connectionAttempted, connectionResult);
|
||||
const endpointSource =
|
||||
db.endpointSource ?? db.connection?.endpointSource ?? db.endpoint?.authoritative?.source ?? DEV_DB_ENV_CONTRACT.endpointAuthority.source;
|
||||
const optionalPublicDnsAlias = db.optionalPublicDnsAlias ?? db.endpoint?.optionalPublicDnsAlias ?? {
|
||||
source: DEV_DB_ENV_CONTRACT.dns.source,
|
||||
serviceName: DEV_DB_ENV_CONTRACT.dns.serviceName,
|
||||
namespace: DEV_DB_ENV_CONTRACT.dns.namespace,
|
||||
port: DEV_DB_ENV_CONTRACT.dns.port,
|
||||
portName: DEV_DB_ENV_CONTRACT.dns.portName,
|
||||
requiredForReadiness: false,
|
||||
usedForProbe: false,
|
||||
envPresent: false,
|
||||
readyGate: "not_readiness_authority"
|
||||
};
|
||||
const liveConnected = Boolean(db.liveConnected ?? db.connected ?? db.ready);
|
||||
const blockerMessage = liveConnected ? null : db.blocker ?? blockerForRuntimeDb(db, connectionAttempted, connectionResult);
|
||||
const status = liveConnected ? "pass" : "blocked";
|
||||
const authorityReady = endpointSource === DEV_DB_ENV_CONTRACT.endpointAuthority.source;
|
||||
const blockerMessage = liveConnected && authorityReady
|
||||
? null
|
||||
: db.blocker ?? (authorityReady ? blockerForRuntimeDb(db, connectionAttempted, connectionResult) : "DB readiness did not use the Secret URL host authority");
|
||||
const status = liveConnected && authorityReady ? "pass" : "blocked";
|
||||
const secretRef = Array.isArray(db.secretRefs) ? db.secretRefs[0] : null;
|
||||
|
||||
return {
|
||||
@@ -495,6 +534,18 @@ function summarizeRuntimeDbReadiness(db, secretPresence = unknownDbSecretPresenc
|
||||
connectionAttempted,
|
||||
connectionResult,
|
||||
connectionClassification,
|
||||
endpointSource,
|
||||
optionalPublicDnsAlias: {
|
||||
source: optionalPublicDnsAlias.source ?? DEV_DB_ENV_CONTRACT.dns.source,
|
||||
serviceName: optionalPublicDnsAlias.serviceName ?? DEV_DB_ENV_CONTRACT.dns.serviceName,
|
||||
namespace: optionalPublicDnsAlias.namespace ?? DEV_DB_ENV_CONTRACT.dns.namespace,
|
||||
port: optionalPublicDnsAlias.port ?? DEV_DB_ENV_CONTRACT.dns.port,
|
||||
portName: optionalPublicDnsAlias.portName ?? DEV_DB_ENV_CONTRACT.dns.portName,
|
||||
requiredForReadiness: optionalPublicDnsAlias.requiredForReadiness === true,
|
||||
usedForProbe: optionalPublicDnsAlias.usedForProbe === true,
|
||||
envPresent: optionalPublicDnsAlias.envPresent === true,
|
||||
readyGate: optionalPublicDnsAlias.readyGate ?? "not_readiness_authority"
|
||||
},
|
||||
liveConnected,
|
||||
valuesRedacted: db.redaction?.valuesRedacted !== false && db.safety?.valuesRedacted === true,
|
||||
secretMaterialRead: false,
|
||||
@@ -507,6 +558,7 @@ function summarizeRuntimeDbReadiness(db, secretPresence = unknownDbSecretPresenc
|
||||
connectionAttempted,
|
||||
connectionResult,
|
||||
classification: connectionClassification,
|
||||
endpointSource,
|
||||
liveConnected,
|
||||
endpointRedacted: db.connection?.endpointRedacted !== false,
|
||||
valueRedacted: true
|
||||
@@ -906,7 +958,7 @@ function classifyMilestoneImpact(edgeHealth) {
|
||||
return {
|
||||
M3: {
|
||||
status: "separate_blocker",
|
||||
summary: "M3 still requires real DEV hardware trusted-loop operation/trace/audit/evidence; DB DNS failure and #143 provider Secret absence do not prove or clear M3."
|
||||
summary: "M3 still requires real DEV hardware trusted-loop operation/trace/audit/evidence; DB Secret URL host readiness failure and #143 provider Secret absence do not prove or clear M3."
|
||||
},
|
||||
M4: {
|
||||
status: dbBlocked || providerBlocked ? "blocked" : "not_proven",
|
||||
|
||||
@@ -28,6 +28,7 @@ test("runtime DB readiness preserves dns resolution classification", () => {
|
||||
attempted: true,
|
||||
result: "dns_error",
|
||||
classification: "dns_resolution_failed",
|
||||
endpointSource: "secret-url-host",
|
||||
endpointRedacted: true,
|
||||
valueRedacted: true
|
||||
},
|
||||
@@ -49,13 +50,70 @@ test("runtime DB readiness preserves dns resolution classification", () => {
|
||||
assert.equal(readiness.status, "blocked");
|
||||
assert.equal(readiness.connectionResult, "dns_error");
|
||||
assert.equal(readiness.connectionClassification, "dns_resolution_failed");
|
||||
assert.equal(readiness.endpointSource, "secret-url-host");
|
||||
assert.equal(readiness.optionalPublicDnsAlias.source, "optional-public-dns-alias");
|
||||
assert.equal(readiness.optionalPublicDnsAlias.requiredForReadiness, false);
|
||||
assert.equal(readiness.optionalPublicDnsAlias.usedForProbe, false);
|
||||
assert.equal(readiness.liveConnected, false);
|
||||
assert.equal(readiness.liveDbEvidence, false);
|
||||
assert.equal(readiness.secretMaterialRead, false);
|
||||
assert.equal(readiness.evidence[0].classification, "dns_resolution_failed");
|
||||
assert.equal(readiness.evidence[0].endpointSource, "secret-url-host");
|
||||
});
|
||||
|
||||
test("parallel blockers keep DB DNS and provider Secret gaps independent", () => {
|
||||
test("runtime DB readiness does not pass when live success uses a non-authoritative DB endpoint source", () => {
|
||||
const readiness = __testHooks.summarizeRuntimeDbReadiness(
|
||||
{
|
||||
configReady: true,
|
||||
ready: true,
|
||||
connected: true,
|
||||
liveConnected: true,
|
||||
liveDbEvidence: true,
|
||||
connectionAttempted: true,
|
||||
connectionResult: "connected",
|
||||
endpointSource: "optional-public-dns-alias",
|
||||
fields: [
|
||||
{ name: "HWLAB_CLOUD_DB_URL", present: true },
|
||||
{ name: "HWLAB_CLOUD_DB_SSL_MODE", present: true }
|
||||
],
|
||||
secretRefs: [
|
||||
{
|
||||
env: "HWLAB_CLOUD_DB_URL",
|
||||
secretName: "hwlab-cloud-api-dev-db",
|
||||
secretKey: "database-url",
|
||||
present: true,
|
||||
envInjected: true
|
||||
}
|
||||
],
|
||||
connection: {
|
||||
attempted: true,
|
||||
result: "connected",
|
||||
classification: "tcp_connected",
|
||||
endpointSource: "optional-public-dns-alias",
|
||||
endpointRedacted: true,
|
||||
valueRedacted: true
|
||||
},
|
||||
redaction: { valuesRedacted: true },
|
||||
safety: { valuesRedacted: true, liveDbEvidence: true }
|
||||
},
|
||||
{
|
||||
observable: true,
|
||||
secretName: "hwlab-cloud-api-dev-db",
|
||||
secretKey: "database-url",
|
||||
secretPresent: true,
|
||||
secretKeyPresent: true,
|
||||
secretValueRead: false,
|
||||
redacted: true
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(readiness.status, "blocked");
|
||||
assert.equal(readiness.liveConnected, true);
|
||||
assert.equal(readiness.endpointSource, "optional-public-dns-alias");
|
||||
assert.match(readiness.blocker, /Secret URL host/u);
|
||||
});
|
||||
|
||||
test("parallel blockers keep DB readiness and provider Secret gaps independent", () => {
|
||||
const edgeHealth = {
|
||||
runtimeDbReadiness: {
|
||||
status: "blocked",
|
||||
|
||||
@@ -841,7 +841,11 @@ function buildDoD(reports, milestones, blockers) {
|
||||
artifactIdentity.targetCoverage?.covered !== false &&
|
||||
artifactIdentity.artifactCatalog?.matchesTarget !== false;
|
||||
const cloudApiDb = cloudApiDbStatus(reports);
|
||||
const dbReady = cloudApiDb.ready === true && cloudApiDb.connected === true && cloudApiDb.liveDbEvidence === true;
|
||||
const dbReady =
|
||||
cloudApiDb.ready === true &&
|
||||
cloudApiDb.connected === true &&
|
||||
cloudApiDb.endpointSource === "secret-url-host" &&
|
||||
cloudApiDb.liveDbEvidence === true;
|
||||
const m3Live = statusIsPass(reports.devM3Hardware.liveOperation?.status);
|
||||
const m4Live = statusIsPass(reports.devM4Agent.livePreflight?.status);
|
||||
|
||||
@@ -885,7 +889,7 @@ function buildDoD(reports, milestones, blockers) {
|
||||
id: "cloud-api-db-ready",
|
||||
status: dbReady ? "pass" : "blocked",
|
||||
evidenceLevel: dbReady ? "DEV-LIVE" : "BLOCKED",
|
||||
summary: `cloud-api DB status=${cloudApiDb.status}; ready=${cloudApiDb.ready}; connected=${cloudApiDb.connected}; liveDbEvidence=${cloudApiDb.liveDbEvidence}.`
|
||||
summary: `cloud-api DB status=${cloudApiDb.status}; ready=${cloudApiDb.ready}; connected=${cloudApiDb.connected}; endpointSource=${cloudApiDb.endpointSource}; liveDbEvidence=${cloudApiDb.liveDbEvidence}.`
|
||||
},
|
||||
{
|
||||
id: "m3-hardware-trusted-loop",
|
||||
@@ -921,6 +925,7 @@ function cloudApiDbStatus(reports) {
|
||||
connected: db.connected === true || db.liveConnected === true,
|
||||
configReady: db.configReady === true,
|
||||
connectionChecked: db.connectionChecked === true,
|
||||
endpointSource: db.endpointSource ?? db.connection?.endpointSource ?? "unknown",
|
||||
liveDbEvidence: db.liveDbEvidence === true
|
||||
};
|
||||
}
|
||||
@@ -929,7 +934,7 @@ function buildCurrentDevLayering(reports, blockers) {
|
||||
const m2EndpointLive = hasCurrentM2EndpointEvidence(reports.devM2Smoke);
|
||||
const edgeLive = hasCurrentLiveEdgeEvidence(reports.devEdgeHealth);
|
||||
const cloudDb = cloudApiDbStatus(reports);
|
||||
const dbReady = cloudDb.ready && cloudDb.connected && cloudDb.liveDbEvidence;
|
||||
const dbReady = cloudDb.ready && cloudDb.connected && cloudDb.endpointSource === "secret-url-host" && cloudDb.liveDbEvidence;
|
||||
const m3Live = statusIsPass(reports.devM3Hardware.liveOperation?.status);
|
||||
const m4Live = statusIsPass(reports.devM4Agent.livePreflight?.status);
|
||||
const artifactIdentity = reports.devPreflight.artifactIdentity;
|
||||
@@ -962,7 +967,7 @@ function buildCurrentDevLayering(reports, blockers) {
|
||||
label: "DB live/degraded",
|
||||
status: dbReady ? "pass" : "blocked",
|
||||
evidenceLevel: dbReady ? "DEV-LIVE" : "BLOCKED",
|
||||
summary: `cloud-api DB status=${cloudDb.status}; configReady=${cloudDb.configReady}; ready=${cloudDb.ready}; connected=${cloudDb.connected}; liveDbEvidence=${cloudDb.liveDbEvidence}.`,
|
||||
summary: `cloud-api DB status=${cloudDb.status}; configReady=${cloudDb.configReady}; ready=${cloudDb.ready}; connected=${cloudDb.connected}; endpointSource=${cloudDb.endpointSource}; liveDbEvidence=${cloudDb.liveDbEvidence}.`,
|
||||
evidence: reports.devM5Gate.devPreconditions?.evidence?.filter((line) => line.includes("/health/live") || line.includes("DB")) ?? [],
|
||||
nextRequired: "Provide live DB connection evidence through redacted health output; route reachability alone is insufficient."
|
||||
},
|
||||
@@ -1079,11 +1084,12 @@ function buildMilestoneBlockerClassification(reports) {
|
||||
status: m4Live ? "pass" : "blocked",
|
||||
currentLevel: m4Live ? "DEV-LIVE" : "BLOCKED",
|
||||
blockerClass: m4Live ? "cleared" : "db-live-readiness",
|
||||
dependency: "Cloud API /health/live must report DB ready=true, connected=true, and liveDbEvidence=true before live agent scheduling/evidence closure.",
|
||||
dependency: "Cloud API /health/live must report DB ready=true, connected=true, endpointSource=secret-url-host, and liveDbEvidence=true before live agent scheduling/evidence closure.",
|
||||
evidence: [
|
||||
`db.status=${cloudDb.status}`,
|
||||
`db.ready=${cloudDb.ready}`,
|
||||
`db.connected=${cloudDb.connected}`,
|
||||
`db.endpointSource=${cloudDb.endpointSource}`,
|
||||
`db.liveDbEvidence=${cloudDb.liveDbEvidence}`
|
||||
],
|
||||
nextRequired: "Repair DB live readiness and rerun the M4 live preflight without scheduling a DEV agent task before preconditions pass.",
|
||||
@@ -1157,7 +1163,7 @@ function fallbackAction(scope) {
|
||||
if (scope === "base-image") return "Preload or tag node:20-bookworm-slim and rerun the base-image and artifact publish preflights.";
|
||||
if (scope.includes("artifact")) return "Publish DEV artifacts for every frozen HWLAB service and record immutable registry digests.";
|
||||
if (scope.includes("edge") || scope.includes("ingress") || scope.includes("frp")) return "Repair frp/master-edge/D601 router path and rerun read-only DEV edge health.";
|
||||
if (scope.includes("cloud-api-db")) return "Configure DEV cloud-api DB env readiness and rerun health/preflight without exposing secrets.";
|
||||
if (scope.includes("cloud-api-db")) return "Configure DEV cloud-api DB env readiness and Secret URL host connectivity, then rerun health/preflight without exposing secrets.";
|
||||
if (scope === "db-live") return "Repair DEV cloud-api DB live readiness, then rerun the read-only health and M4 preflight reports without exposing secret values.";
|
||||
if (scope === "m3-patch-panel-wiring") return "Load/apply DEV patch-panel wiring for res_boxsimu_1:DO1 -> res_boxsimu_2:DI1, then rerun the bounded DEV M3 smoke.";
|
||||
if (scope === "m3-box-simu-identity") return "Fix DEV box-simu instance identity so direct endpoints expose distinct res_boxsimu_1 and res_boxsimu_2 resources.";
|
||||
@@ -1173,8 +1179,8 @@ function evidenceRequiredFor(scope) {
|
||||
if (scope === "hwlab-router" || scope === "hwlab-tunnel-client" || scope === "hwlab-edge-proxy") return "Real repo entrypoints or dedicated Dockerfiles for route services, followed by artifact preflight.";
|
||||
if (scope.includes("artifact") || scope === "ghcr") return "Artifact publish report with ciPublished=true, registryVerified=true, and sha256 digest for each frozen service ID.";
|
||||
if (scope.includes("kubectl") || scope.includes("k3s")) return "Read-only kubectl/k3s report proving pods/services/configmaps are observable in hwlab-dev without reading Secrets.";
|
||||
if (scope.includes("cloud-api-db")) return "Cloud API health/live output showing DB env ready and redacted secret references, without secret material.";
|
||||
if (scope === "db-live") return "Cloud API /health/live output with ready=true, connected=true, liveDbEvidence=true, and redacted secret references.";
|
||||
if (scope.includes("cloud-api-db")) return "Cloud API health/live output showing DB env ready, endpointSource=secret-url-host, redacted secret references, and no secret material.";
|
||||
if (scope === "db-live") return "Cloud API /health/live output with ready=true, connected=true, endpointSource=secret-url-host, liveDbEvidence=true, and redacted secret references.";
|
||||
if (scope === "m3-patch-panel-wiring") return "Read-only patch-panel /status and /wiring showing active res_boxsimu_1:DO1 -> res_boxsimu_2:DI1 before a bounded write/read smoke records operation, trace, audit, and evidence IDs.";
|
||||
if (scope === "m3-box-simu-identity") return "Read-only direct box-simu /health/live and /status output showing distinct res_boxsimu_1 and res_boxsimu_2 resources.";
|
||||
if (scope === "m3-gateway-simu-identity") return "Read-only direct gateway-simu /health/live and /status output showing two distinct gateway identities/sessions.";
|
||||
|
||||
@@ -826,7 +826,7 @@ function validateCloudApiDbContract(reporter, deploy, workloads, services) {
|
||||
"cloud-api-db-env-contract",
|
||||
"db",
|
||||
"pass",
|
||||
"cloud-api DEV DB manifest declares the required env names, redacted Secret reference, and stable k3s DB DNS Service.",
|
||||
"cloud-api DEV DB manifest declares the required env names and redacted Secret reference; optional cloud-api-db alias is not a readiness gate.",
|
||||
evidence
|
||||
);
|
||||
} else {
|
||||
@@ -847,7 +847,7 @@ function validateCloudApiDbContract(reporter, deploy, workloads, services) {
|
||||
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, 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."
|
||||
nextTask: "Repair deploy/deploy.json and deploy/k8s/base/workloads.yaml so they expose the required DB env names and redacted Secret reference. Do not make cloud-api-db a hard readiness gate unless this repo also owns its Service/Endpoint rollout contract."
|
||||
});
|
||||
}
|
||||
|
||||
@@ -914,7 +914,7 @@ function inspectCloudApiDbStaticContract(deploy, workloads, services) {
|
||||
const missingSecretRefs = fields
|
||||
.filter((field) => field.secretRef && !field.secretRef.present)
|
||||
.map((field) => `${field.secretRef.secretName}/${field.secretRef.secretKey}`);
|
||||
const dnsContract = inspectCloudApiDbDnsContract(deployEnv, workloadEnv, services);
|
||||
const dnsContract = inspectCloudApiDbOptionalAliasContract(deployEnv, workloadEnv, services);
|
||||
|
||||
return {
|
||||
contractVersion: DEV_DB_ENV_CONTRACT.contractVersion,
|
||||
@@ -937,11 +937,9 @@ function inspectCloudApiDbStaticContract(deploy, workloads, services) {
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
function inspectCloudApiDbOptionalAliasContract(deployEnv, workloadEnv, services) {
|
||||
const alias = DEV_DB_ENV_CONTRACT.dns;
|
||||
const service = listManifestItems(services).find((item) => item?.metadata?.name === alias.serviceName) ?? null;
|
||||
const actual = {
|
||||
deployServiceName: deployEnv.HWLAB_CLOUD_DB_SERVICE_NAME,
|
||||
deployServiceNamespace: deployEnv.HWLAB_CLOUD_DB_SERVICE_NAMESPACE,
|
||||
@@ -951,43 +949,25 @@ function inspectCloudApiDbDnsContract(deployEnv, workloadEnv, services) {
|
||||
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
|
||||
serviceName: service?.metadata?.name
|
||||
};
|
||||
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"}`);
|
||||
const missing = [];
|
||||
for (const [name, value] of Object.entries(actual)) {
|
||||
if (value !== undefined) {
|
||||
missing.push(`${name} must be absent because ${alias.serviceName} is not the readiness authority`);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
source: alias.source,
|
||||
serviceName: alias.serviceName,
|
||||
namespace: alias.namespace,
|
||||
port: alias.port,
|
||||
portName: alias.portName,
|
||||
requiredForReadiness: alias.requiredForReadiness,
|
||||
usedForProbe: alias.usedForProbe,
|
||||
servicePresent: Boolean(service),
|
||||
secretMaterialRead: false,
|
||||
liveDbEvidence: false,
|
||||
missing,
|
||||
|
||||
@@ -172,19 +172,19 @@ 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(cloudApi.env.HWLAB_CLOUD_RUNTIME_ADAPTER, "postgres", "cloud-api runtime durable adapter");
|
||||
assert.equal(cloudApi.env.HWLAB_CLOUD_RUNTIME_DURABLE, "true", "cloud-api runtime durable flag");
|
||||
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");
|
||||
assert.equal(cloudApiWorkloadEnv.HWLAB_CLOUD_RUNTIME_ADAPTER?.value, "postgres", "cloud-api workload runtime durable adapter");
|
||||
assert.equal(cloudApiWorkloadEnv.HWLAB_CLOUD_RUNTIME_DURABLE?.value, "true", "cloud-api workload runtime durable flag");
|
||||
assertCloudApiDbDnsContract(k8sServices, cloudApi.env);
|
||||
assert.equal(cloudApi.env.HWLAB_CLOUD_DB_SERVICE_NAME, undefined, "cloud-api DB alias service name is optional");
|
||||
assert.equal(cloudApi.env.HWLAB_CLOUD_DB_SERVICE_NAMESPACE, undefined, "cloud-api DB alias namespace is optional");
|
||||
assert.equal(cloudApi.env.HWLAB_CLOUD_DB_HOST, undefined, "cloud-api DB alias host is optional");
|
||||
assert.equal(cloudApi.env.HWLAB_CLOUD_DB_PORT, undefined, "cloud-api DB alias port is optional");
|
||||
assert.equal(cloudApiWorkloadEnv.HWLAB_CLOUD_DB_SERVICE_NAME, undefined, "cloud-api workload DB alias service name is optional");
|
||||
assert.equal(cloudApiWorkloadEnv.HWLAB_CLOUD_DB_SERVICE_NAMESPACE, undefined, "cloud-api workload DB alias namespace is optional");
|
||||
assert.equal(cloudApiWorkloadEnv.HWLAB_CLOUD_DB_HOST, undefined, "cloud-api workload DB alias host is optional");
|
||||
assert.equal(cloudApiWorkloadEnv.HWLAB_CLOUD_DB_PORT, undefined, "cloud-api workload DB alias port is optional");
|
||||
assertCloudApiDbOptionalAlias(k8sServices);
|
||||
assert.equal(cloudApi.env.HWLAB_CODE_AGENT_PROVIDER, "openai", "cloud-api Code Agent provider");
|
||||
assert.equal(cloudApi.env.HWLAB_CODE_AGENT_MODEL, DEV_CODE_AGENT_PROVIDER_CONTRACT.model, "cloud-api Code Agent model");
|
||||
assert.equal(
|
||||
@@ -223,28 +223,14 @@ function getWorkloadEnv(workloads, serviceId) {
|
||||
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");
|
||||
function assertCloudApiDbOptionalAlias(services) {
|
||||
const alias = DEV_DB_ENV_CONTRACT.dns;
|
||||
const service = listItems(services).find((item) => item?.metadata?.name === alias.serviceName);
|
||||
assert.equal(DEV_DB_ENV_CONTRACT.endpointAuthority.source, "secret-url-host", "cloud-api DB runtime authority is the Secret URL host");
|
||||
assert.equal(DEV_DB_ENV_CONTRACT.endpointAuthority.env, "HWLAB_CLOUD_DB_URL", "cloud-api DB runtime authority env");
|
||||
assert.equal(alias.requiredForReadiness, false, "cloud-api DB alias is not a readiness requirement");
|
||||
assert.equal(alias.usedForProbe, false, "cloud-api DB alias is not the runtime TCP probe source");
|
||||
assert.equal(service, undefined, `Service/${alias.serviceName} is optional until this repo owns endpoints and rollout/apply contract`);
|
||||
}
|
||||
|
||||
function assertCodeAgentProviderWorkloadContract(env) {
|
||||
|
||||
@@ -14,7 +14,6 @@ const forbiddenRuntimeSubstitutes = Object.freeze([
|
||||
"provider-gateway",
|
||||
"microservice-proxy"
|
||||
]);
|
||||
const dbDnsComponent = "cloud-api-db";
|
||||
|
||||
async function readText(relativePath) {
|
||||
return readFile(path.join(repoRoot, relativePath), "utf8");
|
||||
@@ -125,31 +124,17 @@ function labelsFor(resource, label) {
|
||||
}
|
||||
|
||||
function assertMetadataServiceId(resource, expectedServiceId, label) {
|
||||
assert.equal(resource.metadata?.name, expectedServiceId, `${label}.metadata.name must match service id`);
|
||||
if (!resource.metadata?.labels?.["hwlab.pikastech.local/instance-id"]) {
|
||||
assert.equal(resource.metadata?.name, expectedServiceId, `${label}.metadata.name must match service id`);
|
||||
} else {
|
||||
assert.ok(resource.metadata?.name.startsWith(`${expectedServiceId}-`), `${label}.metadata.name must include service id prefix`);
|
||||
}
|
||||
assertDevNamespace(resource.metadata?.namespace, `${label}.metadata.namespace`);
|
||||
const labels = labelsFor(resource, label);
|
||||
assert.equal(labels[serviceIdLabel], expectedServiceId, `${label} service-id 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`);
|
||||
@@ -311,24 +296,42 @@ function assertServices(services, guard) {
|
||||
assert.equal(services.kind, "List", "services kind");
|
||||
assertArray(services.items, "services items");
|
||||
|
||||
const runtimeServices = services.items.filter((item) => item.metadata?.name !== dbDnsComponent);
|
||||
const serviceIds = runtimeServices.map((item) => item.metadata?.labels?.[serviceIdLabel]);
|
||||
assertUnique(serviceIds, "k8s service ids");
|
||||
const runtimeServices = services.items;
|
||||
const serviceNames = runtimeServices.map((item) => item.metadata?.name);
|
||||
const serviceIds = [...new Set(runtimeServices.map((item) => item.metadata?.labels?.[serviceIdLabel]))];
|
||||
assertUnique(serviceNames, "k8s service names");
|
||||
assertSameMembers(serviceIds, guard.requiredK3sSkeleton.requiredServiceServiceIds, "k8s service ids");
|
||||
|
||||
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);
|
||||
assert.equal(
|
||||
services.items.some((item) => item.metadata?.name === DEV_DB_ENV_CONTRACT.dns.serviceName),
|
||||
false,
|
||||
"cloud-api-db is an optional future alias and is not required in the current runtime boundary"
|
||||
);
|
||||
assert.equal(DEV_DB_ENV_CONTRACT.endpointAuthority.source, "secret-url-host", "DB readiness authority must be the Secret URL host");
|
||||
assert.equal(DEV_DB_ENV_CONTRACT.dns.requiredForReadiness, false, "DB DNS alias must not be a readiness hard requirement");
|
||||
assert.equal(DEV_DB_ENV_CONTRACT.dns.usedForProbe, false, "DB DNS alias must not be the runtime probe target");
|
||||
|
||||
for (const service of runtimeServices) {
|
||||
const serviceId = service.metadata?.labels?.[serviceIdLabel];
|
||||
assertServiceId(serviceId, `k8s service ${service.metadata?.name}`);
|
||||
assertMetadataServiceId(service, serviceId, `k8s service ${serviceId}`);
|
||||
assertNoExternalNameSubstitute(service, `k8s service ${serviceId}`);
|
||||
assert.equal(service.spec?.selector?.["app.kubernetes.io/name"], serviceId, `${serviceId} selector`);
|
||||
if (service.metadata?.labels?.["hwlab.pikastech.local/instance-id"]) {
|
||||
assert.match(
|
||||
service.spec?.selector?.["statefulset.kubernetes.io/pod-name"],
|
||||
new RegExp(`^${escapeRegExp(serviceId)}-\\d+$`),
|
||||
`${serviceId} indexed selector`
|
||||
);
|
||||
} else {
|
||||
assert.equal(service.spec?.selector?.["app.kubernetes.io/name"], serviceId, `${serviceId} selector`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function escapeRegExp(value) {
|
||||
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
function assertDevHealthConfig(configMap) {
|
||||
assert.equal(configMap.kind, "ConfigMap", "dev health contract kind");
|
||||
assertDevNamespace(configMap.metadata?.namespace, "dev health contract namespace");
|
||||
|
||||
Reference in New Issue
Block a user