@forgezero/agent 0.1.74 → 0.1.76
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 +126 -17
- package/dist/agent-heartbeat.d.ts +25 -1
- package/dist/agent-heartbeat.js +45 -5
- package/dist/bootstrap.js +10 -7
- package/dist/community-rehearsal-host.js +218 -13
- package/dist/credential-schema.d.ts +1 -1
- package/dist/credential-schema.js +1 -1
- package/dist/definition.js +206 -7
- package/dist/deploy-actions.d.ts +7 -0
- package/dist/deploy-compiler.js +191 -10
- package/dist/deploy-file.js +206 -7
- package/dist/deploy-plan-runner.d.ts +3 -0
- package/dist/deploy-plan-runner.js +190 -9
- package/dist/deploy-plan.js +187 -8
- package/dist/deploy-providers.d.ts +19 -0
- package/dist/deploy.d.ts +89 -3
- package/dist/deploy.js +49 -2
- package/dist/deployment-connectivity.d.ts +63 -0
- package/dist/deployment-connectivity.js +155 -0
- package/dist/deployment-pull.d.ts +2 -0
- package/dist/deployment-targets.d.ts +9 -0
- package/dist/deployment-targets.js +141 -0
- package/dist/deployment.d.ts +69 -0
- package/dist/fz-agent.js +1122 -313
- package/dist/fz.js +234 -26
- package/dist/index.d.ts +1 -0
- package/dist/metal-bootstrap.js +1 -1
- package/dist/metal-helper-socket.js +210 -11
- package/dist/metal-provision.js +210 -11
- package/dist/operator-bootstrap.js +11 -9
- package/dist/platform-bootstrap-runtime.js +209 -10
- package/dist/platform-fleet-verification.js +525 -155
- package/dist/platform-genesis.js +206 -7
- package/dist/process-input.d.ts +11 -0
- package/dist/provision.js +472 -99
- package/dist/service-supervisor.d.ts +6 -0
- package/dist/software-helper.d.ts +3 -0
- package/dist/software-helper.js +472 -98
- package/dist/software.d.ts +4 -1
- package/dist/software.js +210 -8
- package/dist/ubuntu.js +206 -7
- package/dist/version.d.ts +1 -1
- package/package.json +12 -4
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// src/deployment-connectivity.ts
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { mkdirSync, renameSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname } from "node:path";
|
|
5
|
+
|
|
6
|
+
// src/process-input.ts
|
|
7
|
+
async function writeAndCloseProcessInput(input, value) {
|
|
8
|
+
input.write(value);
|
|
9
|
+
await input.end();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// src/deployment-connectivity.ts
|
|
13
|
+
var TOKEN = /^[A-Za-z0-9._-]{40,16384}$/;
|
|
14
|
+
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;
|
|
15
|
+
var HOSTNAME = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
16
|
+
var NAME = /^[a-z][a-z0-9._-]{0,99}$/;
|
|
17
|
+
var idFor = (key) => createHash("sha256").update(key).digest("hex").slice(0, 20);
|
|
18
|
+
var checked = async (host, argv, label) => {
|
|
19
|
+
const result = await host.exec(argv);
|
|
20
|
+
if (result.exitCode !== 0)
|
|
21
|
+
throw new Error(`${label} failed: ${result.output.slice(0, 512)}`);
|
|
22
|
+
return result.output.trim();
|
|
23
|
+
};
|
|
24
|
+
async function sealWithSystemd(name, path, value) {
|
|
25
|
+
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
26
|
+
const next = `${path}.next`;
|
|
27
|
+
const child = Bun.spawn(["/usr/bin/systemd-creds", "encrypt", `--name=${name}`, "-", next], {
|
|
28
|
+
stdin: "pipe",
|
|
29
|
+
stdout: "pipe",
|
|
30
|
+
stderr: "pipe",
|
|
31
|
+
env: { PATH: "/usr/sbin:/usr/bin:/sbin:/bin", LANG: "C", LC_ALL: "C" }
|
|
32
|
+
});
|
|
33
|
+
await writeAndCloseProcessInput(child.stdin, value);
|
|
34
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
35
|
+
new Response(child.stdout).text(),
|
|
36
|
+
new Response(child.stderr).text(),
|
|
37
|
+
child.exited
|
|
38
|
+
]);
|
|
39
|
+
if (exitCode !== 0)
|
|
40
|
+
throw new Error(`systemd credential sealing failed: ${`${stdout}${stderr}`.replaceAll(value, "[REDACTED]").slice(0, 512)}`);
|
|
41
|
+
renameSync(next, path);
|
|
42
|
+
}
|
|
43
|
+
var defaultHost = {
|
|
44
|
+
seal: sealWithSystemd,
|
|
45
|
+
write(path, content, mode) {
|
|
46
|
+
mkdirSync(dirname(path), { recursive: true, mode: 493 });
|
|
47
|
+
const next = `${path}.next`;
|
|
48
|
+
writeFileSync(next, content, { mode });
|
|
49
|
+
renameSync(next, path);
|
|
50
|
+
},
|
|
51
|
+
async exec(argv) {
|
|
52
|
+
const child = Bun.spawn([...argv], { stdout: "pipe", stderr: "pipe", env: { PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", LANG: "C", LC_ALL: "C" } });
|
|
53
|
+
const [stdout, stderr, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
|
|
54
|
+
return { exitCode, output: `${stdout}${stderr}` };
|
|
55
|
+
},
|
|
56
|
+
sleep: (ms) => Bun.sleep(ms)
|
|
57
|
+
};
|
|
58
|
+
function validate(request) {
|
|
59
|
+
if (!request.key || request.key.length > 256)
|
|
60
|
+
throw new Error("deployment connectivity key is invalid");
|
|
61
|
+
const publicIntent = request.intent.public;
|
|
62
|
+
const privateIntent = request.intent.private;
|
|
63
|
+
if (publicIntent?.mode === "cloudflare-tunnel") {
|
|
64
|
+
if (!NAME.test(publicIntent.credential) || !HOSTNAME.test(publicIntent.hostname) || !NAME.test(publicIntent.tunnelKey) || publicIntent.service.protocol !== "http" || !Number.isInteger(publicIntent.service.port) || publicIntent.service.port < 1 || publicIntent.service.port > 65535 || !request.capabilities.public || !UUID.test(request.capabilities.public.tunnelId) || !TOKEN.test(request.capabilities.public.connectorToken)) {
|
|
65
|
+
throw new Error("public deployment connectivity is malformed");
|
|
66
|
+
}
|
|
67
|
+
} else if (request.capabilities.public)
|
|
68
|
+
throw new Error("unexpected public deployment capability");
|
|
69
|
+
if (privateIntent?.mode === "cloudflare-warp") {
|
|
70
|
+
if (!NAME.test(privateIntent.credential) || privateIntent.network.length < 1 || privateIntent.network.length > 128 || !request.capabilities.private || !UUID.test(request.capabilities.private.connectorId) || !TOKEN.test(request.capabilities.private.connectorToken)) {
|
|
71
|
+
throw new Error("private deployment connectivity is malformed");
|
|
72
|
+
}
|
|
73
|
+
} else if (request.capabilities.private)
|
|
74
|
+
throw new Error("unexpected private deployment capability");
|
|
75
|
+
}
|
|
76
|
+
async function applyDeploymentConnectivity(request, host = defaultHost) {
|
|
77
|
+
validate(request);
|
|
78
|
+
const id = idFor(request.key);
|
|
79
|
+
const evidence = { key: request.key };
|
|
80
|
+
if (request.intent.public?.mode === "cloudflare-tunnel") {
|
|
81
|
+
const capability = request.capabilities.public;
|
|
82
|
+
const credentialName = `FZ_TUNNEL_${id.toUpperCase()}`;
|
|
83
|
+
const credentialPath = `/etc/forgezero/creds/deployment-tunnel-${id}.cred`;
|
|
84
|
+
const unit = `forgezero-cloudflared-${id}.service`;
|
|
85
|
+
const unitPath = `/etc/systemd/system/${unit}`;
|
|
86
|
+
const metricsPort = 20000 + Number.parseInt(id.slice(0, 4), 16) % 20000;
|
|
87
|
+
await host.seal(credentialName, credentialPath, capability.connectorToken);
|
|
88
|
+
host.write(unitPath, `[Unit]
|
|
89
|
+
Description=ForgeZero deployment Tunnel ${id}
|
|
90
|
+
After=network-online.target
|
|
91
|
+
Wants=network-online.target
|
|
92
|
+
|
|
93
|
+
[Service]
|
|
94
|
+
Type=simple
|
|
95
|
+
DynamicUser=yes
|
|
96
|
+
LoadCredentialEncrypted=${credentialName}:${credentialPath}
|
|
97
|
+
ExecStart=/usr/local/bin/cloudflared tunnel --no-autoupdate --metrics 127.0.0.1:${metricsPort} run --token-file %d/${credentialName}
|
|
98
|
+
Restart=always
|
|
99
|
+
RestartSec=5
|
|
100
|
+
NoNewPrivileges=true
|
|
101
|
+
PrivateTmp=true
|
|
102
|
+
ProtectSystem=strict
|
|
103
|
+
ProtectHome=true
|
|
104
|
+
|
|
105
|
+
[Install]
|
|
106
|
+
WantedBy=multi-user.target
|
|
107
|
+
`, 420);
|
|
108
|
+
await checked(host, ["/usr/bin/systemctl", "daemon-reload"], "cloudflared daemon reload");
|
|
109
|
+
await checked(host, ["/usr/bin/systemctl", "enable", "--now", unit], "cloudflared start");
|
|
110
|
+
await checked(host, ["/usr/bin/systemctl", "is-active", "--quiet", unit], "cloudflared readiness");
|
|
111
|
+
evidence.public = { hostname: request.intent.public.hostname, tunnelId: capability.tunnelId, unit, active: true };
|
|
112
|
+
}
|
|
113
|
+
if (request.intent.private?.mode === "cloudflare-warp") {
|
|
114
|
+
const capability = request.capabilities.private;
|
|
115
|
+
const credentialPath = "/etc/forgezero/creds/CF_WARP_CONNECTOR_TOKEN.cred";
|
|
116
|
+
const unit = "forgezero-deployment-mesh.service";
|
|
117
|
+
await host.seal("CF_WARP_CONNECTOR_TOKEN", credentialPath, capability.connectorToken);
|
|
118
|
+
host.write(`/etc/systemd/system/${unit}`, `[Unit]
|
|
119
|
+
Description=ForgeZero deployment Mesh/WARP connector
|
|
120
|
+
Requires=warp-svc.service
|
|
121
|
+
After=network-online.target warp-svc.service
|
|
122
|
+
Wants=network-online.target
|
|
123
|
+
|
|
124
|
+
[Service]
|
|
125
|
+
Type=oneshot
|
|
126
|
+
RemainAfterExit=yes
|
|
127
|
+
LoadCredentialEncrypted=CF_WARP_CONNECTOR_TOKEN:${credentialPath}
|
|
128
|
+
ExecStart=/usr/local/bin/fz-agent mesh-config
|
|
129
|
+
NoNewPrivileges=true
|
|
130
|
+
PrivateTmp=true
|
|
131
|
+
ProtectSystem=strict
|
|
132
|
+
ProtectHome=true
|
|
133
|
+
ReadWritePaths=/var/lib/cloudflare-warp
|
|
134
|
+
|
|
135
|
+
[Install]
|
|
136
|
+
WantedBy=multi-user.target
|
|
137
|
+
`, 420);
|
|
138
|
+
await checked(host, ["/usr/bin/systemctl", "daemon-reload"], "WARP daemon reload");
|
|
139
|
+
await checked(host, ["/usr/bin/systemctl", "enable", "--now", "warp-svc.service", unit], "WARP connector start");
|
|
140
|
+
for (let attempt = 0;attempt < 10; attempt += 1) {
|
|
141
|
+
const status = await host.exec(["/usr/bin/warp-cli", "--accept-tos", "status"]);
|
|
142
|
+
if (status.exitCode === 0 && /\bconnected\b/i.test(status.output) && !/\bdisconnected\b/i.test(status.output)) {
|
|
143
|
+
evidence.private = { network: request.intent.private.network, connectorId: capability.connectorId, unit, active: true };
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
await host.sleep(1000);
|
|
147
|
+
}
|
|
148
|
+
if (!evidence.private)
|
|
149
|
+
throw new Error("WARP connector readiness failed");
|
|
150
|
+
}
|
|
151
|
+
return evidence;
|
|
152
|
+
}
|
|
153
|
+
export {
|
|
154
|
+
applyDeploymentConnectivity
|
|
155
|
+
};
|
|
@@ -10,6 +10,8 @@ export interface RemoteDeploymentClaim {
|
|
|
10
10
|
attempt: number;
|
|
11
11
|
/** True on the one target deterministically elected for this release. */
|
|
12
12
|
releaseExecutor: boolean;
|
|
13
|
+
assignment?: NonNullable<import('./deployment').DeploymentRequest['assignment']>;
|
|
14
|
+
connectivityCapabilities?: import('./deployment-connectivity').DeploymentConnectivityCapabilities;
|
|
13
15
|
source: {
|
|
14
16
|
repository: string;
|
|
15
17
|
branch: string;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { JsonPrimitive } from './deploy';
|
|
2
|
+
import type { DeploymentPlan } from './deploy-plan';
|
|
3
|
+
import type { ResolvedDeploymentTarget } from './deployment';
|
|
4
|
+
export declare class DeploymentInputError extends Error {
|
|
5
|
+
constructor(message: string);
|
|
6
|
+
}
|
|
7
|
+
/** Resolve stable logical slots without selecting a hostname or mutating capacity. */
|
|
8
|
+
export declare function resolveDeploymentTargets(plan: DeploymentPlan, supplied?: Readonly<Record<string, unknown>>): ResolvedDeploymentTarget[];
|
|
9
|
+
export declare function resolveDeploymentInputs(plan: DeploymentPlan, supplied?: Readonly<Record<string, unknown>>): Record<string, JsonPrimitive>;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// src/deployment-targets.ts
|
|
2
|
+
class DeploymentInputError extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "DeploymentInputError";
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
var HOSTNAME = /^(?=.{1,253}$)[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?$/;
|
|
9
|
+
function inputValue(name, definition, supplied) {
|
|
10
|
+
let value = supplied ?? definition.default;
|
|
11
|
+
if (value === undefined) {
|
|
12
|
+
if (definition.required)
|
|
13
|
+
throw new DeploymentInputError(`deployment input ${name} is required`);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (typeof value === "string") {
|
|
17
|
+
const text = value;
|
|
18
|
+
if (definition.type === "boolean" && (text === "true" || text === "false"))
|
|
19
|
+
value = text === "true";
|
|
20
|
+
if ((definition.type === "integer" || definition.type === "number") && text.trim() !== "")
|
|
21
|
+
value = Number(text);
|
|
22
|
+
}
|
|
23
|
+
if (definition.type === "boolean" && typeof value !== "boolean")
|
|
24
|
+
throw new DeploymentInputError(`deployment input ${name} must be boolean`);
|
|
25
|
+
if (definition.type === "integer" && (!Number.isSafeInteger(value) || Number(value) < (definition.minimum ?? Number.MIN_SAFE_INTEGER) || Number(value) > (definition.maximum ?? Number.MAX_SAFE_INTEGER)))
|
|
26
|
+
throw new DeploymentInputError(`deployment input ${name} must be a bounded integer`);
|
|
27
|
+
if (definition.type === "number" && (typeof value !== "number" || !Number.isFinite(value) || value < (definition.minimum ?? -Number.MAX_VALUE) || value > (definition.maximum ?? Number.MAX_VALUE)))
|
|
28
|
+
throw new DeploymentInputError(`deployment input ${name} must be a bounded number`);
|
|
29
|
+
if (definition.type === "enum" && (typeof value !== "string" || !definition.values.includes(value)))
|
|
30
|
+
throw new DeploymentInputError(`deployment input ${name} must be one of its declared values`);
|
|
31
|
+
if (definition.type === "hostname" && (typeof value !== "string" || !HOSTNAME.test(value)))
|
|
32
|
+
throw new DeploymentInputError(`deployment input ${name} must be a hostname`);
|
|
33
|
+
if (definition.type === "string") {
|
|
34
|
+
if (typeof value !== "string" || value.length < (definition.minimumLength ?? 0) || value.length > (definition.maximumLength ?? 4096))
|
|
35
|
+
throw new DeploymentInputError(`deployment input ${name} must be a bounded string`);
|
|
36
|
+
if (definition.pattern !== undefined && !new RegExp(definition.pattern).test(value))
|
|
37
|
+
throw new DeploymentInputError(`deployment input ${name} does not match its pattern`);
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
function resolveDeploymentTargets(plan, supplied = {}) {
|
|
42
|
+
const inputs = resolveDeploymentInputs(plan, supplied);
|
|
43
|
+
const stringCoordinate = (value, label) => {
|
|
44
|
+
const resolved = typeof value === "string" ? value : value.$ref.startsWith("inputs.") ? inputs[value.$ref.slice("inputs.".length)] : undefined;
|
|
45
|
+
if (typeof resolved !== "string" || resolved.length < 1 || resolved.length > 253) {
|
|
46
|
+
throw new DeploymentInputError(`${label} did not resolve to a bounded string`);
|
|
47
|
+
}
|
|
48
|
+
return resolved.toLowerCase();
|
|
49
|
+
};
|
|
50
|
+
const integerCoordinate = (value, label, minimum, maximum) => {
|
|
51
|
+
const resolved = typeof value === "number" ? value : value.$ref.startsWith("inputs.") ? inputs[value.$ref.slice("inputs.".length)] : undefined;
|
|
52
|
+
if (!Number.isSafeInteger(resolved) || Number(resolved) < minimum || Number(resolved) > maximum) {
|
|
53
|
+
throw new DeploymentInputError(`${label} did not resolve to a bounded integer`);
|
|
54
|
+
}
|
|
55
|
+
return Number(resolved);
|
|
56
|
+
};
|
|
57
|
+
const credentialSource = (alias, label) => {
|
|
58
|
+
const source = plan.spec.credentials?.[alias]?.source.name;
|
|
59
|
+
if (!source)
|
|
60
|
+
throw new DeploymentInputError(`${label} did not resolve to a declared Vault credential`);
|
|
61
|
+
return source;
|
|
62
|
+
};
|
|
63
|
+
return Object.entries(plan.spec.targets).map(([name, target]) => {
|
|
64
|
+
const desired = typeof target.cardinality.desired === "number" ? target.cardinality.desired : target.cardinality.desired.$ref.startsWith("inputs.") ? inputs[target.cardinality.desired.$ref.slice("inputs.".length)] : undefined;
|
|
65
|
+
if (!Number.isSafeInteger(desired) || Number(desired) < target.cardinality.minimum || Number(desired) > target.cardinality.maximum) {
|
|
66
|
+
throw new DeploymentInputError(`target ${name} desired cardinality did not resolve to a bounded integer`);
|
|
67
|
+
}
|
|
68
|
+
let connectivity;
|
|
69
|
+
if (target.connectivity) {
|
|
70
|
+
const privateNetwork = target.connectivity.private;
|
|
71
|
+
const publicNetwork = target.connectivity.public;
|
|
72
|
+
connectivity = {
|
|
73
|
+
...privateNetwork ? { private: privateNetwork.mode === "cloudflare-warp" ? {
|
|
74
|
+
mode: "cloudflare-warp",
|
|
75
|
+
credential: credentialSource(privateNetwork.credential, `target ${name} WARP credential`),
|
|
76
|
+
network: stringCoordinate(privateNetwork.network, `target ${name} WARP network`)
|
|
77
|
+
} : { mode: privateNetwork.mode } } : {},
|
|
78
|
+
...publicNetwork ? { public: publicNetwork.mode === "cloudflare-tunnel" ? {
|
|
79
|
+
mode: "cloudflare-tunnel",
|
|
80
|
+
credential: credentialSource(publicNetwork.credential, `target ${name} Tunnel credential`),
|
|
81
|
+
zone: stringCoordinate(publicNetwork.zone, `target ${name} Tunnel zone`),
|
|
82
|
+
hostname: publicNetwork.hostname.mode === "static" ? {
|
|
83
|
+
mode: "static",
|
|
84
|
+
label: stringCoordinate(publicNetwork.hostname.label, `target ${name} Tunnel label`)
|
|
85
|
+
} : {
|
|
86
|
+
mode: "indexed",
|
|
87
|
+
prefix: stringCoordinate(publicNetwork.hostname.prefix, `target ${name} Tunnel prefix`),
|
|
88
|
+
startAt: publicNetwork.hostname.startAt ?? 1
|
|
89
|
+
},
|
|
90
|
+
service: (() => {
|
|
91
|
+
const applications = Object.values(plan.spec.components).filter((component) => component.target === name && component.kind === "application" && component.network?.public?.mode === "cloudflare-tunnel");
|
|
92
|
+
if (applications.length !== 1)
|
|
93
|
+
throw new DeploymentInputError(`target ${name} Tunnel requires exactly one public application`);
|
|
94
|
+
const port = applications[0].network.ingress?.stablePort;
|
|
95
|
+
if (!Number.isInteger(port))
|
|
96
|
+
throw new DeploymentInputError(`target ${name} Tunnel application requires a stable ingress port`);
|
|
97
|
+
return { protocol: "http", port: Number(port) };
|
|
98
|
+
})()
|
|
99
|
+
} : { mode: "disabled" } } : {}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
name,
|
|
104
|
+
profiles: [...target.selector.profiles],
|
|
105
|
+
operatingSystem: { ...target.selector.operatingSystem },
|
|
106
|
+
confidentialCompute: target.selector.confidentialCompute ?? "disabled",
|
|
107
|
+
labels: { ...target.selector.labels ?? {} },
|
|
108
|
+
minimum: target.cardinality.minimum,
|
|
109
|
+
desired: Number(desired),
|
|
110
|
+
maximum: target.cardinality.maximum,
|
|
111
|
+
allocation: {
|
|
112
|
+
...target.allocation,
|
|
113
|
+
resources: {
|
|
114
|
+
cpuCores: integerCoordinate(target.allocation.resources.cpuCores, `target ${name} cpuCores`, 1, 1024),
|
|
115
|
+
memoryMiB: integerCoordinate(target.allocation.resources.memoryMiB, `target ${name} memoryMiB`, 128, 4194304),
|
|
116
|
+
storageGiB: integerCoordinate(target.allocation.resources.storageGiB, `target ${name} storageGiB`, 1, 1048576)
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
...target.provisioning ? { provisioning: { ...target.provisioning } } : {},
|
|
120
|
+
...connectivity ? { connectivity } : {}
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
function resolveDeploymentInputs(plan, supplied = {}) {
|
|
125
|
+
for (const name of Object.keys(supplied))
|
|
126
|
+
if (!(name in (plan.spec.inputs ?? {}))) {
|
|
127
|
+
throw new DeploymentInputError(`deployment input ${name} is not declared`);
|
|
128
|
+
}
|
|
129
|
+
const inputs = {};
|
|
130
|
+
for (const [name, definition] of Object.entries(plan.spec.inputs ?? {})) {
|
|
131
|
+
const value = inputValue(name, definition, supplied[name]);
|
|
132
|
+
if (value !== undefined)
|
|
133
|
+
inputs[name] = value;
|
|
134
|
+
}
|
|
135
|
+
return inputs;
|
|
136
|
+
}
|
|
137
|
+
export {
|
|
138
|
+
resolveDeploymentTargets,
|
|
139
|
+
resolveDeploymentInputs,
|
|
140
|
+
DeploymentInputError
|
|
141
|
+
};
|
package/dist/deployment.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { SecretCache } from './cache';
|
|
|
9
9
|
import type { AttestationSource } from './socket';
|
|
10
10
|
import { type CapacityCalibration, type CapacityCalibrationOptions } from './capacity-calibration';
|
|
11
11
|
import { type BootstrapBundleManifest } from './bootstrap-bundle';
|
|
12
|
+
import type { DeploymentConnectivityCapabilities, DeploymentConnectivityEvidence, DeploymentConnectivityIntent, DeploymentConnectivityRequest } from './deployment-connectivity';
|
|
12
13
|
export interface CommandInput {
|
|
13
14
|
argv: readonly string[];
|
|
14
15
|
cwd?: string;
|
|
@@ -24,6 +25,20 @@ export interface DeploymentRequest {
|
|
|
24
25
|
revision?: string;
|
|
25
26
|
/** True only for the target deterministically elected for release-scoped steps. */
|
|
26
27
|
releaseExecutor?: boolean;
|
|
28
|
+
/** Exact control-plane placement for this compute. Absence is allowed only for one unambiguous bootstrap target. */
|
|
29
|
+
assignment?: {
|
|
30
|
+
targetName: string;
|
|
31
|
+
slot: number;
|
|
32
|
+
definitionDigest: string;
|
|
33
|
+
operatingSystem: ResolvedDeploymentTarget['operatingSystem'];
|
|
34
|
+
confidentialCompute: ResolvedDeploymentTarget['confidentialCompute'];
|
|
35
|
+
execution: ResolvedDeploymentTarget['allocation']['execution'];
|
|
36
|
+
sharing: ResolvedDeploymentTarget['allocation']['sharing'];
|
|
37
|
+
resources: ResolvedDeploymentTarget['allocation']['resources'];
|
|
38
|
+
connectivity?: DeploymentConnectivityIntent;
|
|
39
|
+
};
|
|
40
|
+
/** Connector-scoped, one-claim capabilities. Never the project Cloudflare management token. */
|
|
41
|
+
connectivityCapabilities?: DeploymentConnectivityCapabilities;
|
|
27
42
|
}
|
|
28
43
|
export interface DeploymentResult {
|
|
29
44
|
key: string;
|
|
@@ -49,11 +64,63 @@ export interface DeploymentResult {
|
|
|
49
64
|
export interface ResolvedDeploymentTarget {
|
|
50
65
|
name: string;
|
|
51
66
|
profiles: readonly string[];
|
|
67
|
+
operatingSystem: {
|
|
68
|
+
id: 'ubuntu';
|
|
69
|
+
version: '24.04' | '26.04';
|
|
70
|
+
architecture: 'x64';
|
|
71
|
+
};
|
|
52
72
|
confidentialCompute: 'required' | 'preferred' | 'disabled';
|
|
53
73
|
labels: Readonly<Record<string, string>>;
|
|
54
74
|
minimum: number;
|
|
55
75
|
desired: number;
|
|
56
76
|
maximum: number;
|
|
77
|
+
allocation: {
|
|
78
|
+
execution: 'native' | 'oci-runc' | 'oci-kata-qemu-snp';
|
|
79
|
+
sharing: 'exclusive' | 'shared';
|
|
80
|
+
resources: {
|
|
81
|
+
cpuCores: number;
|
|
82
|
+
memoryMiB: number;
|
|
83
|
+
storageGiB: number;
|
|
84
|
+
};
|
|
85
|
+
existing: 'prefer' | 'require';
|
|
86
|
+
};
|
|
87
|
+
provisioning?: {
|
|
88
|
+
planKey: string;
|
|
89
|
+
regionKey: string;
|
|
90
|
+
imageKey: string;
|
|
91
|
+
environmentKey: string;
|
|
92
|
+
ownership: 'platform' | 'tenant-metal';
|
|
93
|
+
metalHostname?: string;
|
|
94
|
+
maxMonthlySpendMinor: string;
|
|
95
|
+
};
|
|
96
|
+
connectivity?: {
|
|
97
|
+
private?: {
|
|
98
|
+
mode: 'disabled' | 'private-lan';
|
|
99
|
+
} | {
|
|
100
|
+
mode: 'cloudflare-warp';
|
|
101
|
+
credential: string;
|
|
102
|
+
network: string;
|
|
103
|
+
};
|
|
104
|
+
public?: {
|
|
105
|
+
mode: 'disabled';
|
|
106
|
+
} | {
|
|
107
|
+
mode: 'cloudflare-tunnel';
|
|
108
|
+
credential: string;
|
|
109
|
+
zone: string;
|
|
110
|
+
hostname: {
|
|
111
|
+
mode: 'static';
|
|
112
|
+
label: string;
|
|
113
|
+
} | {
|
|
114
|
+
mode: 'indexed';
|
|
115
|
+
prefix: string;
|
|
116
|
+
startAt: number;
|
|
117
|
+
};
|
|
118
|
+
service: {
|
|
119
|
+
protocol: 'http';
|
|
120
|
+
port: number;
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
};
|
|
57
124
|
}
|
|
58
125
|
/**
|
|
59
126
|
* How the credential-bearing agent may read one server-owned Git source.
|
|
@@ -121,6 +188,8 @@ export interface DeploymentOptions {
|
|
|
121
188
|
buildContainer?: (request: ContainerBuildRequest) => Promise<ContainerBuildResult>;
|
|
122
189
|
/** Root-owned bounded container/Nginx blue-green activation. */
|
|
123
190
|
activateContainer?: (request: ContainerActivateRequest) => Promise<ServiceActivationState>;
|
|
191
|
+
/** Root-owned Tunnel/WARP convergence using connector-scoped capabilities only. */
|
|
192
|
+
applyConnectivity?: (request: DeploymentConnectivityRequest) => Promise<DeploymentConnectivityEvidence>;
|
|
124
193
|
/** Agent-owned directory; definitions cannot choose or overwrite this path. */
|
|
125
194
|
capacityEvidenceDirectory?: string;
|
|
126
195
|
/** Test/embedding seam. Production runs the bounded loopback GET calibrator. */
|