fix: map M3 indexed service identities

Host closeout: reviewed source/test diff and clean merge state. Accepting only the EndpointSlice identity mapping and regression test. The runner's live-mutating M3 smoke evidence is excluded from acceptance because the task prompt forbade live mutation; M3 acceptance remains separate and requires explicit authorized live validation.
This commit is contained in:
Lyon
2026-05-23 10:23:13 +08:00
committed by GitHub
2 changed files with 160 additions and 7 deletions
+72 -7
View File
@@ -76,6 +76,39 @@ const serviceTargets = Object.freeze([
}
]);
const kubernetesM3ServiceIdentities = Object.freeze({
"hwlab-box-simu": Object.freeze({
serviceId: "hwlab-box-simu"
}),
"hwlab-box-simu-1": Object.freeze({
serviceId: "hwlab-box-simu",
targetId: "box-simu-1",
instanceId: "res_boxsimu_1"
}),
"hwlab-box-simu-2": Object.freeze({
serviceId: "hwlab-box-simu",
targetId: "box-simu-2",
instanceId: "res_boxsimu_2"
}),
"hwlab-gateway-simu": Object.freeze({
serviceId: "hwlab-gateway-simu"
}),
"hwlab-gateway-simu-1": Object.freeze({
serviceId: "hwlab-gateway-simu",
targetId: "gateway-simu-1",
instanceId: "gwsimu_1"
}),
"hwlab-gateway-simu-2": Object.freeze({
serviceId: "hwlab-gateway-simu",
targetId: "gateway-simu-2",
instanceId: "gwsimu_2"
}),
"hwlab-patch-panel": Object.freeze({
serviceId: "hwlab-patch-panel",
targetId: "patch-panel"
})
});
function parseArgs(argv) {
const flags = new Set();
const values = new Map();
@@ -744,6 +777,22 @@ function serviceDefinitionByServiceId(serviceId) {
return serviceTargets.find((target) => target.serviceId === serviceId);
}
function kubernetesServiceIdentity(item) {
const serviceName = item?.metadata?.labels?.["kubernetes.io/service-name"];
const mapped = kubernetesM3ServiceIdentities[serviceName];
if (!mapped) {
return null;
}
const labels = item?.metadata?.labels ?? {};
return {
serviceName,
serviceId: mapped.serviceId,
targetId: mapped.targetId ?? null,
instanceId: labels["hwlab.pikastech.local/instance-id"] ?? mapped.instanceId ?? null
};
}
function serviceEndpointUrl(address, port) {
return `http://${address}:${port}`;
}
@@ -795,14 +844,14 @@ function collectKubernetesDirectTargetDiscovery() {
};
}
const requiredServices = new Set(["hwlab-box-simu", "hwlab-gateway-simu", "hwlab-patch-panel"]);
const services = {};
for (const item of document.items ?? []) {
const serviceId = item.metadata?.labels?.["kubernetes.io/service-name"];
if (!requiredServices.has(serviceId)) {
const identity = kubernetesServiceIdentity(item);
if (!identity) {
continue;
}
const { serviceName, serviceId, targetId, instanceId } = identity;
const port = item.ports?.find((entry) => entry.name === "http")?.port ?? item.ports?.[0]?.port;
if (!Number.isInteger(port)) {
continue;
@@ -817,12 +866,20 @@ function collectKubernetesDirectTargetDiscovery() {
url: serviceEndpointUrl(address, port),
address,
port,
targetRef: endpoint.targetRef?.name ?? "unknown"
targetRef: endpoint.targetRef?.name ?? "unknown",
serviceName,
serviceId,
targetId,
instanceId
});
}
}
services[serviceId] = {
services[serviceName] = {
endpointSlice: item.metadata?.name ?? "unknown",
serviceName,
serviceId,
targetId,
instanceId,
port,
count: endpoints.length,
endpoints
@@ -841,7 +898,8 @@ function collectKubernetesDirectTargetDiscovery() {
function candidateTargetsFromKubernetes(discovery) {
const candidates = [];
for (const [serviceId, service] of Object.entries(discovery.services ?? {})) {
for (const [serviceName, service] of Object.entries(discovery.services ?? {})) {
const serviceId = service.serviceId ?? serviceName;
const definition = serviceDefinitionByServiceId(serviceId);
if (!definition) {
continue;
@@ -850,9 +908,11 @@ function candidateTargetsFromKubernetes(discovery) {
for (const endpoint of service.endpoints ?? []) {
candidates.push({
...definition,
id: `${definition.id}-candidate-${index}`,
id: service.targetId ?? endpoint.targetId ?? `${definition.id}-candidate-${index}`,
baseUrl: endpoint.url,
discoverySource: discovery.source,
serviceName,
instanceId: endpoint.instanceId ?? service.instanceId ?? null,
targetRef: endpoint.targetRef,
address: endpoint.address,
port: endpoint.port
@@ -883,6 +943,8 @@ function summarizeCandidate(candidate) {
return {
id: candidate.id,
serviceId: candidate.serviceId,
serviceName: candidate.serviceName,
instanceId: candidate.instanceId,
baseUrl: candidate.baseUrl,
targetRef: candidate.targetRef,
ok: candidate.ok,
@@ -1719,8 +1781,11 @@ async function main() {
export {
classifyDirectTargetBlockerScope,
classifyLiveOperationError,
candidateTargetsFromKubernetes,
createLiveOperationPlan,
collectKubernetesDirectTargetDiscovery,
hasRequiredM3Connection,
kubernetesServiceIdentity,
m3FailureClassifications,
parseArgs,
requireSafetyGates
@@ -6,9 +6,11 @@ import test from "node:test";
import { fileURLToPath } from "node:url";
import {
candidateTargetsFromKubernetes,
classifyDirectTargetBlockerScope,
createLiveOperationPlan,
hasRequiredM3Connection,
kubernetesServiceIdentity,
parseArgs,
requireSafetyGates
} from "./dev-m3-hardware-loop-smoke.mjs";
@@ -153,3 +155,89 @@ test("patch-panel route preflight rejects missing res_boxsimu_1 DO1 to res_boxsi
assert.equal(Boolean(requiredRoute.active), true);
assert.equal(Boolean(requiredRoute.configured), true);
});
test("Kubernetes discovery preserves current indexed M3 service identities", () => {
const endpointSlice = (serviceName, address, port, targetRef, labels = {}) => ({
metadata: {
name: `${serviceName}-slice`,
labels: {
"kubernetes.io/service-name": serviceName,
...labels
}
},
ports: [{ name: "http", port }],
endpoints: [
{
addresses: [address],
conditions: { ready: true },
targetRef: { name: targetRef }
}
]
});
const discovery = {
status: "observed",
source: "kubernetes-endpointslices",
services: Object.fromEntries(
[
endpointSlice("hwlab-box-simu-1", "10.0.0.11", 7201, "hwlab-box-simu-0", {
"hwlab.pikastech.local/instance-id": "res_boxsimu_1"
}),
endpointSlice("hwlab-box-simu-2", "10.0.0.12", 7201, "hwlab-box-simu-1", {
"hwlab.pikastech.local/instance-id": "res_boxsimu_2"
}),
endpointSlice("hwlab-gateway-simu-1", "10.0.0.21", 7101, "hwlab-gateway-simu-0", {
"hwlab.pikastech.local/instance-id": "gwsimu_1"
}),
endpointSlice("hwlab-gateway-simu-2", "10.0.0.22", 7101, "hwlab-gateway-simu-1", {
"hwlab.pikastech.local/instance-id": "gwsimu_2"
}),
endpointSlice("hwlab-patch-panel", "10.0.0.31", 7301, "hwlab-patch-panel-abc")
].map((item) => {
const identity = kubernetesServiceIdentity(item);
const port = item.ports[0].port;
const endpoints = item.endpoints.flatMap((endpoint) =>
endpoint.addresses.map((address) => ({
url: `http://${address}:${port}`,
address,
port,
targetRef: endpoint.targetRef.name,
serviceName: identity.serviceName,
serviceId: identity.serviceId,
targetId: identity.targetId,
instanceId: identity.instanceId
}))
);
return [
identity.serviceName,
{
endpointSlice: item.metadata.name,
...identity,
port,
count: endpoints.length,
endpoints
}
];
})
)
};
const candidates = candidateTargetsFromKubernetes(discovery);
assert.deepEqual(
candidates.map((candidate) => [
candidate.id,
candidate.serviceName,
candidate.serviceId,
candidate.instanceId,
candidate.baseUrl
]),
[
["box-simu-1", "hwlab-box-simu-1", "hwlab-box-simu", "res_boxsimu_1", "http://10.0.0.11:7201"],
["box-simu-2", "hwlab-box-simu-2", "hwlab-box-simu", "res_boxsimu_2", "http://10.0.0.12:7201"],
["gateway-simu-1", "hwlab-gateway-simu-1", "hwlab-gateway-simu", "gwsimu_1", "http://10.0.0.21:7101"],
["gateway-simu-2", "hwlab-gateway-simu-2", "hwlab-gateway-simu", "gwsimu_2", "http://10.0.0.22:7101"],
["patch-panel", "hwlab-patch-panel", "hwlab-patch-panel", null, "http://10.0.0.31:7301"]
]
);
});