@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
package/dist/deploy-compiler.js
CHANGED
|
@@ -38,6 +38,13 @@ function validateBuiltInAction(definition, context, where) {
|
|
|
38
38
|
throw new Error(`${where}.with.requirements must name existing requirements`);
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
|
+
if (definition.uses === "forgezero.network/ensure@1") {
|
|
42
|
+
exact(withValue, ["target"], `${where}.with`);
|
|
43
|
+
if (typeof withValue.target !== "string" || !Object.values(context.components).some((entry) => entry.target === withValue.target)) {
|
|
44
|
+
throw new Error(`${where}.with.target must name a target used by a component`);
|
|
45
|
+
}
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
41
48
|
if (definition.uses === "forgezero.exec/argv@1") {
|
|
42
49
|
const allowed = ["component", "argv", "credentials"];
|
|
43
50
|
const unknown = Object.keys(withValue).filter((key) => !allowed.includes(key));
|
|
@@ -93,6 +100,8 @@ function validateBuiltInProvider(requirement2, where) {
|
|
|
93
100
|
const expected = {
|
|
94
101
|
"forgezero.bun": { capability: "runtime.javascript", versions: ["1.3.14"], keys: ["installScope"] },
|
|
95
102
|
"forgezero.docker": { capability: "runtime.container", versions: ["ubuntu-24.04", "ubuntu-26.04"], keys: ["installScope", "storageDriver", "dataRoot", "liveRestore", "logDriver", "defaultNetwork", "rootless"] },
|
|
103
|
+
"forgezero.containerd": { capability: "runtime.container", versions: ["ubuntu-24.04", "ubuntu-26.04"], keys: ["installScope", "runtimeClass", "namespace", "snapshotter"] },
|
|
104
|
+
"forgezero.kata": { capability: "runtime.container", versions: ["4.0.0"], keys: ["installScope", "runtimeClass", "namespace", "hypervisor", "confidentialGuest"] },
|
|
96
105
|
"forgezero.nginx": { capability: "proxy.http", versions: ["ubuntu-24.04", "ubuntu-26.04"], keys: ["installScope", "websocket", "maximumBodyMiB", "workerProcesses", "workerConnections"] },
|
|
97
106
|
"forgezero.arangodb": { capability: "database.arangodb", versions: ["3.11.14"], keys: ["installScope", "runtime", "dataPath", "bind"] },
|
|
98
107
|
"forgezero.cloudflared": { capability: "network.public-tunnel", versions: ["2026.7.3"], keys: ["installScope", "mode"] },
|
|
@@ -113,6 +122,10 @@ function validateBuiltInProvider(requirement2, where) {
|
|
|
113
122
|
if (row2.storageDriver !== "overlay2" || row2.dataRoot !== "/var/lib/docker" || row2.liveRestore !== true || row2.logDriver !== "local" || row2.defaultNetwork !== "bridge" || row2.rootless !== false)
|
|
114
123
|
throw new Error(`${where} Docker config is unsupported by the fixed host strategy`);
|
|
115
124
|
}
|
|
125
|
+
if (requirement2.provider === "forgezero.containerd" && (row2.runtimeClass !== "runc" || row2.namespace !== "forgezero" || row2.snapshotter !== "overlayfs"))
|
|
126
|
+
throw new Error(`${where} containerd config is unsupported by the fixed host strategy`);
|
|
127
|
+
if (requirement2.provider === "forgezero.kata" && (row2.runtimeClass !== "kata-qemu-snp" || row2.namespace !== "forgezero" || row2.hypervisor !== "qemu" || row2.confidentialGuest !== "sev-snp"))
|
|
128
|
+
throw new Error(`${where} Kata config is unsupported by the fixed host strategy`);
|
|
116
129
|
if (requirement2.provider === "forgezero.nginx" && (typeof row2.websocket !== "boolean" || !Number.isInteger(row2.maximumBodyMiB) || Number(row2.maximumBodyMiB) < 1 || Number(row2.maximumBodyMiB) > 1024 || !(row2.workerProcesses === "auto" || Number.isInteger(row2.workerProcesses)) || !Number.isInteger(row2.workerConnections)))
|
|
117
130
|
throw new Error(`${where} Nginx config is invalid`);
|
|
118
131
|
if (requirement2.provider === "forgezero.arangodb" && (row2.runtime !== "native" || !absolute(row2.dataPath) || row2.bind !== "private"))
|
|
@@ -190,7 +203,7 @@ function reference(value, where) {
|
|
|
190
203
|
const row2 = object(value, where);
|
|
191
204
|
exact3(row2, ["$ref"], where);
|
|
192
205
|
const coordinate = boundedString(row2.$ref, `${where}.$ref`, 256);
|
|
193
|
-
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})$/.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))$/.test(coordinate)) {
|
|
194
207
|
fail(where, "contains an unsupported reference.");
|
|
195
208
|
}
|
|
196
209
|
return { $ref: coordinate };
|
|
@@ -430,10 +443,9 @@ function validateNetwork(value, where) {
|
|
|
430
443
|
named(container.network, `${where}.container.network`);
|
|
431
444
|
}
|
|
432
445
|
}
|
|
433
|
-
function validateComponent(value, where,
|
|
446
|
+
function validateComponent(value, where, targets, requirements) {
|
|
434
447
|
const row2 = object(value, where);
|
|
435
|
-
|
|
436
|
-
fail(`${where}.target`, "must name an existing target.");
|
|
448
|
+
const selectedTarget = targets.get(String(row2.target)) ?? fail(`${where}.target`, "must name an existing target.");
|
|
437
449
|
if (row2.kind === "application") {
|
|
438
450
|
exact3(row2, ["kind", "target", "runtime", "service", "resources", "storage", "network", "rollout"], where);
|
|
439
451
|
const runtime = object(row2.runtime, `${where}.runtime`);
|
|
@@ -444,7 +456,9 @@ function validateComponent(value, where, targetNames, requirements) {
|
|
|
444
456
|
exact3(runtime, ["kind", "provider", "requirement", "argv"], `${where}.runtime`);
|
|
445
457
|
validateArgv(runtime.argv, `${where}.runtime.argv`);
|
|
446
458
|
} else if (runtime.kind === "container") {
|
|
447
|
-
exact3(runtime, ["kind", "provider", "requirement", "image", "entrypoint", "security"], `${where}.runtime`);
|
|
459
|
+
exact3(runtime, ["kind", "provider", "requirement", "runtimeClass", "image", "entrypoint", "security"], `${where}.runtime`);
|
|
460
|
+
if (runtime.runtimeClass !== undefined && !["runc", "kata-qemu-snp"].includes(String(runtime.runtimeClass)))
|
|
461
|
+
fail(`${where}.runtime.runtimeClass`, "is unsupported.");
|
|
448
462
|
const image = object(runtime.image, `${where}.runtime.image`);
|
|
449
463
|
exact3(image, ["source"], `${where}.runtime.image`);
|
|
450
464
|
const source = object(image.source, `${where}.runtime.image.source`);
|
|
@@ -478,6 +492,23 @@ function validateComponent(value, where, targetNames, requirements) {
|
|
|
478
492
|
fail(`${where}.runtime.requirement`, "must provide runtime.container.");
|
|
479
493
|
if (runtimeRequirement && runtime.kind === "native" && !runtimeRequirement.capability.startsWith("runtime."))
|
|
480
494
|
fail(`${where}.runtime.requirement`, "must provide a runtime capability.");
|
|
495
|
+
const execution = selectedTarget.allocation.execution;
|
|
496
|
+
if (execution === "native" && runtime.kind !== "native")
|
|
497
|
+
fail(`${where}.runtime.kind`, "must be native for the selected target.");
|
|
498
|
+
if (execution !== "native" && runtime.kind !== "container")
|
|
499
|
+
fail(`${where}.runtime.kind`, "must be container for the selected target.");
|
|
500
|
+
if (runtime.kind === "container" && execution === "oci-runc") {
|
|
501
|
+
if (!["forgezero.containerd", "forgezero.docker"].includes(String(runtime.provider)))
|
|
502
|
+
fail(`${where}.runtime.provider`, "must be forgezero.containerd for an OCI runc target.");
|
|
503
|
+
if (runtime.runtimeClass !== undefined && runtime.runtimeClass !== "runc")
|
|
504
|
+
fail(`${where}.runtime.runtimeClass`, "must be runc for the selected target.");
|
|
505
|
+
}
|
|
506
|
+
if (runtime.kind === "container" && execution === "oci-kata-qemu-snp") {
|
|
507
|
+
if (runtime.provider !== "forgezero.kata")
|
|
508
|
+
fail(`${where}.runtime.provider`, "must be forgezero.kata for a Kata QEMU SNP target.");
|
|
509
|
+
if (runtime.runtimeClass !== "kata-qemu-snp")
|
|
510
|
+
fail(`${where}.runtime.runtimeClass`, "must be kata-qemu-snp for the selected target.");
|
|
511
|
+
}
|
|
481
512
|
const service = object(row2.service, `${where}.service`);
|
|
482
513
|
exact3(service, ["protocol", "port", "health", "websocket", "maximumConnections"], `${where}.service`);
|
|
483
514
|
if (service.protocol !== "http")
|
|
@@ -509,6 +540,18 @@ function validateComponent(value, where, targetNames, requirements) {
|
|
|
509
540
|
const network = object(row2.network, `${where}.network`);
|
|
510
541
|
if (network.container === undefined)
|
|
511
542
|
fail(`${where}.network.container`, "must select bridge or reviewed host networking.");
|
|
543
|
+
if (selectedTarget.allocation.sharing === "shared") {
|
|
544
|
+
if (object(runtime.security, `${where}.runtime.security`).root !== "read-only")
|
|
545
|
+
fail(`${where}.runtime.security.root`, "must be read-only on a shared compute.");
|
|
546
|
+
if (network.container.mode !== "bridge")
|
|
547
|
+
fail(`${where}.network.container.mode`, "must be bridge on a shared compute.");
|
|
548
|
+
if (Array.isArray(row2.storage) && row2.storage.some((mount) => mount.class !== "ephemeral")) {
|
|
549
|
+
fail(`${where}.storage`, "may contain only size-bounded tmpfs mounts on a shared compute.");
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
if (runtime.kind === "native" && selectedTarget.allocation.sharing !== "exclusive") {
|
|
554
|
+
fail(`${where}.target`, "native applications require an exclusive compute.");
|
|
512
555
|
}
|
|
513
556
|
if (row2.storage !== undefined) {
|
|
514
557
|
if (!Array.isArray(row2.storage) || row2.storage.length > 32)
|
|
@@ -545,6 +588,8 @@ function validateComponent(value, where, targetNames, requirements) {
|
|
|
545
588
|
fail(`${where}.runtime.mode`, "must match the provider runtime mode.");
|
|
546
589
|
if (runtime.mode !== "native")
|
|
547
590
|
fail(`${where}.runtime.mode`, "must be native in deployment plan v1.");
|
|
591
|
+
if (selectedTarget.allocation.execution !== "native")
|
|
592
|
+
fail(`${where}.target`, "must select a native execution target for database components.");
|
|
548
593
|
const topology = object(row2.topology, `${where}.topology`);
|
|
549
594
|
if (topology.mode === "standalone")
|
|
550
595
|
exact3(topology, ["mode"], `${where}.topology`);
|
|
@@ -741,15 +786,20 @@ function compileDeployment(sourceValue) {
|
|
|
741
786
|
if (targetEntries.length < 1 || targetEntries.length > 64)
|
|
742
787
|
fail("deployment.spec.targets", "must contain 1 to 64 targets.");
|
|
743
788
|
const targetNames = new Set(targetEntries.map(([targetName]) => named(targetName, "deployment.spec.targets key")));
|
|
789
|
+
const targetDefinitions = new Map;
|
|
744
790
|
for (const [targetName, value] of targetEntries) {
|
|
745
791
|
const target = object(value, `deployment.spec.targets.${targetName}`);
|
|
746
|
-
exact3(target, ["kind", "selector", "cardinality", "placement"], `deployment.spec.targets.${targetName}`);
|
|
792
|
+
exact3(target, ["kind", "selector", "cardinality", "allocation", "provisioning", "connectivity", "placement"], `deployment.spec.targets.${targetName}`);
|
|
747
793
|
if (target.kind !== "compute")
|
|
748
794
|
fail(`deployment.spec.targets.${targetName}.kind`, "must be compute.");
|
|
749
795
|
const selector = object(target.selector, `deployment.spec.targets.${targetName}.selector`);
|
|
750
|
-
exact3(selector, ["profiles", "confidentialCompute", "labels"], `deployment.spec.targets.${targetName}.selector`);
|
|
796
|
+
exact3(selector, ["profiles", "operatingSystem", "confidentialCompute", "labels"], `deployment.spec.targets.${targetName}.selector`);
|
|
751
797
|
if (!Array.isArray(selector.profiles) || selector.profiles.length < 1 || selector.profiles.length > 32 || selector.profiles.some((profile) => typeof profile !== "string" || !NAME.test(profile)) || new Set(selector.profiles).size !== selector.profiles.length)
|
|
752
798
|
fail(`deployment.spec.targets.${targetName}.selector.profiles`, "is invalid.");
|
|
799
|
+
const operatingSystem = object(selector.operatingSystem, `deployment.spec.targets.${targetName}.selector.operatingSystem`);
|
|
800
|
+
exact3(operatingSystem, ["id", "version", "architecture"], `deployment.spec.targets.${targetName}.selector.operatingSystem`);
|
|
801
|
+
if (operatingSystem.id !== "ubuntu" || !["24.04", "26.04"].includes(String(operatingSystem.version)) || operatingSystem.architecture !== "x64")
|
|
802
|
+
fail(`deployment.spec.targets.${targetName}.selector.operatingSystem`, "is unsupported.");
|
|
753
803
|
if (selector.confidentialCompute !== undefined && !["required", "preferred", "disabled"].includes(String(selector.confidentialCompute)))
|
|
754
804
|
fail(`deployment.spec.targets.${targetName}.selector.confidentialCompute`, "is unsupported.");
|
|
755
805
|
if (selector.labels !== undefined)
|
|
@@ -762,8 +812,108 @@ function compileDeployment(sourceValue) {
|
|
|
762
812
|
integer(cardinality.desired, `deployment.spec.targets.${targetName}.cardinality.desired`, minimum, maximum);
|
|
763
813
|
else
|
|
764
814
|
reference(cardinality.desired, `deployment.spec.targets.${targetName}.cardinality.desired`);
|
|
815
|
+
const allocation = object(target.allocation, `deployment.spec.targets.${targetName}.allocation`);
|
|
816
|
+
exact3(allocation, ["execution", "sharing", "resources", "existing"], `deployment.spec.targets.${targetName}.allocation`);
|
|
817
|
+
if (!["native", "oci-runc", "oci-kata-qemu-snp"].includes(String(allocation.execution)))
|
|
818
|
+
fail(`deployment.spec.targets.${targetName}.allocation.execution`, "is unsupported.");
|
|
819
|
+
if (!["exclusive", "shared"].includes(String(allocation.sharing)))
|
|
820
|
+
fail(`deployment.spec.targets.${targetName}.allocation.sharing`, "is unsupported.");
|
|
821
|
+
if (!["prefer", "require"].includes(String(allocation.existing)))
|
|
822
|
+
fail(`deployment.spec.targets.${targetName}.allocation.existing`, "is unsupported.");
|
|
823
|
+
const capacity = object(allocation.resources, `deployment.spec.targets.${targetName}.allocation.resources`);
|
|
824
|
+
exact3(capacity, ["cpuCores", "memoryMiB", "storageGiB"], `deployment.spec.targets.${targetName}.allocation.resources`);
|
|
825
|
+
const capacityValue = (value2, where, minimumValue, maximumValue) => {
|
|
826
|
+
if (typeof value2 === "number")
|
|
827
|
+
integer(value2, where, minimumValue, maximumValue);
|
|
828
|
+
else
|
|
829
|
+
reference(value2, where);
|
|
830
|
+
};
|
|
831
|
+
capacityValue(capacity.cpuCores, `deployment.spec.targets.${targetName}.allocation.resources.cpuCores`, 1, 1024);
|
|
832
|
+
capacityValue(capacity.memoryMiB, `deployment.spec.targets.${targetName}.allocation.resources.memoryMiB`, 128, 4194304);
|
|
833
|
+
capacityValue(capacity.storageGiB, `deployment.spec.targets.${targetName}.allocation.resources.storageGiB`, 1, 1048576);
|
|
834
|
+
if (allocation.execution === "oci-kata-qemu-snp" && selector.confidentialCompute !== "required")
|
|
835
|
+
fail(`deployment.spec.targets.${targetName}.selector.confidentialCompute`, "must be required for Kata QEMU SNP.");
|
|
836
|
+
if ((allocation.execution === "oci-kata-qemu-snp" || selector.confidentialCompute === "required") && operatingSystem.version !== "26.04")
|
|
837
|
+
fail(`deployment.spec.targets.${targetName}.selector.operatingSystem.version`, "must be Ubuntu 26.04 for SEV-SNP.");
|
|
838
|
+
if (allocation.execution !== "oci-kata-qemu-snp" && selector.confidentialCompute === "required" && allocation.execution !== "native")
|
|
839
|
+
fail(`deployment.spec.targets.${targetName}.allocation.execution`, "cannot satisfy required confidential compute.");
|
|
840
|
+
if (target.provisioning !== undefined) {
|
|
841
|
+
const provisioning = object(target.provisioning, `deployment.spec.targets.${targetName}.provisioning`);
|
|
842
|
+
exact3(provisioning, ["planKey", "regionKey", "imageKey", "environmentKey", "ownership", "metalHostname", "maxMonthlySpendMinor"], `deployment.spec.targets.${targetName}.provisioning`);
|
|
843
|
+
for (const key of ["planKey", "regionKey", "imageKey", "environmentKey"])
|
|
844
|
+
named(provisioning[key], `deployment.spec.targets.${targetName}.provisioning.${key}`);
|
|
845
|
+
if (!["platform", "tenant-metal"].includes(String(provisioning.ownership)) || provisioning.ownership === "tenant-metal" !== Boolean(provisioning.metalHostname))
|
|
846
|
+
fail(`deployment.spec.targets.${targetName}.provisioning.ownership`, "must bind platform capacity or one tenant metal hostname.");
|
|
847
|
+
if (provisioning.metalHostname !== undefined && !/^(?=.{1,253}$)[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?$/.test(String(provisioning.metalHostname)))
|
|
848
|
+
fail(`deployment.spec.targets.${targetName}.provisioning.metalHostname`, "is invalid.");
|
|
849
|
+
if (typeof provisioning.maxMonthlySpendMinor !== "string" || !/^\d{1,18}$/.test(provisioning.maxMonthlySpendMinor))
|
|
850
|
+
fail(`deployment.spec.targets.${targetName}.provisioning.maxMonthlySpendMinor`, "must be a bounded decimal minor-unit amount.");
|
|
851
|
+
}
|
|
852
|
+
if (allocation.existing === "prefer" && target.provisioning === undefined)
|
|
853
|
+
fail(`deployment.spec.targets.${targetName}.provisioning`, "is required when placement may provision after reuse.");
|
|
854
|
+
if (allocation.existing === "require" && target.provisioning !== undefined)
|
|
855
|
+
fail(`deployment.spec.targets.${targetName}.provisioning`, "must be omitted for reuse-only placement.");
|
|
856
|
+
if (target.connectivity !== undefined) {
|
|
857
|
+
const connectivity = object(target.connectivity, `deployment.spec.targets.${targetName}.connectivity`);
|
|
858
|
+
exact3(connectivity, ["private", "public"], `deployment.spec.targets.${targetName}.connectivity`);
|
|
859
|
+
const declaredPrivate = connectivity.private === undefined ? undefined : object(connectivity.private, `deployment.spec.targets.${targetName}.connectivity.private`);
|
|
860
|
+
if (declaredPrivate?.mode === "cloudflare-warp" && allocation.sharing !== "exclusive")
|
|
861
|
+
fail(`deployment.spec.targets.${targetName}.allocation.sharing`, "must be exclusive for a Cloudflare WARP connector.");
|
|
862
|
+
if (connectivity.private !== undefined) {
|
|
863
|
+
const privateNetwork = declaredPrivate;
|
|
864
|
+
if (privateNetwork.mode === "cloudflare-warp") {
|
|
865
|
+
exact3(privateNetwork, ["mode", "credential", "network"], `deployment.spec.targets.${targetName}.connectivity.private`);
|
|
866
|
+
if (!credentials[named(privateNetwork.credential, `deployment.spec.targets.${targetName}.connectivity.private.credential`)])
|
|
867
|
+
fail(`deployment.spec.targets.${targetName}.connectivity.private.credential`, "must name a declared credential.");
|
|
868
|
+
if (typeof privateNetwork.network === "string")
|
|
869
|
+
named(privateNetwork.network, `deployment.spec.targets.${targetName}.connectivity.private.network`);
|
|
870
|
+
else
|
|
871
|
+
reference(privateNetwork.network, `deployment.spec.targets.${targetName}.connectivity.private.network`);
|
|
872
|
+
} else {
|
|
873
|
+
exact3(privateNetwork, ["mode"], `deployment.spec.targets.${targetName}.connectivity.private`);
|
|
874
|
+
if (!["disabled", "private-lan"].includes(String(privateNetwork.mode)))
|
|
875
|
+
fail(`deployment.spec.targets.${targetName}.connectivity.private.mode`, "is unsupported.");
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
if (connectivity.public !== undefined) {
|
|
879
|
+
const publicNetwork = object(connectivity.public, `deployment.spec.targets.${targetName}.connectivity.public`);
|
|
880
|
+
if (publicNetwork.mode === "cloudflare-tunnel") {
|
|
881
|
+
exact3(publicNetwork, ["mode", "credential", "zone", "hostname"], `deployment.spec.targets.${targetName}.connectivity.public`);
|
|
882
|
+
if (!credentials[named(publicNetwork.credential, `deployment.spec.targets.${targetName}.connectivity.public.credential`)])
|
|
883
|
+
fail(`deployment.spec.targets.${targetName}.connectivity.public.credential`, "must name a declared credential.");
|
|
884
|
+
if (typeof publicNetwork.zone === "string") {
|
|
885
|
+
if (!/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(publicNetwork.zone))
|
|
886
|
+
fail(`deployment.spec.targets.${targetName}.connectivity.public.zone`, "is invalid.");
|
|
887
|
+
} else
|
|
888
|
+
reference(publicNetwork.zone, `deployment.spec.targets.${targetName}.connectivity.public.zone`);
|
|
889
|
+
const hostname = object(publicNetwork.hostname, `deployment.spec.targets.${targetName}.connectivity.public.hostname`);
|
|
890
|
+
const value2 = (coordinate, where, pattern) => {
|
|
891
|
+
if (typeof coordinate === "string") {
|
|
892
|
+
if (!pattern.test(coordinate))
|
|
893
|
+
fail(where, "is invalid.");
|
|
894
|
+
} else
|
|
895
|
+
reference(coordinate, where);
|
|
896
|
+
};
|
|
897
|
+
if (hostname.mode === "static") {
|
|
898
|
+
exact3(hostname, ["mode", "label"], `deployment.spec.targets.${targetName}.connectivity.public.hostname`);
|
|
899
|
+
value2(hostname.label, `deployment.spec.targets.${targetName}.connectivity.public.hostname.label`, /^[a-z0-9](?:[a-z0-9-]{0,48}[a-z0-9])?$/);
|
|
900
|
+
} else if (hostname.mode === "indexed") {
|
|
901
|
+
exact3(hostname, ["mode", "prefix", "startAt"], `deployment.spec.targets.${targetName}.connectivity.public.hostname`);
|
|
902
|
+
value2(hostname.prefix, `deployment.spec.targets.${targetName}.connectivity.public.hostname.prefix`, /^[a-z0-9](?:[a-z0-9-]{0,48}[a-z0-9])?$/);
|
|
903
|
+
if (hostname.startAt !== undefined)
|
|
904
|
+
integer(hostname.startAt, `deployment.spec.targets.${targetName}.connectivity.public.hostname.startAt`, 0, 1e6);
|
|
905
|
+
} else
|
|
906
|
+
fail(`deployment.spec.targets.${targetName}.connectivity.public.hostname.mode`, "is unsupported.");
|
|
907
|
+
} else {
|
|
908
|
+
exact3(publicNetwork, ["mode"], `deployment.spec.targets.${targetName}.connectivity.public`);
|
|
909
|
+
if (publicNetwork.mode !== "disabled")
|
|
910
|
+
fail(`deployment.spec.targets.${targetName}.connectivity.public.mode`, "is unsupported.");
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
765
914
|
if (target.placement !== undefined)
|
|
766
915
|
jsonValue(target.placement, `deployment.spec.targets.${targetName}.placement`);
|
|
916
|
+
targetDefinitions.set(targetName, target);
|
|
767
917
|
}
|
|
768
918
|
const requirements = object(spec.requirements ?? {}, "deployment.spec.requirements");
|
|
769
919
|
if (Object.keys(requirements).length > 64)
|
|
@@ -777,13 +927,34 @@ function compileDeployment(sourceValue) {
|
|
|
777
927
|
const key = `${requirementValue.provider}@${requirementValue.contract}:${requirementValue.capability}`;
|
|
778
928
|
requiredProviders.set(key, { provider: requirementValue.provider, contract: requirementValue.contract, capability: requirementValue.capability });
|
|
779
929
|
}
|
|
930
|
+
for (const [targetName, target] of targetDefinitions) {
|
|
931
|
+
if (target.connectivity?.public?.mode === "cloudflare-tunnel" && ![...requirementDefinitions.values()].some(({ capability }) => capability === "network.public-tunnel")) {
|
|
932
|
+
fail(`deployment.spec.targets.${targetName}.connectivity.public`, "requires one network.public-tunnel provider requirement.");
|
|
933
|
+
}
|
|
934
|
+
if (target.connectivity?.private?.mode === "cloudflare-warp" && ![...requirementDefinitions.values()].some(({ capability }) => capability === "network.private")) {
|
|
935
|
+
fail(`deployment.spec.targets.${targetName}.connectivity.private`, "requires one network.private provider requirement.");
|
|
936
|
+
}
|
|
937
|
+
}
|
|
780
938
|
const components = object(spec.components, "deployment.spec.components");
|
|
781
939
|
const componentEntries = Object.entries(components);
|
|
782
940
|
if (componentEntries.length < 1 || componentEntries.length > 128)
|
|
783
941
|
fail("deployment.spec.components", "must contain 1 to 128 components.");
|
|
784
942
|
for (const [componentName, value] of componentEntries) {
|
|
785
943
|
named(componentName, "deployment.spec.components key");
|
|
786
|
-
validateComponent(value, `deployment.spec.components.${componentName}`,
|
|
944
|
+
validateComponent(value, `deployment.spec.components.${componentName}`, targetDefinitions, requirementDefinitions);
|
|
945
|
+
}
|
|
946
|
+
for (const [targetName, target] of targetDefinitions) {
|
|
947
|
+
if (target.connectivity?.public?.mode !== "cloudflare-tunnel")
|
|
948
|
+
continue;
|
|
949
|
+
const publicApplications = componentEntries.filter(([, component2]) => {
|
|
950
|
+
const candidate = component2;
|
|
951
|
+
return candidate.kind === "application" && candidate.target === targetName && candidate.network?.public?.mode === "cloudflare-tunnel";
|
|
952
|
+
});
|
|
953
|
+
if (publicApplications.length !== 1)
|
|
954
|
+
fail(`deployment.spec.targets.${targetName}.connectivity.public`, "requires exactly one application on the target with network.public cloudflare-tunnel.");
|
|
955
|
+
const application = publicApplications[0][1];
|
|
956
|
+
if (application.kind !== "application" || !application.network?.ingress?.stablePort)
|
|
957
|
+
fail(`deployment.spec.components.${publicApplications[0][0]}.network.ingress`, "requires one stablePort for Tunnel ingress.");
|
|
787
958
|
}
|
|
788
959
|
const actionContext = {
|
|
789
960
|
components,
|
|
@@ -825,6 +996,14 @@ function compileDeployment(sourceValue) {
|
|
|
825
996
|
});
|
|
826
997
|
plannedWorkflows.push({ id: workflowName, ...workflowValue.concurrency ? { concurrency: workflowValue.concurrency } : {}, stages: plannedStages });
|
|
827
998
|
}
|
|
999
|
+
const deployWorkflow = plannedWorkflows.find(({ id }) => id === "deploy");
|
|
1000
|
+
for (const [targetName, target] of targetDefinitions) {
|
|
1001
|
+
if (!target.connectivity || target.connectivity.public?.mode !== "cloudflare-tunnel" && target.connectivity.private?.mode !== "cloudflare-warp")
|
|
1002
|
+
continue;
|
|
1003
|
+
const ensures = deployWorkflow?.stages.flatMap(({ steps }) => steps).filter((step2) => step2.uses === "forgezero.network/ensure@1" && step2.with.target === targetName && (step2.scope.kind === "each-target" || step2.scope.kind === "target-batches") && step2.scope.target === targetName) ?? [];
|
|
1004
|
+
if (ensures.length !== 1)
|
|
1005
|
+
fail(`deployment.spec.workflows.deploy`, `must contain exactly one network.ensure step scoped to connected target ${targetName}.`);
|
|
1006
|
+
}
|
|
828
1007
|
const typedSource = source;
|
|
829
1008
|
return {
|
|
830
1009
|
format: DEPLOY_PLAN_FORMAT,
|
|
@@ -936,8 +1115,10 @@ export default defineDeployment({
|
|
|
936
1115
|
inputs: { replicas: input.integer({ minimum: 1, maximum: 32, default: 1 }) },
|
|
937
1116
|
targets: {
|
|
938
1117
|
app: target.compute({
|
|
939
|
-
|
|
940
|
-
|
|
1118
|
+
profiles: ['app'],
|
|
1119
|
+
replicas: { minimum: 1, desired: input.ref('replicas'), maximum: 32 },
|
|
1120
|
+
resources: { cpuCores: 1, memoryMiB: 512, storageGiB: 8 },
|
|
1121
|
+
os: 'ubuntu-24.04', runtime: 'native', isolation: 'standard', reuse: 'require'
|
|
941
1122
|
})
|
|
942
1123
|
},
|
|
943
1124
|
requirements: { bun: providers.bun.require() },
|
package/dist/deploy-file.js
CHANGED
|
@@ -4,22 +4,30 @@ import {
|
|
|
4
4
|
accessSync,
|
|
5
5
|
chmodSync,
|
|
6
6
|
copyFileSync,
|
|
7
|
+
createReadStream,
|
|
7
8
|
existsSync,
|
|
8
9
|
mkdtempSync,
|
|
9
10
|
mkdirSync,
|
|
10
11
|
readFileSync,
|
|
11
12
|
renameSync,
|
|
12
13
|
rmSync,
|
|
14
|
+
statSync,
|
|
13
15
|
symlinkSync,
|
|
14
16
|
unlinkSync,
|
|
15
17
|
writeFileSync
|
|
16
18
|
} from "node:fs";
|
|
17
19
|
import { tmpdir } from "node:os";
|
|
18
|
-
import { join } from "node:path";
|
|
20
|
+
import { dirname, join } from "node:path";
|
|
19
21
|
var PINNED_BUN_VERSION = "1.3.14";
|
|
20
22
|
var BUN_RELEASE_SHA256 = "951ee2aee855f08595aeec6225226a298d3fea83a3dcd6465c09cbccdf7e848f";
|
|
21
23
|
var ARANGO_SHA256 = "b5a9197b4343f2ed554e1ebc1ef8e6529c7c39cde0035cdc311a4747a3355066";
|
|
22
24
|
var CLOUDFLARED_SHA256 = "9d71c677db00134c1bd4144b7783486b654ad281b1ea62b4972098d19f770f17";
|
|
25
|
+
var NERDCTL_VERSION = "2.3.5";
|
|
26
|
+
var BUILDKIT_VERSION = "0.32.2";
|
|
27
|
+
var KATA_CONTAINERS_VERSION = "4.0.0";
|
|
28
|
+
var NERDCTL_SHA256 = "de3206aeb7cbd5f20f5fb1f55c1e3bf2db1be567812a8a3f5e65eba2488347ee";
|
|
29
|
+
var BUILDKIT_SHA256 = "2975d0f651ad96ba8b80b9992ae1f9a964f4408569af5b6dc36544165c3926af";
|
|
30
|
+
var KATA_SHA256 = "2c3b9dfeba355582b40aee462b12916c9740654d0230f696adf719d67b063a8c";
|
|
23
31
|
var OS_CATALOG = [
|
|
24
32
|
{
|
|
25
33
|
id: "ubuntu",
|
|
@@ -49,6 +57,8 @@ var OS_CATALOG = [
|
|
|
49
57
|
var SOFTWARE_CATALOG = [
|
|
50
58
|
{ id: "bun", version: "1.3.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
51
59
|
{ id: "docker", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
60
|
+
{ id: "containerd", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
61
|
+
{ id: "kata-containers", version: "4.0.0", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
52
62
|
{ id: "nginx", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
53
63
|
{ id: "arangodb", version: "3.11.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
54
64
|
{ id: "cloudflared", version: "2026.7.3", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
@@ -57,6 +67,7 @@ var SOFTWARE_CATALOG = [
|
|
|
57
67
|
{ id: "openssh-client", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
58
68
|
{ id: "git", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
59
69
|
{ id: "docker", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
70
|
+
{ id: "containerd", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
60
71
|
{ id: "nginx", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
61
72
|
{ id: "ufw", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
62
73
|
{ id: "openssh-client", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
@@ -65,6 +76,8 @@ var SOFTWARE_CATALOG = [
|
|
|
65
76
|
var UBUNTU_2604_X64 = [
|
|
66
77
|
{ requirement: { id: "bun", version: "1.3.14" } },
|
|
67
78
|
{ requirement: { id: "docker", version: "ubuntu-26.04" } },
|
|
79
|
+
{ requirement: { id: "containerd", version: "ubuntu-26.04" } },
|
|
80
|
+
{ requirement: { id: "kata-containers", version: "4.0.0" } },
|
|
68
81
|
{ requirement: { id: "nginx", version: "ubuntu-26.04" } },
|
|
69
82
|
{ requirement: { id: "arangodb", version: "3.11.14" } },
|
|
70
83
|
{ requirement: { id: "cloudflared", version: "2026.7.3" } },
|
|
@@ -76,6 +89,7 @@ var UBUNTU_2604_X64 = [
|
|
|
76
89
|
var UBUNTU_2404_X64 = [
|
|
77
90
|
{ requirement: { id: "bun", version: "1.3.14" } },
|
|
78
91
|
{ requirement: { id: "docker", version: "ubuntu-24.04" } },
|
|
92
|
+
{ requirement: { id: "containerd", version: "ubuntu-24.04" } },
|
|
79
93
|
{ requirement: { id: "nginx", version: "ubuntu-24.04" } },
|
|
80
94
|
{ requirement: { id: "ufw", version: "ubuntu-24.04" } },
|
|
81
95
|
{ requirement: { id: "openssh-client", version: "ubuntu-24.04" } },
|
|
@@ -92,6 +106,46 @@ var DOCKER_DAEMON_CONFIG = `${JSON.stringify({
|
|
|
92
106
|
"log-opts": { "max-size": "10m", "max-file": "3" }
|
|
93
107
|
}, null, 2)}
|
|
94
108
|
`;
|
|
109
|
+
var CONTAINERD_CONFIG_PATH = "/etc/containerd/config.toml";
|
|
110
|
+
var KATA_SNP_CONFIG_PATH = "/opt/kata/share/defaults/kata-containers/configuration-qemu-snp.toml";
|
|
111
|
+
var KATA_ACTIVE_CONFIG_PATH = "/etc/kata-containers/configuration.toml";
|
|
112
|
+
var CONTAINERD_CONFIG = (kata) => `version = 2
|
|
113
|
+
root = "/var/lib/forgezero/containerd"
|
|
114
|
+
state = "/run/forgezero/containerd"
|
|
115
|
+
|
|
116
|
+
[grpc]
|
|
117
|
+
address = "/run/containerd/containerd.sock"
|
|
118
|
+
|
|
119
|
+
[plugins."io.containerd.grpc.v1.cri".containerd]
|
|
120
|
+
snapshotter = "overlayfs"
|
|
121
|
+
default_runtime_name = "runc"
|
|
122
|
+
|
|
123
|
+
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
|
|
124
|
+
runtime_type = "io.containerd.runc.v2"
|
|
125
|
+
${kata ? `
|
|
126
|
+
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.kata-qemu-snp]
|
|
127
|
+
runtime_type = "io.containerd.kata.v2"
|
|
128
|
+
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.kata-qemu-snp.options]
|
|
129
|
+
ConfigPath = "${KATA_ACTIVE_CONFIG_PATH}"
|
|
130
|
+
` : ""}`;
|
|
131
|
+
var BUILDKIT_UNIT = `[Unit]
|
|
132
|
+
Description=ForgeZero BuildKit daemon
|
|
133
|
+
After=network-online.target containerd.service
|
|
134
|
+
Wants=network-online.target
|
|
135
|
+
Requires=containerd.service
|
|
136
|
+
|
|
137
|
+
[Service]
|
|
138
|
+
Type=notify
|
|
139
|
+
ExecStart=/usr/local/bin/buildkitd --addr unix:///run/buildkit/buildkitd.sock --root /var/lib/forgezero/buildkit
|
|
140
|
+
Restart=on-failure
|
|
141
|
+
RestartSec=2
|
|
142
|
+
NoNewPrivileges=yes
|
|
143
|
+
ProtectSystem=strict
|
|
144
|
+
ReadWritePaths=/var/lib/forgezero/buildkit /run/buildkit
|
|
145
|
+
|
|
146
|
+
[Install]
|
|
147
|
+
WantedBy=multi-user.target
|
|
148
|
+
`;
|
|
95
149
|
var softwareSpawnFailure = (cause, argv) => cause.code === "ENOENT" ? { exitCode: 127, output: `executable is absent: ${argv[0]}` } : undefined;
|
|
96
150
|
var runSoftwareCommand = async (argv, env = {}) => {
|
|
97
151
|
let child;
|
|
@@ -111,14 +165,28 @@ var runSoftwareCommand = async (argv, env = {}) => {
|
|
|
111
165
|
return { exitCode, output: `${stdout}${stderr}` };
|
|
112
166
|
};
|
|
113
167
|
var run = runSoftwareCommand;
|
|
114
|
-
var download = async (url, destination, sha256) => {
|
|
115
|
-
const response = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(
|
|
168
|
+
var download = async (url, destination, sha256, maximumBytes = 512 * 1024 * 1024) => {
|
|
169
|
+
const response = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(30 * 60000) });
|
|
116
170
|
if (!response.ok)
|
|
117
171
|
throw new Error(`download failed with HTTP ${response.status}`);
|
|
118
|
-
const
|
|
119
|
-
if (
|
|
172
|
+
const declared = Number(response.headers.get("content-length") ?? 0);
|
|
173
|
+
if (declared > maximumBytes)
|
|
174
|
+
throw new Error("download exceeds reviewed size bound");
|
|
175
|
+
if (existsSync(destination))
|
|
176
|
+
throw new Error("download destination already exists");
|
|
177
|
+
await Bun.write(destination, response);
|
|
178
|
+
chmodSync(destination, 384);
|
|
179
|
+
if (statSync(destination).size > maximumBytes) {
|
|
180
|
+
rmSync(destination, { force: true });
|
|
181
|
+
throw new Error("download exceeds reviewed size bound");
|
|
182
|
+
}
|
|
183
|
+
const digest = createHash("sha256");
|
|
184
|
+
for await (const chunk of createReadStream(destination))
|
|
185
|
+
digest.update(chunk);
|
|
186
|
+
if (digest.digest("hex") !== sha256) {
|
|
187
|
+
rmSync(destination, { force: true });
|
|
120
188
|
throw new Error("download checksum mismatch");
|
|
121
|
-
|
|
189
|
+
}
|
|
122
190
|
};
|
|
123
191
|
var successful = (result, pattern) => result.exitCode === 0 && (!pattern || pattern.test(result.output));
|
|
124
192
|
var aptInstall = async (name) => {
|
|
@@ -126,6 +194,106 @@ var aptInstall = async (name) => {
|
|
|
126
194
|
const update = await run(["/usr/bin/apt-get", "update", "-qq"], environment);
|
|
127
195
|
return update.exitCode === 0 ? run(["/usr/bin/apt-get", "install", "-y", name], environment) : update;
|
|
128
196
|
};
|
|
197
|
+
var aptInstallMany = async (names) => {
|
|
198
|
+
const environment = { DEBIAN_FRONTEND: "noninteractive" };
|
|
199
|
+
const update = await run(["/usr/bin/apt-get", "update", "-qq"], environment);
|
|
200
|
+
return update.exitCode === 0 ? run(["/usr/bin/apt-get", "install", "-y", ...names], environment) : update;
|
|
201
|
+
};
|
|
202
|
+
var writeOwnedPolicy = (pathValue, content, mode = 420) => {
|
|
203
|
+
if (existsSync(pathValue)) {
|
|
204
|
+
if (readFileSync(pathValue, "utf8") !== content)
|
|
205
|
+
throw new Error(`refusing to overwrite non-ForgeZero policy at ${pathValue}`);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
mkdirSync(dirname(pathValue), { recursive: true, mode: 493 });
|
|
209
|
+
writeFileSync(pathValue, content, { mode, flag: "wx" });
|
|
210
|
+
};
|
|
211
|
+
var installContainerdRuntime = async (directory, osVersion) => {
|
|
212
|
+
const installed = await aptInstallMany(["containerd", "runc", "containernetworking-plugins"]);
|
|
213
|
+
if (installed.exitCode !== 0)
|
|
214
|
+
return installed;
|
|
215
|
+
const nerdctl = join(directory, "nerdctl.tar.gz");
|
|
216
|
+
await download(`https://github.com/containerd/nerdctl/releases/download/v${NERDCTL_VERSION}/nerdctl-${NERDCTL_VERSION}-linux-amd64.tar.gz`, nerdctl, NERDCTL_SHA256);
|
|
217
|
+
const nerdctlExtract = await run(["/usr/bin/tar", "-xzf", nerdctl, "-C", "/usr/local/bin", "nerdctl"]);
|
|
218
|
+
if (nerdctlExtract.exitCode !== 0)
|
|
219
|
+
return nerdctlExtract;
|
|
220
|
+
chmodSync("/usr/local/bin/nerdctl", 493);
|
|
221
|
+
const buildkit = join(directory, "buildkit.tar.gz");
|
|
222
|
+
await download(`https://github.com/moby/buildkit/releases/download/v${BUILDKIT_VERSION}/buildkit-v${BUILDKIT_VERSION}.linux-amd64.tar.gz`, buildkit, BUILDKIT_SHA256);
|
|
223
|
+
const unpacked = join(directory, "buildkit");
|
|
224
|
+
mkdirSync(unpacked, { mode: 448 });
|
|
225
|
+
const buildkitExtract = await run(["/usr/bin/tar", "-xzf", buildkit, "-C", unpacked]);
|
|
226
|
+
if (buildkitExtract.exitCode !== 0)
|
|
227
|
+
return buildkitExtract;
|
|
228
|
+
for (const binary of ["buildctl", "buildkitd"]) {
|
|
229
|
+
copyFileSync(join(unpacked, "bin", binary), `/usr/local/bin/${binary}`);
|
|
230
|
+
chmodSync(`/usr/local/bin/${binary}`, 493);
|
|
231
|
+
}
|
|
232
|
+
mkdirSync("/etc/containerd", { recursive: true, mode: 493 });
|
|
233
|
+
const expected = CONTAINERD_CONFIG(false);
|
|
234
|
+
const kataExpected = CONTAINERD_CONFIG(true);
|
|
235
|
+
if (existsSync(CONTAINERD_CONFIG_PATH) && ![expected, kataExpected].includes(readFileSync(CONTAINERD_CONFIG_PATH, "utf8"))) {
|
|
236
|
+
return { exitCode: 2, output: "refusing to overwrite an existing containerd configuration that differs from the reviewed ForgeZero policy" };
|
|
237
|
+
}
|
|
238
|
+
if (!existsSync(CONTAINERD_CONFIG_PATH))
|
|
239
|
+
writeFileSync(CONTAINERD_CONFIG_PATH, expected, { mode: 420, flag: "wx" });
|
|
240
|
+
writeOwnedPolicy("/etc/systemd/system/forgezero-buildkit.service", BUILDKIT_UNIT);
|
|
241
|
+
mkdirSync("/var/lib/forgezero/containerd", { recursive: true, mode: 448 });
|
|
242
|
+
mkdirSync("/var/lib/forgezero/buildkit", { recursive: true, mode: 448 });
|
|
243
|
+
const reload = await run(["/usr/bin/systemctl", "daemon-reload"]);
|
|
244
|
+
if (reload.exitCode !== 0)
|
|
245
|
+
return reload;
|
|
246
|
+
const enabled = await run(["/usr/bin/systemctl", "enable", "--now", "containerd.service", "forgezero-buildkit.service"]);
|
|
247
|
+
return enabled.exitCode === 0 ? { exitCode: 0, output: `containerd ${osVersion} with nerdctl ${NERDCTL_VERSION} and BuildKit ${BUILDKIT_VERSION}` } : enabled;
|
|
248
|
+
};
|
|
249
|
+
var installKataRuntime = async (directory) => {
|
|
250
|
+
for (const pathValue of ["/dev/kvm", "/dev/sev"])
|
|
251
|
+
if (!existsSync(pathValue))
|
|
252
|
+
return { exitCode: 2, output: `${pathValue} is required for Kata SEV-SNP` };
|
|
253
|
+
for (const parameter of ["/sys/module/kvm_amd/parameters/sev", "/sys/module/kvm_amd/parameters/sev_snp"]) {
|
|
254
|
+
if (!existsSync(parameter) || !/^(1|Y)$/i.test(readFileSync(parameter, "utf8").trim()))
|
|
255
|
+
return { exitCode: 2, output: `${parameter} does not enable SEV-SNP` };
|
|
256
|
+
}
|
|
257
|
+
const dependencies = await aptInstallMany(["zstd"]);
|
|
258
|
+
if (dependencies.exitCode !== 0)
|
|
259
|
+
return dependencies;
|
|
260
|
+
const archive = join(directory, "kata.tar.zst");
|
|
261
|
+
await download(`https://github.com/kata-containers/kata-containers/releases/download/${KATA_CONTAINERS_VERSION}/kata-static-${KATA_CONTAINERS_VERSION}-amd64.tar.zst`, archive, KATA_SHA256, 2200000000);
|
|
262
|
+
const tar = join(directory, "kata.tar");
|
|
263
|
+
const expanded = await run(["/usr/bin/unzstd", "--quiet", archive, "--output", tar]);
|
|
264
|
+
if (expanded.exitCode !== 0)
|
|
265
|
+
return expanded;
|
|
266
|
+
const extracted = await run(["/usr/bin/tar", "-xf", tar, "-C", "/"]);
|
|
267
|
+
if (extracted.exitCode !== 0)
|
|
268
|
+
return extracted;
|
|
269
|
+
if (!existsSync("/opt/kata/bin/containerd-shim-kata-v2") || !existsSync(KATA_SNP_CONFIG_PATH)) {
|
|
270
|
+
return { exitCode: 2, output: "Kata archive does not contain the reviewed QEMU SNP runtime and configuration" };
|
|
271
|
+
}
|
|
272
|
+
const snpConfig = readFileSync(KATA_SNP_CONFIG_PATH, "utf8");
|
|
273
|
+
if (!/^\s*confidential_guest\s*=\s*true\s*$/m.test(snpConfig) || !/^\s*sev_snp_guest\s*=\s*true\s*$/m.test(snpConfig)) {
|
|
274
|
+
return { exitCode: 2, output: "Kata QEMU configuration does not enable confidential SEV-SNP guests" };
|
|
275
|
+
}
|
|
276
|
+
mkdirSync("/etc/kata-containers", { recursive: true, mode: 493 });
|
|
277
|
+
if (existsSync(KATA_ACTIVE_CONFIG_PATH) && readFileSync(KATA_ACTIVE_CONFIG_PATH, "utf8") !== snpConfig) {
|
|
278
|
+
return { exitCode: 2, output: "refusing to overwrite a non-ForgeZero Kata runtime configuration" };
|
|
279
|
+
}
|
|
280
|
+
if (!existsSync(KATA_ACTIVE_CONFIG_PATH))
|
|
281
|
+
copyFileSync(KATA_SNP_CONFIG_PATH, KATA_ACTIVE_CONFIG_PATH);
|
|
282
|
+
chmodSync(KATA_ACTIVE_CONFIG_PATH, 420);
|
|
283
|
+
try {
|
|
284
|
+
unlinkSync("/usr/local/bin/containerd-shim-kata-v2");
|
|
285
|
+
} catch {}
|
|
286
|
+
symlinkSync("/opt/kata/bin/containerd-shim-kata-v2", "/usr/local/bin/containerd-shim-kata-v2");
|
|
287
|
+
const base = CONTAINERD_CONFIG(false);
|
|
288
|
+
const kata = CONTAINERD_CONFIG(true);
|
|
289
|
+
if (!existsSync(CONTAINERD_CONFIG_PATH) || ![base, kata].includes(readFileSync(CONTAINERD_CONFIG_PATH, "utf8"))) {
|
|
290
|
+
return { exitCode: 2, output: "containerd must use the reviewed ForgeZero policy before Kata is installed" };
|
|
291
|
+
}
|
|
292
|
+
if (readFileSync(CONTAINERD_CONFIG_PATH, "utf8") !== kata)
|
|
293
|
+
writeFileSync(CONTAINERD_CONFIG_PATH, kata, { mode: 420 });
|
|
294
|
+
const restarted = await run(["/usr/bin/systemctl", "restart", "containerd.service"]);
|
|
295
|
+
return restarted.exitCode === 0 ? { exitCode: 0, output: `Kata Containers ${KATA_CONTAINERS_VERSION} QEMU SNP` } : restarted;
|
|
296
|
+
};
|
|
129
297
|
async function executeSoftwareOperation(operation) {
|
|
130
298
|
const { software, version } = operation;
|
|
131
299
|
if (![...UBUNTU_2604_X64, ...UBUNTU_2404_X64].some(({ requirement }) => requirement.id === software && requirement.version === version)) {
|
|
@@ -143,6 +311,26 @@ async function executeSoftwareOperation(operation) {
|
|
|
143
311
|
const active = await run(["/usr/bin/systemctl", "is-active", "--quiet", "docker.service"]);
|
|
144
312
|
return active.exitCode === 0 ? { exitCode: 0, output: binary.output } : active;
|
|
145
313
|
}
|
|
314
|
+
if (software === "containerd") {
|
|
315
|
+
const [containerd, nerdctl, buildkit, active, builder] = await Promise.all([
|
|
316
|
+
run(["/usr/bin/containerd", "--version"]),
|
|
317
|
+
run(["/usr/local/bin/nerdctl", "--version"]),
|
|
318
|
+
run(["/usr/local/bin/buildkitd", "--version"]),
|
|
319
|
+
run(["/usr/bin/systemctl", "is-active", "--quiet", "containerd.service"]),
|
|
320
|
+
run(["/usr/bin/systemctl", "is-active", "--quiet", "forgezero-buildkit.service"])
|
|
321
|
+
]);
|
|
322
|
+
const policy = existsSync(CONTAINERD_CONFIG_PATH) ? readFileSync(CONTAINERD_CONFIG_PATH, "utf8") : "";
|
|
323
|
+
const validPolicy = policy === CONTAINERD_CONFIG(false) || policy === CONTAINERD_CONFIG(true);
|
|
324
|
+
return successful(containerd, /containerd/) && successful(nerdctl, new RegExp(NERDCTL_VERSION.replaceAll(".", "\\."))) && successful(buildkit, new RegExp(BUILDKIT_VERSION.replaceAll(".", "\\."))) && active.exitCode === 0 && builder.exitCode === 0 && validPolicy ? { exitCode: 0, output: `${containerd.output}${nerdctl.output}${buildkit.output}` } : { exitCode: 1, output: "containerd, nerdctl, BuildKit, or ForgeZero storage/runtime policy is unavailable" };
|
|
325
|
+
}
|
|
326
|
+
if (software === "kata-containers") {
|
|
327
|
+
const shim = await run(["/opt/kata/bin/containerd-shim-kata-v2", "--version"]);
|
|
328
|
+
const policy = existsSync(CONTAINERD_CONFIG_PATH) ? readFileSync(CONTAINERD_CONFIG_PATH, "utf8") : "";
|
|
329
|
+
const activeConfig = existsSync(KATA_ACTIVE_CONFIG_PATH) ? readFileSync(KATA_ACTIVE_CONFIG_PATH, "utf8") : "";
|
|
330
|
+
const host = ["/dev/kvm", "/dev/sev", KATA_SNP_CONFIG_PATH, KATA_ACTIVE_CONFIG_PATH].every(existsSync) && ["/sys/module/kvm_amd/parameters/sev", "/sys/module/kvm_amd/parameters/sev_snp"].every((parameter) => existsSync(parameter) && /^(1|Y)$/i.test(readFileSync(parameter, "utf8").trim()));
|
|
331
|
+
const active = await run(["/usr/bin/systemctl", "is-active", "--quiet", "containerd.service"]);
|
|
332
|
+
return successful(shim, /kata.*4\.0\.0/i) && policy === CONTAINERD_CONFIG(true) && activeConfig === (existsSync(KATA_SNP_CONFIG_PATH) ? readFileSync(KATA_SNP_CONFIG_PATH, "utf8") : "") && host && active.exitCode === 0 ? { exitCode: 0, output: shim.output } : { exitCode: 1, output: "Kata QEMU SNP runtime, host capability, or containerd policy is unavailable" };
|
|
333
|
+
}
|
|
146
334
|
if (software === "nginx") {
|
|
147
335
|
const binary = await run(["/usr/sbin/nginx", "-v"]);
|
|
148
336
|
return binary.exitCode === 0 ? run(["/usr/bin/systemctl", "is-active", "--quiet", "nginx.service"]) : binary;
|
|
@@ -194,6 +382,17 @@ async function executeSoftwareOperation(operation) {
|
|
|
194
382
|
}
|
|
195
383
|
const directory = mkdtempSync(join(tmpdir(), "forgezero-software-"));
|
|
196
384
|
try {
|
|
385
|
+
if (software === "containerd")
|
|
386
|
+
return installContainerdRuntime(directory, version === "ubuntu-24.04" ? "24.04" : "26.04");
|
|
387
|
+
if (software === "kata-containers") {
|
|
388
|
+
const containerd = await executeSoftwareOperation({ kind: "check", software: "containerd", version: "ubuntu-26.04" });
|
|
389
|
+
if (containerd.exitCode !== 0) {
|
|
390
|
+
const installed2 = await installContainerdRuntime(directory, "26.04");
|
|
391
|
+
if (installed2.exitCode !== 0)
|
|
392
|
+
return installed2;
|
|
393
|
+
}
|
|
394
|
+
return installKataRuntime(directory);
|
|
395
|
+
}
|
|
197
396
|
if (software === "cloudflare-warp") {
|
|
198
397
|
const key = join(directory, "cloudflare-warp-key.gpg");
|
|
199
398
|
await download("https://pkg.cloudflareclient.com/pubkey.gpg", key, "0f37fc298c98e88ee3c0ee68c95b69f1dba9eb477abe3167e13982105911264d");
|
|
@@ -275,7 +474,7 @@ function validateSoftwareRequirements(value, _options = {}) {
|
|
|
275
474
|
if (Object.keys(row).some((key) => key !== "id" && key !== "version")) {
|
|
276
475
|
throw new Error("software requirement contains an unknown field");
|
|
277
476
|
}
|
|
278
|
-
if (!["bun", "docker", "nginx", "arangodb", "cloudflared", "cloudflare-warp", "ufw", "openssh-client", "git"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
|
|
477
|
+
if (!["bun", "docker", "containerd", "kata-containers", "nginx", "arangodb", "cloudflared", "cloudflare-warp", "ufw", "openssh-client", "git"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
|
|
279
478
|
throw new Error("software requirement coordinate is invalid");
|
|
280
479
|
}
|
|
281
480
|
const requirement = { id: row.id, version: row.version };
|