fix: add dev DB DNS contract
This commit is contained in:
@@ -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
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
Reference in New Issue
Block a user