@forgezero/agent 0.1.78 → 0.1.80
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 +57 -6
- package/dist/agent-heartbeat.js +1 -1
- package/dist/bootstrap.js +21 -6
- package/dist/community-rehearsal-host.js +29 -7
- package/dist/definition.js +29 -7
- package/dist/deploy-compiler.js +50 -2
- package/dist/deploy-file.js +29 -7
- 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 +397 -21
- package/dist/fz.js +75 -12
- package/dist/index.d.ts +1 -0
- package/dist/metal-bootstrap.js +1 -1
- package/dist/metal-helper-socket.js +29 -7
- package/dist/metal-provision.js +29 -7
- package/dist/operator-bootstrap.js +21 -6
- package/dist/platform-bootstrap-runtime.d.ts +4 -1
- package/dist/platform-bootstrap-runtime.js +47 -11
- package/dist/platform-fleet-verification.js +176 -13
- package/dist/platform-genesis.js +29 -7
- package/dist/provision.js +158 -9
- package/dist/software-helper.js +157 -8
- package/dist/software.d.ts +1 -0
- package/dist/software.js +30 -7
- package/dist/ubuntu.js +29 -7
- package/dist/version.d.ts +1 -1
- package/package.json +5 -1
package/dist/fz-agent.js
CHANGED
|
@@ -5114,16 +5114,38 @@ var runSoftwareCommand = async (argv, env = {}) => {
|
|
|
5114
5114
|
return { exitCode, output: `${stdout}${stderr}` };
|
|
5115
5115
|
};
|
|
5116
5116
|
var run = runSoftwareCommand;
|
|
5117
|
+
var softwareDownloadCommand = (url, destination, maximumBytes) => {
|
|
5118
|
+
const source = new URL(url);
|
|
5119
|
+
if (source.protocol !== "https:" || source.username || source.password)
|
|
5120
|
+
throw new Error("software download must use credential-free HTTPS");
|
|
5121
|
+
if (!Number.isSafeInteger(maximumBytes) || maximumBytes < 1)
|
|
5122
|
+
throw new Error("software download size bound is invalid");
|
|
5123
|
+
return [
|
|
5124
|
+
"/usr/bin/curl",
|
|
5125
|
+
"--fail",
|
|
5126
|
+
"--location",
|
|
5127
|
+
"--silent",
|
|
5128
|
+
"--show-error",
|
|
5129
|
+
"--proto",
|
|
5130
|
+
"=https",
|
|
5131
|
+
"--tlsv1.2",
|
|
5132
|
+
"--max-time",
|
|
5133
|
+
"1800",
|
|
5134
|
+
"--max-filesize",
|
|
5135
|
+
String(maximumBytes),
|
|
5136
|
+
"--output",
|
|
5137
|
+
destination,
|
|
5138
|
+
source.href
|
|
5139
|
+
];
|
|
5140
|
+
};
|
|
5117
5141
|
var download = async (url, destination, sha2562, maximumBytes = 512 * 1024 * 1024) => {
|
|
5118
|
-
const response = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(30 * 60000) });
|
|
5119
|
-
if (!response.ok)
|
|
5120
|
-
throw new Error(`download failed with HTTP ${response.status}`);
|
|
5121
|
-
const declared = Number(response.headers.get("content-length") ?? 0);
|
|
5122
|
-
if (declared > maximumBytes)
|
|
5123
|
-
throw new Error("download exceeds reviewed size bound");
|
|
5124
5142
|
if (existsSync2(destination))
|
|
5125
5143
|
throw new Error("download destination already exists");
|
|
5126
|
-
await
|
|
5144
|
+
const received = await run(softwareDownloadCommand(url, destination, maximumBytes));
|
|
5145
|
+
if (received.exitCode !== 0) {
|
|
5146
|
+
rmSync(destination, { force: true });
|
|
5147
|
+
throw new Error(`software download failed (${received.exitCode}): ${received.output.trim()}`);
|
|
5148
|
+
}
|
|
5127
5149
|
chmodSync2(destination, 384);
|
|
5128
5150
|
if (statSync(destination).size > maximumBytes) {
|
|
5129
5151
|
rmSync(destination, { force: true });
|
|
@@ -6362,7 +6384,7 @@ function reference(value, where) {
|
|
|
6362
6384
|
const row2 = object(value, where);
|
|
6363
6385
|
exact3(row2, ["$ref"], where);
|
|
6364
6386
|
const coordinate = boundedString(row2.$ref, `${where}.$ref`, 256);
|
|
6365
|
-
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)) {
|
|
6366
6388
|
fail(where, "contains an unsupported reference.");
|
|
6367
6389
|
}
|
|
6368
6390
|
return { $ref: coordinate };
|
|
@@ -6794,6 +6816,12 @@ function validateStep(value, where, targets, context) {
|
|
|
6794
6816
|
if (!targets.has(String(scope.target)))
|
|
6795
6817
|
fail(`${where}.scope.target`, "must name an existing target.");
|
|
6796
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.");
|
|
6797
6825
|
} else
|
|
6798
6826
|
fail(`${where}.scope.kind`, "is unsupported.");
|
|
6799
6827
|
if (row2.timeoutMs !== undefined)
|
|
@@ -6948,7 +6976,7 @@ function compileDeployment(sourceValue) {
|
|
|
6948
6976
|
const targetDefinitions = new Map;
|
|
6949
6977
|
for (const [targetName, value] of targetEntries) {
|
|
6950
6978
|
const target = object(value, `deployment.spec.targets.${targetName}`);
|
|
6951
|
-
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}`);
|
|
6952
6980
|
if (target.kind !== "compute")
|
|
6953
6981
|
fail(`deployment.spec.targets.${targetName}.kind`, "must be compute.");
|
|
6954
6982
|
const selector = object(target.selector, `deployment.spec.targets.${targetName}.selector`);
|
|
@@ -7070,6 +7098,45 @@ function compileDeployment(sourceValue) {
|
|
|
7070
7098
|
}
|
|
7071
7099
|
}
|
|
7072
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
|
+
}
|
|
7073
7140
|
if (target.placement !== undefined)
|
|
7074
7141
|
jsonValue(target.placement, `deployment.spec.targets.${targetName}.placement`);
|
|
7075
7142
|
targetDefinitions.set(targetName, target);
|
|
@@ -7093,6 +7160,9 @@ function compileDeployment(sourceValue) {
|
|
|
7093
7160
|
if (target.connectivity?.private?.mode === "cloudflare-warp" && ![...requirementDefinitions.values()].some(({ capability }) => capability === "network.private")) {
|
|
7094
7161
|
fail(`deployment.spec.targets.${targetName}.connectivity.private`, "requires one network.private provider requirement.");
|
|
7095
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
|
+
}
|
|
7096
7166
|
}
|
|
7097
7167
|
const components = object(spec.components, "deployment.spec.components");
|
|
7098
7168
|
const componentEntries = Object.entries(components);
|
|
@@ -7350,6 +7420,19 @@ function resolveDeploymentTargets(plan, supplied = {}) {
|
|
|
7350
7420
|
} : { mode: "disabled" } } : {}
|
|
7351
7421
|
};
|
|
7352
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
|
+
}
|
|
7353
7436
|
return {
|
|
7354
7437
|
name,
|
|
7355
7438
|
profiles: [...target.selector.profiles],
|
|
@@ -7368,7 +7451,8 @@ function resolveDeploymentTargets(plan, supplied = {}) {
|
|
|
7368
7451
|
}
|
|
7369
7452
|
},
|
|
7370
7453
|
...target.provisioning ? { provisioning: { ...target.provisioning } } : {},
|
|
7371
|
-
...connectivity ? { connectivity } : {}
|
|
7454
|
+
...connectivity ? { connectivity } : {},
|
|
7455
|
+
...topology ? { topology } : {}
|
|
7372
7456
|
};
|
|
7373
7457
|
});
|
|
7374
7458
|
}
|
|
@@ -8074,12 +8158,19 @@ function createDeploymentManager(options) {
|
|
|
8074
8158
|
if (!selected || mismatches.length > 0) {
|
|
8075
8159
|
throw new DeploymentError("PIPELINE_FAILED", `the control-plane target assignment does not match this exact deployment plan (${mismatches.join(", ")})`);
|
|
8076
8160
|
}
|
|
8077
|
-
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;
|
|
8078
8169
|
const expectedPublic = selected.connectivity?.public;
|
|
8079
8170
|
const assignedPrivate = assigned.connectivity?.private;
|
|
8080
8171
|
const assignedPublic = assigned.connectivity?.public;
|
|
8081
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;
|
|
8082
|
-
if (JSON.stringify(assignedPrivate) !== JSON.stringify(expectedPrivate) || !publicMatches) {
|
|
8173
|
+
if (!topologyMatches || JSON.stringify(assignedPrivate) !== JSON.stringify(expectedPrivate) || !publicMatches) {
|
|
8083
8174
|
throw new DeploymentError("PIPELINE_FAILED", "the control-plane connectivity assignment does not match this target slot");
|
|
8084
8175
|
}
|
|
8085
8176
|
targetNames = new Set([assigned.targetName]);
|
|
@@ -8096,7 +8187,24 @@ function createDeploymentManager(options) {
|
|
|
8096
8187
|
FZ_TARGET_CPU_CORES: String(assigned.resources.cpuCores),
|
|
8097
8188
|
FZ_TARGET_MEMORY_MIB: String(assigned.resources.memoryMiB),
|
|
8098
8189
|
FZ_TARGET_STORAGE_GIB: String(assigned.resources.storageGiB),
|
|
8099
|
-
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
|
+
} : {}
|
|
8100
8208
|
});
|
|
8101
8209
|
for (const targetName of targetNames) {
|
|
8102
8210
|
const capacity2 = resolvedTargets.find(({ name }) => name === targetName)?.allocation.resources;
|
|
@@ -8152,11 +8260,15 @@ function createDeploymentManager(options) {
|
|
|
8152
8260
|
return { changed: true };
|
|
8153
8261
|
},
|
|
8154
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
|
+
}
|
|
8155
8266
|
if (!assigned || typeof resolved.target !== "string" || resolved.target !== assigned.targetName) {
|
|
8156
8267
|
throw new DeploymentActionError("refused", "network.ensure must target this exact control-plane assignment");
|
|
8157
8268
|
}
|
|
8158
|
-
if (!assigned.connectivity)
|
|
8159
|
-
return { changed: false, evidence: { target: assigned.targetName } };
|
|
8269
|
+
if (!assigned.connectivity) {
|
|
8270
|
+
return { changed: false, evidence: { target: assigned.targetName, authority: "none" } };
|
|
8271
|
+
}
|
|
8160
8272
|
if (!options.ensureSoftware || !options.applyConnectivity) {
|
|
8161
8273
|
throw new DeploymentActionError("provider-unavailable", "the supervised connectivity helper is unavailable");
|
|
8162
8274
|
}
|
|
@@ -8165,12 +8277,31 @@ function createDeploymentManager(options) {
|
|
|
8165
8277
|
requirements.push({ id: "cloudflared", version: "latest" });
|
|
8166
8278
|
if (assigned.connectivity.private?.mode === "cloudflare-warp")
|
|
8167
8279
|
requirements.push({ id: "cloudflare-warp", version: "latest" });
|
|
8280
|
+
if (assigned.topology)
|
|
8281
|
+
requirements.push({ id: "ufw", version: "latest" });
|
|
8168
8282
|
if (requirements.length > 0)
|
|
8169
8283
|
await options.ensureSoftware(requirements);
|
|
8170
8284
|
const evidence = await options.applyConnectivity({
|
|
8171
8285
|
key: `${options.key}:${assigned.targetName}:${assigned.slot}`,
|
|
8172
8286
|
intent: assigned.connectivity,
|
|
8173
|
-
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
|
+
} } : {}
|
|
8174
8305
|
});
|
|
8175
8306
|
return { changed: true, evidence: structuredClone(evidence) };
|
|
8176
8307
|
},
|
|
@@ -8279,7 +8410,13 @@ function createDeploymentManager(options) {
|
|
|
8279
8410
|
runtime: assigned.execution,
|
|
8280
8411
|
cpuCores: assigned.resources.cpuCores,
|
|
8281
8412
|
memoryMiB: assigned.resources.memoryMiB,
|
|
8282
|
-
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
|
+
} : {}
|
|
8283
8420
|
} : {
|
|
8284
8421
|
targetName: matchingTargetNames[0],
|
|
8285
8422
|
slot: 0,
|
|
@@ -8289,7 +8426,7 @@ function createDeploymentManager(options) {
|
|
|
8289
8426
|
memoryMiB: resolvedTargets[0].allocation.resources.memoryMiB,
|
|
8290
8427
|
storageGiB: resolvedTargets[0].allocation.resources.storageGiB
|
|
8291
8428
|
},
|
|
8292
|
-
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)
|
|
8293
8430
|
});
|
|
8294
8431
|
const phase = {
|
|
8295
8432
|
pipeline: `${plan.name}:deploy`,
|
|
@@ -8729,7 +8866,8 @@ async function pullDeploymentOnce(options) {
|
|
|
8729
8866
|
labels: { ...target.labels },
|
|
8730
8867
|
allocation: { ...target.allocation, resources: { ...target.allocation.resources } },
|
|
8731
8868
|
...target.provisioning ? { provisioning: { ...target.provisioning } } : {},
|
|
8732
|
-
...target.connectivity ? { connectivity: structuredClone(target.connectivity) } : {}
|
|
8869
|
+
...target.connectivity ? { connectivity: structuredClone(target.connectivity) } : {},
|
|
8870
|
+
...target.topology ? { topology: structuredClone(target.topology) } : {}
|
|
8733
8871
|
})) } : {},
|
|
8734
8872
|
...result.capacity ? { capacity: {
|
|
8735
8873
|
recommendedConcurrency: result.capacity.recommendedConcurrency,
|
|
@@ -9193,7 +9331,7 @@ async function writeAndCloseProcessInput(input, value) {
|
|
|
9193
9331
|
}
|
|
9194
9332
|
|
|
9195
9333
|
// src/version.ts
|
|
9196
|
-
var VERSION2 = "0.1.
|
|
9334
|
+
var VERSION2 = "0.1.80";
|
|
9197
9335
|
|
|
9198
9336
|
// src/ssh-bootstrap.ts
|
|
9199
9337
|
class SshBootstrapError extends Error {
|
|
@@ -12838,6 +12976,7 @@ async function activateContainer(requestValue, host = defaultHost2) {
|
|
|
12838
12976
|
// src/deployment-connectivity.ts
|
|
12839
12977
|
import { createHash as createHash11 } from "crypto";
|
|
12840
12978
|
import { mkdirSync as mkdirSync12, renameSync as renameSync10, writeFileSync as writeFileSync13 } from "fs";
|
|
12979
|
+
import { createConnection as createConnection2 } from "net";
|
|
12841
12980
|
import { dirname as dirname9 } from "path";
|
|
12842
12981
|
var TOKEN2 = /^[A-Za-z0-9._-]{40,16384}$/;
|
|
12843
12982
|
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;
|
|
@@ -12882,6 +13021,21 @@ var defaultHost3 = {
|
|
|
12882
13021
|
const [stdout, stderr, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
|
|
12883
13022
|
return { exitCode, output: `${stdout}${stderr}` };
|
|
12884
13023
|
},
|
|
13024
|
+
probe: (address, port, timeoutMs) => new Promise((resolve6) => {
|
|
13025
|
+
const socket = createConnection2({ host: address, port });
|
|
13026
|
+
let settled = false;
|
|
13027
|
+
const finish = (value) => {
|
|
13028
|
+
if (settled)
|
|
13029
|
+
return;
|
|
13030
|
+
settled = true;
|
|
13031
|
+
socket.destroy();
|
|
13032
|
+
resolve6(value);
|
|
13033
|
+
};
|
|
13034
|
+
socket.setTimeout(timeoutMs);
|
|
13035
|
+
socket.once("connect", () => finish(true));
|
|
13036
|
+
socket.once("timeout", () => finish(false));
|
|
13037
|
+
socket.once("error", () => finish(false));
|
|
13038
|
+
}),
|
|
12885
13039
|
sleep: (ms) => Bun.sleep(ms)
|
|
12886
13040
|
};
|
|
12887
13041
|
function validate2(request) {
|
|
@@ -12896,7 +13050,7 @@ function validate2(request) {
|
|
|
12896
13050
|
} else if (request.capabilities.public)
|
|
12897
13051
|
throw new Error("unexpected public deployment capability");
|
|
12898
13052
|
if (privateIntent?.mode === "cloudflare-warp") {
|
|
12899
|
-
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)) {
|
|
13053
|
+
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)) {
|
|
12900
13054
|
throw new Error("private deployment connectivity is malformed");
|
|
12901
13055
|
}
|
|
12902
13056
|
} else if (request.capabilities.private)
|
|
@@ -12906,6 +13060,22 @@ async function applyDeploymentConnectivity(request, host = defaultHost3) {
|
|
|
12906
13060
|
validate2(request);
|
|
12907
13061
|
const id2 = idFor3(request.key);
|
|
12908
13062
|
const evidence = { key: request.key };
|
|
13063
|
+
const topology = request.topology;
|
|
13064
|
+
if (topology) {
|
|
13065
|
+
const values = [topology.localRelayAddress, ...topology.localPeerAddresses, ...topology.remoteRelayAddresses];
|
|
13066
|
+
const identities = [
|
|
13067
|
+
topology.nodeIdentity,
|
|
13068
|
+
...topology.localPeerIdentities,
|
|
13069
|
+
...topology.remoteRelayIdentities,
|
|
13070
|
+
...topology.memberIdentities
|
|
13071
|
+
];
|
|
13072
|
+
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") {
|
|
13073
|
+
throw new Error("deployment topology is malformed");
|
|
13074
|
+
}
|
|
13075
|
+
if (request.intent.private?.mode === "cloudflare-warp" !== (topology.transport === "cloudflare-warp")) {
|
|
13076
|
+
throw new Error("deployment topology transport differs from private connectivity intent");
|
|
13077
|
+
}
|
|
13078
|
+
}
|
|
12909
13079
|
if (request.intent.public?.mode === "cloudflare-tunnel") {
|
|
12910
13080
|
const capability = request.capabilities.public;
|
|
12911
13081
|
const credentialName = `FZ_TUNNEL_${id2.toUpperCase()}`;
|
|
@@ -12943,6 +13113,12 @@ WantedBy=multi-user.target
|
|
|
12943
13113
|
const capability = request.capabilities.private;
|
|
12944
13114
|
const credentialPath = "/etc/forgezero/creds/CF_WARP_CONNECTOR_TOKEN.cred";
|
|
12945
13115
|
const unit2 = "forgezero-deployment-mesh.service";
|
|
13116
|
+
const forwarding = "/etc/sysctl.d/99-forgezero-mesh.conf";
|
|
13117
|
+
host.write(forwarding, `net.ipv4.ip_forward = 1
|
|
13118
|
+
net.ipv6.conf.all.forwarding = 1
|
|
13119
|
+
net.ipv6.conf.all.accept_ra = 2
|
|
13120
|
+
`, 420);
|
|
13121
|
+
await checked7(host, ["/usr/sbin/sysctl", "-p", forwarding], "Mesh subnet forwarding");
|
|
12946
13122
|
await host.seal("CF_WARP_CONNECTOR_TOKEN", credentialPath, capability.connectorToken);
|
|
12947
13123
|
host.write(`/etc/systemd/system/${unit2}`, `[Unit]
|
|
12948
13124
|
Description=ForgeZero deployment Mesh/WARP connector
|
|
@@ -12977,6 +13153,95 @@ WantedBy=multi-user.target
|
|
|
12977
13153
|
if (!evidence.private)
|
|
12978
13154
|
throw new Error("WARP connector readiness failed");
|
|
12979
13155
|
}
|
|
13156
|
+
if (topology) {
|
|
13157
|
+
const syncAddresses = topology.role === "relay" ? [...topology.localPeerAddresses, ...topology.remoteRelayAddresses] : [topology.localRelayAddress];
|
|
13158
|
+
const topologyEpoch = `topology-${topology.generation.slice("sha256:".length, "sha256:".length + 48)}`;
|
|
13159
|
+
host.write("/etc/forgezero/deployment-topology.env", [
|
|
13160
|
+
`FZ_TOPOLOGY_NODE_IDENTITY=${topology.nodeIdentity}`,
|
|
13161
|
+
`FZ_TOPOLOGY_PEER_ADDRESSES=${[...new Set(syncAddresses)].join(",")}`,
|
|
13162
|
+
`FZ_TOPOLOGY_MEMBERS=${topology.memberIdentities.join(",")}`,
|
|
13163
|
+
`FZ_TOPOLOGY_EPOCH=${topologyEpoch}`,
|
|
13164
|
+
`FZ_TOPOLOGY_SITE=${topology.site}`,
|
|
13165
|
+
`FZ_TOPOLOGY_ROLE=${topology.role}`,
|
|
13166
|
+
`FZ_TOPOLOGY_TRANSPORT=${topology.transport}`,
|
|
13167
|
+
""
|
|
13168
|
+
].join(`
|
|
13169
|
+
`), 384);
|
|
13170
|
+
const routeUnit = "forgezero-deployment-topology.service";
|
|
13171
|
+
const routeUnitPath = `/etc/systemd/system/${routeUnit}`;
|
|
13172
|
+
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] })) : [];
|
|
13173
|
+
for (const { via } of routePairs) {
|
|
13174
|
+
await checked7(host, ["/usr/sbin/ip", "route", "get", via], `private route gateway ${via}`);
|
|
13175
|
+
}
|
|
13176
|
+
await host.exec(["/usr/bin/systemctl", "disable", "--now", routeUnit]);
|
|
13177
|
+
const forwarding = topology.role !== "member" && topology.remoteSiteCidrs.length > 0 ? `ExecStart=/usr/sbin/sysctl -w net.ipv4.ip_forward=1
|
|
13178
|
+
` : "";
|
|
13179
|
+
const starts = routePairs.map(({ network, via }) => `ExecStart=/usr/sbin/ip route replace ${network} via ${via}`).join(`
|
|
13180
|
+
`);
|
|
13181
|
+
const stops = routePairs.map(({ network, via }) => `ExecStop=-/usr/sbin/ip route del ${network} via ${via}`).join(`
|
|
13182
|
+
`);
|
|
13183
|
+
const noOp = !forwarding && !starts ? `ExecStart=/usr/bin/true
|
|
13184
|
+
` : "";
|
|
13185
|
+
host.write(routeUnitPath, `[Unit]
|
|
13186
|
+
Description=ForgeZero fenced deployment site routing
|
|
13187
|
+
After=network-online.target${topology.transport === "cloudflare-warp" ? " forgezero-deployment-mesh.service" : ""}
|
|
13188
|
+
Wants=network-online.target
|
|
13189
|
+
|
|
13190
|
+
[Service]
|
|
13191
|
+
Type=oneshot
|
|
13192
|
+
RemainAfterExit=yes
|
|
13193
|
+
${forwarding}${noOp}${starts}${starts ? `
|
|
13194
|
+
` : ""}${stops}${stops ? `
|
|
13195
|
+
` : ""}
|
|
13196
|
+
[Install]
|
|
13197
|
+
WantedBy=multi-user.target
|
|
13198
|
+
`, 420);
|
|
13199
|
+
for (const source of [...new Set([topology.siteCidr, ...topology.remoteSiteCidrs])]) {
|
|
13200
|
+
for (const port of topology.routedTcpPorts) {
|
|
13201
|
+
await checked7(host, ["/usr/sbin/ufw", "allow", "from", source, "to", "any", "port", String(port), "proto", "tcp"], `private service ${source}:${port}`);
|
|
13202
|
+
}
|
|
13203
|
+
}
|
|
13204
|
+
if (topology.role !== "member")
|
|
13205
|
+
for (const remoteCidr of topology.remoteSiteCidrs) {
|
|
13206
|
+
for (const port of topology.routedTcpPorts) {
|
|
13207
|
+
await checked7(host, [
|
|
13208
|
+
"/usr/sbin/ufw",
|
|
13209
|
+
"route",
|
|
13210
|
+
"allow",
|
|
13211
|
+
"proto",
|
|
13212
|
+
"tcp",
|
|
13213
|
+
"from",
|
|
13214
|
+
topology.siteCidr,
|
|
13215
|
+
"to",
|
|
13216
|
+
remoteCidr,
|
|
13217
|
+
"port",
|
|
13218
|
+
String(port)
|
|
13219
|
+
], `private routed service ${topology.siteCidr}->${remoteCidr}:${port}`);
|
|
13220
|
+
}
|
|
13221
|
+
}
|
|
13222
|
+
await checked7(host, ["/usr/bin/systemctl", "daemon-reload"], "topology daemon reload");
|
|
13223
|
+
await checked7(host, ["/usr/bin/systemctl", "enable", "--now", routeUnit], "topology routes");
|
|
13224
|
+
const probes = topology.role === "member" ? [topology.localRelayAddress] : topology.remoteRelayAddresses;
|
|
13225
|
+
for (const address of [...new Set(probes)]) {
|
|
13226
|
+
await checked7(host, ["/usr/sbin/ip", "route", "get", address], `private route ${address}`);
|
|
13227
|
+
let ready = false;
|
|
13228
|
+
for (let attempt = 0;attempt < 10; attempt += 1) {
|
|
13229
|
+
if (await host.probe(address, topology.healthPort, 2000)) {
|
|
13230
|
+
ready = true;
|
|
13231
|
+
break;
|
|
13232
|
+
}
|
|
13233
|
+
await host.sleep(1000);
|
|
13234
|
+
}
|
|
13235
|
+
if (!ready)
|
|
13236
|
+
throw new Error(`private topology relay ${address}:${topology.healthPort} is unreachable`);
|
|
13237
|
+
}
|
|
13238
|
+
evidence.topology = {
|
|
13239
|
+
site: topology.site,
|
|
13240
|
+
siteCidr: topology.siteCidr,
|
|
13241
|
+
role: topology.role,
|
|
13242
|
+
probed: [...new Set(probes)]
|
|
13243
|
+
};
|
|
13244
|
+
}
|
|
12980
13245
|
return evidence;
|
|
12981
13246
|
}
|
|
12982
13247
|
|
|
@@ -16417,6 +16682,115 @@ async function readBoundedStdin(maxBytes) {
|
|
|
16417
16682
|
throw new Error(`stdin request exceeds ${maxBytes} bytes`);
|
|
16418
16683
|
return Buffer.from(stdout).toString("utf8");
|
|
16419
16684
|
}
|
|
16685
|
+
// src/deployment-topology.ts
|
|
16686
|
+
import { isIP as isIP6 } from "net";
|
|
16687
|
+
|
|
16688
|
+
class DeploymentTopologyError extends Error {
|
|
16689
|
+
constructor(message) {
|
|
16690
|
+
super(message);
|
|
16691
|
+
this.name = "DeploymentTopologyError";
|
|
16692
|
+
}
|
|
16693
|
+
}
|
|
16694
|
+
var SAFE = /^(?=.{1,253}$)[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?$/;
|
|
16695
|
+
var PRIVATE_V4 = (value) => {
|
|
16696
|
+
const parts = value.split(".").map(Number);
|
|
16697
|
+
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);
|
|
16698
|
+
};
|
|
16699
|
+
var validSiteCidr = (value) => {
|
|
16700
|
+
const match = value.match(/^((?:\d{1,3}\.){3})0\/24$/);
|
|
16701
|
+
return Boolean(match && PRIVATE_V4(`${match[1]}1`));
|
|
16702
|
+
};
|
|
16703
|
+
function planDeploymentTopology(args) {
|
|
16704
|
+
const topology = args.target.topology;
|
|
16705
|
+
if (!topology)
|
|
16706
|
+
throw new DeploymentTopologyError(`target ${args.target.name} has no per-metal topology`);
|
|
16707
|
+
if (!/^[a-z][a-z0-9-]{0,62}$/.test(args.target.name) || !/^[A-Za-z0-9._:-]{1,128}$/.test(args.generation)) {
|
|
16708
|
+
throw new DeploymentTopologyError("topology identity is invalid");
|
|
16709
|
+
}
|
|
16710
|
+
if (args.placements.length < 1 || args.placements.length > 1024)
|
|
16711
|
+
throw new DeploymentTopologyError("topology requires 1 to 1024 placements");
|
|
16712
|
+
const references = new Set;
|
|
16713
|
+
const slots = new Set;
|
|
16714
|
+
for (const placement of args.placements) {
|
|
16715
|
+
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)) {
|
|
16716
|
+
throw new DeploymentTopologyError("topology placement is malformed or duplicated");
|
|
16717
|
+
}
|
|
16718
|
+
references.add(placement.computeReference);
|
|
16719
|
+
slots.add(placement.slot);
|
|
16720
|
+
}
|
|
16721
|
+
const sites = new Map;
|
|
16722
|
+
for (const placement of args.placements) {
|
|
16723
|
+
const members2 = sites.get(placement.metalHostname) ?? [];
|
|
16724
|
+
members2.push({ ...placement });
|
|
16725
|
+
sites.set(placement.metalHostname, members2);
|
|
16726
|
+
}
|
|
16727
|
+
for (const members2 of sites.values())
|
|
16728
|
+
members2.sort((left, right) => left.slot - right.slot || left.computeReference.localeCompare(right.computeReference));
|
|
16729
|
+
const siteCidrs = new Map;
|
|
16730
|
+
for (const [site, members2] of sites) {
|
|
16731
|
+
const cidrs = new Set(members2.map(({ siteCidr }) => siteCidr));
|
|
16732
|
+
if (cidrs.size !== 1)
|
|
16733
|
+
throw new DeploymentTopologyError(`metal ${site} has inconsistent private CIDRs`);
|
|
16734
|
+
siteCidrs.set(site, members2[0].siteCidr);
|
|
16735
|
+
}
|
|
16736
|
+
if (new Set(siteCidrs.values()).size !== siteCidrs.size) {
|
|
16737
|
+
throw new DeploymentTopologyError("deployment metals require non-overlapping private CIDRs");
|
|
16738
|
+
}
|
|
16739
|
+
const required = 1 + topology.relays.standbyPerMetal;
|
|
16740
|
+
for (const [site, members2] of sites)
|
|
16741
|
+
if (members2.length < required) {
|
|
16742
|
+
throw new DeploymentTopologyError(`metal ${site} cannot satisfy its relay and standby topology`);
|
|
16743
|
+
}
|
|
16744
|
+
const siteNames = [...sites.keys()].sort();
|
|
16745
|
+
let crossTransport;
|
|
16746
|
+
if (siteNames.length <= 2)
|
|
16747
|
+
crossTransport = "private-lan";
|
|
16748
|
+
else {
|
|
16749
|
+
if (topology.crossMetal.mode === "private-lan") {
|
|
16750
|
+
throw new DeploymentTopologyError("three or more metals require the declared Mesh/WARP authority");
|
|
16751
|
+
}
|
|
16752
|
+
crossTransport = "cloudflare-warp";
|
|
16753
|
+
}
|
|
16754
|
+
const active = new Map(siteNames.map((site) => [site, sites.get(site)[0]]));
|
|
16755
|
+
const memberIdentities = [...args.placements].sort((left, right) => left.slot - right.slot || left.computeReference.localeCompare(right.computeReference)).map(({ nodeIdentity }) => nodeIdentity);
|
|
16756
|
+
if (new Set(memberIdentities).size !== memberIdentities.length) {
|
|
16757
|
+
throw new DeploymentTopologyError("deployment node identities must be unique");
|
|
16758
|
+
}
|
|
16759
|
+
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)) {
|
|
16760
|
+
throw new DeploymentTopologyError("relay health and routed TCP ports are malformed");
|
|
16761
|
+
}
|
|
16762
|
+
return siteNames.flatMap((site) => {
|
|
16763
|
+
const members2 = sites.get(site);
|
|
16764
|
+
const relay = active.get(site);
|
|
16765
|
+
const remoteRelayAddresses = siteNames.filter((other) => other !== site).map((other) => active.get(other).privateAddress);
|
|
16766
|
+
const remoteRelayIdentities = siteNames.filter((other) => other !== site).map((other) => active.get(other).nodeIdentity);
|
|
16767
|
+
const remoteSiteCidrs = siteNames.filter((other) => other !== site).map((other) => siteCidrs.get(other));
|
|
16768
|
+
return members2.map((placement, index) => {
|
|
16769
|
+
const role = index === 0 ? "relay" : index === 1 && topology.relays.standbyPerMetal === 1 ? "standby" : "member";
|
|
16770
|
+
const crossSite = role !== "member" && siteNames.length > 1;
|
|
16771
|
+
const localPeers = role === "relay" ? members2.filter(({ privateAddress }) => privateAddress !== placement.privateAddress) : [relay];
|
|
16772
|
+
const localPeerAddresses = role === "relay" ? localPeers.map(({ privateAddress }) => privateAddress) : [relay.privateAddress];
|
|
16773
|
+
const localPeerIdentities = role === "relay" ? localPeers.map(({ nodeIdentity }) => nodeIdentity) : [relay.nodeIdentity];
|
|
16774
|
+
return {
|
|
16775
|
+
...placement,
|
|
16776
|
+
role,
|
|
16777
|
+
transport: crossSite ? crossTransport : "private-lan",
|
|
16778
|
+
localRelayReference: relay.computeReference,
|
|
16779
|
+
localRelayAddress: relay.privateAddress,
|
|
16780
|
+
localPeerAddresses,
|
|
16781
|
+
localPeerIdentities,
|
|
16782
|
+
remoteRelayAddresses: crossSite ? remoteRelayAddresses : [],
|
|
16783
|
+
remoteRelayIdentities: crossSite ? remoteRelayIdentities : [],
|
|
16784
|
+
remoteSiteCidrs: siteNames.length > 1 ? remoteSiteCidrs : [],
|
|
16785
|
+
healthPort: topology.relays.healthPort,
|
|
16786
|
+
routedTcpPorts: [...topology.relays.routedTcpPorts],
|
|
16787
|
+
memberIdentities,
|
|
16788
|
+
warpRequired: crossSite && crossTransport === "cloudflare-warp",
|
|
16789
|
+
generation: args.generation
|
|
16790
|
+
};
|
|
16791
|
+
});
|
|
16792
|
+
});
|
|
16793
|
+
}
|
|
16420
16794
|
|
|
16421
16795
|
// src/index.ts
|
|
16422
16796
|
function agentCommandArgs(args, command3) {
|
|
@@ -17500,6 +17874,7 @@ export {
|
|
|
17500
17874
|
projectVaultCoordinate,
|
|
17501
17875
|
projectVaultCacheKey,
|
|
17502
17876
|
probeAgentSocket,
|
|
17877
|
+
planDeploymentTopology,
|
|
17503
17878
|
observeSoftwareHost,
|
|
17504
17879
|
observeAgentHost,
|
|
17505
17880
|
normalizeEgressTcpPorts,
|
|
@@ -17555,6 +17930,7 @@ export {
|
|
|
17555
17930
|
OS_CATALOG,
|
|
17556
17931
|
METAL_SYSTEMD_CREDENTIALS,
|
|
17557
17932
|
MAX_AGENT_TARBALL_BYTES,
|
|
17933
|
+
DeploymentTopologyError,
|
|
17558
17934
|
DeploymentInputError,
|
|
17559
17935
|
DeploymentError,
|
|
17560
17936
|
DEFAULT_SOFTWARE_HELPER_SOCKET,
|