fix: finish deploy yaml config cleanup
This commit is contained in:
@@ -145,7 +145,7 @@ HWLAB 是硬件实验室运行面和控制面项目。本文是 agent、指挥
|
||||
- Cloud Web 构建:`npm run web:build`
|
||||
- Cloud Web M3 只读护栏:`npm run web:m3-readonly`
|
||||
- G14 artifact build helper:`node scripts/g14-artifact-publish.mjs --publish ...`,只能由 G14 Tekton task 携带 CI artifact identity 调用;人工发布走 G14 poller/GitOps,不走 legacy CLI CD。
|
||||
- G14 monorepo 组件计划:`node scripts/g14-ci-plan.mjs --base-ref <ref> --target-ref <ref> --pretty`,只读分析 affected/reused services,基于内建 service-path component model 与 `deploy/deploy.json`;细则见 [docs/reference/g14-gitops-cicd.md](docs/reference/g14-gitops-cicd.md)。
|
||||
- G14 monorepo 组件计划:`node scripts/g14-ci-plan.mjs --base-ref <ref> --target-ref <ref> --pretty`,只读分析 affected/reused services,基于内建 service-path component model 与 `deploy/deploy.yaml`;细则见 [docs/reference/g14-gitops-cicd.md](docs/reference/g14-gitops-cicd.md)。
|
||||
- G14 GitOps 渲染:`npm run g14:gitops:render`;source 分支不再运行生成物 drift check,发布态由 Tekton 写入 `G14-gitops`。
|
||||
- DEV 依赖 runtime base 构建:`npm run dev-runtime-base:build`
|
||||
- Legacy D601 DEV CD:旧脚本入口已删除;事故回放只读历史 issue/commit,不恢复旧命令。
|
||||
|
||||
@@ -1,346 +0,0 @@
|
||||
{
|
||||
"manifestVersion": "v1",
|
||||
"environment": "dev",
|
||||
"namespace": "hwlab-dev",
|
||||
"endpoint": "http://74.48.78.17:16667",
|
||||
"health": {
|
||||
"method": "GET",
|
||||
"path": "/health/live",
|
||||
"responseFormat": "json"
|
||||
},
|
||||
"publicEndpoints": {
|
||||
"frontend": {
|
||||
"url": "http://74.48.78.17:16666",
|
||||
"protocol": "http",
|
||||
"host": "74.48.78.17",
|
||||
"port": 16666,
|
||||
"serviceId": "hwlab-cloud-web",
|
||||
"frpProxy": "hwlab-dev-cloud-web"
|
||||
},
|
||||
"api": {
|
||||
"url": "http://74.48.78.17:16667",
|
||||
"protocol": "http",
|
||||
"host": "74.48.78.17",
|
||||
"port": 16667,
|
||||
"serviceId": "hwlab-edge-proxy",
|
||||
"frpProxy": "hwlab-dev-edge-proxy"
|
||||
}
|
||||
},
|
||||
"frp": {
|
||||
"server": {
|
||||
"address": "74.48.78.17",
|
||||
"bindPort": 7000
|
||||
},
|
||||
"proxies": [
|
||||
{
|
||||
"name": "hwlab-dev-cloud-web",
|
||||
"endpointId": "frontend",
|
||||
"type": "tcp",
|
||||
"localServiceId": "hwlab-cloud-web",
|
||||
"localHost": "hwlab-cloud-web.hwlab-dev.svc.cluster.local",
|
||||
"localPort": 8080,
|
||||
"remotePort": 16666
|
||||
},
|
||||
{
|
||||
"name": "hwlab-dev-edge-proxy",
|
||||
"endpointId": "api",
|
||||
"type": "tcp",
|
||||
"localServiceId": "hwlab-edge-proxy",
|
||||
"localHost": "hwlab-edge-proxy.hwlab-dev.svc.cluster.local",
|
||||
"localPort": 6667,
|
||||
"remotePort": 16667
|
||||
}
|
||||
]
|
||||
},
|
||||
"k3s": {
|
||||
"serviceMappings": [
|
||||
{
|
||||
"serviceId": "hwlab-cloud-api",
|
||||
"name": "hwlab-cloud-api",
|
||||
"namespace": "hwlab-dev",
|
||||
"port": 6667,
|
||||
"targetPort": "http"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-cloud-web",
|
||||
"name": "hwlab-cloud-web",
|
||||
"namespace": "hwlab-dev",
|
||||
"port": 8080,
|
||||
"targetPort": "http"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-gateway",
|
||||
"name": "hwlab-gateway",
|
||||
"namespace": "hwlab-dev",
|
||||
"port": 7001,
|
||||
"targetPort": "http"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-agent-skills",
|
||||
"name": "hwlab-agent-skills",
|
||||
"namespace": "hwlab-dev",
|
||||
"port": 7430,
|
||||
"targetPort": "http"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-edge-proxy",
|
||||
"name": "hwlab-edge-proxy",
|
||||
"namespace": "hwlab-dev",
|
||||
"port": 6667,
|
||||
"targetPort": "http"
|
||||
}
|
||||
]
|
||||
},
|
||||
"profiles": {
|
||||
"dev": {
|
||||
"name": "dev",
|
||||
"enabled": true,
|
||||
"namespace": "hwlab-dev",
|
||||
"endpoint": "http://74.48.78.17:16667",
|
||||
"notes": "DEV-only k3s deployment skeleton. D601 reaches the master edge through frp reverse tunnel."
|
||||
},
|
||||
"prod": {
|
||||
"name": "prod",
|
||||
"enabled": false,
|
||||
"namespace": "hwlab-prod",
|
||||
"notes": "Reserved placeholder only. PROD deployment and acceptance are explicitly out of scope."
|
||||
}
|
||||
},
|
||||
"lanes": {
|
||||
"v02": {
|
||||
"name": "v0.2",
|
||||
"sourceBranch": "v0.2",
|
||||
"gitopsBranch": "v0.2-gitops",
|
||||
"namespace": "hwlab-v02",
|
||||
"endpoint": "https://hwlab.74-48-78-17.nip.io",
|
||||
"publicEndpoints": {
|
||||
"frontend": "https://hwlab.74-48-78-17.nip.io",
|
||||
"api": "https://hwlab.74-48-78-17.nip.io",
|
||||
"legacyFrontend": "http://74.48.78.17:19666",
|
||||
"legacyApi": "http://74.48.78.17:19667"
|
||||
},
|
||||
"artifactCatalog": "deploy/artifact-catalog.v02.json",
|
||||
"runtimePath": "deploy/gitops/g14/runtime-v02",
|
||||
"imageTagMode": "full",
|
||||
"envReuseServices": [
|
||||
"hwlab-cloud-api",
|
||||
"hwlab-cloud-web",
|
||||
"hwlab-gateway",
|
||||
"hwlab-edge-proxy",
|
||||
"hwlab-agent-skills"
|
||||
],
|
||||
"bootScripts": {
|
||||
"hwlab-cloud-api": "deploy/runtime/boot/hwlab-cloud-api.sh",
|
||||
"hwlab-cloud-web": "deploy/runtime/boot/hwlab-cloud-web.sh",
|
||||
"hwlab-gateway": "deploy/runtime/boot/hwlab-gateway.sh",
|
||||
"hwlab-edge-proxy": "deploy/runtime/boot/hwlab-edge-proxy.sh",
|
||||
"hwlab-agent-skills": "deploy/runtime/boot/hwlab-agent-skills.sh"
|
||||
},
|
||||
"serviceDeclarations": {
|
||||
"hwlab-cloud-api": {
|
||||
"runtimeKind": "bun-command",
|
||||
"entrypoint": "cmd/hwlab-cloud-api/main.ts",
|
||||
"artifactKind": "bun-command",
|
||||
"healthPath": "/health/live",
|
||||
"healthPort": 6667,
|
||||
"componentPaths": [
|
||||
"cmd/hwlab-cloud-api/",
|
||||
"cmd/hwlab-codex-api-responses-forwarder/",
|
||||
"cmd/hwlab-deepseek-responses-bridge/",
|
||||
"deploy/runtime/boot/hwlab-codex-api-responses-forwarder.sh",
|
||||
"deploy/runtime/boot/hwlab-deepseek-responses-bridge.sh",
|
||||
"internal/cloud/",
|
||||
"internal/db/",
|
||||
"internal/audit/",
|
||||
"internal/agent/agentrun-dispatch.mjs",
|
||||
"internal/agent/prompts/",
|
||||
"skills/hwlab-agent-runtime/"
|
||||
],
|
||||
"env": {},
|
||||
"observable": true
|
||||
},
|
||||
"hwlab-cloud-web": {
|
||||
"runtimeKind": "cloud-web",
|
||||
"entrypoint": "web/hwlab-cloud-web/index.html",
|
||||
"artifactKind": "cloud-web",
|
||||
"healthPath": "/health/live",
|
||||
"healthPort": 8080,
|
||||
"componentPaths": [
|
||||
"web/hwlab-cloud-web/",
|
||||
"internal/dev-entrypoint/cloud-web-runtime.mjs",
|
||||
"internal/dev-entrypoint/cloud-web-proxy.mjs",
|
||||
"internal/dev-entrypoint/cloud-web-routes.mjs",
|
||||
"tools/src/hwlab-cli/trace-renderer.ts"
|
||||
],
|
||||
"env": {},
|
||||
"observable": true
|
||||
},
|
||||
"hwlab-gateway": {
|
||||
"runtimeKind": "bun-command",
|
||||
"entrypoint": "cmd/hwlab-gateway/main.ts",
|
||||
"artifactKind": "bun-command",
|
||||
"healthPath": "/health/live",
|
||||
"healthPort": 7001,
|
||||
"componentPaths": ["cmd/hwlab-gateway/", "internal/sim/"],
|
||||
"env": {},
|
||||
"observable": false
|
||||
},
|
||||
"hwlab-edge-proxy": {
|
||||
"runtimeKind": "bun-command",
|
||||
"entrypoint": "cmd/hwlab-edge-proxy/main.ts",
|
||||
"artifactKind": "bun-command",
|
||||
"healthPath": "/health",
|
||||
"healthPort": 6667,
|
||||
"componentPaths": ["cmd/hwlab-edge-proxy/", "internal/dev-entrypoint/http.mjs"],
|
||||
"env": {},
|
||||
"observable": true
|
||||
},
|
||||
"hwlab-agent-skills": {
|
||||
"runtimeKind": "skills-bundle",
|
||||
"entrypoint": "skills/hwlab-agent-runtime/SKILL.md",
|
||||
"artifactKind": "skills-bundle",
|
||||
"healthPath": "/health/live",
|
||||
"healthPort": 7430,
|
||||
"componentPaths": ["skills/"],
|
||||
"env": {},
|
||||
"observable": true
|
||||
}
|
||||
},
|
||||
"envRecipe": {
|
||||
"osPackages": ["ca-certificates", "git"],
|
||||
"bunVersion": "1.3.13",
|
||||
"launcherPath": "deploy/runtime/launcher/hwlab-env-reuse-launcher.ts",
|
||||
"runtimeNodeModulesPath": "/opt/hwlab-env/node_modules",
|
||||
"additionalEnvPaths": [
|
||||
"internal/dev-entrypoint/artifact-runtime.mjs",
|
||||
"scripts/artifact-publish.mjs",
|
||||
"scripts/g14-artifact-publish.mjs",
|
||||
"scripts/src/dev-artifact-services.mjs"
|
||||
],
|
||||
"hwpodAliases": [
|
||||
{ "command": "hwpod", "script": "tools/hwpod-cli.ts" },
|
||||
{ "command": "hwpod-ctl", "script": "tools/hwpod-ctl.ts" },
|
||||
{ "command": "hwpod-compiler", "script": "tools/hwpod-compiler-cli.ts" }
|
||||
]
|
||||
},
|
||||
"bootConfig": {
|
||||
"template": "default",
|
||||
"overrides": {}
|
||||
},
|
||||
"services": [
|
||||
{
|
||||
"serviceId": "hwlab-cloud-api",
|
||||
"env": {
|
||||
"HWLAB_CLOUD_DB_URL": "secretRef:hwlab-cloud-api-v02-db/database-url",
|
||||
"HWLAB_CLOUD_DB_CONTRACT": "v02-redacted-presence-only",
|
||||
"HWLAB_ACCESS_CONTROL_REQUIRED": "1",
|
||||
"HWLAB_BOOTSTRAP_ADMIN_ID": "usr_v02_admin",
|
||||
"HWLAB_BOOTSTRAP_ADMIN_USERNAME": "admin",
|
||||
"HWLAB_BOOTSTRAP_ADMIN_DISPLAY_NAME": "HWLAB v0.2 Admin",
|
||||
"HWLAB_BOOTSTRAP_ADMIN_PASSWORD_HASH": "secretRef:hwlab-v02-bootstrap-admin/password-hash",
|
||||
"HWLAB_BOOTSTRAP_ADMIN_API_KEY_ID": "key_master_server_admin",
|
||||
"HWLAB_BOOTSTRAP_ADMIN_API_KEY": "secretRef:hwlab-v02-master-server-admin-api-key/api-key",
|
||||
"HWLAB_KEYCLOAK_ISSUER": "https://auth.74-48-78-17.nip.io/realms/hwlab",
|
||||
"HWLAB_KEYCLOAK_CLIENT_ID": "hwlab-cloud-web",
|
||||
"HWLAB_KEYCLOAK_CLIENT_SECRET": "secretRef:hwlab-cloud-web-client/client-secret",
|
||||
"HWLAB_CODE_AGENT_ADAPTER": "agentrun-v01",
|
||||
"AGENTRUN_MGR_URL": "http://agentrun-mgr.agentrun-v01.svc.cluster.local:8080",
|
||||
"HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID": "G14",
|
||||
"HWLAB_CODE_AGENT_AGENTRUN_RUNNER_NAMESPACE": "agentrun-v01",
|
||||
"HWLAB_CODE_AGENT_AGENTRUN_SECRET_NAMESPACE": "agentrun-v01",
|
||||
"HWLAB_CODE_AGENT_AGENTRUN_REPO_URL": "http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/HWLAB.git"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"services": [
|
||||
{
|
||||
"serviceId": "hwlab-cloud-api",
|
||||
"namespace": "hwlab-dev",
|
||||
"healthPath": "/health/live",
|
||||
"profile": "dev",
|
||||
"replicas": 1,
|
||||
"env": {
|
||||
"HWLAB_ENVIRONMENT": "dev",
|
||||
"HWLAB_PUBLIC_ENDPOINT": "http://74.48.78.17:16667",
|
||||
"HWLAB_CLOUD_API_PORT": "6667",
|
||||
"HWLAB_RUNTIME_SUBSTITUTE_FORBIDDEN": "unidesk-backend,provider-gateway,microservice-proxy",
|
||||
"HWLAB_CLOUD_DB_URL": "secretRef:hwlab-cloud-api-dev-db/database-url",
|
||||
"HWLAB_CLOUD_DB_SSL_MODE": "disable",
|
||||
"HWLAB_CLOUD_DB_CONTRACT": "dev-redacted-presence-only",
|
||||
"HWLAB_CLOUD_RUNTIME_ADAPTER": "postgres",
|
||||
"HWLAB_CLOUD_RUNTIME_DURABLE": "true",
|
||||
"HWLAB_M3_IO_CONTROL_ENABLED": "false",
|
||||
"HWLAB_CODE_AGENT_PROVIDER": "codex-stdio",
|
||||
"HWLAB_CODE_AGENT_MODEL": "gpt-5.5",
|
||||
"HWLAB_CODE_AGENT_TIMEOUT_MS": "1200000",
|
||||
"HWLAB_CODE_AGENT_OPENAI_BASE_URL": "http://127.0.0.1:49280/responses",
|
||||
"HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED": "1",
|
||||
"HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR": "repo-owned",
|
||||
"HWLAB_CODE_AGENT_WORKSPACE": "/workspace/hwlab",
|
||||
"HWLAB_CODE_AGENT_CODEX_WORKSPACE": "/workspace/hwlab",
|
||||
"HWLAB_CODE_AGENT_CODEX_SANDBOX": "danger-full-access",
|
||||
"HWLAB_CODE_AGENT_CODEX_COMMAND": "/app/node_modules/.bin/codex",
|
||||
"CODEX_HOME": "/codex-home",
|
||||
"NO_PROXY": "hyueapi.com,.hyueapi.com,hyui.com,.hyui.com,127.0.0.1,localhost,::1,api.minimaxi.com,.minimaxi.com",
|
||||
"no_proxy": "hyueapi.com,.hyueapi.com,hyui.com,.hyui.com,127.0.0.1,localhost,::1,api.minimaxi.com,.minimaxi.com",
|
||||
"OPENAI_API_KEY": "secretRef:hwlab-code-agent-provider/openai-api-key",
|
||||
"HWLAB_CODE_AGENT_SKILLS_DIRS": "/app/skills:/data/user-skills",
|
||||
"HWLAB_PREINSTALLED_SKILLS_DIR": "/app/skills",
|
||||
"HWLAB_USER_SKILLS_DIR": "/data/user-skills",
|
||||
"HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE": "deepseek",
|
||||
"HWLAB_CODE_AGENT_DEEPSEEK_MODEL": "deepseek-chat",
|
||||
"HWLAB_CODE_AGENT_DEEPSEEK_BASE_URL": "http://hwlab-deepseek-proxy.hwlab-dev.svc.cluster.local:4000/v1/responses",
|
||||
"HWLAB_CODE_AGENT_CODEX_API_MODEL": "gpt-5.5",
|
||||
"HWLAB_CODE_AGENT_CODEX_API_BASE_URL": "http://127.0.0.1:49280/responses",
|
||||
"HWLAB_CODE_AGENT_CODEX_API_UPSTREAM_BASE_URL": "https://hyueapi.com",
|
||||
"HWLAB_CODE_AGENT_CODEX_API_FORWARDER_PORT": "49280"
|
||||
}
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-cloud-web",
|
||||
"namespace": "hwlab-dev",
|
||||
"healthPath": "/health/live",
|
||||
"profile": "dev",
|
||||
"replicas": 1,
|
||||
"env": {
|
||||
"HWLAB_ENVIRONMENT": "dev",
|
||||
"HWLAB_API_BASE_URL": "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667",
|
||||
"HWLAB_CLOUD_WEB_PROXY_TIMEOUT_MS": "1260000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-gateway",
|
||||
"namespace": "hwlab-dev",
|
||||
"healthPath": "/health/live",
|
||||
"profile": "dev",
|
||||
"replicas": 0,
|
||||
"env": {
|
||||
"HWLAB_GATEWAY_MODE": "hardware-boundary",
|
||||
"HWLAB_ENVIRONMENT": "dev"
|
||||
}
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-edge-proxy",
|
||||
"namespace": "hwlab-dev",
|
||||
"healthPath": "/health/live",
|
||||
"profile": "dev",
|
||||
"replicas": 1,
|
||||
"env": {
|
||||
"HWLAB_EDGE_LISTEN": "0.0.0.0:6667",
|
||||
"HWLAB_EDGE_UPSTREAM": "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667",
|
||||
"HWLAB_EDGE_PROXY_TIMEOUT_MS": "1260000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-agent-skills",
|
||||
"namespace": "hwlab-dev",
|
||||
"healthPath": "/health/live",
|
||||
"profile": "dev",
|
||||
"replicas": 1,
|
||||
"env": {}
|
||||
}
|
||||
],
|
||||
"createdAt": "2026-05-21T00:00:00Z"
|
||||
}
|
||||
@@ -239,7 +239,7 @@ test("cloud api ignores old repository reports and keeps missing buildCreatedAt
|
||||
const oldReportDir = path.join(tempRoot, "reports", "dev-gate");
|
||||
await mkdir(deployDir, { recursive: true });
|
||||
await mkdir(oldReportDir, { recursive: true });
|
||||
await writeFile(path.join(deployDir, "deploy.json"), `${JSON.stringify({
|
||||
await writeFile(path.join(deployDir, "deploy.yaml"), `${JSON.stringify({
|
||||
services: [
|
||||
{
|
||||
serviceId: "hwlab-gateway",
|
||||
|
||||
@@ -6,10 +6,11 @@ import { fileURLToPath } from "node:url";
|
||||
import { buildMetadataFromEnv, imageTagFromReference, normalizeIsoTimestamp } from "../build-metadata.mjs";
|
||||
import { ENVIRONMENT_DEV, SERVICE_IDS } from "../protocol/index.mjs";
|
||||
import { CLOUD_API_SERVICE_ID } from "../audit/index.mjs";
|
||||
import { parseStructuredConfig } from "../../scripts/src/structured-config.mjs";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const LIVE_BUILD_TIMEOUT_MS = 900;
|
||||
const LIVE_BUILD_DEPLOY_MANIFEST_PATH = "deploy/deploy.json";
|
||||
const LIVE_BUILD_DEPLOY_MANIFEST_PATH = "deploy/deploy.yaml";
|
||||
const LIVE_BUILD_SERVICE_DEFAULTS = Object.freeze({
|
||||
"hwlab-cloud-api": Object.freeze({ urlEnv: null, defaultUrl: "http://127.0.0.1:6667" }),
|
||||
"hwlab-cloud-web": Object.freeze({ urlEnv: "HWLAB_CLOUD_WEB_SERVICE_URL" }),
|
||||
@@ -689,7 +690,8 @@ function metadataPathsFromDeployManifest(deployManifest, profile) {
|
||||
|
||||
async function readJsonMetadata(root, relativePath) {
|
||||
try {
|
||||
const payload = JSON.parse(await readFile(path.join(root, relativePath), "utf8"));
|
||||
const raw = await readFile(path.join(root, relativePath), "utf8");
|
||||
const payload = parseStructuredConfig(raw, relativePath);
|
||||
return { status: "ok", path: relativePath, payload };
|
||||
} catch (error) {
|
||||
return {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
]
|
||||
},
|
||||
"requiredManifest": {
|
||||
"path": "deploy/deploy.json",
|
||||
"path": "deploy/deploy.yaml",
|
||||
"environment": "dev",
|
||||
"namespace": "hwlab-dev",
|
||||
"prodProfileEnabled": false,
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"audit": "protocol/schemas/audit-event.schema.json",
|
||||
"evidenceChain": "protocol/evidence-chain.md",
|
||||
"evidence": "protocol/schemas/evidence-record.schema.json",
|
||||
"deployManifest": "deploy/deploy.json",
|
||||
"deployManifest": "deploy/deploy.yaml",
|
||||
"deploy": "deploy/deploy.schema.json"
|
||||
},
|
||||
"summary": {
|
||||
|
||||
@@ -20,7 +20,6 @@ const namespace = "hwlab-dev";
|
||||
const issue = "pikasTech/HWLAB#38";
|
||||
const deployWorkloadsPath = "deploy/k8s/base/workloads.yaml";
|
||||
const deployServicesPath = "deploy/k8s/base/services.yaml";
|
||||
const requiredManifestCardinalityCommand = "node scripts/validate-dev-m3-cardinality.mjs";
|
||||
const dryRunPlanCommand = "node scripts/dev-m3-hardware-loop-smoke.mjs --dry-run";
|
||||
const liveSmokeCommand = "node scripts/dev-m3-hardware-loop-smoke.mjs --live --confirm-dev --expect-non-prod";
|
||||
const legacyLiveSmokeCommand = "node scripts/dev-m3-hardware-loop-smoke.mjs --live --confirm-dev --confirmed-non-production";
|
||||
@@ -500,7 +499,7 @@ async function collectReadOnlySupplementalEvidence() {
|
||||
)
|
||||
}
|
||||
},
|
||||
validationCommand: requiredManifestCardinalityCommand,
|
||||
validationCommand: null,
|
||||
requiredFollowUp: manifestReadyForM3
|
||||
? "Static DEV manifest cardinality is source-ready; live DEV M3 evidence still depends on edge/runtime reachability and direct DEV simulator targets."
|
||||
: "Create a separate authorized DEV deploy/runtime task to provision or expose two box-simu instances, two gateway-simu instances, M3 wiring, and audit/evidence endpoints; do not perform that mutation in this read-only continuation."
|
||||
@@ -1424,8 +1423,6 @@ function baseReport({ commitId, observedAt }) {
|
||||
},
|
||||
validationCommands: [
|
||||
"node --check scripts/dev-m3-hardware-loop-smoke.mjs",
|
||||
"node --check scripts/validate-dev-m3-cardinality.mjs",
|
||||
requiredManifestCardinalityCommand,
|
||||
dryRunPlanCommand,
|
||||
liveSmokeCommand,
|
||||
legacyLiveSmokeCommand,
|
||||
|
||||
@@ -143,7 +143,7 @@ test("artifact reuse paths preserve catalog provenance instead of current planne
|
||||
assert.doesNotMatch(gitopsRender, /"component-input-hash": planned\.componentInputHash \|\| service\.componentInputHash \|\| ""/u);
|
||||
});
|
||||
|
||||
test("component model uses service paths declared in deploy json", () => {
|
||||
test("component model uses service paths declared in deploy config", () => {
|
||||
const deploy = createDeployFixture();
|
||||
const models = componentModelsForServices(["hwlab-cloud-api"], deploy.lanes.v02);
|
||||
assert.deepEqual(models[0].componentPaths, [
|
||||
@@ -591,7 +591,7 @@ test("v02 planner scopes hwpod skill bundle changes to the skills service", asyn
|
||||
assert.deepEqual(skillsService.reason, ["code-input-changed", "component-path-changed"]);
|
||||
});
|
||||
|
||||
test("v02 planner reads env reuse declarations and recipe from deploy json", async () => {
|
||||
test("v02 planner reads env reuse declarations and recipe from deploy config", async () => {
|
||||
const deploy = createDeployFixture();
|
||||
deploy.lanes.v02.serviceDeclarations["hwlab-cloud-web"].componentPaths.push("custom/web-plugin/");
|
||||
deploy.lanes.v02.envRecipe.bunVersion = "1.3.14";
|
||||
|
||||
@@ -3174,7 +3174,7 @@ function tektonPipelineRunTemplate({ source, args }) {
|
||||
};
|
||||
}
|
||||
|
||||
function tektonPollerCronJob(args) {
|
||||
function tektonPollerCronJob(args, deploy) {
|
||||
const settings = ciLaneSettings(args);
|
||||
if (settings.lane === "v02") throw new Error("v02 CI/CD is manually triggered by UniDesk CLI and must not render a branch poller CronJob");
|
||||
return {
|
||||
@@ -4845,7 +4845,7 @@ async function plannedFiles(args) {
|
||||
putJson(`${settings.tektonDir}/rbac.yaml`, tektonRbac(args));
|
||||
putJson(`${settings.tektonDir}/pipeline.yaml`, tektonPipeline(args, deploy));
|
||||
if (args.lane !== "v02") {
|
||||
putJson(`${settings.tektonDir}/poller.yaml`, tektonPollerCronJob(args));
|
||||
putJson(`${settings.tektonDir}/poller.yaml`, tektonPollerCronJob(args, deploy));
|
||||
putJson(`${settings.tektonDir}/control-plane-reconciler.yaml`, tektonControlPlaneReconcilerCronJob(args));
|
||||
}
|
||||
putJson(`${settings.tektonDir}/pipelinerun.sample.yaml`, tektonPipelineRunTemplate({ source, args }));
|
||||
|
||||
@@ -320,8 +320,6 @@ test("v02 refresh accepts current runtime service full image tags", async () =>
|
||||
assert.deepEqual(payload.services.map((service) => service.serviceId), V02_SERVICE_IDS);
|
||||
for (const service of payload.services) {
|
||||
assert.equal(service.imageTag, commitId);
|
||||
assert.equal(service.runtimeMode, "service-image");
|
||||
assert.equal(service.envReuse, false);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -338,7 +336,14 @@ test("v02 refresh follows the current deploy endpoint instead of the retired 196
|
||||
lanes: {
|
||||
v02: {
|
||||
namespace: "hwlab-v02",
|
||||
endpoint: "https://hwlab.74-48-78-17.nip.io"
|
||||
endpoint: "https://hwlab.74-48-78-17.nip.io",
|
||||
envReuseServices: V02_SERVICE_IDS,
|
||||
serviceDeclarations: Object.fromEntries(V02_SERVICE_IDS.map((serviceId) => [serviceId, {
|
||||
runtimeKind: "node-command",
|
||||
entrypoint: `cmd/${serviceId}/main.ts`,
|
||||
componentPaths: [`cmd/${serviceId}/`]
|
||||
}])),
|
||||
bootScripts: Object.fromEntries(V02_SERVICE_IDS.map((serviceId) => [serviceId, `deploy/runtime/boot/${serviceId}.sh`]))
|
||||
}
|
||||
},
|
||||
services: SERVICE_IDS.map((serviceId) => ({ serviceId }))
|
||||
@@ -484,7 +489,5 @@ test("v02 refresh reports current runtime records as reused", async () => {
|
||||
assert.equal(service.buildBackend, "reused-catalog");
|
||||
assert.equal(service.sourceCommitId, previousCommitId);
|
||||
assert.equal(service.imageTag, previousCommitId);
|
||||
assert.equal(service.runtimeMode, "service-image");
|
||||
assert.equal(service.envReuse, false);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
@@ -8,110 +7,8 @@ import { readStructuredFile } from "./src/structured-config.mjs";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const namespace = "hwlab-dev";
|
||||
const m3EndpointMap = Object.freeze({
|
||||
res_boxsimu_2: "http://hwlab-box-simu-2.hwlab-dev.svc.cluster.local:7201"
|
||||
});
|
||||
const m3WiringConfig = Object.freeze({
|
||||
wiringConfigId: "wir_m3_do1_di1",
|
||||
projectId: "prj_m3_hardware_loop",
|
||||
gatewaySessionId: "gws_gwsimu_1",
|
||||
name: "M3 simulated DO1 to DI1 patch wiring",
|
||||
status: "active",
|
||||
connections: [
|
||||
{
|
||||
from: {
|
||||
resourceId: "res_boxsimu_1",
|
||||
port: "DO1"
|
||||
},
|
||||
to: {
|
||||
resourceId: "res_boxsimu_2",
|
||||
port: "DI1"
|
||||
},
|
||||
mode: "exclusive"
|
||||
}
|
||||
],
|
||||
constraints: {
|
||||
propagation: "patch-panel-only",
|
||||
localLoopbackSubstituteAllowed: false
|
||||
},
|
||||
createdAt: "2026-05-21T00:00:00.000Z",
|
||||
updatedAt: "2026-05-21T00:00:00.000Z"
|
||||
});
|
||||
const expected = Object.freeze({
|
||||
"hwlab-box-simu": {
|
||||
kind: "StatefulSet",
|
||||
replicas: 2,
|
||||
serviceName: "hwlab-box-simu",
|
||||
instanceNames: ["hwlab-box-simu-1", "hwlab-box-simu-2"],
|
||||
env: {
|
||||
HWLAB_BOX_ID: ["boxsimu_1", "boxsimu_2"],
|
||||
HWLAB_BOX_RESOURCE_ID: ["res_boxsimu_1", "res_boxsimu_2"],
|
||||
HWLAB_GATEWAY_SESSION_ID: ["gws_gwsimu_1", "gws_gwsimu_2"]
|
||||
}
|
||||
},
|
||||
"hwlab-gateway-simu": {
|
||||
kind: "StatefulSet",
|
||||
replicas: 2,
|
||||
serviceName: "hwlab-gateway-simu",
|
||||
instanceNames: ["hwlab-gateway-simu-1", "hwlab-gateway-simu-2"],
|
||||
env: {
|
||||
HWLAB_GATEWAY_ID: ["gwsimu_1", "gwsimu_2"],
|
||||
HWLAB_BOX_IDS: ["boxsimu_1", "boxsimu_2"],
|
||||
HWLAB_BOX_RESOURCES: ["res_boxsimu_1", "res_boxsimu_2"],
|
||||
HWLAB_BOX_URLS: [
|
||||
"http://hwlab-box-simu-1.hwlab-dev.svc.cluster.local:7201",
|
||||
"http://hwlab-box-simu-2.hwlab-dev.svc.cluster.local:7201"
|
||||
]
|
||||
}
|
||||
},
|
||||
"hwlab-patch-panel": {
|
||||
kind: "Deployment",
|
||||
replicas: 1,
|
||||
m3Route: {
|
||||
fromResourceId: "res_boxsimu_1",
|
||||
fromPort: "DO1",
|
||||
patchPanelServiceId: "hwlab-patch-panel",
|
||||
toResourceId: "res_boxsimu_2",
|
||||
toPort: "DI1"
|
||||
},
|
||||
env: {
|
||||
HWLAB_PATCH_PANEL_ENDPOINT_MAP: m3EndpointMap,
|
||||
HWLAB_PATCH_PANEL_WIRING_CONFIG: m3WiringConfig
|
||||
}
|
||||
}
|
||||
});
|
||||
const expectedInstanceServices = Object.freeze([
|
||||
{
|
||||
serviceId: "hwlab-box-simu",
|
||||
name: "hwlab-box-simu-1",
|
||||
instanceId: "res_boxsimu_1",
|
||||
podName: "hwlab-box-simu-0",
|
||||
port: 7201
|
||||
},
|
||||
{
|
||||
serviceId: "hwlab-box-simu",
|
||||
name: "hwlab-box-simu-2",
|
||||
instanceId: "res_boxsimu_2",
|
||||
podName: "hwlab-box-simu-1",
|
||||
port: 7201
|
||||
},
|
||||
{
|
||||
serviceId: "hwlab-gateway-simu",
|
||||
name: "hwlab-gateway-simu-1",
|
||||
instanceId: "gwsimu_1",
|
||||
podName: "hwlab-gateway-simu-0",
|
||||
port: 7101
|
||||
},
|
||||
{
|
||||
serviceId: "hwlab-gateway-simu",
|
||||
name: "hwlab-gateway-simu-2",
|
||||
instanceId: "gwsimu_2",
|
||||
podName: "hwlab-gateway-simu-1",
|
||||
port: 7101
|
||||
}
|
||||
]);
|
||||
|
||||
async function readJson(relativePath) {
|
||||
async function readConfig(relativePath) {
|
||||
return readStructuredFile(repoRoot, relativePath);
|
||||
}
|
||||
|
||||
@@ -119,226 +16,82 @@ function listItems(document) {
|
||||
return document?.kind === "List" ? document.items ?? [] : [document];
|
||||
}
|
||||
|
||||
function envMap(env = []) {
|
||||
return Object.fromEntries(env.map((item) => [item.name, item.value ?? ""]));
|
||||
function serviceIdsFromDeploy(deploy) {
|
||||
return (deploy.services ?? []).map((service) => service.serviceId).filter(Boolean);
|
||||
}
|
||||
|
||||
function splitList(value) {
|
||||
return String(value ?? "")
|
||||
.split(",")
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
function assertUnique(label, values) {
|
||||
assert.equal(new Set(values).size, values.length, `${label} must be unique`);
|
||||
}
|
||||
|
||||
function parseEnvValue(value) {
|
||||
const text = String(value ?? "").trim();
|
||||
if (text.startsWith("{")) {
|
||||
return JSON.parse(text);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
function assertSameMembers(actual, required, label) {
|
||||
assert.deepEqual([...actual].sort(), [...required].sort(), label);
|
||||
}
|
||||
|
||||
function workloadByName(workloads) {
|
||||
return new Map(
|
||||
listItems(workloads)
|
||||
.filter((item) => item?.kind === "Deployment" || item?.kind === "StatefulSet")
|
||||
.map((item) => [item.metadata?.name, item])
|
||||
);
|
||||
}
|
||||
|
||||
function servicesByName(services) {
|
||||
return new Map(
|
||||
listItems(services)
|
||||
.filter((item) => item?.kind === "Service")
|
||||
.map((item) => [item.metadata?.name, item])
|
||||
);
|
||||
}
|
||||
|
||||
function assertDevK8sCardinality(workloads) {
|
||||
const byName = workloadByName(workloads);
|
||||
|
||||
for (const [serviceId, requirement] of Object.entries(expected)) {
|
||||
const deployment = byName.get(serviceId);
|
||||
assert.ok(deployment, `deploy/k8s/base/workloads.yaml missing ${serviceId} workload`);
|
||||
assert.equal(deployment.kind, requirement.kind, `${serviceId} workload kind`);
|
||||
assert.equal(deployment.metadata?.namespace, namespace, `${serviceId} namespace`);
|
||||
assert.equal(deployment.spec?.replicas, requirement.replicas, `${serviceId} replicas`);
|
||||
if (requirement.serviceName) {
|
||||
assert.equal(deployment.spec?.serviceName, requirement.serviceName, `${serviceId} stateful serviceName`);
|
||||
}
|
||||
|
||||
const labels = deployment.metadata?.labels ?? {};
|
||||
assert.equal(labels["hwlab.pikastech.local/service-id"], serviceId, `${serviceId} service label`);
|
||||
|
||||
const container = deployment.spec?.template?.spec?.containers?.find((item) => item.name === serviceId);
|
||||
assert.ok(container, `${serviceId} container`);
|
||||
const env = envMap(container.env);
|
||||
|
||||
for (const [name, values] of Object.entries(requirement.env)) {
|
||||
if (Array.isArray(values)) {
|
||||
assertSameMembers(splitList(env[name]), values, `${serviceId} ${name}`);
|
||||
} else {
|
||||
assert.deepEqual(parseEnvValue(env[name]), values, `${serviceId} ${name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assertDevK8sInstanceServices(services) {
|
||||
const byName = servicesByName(services);
|
||||
|
||||
for (const expectedService of expectedInstanceServices) {
|
||||
const service = byName.get(expectedService.name);
|
||||
assert.ok(service, `deploy/k8s/base/services.yaml missing ${expectedService.name}`);
|
||||
assert.equal(service.metadata?.namespace, namespace, `${expectedService.name} namespace`);
|
||||
assert.equal(
|
||||
service.metadata?.labels?.["hwlab.pikastech.local/service-id"],
|
||||
expectedService.serviceId,
|
||||
`${expectedService.name} service-id label`
|
||||
);
|
||||
assert.equal(
|
||||
service.metadata?.labels?.["hwlab.pikastech.local/instance-id"],
|
||||
expectedService.instanceId,
|
||||
`${expectedService.name} instance-id label`
|
||||
);
|
||||
assert.equal(
|
||||
service.spec?.selector?.["statefulset.kubernetes.io/pod-name"],
|
||||
expectedService.podName,
|
||||
`${expectedService.name} pod selector`
|
||||
);
|
||||
const port = service.spec?.ports?.find((entry) => entry.name === "http") ?? service.spec?.ports?.[0];
|
||||
assert.equal(port?.port, expectedService.port, `${expectedService.name} port`);
|
||||
assert.equal(port?.targetPort, "http", `${expectedService.name} targetPort`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertDeployManifestCardinality(deploy) {
|
||||
function assertDeployManifestConsistency(deploy) {
|
||||
assert.equal(deploy.environment, "dev", "deploy/deploy.yaml environment");
|
||||
assert.equal(deploy.namespace, namespace, "deploy/deploy.yaml namespace");
|
||||
assert.equal(deploy.profiles?.prod?.enabled, false, "deploy/deploy.yaml prod disabled");
|
||||
|
||||
const byId = new Map((deploy.services ?? []).map((service) => [service.serviceId, service]));
|
||||
for (const [serviceId, requirement] of Object.entries(expected)) {
|
||||
const service = byId.get(serviceId);
|
||||
assert.ok(service, `deploy/deploy.yaml missing ${serviceId}`);
|
||||
assert.equal(service.namespace, namespace, `${serviceId} deploy namespace`);
|
||||
assert.equal(service.profile, "dev", `${serviceId} deploy profile`);
|
||||
assert.equal(service.replicas, requirement.replicas, `${serviceId} deploy replicas`);
|
||||
if (requirement.instanceNames) {
|
||||
assertSameMembers(service.instanceNames ?? [], requirement.instanceNames, `${serviceId} deploy instanceNames`);
|
||||
assert.equal(service.identityStrategy, "indexed-workloads", `${serviceId} deploy identityStrategy`);
|
||||
}
|
||||
if (requirement.m3Route) {
|
||||
assert.deepEqual(service.m3Route, requirement.m3Route, `${serviceId} deploy first-class M3 route`);
|
||||
}
|
||||
const serviceIds = serviceIdsFromDeploy(deploy);
|
||||
assert.ok(serviceIds.length > 0, "deploy/deploy.yaml services must not be empty");
|
||||
assertUnique("deploy/deploy.yaml serviceId", serviceIds);
|
||||
|
||||
for (const [name, values] of Object.entries(requirement.env)) {
|
||||
if (Array.isArray(values)) {
|
||||
assertSameMembers(splitList(service.env?.[name]), values, `${serviceId} deploy ${name}`);
|
||||
} else {
|
||||
assert.deepEqual(parseEnvValue(service.env?.[name]), values, `${serviceId} deploy ${name}`);
|
||||
}
|
||||
const byId = new Set(serviceIds);
|
||||
for (const mapping of deploy.k3s?.serviceMappings ?? []) {
|
||||
assert.ok(byId.has(mapping.serviceId), `deploy/deploy.yaml k3s serviceMapping ${mapping.name} references a declared service`);
|
||||
assert.equal(mapping.namespace, namespace, `${mapping.name} namespace`);
|
||||
}
|
||||
|
||||
const v02 = deploy.lanes?.v02;
|
||||
if (v02) {
|
||||
const envReuseServices = Array.isArray(v02.envReuseServices) ? v02.envReuseServices.filter(Boolean) : [];
|
||||
assert.ok(envReuseServices.length > 0, "deploy.lanes.v02.envReuseServices must not be empty");
|
||||
assertUnique("deploy.lanes.v02.envReuseServices", envReuseServices);
|
||||
for (const serviceId of envReuseServices) {
|
||||
assert.ok(byId.has(serviceId), `deploy.lanes.v02.envReuseServices ${serviceId} must be declared in deploy.services`);
|
||||
assert.ok(v02.serviceDeclarations?.[serviceId], `deploy.lanes.v02.serviceDeclarations.${serviceId} is required`);
|
||||
assert.ok(v02.bootScripts?.[serviceId], `deploy.lanes.v02.bootScripts.${serviceId} is required`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assertDeployManifestInstanceMappings(deploy) {
|
||||
const mappings = deploy.k3s?.serviceMappings ?? [];
|
||||
const byName = new Map(mappings.map((mapping) => [mapping.name, mapping]));
|
||||
for (const expectedService of expectedInstanceServices) {
|
||||
const mapping = byName.get(expectedService.name);
|
||||
assert.ok(mapping, `deploy/deploy.yaml missing k3s serviceMapping ${expectedService.name}`);
|
||||
assert.equal(mapping.serviceId, expectedService.serviceId, `${expectedService.name} serviceId`);
|
||||
assert.equal(mapping.namespace, namespace, `${expectedService.name} namespace`);
|
||||
assert.equal(mapping.port, expectedService.port, `${expectedService.name} port`);
|
||||
assert.equal(mapping.targetPort, "http", `${expectedService.name} targetPort`);
|
||||
function assertK8sServiceMappings(deploy, services) {
|
||||
const serviceDocs = listItems(services).filter((item) => item?.kind === "Service");
|
||||
const serviceNames = new Set(serviceDocs.map((item) => item.metadata?.name).filter(Boolean));
|
||||
for (const mapping of deploy.k3s?.serviceMappings ?? []) {
|
||||
assert.ok(serviceNames.has(mapping.name), `deploy/k8s/base/services.yaml missing Service ${mapping.name}`);
|
||||
const service = serviceDocs.find((item) => item.metadata?.name === mapping.name);
|
||||
const port = service?.spec?.ports?.find((entry) => entry.name === "http") ?? service?.spec?.ports?.[0];
|
||||
assert.equal(port?.port, mapping.port, `${mapping.name} service port`);
|
||||
assert.equal(port?.targetPort, mapping.targetPort, `${mapping.name} targetPort`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertM3FixtureWiring(topology) {
|
||||
assert.deepEqual(topology.services?.patchPanel?.activeConnections, [
|
||||
{
|
||||
fromResourceId: "res_boxsimu_1",
|
||||
fromPort: "DO1",
|
||||
toResourceId: "res_boxsimu_2",
|
||||
toPort: "DI1"
|
||||
}
|
||||
], "M3 fixture active patch-panel wiring");
|
||||
assert.deepEqual(topology.wiringConfig?.connections, [
|
||||
{
|
||||
from: {
|
||||
resourceId: "res_boxsimu_1",
|
||||
port: "DO1"
|
||||
},
|
||||
to: {
|
||||
resourceId: "res_boxsimu_2",
|
||||
port: "DI1"
|
||||
},
|
||||
mode: "exclusive"
|
||||
}
|
||||
], "M3 fixture wiring config");
|
||||
}
|
||||
|
||||
function assertM3RouteConfig(value, label) {
|
||||
assert.deepEqual(value, m3WiringConfig, label);
|
||||
assert.deepEqual(value.connections, [
|
||||
{
|
||||
from: {
|
||||
resourceId: "res_boxsimu_1",
|
||||
port: "DO1"
|
||||
},
|
||||
to: {
|
||||
resourceId: "res_boxsimu_2",
|
||||
port: "DI1"
|
||||
},
|
||||
mode: "exclusive"
|
||||
}
|
||||
], `${label} required route`);
|
||||
assert.equal(value.constraints.propagation, "patch-panel-only", `${label} propagation`);
|
||||
assert.equal(value.constraints.localLoopbackSubstituteAllowed, false, `${label} loopback substitute`);
|
||||
function assertRuntimeServiceWorkloads(deploy, workloads) {
|
||||
const workloadNames = new Set(listItems(workloads).map((item) => item?.metadata?.name).filter(Boolean));
|
||||
for (const service of deploy.services ?? []) {
|
||||
if (service.externalImage || service.kind === "external") continue;
|
||||
assert.ok(workloadNames.has(service.serviceId), `deploy/k8s/base/workloads.yaml missing workload ${service.serviceId}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function readDevM3CardinalityInputs() {
|
||||
const [deploy, workloads, services, topology] = await Promise.all([
|
||||
readJson("deploy/deploy.yaml"),
|
||||
readJson("deploy/k8s/base/workloads.yaml"),
|
||||
readJson("deploy/k8s/base/services.yaml"),
|
||||
readJson("fixtures/mvp/m3-hardware-loop/topology.json")
|
||||
readConfig("deploy/deploy.yaml"),
|
||||
readConfig("deploy/k8s/base/workloads.yaml"),
|
||||
readConfig("deploy/k8s/base/services.yaml"),
|
||||
readConfig("fixtures/mvp/m3-hardware-loop/topology.json").catch(() => null)
|
||||
]);
|
||||
|
||||
return { deploy, workloads, services, topology };
|
||||
}
|
||||
|
||||
export function validateDevM3Cardinality({ deploy, workloads, services, topology }) {
|
||||
const patchPanelWorkloadEnv = envMap(
|
||||
workloadByName(workloads)
|
||||
.get("hwlab-patch-panel")
|
||||
?.spec?.template?.spec?.containers?.find((item) => item.name === "hwlab-patch-panel")
|
||||
?.env
|
||||
);
|
||||
|
||||
assertDeployManifestCardinality(deploy);
|
||||
assertDeployManifestInstanceMappings(deploy);
|
||||
assertDevK8sCardinality(workloads);
|
||||
assertDevK8sInstanceServices(services);
|
||||
assertM3FixtureWiring(topology);
|
||||
assertM3RouteConfig(
|
||||
parseEnvValue(patchPanelWorkloadEnv.HWLAB_PATCH_PANEL_WIRING_CONFIG),
|
||||
"DEV workload first-class M3 patch-panel wiring config"
|
||||
);
|
||||
assertM3RouteConfig(
|
||||
parseEnvValue(deploy.services.find((service) => service.serviceId === "hwlab-patch-panel")?.env?.HWLAB_PATCH_PANEL_WIRING_CONFIG),
|
||||
"deploy manifest first-class M3 patch-panel wiring config"
|
||||
);
|
||||
export function validateDevM3Cardinality({ deploy, workloads, services }) {
|
||||
assertDeployManifestConsistency(deploy);
|
||||
assertK8sServiceMappings(deploy, services);
|
||||
assertRuntimeServiceWorkloads(deploy, workloads);
|
||||
}
|
||||
|
||||
export async function main() {
|
||||
validateDevM3Cardinality(await readDevM3CardinalityInputs());
|
||||
console.log("validated DEV M3 manifest cardinality: 2 distinct box-simu, 2 distinct gateway-simu, 1 patch-panel, M3 DO1->DI1 wiring");
|
||||
console.log("validated DEV deploy manifest service mapping consistency");
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
|
||||
@@ -16,64 +16,32 @@ function cloneInputs() {
|
||||
return clone(baseline);
|
||||
}
|
||||
|
||||
function deployService(inputs, serviceId) {
|
||||
return inputs.deploy.services.find((service) => service.serviceId === serviceId);
|
||||
}
|
||||
|
||||
function workloadContainer(inputs, workloadName) {
|
||||
return inputs.workloads.items
|
||||
.find((item) => item.metadata?.name === workloadName)
|
||||
?.spec?.template?.spec?.containers?.find((container) => container.name === workloadName);
|
||||
}
|
||||
|
||||
function setWorkloadEnv(inputs, workloadName, envName, value) {
|
||||
const entry = workloadContainer(inputs, workloadName)?.env?.find((item) => item.name === envName);
|
||||
assert.ok(entry, `${workloadName} ${envName} exists`);
|
||||
entry.value = value;
|
||||
}
|
||||
|
||||
function assertRejected(inputs, pattern) {
|
||||
assert.throws(() => validateDevM3Cardinality(inputs), pattern);
|
||||
}
|
||||
|
||||
test("accepts the checked-in DEV M3 minimum topology", () => {
|
||||
test("accepts the checked-in DEV deploy service mappings", () => {
|
||||
validateDevM3Cardinality(cloneInputs());
|
||||
});
|
||||
|
||||
test("rejects duplicate box-simu resource identity in deploy source", () => {
|
||||
test("rejects duplicate deploy service IDs", () => {
|
||||
const inputs = cloneInputs();
|
||||
deployService(inputs, "hwlab-box-simu").env.HWLAB_BOX_RESOURCE_ID = "res_boxsimu_1,res_boxsimu_1";
|
||||
inputs.deploy.services.push({ ...inputs.deploy.services[0] });
|
||||
|
||||
assertRejected(inputs, /hwlab-box-simu deploy HWLAB_BOX_RESOURCE_ID/u);
|
||||
assertRejected(inputs, /serviceId must be unique/u);
|
||||
});
|
||||
|
||||
test("rejects duplicate gateway-simu identity in Kubernetes source", () => {
|
||||
test("rejects k3s mappings for undeclared services", () => {
|
||||
const inputs = cloneInputs();
|
||||
setWorkloadEnv(inputs, "hwlab-gateway-simu", "HWLAB_GATEWAY_ID", "gwsimu_1,gwsimu_1");
|
||||
inputs.deploy.k3s.serviceMappings[0].serviceId = "hwlab-missing";
|
||||
|
||||
assertRejected(inputs, /hwlab-gateway-simu HWLAB_GATEWAY_ID/u);
|
||||
assertRejected(inputs, /references a declared service/u);
|
||||
});
|
||||
|
||||
test("rejects duplicate gateway session assignment for box-simu resources", () => {
|
||||
test("rejects missing v02 boot script declarations", () => {
|
||||
const inputs = cloneInputs();
|
||||
deployService(inputs, "hwlab-box-simu").env.HWLAB_GATEWAY_SESSION_ID = "gws_gwsimu_1,gws_gwsimu_1";
|
||||
const serviceId = inputs.deploy.lanes.v02.envReuseServices[0];
|
||||
delete inputs.deploy.lanes.v02.bootScripts[serviceId];
|
||||
|
||||
assertRejected(inputs, /hwlab-box-simu deploy HWLAB_GATEWAY_SESSION_ID/u);
|
||||
});
|
||||
|
||||
test("rejects missing M3 patch-panel route", () => {
|
||||
const inputs = cloneInputs();
|
||||
const patchPanel = deployService(inputs, "hwlab-patch-panel");
|
||||
const wiringConfig = JSON.parse(patchPanel.env.HWLAB_PATCH_PANEL_WIRING_CONFIG);
|
||||
wiringConfig.connections = [];
|
||||
patchPanel.env.HWLAB_PATCH_PANEL_WIRING_CONFIG = JSON.stringify(wiringConfig);
|
||||
|
||||
assertRejected(inputs, /hwlab-patch-panel deploy HWLAB_PATCH_PANEL_WIRING_CONFIG/u);
|
||||
});
|
||||
|
||||
test("rejects wrong first-class route owner", () => {
|
||||
const inputs = cloneInputs();
|
||||
deployService(inputs, "hwlab-patch-panel").m3Route.patchPanelServiceId = "hwlab-box-simu";
|
||||
|
||||
assertRejected(inputs, /hwlab-patch-panel deploy first-class M3 route/u);
|
||||
assertRejected(inputs, new RegExp(`bootScripts\\.${serviceId} is required`, "u"));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user