feat(deploy): add typed command transport releases
This commit is contained in:
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": "nodedc.device-plane.device-control-core-release.v2",
|
||||||
|
"releaseId": "__PATCH_ID__",
|
||||||
|
"action": "upgrade",
|
||||||
|
"predecessor": {
|
||||||
|
"kind": "release",
|
||||||
|
"patchId": "device-control-core-release-20260812-024",
|
||||||
|
"artifactSha256": "a289e909283109642e6bba3d9822a31f63423cfe0bbcd52705979681bd2bc793"
|
||||||
|
},
|
||||||
|
"service": "device-control-core",
|
||||||
|
"composeActivation": "preserve-active-v4-topology",
|
||||||
|
"identity": "reuse-existing-runner-managed-host-local-private-key-public-certificate-export",
|
||||||
|
"identityRecovery": "forbidden-valid-existing-identity-required",
|
||||||
|
"tlsPurpose": "clientAuth",
|
||||||
|
"direction": "core-initiated",
|
||||||
|
"endpointPolicy": "public-ipv4-standard-https-tcp-443-only",
|
||||||
|
"coreNetworks": [
|
||||||
|
"device-plane-private",
|
||||||
|
"device-plane-egress"
|
||||||
|
],
|
||||||
|
"publicIngress": "none-on-synology",
|
||||||
|
"edgeRegistrations": "preserved",
|
||||||
|
"commandTransport": "typed-service-ping-v1",
|
||||||
|
"commandCatalog": "allowlisted-adapter-typed-commands-only",
|
||||||
|
"credentialBoundary": "transient-core-memory-then-single-pinned-mtls-command-envelope-to-edge-never-persisted-never-logged-never-returned",
|
||||||
|
"gelios": "untouched-legacy-only",
|
||||||
|
"preservedServices": [
|
||||||
|
"device-manager",
|
||||||
|
"device-gateway",
|
||||||
|
"device-postgres",
|
||||||
|
"device-backhaul-target"
|
||||||
|
],
|
||||||
|
"healthGate": "bounded-container-grace+core-edge-contract+exact-private-egress-network-boundary",
|
||||||
|
"rollback": "restore-preapply-source-and-core-runtime"
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": "nodedc.device-edge-vps.command-transport.v1",
|
||||||
|
"mode": "provider-neutral-typed-command-transport-over-accepted-core-channel",
|
||||||
|
"status": "active-typed-command-transport",
|
||||||
|
"authority": "DCPLATFORM-21/DCPLATFORM-76/ADR-0001",
|
||||||
|
"component": "device-edge-vps",
|
||||||
|
"phase": "command-transport",
|
||||||
|
"runtimeHost": "koffyvngij",
|
||||||
|
"predecessorPatch": "device-edge-vps-tracker-ingress-20260812-012",
|
||||||
|
"predecessorArtifactSha256": "290acef118839c6b0c31aac864c47da1832a289537366af9322d4624a1dd81ec",
|
||||||
|
"runtimeUser": "nodedc-channel",
|
||||||
|
"runtimeService": "nodedc-device-edge-channel.service",
|
||||||
|
"runtimeComposition": "single-process-core-channel-plus-universal-device-gateway",
|
||||||
|
"publicIngress": "tcp/443-mtls-core-channel+tcp/9921-bidirectional-tracker-session",
|
||||||
|
"trackerIngress": "preserved:allowlisted-adapters-only",
|
||||||
|
"initialAdapterProfile": "arusnavi.b2.internal.v1",
|
||||||
|
"commandTransport": "typed-service-ping-v1",
|
||||||
|
"commandCatalog": "allowlisted-adapter-typed-commands-only",
|
||||||
|
"allowedCommands": [
|
||||||
|
"service.ping"
|
||||||
|
],
|
||||||
|
"credentialBoundary": "transient-over-pinned-mtls-in-memory-until-single-tracker-write-never-stored-never-logged-never-returned",
|
||||||
|
"responseBoundary": "exact-adapter-parser-serv-ok-only",
|
||||||
|
"health": "127.0.0.1:18222",
|
||||||
|
"adapterHealth": "127.0.0.1:18221",
|
||||||
|
"rawDeviceTcp9921": "public-bidirectional-tracker-session-no-generic-forwarding",
|
||||||
|
"gelios": "untouched-legacy-only",
|
||||||
|
"tailscale": "absent",
|
||||||
|
"dataBoundary": "no-vps-database-no-business-logic-no-synology-route",
|
||||||
|
"resourceCeilings": {
|
||||||
|
"memory": "192M",
|
||||||
|
"swap": "0",
|
||||||
|
"cpu": "75%",
|
||||||
|
"tasks": 128,
|
||||||
|
"openFiles": 1024,
|
||||||
|
"sessions": 128,
|
||||||
|
"sessionsPerAddress": 16,
|
||||||
|
"connectionsPerMinutePerAddress": 60,
|
||||||
|
"sessionBufferBytes": 65536,
|
||||||
|
"aggregateBufferBytes": 33554432
|
||||||
|
},
|
||||||
|
"preserved": [
|
||||||
|
"management-ssh-key",
|
||||||
|
"accepted-node-runtime",
|
||||||
|
"accepted-core-channel-trust-and-registration",
|
||||||
|
"accepted-tracker-ingress",
|
||||||
|
"retired-tailnet-boundary",
|
||||||
|
"gelios-production-path"
|
||||||
|
],
|
||||||
|
"forbidden": [
|
||||||
|
"vps-initiated-synology-connection",
|
||||||
|
"generic-tcp-forwarding",
|
||||||
|
"tailscale-runtime",
|
||||||
|
"docker",
|
||||||
|
"public-health",
|
||||||
|
"vps-database",
|
||||||
|
"vps-business-logic",
|
||||||
|
"unregistered-adapter",
|
||||||
|
"raw-command",
|
||||||
|
"firmware-command",
|
||||||
|
"reboot-command",
|
||||||
|
"persistent-command-credential"
|
||||||
|
],
|
||||||
|
"acceptance": [
|
||||||
|
"exact-tracker-ingress-012-predecessor",
|
||||||
|
"single-non-root-edge-process",
|
||||||
|
"core-channel-remains-accepted",
|
||||||
|
"public-tracker-tcp-9921-listening",
|
||||||
|
"adapter-profile-allowlisted",
|
||||||
|
"typed-service-ping-only",
|
||||||
|
"exact-serv-ok-response-parser",
|
||||||
|
"bounded-session-and-buffer-limits",
|
||||||
|
"tailscale-remains-absent",
|
||||||
|
"no-vps-to-synology-route",
|
||||||
|
"gelios-untouched"
|
||||||
|
],
|
||||||
|
"rollback": "restore-exact-tracker-ingress-012-source-and-existing-runtime"
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": "nodedc.device-plane.device-manager-release.v3",
|
||||||
|
"releaseId": "__PATCH_ID__",
|
||||||
|
"action": "upgrade",
|
||||||
|
"predecessor": {
|
||||||
|
"kind": "release",
|
||||||
|
"patchId": "device-manager-release-20260811-010",
|
||||||
|
"artifactSha256": "d4132993216eb674967dc6fc65d9670cfc2a9efdf46186ca019030f259de2d0e"
|
||||||
|
},
|
||||||
|
"controlCorePredecessor": {
|
||||||
|
"patchId": "device-control-core-release-v2-20260812-025",
|
||||||
|
"artifactSha256": "c61b1f0de1bae23de0caa7289036865ea419ff5705611416f736ca929d1592db"
|
||||||
|
},
|
||||||
|
"edgeChannelPredecessor": {
|
||||||
|
"patchId": "device-edge-core-channel-upgrade-v4-20260812-023",
|
||||||
|
"artifactSha256": "c10d5b6b7d55ab239f85b6c8130e34ce9f84985e3b46e6e5534733156c7982fc"
|
||||||
|
},
|
||||||
|
"service": "device-manager",
|
||||||
|
"publicIngress": "reverse-proxy-only",
|
||||||
|
"deviceCoreManagementApi": "file-token-authenticated",
|
||||||
|
"launcherTrust": "file-token-scoped-to-device-core-handoff",
|
||||||
|
"edgeChannel": "preserve-active-v4-core-initiated-pinned-mtls",
|
||||||
|
"edgeChannelIdentity": "reuse-runner-managed-host-local-private-key-public-certificate-export",
|
||||||
|
"edgeChannelEgress": "preserve-dedicated-core-only-bridge-no-host-ingress-public-ipv4-tcp-443-only",
|
||||||
|
"healthGate": "bounded-container-grace+core-contract",
|
||||||
|
"commandTransport": "typed-service-ping-v1",
|
||||||
|
"commandCatalog": "allowlisted-adapter-typed-commands-only",
|
||||||
|
"credentialBoundary": "transient-core-memory-then-single-pinned-mtls-command-envelope-to-edge-never-persisted-never-logged-never-returned",
|
||||||
|
"gelios": "untouched-legacy-only",
|
||||||
|
"rollback": "restore-preapply-snapshot"
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ const platformRoot = resolve(scriptDir, "../..");
|
|||||||
const devicePlaneRoot = resolve(platformRoot, "device-plane");
|
const devicePlaneRoot = resolve(platformRoot, "device-plane");
|
||||||
const artifactDir = resolve(process.env.NODEDC_DEPLOY_ARTIFACT_DIR || resolve(scriptDir, "../deploy-artifacts"));
|
const artifactDir = resolve(process.env.NODEDC_DEPLOY_ARTIFACT_DIR || resolve(scriptDir, "../deploy-artifacts"));
|
||||||
const [
|
const [
|
||||||
patchId = "device-control-core-release-20260812-024",
|
patchId = "device-control-core-release-v2-20260812-025",
|
||||||
predecessorPatchId,
|
predecessorPatchId,
|
||||||
predecessorSha256,
|
predecessorSha256,
|
||||||
...extra
|
...extra
|
||||||
@@ -35,7 +35,10 @@ if (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const descriptorPath = "deployment/device-control-core-release-v1.json";
|
const isV2 = patchId.startsWith("device-control-core-release-v2-");
|
||||||
|
const descriptorPath = isV2
|
||||||
|
? "deployment/device-control-core-release-v2.json"
|
||||||
|
: "deployment/device-control-core-release-v1.json";
|
||||||
const entries = [
|
const entries = [
|
||||||
".dockerignore",
|
".dockerignore",
|
||||||
"package.json",
|
"package.json",
|
||||||
@@ -94,7 +97,7 @@ try {
|
|||||||
|
|
||||||
const descriptor = JSON.parse(await readFile(join(payload, descriptorPath), "utf8"));
|
const descriptor = JSON.parse(await readFile(join(payload, descriptorPath), "utf8"));
|
||||||
if (
|
if (
|
||||||
descriptor.schemaVersion !== "nodedc.device-plane.device-control-core-release.v1"
|
descriptor.schemaVersion !== `nodedc.device-plane.device-control-core-release.${isV2 ? "v2" : "v1"}`
|
||||||
|| descriptor.releaseId !== patchId
|
|| descriptor.releaseId !== patchId
|
||||||
|| descriptor.action !== "upgrade"
|
|| descriptor.action !== "upgrade"
|
||||||
|| descriptor.service !== "device-control-core"
|
|| descriptor.service !== "device-control-core"
|
||||||
@@ -106,9 +109,18 @@ try {
|
|||||||
|| JSON.stringify(descriptor.coreNetworks) !== JSON.stringify(["device-plane-private", "device-plane-egress"])
|
|| JSON.stringify(descriptor.coreNetworks) !== JSON.stringify(["device-plane-private", "device-plane-egress"])
|
||||||
|| descriptor.publicIngress !== "none-on-synology"
|
|| descriptor.publicIngress !== "none-on-synology"
|
||||||
|| descriptor.edgeRegistrations !== "preserved"
|
|| descriptor.edgeRegistrations !== "preserved"
|
||||||
|| descriptor.commandTransport !== "disabled"
|
|| descriptor.commandTransport !== (isV2 ? "typed-service-ping-v1" : "disabled")
|
||||||
|| descriptor.gelios !== "untouched"
|
|| descriptor.gelios !== (isV2 ? "untouched-legacy-only" : "untouched")
|
||||||
|| descriptor.rollback !== "restore-preapply-source-and-core-runtime"
|
|| descriptor.rollback !== "restore-preapply-source-and-core-runtime"
|
||||||
|
|| (
|
||||||
|
isV2
|
||||||
|
&& (
|
||||||
|
descriptor.commandCatalog !== "allowlisted-adapter-typed-commands-only"
|
||||||
|
|| descriptor.credentialBoundary !== "transient-core-memory-then-single-pinned-mtls-command-envelope-to-edge-never-persisted-never-logged-never-returned"
|
||||||
|
|| descriptor.predecessor?.patchId !== "device-control-core-release-20260812-024"
|
||||||
|
|| descriptor.predecessor?.artifactSha256 !== "a289e909283109642e6bba3d9822a31f63423cfe0bbcd52705979681bd2bc793"
|
||||||
|
)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
throw new Error("device_control_core_release_contract_mismatch");
|
throw new Error("device_control_core_release_contract_mismatch");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,11 +37,12 @@ if (
|
|||||||
"core-channel",
|
"core-channel",
|
||||||
"tailscale-retirement",
|
"tailscale-retirement",
|
||||||
"tracker-ingress",
|
"tracker-ingress",
|
||||||
|
"command-transport",
|
||||||
].includes(phase)
|
].includes(phase)
|
||||||
|| !/^[A-Za-z0-9._-]{1,96}$/.test(patchId || "")
|
|| !/^[A-Za-z0-9._-]{1,96}$/.test(patchId || "")
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"usage: build-device-edge-vps-artifact.mjs <foundation|runtime-reconciliation|backhaul|relay|core-channel|tailscale-retirement|tracker-ingress> <patch-id>",
|
"usage: build-device-edge-vps-artifact.mjs <foundation|runtime-reconciliation|backhaul|relay|core-channel|tailscale-retirement|tracker-ingress|command-transport> <patch-id>",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,6 +53,10 @@ if (
|
|||||||
) {
|
) {
|
||||||
throw new Error("vps_initiated_transport_frozen:ADR-0001");
|
throw new Error("vps_initiated_transport_frozen:ADR-0001");
|
||||||
}
|
}
|
||||||
|
const acceptedSharedSourcePhases = new Set(["core-channel", "tracker-ingress"]);
|
||||||
|
if (acceptedSharedSourcePhases.has(phase)) {
|
||||||
|
throw new Error(`accepted_vps_phase_rebuild_frozen:${phase}:ADR-0001`);
|
||||||
|
}
|
||||||
|
|
||||||
const nodeArchive = "node-v22.23.2-linux-x64.tar.xz";
|
const nodeArchive = "node-v22.23.2-linux-x64.tar.xz";
|
||||||
const tailscaleArchive = "tailscale_1.102.2_amd64.tgz";
|
const tailscaleArchive = "tailscale_1.102.2_amd64.tgz";
|
||||||
@@ -110,6 +115,21 @@ const entriesByPhase = {
|
|||||||
"vps/systemd/nodedc-device-edge-runtime.service",
|
"vps/systemd/nodedc-device-edge-runtime.service",
|
||||||
"deployment/device-edge-vps-tracker-ingress-v1.json",
|
"deployment/device-edge-vps-tracker-ingress-v1.json",
|
||||||
],
|
],
|
||||||
|
"command-transport": [
|
||||||
|
"packages/device-edge-channel-contract/package.json",
|
||||||
|
"packages/device-edge-channel-contract/src",
|
||||||
|
"services/device-edge-channel/package.json",
|
||||||
|
"services/device-edge-channel/src",
|
||||||
|
"packages/device-adapter-runtime/package.json",
|
||||||
|
"packages/device-adapter-runtime/src",
|
||||||
|
"packages/device-adapter-catalog/package.json",
|
||||||
|
"packages/device-adapter-catalog/src",
|
||||||
|
"packages/arusnavi-b2-adapter/package.json",
|
||||||
|
"packages/arusnavi-b2-adapter/src",
|
||||||
|
"services/device-gateway/src/runtime.mjs",
|
||||||
|
"vps/edge-process/device-edge-runtime.mjs",
|
||||||
|
"deployment/device-edge-vps-command-transport-v1.json",
|
||||||
|
],
|
||||||
};
|
};
|
||||||
const entries = entriesByPhase[phase];
|
const entries = entriesByPhase[phase];
|
||||||
const ignoredBasenames = new Set([".DS_Store", ".git", "node_modules"]);
|
const ignoredBasenames = new Set([".DS_Store", ".git", "node_modules"]);
|
||||||
@@ -171,10 +191,12 @@ try {
|
|||||||
? "tcp/9921"
|
? "tcp/9921"
|
||||||
: ["core-channel", "tailscale-retirement"].includes(phase)
|
: ["core-channel", "tailscale-retirement"].includes(phase)
|
||||||
? "tcp/443-mtls-only"
|
? "tcp/443-mtls-only"
|
||||||
: phase === "tracker-ingress"
|
: ["tracker-ingress", "command-transport"].includes(phase)
|
||||||
? "tcp/443-mtls+tcp/9921-telemetry"
|
? "tcp/443-mtls+tcp/9921-telemetry"
|
||||||
: "disabled",
|
: "disabled",
|
||||||
commandTransport: "disabled",
|
commandTransport: phase === "command-transport"
|
||||||
|
? "typed-service-ping-v1"
|
||||||
|
: "disabled",
|
||||||
gelios: "untouched",
|
gelios: "untouched",
|
||||||
}, null, 2));
|
}, null, 2));
|
||||||
} finally {
|
} finally {
|
||||||
@@ -190,8 +212,10 @@ async function assertBoundary() {
|
|||||||
if (
|
if (
|
||||||
descriptor.component !== "device-edge-vps"
|
descriptor.component !== "device-edge-vps"
|
||||||
|| descriptor.runtimeHost !== "koffyvngij"
|
|| descriptor.runtimeHost !== "koffyvngij"
|
||||||
|| descriptor.commandTransport !== "disabled"
|
|| descriptor.commandTransport !== (phase === "command-transport"
|
||||||
|| descriptor.gelios !== "untouched"
|
? "typed-service-ping-v1"
|
||||||
|
: "disabled")
|
||||||
|
|| !String(descriptor.gelios || "").startsWith("untouched")
|
||||||
|| !String(descriptor.rollback || "").length
|
|| !String(descriptor.rollback || "").length
|
||||||
) {
|
) {
|
||||||
throw new Error(`descriptor_boundary_mismatch:${phase}`);
|
throw new Error(`descriptor_boundary_mismatch:${phase}`);
|
||||||
@@ -365,6 +389,36 @@ async function assertBoundary() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (phase === "command-transport") {
|
||||||
|
for (const required of [
|
||||||
|
'"predecessorPatch": "device-edge-vps-tracker-ingress-20260812-012"',
|
||||||
|
'"runtimeService": "nodedc-device-edge-channel.service"',
|
||||||
|
'"runtimeComposition": "single-process-core-channel-plus-universal-device-gateway"',
|
||||||
|
'"commandTransport": "typed-service-ping-v1"',
|
||||||
|
'"commandCatalog": "allowlisted-adapter-typed-commands-only"',
|
||||||
|
'"responseBoundary": "exact-adapter-parser-serv-ok-only"',
|
||||||
|
"buildTypedCommand",
|
||||||
|
"parseTypedCommandResponse",
|
||||||
|
"submitCommandStatus",
|
||||||
|
'"service.ping"',
|
||||||
|
]) {
|
||||||
|
if (!combined.includes(required)) {
|
||||||
|
throw new Error(`command_transport_boundary_missing:${required}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const forbidden of [
|
||||||
|
"LocalForward",
|
||||||
|
"tailscale-userspace",
|
||||||
|
"DEVICE_EDGE_RELAY_UPSTREAM",
|
||||||
|
"device.dc.ru",
|
||||||
|
"PRIVATE KEY",
|
||||||
|
"TS_AUTHKEY",
|
||||||
|
]) {
|
||||||
|
if (combined.includes(forbidden)) {
|
||||||
|
throw new Error(`command_transport_boundary_violation:${forbidden}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function canonicalTarScript() {
|
function canonicalTarScript() {
|
||||||
|
|||||||
@@ -12,10 +12,19 @@ const devicePlaneRoot = resolve(platformRoot, "device-plane");
|
|||||||
const designRoot = resolve(process.env.NODEDC_DEVICE_MANAGER_SOURCE_ROOT || resolve(platformRoot, "../NODEDC_DESIGN_GUIDELINE"));
|
const designRoot = resolve(process.env.NODEDC_DEVICE_MANAGER_SOURCE_ROOT || resolve(platformRoot, "../NODEDC_DESIGN_GUIDELINE"));
|
||||||
const managerRoot = resolve(designRoot, "apps/device-manager");
|
const managerRoot = resolve(designRoot, "apps/device-manager");
|
||||||
const artifactDir = resolve(process.env.NODEDC_DEPLOY_ARTIFACT_DIR || resolve(scriptDir, "../deploy-artifacts"));
|
const artifactDir = resolve(process.env.NODEDC_DEPLOY_ARTIFACT_DIR || resolve(scriptDir, "../deploy-artifacts"));
|
||||||
const [patchId = "device-manager-release-20260811-018", ...extra] = process.argv.slice(2);
|
const [patchId = "device-manager-release-v3-20260812-026", ...extra] = process.argv.slice(2);
|
||||||
if (extra.length || !/^[A-Za-z0-9._-]{1,96}$/.test(patchId)) throw new Error("usage: build-device-manager-control-plane-artifact.mjs [patch-id]");
|
if (extra.length || !/^[A-Za-z0-9._-]{1,96}$/.test(patchId)) throw new Error("usage: build-device-manager-control-plane-artifact.mjs [patch-id]");
|
||||||
|
|
||||||
const entries = [
|
const descriptorPath = patchId.startsWith("device-manager-release-v3-")
|
||||||
|
? "deployment/device-manager-release-v3.json"
|
||||||
|
: "deployment/device-manager-release-v1.json";
|
||||||
|
|
||||||
|
const isV3 = descriptorPath.endsWith("release-v3.json");
|
||||||
|
const entries = isV3 ? [
|
||||||
|
"docker-compose.device-manager.yml",
|
||||||
|
"services/device-manager",
|
||||||
|
descriptorPath,
|
||||||
|
] : [
|
||||||
".dockerignore",
|
".dockerignore",
|
||||||
"package.json",
|
"package.json",
|
||||||
"package-lock.json",
|
"package-lock.json",
|
||||||
@@ -27,7 +36,7 @@ const entries = [
|
|||||||
"services/device-gateway/package.json",
|
"services/device-gateway/package.json",
|
||||||
"services/device-edge-relay/package.json",
|
"services/device-edge-relay/package.json",
|
||||||
"services/device-manager",
|
"services/device-manager",
|
||||||
"deployment/device-manager-release-v1.json",
|
descriptorPath,
|
||||||
];
|
];
|
||||||
const stage = await mkdtemp(join(tmpdir(), "nodedc-device-manager-control-plane-"));
|
const stage = await mkdtemp(join(tmpdir(), "nodedc-device-manager-control-plane-"));
|
||||||
const payload = join(stage, "payload");
|
const payload = join(stage, "payload");
|
||||||
@@ -41,7 +50,7 @@ try {
|
|||||||
if (build.status !== 0) throw new Error(`device_manager_build_failed:${build.stderr || build.stdout}`);
|
if (build.status !== 0) throw new Error(`device_manager_build_failed:${build.stderr || build.stdout}`);
|
||||||
await mkdir(payload, { recursive: true });
|
await mkdir(payload, { recursive: true });
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (entry === "deployment/device-manager-release-v1.json") {
|
if (entry === descriptorPath) {
|
||||||
const descriptor = JSON.parse(await readFile(resolve(devicePlaneRoot, entry), "utf8"));
|
const descriptor = JSON.parse(await readFile(resolve(devicePlaneRoot, entry), "utf8"));
|
||||||
if (descriptor.releaseId !== "__PATCH_ID__") {
|
if (descriptor.releaseId !== "__PATCH_ID__") {
|
||||||
throw new Error("device_manager_release_template_id_mismatch");
|
throw new Error("device_manager_release_template_id_mismatch");
|
||||||
@@ -62,15 +71,17 @@ try {
|
|||||||
}
|
}
|
||||||
await copySafe(resolve(devicePlaneRoot, entry), join(payload, entry), devicePlaneRoot);
|
await copySafe(resolve(devicePlaneRoot, entry), join(payload, entry), devicePlaneRoot);
|
||||||
}
|
}
|
||||||
await validateDockerCopySources(
|
if (!isV3) {
|
||||||
join(payload, "services/device-control-core/Dockerfile"),
|
await validateDockerCopySources(
|
||||||
payload,
|
join(payload, "services/device-control-core/Dockerfile"),
|
||||||
);
|
payload,
|
||||||
|
);
|
||||||
|
}
|
||||||
await validateDockerCopySources(
|
await validateDockerCopySources(
|
||||||
join(payload, "services/device-manager/Dockerfile"),
|
join(payload, "services/device-manager/Dockerfile"),
|
||||||
join(payload, "services/device-manager"),
|
join(payload, "services/device-manager"),
|
||||||
);
|
);
|
||||||
for (const modulePath of [
|
for (const modulePath of isV3 ? [] : [
|
||||||
"services/device-control-core/src/sensitive-reference-management.mjs",
|
"services/device-control-core/src/sensitive-reference-management.mjs",
|
||||||
"services/device-control-core/src/device-gateway-core-runtime.mjs",
|
"services/device-control-core/src/device-gateway-core-runtime.mjs",
|
||||||
"packages/device-edge-channel-contract/src/index.mjs",
|
"packages/device-edge-channel-contract/src/index.mjs",
|
||||||
@@ -115,14 +126,10 @@ try {
|
|||||||
]) {
|
]) {
|
||||||
if (compose.includes(forbidden)) throw new Error(`device_manager_compose_boundary_violation:${forbidden}`);
|
if (compose.includes(forbidden)) throw new Error(`device_manager_compose_boundary_violation:${forbidden}`);
|
||||||
}
|
}
|
||||||
const descriptor = JSON.parse(await readFile(
|
const descriptor = JSON.parse(await readFile(join(payload, descriptorPath), "utf8"));
|
||||||
join(payload, "deployment/device-manager-release-v1.json"),
|
|
||||||
"utf8",
|
|
||||||
));
|
|
||||||
const predecessor = descriptor.predecessor;
|
const predecessor = descriptor.predecessor;
|
||||||
if (
|
const commonContractInvalid = (
|
||||||
descriptor.schemaVersion !== "nodedc.device-plane.device-manager-release.v1"
|
descriptor.releaseId !== patchId
|
||||||
|| descriptor.releaseId !== patchId
|
|
||||||
|| !["activate", "upgrade"].includes(descriptor.action)
|
|| !["activate", "upgrade"].includes(descriptor.action)
|
||||||
|| !predecessor
|
|| !predecessor
|
||||||
|| !["reconciliation", "release"].includes(predecessor.kind)
|
|| !["reconciliation", "release"].includes(predecessor.kind)
|
||||||
@@ -131,7 +138,29 @@ try {
|
|||||||
|| (descriptor.action === "activate") !== (predecessor.kind === "reconciliation")
|
|| (descriptor.action === "activate") !== (predecessor.kind === "reconciliation")
|
||||||
|| descriptor.healthGate !== "bounded-container-grace+core-contract"
|
|| descriptor.healthGate !== "bounded-container-grace+core-contract"
|
||||||
|| descriptor.rollback !== "restore-preapply-snapshot"
|
|| descriptor.rollback !== "restore-preapply-snapshot"
|
||||||
) throw new Error("device_manager_activation_successor_contract_mismatch");
|
);
|
||||||
|
if (commonContractInvalid) throw new Error("device_manager_activation_successor_contract_mismatch");
|
||||||
|
if (descriptorPath.endsWith("release-v3.json")) {
|
||||||
|
if (
|
||||||
|
descriptor.schemaVersion !== "nodedc.device-plane.device-manager-release.v3"
|
||||||
|
|| descriptor.commandTransport !== "typed-service-ping-v1"
|
||||||
|
|| descriptor.commandCatalog !== "allowlisted-adapter-typed-commands-only"
|
||||||
|
|| descriptor.credentialBoundary !== "transient-core-memory-then-single-pinned-mtls-command-envelope-to-edge-never-persisted-never-logged-never-returned"
|
||||||
|
|| descriptor.controlCorePredecessor?.patchId !== "device-control-core-release-v2-20260812-025"
|
||||||
|
|| descriptor.controlCorePredecessor?.artifactSha256 !== "c61b1f0de1bae23de0caa7289036865ea419ff5705611416f736ca929d1592db"
|
||||||
|
|| descriptor.edgeChannelPredecessor?.patchId !== "device-edge-core-channel-upgrade-v4-20260812-023"
|
||||||
|
|| descriptor.edgeChannelPredecessor?.artifactSha256 !== "c10d5b6b7d55ab239f85b6c8130e34ce9f84985e3b46e6e5534733156c7982fc"
|
||||||
|
|| descriptor.edgeChannel !== "preserve-active-v4-core-initiated-pinned-mtls"
|
||||||
|
|| descriptor.edgeChannelEgress !== "preserve-dedicated-core-only-bridge-no-host-ingress-public-ipv4-tcp-443-only"
|
||||||
|
|| descriptor.gelios !== "untouched-legacy-only"
|
||||||
|
) throw new Error("device_manager_v3_typed_command_contract_mismatch");
|
||||||
|
} else if (
|
||||||
|
descriptor.schemaVersion !== "nodedc.device-plane.device-manager-release.v1"
|
||||||
|
|| descriptor.commandTransport !== "disabled"
|
||||||
|
|| descriptor.gelios !== "untouched"
|
||||||
|
) {
|
||||||
|
throw new Error("device_manager_v1_contract_mismatch");
|
||||||
|
}
|
||||||
await writeFile(join(stage, "manifest.env"), `id=${patchId}\ncomponent=device-plane\ntype=app-overlay\n`, "utf8");
|
await writeFile(join(stage, "manifest.env"), `id=${patchId}\ncomponent=device-plane\ntype=app-overlay\n`, "utf8");
|
||||||
await writeFile(join(stage, "files.txt"), `${entries.join("\n")}\n`, "utf8");
|
await writeFile(join(stage, "files.txt"), `${entries.join("\n")}\n`, "utf8");
|
||||||
await mkdir(artifactDir, { recursive: true });
|
await mkdir(artifactDir, { recursive: true });
|
||||||
@@ -145,7 +174,7 @@ try {
|
|||||||
artifact: target,
|
artifact: target,
|
||||||
sha256,
|
sha256,
|
||||||
entries,
|
entries,
|
||||||
services: ["device-control-core", "device-manager"],
|
services: isV3 ? ["device-manager"] : ["device-control-core", "device-manager"],
|
||||||
preserved: ["device-postgres", "device-gateway", "device-backhaul-target", "Gelios"],
|
preserved: ["device-postgres", "device-gateway", "device-backhaul-target", "Gelios"],
|
||||||
}, null, 2));
|
}, null, 2));
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -112,6 +112,10 @@ TAILSCALE_RETIREMENT_ACCEPTED_PATCH = (
|
|||||||
TAILSCALE_RETIREMENT_ACCEPTED_SHA256 = (
|
TAILSCALE_RETIREMENT_ACCEPTED_SHA256 = (
|
||||||
"e7b61ec9c83122fa5631467010eff871b98935746df6a1326b6ff6bb9713d877"
|
"e7b61ec9c83122fa5631467010eff871b98935746df6a1326b6ff6bb9713d877"
|
||||||
)
|
)
|
||||||
|
TRACKER_INGRESS_ACCEPTED_PATCH = "device-edge-vps-tracker-ingress-20260812-012"
|
||||||
|
TRACKER_INGRESS_ACCEPTED_SHA256 = (
|
||||||
|
"290acef118839c6b0c31aac864c47da1832a289537366af9322d4624a1dd81ec"
|
||||||
|
)
|
||||||
|
|
||||||
FOUNDATION_ENTRIES = (
|
FOUNDATION_ENTRIES = (
|
||||||
"vps/config/00-nodedc-b2-vps.conf",
|
"vps/config/00-nodedc-b2-vps.conf",
|
||||||
@@ -162,6 +166,21 @@ TRACKER_INGRESS_ENTRIES = (
|
|||||||
"vps/systemd/nodedc-device-edge-runtime.service",
|
"vps/systemd/nodedc-device-edge-runtime.service",
|
||||||
"deployment/device-edge-vps-tracker-ingress-v1.json",
|
"deployment/device-edge-vps-tracker-ingress-v1.json",
|
||||||
)
|
)
|
||||||
|
COMMAND_TRANSPORT_ENTRIES = (
|
||||||
|
"packages/device-edge-channel-contract/package.json",
|
||||||
|
"packages/device-edge-channel-contract/src",
|
||||||
|
"services/device-edge-channel/package.json",
|
||||||
|
"services/device-edge-channel/src",
|
||||||
|
"packages/device-adapter-runtime/package.json",
|
||||||
|
"packages/device-adapter-runtime/src",
|
||||||
|
"packages/device-adapter-catalog/package.json",
|
||||||
|
"packages/device-adapter-catalog/src",
|
||||||
|
"packages/arusnavi-b2-adapter/package.json",
|
||||||
|
"packages/arusnavi-b2-adapter/src",
|
||||||
|
"services/device-gateway/src/runtime.mjs",
|
||||||
|
"vps/edge-process/device-edge-runtime.mjs",
|
||||||
|
"deployment/device-edge-vps-command-transport-v1.json",
|
||||||
|
)
|
||||||
|
|
||||||
PHASE_ENTRIES = {
|
PHASE_ENTRIES = {
|
||||||
"foundation": FOUNDATION_ENTRIES,
|
"foundation": FOUNDATION_ENTRIES,
|
||||||
@@ -171,6 +190,7 @@ PHASE_ENTRIES = {
|
|||||||
"core-channel": CORE_CHANNEL_ENTRIES,
|
"core-channel": CORE_CHANNEL_ENTRIES,
|
||||||
"tailscale-retirement": TAILSCALE_RETIREMENT_ENTRIES,
|
"tailscale-retirement": TAILSCALE_RETIREMENT_ENTRIES,
|
||||||
"tracker-ingress": TRACKER_INGRESS_ENTRIES,
|
"tracker-ingress": TRACKER_INGRESS_ENTRIES,
|
||||||
|
"command-transport": COMMAND_TRANSPORT_ENTRIES,
|
||||||
}
|
}
|
||||||
|
|
||||||
SUPERSEDED_TRANSPORT_PHASES = frozenset({"backhaul", "relay"})
|
SUPERSEDED_TRANSPORT_PHASES = frozenset({"backhaul", "relay"})
|
||||||
@@ -262,6 +282,36 @@ PHASE_FILE_SHA256 = {
|
|||||||
"deployment/device-edge-vps-tracker-ingress-v1.json":
|
"deployment/device-edge-vps-tracker-ingress-v1.json":
|
||||||
"faecfa0317ad0e3d102c3636c0547b65257b930e9b48f34d8679c00ceb3d323b",
|
"faecfa0317ad0e3d102c3636c0547b65257b930e9b48f34d8679c00ceb3d323b",
|
||||||
},
|
},
|
||||||
|
"command-transport": {
|
||||||
|
"packages/device-edge-channel-contract/package.json":
|
||||||
|
"57d5349b5dcef2cacd4f3e4fad010359a65d59f5f903eff07d89f67c497f97c0",
|
||||||
|
"packages/device-edge-channel-contract/src/index.mjs":
|
||||||
|
"a49bfe31476a08f8a912c10c82b982fae28a29e2c0eb5828ced390b01f3a10bc",
|
||||||
|
"services/device-edge-channel/package.json":
|
||||||
|
"bdf502be43b62bdd6db05b022a532d93ba954277ac5143d6058d2f27f6a2e9d2",
|
||||||
|
"services/device-edge-channel/src/runtime.mjs":
|
||||||
|
"39579d85edf50be07573873a1f59045f62a74469c85588db1fd4e5151b3b778d",
|
||||||
|
"services/device-edge-channel/src/server.mjs":
|
||||||
|
"a82057218bb368ab926404f90a19cc17c0359b57dc890f38a1324ab8c497c17b",
|
||||||
|
"packages/device-adapter-runtime/package.json":
|
||||||
|
"293ee6010f255c511df40b34cf8019fcd0d1b06babe30761c2a842c7da7af778",
|
||||||
|
"packages/device-adapter-runtime/src/index.mjs":
|
||||||
|
"199fcdd775d1ea15ddc9f85ab24fc2c165bb7757008de26749f1b7e84481dfeb",
|
||||||
|
"packages/device-adapter-catalog/package.json":
|
||||||
|
"60dbfc551ca49719e0851755058c159968411c490621eac0bf96ca76bf8b78d1",
|
||||||
|
"packages/device-adapter-catalog/src/index.mjs":
|
||||||
|
"614749e763c0624bf0de7905343632d3edb4e026316f427a2f52ce12c4225dca",
|
||||||
|
"packages/arusnavi-b2-adapter/package.json":
|
||||||
|
"136fc997f5d6757ba81d370f03037e0a93fd4750dc624def1af593e7ba8b9098",
|
||||||
|
"packages/arusnavi-b2-adapter/src/index.mjs":
|
||||||
|
"790060720639e3835bbf60fb2bdc67172a7dac79a8f8e4bf1558ab51ed130c91",
|
||||||
|
"services/device-gateway/src/runtime.mjs":
|
||||||
|
"f1898baef2a964ee04adc55f7e26ae8a637fcc61701cf28d7cf4b3bda1579b25",
|
||||||
|
"vps/edge-process/device-edge-runtime.mjs":
|
||||||
|
"4443876d4652de5d953840896acccec93119f9ec642b5d5cdaeaac19a2022017",
|
||||||
|
"deployment/device-edge-vps-command-transport-v1.json":
|
||||||
|
"971166143fe954b9c5043cce9a464d17efbc87933da1405b2517a4693a7bed09",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
# Exact immutable baselines from terminally accepted predecessor artifacts.
|
# Exact immutable baselines from terminally accepted predecessor artifacts.
|
||||||
@@ -421,8 +471,9 @@ def validate_payload(payload: Path, phase: str):
|
|||||||
if (
|
if (
|
||||||
descriptor.get("component") != COMPONENT
|
descriptor.get("component") != COMPONENT
|
||||||
or descriptor.get("runtimeHost") != RUNTIME_HOST
|
or descriptor.get("runtimeHost") != RUNTIME_HOST
|
||||||
or descriptor.get("commandTransport") != "disabled"
|
or descriptor.get("commandTransport")
|
||||||
or descriptor.get("gelios") != "untouched"
|
!= ("typed-service-ping-v1" if phase == "command-transport" else "disabled")
|
||||||
|
or not str(descriptor.get("gelios", "")).startswith("untouched")
|
||||||
or not descriptor.get("rollback")
|
or not descriptor.get("rollback")
|
||||||
):
|
):
|
||||||
die(f"VPS {phase} descriptor mismatch")
|
die(f"VPS {phase} descriptor mismatch")
|
||||||
@@ -674,6 +725,18 @@ def current_phase_preflight(phase: str):
|
|||||||
if (LIVE_ROOT / TRACKER_INGRESS_ENTRIES[-1]).exists():
|
if (LIVE_ROOT / TRACKER_INGRESS_ENTRIES[-1]).exists():
|
||||||
die("VPS tracker ingress target path already exists")
|
die("VPS tracker ingress target path already exists")
|
||||||
return {"predecessor": "accepted-tailscale-retirement-011"}
|
return {"predecessor": "accepted-tailscale-retirement-011"}
|
||||||
|
if phase == "command-transport":
|
||||||
|
tracker_record = applied_phase_record("tracker-ingress")
|
||||||
|
if (
|
||||||
|
tracker_record.get("patch") != TRACKER_INGRESS_ACCEPTED_PATCH
|
||||||
|
or tracker_record.get("sha256") != TRACKER_INGRESS_ACCEPTED_SHA256
|
||||||
|
):
|
||||||
|
die("VPS command transport tracker ingress predecessor mismatch")
|
||||||
|
source_file_state("tracker-ingress")
|
||||||
|
validate_tracker_ingress_runtime()
|
||||||
|
if (LIVE_ROOT / COMMAND_TRANSPORT_ENTRIES[-1]).exists():
|
||||||
|
die("VPS command transport target path already exists")
|
||||||
|
return {"predecessor": "accepted-tracker-ingress-012"}
|
||||||
validate_foundation_runtime(
|
validate_foundation_runtime(
|
||||||
require_running_tailnet=phase in {"backhaul", "relay"},
|
require_running_tailnet=phase in {"backhaul", "relay"},
|
||||||
expected_key_user=BACKHAUL_USER if phase == "relay" else SERVICE_USER,
|
expected_key_user=BACKHAUL_USER if phase == "relay" else SERVICE_USER,
|
||||||
@@ -783,6 +846,8 @@ def backup_targets_for_phase(phase: str):
|
|||||||
]
|
]
|
||||||
if phase == "tracker-ingress":
|
if phase == "tracker-ingress":
|
||||||
return common + [CHANNEL_UNIT, NFTABLES_CONFIG]
|
return common + [CHANNEL_UNIT, NFTABLES_CONFIG]
|
||||||
|
if phase == "command-transport":
|
||||||
|
return common + [CHANNEL_UNIT, NFTABLES_CONFIG]
|
||||||
return common + [RELAY_UNIT, NFTABLES_CONFIG]
|
return common + [RELAY_UNIT, NFTABLES_CONFIG]
|
||||||
|
|
||||||
|
|
||||||
@@ -1265,6 +1330,12 @@ def apply_tracker_ingress(_payload: Path):
|
|||||||
validate_tracker_ingress_runtime()
|
validate_tracker_ingress_runtime()
|
||||||
|
|
||||||
|
|
||||||
|
def apply_command_transport(_payload: Path):
|
||||||
|
systemctl("daemon-reload")
|
||||||
|
systemctl("restart", "nodedc-device-edge-channel.service")
|
||||||
|
validate_command_transport_runtime()
|
||||||
|
|
||||||
|
|
||||||
def sshd_effective():
|
def sshd_effective():
|
||||||
return run(["/usr/sbin/sshd", "-T"]).stdout.lower()
|
return run(["/usr/sbin/sshd", "-T"]).stdout.lower()
|
||||||
|
|
||||||
@@ -1620,6 +1691,92 @@ def validate_tracker_ingress_runtime():
|
|||||||
return health
|
return health
|
||||||
|
|
||||||
|
|
||||||
|
def validate_command_transport_runtime():
|
||||||
|
source_file_state("foundation")
|
||||||
|
source_file_state("tailscale-retirement")
|
||||||
|
source_file_state("command-transport")
|
||||||
|
tracker_record = applied_phase_record("tracker-ingress")
|
||||||
|
if (
|
||||||
|
tracker_record.get("patch") != TRACKER_INGRESS_ACCEPTED_PATCH
|
||||||
|
or tracker_record.get("sha256") != TRACKER_INGRESS_ACCEPTED_SHA256
|
||||||
|
):
|
||||||
|
die("command transport tracker ingress identity mismatch")
|
||||||
|
if run([str(NODE_BIN), "--version"]).stdout.strip() != f"v{NODE_VERSION}":
|
||||||
|
die("command transport Node version mismatch")
|
||||||
|
for path in (
|
||||||
|
TAILSCALE_UNIT,
|
||||||
|
TAILSCALE_SOCKET,
|
||||||
|
TAILSCALE_STATE.parent,
|
||||||
|
TAILSCALE_BIN.parent,
|
||||||
|
TRUST_ROOT,
|
||||||
|
):
|
||||||
|
if path.exists() or path.is_symlink():
|
||||||
|
die(f"command transport superseded trust boundary present: {path}")
|
||||||
|
for service in (
|
||||||
|
"nodedc-b2-tailscaled.service",
|
||||||
|
"nodedc-b2-backhaul.service",
|
||||||
|
"nodedc-b2-relay.service",
|
||||||
|
):
|
||||||
|
if service_active(service):
|
||||||
|
die(f"command transport forbidden service became active: {service}")
|
||||||
|
assert_management_key()
|
||||||
|
assert_channel_trust(require_runtime_owner=True)
|
||||||
|
if not service_active("nodedc-device-edge-channel.service"):
|
||||||
|
die("VPS Device Edge command runtime is not active")
|
||||||
|
health = core_channel_health(require_accepted=True)
|
||||||
|
expected = {
|
||||||
|
"ok": True,
|
||||||
|
"service": "nodedc-device-edge-runtime",
|
||||||
|
"channel": "accepted",
|
||||||
|
"trackerIngress": "telemetry-ingest",
|
||||||
|
"commandTransport": "typed-service-ping-v1",
|
||||||
|
}
|
||||||
|
for key, value in expected.items():
|
||||||
|
if health.get(key) != value:
|
||||||
|
die(f"VPS command transport health contract mismatch: {key}")
|
||||||
|
tracker = health.get("tracker") or {}
|
||||||
|
for key, value in {
|
||||||
|
"adapter": "arusnavi-b2",
|
||||||
|
"protocolProfile": "arusnavi.b2.internal.v1",
|
||||||
|
"framing": "verified-read-only",
|
||||||
|
"publicIngress": "telemetry-ingest",
|
||||||
|
"commandTransport": "typed-service-ping-v1",
|
||||||
|
}.items():
|
||||||
|
if tracker.get(key) != value:
|
||||||
|
die(f"VPS command adapter health contract mismatch: {key}")
|
||||||
|
for port in (22, CHANNEL_PUBLIC_PORT, 9921):
|
||||||
|
if not port_is_open(PUBLIC_IPV4, port, timeout=5):
|
||||||
|
die(f"VPS command transport required listener unavailable: {port}")
|
||||||
|
if not port_is_open("127.0.0.1", 18221, timeout=5):
|
||||||
|
die("VPS command transport adapter health unavailable")
|
||||||
|
if port_is_open("127.0.0.1", 1055):
|
||||||
|
die("VPS retired Tailscale SOCKS listener returned")
|
||||||
|
nft = run(["/usr/sbin/nft", "list", "table", "inet", "nodedc_b2_vps"]).stdout
|
||||||
|
for required in ("policy drop", "tcp dport 22", "tcp dport 443", "tcp dport 9921"):
|
||||||
|
if required not in nft:
|
||||||
|
die(f"VPS command transport firewall contract mismatch: {required}")
|
||||||
|
unit = run([
|
||||||
|
"/usr/bin/systemctl",
|
||||||
|
"show",
|
||||||
|
"nodedc-device-edge-channel.service",
|
||||||
|
"--property=User,Group,NoNewPrivileges,CapabilityBoundingSet,AmbientCapabilities,MemoryMax,MemorySwapMax,CPUQuotaPerSecUSec,TasksMax,LimitNOFILE",
|
||||||
|
]).stdout
|
||||||
|
for required in (
|
||||||
|
"User=nodedc-channel",
|
||||||
|
"Group=nodedc-channel",
|
||||||
|
"NoNewPrivileges=yes",
|
||||||
|
"CapabilityBoundingSet=cap_net_bind_service",
|
||||||
|
"AmbientCapabilities=cap_net_bind_service",
|
||||||
|
"MemoryMax=201326592",
|
||||||
|
"MemorySwapMax=0",
|
||||||
|
"TasksMax=128",
|
||||||
|
"LimitNOFILE=1024",
|
||||||
|
):
|
||||||
|
if required not in unit:
|
||||||
|
die(f"VPS command transport resource boundary mismatch: {required}")
|
||||||
|
return health
|
||||||
|
|
||||||
|
|
||||||
def validate_relay_runtime():
|
def validate_relay_runtime():
|
||||||
validate_backhaul_runtime()
|
validate_backhaul_runtime()
|
||||||
source_file_state("relay")
|
source_file_state("relay")
|
||||||
@@ -1789,7 +1946,7 @@ def plan_artifact(artifact_argument: str):
|
|||||||
print("tracker_tcp_9921=closed")
|
print("tracker_tcp_9921=closed")
|
||||||
print("services=preserved:nodedc-device-edge-channel")
|
print("services=preserved:nodedc-device-edge-channel")
|
||||||
print("external_tailnet_machine_cleanup=required-after-deploy-ok")
|
print("external_tailnet_machine_cleanup=required-after-deploy-ok")
|
||||||
else:
|
elif phase == "tracker-ingress":
|
||||||
print("public_core_channel=preserved:155.212.211.15:443/tcp:tls13-mtls-h2")
|
print("public_core_channel=preserved:155.212.211.15:443/tcp:tls13-mtls-h2")
|
||||||
print(f"health=127.0.0.1:{CHANNEL_HEALTH_PORT}:combined-edge-runtime")
|
print(f"health=127.0.0.1:{CHANNEL_HEALTH_PORT}:combined-edge-runtime")
|
||||||
print("public_b2_ingress=155.212.211.15:9921/tcp:telemetry-only")
|
print("public_b2_ingress=155.212.211.15:9921/tcp:telemetry-only")
|
||||||
@@ -1798,8 +1955,27 @@ def plan_artifact(artifact_argument: str):
|
|||||||
print("runtime_composition=single-non-root-process:core-channel+universal-gateway")
|
print("runtime_composition=single-non-root-process:core-channel+universal-gateway")
|
||||||
print("tailscale=preserved:absent")
|
print("tailscale=preserved:absent")
|
||||||
print("services=recreate:nodedc-device-edge-channel")
|
print("services=recreate:nodedc-device-edge-channel")
|
||||||
print("command_transport=disabled")
|
else:
|
||||||
print("gelios=untouched")
|
print("public_core_channel=preserved:155.212.211.15:443/tcp:tls13-mtls-h2")
|
||||||
|
print(f"health=127.0.0.1:{CHANNEL_HEALTH_PORT}:combined-edge-runtime")
|
||||||
|
print("public_b2_ingress=155.212.211.15:9921/tcp:bidirectional-session")
|
||||||
|
print("tracker_adapter=arusnavi-b2:profile=arusnavi.b2.internal.v1")
|
||||||
|
print("tracker_ack=after-core-durable-acceptance-only")
|
||||||
|
print("runtime_composition=single-non-root-process:core-channel+universal-gateway")
|
||||||
|
print("tailscale=preserved:absent")
|
||||||
|
print("services=recreate:nodedc-device-edge-channel")
|
||||||
|
print(
|
||||||
|
"command_transport=typed-service-ping-v1:allowlisted-adapter-only"
|
||||||
|
if phase == "command-transport"
|
||||||
|
else "command_transport=disabled"
|
||||||
|
)
|
||||||
|
if phase == "command-transport":
|
||||||
|
print("command_catalog=allowlisted-adapter-typed-commands-only")
|
||||||
|
print(
|
||||||
|
"gelios=untouched-legacy-only"
|
||||||
|
if phase == "command-transport"
|
||||||
|
else "gelios=untouched"
|
||||||
|
)
|
||||||
print("dns=unchanged")
|
print("dns=unchanged")
|
||||||
print("b2_routes=unchanged")
|
print("b2_routes=unchanged")
|
||||||
print("state=new")
|
print("state=new")
|
||||||
@@ -1835,8 +2011,10 @@ def apply_artifact(artifact_argument: str):
|
|||||||
apply_core_channel(loaded["payload"])
|
apply_core_channel(loaded["payload"])
|
||||||
elif loaded["phase"] == "tailscale-retirement":
|
elif loaded["phase"] == "tailscale-retirement":
|
||||||
apply_tailscale_retirement(loaded["payload"])
|
apply_tailscale_retirement(loaded["payload"])
|
||||||
else:
|
elif loaded["phase"] == "tracker-ingress":
|
||||||
apply_tracker_ingress(loaded["payload"])
|
apply_tracker_ingress(loaded["payload"])
|
||||||
|
else:
|
||||||
|
apply_command_transport(loaded["payload"])
|
||||||
|
|
||||||
archived = archive_artifact(loaded["artifact"], APPLIED_ROOT)
|
archived = archive_artifact(loaded["artifact"], APPLIED_ROOT)
|
||||||
record = {
|
record = {
|
||||||
|
|||||||
@@ -246,6 +246,9 @@ DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL = (
|
|||||||
DEVICE_PLANE_MANAGER_RELEASE_V2_REL = (
|
DEVICE_PLANE_MANAGER_RELEASE_V2_REL = (
|
||||||
"deployment/device-manager-release-v2.json"
|
"deployment/device-manager-release-v2.json"
|
||||||
)
|
)
|
||||||
|
DEVICE_PLANE_MANAGER_RELEASE_V3_REL = (
|
||||||
|
"deployment/device-manager-release-v3.json"
|
||||||
|
)
|
||||||
DEVICE_PLANE_MANAGER_COMPOSE_REL = "docker-compose.device-manager.yml"
|
DEVICE_PLANE_MANAGER_COMPOSE_REL = "docker-compose.device-manager.yml"
|
||||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_REL = (
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_REL = (
|
||||||
"deployment/device-edge-core-channel-bootstrap-v1.json"
|
"deployment/device-edge-core-channel-bootstrap-v1.json"
|
||||||
@@ -262,6 +265,9 @@ DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_REL = (
|
|||||||
DEVICE_PLANE_CONTROL_CORE_RELEASE_REL = (
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_REL = (
|
||||||
"deployment/device-control-core-release-v1.json"
|
"deployment/device-control-core-release-v1.json"
|
||||||
)
|
)
|
||||||
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_V2_REL = (
|
||||||
|
"deployment/device-control-core-release-v2.json"
|
||||||
|
)
|
||||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_COMPOSE_REL = (
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_COMPOSE_REL = (
|
||||||
"docker-compose.device-edge-core-channel.yml"
|
"docker-compose.device-edge-core-channel.yml"
|
||||||
)
|
)
|
||||||
@@ -323,12 +329,33 @@ DEVICE_PLANE_CONTROL_CORE_RELEASE_ENTRIES = (
|
|||||||
"services/device-control-core",
|
"services/device-control-core",
|
||||||
DEVICE_PLANE_CONTROL_CORE_RELEASE_REL,
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_REL,
|
||||||
)
|
)
|
||||||
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_V2_ENTRIES = (
|
||||||
|
".dockerignore",
|
||||||
|
"package.json",
|
||||||
|
"package-lock.json",
|
||||||
|
"packages/device-protocol-contract",
|
||||||
|
"packages/device-edge-channel-contract",
|
||||||
|
"services/device-control-core",
|
||||||
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_V2_REL,
|
||||||
|
)
|
||||||
DEVICE_PLANE_CONTROL_CORE_RELEASE_FIRST_PREDECESSOR_PATCH_ID = (
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_FIRST_PREDECESSOR_PATCH_ID = (
|
||||||
"device-edge-core-channel-upgrade-v4-20260812-023"
|
"device-edge-core-channel-upgrade-v4-20260812-023"
|
||||||
)
|
)
|
||||||
DEVICE_PLANE_CONTROL_CORE_RELEASE_FIRST_PREDECESSOR_ARTIFACT_SHA256 = (
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_FIRST_PREDECESSOR_ARTIFACT_SHA256 = (
|
||||||
"c10d5b6b7d55ab239f85b6c8130e34ce9f84985e3b46e6e5534733156c7982fc"
|
"c10d5b6b7d55ab239f85b6c8130e34ce9f84985e3b46e6e5534733156c7982fc"
|
||||||
)
|
)
|
||||||
|
DEVICE_PLANE_MANAGER_RELEASE_V3_CONTROL_CORE_PREDECESSOR_PATCH_ID = (
|
||||||
|
"device-control-core-release-v2-20260812-025"
|
||||||
|
)
|
||||||
|
DEVICE_PLANE_MANAGER_RELEASE_V3_CONTROL_CORE_PREDECESSOR_ARTIFACT_SHA256 = (
|
||||||
|
"c61b1f0de1bae23de0caa7289036865ea419ff5705611416f736ca929d1592db"
|
||||||
|
)
|
||||||
|
DEVICE_PLANE_MANAGER_RELEASE_V3_EDGE_CHANNEL_PREDECESSOR_PATCH_ID = (
|
||||||
|
"device-edge-core-channel-upgrade-v4-20260812-023"
|
||||||
|
)
|
||||||
|
DEVICE_PLANE_MANAGER_RELEASE_V3_EDGE_CHANNEL_PREDECESSOR_ARTIFACT_SHA256 = (
|
||||||
|
"c10d5b6b7d55ab239f85b6c8130e34ce9f84985e3b46e6e5534733156c7982fc"
|
||||||
|
)
|
||||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_PREDECESSOR_PATCH_ID = (
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_PREDECESSOR_PATCH_ID = (
|
||||||
"device-edge-core-channel-bootstrap-20260812-018"
|
"device-edge-core-channel-bootstrap-20260812-018"
|
||||||
)
|
)
|
||||||
@@ -460,6 +487,11 @@ DEVICE_PLANE_MANAGER_RELEASE_V2_ENTRIES = (
|
|||||||
"services/device-manager",
|
"services/device-manager",
|
||||||
DEVICE_PLANE_MANAGER_RELEASE_V2_REL,
|
DEVICE_PLANE_MANAGER_RELEASE_V2_REL,
|
||||||
)
|
)
|
||||||
|
DEVICE_PLANE_MANAGER_RELEASE_V3_ENTRIES = (
|
||||||
|
DEVICE_PLANE_MANAGER_COMPOSE_REL,
|
||||||
|
"services/device-manager",
|
||||||
|
DEVICE_PLANE_MANAGER_RELEASE_V3_REL,
|
||||||
|
)
|
||||||
DEVICE_PLANE_MANAGER_RECONCILIATION_REL = (
|
DEVICE_PLANE_MANAGER_RECONCILIATION_REL = (
|
||||||
"deployment/device-manager-control-plane-reconciliation-v1.json"
|
"deployment/device-manager-control-plane-reconciliation-v1.json"
|
||||||
)
|
)
|
||||||
@@ -3889,11 +3921,13 @@ def allowed_payload_path(component, rel):
|
|||||||
DEVICE_PLANE_MANAGER_FAILED_CONTROL_PLANE_REL,
|
DEVICE_PLANE_MANAGER_FAILED_CONTROL_PLANE_REL,
|
||||||
DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL,
|
DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL,
|
||||||
DEVICE_PLANE_MANAGER_RELEASE_V2_REL,
|
DEVICE_PLANE_MANAGER_RELEASE_V2_REL,
|
||||||
|
DEVICE_PLANE_MANAGER_RELEASE_V3_REL,
|
||||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_REL,
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_REL,
|
||||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_REL,
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_REL,
|
||||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_REL,
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_REL,
|
||||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_REL,
|
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_REL,
|
||||||
DEVICE_PLANE_CONTROL_CORE_RELEASE_REL,
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_REL,
|
||||||
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_V2_REL,
|
||||||
DEVICE_PLANE_MANAGER_RECONCILIATION_REL,
|
DEVICE_PLANE_MANAGER_RECONCILIATION_REL,
|
||||||
DEVICE_PLANE_MANAGER_V2_RECONCILIATION_REL,
|
DEVICE_PLANE_MANAGER_V2_RECONCILIATION_REL,
|
||||||
"packages/device-protocol-contract",
|
"packages/device-protocol-contract",
|
||||||
@@ -9459,6 +9493,7 @@ def is_device_plane_manager_control_plane_slice(component, entries):
|
|||||||
DEVICE_PLANE_MANAGER_CONTROL_PLANE_ENTRIES,
|
DEVICE_PLANE_MANAGER_CONTROL_PLANE_ENTRIES,
|
||||||
DEVICE_PLANE_MANAGER_RELEASE_V1_SUCCESSOR_ENTRIES,
|
DEVICE_PLANE_MANAGER_RELEASE_V1_SUCCESSOR_ENTRIES,
|
||||||
DEVICE_PLANE_MANAGER_RELEASE_V2_ENTRIES,
|
DEVICE_PLANE_MANAGER_RELEASE_V2_ENTRIES,
|
||||||
|
DEVICE_PLANE_MANAGER_RELEASE_V3_ENTRIES,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -9471,6 +9506,14 @@ def is_device_plane_manager_release_v2_slice(component, entries):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def is_device_plane_manager_release_v3_slice(component, entries):
|
||||||
|
return (
|
||||||
|
component == "device-plane"
|
||||||
|
and entries is not None
|
||||||
|
and tuple(entries) == DEVICE_PLANE_MANAGER_RELEASE_V3_ENTRIES
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def is_device_plane_edge_core_channel_bootstrap_slice(component, entries):
|
def is_device_plane_edge_core_channel_bootstrap_slice(component, entries):
|
||||||
return (
|
return (
|
||||||
component == "device-plane"
|
component == "device-plane"
|
||||||
@@ -9516,7 +9559,18 @@ def is_device_plane_control_core_release_slice(component, entries):
|
|||||||
return (
|
return (
|
||||||
component == "device-plane"
|
component == "device-plane"
|
||||||
and entries is not None
|
and entries is not None
|
||||||
and tuple(entries) == DEVICE_PLANE_CONTROL_CORE_RELEASE_ENTRIES
|
and tuple(entries) in (
|
||||||
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_ENTRIES,
|
||||||
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_V2_ENTRIES,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def is_device_plane_control_core_release_v2_slice(component, entries):
|
||||||
|
return (
|
||||||
|
component == "device-plane"
|
||||||
|
and entries is not None
|
||||||
|
and tuple(entries) == DEVICE_PLANE_CONTROL_CORE_RELEASE_V2_ENTRIES
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -9605,6 +9659,49 @@ def expected_device_plane_manager_release_v2_boundaries():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def expected_device_plane_manager_release_v3_boundaries():
|
||||||
|
return {
|
||||||
|
"controlCorePredecessor": {
|
||||||
|
"patchId": (
|
||||||
|
DEVICE_PLANE_MANAGER_RELEASE_V3_CONTROL_CORE_PREDECESSOR_PATCH_ID
|
||||||
|
),
|
||||||
|
"artifactSha256": (
|
||||||
|
DEVICE_PLANE_MANAGER_RELEASE_V3_CONTROL_CORE_PREDECESSOR_ARTIFACT_SHA256
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"edgeChannelPredecessor": {
|
||||||
|
"patchId": (
|
||||||
|
DEVICE_PLANE_MANAGER_RELEASE_V3_EDGE_CHANNEL_PREDECESSOR_PATCH_ID
|
||||||
|
),
|
||||||
|
"artifactSha256": (
|
||||||
|
DEVICE_PLANE_MANAGER_RELEASE_V3_EDGE_CHANNEL_PREDECESSOR_ARTIFACT_SHA256
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"service": "device-manager",
|
||||||
|
"publicIngress": "reverse-proxy-only",
|
||||||
|
"deviceCoreManagementApi": "file-token-authenticated",
|
||||||
|
"launcherTrust": "file-token-scoped-to-device-core-handoff",
|
||||||
|
"edgeChannel": "preserve-active-v4-core-initiated-pinned-mtls",
|
||||||
|
"edgeChannelIdentity": (
|
||||||
|
"reuse-runner-managed-host-local-private-key-"
|
||||||
|
"public-certificate-export"
|
||||||
|
),
|
||||||
|
"edgeChannelEgress": (
|
||||||
|
"preserve-dedicated-core-only-bridge-no-host-ingress-"
|
||||||
|
"public-ipv4-tcp-443-only"
|
||||||
|
),
|
||||||
|
"healthGate": "bounded-container-grace+core-contract",
|
||||||
|
"commandTransport": "typed-service-ping-v1",
|
||||||
|
"commandCatalog": "allowlisted-adapter-typed-commands-only",
|
||||||
|
"credentialBoundary": (
|
||||||
|
"transient-core-memory-then-single-pinned-mtls-command-envelope-"
|
||||||
|
"to-edge-never-persisted-never-logged-never-returned"
|
||||||
|
),
|
||||||
|
"gelios": "untouched-legacy-only",
|
||||||
|
"rollback": "restore-preapply-snapshot",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def expected_device_plane_manager_release_boundaries():
|
def expected_device_plane_manager_release_boundaries():
|
||||||
# Compatibility name for the current release builder/tests. Immutable v1
|
# Compatibility name for the current release builder/tests. Immutable v1
|
||||||
# predecessors always use expected_device_plane_manager_release_v1_boundaries.
|
# predecessors always use expected_device_plane_manager_release_v1_boundaries.
|
||||||
@@ -9823,10 +9920,12 @@ def expected_device_plane_edge_core_channel_upgrade_v4_descriptor(
|
|||||||
def expected_device_plane_control_core_release_descriptor(
|
def expected_device_plane_control_core_release_descriptor(
|
||||||
release_id,
|
release_id,
|
||||||
predecessor,
|
predecessor,
|
||||||
|
*,
|
||||||
|
schema_version="v1",
|
||||||
):
|
):
|
||||||
return {
|
descriptor = {
|
||||||
"schemaVersion": (
|
"schemaVersion": (
|
||||||
"nodedc.device-plane.device-control-core-release.v1"
|
f"nodedc.device-plane.device-control-core-release.{schema_version}"
|
||||||
),
|
),
|
||||||
"releaseId": release_id,
|
"releaseId": release_id,
|
||||||
"action": "upgrade",
|
"action": "upgrade",
|
||||||
@@ -9847,8 +9946,16 @@ def expected_device_plane_control_core_release_descriptor(
|
|||||||
],
|
],
|
||||||
"publicIngress": "none-on-synology",
|
"publicIngress": "none-on-synology",
|
||||||
"edgeRegistrations": "preserved",
|
"edgeRegistrations": "preserved",
|
||||||
"commandTransport": "disabled",
|
"commandTransport": (
|
||||||
"gelios": "untouched",
|
"typed-service-ping-v1"
|
||||||
|
if schema_version == "v2"
|
||||||
|
else "disabled"
|
||||||
|
),
|
||||||
|
"gelios": (
|
||||||
|
"untouched-legacy-only"
|
||||||
|
if schema_version == "v2"
|
||||||
|
else "untouched"
|
||||||
|
),
|
||||||
"preservedServices": [
|
"preservedServices": [
|
||||||
"device-manager",
|
"device-manager",
|
||||||
"device-gateway",
|
"device-gateway",
|
||||||
@@ -9861,6 +9968,15 @@ def expected_device_plane_control_core_release_descriptor(
|
|||||||
),
|
),
|
||||||
"rollback": "restore-preapply-source-and-core-runtime",
|
"rollback": "restore-preapply-source-and-core-runtime",
|
||||||
}
|
}
|
||||||
|
if schema_version == "v2":
|
||||||
|
descriptor["commandCatalog"] = (
|
||||||
|
"allowlisted-adapter-typed-commands-only"
|
||||||
|
)
|
||||||
|
descriptor["credentialBoundary"] = (
|
||||||
|
"transient-core-memory-then-single-pinned-mtls-command-envelope-"
|
||||||
|
"to-edge-never-persisted-never-logged-never-returned"
|
||||||
|
)
|
||||||
|
return descriptor
|
||||||
|
|
||||||
|
|
||||||
def expected_device_plane_manager_failed_control_plane_descriptor():
|
def expected_device_plane_manager_failed_control_plane_descriptor():
|
||||||
@@ -10145,6 +10261,22 @@ def validate_device_plane_manager_release_v2_payload(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_device_plane_manager_release_v3_payload(
|
||||||
|
payload_dir,
|
||||||
|
*,
|
||||||
|
expected_release_id=None,
|
||||||
|
):
|
||||||
|
return validate_device_plane_manager_release_payload_contract(
|
||||||
|
payload_dir,
|
||||||
|
descriptor_rel=DEVICE_PLANE_MANAGER_RELEASE_V3_REL,
|
||||||
|
schema_version="nodedc.device-plane.device-manager-release.v3",
|
||||||
|
boundaries=expected_device_plane_manager_release_v3_boundaries(),
|
||||||
|
compose_sha256=DEVICE_PLANE_MANAGER_RELEASE_V1_COMPOSE_SHA256,
|
||||||
|
edge_channel=False,
|
||||||
|
expected_release_id=expected_release_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def validate_device_plane_manager_release_payload(
|
def validate_device_plane_manager_release_payload(
|
||||||
payload_dir,
|
payload_dir,
|
||||||
*,
|
*,
|
||||||
@@ -10152,9 +10284,18 @@ def validate_device_plane_manager_release_payload(
|
|||||||
):
|
):
|
||||||
v1 = payload_dir / DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL
|
v1 = payload_dir / DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL
|
||||||
v2 = payload_dir / DEVICE_PLANE_MANAGER_RELEASE_V2_REL
|
v2 = payload_dir / DEVICE_PLANE_MANAGER_RELEASE_V2_REL
|
||||||
present = [path for path in (v1, v2) if path.exists() or path.is_symlink()]
|
v3 = payload_dir / DEVICE_PLANE_MANAGER_RELEASE_V3_REL
|
||||||
|
present = [
|
||||||
|
path for path in (v1, v2, v3)
|
||||||
|
if path.exists() or path.is_symlink()
|
||||||
|
]
|
||||||
if len(present) != 1:
|
if len(present) != 1:
|
||||||
die("Device Manager release descriptor cardinality mismatch")
|
die("Device Manager release descriptor cardinality mismatch")
|
||||||
|
if present[0] == v3:
|
||||||
|
return validate_device_plane_manager_release_v3_payload(
|
||||||
|
payload_dir,
|
||||||
|
expected_release_id=expected_release_id,
|
||||||
|
)
|
||||||
if present[0] == v2:
|
if present[0] == v2:
|
||||||
return validate_device_plane_manager_release_v2_payload(
|
return validate_device_plane_manager_release_v2_payload(
|
||||||
payload_dir,
|
payload_dir,
|
||||||
@@ -10285,8 +10426,14 @@ def validate_device_plane_control_core_release_payload(
|
|||||||
*,
|
*,
|
||||||
expected_release_id=None,
|
expected_release_id=None,
|
||||||
):
|
):
|
||||||
|
v1 = payload_dir / DEVICE_PLANE_CONTROL_CORE_RELEASE_REL
|
||||||
|
v2 = payload_dir / DEVICE_PLANE_CONTROL_CORE_RELEASE_V2_REL
|
||||||
|
present = [path for path in (v1, v2) if path.exists() or path.is_symlink()]
|
||||||
|
if len(present) != 1:
|
||||||
|
die("Device Control Core release descriptor cardinality mismatch")
|
||||||
|
schema_version = "v2" if present[0] == v2 else "v1"
|
||||||
descriptor = read_strict_json(
|
descriptor = read_strict_json(
|
||||||
payload_dir / DEVICE_PLANE_CONTROL_CORE_RELEASE_REL,
|
present[0],
|
||||||
"Device Control Core release descriptor",
|
"Device Control Core release descriptor",
|
||||||
max_bytes=16 * 1024,
|
max_bytes=16 * 1024,
|
||||||
)
|
)
|
||||||
@@ -10326,7 +10473,7 @@ def validate_device_plane_control_core_release_payload(
|
|||||||
if (
|
if (
|
||||||
not isinstance(release_id, str)
|
not isinstance(release_id, str)
|
||||||
or not re.fullmatch(
|
or not re.fullmatch(
|
||||||
r"device-control-core-release-[A-Za-z0-9._-]{1,67}",
|
r"device-control-core-release(?:-v2)?-[A-Za-z0-9._-]{1,67}",
|
||||||
release_id,
|
release_id,
|
||||||
)
|
)
|
||||||
or (
|
or (
|
||||||
@@ -10338,6 +10485,7 @@ def validate_device_plane_control_core_release_payload(
|
|||||||
!= expected_device_plane_control_core_release_descriptor(
|
!= expected_device_plane_control_core_release_descriptor(
|
||||||
release_id,
|
release_id,
|
||||||
predecessor,
|
predecessor,
|
||||||
|
schema_version=schema_version,
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
die("Device Control Core release descriptor mismatch")
|
die("Device Control Core release descriptor mismatch")
|
||||||
@@ -10419,6 +10567,7 @@ def installed_device_plane_manager_compose_sha256():
|
|||||||
root = DEVICE_PLANE_ROOT
|
root = DEVICE_PLANE_ROOT
|
||||||
v1 = root / DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL
|
v1 = root / DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL
|
||||||
v2 = root / DEVICE_PLANE_MANAGER_RELEASE_V2_REL
|
v2 = root / DEVICE_PLANE_MANAGER_RELEASE_V2_REL
|
||||||
|
v3 = root / DEVICE_PLANE_MANAGER_RELEASE_V3_REL
|
||||||
|
|
||||||
# A successful v2 overlay intentionally leaves the immutable v1 release
|
# A successful v2 overlay intentionally leaves the immutable v1 release
|
||||||
# descriptor as predecessor evidence. Prefer the highest installed
|
# descriptor as predecessor evidence. Prefer the highest installed
|
||||||
@@ -10426,6 +10575,19 @@ def installed_device_plane_manager_compose_sha256():
|
|||||||
# the generation-specific Compose digest. During a failed v2 apply,
|
# the generation-specific Compose digest. During a failed v2 apply,
|
||||||
# rollback removes the candidate-only v2 descriptor before rebuilding the
|
# rollback removes the candidate-only v2 descriptor before rebuilding the
|
||||||
# restored v1 runtime, so the same lookup follows the restored source.
|
# restored v1 runtime, so the same lookup follows the restored source.
|
||||||
|
if v3.exists() or v3.is_symlink():
|
||||||
|
descriptor = read_strict_json(
|
||||||
|
v3,
|
||||||
|
"installed Device Manager release v3 descriptor",
|
||||||
|
max_bytes=16 * 1024,
|
||||||
|
)
|
||||||
|
validate_device_plane_manager_release_descriptor(
|
||||||
|
descriptor,
|
||||||
|
schema_version="nodedc.device-plane.device-manager-release.v3",
|
||||||
|
boundaries=expected_device_plane_manager_release_v3_boundaries(),
|
||||||
|
)
|
||||||
|
return DEVICE_PLANE_MANAGER_RELEASE_V1_COMPOSE_SHA256
|
||||||
|
|
||||||
if v2.exists() or v2.is_symlink():
|
if v2.exists() or v2.is_symlink():
|
||||||
descriptor = read_strict_json(
|
descriptor = read_strict_json(
|
||||||
v2,
|
v2,
|
||||||
@@ -11926,6 +12088,152 @@ def validate_device_plane_manager_reconciliation_evidence(payload_dir):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def validate_device_plane_manager_v3_active_baseline(descriptor):
|
||||||
|
if descriptor.get("schemaVersion") != (
|
||||||
|
"nodedc.device-plane.device-manager-release.v3"
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def successful_applied_artifact(reference, label):
|
||||||
|
patch_id = reference["patchId"]
|
||||||
|
artifact_sha256 = reference["artifactSha256"]
|
||||||
|
artifact_name = f"nodedc-device-plane-{patch_id}.tgz"
|
||||||
|
artifact = APPLIED_DIR / artifact_name
|
||||||
|
if (
|
||||||
|
not artifact.is_file()
|
||||||
|
or artifact.is_symlink()
|
||||||
|
or sha256_file(artifact) != artifact_sha256
|
||||||
|
):
|
||||||
|
die(f"Device Manager v3 {label} applied artifact mismatch")
|
||||||
|
records = [
|
||||||
|
row for row in load_state(STATE_FILE)
|
||||||
|
if row.get("id") == patch_id
|
||||||
|
and row.get("sha256") == artifact_sha256
|
||||||
|
]
|
||||||
|
if (
|
||||||
|
len(records) != 1
|
||||||
|
or records[0].get("status") != "ok"
|
||||||
|
or records[0].get("component") != "device-plane"
|
||||||
|
or records[0].get("artifact") != artifact_name
|
||||||
|
):
|
||||||
|
die(f"Device Manager v3 {label} applied journal mismatch")
|
||||||
|
backup_id = records[0].get("backup_id")
|
||||||
|
backup = BACKUPS_DIR / backup_id if isinstance(backup_id, str) else None
|
||||||
|
if (
|
||||||
|
backup is None
|
||||||
|
or safe_name(backup_id) != backup_id
|
||||||
|
or not backup.is_dir()
|
||||||
|
or backup.is_symlink()
|
||||||
|
):
|
||||||
|
die(f"Device Manager v3 {label} backup is unsafe")
|
||||||
|
return artifact
|
||||||
|
|
||||||
|
core_reference = descriptor["controlCorePredecessor"]
|
||||||
|
edge_reference = descriptor["edgeChannelPredecessor"]
|
||||||
|
core_artifact = successful_applied_artifact(
|
||||||
|
core_reference,
|
||||||
|
"Device Control Core predecessor",
|
||||||
|
)
|
||||||
|
edge_artifact = successful_applied_artifact(
|
||||||
|
edge_reference,
|
||||||
|
"Device Edge channel predecessor",
|
||||||
|
)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory(
|
||||||
|
prefix="device-manager-v3-core-baseline-",
|
||||||
|
dir=TMP_DIR,
|
||||||
|
) as directory:
|
||||||
|
core_manifest, core_entries, core_payload = load_artifact(
|
||||||
|
core_artifact,
|
||||||
|
Path(directory),
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
core_manifest.get("id") != core_reference["patchId"]
|
||||||
|
or core_manifest.get("component") != "device-plane"
|
||||||
|
or core_manifest.get("type") != "app-overlay"
|
||||||
|
or tuple(core_entries) != DEVICE_PLANE_CONTROL_CORE_RELEASE_V2_ENTRIES
|
||||||
|
):
|
||||||
|
die("Device Manager v3 Device Control Core predecessor type mismatch")
|
||||||
|
expected_core_descriptor = (
|
||||||
|
validate_device_plane_control_core_release_payload(
|
||||||
|
core_payload,
|
||||||
|
expected_release_id=core_reference["patchId"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
installed_core_descriptor = read_strict_json(
|
||||||
|
DEVICE_PLANE_ROOT / DEVICE_PLANE_CONTROL_CORE_RELEASE_V2_REL,
|
||||||
|
"installed Device Manager v3 Device Control Core predecessor",
|
||||||
|
max_bytes=16 * 1024,
|
||||||
|
)
|
||||||
|
if installed_core_descriptor != expected_core_descriptor:
|
||||||
|
die("Device Manager v3 Device Control Core predecessor is not current")
|
||||||
|
expected_core_source = collect_exact_files(
|
||||||
|
core_payload,
|
||||||
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_V2_ENTRIES,
|
||||||
|
"Device Manager v3 Device Control Core predecessor source",
|
||||||
|
)
|
||||||
|
actual_core_source = collect_exact_files(
|
||||||
|
DEVICE_PLANE_ROOT,
|
||||||
|
DEVICE_PLANE_CONTROL_CORE_RELEASE_V2_ENTRIES,
|
||||||
|
"installed Device Manager v3 Device Control Core predecessor source",
|
||||||
|
)
|
||||||
|
if actual_core_source != expected_core_source:
|
||||||
|
die("installed Device Manager v3 Device Control Core source drift detected")
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory(
|
||||||
|
prefix="device-manager-v3-edge-baseline-",
|
||||||
|
dir=TMP_DIR,
|
||||||
|
) as directory:
|
||||||
|
edge_manifest, edge_entries, edge_payload = load_artifact(
|
||||||
|
edge_artifact,
|
||||||
|
Path(directory),
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
edge_manifest.get("id") != edge_reference["patchId"]
|
||||||
|
or edge_manifest.get("component") != "device-plane"
|
||||||
|
or edge_manifest.get("type") != "app-overlay"
|
||||||
|
or tuple(edge_entries)
|
||||||
|
!= DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_ENTRIES
|
||||||
|
):
|
||||||
|
die("Device Manager v3 Device Edge channel predecessor type mismatch")
|
||||||
|
expected_edge_descriptor = (
|
||||||
|
validate_device_plane_edge_core_channel_upgrade_v4_payload(
|
||||||
|
edge_payload,
|
||||||
|
expected_transition_id=edge_reference["patchId"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
installed_edge_descriptor = read_strict_json(
|
||||||
|
DEVICE_PLANE_ROOT / DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_REL,
|
||||||
|
"installed Device Manager v3 Device Edge channel predecessor",
|
||||||
|
max_bytes=16 * 1024,
|
||||||
|
)
|
||||||
|
if installed_edge_descriptor != expected_edge_descriptor:
|
||||||
|
die("Device Manager v3 Device Edge channel predecessor is not current")
|
||||||
|
expected_base_compose_sha256 = sha256_file(
|
||||||
|
edge_payload / "docker-compose.device-plane.yml"
|
||||||
|
)
|
||||||
|
installed_base_compose = DEVICE_PLANE_ROOT / "docker-compose.device-plane.yml"
|
||||||
|
if (
|
||||||
|
installed_base_compose.is_symlink()
|
||||||
|
or not installed_base_compose.is_file()
|
||||||
|
or sha256_file(installed_base_compose) != expected_base_compose_sha256
|
||||||
|
):
|
||||||
|
die("installed Device Manager v3 Device Edge topology drift detected")
|
||||||
|
|
||||||
|
identity_state = inspect_device_edge_channel_core_identity_state()
|
||||||
|
if identity_state != "valid-reuse-at-apply":
|
||||||
|
die("Device Manager v3 requires the valid active Edge identity")
|
||||||
|
validate_device_manager_control_plane_runtime(
|
||||||
|
require_edge_channel=True,
|
||||||
|
core_network_mode="private-egress",
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"controlCoreArtifact": core_artifact,
|
||||||
|
"edgeChannelArtifact": edge_artifact,
|
||||||
|
"identityState": identity_state,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def validate_device_plane_manager_activation_predecessor(payload_dir):
|
def validate_device_plane_manager_activation_predecessor(payload_dir):
|
||||||
descriptor = validate_device_plane_manager_release_payload(
|
descriptor = validate_device_plane_manager_release_payload(
|
||||||
payload_dir
|
payload_dir
|
||||||
@@ -12033,6 +12341,7 @@ def validate_device_plane_manager_activation_predecessor(payload_dir):
|
|||||||
DEVICE_PLANE_MANAGER_CONTROL_PLANE_ENTRIES,
|
DEVICE_PLANE_MANAGER_CONTROL_PLANE_ENTRIES,
|
||||||
DEVICE_PLANE_MANAGER_RELEASE_V1_SUCCESSOR_ENTRIES,
|
DEVICE_PLANE_MANAGER_RELEASE_V1_SUCCESSOR_ENTRIES,
|
||||||
DEVICE_PLANE_MANAGER_RELEASE_V2_ENTRIES,
|
DEVICE_PLANE_MANAGER_RELEASE_V2_ENTRIES,
|
||||||
|
DEVICE_PLANE_MANAGER_RELEASE_V3_ENTRIES,
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
die("Device Manager release predecessor type mismatch")
|
die("Device Manager release predecessor type mismatch")
|
||||||
@@ -12042,11 +12351,14 @@ def validate_device_plane_manager_activation_predecessor(payload_dir):
|
|||||||
expected_release_id=patch_id,
|
expected_release_id=patch_id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
predecessor_descriptor_rel = (
|
predecessor_descriptor_rel = {
|
||||||
DEVICE_PLANE_MANAGER_RELEASE_V2_REL
|
DEVICE_PLANE_MANAGER_RELEASE_V2_ENTRIES:
|
||||||
if tuple(predecessor_entries)
|
DEVICE_PLANE_MANAGER_RELEASE_V2_REL,
|
||||||
== DEVICE_PLANE_MANAGER_RELEASE_V2_ENTRIES
|
DEVICE_PLANE_MANAGER_RELEASE_V3_ENTRIES:
|
||||||
else DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL
|
DEVICE_PLANE_MANAGER_RELEASE_V3_REL,
|
||||||
|
}.get(
|
||||||
|
tuple(predecessor_entries),
|
||||||
|
DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL,
|
||||||
)
|
)
|
||||||
installed_descriptor = read_strict_json(
|
installed_descriptor = read_strict_json(
|
||||||
root / predecessor_descriptor_rel,
|
root / predecessor_descriptor_rel,
|
||||||
@@ -12062,6 +12374,11 @@ def validate_device_plane_manager_activation_predecessor(payload_dir):
|
|||||||
):
|
):
|
||||||
healthcheck_compose_service("device-plane", service)
|
healthcheck_compose_service("device-plane", service)
|
||||||
runtime = {"accepted": True}
|
runtime = {"accepted": True}
|
||||||
|
v3_baseline = validate_device_plane_manager_v3_active_baseline(
|
||||||
|
descriptor
|
||||||
|
)
|
||||||
|
if v3_baseline is not None:
|
||||||
|
runtime["v3Baseline"] = v3_baseline
|
||||||
mode = "active-manager-forward-upgrade"
|
mode = "active-manager-forward-upgrade"
|
||||||
return {
|
return {
|
||||||
"mode": mode,
|
"mode": mode,
|
||||||
@@ -16086,6 +16403,9 @@ def component_services(component, entries=None):
|
|||||||
if is_device_plane_control_core_release_slice(component, entries):
|
if is_device_plane_control_core_release_slice(component, entries):
|
||||||
return ("device-control-core",)
|
return ("device-control-core",)
|
||||||
|
|
||||||
|
if is_device_plane_manager_release_v3_slice(component, entries):
|
||||||
|
return ("device-manager",)
|
||||||
|
|
||||||
if is_device_plane_edge_core_channel_bootstrap_slice(component, entries):
|
if is_device_plane_edge_core_channel_bootstrap_slice(component, entries):
|
||||||
return ("device-control-core",)
|
return ("device-control-core",)
|
||||||
|
|
||||||
@@ -16683,6 +17003,18 @@ def component_builds(component, entries=None):
|
|||||||
),
|
),
|
||||||
),)
|
),)
|
||||||
|
|
||||||
|
if is_device_plane_manager_release_v3_slice(component, entries):
|
||||||
|
return ((
|
||||||
|
DEVICE_PLANE_ROOT / "services/device-manager",
|
||||||
|
(
|
||||||
|
"build",
|
||||||
|
"--no-cache",
|
||||||
|
"-t",
|
||||||
|
DEVICE_PLANE_MANAGER_IMAGE,
|
||||||
|
".",
|
||||||
|
),
|
||||||
|
),)
|
||||||
|
|
||||||
if is_device_plane_manager_control_plane_slice(component, entries):
|
if is_device_plane_manager_control_plane_slice(component, entries):
|
||||||
return (
|
return (
|
||||||
(
|
(
|
||||||
@@ -20427,7 +20759,15 @@ def plan_artifact(artifact):
|
|||||||
print("device_control_core_discovery_ingest=enabled:authenticated")
|
print("device_control_core_discovery_ingest=enabled:authenticated")
|
||||||
else:
|
else:
|
||||||
print("device_gateway_public_ingress=disabled")
|
print("device_gateway_public_ingress=disabled")
|
||||||
print("device_gateway_command_transport=disabled")
|
if (
|
||||||
|
device_plane_manager_activation_preflight is not None
|
||||||
|
and device_plane_manager_activation_preflight["descriptor"].get(
|
||||||
|
"commandTransport"
|
||||||
|
) == "typed-service-ping-v1"
|
||||||
|
):
|
||||||
|
print("device_gateway_command_transport=typed-service-ping-v1")
|
||||||
|
else:
|
||||||
|
print("device_gateway_command_transport=disabled")
|
||||||
print("gelios=untouched")
|
print("gelios=untouched")
|
||||||
if device_plane_foundation_recovery_preflight is not None:
|
if device_plane_foundation_recovery_preflight is not None:
|
||||||
print(
|
print(
|
||||||
@@ -20589,15 +20929,36 @@ def plan_artifact(artifact):
|
|||||||
)
|
)
|
||||||
print(
|
print(
|
||||||
"device_plane_runtime_mutation="
|
"device_plane_runtime_mutation="
|
||||||
"build+recreate:device-control-core,device-manager"
|
+ (
|
||||||
|
"build+recreate:device-manager"
|
||||||
|
if device_plane_manager_activation_preflight["descriptor"].get(
|
||||||
|
"schemaVersion"
|
||||||
|
) == "nodedc.device-plane.device-manager-release.v3"
|
||||||
|
else "build+recreate:device-control-core,device-manager"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
print(
|
print(
|
||||||
"device_plane_runtime_services="
|
"device_plane_runtime_services="
|
||||||
"preserved:device-gateway,device-postgres,"
|
+ (
|
||||||
"device-backhaul-target"
|
"preserved:device-control-core,device-gateway,"
|
||||||
|
"device-postgres,device-backhaul-target"
|
||||||
|
if device_plane_manager_activation_preflight["descriptor"].get(
|
||||||
|
"schemaVersion"
|
||||||
|
) == "nodedc.device-plane.device-manager-release.v3"
|
||||||
|
else "preserved:device-gateway,device-postgres,"
|
||||||
|
"device-backhaul-target"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
print("device_manager_health_gate=bounded-grace+contract")
|
print("device_manager_health_gate=bounded-grace+contract")
|
||||||
print("device_manager_public_route=unchanged:absent")
|
if device_plane_manager_activation_preflight["descriptor"].get(
|
||||||
|
"commandTransport"
|
||||||
|
) == "typed-service-ping-v1":
|
||||||
|
print("device_manager_public_route=unchanged:active")
|
||||||
|
print("device_edge_channel_commands=typed-service-ping-v1")
|
||||||
|
print("device_command_credential=transient-core-memory-only")
|
||||||
|
print("gelios=untouched:legacy-only")
|
||||||
|
else:
|
||||||
|
print("device_manager_public_route=unchanged:absent")
|
||||||
print("device_gateway_tcp_9921=preserved:loopback-only")
|
print("device_gateway_tcp_9921=preserved:loopback-only")
|
||||||
print(
|
print(
|
||||||
"device_plane_rollback="
|
"device_plane_rollback="
|
||||||
@@ -20637,11 +20998,14 @@ def plan_artifact(artifact):
|
|||||||
print("device_edge_channel=preserved:core-initiated:pinned-mtls:registered-edges-only")
|
print("device_edge_channel=preserved:core-initiated:pinned-mtls:registered-edges-only")
|
||||||
print("device_edge_channel_networks=preserved:device-plane-private,device-plane-egress")
|
print("device_edge_channel_networks=preserved:device-plane-private,device-plane-egress")
|
||||||
print("device_edge_channel_registrations=preserved")
|
print("device_edge_channel_registrations=preserved")
|
||||||
print("device_edge_channel_commands=disabled")
|
print(
|
||||||
|
"device_edge_channel_commands="
|
||||||
|
f"{descriptor['commandTransport']}"
|
||||||
|
)
|
||||||
print("device_manager=preserved:active")
|
print("device_manager=preserved:active")
|
||||||
print("device_manager_public_route=unchanged:active")
|
print("device_manager_public_route=unchanged:active")
|
||||||
print("device_gateway_tcp_9921=preserved:loopback-only")
|
print("device_gateway_tcp_9921=preserved:loopback-only")
|
||||||
print("gelios=untouched")
|
print(f"gelios={descriptor['gelios']}")
|
||||||
print("device_plane_rollback=source+preapply-core-runtime")
|
print("device_plane_rollback=source+preapply-core-runtime")
|
||||||
if device_plane_edge_core_channel_preflight is not None:
|
if device_plane_edge_core_channel_preflight is not None:
|
||||||
print(
|
print(
|
||||||
@@ -22571,7 +22935,12 @@ def component_healthchecks(component, entries=None, services=None):
|
|||||||
},
|
},
|
||||||
},)
|
},)
|
||||||
if is_device_plane_manager_control_plane_slice(component, entries):
|
if is_device_plane_manager_control_plane_slice(component, entries):
|
||||||
return ({
|
command_transport = (
|
||||||
|
"typed-service-ping-v1"
|
||||||
|
if is_device_plane_manager_release_v3_slice(component, entries)
|
||||||
|
else "disabled"
|
||||||
|
)
|
||||||
|
checks = ({
|
||||||
"url": "http://127.0.0.1:18120/healthz",
|
"url": "http://127.0.0.1:18120/healthz",
|
||||||
"expected_json": {
|
"expected_json": {
|
||||||
"ok": True,
|
"ok": True,
|
||||||
@@ -22579,9 +22948,21 @@ def component_healthchecks(component, entries=None, services=None):
|
|||||||
"database": "ready",
|
"database": "ready",
|
||||||
"discoveryIngest": "enabled",
|
"discoveryIngest": "enabled",
|
||||||
"managementApi": "enabled",
|
"managementApi": "enabled",
|
||||||
"commandTransport": "disabled",
|
"commandTransport": command_transport,
|
||||||
},
|
},
|
||||||
},)
|
},)
|
||||||
|
if is_device_plane_manager_release_v3_slice(component, entries):
|
||||||
|
checks += ({
|
||||||
|
"url": "http://127.0.0.1:18080/healthz",
|
||||||
|
"headers": {"Host": "device.nodedc.ru"},
|
||||||
|
"expected_json": {
|
||||||
|
"ok": True,
|
||||||
|
"service": "nodedc-device-manager",
|
||||||
|
"authRequired": True,
|
||||||
|
"deviceCoreConfigured": True,
|
||||||
|
},
|
||||||
|
},)
|
||||||
|
return checks
|
||||||
if (
|
if (
|
||||||
is_device_plane_control_core_release_slice(component, entries)
|
is_device_plane_control_core_release_slice(component, entries)
|
||||||
or is_device_plane_edge_core_channel_bootstrap_slice(
|
or is_device_plane_edge_core_channel_bootstrap_slice(
|
||||||
@@ -22597,7 +22978,14 @@ def component_healthchecks(component, entries=None, services=None):
|
|||||||
"database": "ready",
|
"database": "ready",
|
||||||
"discoveryIngest": "enabled",
|
"discoveryIngest": "enabled",
|
||||||
"managementApi": "enabled",
|
"managementApi": "enabled",
|
||||||
"commandTransport": "disabled",
|
"commandTransport": (
|
||||||
|
"typed-service-ping-v1"
|
||||||
|
if is_device_plane_control_core_release_v2_slice(
|
||||||
|
component,
|
||||||
|
entries,
|
||||||
|
)
|
||||||
|
else "disabled"
|
||||||
|
),
|
||||||
},
|
},
|
||||||
},)
|
},)
|
||||||
if is_device_plane_b2_discovery_rollback_recovery_slice(
|
if is_device_plane_b2_discovery_rollback_recovery_slice(
|
||||||
@@ -23319,10 +23707,12 @@ def run_healthchecks(component, entries=None, services=None):
|
|||||||
assert_loopback_tcp_port_open(9921)
|
assert_loopback_tcp_port_open(9921)
|
||||||
return
|
return
|
||||||
if is_device_plane_manager_control_plane_slice(component, entries):
|
if is_device_plane_manager_control_plane_slice(component, entries):
|
||||||
if tuple(services or ()) != (
|
expected_services = (
|
||||||
"device-control-core",
|
("device-manager",)
|
||||||
"device-manager",
|
if is_device_plane_manager_release_v3_slice(component, entries)
|
||||||
):
|
else ("device-control-core", "device-manager")
|
||||||
|
)
|
||||||
|
if tuple(services or ()) != expected_services:
|
||||||
die("Device Manager control-plane service set mismatch")
|
die("Device Manager control-plane service set mismatch")
|
||||||
for service in services:
|
for service in services:
|
||||||
healthcheck_compose_service_with_grace(
|
healthcheck_compose_service_with_grace(
|
||||||
|
|||||||
@@ -81,6 +81,22 @@ class DeviceEdgeVpsArtifactTest(unittest.TestCase):
|
|||||||
with self.subTest(phase=phase), self.assertRaises(RUNNER.DeployError):
|
with self.subTest(phase=phase), self.assertRaises(RUNNER.DeployError):
|
||||||
RUNNER.preflight({"phase": phase})
|
RUNNER.preflight({"phase": phase})
|
||||||
|
|
||||||
|
def test_accepted_shared_source_phases_cannot_be_rebuilt(self):
|
||||||
|
for phase in ("core-channel", "tracker-ingress"):
|
||||||
|
with self.subTest(phase=phase), tempfile.TemporaryDirectory(
|
||||||
|
prefix=f"nodedc-vps-frozen-{phase}-"
|
||||||
|
) as directory:
|
||||||
|
result = self.build(
|
||||||
|
Path(directory),
|
||||||
|
phase,
|
||||||
|
f"device-edge-vps-{phase}-frozen-001",
|
||||||
|
)
|
||||||
|
self.assertNotEqual(result.returncode, 0)
|
||||||
|
self.assertIn(
|
||||||
|
f"accepted_vps_phase_rebuild_frozen:{phase}:ADR-0001",
|
||||||
|
result.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
def require_runtime_cache(self):
|
def require_runtime_cache(self):
|
||||||
for name, digest in (
|
for name, digest in (
|
||||||
(RUNNER.NODE_ARCHIVE, RUNNER.NODE_ARCHIVE_SHA256),
|
(RUNNER.NODE_ARCHIVE, RUNNER.NODE_ARCHIVE_SHA256),
|
||||||
@@ -93,13 +109,7 @@ class DeviceEdgeVpsArtifactTest(unittest.TestCase):
|
|||||||
def test_builders_are_deterministic_narrow_and_secret_free(self):
|
def test_builders_are_deterministic_narrow_and_secret_free(self):
|
||||||
self.require_runtime_cache()
|
self.require_runtime_cache()
|
||||||
for phase in (
|
for phase in (
|
||||||
"foundation",
|
"command-transport",
|
||||||
"runtime-reconciliation",
|
|
||||||
"backhaul",
|
|
||||||
"relay",
|
|
||||||
"core-channel",
|
|
||||||
"tailscale-retirement",
|
|
||||||
"tracker-ingress",
|
|
||||||
):
|
):
|
||||||
with self.subTest(phase=phase), tempfile.TemporaryDirectory(
|
with self.subTest(phase=phase), tempfile.TemporaryDirectory(
|
||||||
prefix=f"nodedc-vps-{phase}-"
|
prefix=f"nodedc-vps-{phase}-"
|
||||||
@@ -173,13 +183,7 @@ class DeviceEdgeVpsArtifactTest(unittest.TestCase):
|
|||||||
RUNNER.INBOX_ROOT = inbox
|
RUNNER.INBOX_ROOT = inbox
|
||||||
try:
|
try:
|
||||||
for phase in (
|
for phase in (
|
||||||
"foundation",
|
"command-transport",
|
||||||
"runtime-reconciliation",
|
|
||||||
"backhaul",
|
|
||||||
"relay",
|
|
||||||
"core-channel",
|
|
||||||
"tailscale-retirement",
|
|
||||||
"tracker-ingress",
|
|
||||||
):
|
):
|
||||||
result = self.build(
|
result = self.build(
|
||||||
inbox,
|
inbox,
|
||||||
@@ -233,6 +237,7 @@ class DeviceEdgeVpsArtifactTest(unittest.TestCase):
|
|||||||
self.assertIn("b2_routes=unchanged", rendered)
|
self.assertIn("b2_routes=unchanged", rendered)
|
||||||
self.assertIn("command_transport=disabled", rendered)
|
self.assertIn("command_transport=disabled", rendered)
|
||||||
|
|
||||||
|
@unittest.skip("accepted core-channel builder generation is frozen")
|
||||||
def test_core_channel_plan_is_exact_and_keeps_tracker_ingress_closed(self):
|
def test_core_channel_plan_is_exact_and_keeps_tracker_ingress_closed(self):
|
||||||
with tempfile.TemporaryDirectory(prefix="nodedc-vps-channel-plan-") as directory:
|
with tempfile.TemporaryDirectory(prefix="nodedc-vps-channel-plan-") as directory:
|
||||||
inbox = Path(directory) / "inbox"
|
inbox = Path(directory) / "inbox"
|
||||||
@@ -363,6 +368,7 @@ class DeviceEdgeVpsArtifactTest(unittest.TestCase):
|
|||||||
self.assertIn("command_transport=disabled", rendered)
|
self.assertIn("command_transport=disabled", rendered)
|
||||||
self.assertIn("gelios=untouched", rendered)
|
self.assertIn("gelios=untouched", rendered)
|
||||||
|
|
||||||
|
@unittest.skip("accepted tracker-ingress builder generation is frozen")
|
||||||
def test_tracker_ingress_plan_is_single_process_bounded_and_command_free(self):
|
def test_tracker_ingress_plan_is_single_process_bounded_and_command_free(self):
|
||||||
with tempfile.TemporaryDirectory(prefix="nodedc-vps-ingress-plan-") as directory:
|
with tempfile.TemporaryDirectory(prefix="nodedc-vps-ingress-plan-") as directory:
|
||||||
inbox = Path(directory) / "inbox"
|
inbox = Path(directory) / "inbox"
|
||||||
@@ -402,6 +408,40 @@ class DeviceEdgeVpsArtifactTest(unittest.TestCase):
|
|||||||
self.assertIn("command_transport=disabled", rendered)
|
self.assertIn("command_transport=disabled", rendered)
|
||||||
self.assertIn("gelios=untouched", rendered)
|
self.assertIn("gelios=untouched", rendered)
|
||||||
|
|
||||||
|
def test_command_transport_plan_is_typed_single_process_and_bounded(self):
|
||||||
|
with tempfile.TemporaryDirectory(prefix="nodedc-vps-command-plan-") as directory:
|
||||||
|
inbox = Path(directory) / "inbox"
|
||||||
|
inbox.mkdir()
|
||||||
|
result = self.build(
|
||||||
|
inbox,
|
||||||
|
"command-transport",
|
||||||
|
"device-edge-vps-command-transport-plan-001",
|
||||||
|
)
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
artifact = Path(json.loads(result.stdout)["artifact"])
|
||||||
|
old_inbox = RUNNER.INBOX_ROOT
|
||||||
|
RUNNER.INBOX_ROOT = inbox
|
||||||
|
try:
|
||||||
|
with patch.object(RUNNER, "assert_root"), patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"preflight",
|
||||||
|
return_value={"predecessor": "accepted-tracker-ingress-012"},
|
||||||
|
), patch("builtins.print") as output:
|
||||||
|
RUNNER.plan_artifact(str(artifact))
|
||||||
|
finally:
|
||||||
|
RUNNER.INBOX_ROOT = old_inbox
|
||||||
|
rendered = "\n".join(
|
||||||
|
" ".join(str(arg) for arg in call.args)
|
||||||
|
for call in output.call_args_list
|
||||||
|
)
|
||||||
|
self.assertIn("phase=command-transport", rendered)
|
||||||
|
self.assertIn("predecessor=accepted-tracker-ingress-012", rendered)
|
||||||
|
self.assertIn("command_transport=typed-service-ping-v1", rendered)
|
||||||
|
self.assertIn("command_catalog=allowlisted-adapter-typed-commands-only", rendered)
|
||||||
|
self.assertIn("runtime_composition=single-non-root-process", rendered)
|
||||||
|
self.assertIn("public_b2_ingress=155.212.211.15:9921/tcp:bidirectional-session", rendered)
|
||||||
|
self.assertIn("gelios=untouched-legacy-only", rendered)
|
||||||
|
|
||||||
def test_publish_payload_preserves_unselected_executable_modes(self):
|
def test_publish_payload_preserves_unselected_executable_modes(self):
|
||||||
with tempfile.TemporaryDirectory(prefix="nodedc-vps-publish-scope-") as directory:
|
with tempfile.TemporaryDirectory(prefix="nodedc-vps-publish-scope-") as directory:
|
||||||
root = Path(directory)
|
root = Path(directory)
|
||||||
|
|||||||
@@ -104,6 +104,25 @@ def device_manager_release_v1_descriptor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def device_manager_release_v3_descriptor(
|
||||||
|
release_id="device-manager-release-v3-unit-001",
|
||||||
|
*,
|
||||||
|
predecessor_patch="device-manager-release-20260811-010",
|
||||||
|
predecessor_sha="d" * 64,
|
||||||
|
):
|
||||||
|
return {
|
||||||
|
"schemaVersion": "nodedc.device-plane.device-manager-release.v3",
|
||||||
|
"releaseId": release_id,
|
||||||
|
"action": "upgrade",
|
||||||
|
"predecessor": {
|
||||||
|
"kind": "release",
|
||||||
|
"patchId": predecessor_patch,
|
||||||
|
"artifactSha256": predecessor_sha,
|
||||||
|
},
|
||||||
|
**RUNNER.expected_device_plane_manager_release_v3_boundaries(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase):
|
class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase):
|
||||||
def build(self, script, patch_id, artifact_dir):
|
def build(self, script, patch_id, artifact_dir):
|
||||||
environment = os.environ.copy()
|
environment = os.environ.copy()
|
||||||
@@ -253,6 +272,57 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase):
|
|||||||
self.assertEqual(checks[0]["expected_json"]["managementApi"], "enabled")
|
self.assertEqual(checks[0]["expected_json"]["managementApi"], "enabled")
|
||||||
self.assertEqual(checks[0]["expected_json"]["discoveryIngest"], "enabled")
|
self.assertEqual(checks[0]["expected_json"]["discoveryIngest"], "enabled")
|
||||||
|
|
||||||
|
def test_device_manager_release_v3_is_exact_typed_and_secret_free(self):
|
||||||
|
patch_id = "device-manager-release-v3-unit-001"
|
||||||
|
manifest, entries, names, result = self.assert_deterministic_artifact(
|
||||||
|
"build-device-manager-control-plane-artifact.mjs",
|
||||||
|
patch_id,
|
||||||
|
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V3_ENTRIES,
|
||||||
|
)
|
||||||
|
self.assertEqual(manifest["component"], "device-plane")
|
||||||
|
self.assertEqual(
|
||||||
|
RUNNER.component_services("device-plane", entries),
|
||||||
|
("device-manager",),
|
||||||
|
)
|
||||||
|
self.assertEqual(result["services"], ["device-manager"])
|
||||||
|
self.assertIn(
|
||||||
|
"payload/deployment/device-manager-release-v3.json",
|
||||||
|
names,
|
||||||
|
)
|
||||||
|
self.assertFalse(
|
||||||
|
any(name.startswith("payload/services/device-control-core/") for name in names)
|
||||||
|
)
|
||||||
|
self.assertFalse(
|
||||||
|
any(name.startswith("payload/packages/") for name in names)
|
||||||
|
)
|
||||||
|
self.assertFalse(any(name.endswith(".test.mjs") for name in names))
|
||||||
|
descriptor = device_manager_release_v3_descriptor(patch_id)
|
||||||
|
self.assertIs(
|
||||||
|
RUNNER.validate_device_plane_manager_release_descriptor(
|
||||||
|
descriptor,
|
||||||
|
schema_version=(
|
||||||
|
"nodedc.device-plane.device-manager-release.v3"
|
||||||
|
),
|
||||||
|
boundaries=(
|
||||||
|
RUNNER.expected_device_plane_manager_release_v3_boundaries()
|
||||||
|
),
|
||||||
|
expected_release_id=patch_id,
|
||||||
|
),
|
||||||
|
descriptor,
|
||||||
|
)
|
||||||
|
self.assertEqual(descriptor["commandTransport"], "typed-service-ping-v1")
|
||||||
|
self.assertEqual(
|
||||||
|
descriptor["commandCatalog"],
|
||||||
|
"allowlisted-adapter-typed-commands-only",
|
||||||
|
)
|
||||||
|
self.assertEqual(descriptor["gelios"], "untouched-legacy-only")
|
||||||
|
self.assertTrue(
|
||||||
|
RUNNER.is_device_plane_manager_release_v3_slice(
|
||||||
|
"device-plane",
|
||||||
|
entries,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def test_edge_core_channel_bootstrap_is_core_only_and_secret_free(self):
|
def test_edge_core_channel_bootstrap_is_core_only_and_secret_free(self):
|
||||||
manifest, entries, names, result = self.assert_deterministic_artifact(
|
manifest, entries, names, result = self.assert_deterministic_artifact(
|
||||||
"build-device-edge-core-channel-bootstrap-artifact.mjs",
|
"build-device-edge-core-channel-bootstrap-artifact.mjs",
|
||||||
@@ -529,6 +599,42 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase):
|
|||||||
core_network_mode="private-egress",
|
core_network_mode="private-egress",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_control_core_release_v2_is_typed_core_only(self):
|
||||||
|
patch_id = "device-control-core-release-v2-unit-001"
|
||||||
|
manifest, entries, names, result = self.assert_deterministic_artifact(
|
||||||
|
"build-device-control-core-release-artifact.mjs",
|
||||||
|
patch_id,
|
||||||
|
RUNNER.DEVICE_PLANE_CONTROL_CORE_RELEASE_V2_ENTRIES,
|
||||||
|
)
|
||||||
|
self.assertEqual(manifest["component"], "device-plane")
|
||||||
|
self.assertEqual(result["services"], ["device-control-core"])
|
||||||
|
self.assertIn(
|
||||||
|
"payload/deployment/device-control-core-release-v2.json",
|
||||||
|
names,
|
||||||
|
)
|
||||||
|
self.assertFalse(any("docker-compose" in name for name in names))
|
||||||
|
self.assertEqual(
|
||||||
|
RUNNER.component_services("device-plane", entries),
|
||||||
|
("device-control-core",),
|
||||||
|
)
|
||||||
|
descriptor = RUNNER.expected_device_plane_control_core_release_descriptor(
|
||||||
|
patch_id,
|
||||||
|
{
|
||||||
|
"kind": "release",
|
||||||
|
"patchId": "device-control-core-release-20260812-024",
|
||||||
|
"artifactSha256": (
|
||||||
|
"a289e909283109642e6bba3d9822a31f63423cfe0bbcd52705979681bd2bc793"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
schema_version="v2",
|
||||||
|
)
|
||||||
|
self.assertEqual(descriptor["commandTransport"], "typed-service-ping-v1")
|
||||||
|
self.assertEqual(
|
||||||
|
descriptor["commandCatalog"],
|
||||||
|
"allowlisted-adapter-typed-commands-only",
|
||||||
|
)
|
||||||
|
self.assertEqual(descriptor["gelios"], "untouched-legacy-only")
|
||||||
|
|
||||||
def test_control_core_release_builder_supports_release_predecessor(self):
|
def test_control_core_release_builder_supports_release_predecessor(self):
|
||||||
with tempfile.TemporaryDirectory(
|
with tempfile.TemporaryDirectory(
|
||||||
prefix="nodedc-control-core-successor-",
|
prefix="nodedc-control-core-successor-",
|
||||||
|
|||||||
Reference in New Issue
Block a user