@forgezero/agent 0.1.79 → 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/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 +368 -14
- package/dist/fz.js +75 -12
- package/dist/index.d.ts +1 -0
- package/dist/metal-bootstrap.js +1 -1
- package/dist/operator-bootstrap.js +21 -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/README.md
CHANGED
|
@@ -64,7 +64,8 @@ Every row links to the detailed explanation and named-import/example area below.
|
|
|
64
64
|
| @forgezero/agent/definition | Legacy version 3 JSON deployment compatibility contract. | Bun/Node host | [Reference + usage](#forgezero-agent-definition) |
|
|
65
65
|
| @forgezero/agent/deploy | Typed deployment authoring builders, built-in provider requirements and versioned action constructors. | Bun/Node host | [Reference + usage](#forgezero-agent-deploy) |
|
|
66
66
|
| @forgezero/agent/deployment-targets | Bounded input resolution and stable compute-target assignment for typed plans; emits requirements and slots rather than physical hostnames. | Bun/Node host | [Reference + usage](#forgezero-agent-deployment-targets) |
|
|
67
|
-
| @forgezero/agent/deployment-connectivity | Root-owned
|
|
67
|
+
| @forgezero/agent/deployment-connectivity | Root-owned convergence of exact private routes, routed-port firewall rules and per-target Cloudflare Tunnel or Mesh connector capabilities without exposing the project management token to a compute. | Bun/Node host | [Reference + usage](#forgezero-agent-deployment-connectivity) |
|
|
68
|
+
| @forgezero/agent/deployment-topology | Pure per-Metal relay election and hierarchical peer assignment: one or two Metals stay direct-private, while three or more grant Mesh/WARP only to relay and standby. | Bun/Node host | [Reference + usage](#forgezero-agent-deployment-topology) |
|
|
68
69
|
| @forgezero/agent/deploy-plan | Runtime validation, canonicalization and semantic digest for inert deployment plans. | Bun/Node host | [Reference + usage](#forgezero-agent-deploy-plan) |
|
|
69
70
|
| @forgezero/agent/deploy-plan-runner | Conditional DAG execution with bounded retry, compensation and failure policy over injected action handlers. | Bun/Node host | [Reference + usage](#forgezero-agent-deploy-plan-runner) |
|
|
70
71
|
| @forgezero/agent/deploy-compiler | Explicit developer/CI compiler from forgezero.deploy.ts to canonical .fz/deploy.plan.json; never used to execute project TypeScript in production. | Bun/Node host | [Reference + usage](#forgezero-agent-deploy-compiler) |
|
|
@@ -1067,7 +1068,7 @@ export const selectedCapability = DEPLOY_SCHEMA_URL;
|
|
|
1067
1068
|
<a id="forgezero-agent-deploy"></a>
|
|
1068
1069
|
## @forgezero/agent/deploy
|
|
1069
1070
|
|
|
1070
|
-
Typed deployment authoring builders, built-in provider requirements and versioned action constructors. This entry exposes 14 named value exports and
|
|
1071
|
+
Typed deployment authoring builders, built-in provider requirements and versioned action constructors. This entry exposes 14 named value exports and 42 named type exports. The generated import block lists one name per line for scanning and copying; keep only the names used by your file.
|
|
1071
1072
|
|
|
1072
1073
|
```text
|
|
1073
1074
|
import {
|
|
@@ -1096,6 +1097,7 @@ import type {
|
|
|
1096
1097
|
ComponentDefinition,
|
|
1097
1098
|
ComputeConnectivityDefinition,
|
|
1098
1099
|
ComputePoolDefinition,
|
|
1100
|
+
ComputeTopologyDefinition,
|
|
1099
1101
|
Condition,
|
|
1100
1102
|
ConditionOperand,
|
|
1101
1103
|
ContainerdProviderConfig,
|
|
@@ -1134,7 +1136,7 @@ import type {
|
|
|
1134
1136
|
|
|
1135
1137
|
## @forgezero/agent/deploy — Author a typed, placed containerd deployment
|
|
1136
1138
|
|
|
1137
|
-
TypeScript is authoring only. The checked-in canonical plan is what Agents validate and execute. Targets declare bounded resource/cardinality intent, not hostnames. The API reserves eligible project capacity or provisions only under the reviewed plan and spend ceiling, assigns stable slots, and the Agent runs only its exact assignment. runc containers must declare hard CPU, memory and PID limits, bounded storage, loopback ingress and a health-gated rollout; select Kata QEMU SNP when the workload needs host isolation.
|
|
1139
|
+
TypeScript is authoring only. The checked-in canonical plan is what Agents validate and execute. Targets declare bounded resource/cardinality intent, not hostnames. The API reserves eligible project capacity or provisions only under the reviewed plan and spend ceiling, assigns stable slots, and the Agent runs only its exact assignment. An optional per-metal topology elects one active relay and one standby without naming computes: one or two Metals use proven private relay links and never install WARP, while three or more give Mesh/WARP authority only to relay and standby. runc containers must declare hard CPU, memory and PID limits, bounded storage, loopback ingress and a health-gated rollout; select Kata QEMU SNP when the workload needs host isolation.
|
|
1138
1140
|
|
|
1139
1141
|
```text
|
|
1140
1142
|
import {
|
|
@@ -1174,7 +1176,23 @@ export default defineDeployment({
|
|
|
1174
1176
|
connectivity: {
|
|
1175
1177
|
private: { mode: 'private-lan' },
|
|
1176
1178
|
public: { mode: 'cloudflare-tunnel', credential: 'cloudflare', zone: 'example.com', hostname: { mode: 'indexed', prefix: 'api' } }
|
|
1177
|
-
}
|
|
1179
|
+
},
|
|
1180
|
+
topology: {
|
|
1181
|
+
groupBy: 'metal',
|
|
1182
|
+
relays: {
|
|
1183
|
+
activePerMetal: 1,
|
|
1184
|
+
standbyPerMetal: 1,
|
|
1185
|
+
healthPort: 3000,
|
|
1186
|
+
routedTcpPorts: [3000]
|
|
1187
|
+
},
|
|
1188
|
+
crossMetal: {
|
|
1189
|
+
mode: 'auto',
|
|
1190
|
+
directPrivateMaximumMetals: 2,
|
|
1191
|
+
credential: 'cloudflare',
|
|
1192
|
+
network: 'orders-api'
|
|
1193
|
+
}
|
|
1194
|
+
},
|
|
1195
|
+
placement: { spreadBy: ['metal'], antiAffinity: ['orders-api'] }
|
|
1178
1196
|
}) },
|
|
1179
1197
|
requirements: {
|
|
1180
1198
|
containerd: providers.containerd.require(), nginx: providers.nginx.require(),
|
|
@@ -1237,7 +1255,7 @@ export const selectedCapability = DeploymentInputError;
|
|
|
1237
1255
|
<a id="forgezero-agent-deployment-connectivity"></a>
|
|
1238
1256
|
## @forgezero/agent/deployment-connectivity
|
|
1239
1257
|
|
|
1240
|
-
Root-owned
|
|
1258
|
+
Root-owned convergence of exact private routes, routed-port firewall rules and per-target Cloudflare Tunnel or Mesh connector capabilities without exposing the project management token to a compute. This entry exposes 1 named value export and 6 named type exports. The generated import block lists one name per line for scanning and copying; keep only the names used by your file.
|
|
1241
1259
|
|
|
1242
1260
|
```text
|
|
1243
1261
|
import {
|
|
@@ -1250,6 +1268,7 @@ import type {
|
|
|
1250
1268
|
DeploymentConnectivityHost,
|
|
1251
1269
|
DeploymentConnectivityIntent,
|
|
1252
1270
|
DeploymentConnectivityRequest,
|
|
1271
|
+
DeploymentConnectivityTopology,
|
|
1253
1272
|
} from '@forgezero/agent/deployment-connectivity';
|
|
1254
1273
|
```
|
|
1255
1274
|
|
|
@@ -1265,6 +1284,37 @@ import {
|
|
|
1265
1284
|
export const selectedCapability = applyDeploymentConnectivity;
|
|
1266
1285
|
```
|
|
1267
1286
|
|
|
1287
|
+
<a id="forgezero-agent-deployment-topology"></a>
|
|
1288
|
+
## @forgezero/agent/deployment-topology
|
|
1289
|
+
|
|
1290
|
+
Pure per-Metal relay election and hierarchical peer assignment: one or two Metals stay direct-private, while three or more grant Mesh/WARP only to relay and standby. This entry exposes 2 named value exports and 4 named type exports. The generated import block lists one name per line for scanning and copying; keep only the names used by your file.
|
|
1291
|
+
|
|
1292
|
+
```text
|
|
1293
|
+
import {
|
|
1294
|
+
DeploymentTopologyError,
|
|
1295
|
+
planDeploymentTopology,
|
|
1296
|
+
} from '@forgezero/agent/deployment-topology';
|
|
1297
|
+
|
|
1298
|
+
import type {
|
|
1299
|
+
DeploymentTopologyAssignment,
|
|
1300
|
+
DeploymentTopologyPlacement,
|
|
1301
|
+
DeploymentTopologyRole,
|
|
1302
|
+
DeploymentTopologyTransport,
|
|
1303
|
+
} from '@forgezero/agent/deployment-topology';
|
|
1304
|
+
```
|
|
1305
|
+
|
|
1306
|
+
## @forgezero/agent/deployment-topology — Use this entry point
|
|
1307
|
+
|
|
1308
|
+
This minimal executable use imports one concrete value from this exact entry point without a wildcard or package-root detour. Keep the value or values the application actually needs; the full named inventory remains directly above it.
|
|
1309
|
+
|
|
1310
|
+
```text
|
|
1311
|
+
import {
|
|
1312
|
+
DeploymentTopologyError,
|
|
1313
|
+
} from '@forgezero/agent/deployment-topology';
|
|
1314
|
+
|
|
1315
|
+
export const selectedCapability = DeploymentTopologyError;
|
|
1316
|
+
```
|
|
1317
|
+
|
|
1268
1318
|
<a id="forgezero-agent-deploy-plan"></a>
|
|
1269
1319
|
## @forgezero/agent/deploy-plan
|
|
1270
1320
|
|
|
@@ -1645,7 +1695,7 @@ if (!validate(deploymentDefinition)) throw new Error(JSON.stringify(validate.err
|
|
|
1645
1695
|
<a id="forgezero-agent-software"></a>
|
|
1646
1696
|
## @forgezero/agent/software
|
|
1647
1697
|
|
|
1648
|
-
Exact managed-software inventory, requirement and evidence contracts. This entry exposes
|
|
1698
|
+
Exact managed-software inventory, requirement and evidence contracts. This entry exposes 14 named value exports and 10 named type exports. The generated import block lists one name per line for scanning and copying; keep only the names used by your file.
|
|
1649
1699
|
|
|
1650
1700
|
```text
|
|
1651
1701
|
import {
|
|
@@ -1660,6 +1710,7 @@ import {
|
|
|
1660
1710
|
executeSoftwareOperation,
|
|
1661
1711
|
observeSoftwareHost,
|
|
1662
1712
|
runSoftwareCommand,
|
|
1713
|
+
softwareDownloadCommand,
|
|
1663
1714
|
softwareSpawnFailure,
|
|
1664
1715
|
validateSoftwareRequirements,
|
|
1665
1716
|
} from '@forgezero/agent/software';
|
package/dist/agent-heartbeat.js
CHANGED
package/dist/bootstrap.js
CHANGED
|
@@ -1416,7 +1416,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
|
|
|
1416
1416
|
var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
|
|
1417
1417
|
|
|
1418
1418
|
// src/version.ts
|
|
1419
|
-
var VERSION = "0.1.
|
|
1419
|
+
var VERSION = "0.1.80";
|
|
1420
1420
|
|
|
1421
1421
|
// src/software.ts
|
|
1422
1422
|
var PINNED_BUN_VERSION = "1.3.14";
|
|
@@ -3089,7 +3089,14 @@ function validatePlatformSharedEnvironment(input) {
|
|
|
3089
3089
|
throw new Error("Bootstrap email provider must be smtp or jetemail.");
|
|
3090
3090
|
}
|
|
3091
3091
|
boundedInteger("publicApiPort", input.publicApiPort, 1024, 65533);
|
|
3092
|
-
|
|
3092
|
+
if (!Array.isArray(input.seedSyncMembers) || input.seedSyncMembers.length < 3 || input.seedSyncMembers.length > 64 || new Set(input.seedSyncMembers).size !== input.seedSyncMembers.length) {
|
|
3093
|
+
throw new Error("seedSyncMembers must contain 3 to 64 unique physical host identities.");
|
|
3094
|
+
}
|
|
3095
|
+
for (const member of input.seedSyncMembers)
|
|
3096
|
+
safeAtom("seedSyncMembers item", member);
|
|
3097
|
+
if (!input.seedSyncMembers.includes(input.nodeHostname)) {
|
|
3098
|
+
throw new Error("seedSyncMembers must include this nodeHostname.");
|
|
3099
|
+
}
|
|
3093
3100
|
boundedInteger("concurrencyLimit", input.concurrencyLimit, 1, 1e6);
|
|
3094
3101
|
boundedInteger("drainDeadlineMs", input.drainDeadlineMs, 1000, 300000);
|
|
3095
3102
|
boundedInteger("otlpFlushIntervalMs", input.otlpFlushIntervalMs, 1000, 300000);
|
|
@@ -3169,7 +3176,7 @@ function renderPlatformSharedEnvironment(input) {
|
|
|
3169
3176
|
FZ_DB_MASTER: value.databaseMaster ?? "",
|
|
3170
3177
|
FZ_DB_NETWORK_MODE: value.databaseNetworkMode,
|
|
3171
3178
|
FZ_SEED_SYNC_PEERS: value.seedSyncPeers.join(","),
|
|
3172
|
-
FZ_SEED_SYNC_MEMBERS:
|
|
3179
|
+
FZ_SEED_SYNC_MEMBERS: value.seedSyncMembers.join(","),
|
|
3173
3180
|
FZ_SEED_SYNC_EPOCH: value.seedSyncEpoch,
|
|
3174
3181
|
FZ_SEED_SYNC_CREDENTIAL: "seed-sync-root",
|
|
3175
3182
|
FZ_SHARED_DIR: value.sharedDirectory,
|
|
@@ -3238,7 +3245,12 @@ function platformApiCredentialSpecs(options) {
|
|
|
3238
3245
|
];
|
|
3239
3246
|
}
|
|
3240
3247
|
function renderPlatformApiUnits(input) {
|
|
3241
|
-
for (const path of [
|
|
3248
|
+
for (const path of [
|
|
3249
|
+
input.sharedDirectory,
|
|
3250
|
+
input.sharedEnvironmentFile,
|
|
3251
|
+
input.slotsDirectory,
|
|
3252
|
+
...input.topologyEnvironmentFile ? [input.topologyEnvironmentFile] : []
|
|
3253
|
+
]) {
|
|
3242
3254
|
if (!path.startsWith("/") || /[\r\n]/.test(path))
|
|
3243
3255
|
throw new Error("Runtime paths must be absolute and single-line.");
|
|
3244
3256
|
}
|
|
@@ -3252,6 +3264,8 @@ function renderPlatformApiUnits(input) {
|
|
|
3252
3264
|
const credentials = input.credentials.map((credential) => `LoadCredentialEncrypted=${credential.name}:${credential.encryptedPath}`).join(`
|
|
3253
3265
|
`);
|
|
3254
3266
|
const capacityEnvironment = input.capacityEnvironmentFile ? `EnvironmentFile=-${input.capacityEnvironmentFile}
|
|
3267
|
+
` : "";
|
|
3268
|
+
const topologyEnvironment = input.topologyEnvironmentFile ? `EnvironmentFile=-${input.topologyEnvironmentFile}
|
|
3255
3269
|
` : "";
|
|
3256
3270
|
const template = `[Unit]
|
|
3257
3271
|
Description=ForgeZero (%i slot)
|
|
@@ -3265,7 +3279,7 @@ WorkingDirectory=${input.slotsDirectory}/%i
|
|
|
3265
3279
|
Environment=NODE_ENV=production
|
|
3266
3280
|
Environment=FZ_SLOT=%i
|
|
3267
3281
|
EnvironmentFile=${input.sharedEnvironmentFile}
|
|
3268
|
-
${capacityEnvironment}${credentials}
|
|
3282
|
+
${capacityEnvironment}${topologyEnvironment}${credentials}
|
|
3269
3283
|
ExecStart=/usr/local/bin/bun run ${input.slotsDirectory}/%i/src/index.ts
|
|
3270
3284
|
Restart=always
|
|
3271
3285
|
RestartSec=2
|
|
@@ -4517,7 +4531,8 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4517
4531
|
greenPort: runtime.greenPort,
|
|
4518
4532
|
collectorUnit: runtime.environment.otlpCollectorUnit,
|
|
4519
4533
|
credentials,
|
|
4520
|
-
capacityEnvironmentFile: "/etc/forgezero/capacity.env"
|
|
4534
|
+
capacityEnvironmentFile: "/etc/forgezero/capacity.env",
|
|
4535
|
+
topologyEnvironmentFile: "/etc/forgezero/deployment-topology.env"
|
|
4521
4536
|
});
|
|
4522
4537
|
const edge = renderPlatformNginx({
|
|
4523
4538
|
publicPort: runtime.environment.publicApiPort,
|
package/dist/deploy-compiler.js
CHANGED
|
@@ -203,7 +203,7 @@ function reference(value, where) {
|
|
|
203
203
|
const row2 = object(value, where);
|
|
204
204
|
exact3(row2, ["$ref"], where);
|
|
205
205
|
const coordinate = boundedString(row2.$ref, `${where}.$ref`, 256);
|
|
206
|
-
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)) {
|
|
206
|
+
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)) {
|
|
207
207
|
fail(where, "contains an unsupported reference.");
|
|
208
208
|
}
|
|
209
209
|
return { $ref: coordinate };
|
|
@@ -635,6 +635,12 @@ function validateStep(value, where, targets, context) {
|
|
|
635
635
|
if (!targets.has(String(scope.target)))
|
|
636
636
|
fail(`${where}.scope.target`, "must name an existing target.");
|
|
637
637
|
integer(scope.size, `${where}.scope.size`, 1, 32);
|
|
638
|
+
} else if (scope.kind === "topology-role") {
|
|
639
|
+
exact3(scope, ["kind", "target", "role"], `${where}.scope`);
|
|
640
|
+
if (!targets.has(String(scope.target)))
|
|
641
|
+
fail(`${where}.scope.target`, "must name an existing target.");
|
|
642
|
+
if (!["relay", "standby", "member"].includes(String(scope.role)))
|
|
643
|
+
fail(`${where}.scope.role`, "is unsupported.");
|
|
638
644
|
} else
|
|
639
645
|
fail(`${where}.scope.kind`, "is unsupported.");
|
|
640
646
|
if (row2.timeoutMs !== undefined)
|
|
@@ -789,7 +795,7 @@ function compileDeployment(sourceValue) {
|
|
|
789
795
|
const targetDefinitions = new Map;
|
|
790
796
|
for (const [targetName, value] of targetEntries) {
|
|
791
797
|
const target = object(value, `deployment.spec.targets.${targetName}`);
|
|
792
|
-
exact3(target, ["kind", "selector", "cardinality", "allocation", "provisioning", "connectivity", "placement"], `deployment.spec.targets.${targetName}`);
|
|
798
|
+
exact3(target, ["kind", "selector", "cardinality", "allocation", "provisioning", "connectivity", "topology", "placement"], `deployment.spec.targets.${targetName}`);
|
|
793
799
|
if (target.kind !== "compute")
|
|
794
800
|
fail(`deployment.spec.targets.${targetName}.kind`, "must be compute.");
|
|
795
801
|
const selector = object(target.selector, `deployment.spec.targets.${targetName}.selector`);
|
|
@@ -911,6 +917,45 @@ function compileDeployment(sourceValue) {
|
|
|
911
917
|
}
|
|
912
918
|
}
|
|
913
919
|
}
|
|
920
|
+
if (target.topology !== undefined) {
|
|
921
|
+
const topology = object(target.topology, `deployment.spec.targets.${targetName}.topology`);
|
|
922
|
+
exact3(topology, ["groupBy", "relays", "crossMetal"], `deployment.spec.targets.${targetName}.topology`);
|
|
923
|
+
if (topology.groupBy !== "metal")
|
|
924
|
+
fail(`deployment.spec.targets.${targetName}.topology.groupBy`, "must be metal.");
|
|
925
|
+
const relays = object(topology.relays, `deployment.spec.targets.${targetName}.topology.relays`);
|
|
926
|
+
exact3(relays, ["activePerMetal", "standbyPerMetal", "healthPort", "routedTcpPorts"], `deployment.spec.targets.${targetName}.topology.relays`);
|
|
927
|
+
if (relays.activePerMetal !== 1 || ![0, 1].includes(Number(relays.standbyPerMetal)))
|
|
928
|
+
fail(`deployment.spec.targets.${targetName}.topology.relays`, "requires one active relay and zero or one standby per metal.");
|
|
929
|
+
integer(relays.healthPort, `deployment.spec.targets.${targetName}.topology.relays.healthPort`, 1, 65535);
|
|
930
|
+
if (!Array.isArray(relays.routedTcpPorts) || relays.routedTcpPorts.length < 1 || relays.routedTcpPorts.length > 64 || new Set(relays.routedTcpPorts).size !== relays.routedTcpPorts.length) {
|
|
931
|
+
fail(`deployment.spec.targets.${targetName}.topology.relays.routedTcpPorts`, "must contain 1 to 64 unique ports.");
|
|
932
|
+
}
|
|
933
|
+
for (const [index, port] of relays.routedTcpPorts.entries()) {
|
|
934
|
+
integer(port, `deployment.spec.targets.${targetName}.topology.relays.routedTcpPorts.${index}`, 1, 65535);
|
|
935
|
+
}
|
|
936
|
+
if (!relays.routedTcpPorts.includes(relays.healthPort)) {
|
|
937
|
+
fail(`deployment.spec.targets.${targetName}.topology.relays`, "routedTcpPorts must include healthPort.");
|
|
938
|
+
}
|
|
939
|
+
const crossMetal = object(topology.crossMetal, `deployment.spec.targets.${targetName}.topology.crossMetal`);
|
|
940
|
+
if (crossMetal.mode === "private-lan")
|
|
941
|
+
exact3(crossMetal, ["mode"], `deployment.spec.targets.${targetName}.topology.crossMetal`);
|
|
942
|
+
else if (crossMetal.mode === "cloudflare-warp" || crossMetal.mode === "auto") {
|
|
943
|
+
exact3(crossMetal, crossMetal.mode === "auto" ? ["mode", "directPrivateMaximumMetals", "credential", "network"] : ["mode", "credential", "network"], `deployment.spec.targets.${targetName}.topology.crossMetal`);
|
|
944
|
+
if (crossMetal.mode === "auto" && crossMetal.directPrivateMaximumMetals !== 2)
|
|
945
|
+
fail(`deployment.spec.targets.${targetName}.topology.crossMetal.directPrivateMaximumMetals`, "must be 2.");
|
|
946
|
+
if (!credentials[named(crossMetal.credential, `deployment.spec.targets.${targetName}.topology.crossMetal.credential`)])
|
|
947
|
+
fail(`deployment.spec.targets.${targetName}.topology.crossMetal.credential`, "must name a declared credential.");
|
|
948
|
+
if (typeof crossMetal.network === "string")
|
|
949
|
+
named(crossMetal.network, `deployment.spec.targets.${targetName}.topology.crossMetal.network`);
|
|
950
|
+
else
|
|
951
|
+
reference(crossMetal.network, `deployment.spec.targets.${targetName}.topology.crossMetal.network`);
|
|
952
|
+
} else
|
|
953
|
+
fail(`deployment.spec.targets.${targetName}.topology.crossMetal.mode`, "is unsupported.");
|
|
954
|
+
if (allocation.sharing !== "exclusive")
|
|
955
|
+
fail(`deployment.spec.targets.${targetName}.allocation.sharing`, "must be exclusive for a per-metal relay topology.");
|
|
956
|
+
if (minimum < 1 + Number(relays.standbyPerMetal))
|
|
957
|
+
fail(`deployment.spec.targets.${targetName}.cardinality.minimum`, "must allow the declared relay and standby.");
|
|
958
|
+
}
|
|
914
959
|
if (target.placement !== undefined)
|
|
915
960
|
jsonValue(target.placement, `deployment.spec.targets.${targetName}.placement`);
|
|
916
961
|
targetDefinitions.set(targetName, target);
|
|
@@ -934,6 +979,9 @@ function compileDeployment(sourceValue) {
|
|
|
934
979
|
if (target.connectivity?.private?.mode === "cloudflare-warp" && ![...requirementDefinitions.values()].some(({ capability }) => capability === "network.private")) {
|
|
935
980
|
fail(`deployment.spec.targets.${targetName}.connectivity.private`, "requires one network.private provider requirement.");
|
|
936
981
|
}
|
|
982
|
+
if (target.topology && target.topology.crossMetal.mode !== "private-lan" && ![...requirementDefinitions.values()].some(({ capability }) => capability === "network.private")) {
|
|
983
|
+
fail(`deployment.spec.targets.${targetName}.topology.crossMetal`, "requires one network.private provider requirement.");
|
|
984
|
+
}
|
|
937
985
|
}
|
|
938
986
|
const components = object(spec.components, "deployment.spec.components");
|
|
939
987
|
const componentEntries = Object.entries(components);
|
|
@@ -203,7 +203,7 @@ function reference(value, where) {
|
|
|
203
203
|
const row2 = object(value, where);
|
|
204
204
|
exact3(row2, ["$ref"], where);
|
|
205
205
|
const coordinate = boundedString(row2.$ref, `${where}.$ref`, 256);
|
|
206
|
-
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)) {
|
|
206
|
+
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)) {
|
|
207
207
|
fail(where, "contains an unsupported reference.");
|
|
208
208
|
}
|
|
209
209
|
return { $ref: coordinate };
|
|
@@ -635,6 +635,12 @@ function validateStep(value, where, targets, context) {
|
|
|
635
635
|
if (!targets.has(String(scope.target)))
|
|
636
636
|
fail(`${where}.scope.target`, "must name an existing target.");
|
|
637
637
|
integer(scope.size, `${where}.scope.size`, 1, 32);
|
|
638
|
+
} else if (scope.kind === "topology-role") {
|
|
639
|
+
exact3(scope, ["kind", "target", "role"], `${where}.scope`);
|
|
640
|
+
if (!targets.has(String(scope.target)))
|
|
641
|
+
fail(`${where}.scope.target`, "must name an existing target.");
|
|
642
|
+
if (!["relay", "standby", "member"].includes(String(scope.role)))
|
|
643
|
+
fail(`${where}.scope.role`, "is unsupported.");
|
|
638
644
|
} else
|
|
639
645
|
fail(`${where}.scope.kind`, "is unsupported.");
|
|
640
646
|
if (row2.timeoutMs !== undefined)
|
|
@@ -789,7 +795,7 @@ function compileDeployment(sourceValue) {
|
|
|
789
795
|
const targetDefinitions = new Map;
|
|
790
796
|
for (const [targetName, value] of targetEntries) {
|
|
791
797
|
const target = object(value, `deployment.spec.targets.${targetName}`);
|
|
792
|
-
exact3(target, ["kind", "selector", "cardinality", "allocation", "provisioning", "connectivity", "placement"], `deployment.spec.targets.${targetName}`);
|
|
798
|
+
exact3(target, ["kind", "selector", "cardinality", "allocation", "provisioning", "connectivity", "topology", "placement"], `deployment.spec.targets.${targetName}`);
|
|
793
799
|
if (target.kind !== "compute")
|
|
794
800
|
fail(`deployment.spec.targets.${targetName}.kind`, "must be compute.");
|
|
795
801
|
const selector = object(target.selector, `deployment.spec.targets.${targetName}.selector`);
|
|
@@ -911,6 +917,45 @@ function compileDeployment(sourceValue) {
|
|
|
911
917
|
}
|
|
912
918
|
}
|
|
913
919
|
}
|
|
920
|
+
if (target.topology !== undefined) {
|
|
921
|
+
const topology = object(target.topology, `deployment.spec.targets.${targetName}.topology`);
|
|
922
|
+
exact3(topology, ["groupBy", "relays", "crossMetal"], `deployment.spec.targets.${targetName}.topology`);
|
|
923
|
+
if (topology.groupBy !== "metal")
|
|
924
|
+
fail(`deployment.spec.targets.${targetName}.topology.groupBy`, "must be metal.");
|
|
925
|
+
const relays = object(topology.relays, `deployment.spec.targets.${targetName}.topology.relays`);
|
|
926
|
+
exact3(relays, ["activePerMetal", "standbyPerMetal", "healthPort", "routedTcpPorts"], `deployment.spec.targets.${targetName}.topology.relays`);
|
|
927
|
+
if (relays.activePerMetal !== 1 || ![0, 1].includes(Number(relays.standbyPerMetal)))
|
|
928
|
+
fail(`deployment.spec.targets.${targetName}.topology.relays`, "requires one active relay and zero or one standby per metal.");
|
|
929
|
+
integer(relays.healthPort, `deployment.spec.targets.${targetName}.topology.relays.healthPort`, 1, 65535);
|
|
930
|
+
if (!Array.isArray(relays.routedTcpPorts) || relays.routedTcpPorts.length < 1 || relays.routedTcpPorts.length > 64 || new Set(relays.routedTcpPorts).size !== relays.routedTcpPorts.length) {
|
|
931
|
+
fail(`deployment.spec.targets.${targetName}.topology.relays.routedTcpPorts`, "must contain 1 to 64 unique ports.");
|
|
932
|
+
}
|
|
933
|
+
for (const [index, port] of relays.routedTcpPorts.entries()) {
|
|
934
|
+
integer(port, `deployment.spec.targets.${targetName}.topology.relays.routedTcpPorts.${index}`, 1, 65535);
|
|
935
|
+
}
|
|
936
|
+
if (!relays.routedTcpPorts.includes(relays.healthPort)) {
|
|
937
|
+
fail(`deployment.spec.targets.${targetName}.topology.relays`, "routedTcpPorts must include healthPort.");
|
|
938
|
+
}
|
|
939
|
+
const crossMetal = object(topology.crossMetal, `deployment.spec.targets.${targetName}.topology.crossMetal`);
|
|
940
|
+
if (crossMetal.mode === "private-lan")
|
|
941
|
+
exact3(crossMetal, ["mode"], `deployment.spec.targets.${targetName}.topology.crossMetal`);
|
|
942
|
+
else if (crossMetal.mode === "cloudflare-warp" || crossMetal.mode === "auto") {
|
|
943
|
+
exact3(crossMetal, crossMetal.mode === "auto" ? ["mode", "directPrivateMaximumMetals", "credential", "network"] : ["mode", "credential", "network"], `deployment.spec.targets.${targetName}.topology.crossMetal`);
|
|
944
|
+
if (crossMetal.mode === "auto" && crossMetal.directPrivateMaximumMetals !== 2)
|
|
945
|
+
fail(`deployment.spec.targets.${targetName}.topology.crossMetal.directPrivateMaximumMetals`, "must be 2.");
|
|
946
|
+
if (!credentials[named(crossMetal.credential, `deployment.spec.targets.${targetName}.topology.crossMetal.credential`)])
|
|
947
|
+
fail(`deployment.spec.targets.${targetName}.topology.crossMetal.credential`, "must name a declared credential.");
|
|
948
|
+
if (typeof crossMetal.network === "string")
|
|
949
|
+
named(crossMetal.network, `deployment.spec.targets.${targetName}.topology.crossMetal.network`);
|
|
950
|
+
else
|
|
951
|
+
reference(crossMetal.network, `deployment.spec.targets.${targetName}.topology.crossMetal.network`);
|
|
952
|
+
} else
|
|
953
|
+
fail(`deployment.spec.targets.${targetName}.topology.crossMetal.mode`, "is unsupported.");
|
|
954
|
+
if (allocation.sharing !== "exclusive")
|
|
955
|
+
fail(`deployment.spec.targets.${targetName}.allocation.sharing`, "must be exclusive for a per-metal relay topology.");
|
|
956
|
+
if (minimum < 1 + Number(relays.standbyPerMetal))
|
|
957
|
+
fail(`deployment.spec.targets.${targetName}.cardinality.minimum`, "must allow the declared relay and standby.");
|
|
958
|
+
}
|
|
914
959
|
if (target.placement !== undefined)
|
|
915
960
|
jsonValue(target.placement, `deployment.spec.targets.${targetName}.placement`);
|
|
916
961
|
targetDefinitions.set(targetName, target);
|
|
@@ -934,6 +979,9 @@ function compileDeployment(sourceValue) {
|
|
|
934
979
|
if (target.connectivity?.private?.mode === "cloudflare-warp" && ![...requirementDefinitions.values()].some(({ capability }) => capability === "network.private")) {
|
|
935
980
|
fail(`deployment.spec.targets.${targetName}.connectivity.private`, "requires one network.private provider requirement.");
|
|
936
981
|
}
|
|
982
|
+
if (target.topology && target.topology.crossMetal.mode !== "private-lan" && ![...requirementDefinitions.values()].some(({ capability }) => capability === "network.private")) {
|
|
983
|
+
fail(`deployment.spec.targets.${targetName}.topology.crossMetal`, "requires one network.private provider requirement.");
|
|
984
|
+
}
|
|
937
985
|
}
|
|
938
986
|
const components = object(spec.components, "deployment.spec.components");
|
|
939
987
|
const componentEntries = Object.entries(components);
|
package/dist/deploy-plan.js
CHANGED
|
@@ -203,7 +203,7 @@ function reference(value, where) {
|
|
|
203
203
|
const row2 = object(value, where);
|
|
204
204
|
exact3(row2, ["$ref"], where);
|
|
205
205
|
const coordinate = boundedString(row2.$ref, `${where}.$ref`, 256);
|
|
206
|
-
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)) {
|
|
206
|
+
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)) {
|
|
207
207
|
fail(where, "contains an unsupported reference.");
|
|
208
208
|
}
|
|
209
209
|
return { $ref: coordinate };
|
|
@@ -635,6 +635,12 @@ function validateStep(value, where, targets, context) {
|
|
|
635
635
|
if (!targets.has(String(scope.target)))
|
|
636
636
|
fail(`${where}.scope.target`, "must name an existing target.");
|
|
637
637
|
integer(scope.size, `${where}.scope.size`, 1, 32);
|
|
638
|
+
} else if (scope.kind === "topology-role") {
|
|
639
|
+
exact3(scope, ["kind", "target", "role"], `${where}.scope`);
|
|
640
|
+
if (!targets.has(String(scope.target)))
|
|
641
|
+
fail(`${where}.scope.target`, "must name an existing target.");
|
|
642
|
+
if (!["relay", "standby", "member"].includes(String(scope.role)))
|
|
643
|
+
fail(`${where}.scope.role`, "is unsupported.");
|
|
638
644
|
} else
|
|
639
645
|
fail(`${where}.scope.kind`, "is unsupported.");
|
|
640
646
|
if (row2.timeoutMs !== undefined)
|
|
@@ -789,7 +795,7 @@ function compileDeployment(sourceValue) {
|
|
|
789
795
|
const targetDefinitions = new Map;
|
|
790
796
|
for (const [targetName, value] of targetEntries) {
|
|
791
797
|
const target = object(value, `deployment.spec.targets.${targetName}`);
|
|
792
|
-
exact3(target, ["kind", "selector", "cardinality", "allocation", "provisioning", "connectivity", "placement"], `deployment.spec.targets.${targetName}`);
|
|
798
|
+
exact3(target, ["kind", "selector", "cardinality", "allocation", "provisioning", "connectivity", "topology", "placement"], `deployment.spec.targets.${targetName}`);
|
|
793
799
|
if (target.kind !== "compute")
|
|
794
800
|
fail(`deployment.spec.targets.${targetName}.kind`, "must be compute.");
|
|
795
801
|
const selector = object(target.selector, `deployment.spec.targets.${targetName}.selector`);
|
|
@@ -911,6 +917,45 @@ function compileDeployment(sourceValue) {
|
|
|
911
917
|
}
|
|
912
918
|
}
|
|
913
919
|
}
|
|
920
|
+
if (target.topology !== undefined) {
|
|
921
|
+
const topology = object(target.topology, `deployment.spec.targets.${targetName}.topology`);
|
|
922
|
+
exact3(topology, ["groupBy", "relays", "crossMetal"], `deployment.spec.targets.${targetName}.topology`);
|
|
923
|
+
if (topology.groupBy !== "metal")
|
|
924
|
+
fail(`deployment.spec.targets.${targetName}.topology.groupBy`, "must be metal.");
|
|
925
|
+
const relays = object(topology.relays, `deployment.spec.targets.${targetName}.topology.relays`);
|
|
926
|
+
exact3(relays, ["activePerMetal", "standbyPerMetal", "healthPort", "routedTcpPorts"], `deployment.spec.targets.${targetName}.topology.relays`);
|
|
927
|
+
if (relays.activePerMetal !== 1 || ![0, 1].includes(Number(relays.standbyPerMetal)))
|
|
928
|
+
fail(`deployment.spec.targets.${targetName}.topology.relays`, "requires one active relay and zero or one standby per metal.");
|
|
929
|
+
integer(relays.healthPort, `deployment.spec.targets.${targetName}.topology.relays.healthPort`, 1, 65535);
|
|
930
|
+
if (!Array.isArray(relays.routedTcpPorts) || relays.routedTcpPorts.length < 1 || relays.routedTcpPorts.length > 64 || new Set(relays.routedTcpPorts).size !== relays.routedTcpPorts.length) {
|
|
931
|
+
fail(`deployment.spec.targets.${targetName}.topology.relays.routedTcpPorts`, "must contain 1 to 64 unique ports.");
|
|
932
|
+
}
|
|
933
|
+
for (const [index, port] of relays.routedTcpPorts.entries()) {
|
|
934
|
+
integer(port, `deployment.spec.targets.${targetName}.topology.relays.routedTcpPorts.${index}`, 1, 65535);
|
|
935
|
+
}
|
|
936
|
+
if (!relays.routedTcpPorts.includes(relays.healthPort)) {
|
|
937
|
+
fail(`deployment.spec.targets.${targetName}.topology.relays`, "routedTcpPorts must include healthPort.");
|
|
938
|
+
}
|
|
939
|
+
const crossMetal = object(topology.crossMetal, `deployment.spec.targets.${targetName}.topology.crossMetal`);
|
|
940
|
+
if (crossMetal.mode === "private-lan")
|
|
941
|
+
exact3(crossMetal, ["mode"], `deployment.spec.targets.${targetName}.topology.crossMetal`);
|
|
942
|
+
else if (crossMetal.mode === "cloudflare-warp" || crossMetal.mode === "auto") {
|
|
943
|
+
exact3(crossMetal, crossMetal.mode === "auto" ? ["mode", "directPrivateMaximumMetals", "credential", "network"] : ["mode", "credential", "network"], `deployment.spec.targets.${targetName}.topology.crossMetal`);
|
|
944
|
+
if (crossMetal.mode === "auto" && crossMetal.directPrivateMaximumMetals !== 2)
|
|
945
|
+
fail(`deployment.spec.targets.${targetName}.topology.crossMetal.directPrivateMaximumMetals`, "must be 2.");
|
|
946
|
+
if (!credentials[named(crossMetal.credential, `deployment.spec.targets.${targetName}.topology.crossMetal.credential`)])
|
|
947
|
+
fail(`deployment.spec.targets.${targetName}.topology.crossMetal.credential`, "must name a declared credential.");
|
|
948
|
+
if (typeof crossMetal.network === "string")
|
|
949
|
+
named(crossMetal.network, `deployment.spec.targets.${targetName}.topology.crossMetal.network`);
|
|
950
|
+
else
|
|
951
|
+
reference(crossMetal.network, `deployment.spec.targets.${targetName}.topology.crossMetal.network`);
|
|
952
|
+
} else
|
|
953
|
+
fail(`deployment.spec.targets.${targetName}.topology.crossMetal.mode`, "is unsupported.");
|
|
954
|
+
if (allocation.sharing !== "exclusive")
|
|
955
|
+
fail(`deployment.spec.targets.${targetName}.allocation.sharing`, "must be exclusive for a per-metal relay topology.");
|
|
956
|
+
if (minimum < 1 + Number(relays.standbyPerMetal))
|
|
957
|
+
fail(`deployment.spec.targets.${targetName}.cardinality.minimum`, "must allow the declared relay and standby.");
|
|
958
|
+
}
|
|
914
959
|
if (target.placement !== undefined)
|
|
915
960
|
jsonValue(target.placement, `deployment.spec.targets.${targetName}.placement`);
|
|
916
961
|
targetDefinitions.set(targetName, target);
|
|
@@ -934,6 +979,9 @@ function compileDeployment(sourceValue) {
|
|
|
934
979
|
if (target.connectivity?.private?.mode === "cloudflare-warp" && ![...requirementDefinitions.values()].some(({ capability }) => capability === "network.private")) {
|
|
935
980
|
fail(`deployment.spec.targets.${targetName}.connectivity.private`, "requires one network.private provider requirement.");
|
|
936
981
|
}
|
|
982
|
+
if (target.topology && target.topology.crossMetal.mode !== "private-lan" && ![...requirementDefinitions.values()].some(({ capability }) => capability === "network.private")) {
|
|
983
|
+
fail(`deployment.spec.targets.${targetName}.topology.crossMetal`, "requires one network.private provider requirement.");
|
|
984
|
+
}
|
|
937
985
|
}
|
|
938
986
|
const components = object(spec.components, "deployment.spec.components");
|
|
939
987
|
const componentEntries = Object.entries(components);
|
package/dist/deploy.d.ts
CHANGED
|
@@ -74,6 +74,36 @@ export interface ComputeConnectivityDefinition {
|
|
|
74
74
|
};
|
|
75
75
|
};
|
|
76
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* A deployment may require one stable coordination role per physical site.
|
|
79
|
+
* The control plane groups computes by their admitted Metal identity; users do
|
|
80
|
+
* not name a particular machine as "master". A failed relay is replaced by
|
|
81
|
+
* the declared standby and the assignment generation is fenced by the API.
|
|
82
|
+
*/
|
|
83
|
+
export interface ComputeTopologyDefinition {
|
|
84
|
+
groupBy: 'metal';
|
|
85
|
+
relays: {
|
|
86
|
+
activePerMetal: 1;
|
|
87
|
+
standbyPerMetal: 0 | 1;
|
|
88
|
+
/** Exact TCP endpoint used to prove the selected relay is actually serving. */
|
|
89
|
+
healthPort: number;
|
|
90
|
+
/** Exact private TCP services routed and admitted between deployment sites. */
|
|
91
|
+
routedTcpPorts: readonly number[];
|
|
92
|
+
};
|
|
93
|
+
crossMetal: {
|
|
94
|
+
mode: 'private-lan';
|
|
95
|
+
} | {
|
|
96
|
+
mode: 'cloudflare-warp';
|
|
97
|
+
credential: string;
|
|
98
|
+
network: StringValue;
|
|
99
|
+
} | {
|
|
100
|
+
mode: 'auto';
|
|
101
|
+
/** One or two directly reachable sites never install WARP. */
|
|
102
|
+
directPrivateMaximumMetals: 2;
|
|
103
|
+
credential: string;
|
|
104
|
+
network: StringValue;
|
|
105
|
+
};
|
|
106
|
+
}
|
|
77
107
|
export type ResolvableJsonValue = JsonPrimitive | ValueReference | readonly ResolvableJsonValue[] | {
|
|
78
108
|
[key: string]: ResolvableJsonValue;
|
|
79
109
|
};
|
|
@@ -165,6 +195,7 @@ export interface TargetDefinition {
|
|
|
165
195
|
};
|
|
166
196
|
/** Per-slot connectivity intent. Credential names resolve through the scoped Vault, never to plan values. */
|
|
167
197
|
connectivity?: ComputeConnectivityDefinition;
|
|
198
|
+
topology?: ComputeTopologyDefinition;
|
|
168
199
|
placement?: {
|
|
169
200
|
spreadBy?: readonly ('metal' | 'region' | 'zone')[];
|
|
170
201
|
antiAffinity?: readonly string[];
|
|
@@ -190,6 +221,7 @@ export interface ComputePoolDefinition {
|
|
|
190
221
|
reuse?: 'prefer' | 'require';
|
|
191
222
|
provisioning?: TargetDefinition['provisioning'];
|
|
192
223
|
connectivity?: ComputeConnectivityDefinition;
|
|
224
|
+
topology?: ComputeTopologyDefinition;
|
|
193
225
|
labels?: Readonly<Record<string, string>>;
|
|
194
226
|
placement?: TargetDefinition['placement'];
|
|
195
227
|
}
|
|
@@ -354,6 +386,10 @@ export type StepScope = {
|
|
|
354
386
|
kind: 'target-batches';
|
|
355
387
|
target: string;
|
|
356
388
|
size: number;
|
|
389
|
+
} | {
|
|
390
|
+
kind: 'topology-role';
|
|
391
|
+
target: string;
|
|
392
|
+
role: 'relay' | 'standby' | 'member';
|
|
357
393
|
};
|
|
358
394
|
export interface RetryDefinition {
|
|
359
395
|
attempts: number;
|
|
@@ -480,7 +516,7 @@ export declare const ref: {
|
|
|
480
516
|
target: (target: string, property: string) => ValueReference;
|
|
481
517
|
deployment: (property: string) => ValueReference;
|
|
482
518
|
/** Secret-free control-plane assignment of the compute executing this step. */
|
|
483
|
-
execution: (property: "targetName" | "slot" | "releaseExecutor" | "runtime" | "cpuCores" | "memoryMiB" | "storageGiB") => ValueReference;
|
|
519
|
+
execution: (property: "targetName" | "slot" | "releaseExecutor" | "runtime" | "cpuCores" | "memoryMiB" | "storageGiB" | "site" | "topologyRole" | "topologyTransport" | "localRelayAddress") => ValueReference;
|
|
484
520
|
};
|
|
485
521
|
export declare const conditions: {
|
|
486
522
|
all: (...values: Condition[]) => Condition;
|
package/dist/deploy.js
CHANGED
|
@@ -173,6 +173,7 @@ var target = {
|
|
|
173
173
|
},
|
|
174
174
|
...value.provisioning ? { provisioning: value.provisioning } : {},
|
|
175
175
|
...value.connectivity ? { connectivity: value.connectivity } : {},
|
|
176
|
+
...value.topology ? { topology: value.topology } : {},
|
|
176
177
|
...value.placement ? { placement: value.placement } : {}
|
|
177
178
|
};
|
|
178
179
|
}
|
|
@@ -28,12 +28,31 @@ export interface DeploymentConnectivityCapabilities {
|
|
|
28
28
|
private?: {
|
|
29
29
|
connectorId: string;
|
|
30
30
|
connectorToken: string;
|
|
31
|
+
siteRouteId: string;
|
|
31
32
|
};
|
|
32
33
|
}
|
|
34
|
+
export interface DeploymentConnectivityTopology {
|
|
35
|
+
site: string;
|
|
36
|
+
nodeIdentity: string;
|
|
37
|
+
siteCidr: string;
|
|
38
|
+
role: 'relay' | 'standby' | 'member';
|
|
39
|
+
transport: 'private-lan' | 'cloudflare-warp';
|
|
40
|
+
localRelayAddress: string;
|
|
41
|
+
localPeerAddresses: readonly string[];
|
|
42
|
+
localPeerIdentities: readonly string[];
|
|
43
|
+
remoteRelayAddresses: readonly string[];
|
|
44
|
+
remoteRelayIdentities: readonly string[];
|
|
45
|
+
remoteSiteCidrs: readonly string[];
|
|
46
|
+
memberIdentities: readonly string[];
|
|
47
|
+
healthPort: number;
|
|
48
|
+
routedTcpPorts: readonly number[];
|
|
49
|
+
generation: string;
|
|
50
|
+
}
|
|
33
51
|
export interface DeploymentConnectivityRequest {
|
|
34
52
|
key: string;
|
|
35
53
|
intent: DeploymentConnectivityIntent;
|
|
36
54
|
capabilities: DeploymentConnectivityCapabilities;
|
|
55
|
+
topology?: DeploymentConnectivityTopology;
|
|
37
56
|
}
|
|
38
57
|
export interface DeploymentConnectivityEvidence {
|
|
39
58
|
key: string;
|
|
@@ -49,6 +68,12 @@ export interface DeploymentConnectivityEvidence {
|
|
|
49
68
|
unit: string;
|
|
50
69
|
active: true;
|
|
51
70
|
};
|
|
71
|
+
topology?: {
|
|
72
|
+
site: string;
|
|
73
|
+
siteCidr: string;
|
|
74
|
+
role: DeploymentConnectivityTopology['role'];
|
|
75
|
+
probed: string[];
|
|
76
|
+
};
|
|
52
77
|
}
|
|
53
78
|
export interface DeploymentConnectivityHost {
|
|
54
79
|
seal(name: string, path: string, value: string): Promise<void>;
|
|
@@ -57,6 +82,7 @@ export interface DeploymentConnectivityHost {
|
|
|
57
82
|
exitCode: number;
|
|
58
83
|
output: string;
|
|
59
84
|
}>;
|
|
85
|
+
probe(address: string, port: number, timeoutMs: number): Promise<boolean>;
|
|
60
86
|
sleep(ms: number): Promise<void>;
|
|
61
87
|
}
|
|
62
88
|
/** Apply only connector-scoped capabilities; the broad Cloudflare token never reaches this helper. */
|