@forgezero/agent 0.1.79 → 0.1.81
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -7
- package/dist/agent-heartbeat.js +1 -1
- package/dist/bootstrap.js +21 -6
- package/dist/cli/app-build.d.ts +10 -0
- package/dist/deploy-compiler.js +50 -2
- package/dist/deploy-plan-runner.js +50 -2
- package/dist/deploy-plan.js +50 -2
- package/dist/deploy.d.ts +37 -1
- package/dist/deploy.js +1 -0
- package/dist/deployment-connectivity.d.ts +26 -0
- package/dist/deployment-connectivity.js +128 -1
- package/dist/deployment-targets.js +15 -1
- package/dist/deployment-topology.d.ts +46 -0
- package/dist/deployment-topology.js +113 -0
- package/dist/deployment.d.ts +40 -0
- package/dist/fz-agent.js +423 -30
- package/dist/fz.js +155 -26
- package/dist/index.d.ts +1 -0
- package/dist/metal-bootstrap.js +13 -1
- package/dist/metal-helper-socket.js +55 -16
- package/dist/metal-provision.d.ts +6 -3
- package/dist/metal-provision.js +55 -16
- package/dist/operator-bootstrap.js +33 -6
- package/dist/platform-bootstrap-runtime.d.ts +4 -1
- package/dist/platform-bootstrap-runtime.js +18 -4
- package/dist/platform-fleet-verification.js +147 -6
- package/dist/provision.js +129 -2
- package/dist/software-helper.js +128 -1
- package/dist/version.d.ts +1 -1
- package/package.json +5 -1
package/dist/fz-agent.js
CHANGED
|
@@ -6384,7 +6384,7 @@ function reference(value, where) {
|
|
|
6384
6384
|
const row2 = object(value, where);
|
|
6385
6385
|
exact3(row2, ["$ref"], where);
|
|
6386
6386
|
const coordinate = boundedString(row2.$ref, `${where}.$ref`, 256);
|
|
6387
|
-
if (!/^(inputs\.[a-z][A-Za-z0-9-]{0,62}|steps\.[a-z][A-Za-z0-9-]{0,62}\.(status|outputs\.[a-z][A-Za-z0-9-]{0,62})|components\.[a-z][A-Za-z0-9-]{0,62}\.[a-z][A-Za-z0-9-]{0,62}|targets\.[a-z][A-Za-z0-9-]{0,62}\.[a-z][A-Za-z0-9-]{0,62}|deployment\.[a-z][A-Za-z0-9-]{0,62}|execution\.(targetName|slot|releaseExecutor|runtime|cpuCores|memoryMiB|storageGiB))$/.test(coordinate)) {
|
|
6387
|
+
if (!/^(inputs\.[a-z][A-Za-z0-9-]{0,62}|steps\.[a-z][A-Za-z0-9-]{0,62}\.(status|outputs\.[a-z][A-Za-z0-9-]{0,62})|components\.[a-z][A-Za-z0-9-]{0,62}\.[a-z][A-Za-z0-9-]{0,62}|targets\.[a-z][A-Za-z0-9-]{0,62}\.[a-z][A-Za-z0-9-]{0,62}|deployment\.[a-z][A-Za-z0-9-]{0,62}|execution\.(targetName|slot|releaseExecutor|runtime|cpuCores|memoryMiB|storageGiB|site|topologyRole|topologyTransport|localRelayAddress))$/.test(coordinate)) {
|
|
6388
6388
|
fail(where, "contains an unsupported reference.");
|
|
6389
6389
|
}
|
|
6390
6390
|
return { $ref: coordinate };
|
|
@@ -6816,6 +6816,12 @@ function validateStep(value, where, targets, context) {
|
|
|
6816
6816
|
if (!targets.has(String(scope.target)))
|
|
6817
6817
|
fail(`${where}.scope.target`, "must name an existing target.");
|
|
6818
6818
|
integer(scope.size, `${where}.scope.size`, 1, 32);
|
|
6819
|
+
} else if (scope.kind === "topology-role") {
|
|
6820
|
+
exact3(scope, ["kind", "target", "role"], `${where}.scope`);
|
|
6821
|
+
if (!targets.has(String(scope.target)))
|
|
6822
|
+
fail(`${where}.scope.target`, "must name an existing target.");
|
|
6823
|
+
if (!["relay", "standby", "member"].includes(String(scope.role)))
|
|
6824
|
+
fail(`${where}.scope.role`, "is unsupported.");
|
|
6819
6825
|
} else
|
|
6820
6826
|
fail(`${where}.scope.kind`, "is unsupported.");
|
|
6821
6827
|
if (row2.timeoutMs !== undefined)
|
|
@@ -6970,7 +6976,7 @@ function compileDeployment(sourceValue) {
|
|
|
6970
6976
|
const targetDefinitions = new Map;
|
|
6971
6977
|
for (const [targetName, value] of targetEntries) {
|
|
6972
6978
|
const target = object(value, `deployment.spec.targets.${targetName}`);
|
|
6973
|
-
exact3(target, ["kind", "selector", "cardinality", "allocation", "provisioning", "connectivity", "placement"], `deployment.spec.targets.${targetName}`);
|
|
6979
|
+
exact3(target, ["kind", "selector", "cardinality", "allocation", "provisioning", "connectivity", "topology", "placement"], `deployment.spec.targets.${targetName}`);
|
|
6974
6980
|
if (target.kind !== "compute")
|
|
6975
6981
|
fail(`deployment.spec.targets.${targetName}.kind`, "must be compute.");
|
|
6976
6982
|
const selector = object(target.selector, `deployment.spec.targets.${targetName}.selector`);
|
|
@@ -7092,6 +7098,45 @@ function compileDeployment(sourceValue) {
|
|
|
7092
7098
|
}
|
|
7093
7099
|
}
|
|
7094
7100
|
}
|
|
7101
|
+
if (target.topology !== undefined) {
|
|
7102
|
+
const topology = object(target.topology, `deployment.spec.targets.${targetName}.topology`);
|
|
7103
|
+
exact3(topology, ["groupBy", "relays", "crossMetal"], `deployment.spec.targets.${targetName}.topology`);
|
|
7104
|
+
if (topology.groupBy !== "metal")
|
|
7105
|
+
fail(`deployment.spec.targets.${targetName}.topology.groupBy`, "must be metal.");
|
|
7106
|
+
const relays = object(topology.relays, `deployment.spec.targets.${targetName}.topology.relays`);
|
|
7107
|
+
exact3(relays, ["activePerMetal", "standbyPerMetal", "healthPort", "routedTcpPorts"], `deployment.spec.targets.${targetName}.topology.relays`);
|
|
7108
|
+
if (relays.activePerMetal !== 1 || ![0, 1].includes(Number(relays.standbyPerMetal)))
|
|
7109
|
+
fail(`deployment.spec.targets.${targetName}.topology.relays`, "requires one active relay and zero or one standby per metal.");
|
|
7110
|
+
integer(relays.healthPort, `deployment.spec.targets.${targetName}.topology.relays.healthPort`, 1, 65535);
|
|
7111
|
+
if (!Array.isArray(relays.routedTcpPorts) || relays.routedTcpPorts.length < 1 || relays.routedTcpPorts.length > 64 || new Set(relays.routedTcpPorts).size !== relays.routedTcpPorts.length) {
|
|
7112
|
+
fail(`deployment.spec.targets.${targetName}.topology.relays.routedTcpPorts`, "must contain 1 to 64 unique ports.");
|
|
7113
|
+
}
|
|
7114
|
+
for (const [index, port] of relays.routedTcpPorts.entries()) {
|
|
7115
|
+
integer(port, `deployment.spec.targets.${targetName}.topology.relays.routedTcpPorts.${index}`, 1, 65535);
|
|
7116
|
+
}
|
|
7117
|
+
if (!relays.routedTcpPorts.includes(relays.healthPort)) {
|
|
7118
|
+
fail(`deployment.spec.targets.${targetName}.topology.relays`, "routedTcpPorts must include healthPort.");
|
|
7119
|
+
}
|
|
7120
|
+
const crossMetal = object(topology.crossMetal, `deployment.spec.targets.${targetName}.topology.crossMetal`);
|
|
7121
|
+
if (crossMetal.mode === "private-lan")
|
|
7122
|
+
exact3(crossMetal, ["mode"], `deployment.spec.targets.${targetName}.topology.crossMetal`);
|
|
7123
|
+
else if (crossMetal.mode === "cloudflare-warp" || crossMetal.mode === "auto") {
|
|
7124
|
+
exact3(crossMetal, crossMetal.mode === "auto" ? ["mode", "directPrivateMaximumMetals", "credential", "network"] : ["mode", "credential", "network"], `deployment.spec.targets.${targetName}.topology.crossMetal`);
|
|
7125
|
+
if (crossMetal.mode === "auto" && crossMetal.directPrivateMaximumMetals !== 2)
|
|
7126
|
+
fail(`deployment.spec.targets.${targetName}.topology.crossMetal.directPrivateMaximumMetals`, "must be 2.");
|
|
7127
|
+
if (!credentials[named(crossMetal.credential, `deployment.spec.targets.${targetName}.topology.crossMetal.credential`)])
|
|
7128
|
+
fail(`deployment.spec.targets.${targetName}.topology.crossMetal.credential`, "must name a declared credential.");
|
|
7129
|
+
if (typeof crossMetal.network === "string")
|
|
7130
|
+
named(crossMetal.network, `deployment.spec.targets.${targetName}.topology.crossMetal.network`);
|
|
7131
|
+
else
|
|
7132
|
+
reference(crossMetal.network, `deployment.spec.targets.${targetName}.topology.crossMetal.network`);
|
|
7133
|
+
} else
|
|
7134
|
+
fail(`deployment.spec.targets.${targetName}.topology.crossMetal.mode`, "is unsupported.");
|
|
7135
|
+
if (allocation.sharing !== "exclusive")
|
|
7136
|
+
fail(`deployment.spec.targets.${targetName}.allocation.sharing`, "must be exclusive for a per-metal relay topology.");
|
|
7137
|
+
if (minimum < 1 + Number(relays.standbyPerMetal))
|
|
7138
|
+
fail(`deployment.spec.targets.${targetName}.cardinality.minimum`, "must allow the declared relay and standby.");
|
|
7139
|
+
}
|
|
7095
7140
|
if (target.placement !== undefined)
|
|
7096
7141
|
jsonValue(target.placement, `deployment.spec.targets.${targetName}.placement`);
|
|
7097
7142
|
targetDefinitions.set(targetName, target);
|
|
@@ -7115,6 +7160,9 @@ function compileDeployment(sourceValue) {
|
|
|
7115
7160
|
if (target.connectivity?.private?.mode === "cloudflare-warp" && ![...requirementDefinitions.values()].some(({ capability }) => capability === "network.private")) {
|
|
7116
7161
|
fail(`deployment.spec.targets.${targetName}.connectivity.private`, "requires one network.private provider requirement.");
|
|
7117
7162
|
}
|
|
7163
|
+
if (target.topology && target.topology.crossMetal.mode !== "private-lan" && ![...requirementDefinitions.values()].some(({ capability }) => capability === "network.private")) {
|
|
7164
|
+
fail(`deployment.spec.targets.${targetName}.topology.crossMetal`, "requires one network.private provider requirement.");
|
|
7165
|
+
}
|
|
7118
7166
|
}
|
|
7119
7167
|
const components = object(spec.components, "deployment.spec.components");
|
|
7120
7168
|
const componentEntries = Object.entries(components);
|
|
@@ -7372,6 +7420,19 @@ function resolveDeploymentTargets(plan, supplied = {}) {
|
|
|
7372
7420
|
} : { mode: "disabled" } } : {}
|
|
7373
7421
|
};
|
|
7374
7422
|
}
|
|
7423
|
+
let topology;
|
|
7424
|
+
if (target.topology) {
|
|
7425
|
+
const crossMetal = target.topology.crossMetal;
|
|
7426
|
+
topology = {
|
|
7427
|
+
groupBy: "metal",
|
|
7428
|
+
relays: { ...target.topology.relays },
|
|
7429
|
+
crossMetal: crossMetal.mode === "private-lan" ? { mode: "private-lan" } : {
|
|
7430
|
+
...crossMetal,
|
|
7431
|
+
credential: credentialSource(crossMetal.credential, `target ${name} topology WARP credential`),
|
|
7432
|
+
network: stringCoordinate(crossMetal.network, `target ${name} topology WARP network`)
|
|
7433
|
+
}
|
|
7434
|
+
};
|
|
7435
|
+
}
|
|
7375
7436
|
return {
|
|
7376
7437
|
name,
|
|
7377
7438
|
profiles: [...target.selector.profiles],
|
|
@@ -7390,7 +7451,8 @@ function resolveDeploymentTargets(plan, supplied = {}) {
|
|
|
7390
7451
|
}
|
|
7391
7452
|
},
|
|
7392
7453
|
...target.provisioning ? { provisioning: { ...target.provisioning } } : {},
|
|
7393
|
-
...connectivity ? { connectivity } : {}
|
|
7454
|
+
...connectivity ? { connectivity } : {},
|
|
7455
|
+
...topology ? { topology } : {}
|
|
7394
7456
|
};
|
|
7395
7457
|
});
|
|
7396
7458
|
}
|
|
@@ -8096,12 +8158,19 @@ function createDeploymentManager(options) {
|
|
|
8096
8158
|
if (!selected || mismatches.length > 0) {
|
|
8097
8159
|
throw new DeploymentError("PIPELINE_FAILED", `the control-plane target assignment does not match this exact deployment plan (${mismatches.join(", ")})`);
|
|
8098
8160
|
}
|
|
8099
|
-
const
|
|
8161
|
+
const assignedTopology = assigned.topology;
|
|
8162
|
+
const selectedTopology = selected.topology;
|
|
8163
|
+
const topologyMatches = selectedTopology ? Boolean(assignedTopology && /^[A-Za-z0-9.-]{1,253}$/.test(assignedTopology.site) && /^[A-Za-z0-9.-]{1,253}$/.test(assignedTopology.nodeIdentity) && /^[A-Za-z0-9._:-]{1,256}$/.test(assignedTopology.localRelayReference) && /^sha256:[a-f0-9]{64}$/.test(assignedTopology.generation) && /^(?:\d{1,3}\.){3}0\/24$/.test(assignedTopology.siteCidr) && assignedTopology.localRelayAddress.length <= 64 && assignedTopology.localPeerAddresses.length <= 1023 && assignedTopology.localPeerAddresses.every((address) => typeof address === "string" && address.length <= 64) && assignedTopology.localPeerIdentities.length === assignedTopology.localPeerAddresses.length && assignedTopology.localPeerIdentities.every((identity) => /^[A-Za-z0-9.-]{1,253}$/.test(identity)) && assignedTopology.remoteRelayAddresses.length <= 1023 && assignedTopology.remoteRelayAddresses.every((address) => typeof address === "string" && address.length <= 64) && assignedTopology.remoteRelayIdentities.length === assignedTopology.remoteRelayAddresses.length && assignedTopology.remoteRelayIdentities.every((identity) => /^[A-Za-z0-9.-]{1,253}$/.test(identity)) && assignedTopology.memberIdentities.length >= 1 && assignedTopology.memberIdentities.length <= 1024 && new Set(assignedTopology.memberIdentities).size === assignedTopology.memberIdentities.length && assignedTopology.memberIdentities.includes(assignedTopology.nodeIdentity) && assignedTopology.memberIdentities.every((identity) => /^[A-Za-z0-9.-]{1,253}$/.test(identity)) && (assignedTopology.role === "member" ? assignedTopology.remoteRelayAddresses.length === 0 : assignedTopology.remoteSiteCidrs.length === assignedTopology.remoteRelayAddresses.length) && assignedTopology.remoteSiteCidrs.every((cidr) => typeof cidr === "string" && /^(?:\d{1,3}\.){3}0\/24$/.test(cidr)) && Number.isSafeInteger(assignedTopology.healthPort) && assignedTopology.healthPort >= 1 && assignedTopology.healthPort <= 65535 && assignedTopology.routedTcpPorts.length >= 1 && assignedTopology.routedTcpPorts.length <= 64 && new Set(assignedTopology.routedTcpPorts).size === assignedTopology.routedTcpPorts.length && assignedTopology.routedTcpPorts.includes(assignedTopology.healthPort) && assignedTopology.routedTcpPorts.every((port) => Number.isSafeInteger(port) && port >= 1 && port <= 65535) && (assignedTopology.role !== "member" || assignedTopology.transport === "private-lan" && assignedTopology.warpRequired === false && assignedTopology.remoteRelayAddresses.length === 0 && assignedTopology.localPeerAddresses.length === 1) && (assignedTopology.warpRequired ? assignedTopology.role !== "member" && assignedTopology.transport === "cloudflare-warp" && selectedTopology.crossMetal.mode !== "private-lan" : assignedTopology.transport === "private-lan")) : assignedTopology === undefined;
|
|
8164
|
+
const expectedPrivate = selectedTopology ? assignedTopology?.warpRequired ? selectedTopology.crossMetal.mode === "private-lan" ? undefined : {
|
|
8165
|
+
mode: "cloudflare-warp",
|
|
8166
|
+
credential: selectedTopology.crossMetal.credential,
|
|
8167
|
+
network: selectedTopology.crossMetal.network
|
|
8168
|
+
} : { mode: "private-lan" } : selected.connectivity?.private;
|
|
8100
8169
|
const expectedPublic = selected.connectivity?.public;
|
|
8101
8170
|
const assignedPrivate = assigned.connectivity?.private;
|
|
8102
8171
|
const assignedPublic = assigned.connectivity?.public;
|
|
8103
8172
|
const publicMatches = expectedPublic?.mode === "cloudflare-tunnel" ? assignedPublic?.mode === "cloudflare-tunnel" && assignedPublic.credential === expectedPublic.credential && assignedPublic.service.protocol === expectedPublic.service.protocol && assignedPublic.service.port === expectedPublic.service.port && assignedPublic.hostname === (expectedPublic.hostname.mode === "static" ? `${expectedPublic.hostname.label}.${expectedPublic.zone}` : `${expectedPublic.hostname.prefix}${expectedPublic.hostname.startAt + assigned.slot}.${expectedPublic.zone}`) : expectedPublic?.mode === "disabled" ? assignedPublic?.mode === "disabled" : assignedPublic === undefined;
|
|
8104
|
-
if (JSON.stringify(assignedPrivate) !== JSON.stringify(expectedPrivate) || !publicMatches) {
|
|
8173
|
+
if (!topologyMatches || JSON.stringify(assignedPrivate) !== JSON.stringify(expectedPrivate) || !publicMatches) {
|
|
8105
8174
|
throw new DeploymentError("PIPELINE_FAILED", "the control-plane connectivity assignment does not match this target slot");
|
|
8106
8175
|
}
|
|
8107
8176
|
targetNames = new Set([assigned.targetName]);
|
|
@@ -8118,7 +8187,24 @@ function createDeploymentManager(options) {
|
|
|
8118
8187
|
FZ_TARGET_CPU_CORES: String(assigned.resources.cpuCores),
|
|
8119
8188
|
FZ_TARGET_MEMORY_MIB: String(assigned.resources.memoryMiB),
|
|
8120
8189
|
FZ_TARGET_STORAGE_GIB: String(assigned.resources.storageGiB),
|
|
8121
|
-
FZ_TARGET_EXECUTION: assigned.execution
|
|
8190
|
+
FZ_TARGET_EXECUTION: assigned.execution,
|
|
8191
|
+
...assigned.topology ? {
|
|
8192
|
+
FZ_TOPOLOGY_SITE: assigned.topology.site,
|
|
8193
|
+
FZ_TOPOLOGY_NODE_IDENTITY: assigned.topology.nodeIdentity,
|
|
8194
|
+
FZ_TOPOLOGY_SITE_CIDR: assigned.topology.siteCidr,
|
|
8195
|
+
FZ_TOPOLOGY_ROLE: assigned.topology.role,
|
|
8196
|
+
FZ_TOPOLOGY_TRANSPORT: assigned.topology.transport,
|
|
8197
|
+
FZ_TOPOLOGY_LOCAL_RELAY: assigned.topology.localRelayAddress,
|
|
8198
|
+
FZ_TOPOLOGY_LOCAL_PEERS: assigned.topology.localPeerAddresses.join(","),
|
|
8199
|
+
FZ_TOPOLOGY_LOCAL_PEER_IDENTITIES: assigned.topology.localPeerIdentities.join(","),
|
|
8200
|
+
FZ_TOPOLOGY_REMOTE_RELAYS: assigned.topology.remoteRelayAddresses.join(","),
|
|
8201
|
+
FZ_TOPOLOGY_REMOTE_RELAY_IDENTITIES: assigned.topology.remoteRelayIdentities.join(","),
|
|
8202
|
+
FZ_TOPOLOGY_REMOTE_CIDRS: assigned.topology.remoteSiteCidrs.join(","),
|
|
8203
|
+
FZ_TOPOLOGY_MEMBERS: assigned.topology.memberIdentities.join(","),
|
|
8204
|
+
FZ_TOPOLOGY_HEALTH_PORT: String(assigned.topology.healthPort),
|
|
8205
|
+
FZ_TOPOLOGY_ROUTED_TCP_PORTS: assigned.topology.routedTcpPorts.join(","),
|
|
8206
|
+
FZ_TOPOLOGY_GENERATION: assigned.topology.generation
|
|
8207
|
+
} : {}
|
|
8122
8208
|
});
|
|
8123
8209
|
for (const targetName of targetNames) {
|
|
8124
8210
|
const capacity2 = resolvedTargets.find(({ name }) => name === targetName)?.allocation.resources;
|
|
@@ -8174,11 +8260,15 @@ function createDeploymentManager(options) {
|
|
|
8174
8260
|
return { changed: true };
|
|
8175
8261
|
},
|
|
8176
8262
|
"forgezero.network/ensure@1": async ({ resolved }) => {
|
|
8263
|
+
if (!assigned && bootstrapBundle && typeof resolved.target === "string" && targetNames.has(resolved.target)) {
|
|
8264
|
+
return { changed: false, evidence: { target: resolved.target, authority: "attended-bootstrap" } };
|
|
8265
|
+
}
|
|
8177
8266
|
if (!assigned || typeof resolved.target !== "string" || resolved.target !== assigned.targetName) {
|
|
8178
8267
|
throw new DeploymentActionError("refused", "network.ensure must target this exact control-plane assignment");
|
|
8179
8268
|
}
|
|
8180
|
-
if (!assigned.connectivity)
|
|
8181
|
-
return { changed: false, evidence: { target: assigned.targetName } };
|
|
8269
|
+
if (!assigned.connectivity) {
|
|
8270
|
+
return { changed: false, evidence: { target: assigned.targetName, authority: "none" } };
|
|
8271
|
+
}
|
|
8182
8272
|
if (!options.ensureSoftware || !options.applyConnectivity) {
|
|
8183
8273
|
throw new DeploymentActionError("provider-unavailable", "the supervised connectivity helper is unavailable");
|
|
8184
8274
|
}
|
|
@@ -8187,12 +8277,31 @@ function createDeploymentManager(options) {
|
|
|
8187
8277
|
requirements.push({ id: "cloudflared", version: "latest" });
|
|
8188
8278
|
if (assigned.connectivity.private?.mode === "cloudflare-warp")
|
|
8189
8279
|
requirements.push({ id: "cloudflare-warp", version: "latest" });
|
|
8280
|
+
if (assigned.topology)
|
|
8281
|
+
requirements.push({ id: "ufw", version: "latest" });
|
|
8190
8282
|
if (requirements.length > 0)
|
|
8191
8283
|
await options.ensureSoftware(requirements);
|
|
8192
8284
|
const evidence = await options.applyConnectivity({
|
|
8193
8285
|
key: `${options.key}:${assigned.targetName}:${assigned.slot}`,
|
|
8194
8286
|
intent: assigned.connectivity,
|
|
8195
|
-
capabilities: request.connectivityCapabilities ?? {}
|
|
8287
|
+
capabilities: request.connectivityCapabilities ?? {},
|
|
8288
|
+
...assigned.topology ? { topology: {
|
|
8289
|
+
site: assigned.topology.site,
|
|
8290
|
+
nodeIdentity: assigned.topology.nodeIdentity,
|
|
8291
|
+
siteCidr: assigned.topology.siteCidr,
|
|
8292
|
+
role: assigned.topology.role,
|
|
8293
|
+
transport: assigned.topology.transport,
|
|
8294
|
+
localRelayAddress: assigned.topology.localRelayAddress,
|
|
8295
|
+
localPeerAddresses: assigned.topology.localPeerAddresses,
|
|
8296
|
+
localPeerIdentities: assigned.topology.localPeerIdentities,
|
|
8297
|
+
remoteRelayAddresses: assigned.topology.remoteRelayAddresses,
|
|
8298
|
+
remoteRelayIdentities: assigned.topology.remoteRelayIdentities,
|
|
8299
|
+
remoteSiteCidrs: assigned.topology.remoteSiteCidrs,
|
|
8300
|
+
memberIdentities: assigned.topology.memberIdentities,
|
|
8301
|
+
healthPort: assigned.topology.healthPort,
|
|
8302
|
+
routedTcpPorts: assigned.topology.routedTcpPorts,
|
|
8303
|
+
generation: assigned.topology.generation
|
|
8304
|
+
} } : {}
|
|
8196
8305
|
});
|
|
8197
8306
|
return { changed: true, evidence: structuredClone(evidence) };
|
|
8198
8307
|
},
|
|
@@ -8301,7 +8410,13 @@ function createDeploymentManager(options) {
|
|
|
8301
8410
|
runtime: assigned.execution,
|
|
8302
8411
|
cpuCores: assigned.resources.cpuCores,
|
|
8303
8412
|
memoryMiB: assigned.resources.memoryMiB,
|
|
8304
|
-
storageGiB: assigned.resources.storageGiB
|
|
8413
|
+
storageGiB: assigned.resources.storageGiB,
|
|
8414
|
+
...assigned.topology ? {
|
|
8415
|
+
site: assigned.topology.site,
|
|
8416
|
+
topologyRole: assigned.topology.role,
|
|
8417
|
+
topologyTransport: assigned.topology.transport,
|
|
8418
|
+
localRelayAddress: assigned.topology.localRelayAddress
|
|
8419
|
+
} : {}
|
|
8305
8420
|
} : {
|
|
8306
8421
|
targetName: matchingTargetNames[0],
|
|
8307
8422
|
slot: 0,
|
|
@@ -8311,7 +8426,7 @@ function createDeploymentManager(options) {
|
|
|
8311
8426
|
memoryMiB: resolvedTargets[0].allocation.resources.memoryMiB,
|
|
8312
8427
|
storageGiB: resolvedTargets[0].allocation.resources.storageGiB
|
|
8313
8428
|
},
|
|
8314
|
-
shouldRun: (step2) => step2.scope.kind === "release-executor" || step2.scope.kind === "elected-one" ? request.releaseExecutor === true : targetNames.has(step2.scope.target)
|
|
8429
|
+
shouldRun: (step2) => step2.scope.kind === "release-executor" || step2.scope.kind === "elected-one" ? request.releaseExecutor === true : step2.scope.kind === "topology-role" ? targetNames.has(step2.scope.target) && assigned?.topology?.role === step2.scope.role : targetNames.has(step2.scope.target)
|
|
8315
8430
|
});
|
|
8316
8431
|
const phase = {
|
|
8317
8432
|
pipeline: `${plan.name}:deploy`,
|
|
@@ -8751,7 +8866,8 @@ async function pullDeploymentOnce(options) {
|
|
|
8751
8866
|
labels: { ...target.labels },
|
|
8752
8867
|
allocation: { ...target.allocation, resources: { ...target.allocation.resources } },
|
|
8753
8868
|
...target.provisioning ? { provisioning: { ...target.provisioning } } : {},
|
|
8754
|
-
...target.connectivity ? { connectivity: structuredClone(target.connectivity) } : {}
|
|
8869
|
+
...target.connectivity ? { connectivity: structuredClone(target.connectivity) } : {},
|
|
8870
|
+
...target.topology ? { topology: structuredClone(target.topology) } : {}
|
|
8755
8871
|
})) } : {},
|
|
8756
8872
|
...result.capacity ? { capacity: {
|
|
8757
8873
|
recommendedConcurrency: result.capacity.recommendedConcurrency,
|
|
@@ -9215,7 +9331,7 @@ async function writeAndCloseProcessInput(input, value) {
|
|
|
9215
9331
|
}
|
|
9216
9332
|
|
|
9217
9333
|
// src/version.ts
|
|
9218
|
-
var VERSION2 = "0.1.
|
|
9334
|
+
var VERSION2 = "0.1.81";
|
|
9219
9335
|
|
|
9220
9336
|
// src/ssh-bootstrap.ts
|
|
9221
9337
|
class SshBootstrapError extends Error {
|
|
@@ -10092,6 +10208,32 @@ function membersOfLinuxList(value, label) {
|
|
|
10092
10208
|
throw new MetalProvisionError(`${label} list overlaps itself`);
|
|
10093
10209
|
return members;
|
|
10094
10210
|
}
|
|
10211
|
+
function linuxList(members) {
|
|
10212
|
+
const sorted = [...new Set(members)].sort((left, right) => left - right);
|
|
10213
|
+
const ranges = [];
|
|
10214
|
+
for (let index = 0;index < sorted.length; ) {
|
|
10215
|
+
const start = sorted[index];
|
|
10216
|
+
let end = start;
|
|
10217
|
+
while (index + 1 < sorted.length && sorted[index + 1] === end + 1) {
|
|
10218
|
+
index += 1;
|
|
10219
|
+
end = sorted[index];
|
|
10220
|
+
}
|
|
10221
|
+
ranges.push(start === end ? String(start) : `${start}-${end}`);
|
|
10222
|
+
index += 1;
|
|
10223
|
+
}
|
|
10224
|
+
return ranges.join(",");
|
|
10225
|
+
}
|
|
10226
|
+
function physicalCoreGroups(pool) {
|
|
10227
|
+
const cpus = membersOfLinuxList(pool.cpus, "CPU");
|
|
10228
|
+
if (cpus.length % pool.physicalCores !== 0) {
|
|
10229
|
+
throw new MetalProvisionError("CPU pool threads must divide evenly across physical cores");
|
|
10230
|
+
}
|
|
10231
|
+
const threadsPerCore = cpus.length / pool.physicalCores;
|
|
10232
|
+
if (!Number.isInteger(threadsPerCore) || threadsPerCore < 1 || threadsPerCore > 8) {
|
|
10233
|
+
throw new MetalProvisionError("invalid CPU pool threads-per-core topology");
|
|
10234
|
+
}
|
|
10235
|
+
return Array.from({ length: pool.physicalCores }, (_, core) => Array.from({ length: threadsPerCore }, (_2, thread) => cpus[core + thread * pool.physicalCores]));
|
|
10236
|
+
}
|
|
10095
10237
|
var guestNameFor = (computeKey) => `fzg-${createHash7("sha256").update(computeKey).digest("hex").slice(0, 16)}`;
|
|
10096
10238
|
var tapNameFor = (computeKey) => `fzt${createHash7("sha256").update(computeKey).digest("hex").slice(0, 12)}`;
|
|
10097
10239
|
var macForAddress = (address) => {
|
|
@@ -10149,6 +10291,7 @@ function validateMetalProfile(profile) {
|
|
|
10149
10291
|
if (!Number.isInteger(pool.physicalCores) || pool.physicalCores < 1 || pool.physicalCores > cpus.length) {
|
|
10150
10292
|
throw new MetalProvisionError("invalid CPU pool physical-core count");
|
|
10151
10293
|
}
|
|
10294
|
+
physicalCoreGroups(pool);
|
|
10152
10295
|
for (const cpu of cpus) {
|
|
10153
10296
|
if (assigned.has(cpu))
|
|
10154
10297
|
throw new MetalProvisionError("CPU pools overlap");
|
|
@@ -10218,26 +10361,38 @@ function allocateCpuPool(profile, claim, rows) {
|
|
|
10218
10361
|
const prior = rows.find((row2) => row2.computeKey === claim.computeKey);
|
|
10219
10362
|
if (prior) {
|
|
10220
10363
|
const retained = profile.cpuPools.find((pool) => pool.key === prior.cpuPoolKey);
|
|
10221
|
-
|
|
10364
|
+
const groups2 = retained ? physicalCoreGroups(retained) : [];
|
|
10365
|
+
const retainedCpus = new Set(membersOfLinuxList(prior.allowedCpus, "persisted guest CPU"));
|
|
10366
|
+
const matchedGroups = groups2.filter((group) => group.every((cpu) => retainedCpus.has(cpu)));
|
|
10367
|
+
if (!retained || retained.memoryNodes !== prior.allowedMemoryNodes || matchedGroups.length !== claim.spec.physicalCores || matchedGroups.flat().length !== retainedCpus.size || retainedCpus.size < claim.spec.vcpu || claim.spec.cpuPoolKey !== undefined && claim.spec.cpuPoolKey !== retained.key) {
|
|
10222
10368
|
throw new MetalProvisionError("persisted guest CPU pool no longer matches the host profile");
|
|
10223
10369
|
}
|
|
10224
|
-
return
|
|
10225
|
-
|
|
10226
|
-
|
|
10227
|
-
|
|
10228
|
-
|
|
10229
|
-
|
|
10230
|
-
throw new MetalProvisionError("requested CPU pool is unavailable");
|
|
10231
|
-
}
|
|
10232
|
-
if (requested.physicalCores < claim.spec.physicalCores || membersOfLinuxList(requested.cpus, "CPU").length < claim.spec.vcpu)
|
|
10233
|
-
throw new MetalProvisionError("requested CPU pool cannot satisfy this guest");
|
|
10234
|
-
return requested;
|
|
10370
|
+
return {
|
|
10371
|
+
key: retained.key,
|
|
10372
|
+
cpus: prior.allowedCpus,
|
|
10373
|
+
physicalCores: matchedGroups.length,
|
|
10374
|
+
memoryNodes: prior.allowedMemoryNodes
|
|
10375
|
+
};
|
|
10235
10376
|
}
|
|
10236
|
-
const
|
|
10377
|
+
const usedCpus = new Set(rows.flatMap((row2) => membersOfLinuxList(row2.allowedCpus, "persisted guest CPU")));
|
|
10378
|
+
const candidates = profile.cpuPools.filter((pool) => !claim.spec.cpuPoolKey || pool.key === claim.spec.cpuPoolKey).map((pool) => ({
|
|
10379
|
+
pool,
|
|
10380
|
+
available: physicalCoreGroups(pool).filter((group) => group.every((cpu) => !usedCpus.has(cpu)))
|
|
10381
|
+
})).filter(({ available }) => {
|
|
10382
|
+
const selected2 = available.slice(0, claim.spec.physicalCores);
|
|
10383
|
+
return selected2.length === claim.spec.physicalCores && selected2.flat().length >= claim.spec.vcpu;
|
|
10384
|
+
}).sort((left, right) => left.available.length - right.available.length || left.pool.key.localeCompare(right.pool.key));
|
|
10237
10385
|
const selected = candidates[0];
|
|
10238
|
-
if (!selected)
|
|
10239
|
-
throw new MetalProvisionError("no exclusive CPU
|
|
10240
|
-
|
|
10386
|
+
if (!selected) {
|
|
10387
|
+
throw new MetalProvisionError(claim.spec.cpuPoolKey ? "requested CPU pool cannot satisfy this guest" : "no exclusive CPU capacity can satisfy this guest");
|
|
10388
|
+
}
|
|
10389
|
+
const groups = selected.available.slice(0, claim.spec.physicalCores);
|
|
10390
|
+
return {
|
|
10391
|
+
key: selected.pool.key,
|
|
10392
|
+
cpus: linuxList(groups.flat()),
|
|
10393
|
+
physicalCores: groups.length,
|
|
10394
|
+
memoryNodes: selected.pool.memoryNodes
|
|
10395
|
+
};
|
|
10241
10396
|
}
|
|
10242
10397
|
var base64 = (value) => Buffer.from(value).toString("base64");
|
|
10243
10398
|
var yamlFile = (path2, content, permissions) => ` - path: ${JSON.stringify(path2)}
|
|
@@ -12860,6 +13015,7 @@ async function activateContainer(requestValue, host = defaultHost2) {
|
|
|
12860
13015
|
// src/deployment-connectivity.ts
|
|
12861
13016
|
import { createHash as createHash11 } from "crypto";
|
|
12862
13017
|
import { mkdirSync as mkdirSync12, renameSync as renameSync10, writeFileSync as writeFileSync13 } from "fs";
|
|
13018
|
+
import { createConnection as createConnection2 } from "net";
|
|
12863
13019
|
import { dirname as dirname9 } from "path";
|
|
12864
13020
|
var TOKEN2 = /^[A-Za-z0-9._-]{40,16384}$/;
|
|
12865
13021
|
var UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i;
|
|
@@ -12904,6 +13060,21 @@ var defaultHost3 = {
|
|
|
12904
13060
|
const [stdout, stderr, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
|
|
12905
13061
|
return { exitCode, output: `${stdout}${stderr}` };
|
|
12906
13062
|
},
|
|
13063
|
+
probe: (address, port, timeoutMs) => new Promise((resolve6) => {
|
|
13064
|
+
const socket = createConnection2({ host: address, port });
|
|
13065
|
+
let settled = false;
|
|
13066
|
+
const finish = (value) => {
|
|
13067
|
+
if (settled)
|
|
13068
|
+
return;
|
|
13069
|
+
settled = true;
|
|
13070
|
+
socket.destroy();
|
|
13071
|
+
resolve6(value);
|
|
13072
|
+
};
|
|
13073
|
+
socket.setTimeout(timeoutMs);
|
|
13074
|
+
socket.once("connect", () => finish(true));
|
|
13075
|
+
socket.once("timeout", () => finish(false));
|
|
13076
|
+
socket.once("error", () => finish(false));
|
|
13077
|
+
}),
|
|
12907
13078
|
sleep: (ms) => Bun.sleep(ms)
|
|
12908
13079
|
};
|
|
12909
13080
|
function validate2(request) {
|
|
@@ -12918,7 +13089,7 @@ function validate2(request) {
|
|
|
12918
13089
|
} else if (request.capabilities.public)
|
|
12919
13090
|
throw new Error("unexpected public deployment capability");
|
|
12920
13091
|
if (privateIntent?.mode === "cloudflare-warp") {
|
|
12921
|
-
if (!NAME3.test(privateIntent.credential) || privateIntent.network.length < 1 || privateIntent.network.length > 128 || !request.capabilities.private || !UUID.test(request.capabilities.private.connectorId) || !TOKEN2.test(request.capabilities.private.connectorToken)) {
|
|
13092
|
+
if (!NAME3.test(privateIntent.credential) || privateIntent.network.length < 1 || privateIntent.network.length > 128 || !request.capabilities.private || !UUID.test(request.capabilities.private.connectorId) || !UUID.test(request.capabilities.private.siteRouteId) || !TOKEN2.test(request.capabilities.private.connectorToken)) {
|
|
12922
13093
|
throw new Error("private deployment connectivity is malformed");
|
|
12923
13094
|
}
|
|
12924
13095
|
} else if (request.capabilities.private)
|
|
@@ -12928,6 +13099,22 @@ async function applyDeploymentConnectivity(request, host = defaultHost3) {
|
|
|
12928
13099
|
validate2(request);
|
|
12929
13100
|
const id2 = idFor3(request.key);
|
|
12930
13101
|
const evidence = { key: request.key };
|
|
13102
|
+
const topology = request.topology;
|
|
13103
|
+
if (topology) {
|
|
13104
|
+
const values = [topology.localRelayAddress, ...topology.localPeerAddresses, ...topology.remoteRelayAddresses];
|
|
13105
|
+
const identities = [
|
|
13106
|
+
topology.nodeIdentity,
|
|
13107
|
+
...topology.localPeerIdentities,
|
|
13108
|
+
...topology.remoteRelayIdentities,
|
|
13109
|
+
...topology.memberIdentities
|
|
13110
|
+
];
|
|
13111
|
+
if (!/^[A-Za-z0-9.-]{1,253}$/.test(topology.site) || !/^(?:\d{1,3}\.){3}0\/24$/.test(topology.siteCidr) || values.some((value) => !/^(?:\d{1,3}\.){3}\d{1,3}$/.test(value)) || identities.some((value) => !/^[A-Za-z0-9.-]{1,253}$/.test(value)) || topology.localPeerIdentities.length !== topology.localPeerAddresses.length || topology.remoteRelayIdentities.length !== topology.remoteRelayAddresses.length || topology.memberIdentities.length < 1 || topology.memberIdentities.length > 1024 || new Set(topology.memberIdentities).size !== topology.memberIdentities.length || !topology.memberIdentities.includes(topology.nodeIdentity) || !/^sha256:[a-f0-9]{64}$/.test(topology.generation) || (topology.role === "member" ? topology.remoteRelayAddresses.length !== 0 : topology.remoteSiteCidrs.length !== topology.remoteRelayAddresses.length) || topology.remoteSiteCidrs.some((value) => !/^(?:\d{1,3}\.){3}0\/24$/.test(value)) || new Set(topology.remoteSiteCidrs).size !== topology.remoteSiteCidrs.length || !Number.isSafeInteger(topology.healthPort) || topology.healthPort < 1 || topology.healthPort > 65535 || topology.routedTcpPorts.length < 1 || topology.routedTcpPorts.length > 64 || new Set(topology.routedTcpPorts).size !== topology.routedTcpPorts.length || !topology.routedTcpPorts.includes(topology.healthPort) || topology.routedTcpPorts.some((port) => !Number.isSafeInteger(port) || port < 1 || port > 65535) || topology.role === "member" && topology.transport !== "private-lan") {
|
|
13112
|
+
throw new Error("deployment topology is malformed");
|
|
13113
|
+
}
|
|
13114
|
+
if (request.intent.private?.mode === "cloudflare-warp" !== (topology.transport === "cloudflare-warp")) {
|
|
13115
|
+
throw new Error("deployment topology transport differs from private connectivity intent");
|
|
13116
|
+
}
|
|
13117
|
+
}
|
|
12931
13118
|
if (request.intent.public?.mode === "cloudflare-tunnel") {
|
|
12932
13119
|
const capability = request.capabilities.public;
|
|
12933
13120
|
const credentialName = `FZ_TUNNEL_${id2.toUpperCase()}`;
|
|
@@ -12965,6 +13152,12 @@ WantedBy=multi-user.target
|
|
|
12965
13152
|
const capability = request.capabilities.private;
|
|
12966
13153
|
const credentialPath = "/etc/forgezero/creds/CF_WARP_CONNECTOR_TOKEN.cred";
|
|
12967
13154
|
const unit2 = "forgezero-deployment-mesh.service";
|
|
13155
|
+
const forwarding = "/etc/sysctl.d/99-forgezero-mesh.conf";
|
|
13156
|
+
host.write(forwarding, `net.ipv4.ip_forward = 1
|
|
13157
|
+
net.ipv6.conf.all.forwarding = 1
|
|
13158
|
+
net.ipv6.conf.all.accept_ra = 2
|
|
13159
|
+
`, 420);
|
|
13160
|
+
await checked7(host, ["/usr/sbin/sysctl", "-p", forwarding], "Mesh subnet forwarding");
|
|
12968
13161
|
await host.seal("CF_WARP_CONNECTOR_TOKEN", credentialPath, capability.connectorToken);
|
|
12969
13162
|
host.write(`/etc/systemd/system/${unit2}`, `[Unit]
|
|
12970
13163
|
Description=ForgeZero deployment Mesh/WARP connector
|
|
@@ -12999,6 +13192,95 @@ WantedBy=multi-user.target
|
|
|
12999
13192
|
if (!evidence.private)
|
|
13000
13193
|
throw new Error("WARP connector readiness failed");
|
|
13001
13194
|
}
|
|
13195
|
+
if (topology) {
|
|
13196
|
+
const syncAddresses = topology.role === "relay" ? [...topology.localPeerAddresses, ...topology.remoteRelayAddresses] : [topology.localRelayAddress];
|
|
13197
|
+
const topologyEpoch = `topology-${topology.generation.slice("sha256:".length, "sha256:".length + 48)}`;
|
|
13198
|
+
host.write("/etc/forgezero/deployment-topology.env", [
|
|
13199
|
+
`FZ_TOPOLOGY_NODE_IDENTITY=${topology.nodeIdentity}`,
|
|
13200
|
+
`FZ_TOPOLOGY_PEER_ADDRESSES=${[...new Set(syncAddresses)].join(",")}`,
|
|
13201
|
+
`FZ_TOPOLOGY_MEMBERS=${topology.memberIdentities.join(",")}`,
|
|
13202
|
+
`FZ_TOPOLOGY_EPOCH=${topologyEpoch}`,
|
|
13203
|
+
`FZ_TOPOLOGY_SITE=${topology.site}`,
|
|
13204
|
+
`FZ_TOPOLOGY_ROLE=${topology.role}`,
|
|
13205
|
+
`FZ_TOPOLOGY_TRANSPORT=${topology.transport}`,
|
|
13206
|
+
""
|
|
13207
|
+
].join(`
|
|
13208
|
+
`), 384);
|
|
13209
|
+
const routeUnit = "forgezero-deployment-topology.service";
|
|
13210
|
+
const routeUnitPath = `/etc/systemd/system/${routeUnit}`;
|
|
13211
|
+
const routePairs = topology.role === "member" ? topology.remoteSiteCidrs.map((network) => ({ network, via: topology.localRelayAddress })) : topology.transport === "private-lan" ? topology.remoteSiteCidrs.map((network, index) => ({ network, via: topology.remoteRelayAddresses[index] })) : [];
|
|
13212
|
+
for (const { via } of routePairs) {
|
|
13213
|
+
await checked7(host, ["/usr/sbin/ip", "route", "get", via], `private route gateway ${via}`);
|
|
13214
|
+
}
|
|
13215
|
+
await host.exec(["/usr/bin/systemctl", "disable", "--now", routeUnit]);
|
|
13216
|
+
const forwarding = topology.role !== "member" && topology.remoteSiteCidrs.length > 0 ? `ExecStart=/usr/sbin/sysctl -w net.ipv4.ip_forward=1
|
|
13217
|
+
` : "";
|
|
13218
|
+
const starts = routePairs.map(({ network, via }) => `ExecStart=/usr/sbin/ip route replace ${network} via ${via}`).join(`
|
|
13219
|
+
`);
|
|
13220
|
+
const stops = routePairs.map(({ network, via }) => `ExecStop=-/usr/sbin/ip route del ${network} via ${via}`).join(`
|
|
13221
|
+
`);
|
|
13222
|
+
const noOp = !forwarding && !starts ? `ExecStart=/usr/bin/true
|
|
13223
|
+
` : "";
|
|
13224
|
+
host.write(routeUnitPath, `[Unit]
|
|
13225
|
+
Description=ForgeZero fenced deployment site routing
|
|
13226
|
+
After=network-online.target${topology.transport === "cloudflare-warp" ? " forgezero-deployment-mesh.service" : ""}
|
|
13227
|
+
Wants=network-online.target
|
|
13228
|
+
|
|
13229
|
+
[Service]
|
|
13230
|
+
Type=oneshot
|
|
13231
|
+
RemainAfterExit=yes
|
|
13232
|
+
${forwarding}${noOp}${starts}${starts ? `
|
|
13233
|
+
` : ""}${stops}${stops ? `
|
|
13234
|
+
` : ""}
|
|
13235
|
+
[Install]
|
|
13236
|
+
WantedBy=multi-user.target
|
|
13237
|
+
`, 420);
|
|
13238
|
+
for (const source of [...new Set([topology.siteCidr, ...topology.remoteSiteCidrs])]) {
|
|
13239
|
+
for (const port of topology.routedTcpPorts) {
|
|
13240
|
+
await checked7(host, ["/usr/sbin/ufw", "allow", "from", source, "to", "any", "port", String(port), "proto", "tcp"], `private service ${source}:${port}`);
|
|
13241
|
+
}
|
|
13242
|
+
}
|
|
13243
|
+
if (topology.role !== "member")
|
|
13244
|
+
for (const remoteCidr of topology.remoteSiteCidrs) {
|
|
13245
|
+
for (const port of topology.routedTcpPorts) {
|
|
13246
|
+
await checked7(host, [
|
|
13247
|
+
"/usr/sbin/ufw",
|
|
13248
|
+
"route",
|
|
13249
|
+
"allow",
|
|
13250
|
+
"proto",
|
|
13251
|
+
"tcp",
|
|
13252
|
+
"from",
|
|
13253
|
+
topology.siteCidr,
|
|
13254
|
+
"to",
|
|
13255
|
+
remoteCidr,
|
|
13256
|
+
"port",
|
|
13257
|
+
String(port)
|
|
13258
|
+
], `private routed service ${topology.siteCidr}->${remoteCidr}:${port}`);
|
|
13259
|
+
}
|
|
13260
|
+
}
|
|
13261
|
+
await checked7(host, ["/usr/bin/systemctl", "daemon-reload"], "topology daemon reload");
|
|
13262
|
+
await checked7(host, ["/usr/bin/systemctl", "enable", "--now", routeUnit], "topology routes");
|
|
13263
|
+
const probes = topology.role === "member" ? [topology.localRelayAddress] : topology.remoteRelayAddresses;
|
|
13264
|
+
for (const address of [...new Set(probes)]) {
|
|
13265
|
+
await checked7(host, ["/usr/sbin/ip", "route", "get", address], `private route ${address}`);
|
|
13266
|
+
let ready = false;
|
|
13267
|
+
for (let attempt = 0;attempt < 10; attempt += 1) {
|
|
13268
|
+
if (await host.probe(address, topology.healthPort, 2000)) {
|
|
13269
|
+
ready = true;
|
|
13270
|
+
break;
|
|
13271
|
+
}
|
|
13272
|
+
await host.sleep(1000);
|
|
13273
|
+
}
|
|
13274
|
+
if (!ready)
|
|
13275
|
+
throw new Error(`private topology relay ${address}:${topology.healthPort} is unreachable`);
|
|
13276
|
+
}
|
|
13277
|
+
evidence.topology = {
|
|
13278
|
+
site: topology.site,
|
|
13279
|
+
siteCidr: topology.siteCidr,
|
|
13280
|
+
role: topology.role,
|
|
13281
|
+
probed: [...new Set(probes)]
|
|
13282
|
+
};
|
|
13283
|
+
}
|
|
13002
13284
|
return evidence;
|
|
13003
13285
|
}
|
|
13004
13286
|
|
|
@@ -16439,6 +16721,115 @@ async function readBoundedStdin(maxBytes) {
|
|
|
16439
16721
|
throw new Error(`stdin request exceeds ${maxBytes} bytes`);
|
|
16440
16722
|
return Buffer.from(stdout).toString("utf8");
|
|
16441
16723
|
}
|
|
16724
|
+
// src/deployment-topology.ts
|
|
16725
|
+
import { isIP as isIP6 } from "net";
|
|
16726
|
+
|
|
16727
|
+
class DeploymentTopologyError extends Error {
|
|
16728
|
+
constructor(message) {
|
|
16729
|
+
super(message);
|
|
16730
|
+
this.name = "DeploymentTopologyError";
|
|
16731
|
+
}
|
|
16732
|
+
}
|
|
16733
|
+
var SAFE = /^(?=.{1,253}$)[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?$/;
|
|
16734
|
+
var PRIVATE_V4 = (value) => {
|
|
16735
|
+
const parts = value.split(".").map(Number);
|
|
16736
|
+
return parts.length === 4 && parts.every((part) => Number.isInteger(part) && part >= 0 && part <= 255) && (parts[0] === 10 || parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31 || parts[0] === 192 && parts[1] === 168);
|
|
16737
|
+
};
|
|
16738
|
+
var validSiteCidr = (value) => {
|
|
16739
|
+
const match = value.match(/^((?:\d{1,3}\.){3})0\/24$/);
|
|
16740
|
+
return Boolean(match && PRIVATE_V4(`${match[1]}1`));
|
|
16741
|
+
};
|
|
16742
|
+
function planDeploymentTopology(args) {
|
|
16743
|
+
const topology = args.target.topology;
|
|
16744
|
+
if (!topology)
|
|
16745
|
+
throw new DeploymentTopologyError(`target ${args.target.name} has no per-metal topology`);
|
|
16746
|
+
if (!/^[a-z][a-z0-9-]{0,62}$/.test(args.target.name) || !/^[A-Za-z0-9._:-]{1,128}$/.test(args.generation)) {
|
|
16747
|
+
throw new DeploymentTopologyError("topology identity is invalid");
|
|
16748
|
+
}
|
|
16749
|
+
if (args.placements.length < 1 || args.placements.length > 1024)
|
|
16750
|
+
throw new DeploymentTopologyError("topology requires 1 to 1024 placements");
|
|
16751
|
+
const references = new Set;
|
|
16752
|
+
const slots = new Set;
|
|
16753
|
+
for (const placement of args.placements) {
|
|
16754
|
+
if (!SAFE.test(placement.computeReference) || !SAFE.test(placement.nodeIdentity) || !SAFE.test(placement.metalHostname) || isIP6(placement.privateAddress) !== 4 || !PRIVATE_V4(placement.privateAddress) || !validSiteCidr(placement.siteCidr) || !placement.privateAddress.startsWith(placement.siteCidr.slice(0, -4)) || !Number.isSafeInteger(placement.slot) || placement.slot < 0 || placement.slot > 1e6 || references.has(placement.computeReference) || slots.has(placement.slot)) {
|
|
16755
|
+
throw new DeploymentTopologyError("topology placement is malformed or duplicated");
|
|
16756
|
+
}
|
|
16757
|
+
references.add(placement.computeReference);
|
|
16758
|
+
slots.add(placement.slot);
|
|
16759
|
+
}
|
|
16760
|
+
const sites = new Map;
|
|
16761
|
+
for (const placement of args.placements) {
|
|
16762
|
+
const members2 = sites.get(placement.metalHostname) ?? [];
|
|
16763
|
+
members2.push({ ...placement });
|
|
16764
|
+
sites.set(placement.metalHostname, members2);
|
|
16765
|
+
}
|
|
16766
|
+
for (const members2 of sites.values())
|
|
16767
|
+
members2.sort((left, right) => left.slot - right.slot || left.computeReference.localeCompare(right.computeReference));
|
|
16768
|
+
const siteCidrs = new Map;
|
|
16769
|
+
for (const [site, members2] of sites) {
|
|
16770
|
+
const cidrs = new Set(members2.map(({ siteCidr }) => siteCidr));
|
|
16771
|
+
if (cidrs.size !== 1)
|
|
16772
|
+
throw new DeploymentTopologyError(`metal ${site} has inconsistent private CIDRs`);
|
|
16773
|
+
siteCidrs.set(site, members2[0].siteCidr);
|
|
16774
|
+
}
|
|
16775
|
+
if (new Set(siteCidrs.values()).size !== siteCidrs.size) {
|
|
16776
|
+
throw new DeploymentTopologyError("deployment metals require non-overlapping private CIDRs");
|
|
16777
|
+
}
|
|
16778
|
+
const required = 1 + topology.relays.standbyPerMetal;
|
|
16779
|
+
for (const [site, members2] of sites)
|
|
16780
|
+
if (members2.length < required) {
|
|
16781
|
+
throw new DeploymentTopologyError(`metal ${site} cannot satisfy its relay and standby topology`);
|
|
16782
|
+
}
|
|
16783
|
+
const siteNames = [...sites.keys()].sort();
|
|
16784
|
+
let crossTransport;
|
|
16785
|
+
if (siteNames.length <= 2)
|
|
16786
|
+
crossTransport = "private-lan";
|
|
16787
|
+
else {
|
|
16788
|
+
if (topology.crossMetal.mode === "private-lan") {
|
|
16789
|
+
throw new DeploymentTopologyError("three or more metals require the declared Mesh/WARP authority");
|
|
16790
|
+
}
|
|
16791
|
+
crossTransport = "cloudflare-warp";
|
|
16792
|
+
}
|
|
16793
|
+
const active = new Map(siteNames.map((site) => [site, sites.get(site)[0]]));
|
|
16794
|
+
const memberIdentities = [...args.placements].sort((left, right) => left.slot - right.slot || left.computeReference.localeCompare(right.computeReference)).map(({ nodeIdentity }) => nodeIdentity);
|
|
16795
|
+
if (new Set(memberIdentities).size !== memberIdentities.length) {
|
|
16796
|
+
throw new DeploymentTopologyError("deployment node identities must be unique");
|
|
16797
|
+
}
|
|
16798
|
+
if (!Number.isSafeInteger(topology.relays.healthPort) || topology.relays.healthPort < 1 || topology.relays.healthPort > 65535 || !Array.isArray(topology.relays.routedTcpPorts) || topology.relays.routedTcpPorts.length < 1 || topology.relays.routedTcpPorts.length > 64 || new Set(topology.relays.routedTcpPorts).size !== topology.relays.routedTcpPorts.length || !topology.relays.routedTcpPorts.includes(topology.relays.healthPort) || topology.relays.routedTcpPorts.some((port) => !Number.isSafeInteger(port) || port < 1 || port > 65535)) {
|
|
16799
|
+
throw new DeploymentTopologyError("relay health and routed TCP ports are malformed");
|
|
16800
|
+
}
|
|
16801
|
+
return siteNames.flatMap((site) => {
|
|
16802
|
+
const members2 = sites.get(site);
|
|
16803
|
+
const relay = active.get(site);
|
|
16804
|
+
const remoteRelayAddresses = siteNames.filter((other) => other !== site).map((other) => active.get(other).privateAddress);
|
|
16805
|
+
const remoteRelayIdentities = siteNames.filter((other) => other !== site).map((other) => active.get(other).nodeIdentity);
|
|
16806
|
+
const remoteSiteCidrs = siteNames.filter((other) => other !== site).map((other) => siteCidrs.get(other));
|
|
16807
|
+
return members2.map((placement, index) => {
|
|
16808
|
+
const role = index === 0 ? "relay" : index === 1 && topology.relays.standbyPerMetal === 1 ? "standby" : "member";
|
|
16809
|
+
const crossSite = role !== "member" && siteNames.length > 1;
|
|
16810
|
+
const localPeers = role === "relay" ? members2.filter(({ privateAddress }) => privateAddress !== placement.privateAddress) : [relay];
|
|
16811
|
+
const localPeerAddresses = role === "relay" ? localPeers.map(({ privateAddress }) => privateAddress) : [relay.privateAddress];
|
|
16812
|
+
const localPeerIdentities = role === "relay" ? localPeers.map(({ nodeIdentity }) => nodeIdentity) : [relay.nodeIdentity];
|
|
16813
|
+
return {
|
|
16814
|
+
...placement,
|
|
16815
|
+
role,
|
|
16816
|
+
transport: crossSite ? crossTransport : "private-lan",
|
|
16817
|
+
localRelayReference: relay.computeReference,
|
|
16818
|
+
localRelayAddress: relay.privateAddress,
|
|
16819
|
+
localPeerAddresses,
|
|
16820
|
+
localPeerIdentities,
|
|
16821
|
+
remoteRelayAddresses: crossSite ? remoteRelayAddresses : [],
|
|
16822
|
+
remoteRelayIdentities: crossSite ? remoteRelayIdentities : [],
|
|
16823
|
+
remoteSiteCidrs: siteNames.length > 1 ? remoteSiteCidrs : [],
|
|
16824
|
+
healthPort: topology.relays.healthPort,
|
|
16825
|
+
routedTcpPorts: [...topology.relays.routedTcpPorts],
|
|
16826
|
+
memberIdentities,
|
|
16827
|
+
warpRequired: crossSite && crossTransport === "cloudflare-warp",
|
|
16828
|
+
generation: args.generation
|
|
16829
|
+
};
|
|
16830
|
+
});
|
|
16831
|
+
});
|
|
16832
|
+
}
|
|
16442
16833
|
|
|
16443
16834
|
// src/index.ts
|
|
16444
16835
|
function agentCommandArgs(args, command3) {
|
|
@@ -17522,6 +17913,7 @@ export {
|
|
|
17522
17913
|
projectVaultCoordinate,
|
|
17523
17914
|
projectVaultCacheKey,
|
|
17524
17915
|
probeAgentSocket,
|
|
17916
|
+
planDeploymentTopology,
|
|
17525
17917
|
observeSoftwareHost,
|
|
17526
17918
|
observeAgentHost,
|
|
17527
17919
|
normalizeEgressTcpPorts,
|
|
@@ -17577,6 +17969,7 @@ export {
|
|
|
17577
17969
|
OS_CATALOG,
|
|
17578
17970
|
METAL_SYSTEMD_CREDENTIALS,
|
|
17579
17971
|
MAX_AGENT_TARBALL_BYTES,
|
|
17972
|
+
DeploymentTopologyError,
|
|
17580
17973
|
DeploymentInputError,
|
|
17581
17974
|
DeploymentError,
|
|
17582
17975
|
DEFAULT_SOFTWARE_HELPER_SOCKET,
|