351 lines
12 KiB
JavaScript
351 lines
12 KiB
JavaScript
#!/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";
|
|
|
|
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) {
|
|
const raw = await readFile(path.join(repoRoot, relativePath), "utf8");
|
|
return JSON.parse(raw);
|
|
}
|
|
|
|
function listItems(document) {
|
|
return document?.kind === "List" ? document.items ?? [] : [document];
|
|
}
|
|
|
|
function envMap(env = []) {
|
|
return Object.fromEntries(env.map((item) => [item.name, item.value ?? ""]));
|
|
}
|
|
|
|
function splitList(value) {
|
|
return String(value ?? "")
|
|
.split(",")
|
|
.map((item) => item.trim())
|
|
.filter(Boolean);
|
|
}
|
|
|
|
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) {
|
|
assert.equal(deploy.environment, "dev", "deploy/deploy.json environment");
|
|
assert.equal(deploy.namespace, namespace, "deploy/deploy.json namespace");
|
|
assert.equal(deploy.profiles?.prod?.enabled, false, "deploy/deploy.json 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.json 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`);
|
|
}
|
|
|
|
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}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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.json 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 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`);
|
|
}
|
|
|
|
export async function readDevM3CardinalityInputs() {
|
|
const [deploy, workloads, services, topology] = await Promise.all([
|
|
readJson("deploy/deploy.json"),
|
|
readJson("deploy/k8s/base/workloads.yaml"),
|
|
readJson("deploy/k8s/base/services.yaml"),
|
|
readJson("fixtures/mvp/m3-hardware-loop/topology.json")
|
|
]);
|
|
|
|
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 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");
|
|
}
|
|
|
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
try {
|
|
await main();
|
|
} catch (error) {
|
|
console.error(error instanceof Error ? error.message : String(error));
|
|
process.exitCode = 1;
|
|
}
|
|
}
|