@forgezero/agent 0.1.87 → 0.1.89
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 +23 -9
- package/dist/agent-handover.d.ts +7 -1
- package/dist/agent-heartbeat.js +25 -9
- package/dist/agent-update-helper.d.ts +1 -0
- package/dist/agent-update-helper.js +16 -7
- package/dist/bootstrap.d.ts +8 -1
- package/dist/bootstrap.js +228 -54
- package/dist/capacity-calibration.d.ts +35 -5
- package/dist/capacity-calibration.js +66 -22
- package/dist/cli/agent-install.d.ts +3 -0
- package/dist/community-rehearsal-host.js +7 -2
- package/dist/control.d.ts +3 -1
- package/dist/definition.js +83 -28
- package/dist/deploy-file.js +83 -28
- package/dist/deployment.d.ts +5 -0
- package/dist/egress-policy.d.ts +4 -0
- package/dist/fz-agent.js +440 -158
- package/dist/fz.js +413 -129
- package/dist/host-maintenance.js +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/metal-bootstrap.js +1 -1
- package/dist/metal-helper-socket.js +7 -2
- package/dist/metal-provision.js +7 -2
- package/dist/operator-bootstrap.d.ts +12 -0
- package/dist/operator-bootstrap.js +372 -64
- package/dist/platform-bootstrap-runtime.d.ts +1 -0
- package/dist/platform-bootstrap-runtime.js +47 -8
- package/dist/platform-fleet-verification.js +278 -94
- package/dist/platform-genesis.js +7 -2
- package/dist/provision.d.ts +5 -1
- package/dist/provision.js +201 -78
- package/dist/recovery-host.js +1 -1
- package/dist/software-helper.js +144 -48
- package/dist/software.js +7 -2
- package/dist/ubuntu.js +7 -2
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/schema/deploy-v3.json +5 -2
package/dist/fz.js
CHANGED
|
@@ -4810,9 +4810,8 @@ async function spawnWith(command, env, report = () => {}, options = {}) {
|
|
|
4810
4810
|
}
|
|
4811
4811
|
|
|
4812
4812
|
// src/cli/index.ts
|
|
4813
|
-
import {
|
|
4813
|
+
import { existsSync as existsSync13, lstatSync as lstatSync10, mkdirSync as mkdirSync13, readFileSync as readFileSync16, writeFileSync as writeFileSync13 } from "fs";
|
|
4814
4814
|
import { basename as basename3, dirname as dirname14, isAbsolute as isAbsolute7, join as join13, resolve as resolve12 } from "path";
|
|
4815
|
-
import { randomBytes as randomBytes10 } from "crypto";
|
|
4816
4815
|
|
|
4817
4816
|
// src/process-input.ts
|
|
4818
4817
|
async function writeAndCloseProcessInput(input, value) {
|
|
@@ -4822,8 +4821,7 @@ async function writeAndCloseProcessInput(input, value) {
|
|
|
4822
4821
|
|
|
4823
4822
|
// src/cli/index.ts
|
|
4824
4823
|
init_dist();
|
|
4825
|
-
import {
|
|
4826
|
-
import { hostname } from "os";
|
|
4824
|
+
import { homedir as homedir2, hostname } from "os";
|
|
4827
4825
|
|
|
4828
4826
|
// src/agent-update.ts
|
|
4829
4827
|
var DEFAULT_AGENT_RELEASE_ROOT = "/opt/forgezero/agent";
|
|
@@ -4842,7 +4840,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
|
|
|
4842
4840
|
var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
|
|
4843
4841
|
|
|
4844
4842
|
// src/version.ts
|
|
4845
|
-
var VERSION2 = "0.1.
|
|
4843
|
+
var VERSION2 = "0.1.89";
|
|
4846
4844
|
|
|
4847
4845
|
// src/software.ts
|
|
4848
4846
|
var PINNED_BUN_VERSION = "1.3.14";
|
|
@@ -4944,9 +4942,12 @@ function validateCapacityCalibrationOptions(options) {
|
|
|
4944
4942
|
const requestsPerWorker = options.requestsPerWorker ?? 8;
|
|
4945
4943
|
const maxP95Ms = options.maxP95Ms ?? 250;
|
|
4946
4944
|
const maxErrorRate = options.maxErrorRate ?? 0.01;
|
|
4947
|
-
const
|
|
4945
|
+
const safetyRatio = options.safetyRatio ?? 0.8;
|
|
4948
4946
|
const requestTimeoutMs = options.requestTimeoutMs ?? 5000;
|
|
4949
|
-
|
|
4947
|
+
const minimumStageDurationMs = options.minimumStageDurationMs ?? 5000;
|
|
4948
|
+
const maxCpuUtilizationPercent = options.maxCpuUtilizationPercent ?? 90;
|
|
4949
|
+
const maxMemoryUtilizationPercent = options.maxMemoryUtilizationPercent ?? 90;
|
|
4950
|
+
if (!Number.isSafeInteger(maxConcurrency) || maxConcurrency < 1 || maxConcurrency > 4096 || !Number.isSafeInteger(requestsPerWorker) || requestsPerWorker < 2 || requestsPerWorker > 100 || !Number.isFinite(maxP95Ms) || maxP95Ms < 1 || !Number.isFinite(maxErrorRate) || maxErrorRate < 0 || maxErrorRate > 0.2 || !Number.isFinite(safetyRatio) || safetyRatio < 0.25 || safetyRatio > 0.95 || !Number.isSafeInteger(requestTimeoutMs) || requestTimeoutMs < 100 || requestTimeoutMs > 30000 || !Number.isSafeInteger(minimumStageDurationMs) || minimumStageDurationMs < 100 || minimumStageDurationMs > 60000 || !Number.isFinite(maxCpuUtilizationPercent) || maxCpuUtilizationPercent < 25 || maxCpuUtilizationPercent > 100 || !Number.isFinite(maxMemoryUtilizationPercent) || maxMemoryUtilizationPercent < 25 || maxMemoryUtilizationPercent > 100) {
|
|
4950
4951
|
throw new Error("Capacity calibration bounds are invalid.");
|
|
4951
4952
|
}
|
|
4952
4953
|
return {
|
|
@@ -4955,8 +4956,11 @@ function validateCapacityCalibrationOptions(options) {
|
|
|
4955
4956
|
requestsPerWorker,
|
|
4956
4957
|
maxP95Ms,
|
|
4957
4958
|
maxErrorRate,
|
|
4958
|
-
|
|
4959
|
-
requestTimeoutMs
|
|
4959
|
+
safetyRatio,
|
|
4960
|
+
requestTimeoutMs,
|
|
4961
|
+
minimumStageDurationMs,
|
|
4962
|
+
maxCpuUtilizationPercent,
|
|
4963
|
+
maxMemoryUtilizationPercent
|
|
4960
4964
|
};
|
|
4961
4965
|
}
|
|
4962
4966
|
|
|
@@ -5031,8 +5035,11 @@ function capacityCalibration(value, where) {
|
|
|
5031
5035
|
"requestsPerWorker",
|
|
5032
5036
|
"maxP95Ms",
|
|
5033
5037
|
"maxErrorRate",
|
|
5034
|
-
"
|
|
5035
|
-
"requestTimeoutMs"
|
|
5038
|
+
"safetyRatio",
|
|
5039
|
+
"requestTimeoutMs",
|
|
5040
|
+
"minimumStageDurationMs",
|
|
5041
|
+
"maxCpuUtilizationPercent",
|
|
5042
|
+
"maxMemoryUtilizationPercent"
|
|
5036
5043
|
], where);
|
|
5037
5044
|
const endpoint = text(calibration.endpoint, `${where}.endpoint`);
|
|
5038
5045
|
try {
|
|
@@ -5056,8 +5063,11 @@ function capacityCalibration(value, where) {
|
|
|
5056
5063
|
"requestsPerWorker",
|
|
5057
5064
|
"maxP95Ms",
|
|
5058
5065
|
"maxErrorRate",
|
|
5059
|
-
"
|
|
5060
|
-
"requestTimeoutMs"
|
|
5066
|
+
"safetyRatio",
|
|
5067
|
+
"requestTimeoutMs",
|
|
5068
|
+
"minimumStageDurationMs",
|
|
5069
|
+
"maxCpuUtilizationPercent",
|
|
5070
|
+
"maxMemoryUtilizationPercent"
|
|
5061
5071
|
].flatMap((name) => {
|
|
5062
5072
|
const found = optionalNumber(name);
|
|
5063
5073
|
return found === undefined ? [] : [[name, found]];
|
|
@@ -5479,6 +5489,7 @@ function agentEgressUnit(options) {
|
|
|
5479
5489
|
}
|
|
5480
5490
|
const deploymentEnabled = Boolean(options.repository || bootstrapBundleEnabled || options.pullDeployments);
|
|
5481
5491
|
const runnerLoopbackPorts = normalizeEgressTcpPorts(options.runnerLoopbackPorts ?? []);
|
|
5492
|
+
const agentLoopbackPorts = normalizeEgressTcpPorts(options.agentLoopbackPorts ?? []);
|
|
5482
5493
|
const runnerPublicTcpPorts = normalizeEgressTcpPorts(options.runnerPublicTcpPorts ?? DEFAULT_RUNNER_PUBLIC_TCP_PORTS);
|
|
5483
5494
|
if (deploymentEnabled && runnerPublicTcpPorts.length < 1) {
|
|
5484
5495
|
throw new Error("deployed project runner needs at least one vetted public TCP port");
|
|
@@ -5486,8 +5497,10 @@ function agentEgressUnit(options) {
|
|
|
5486
5497
|
if (deploymentEnabled)
|
|
5487
5498
|
systemdAgentEgressDirectives(runnerLoopbackPorts);
|
|
5488
5499
|
const users = [user, ...deploymentEnabled ? [DEPLOYMENT_RUNNER_USER] : []];
|
|
5489
|
-
const
|
|
5490
|
-
const
|
|
5500
|
+
const portGrantEnabled = deploymentEnabled || agentLoopbackPorts.length > 0;
|
|
5501
|
+
const restrictedUser = deploymentEnabled ? DEPLOYMENT_RUNNER_USER : user;
|
|
5502
|
+
const portGrant = portGrantEnabled ? ` --loopback-user=${restrictedUser}` + (deploymentEnabled ? runnerLoopbackPorts.map((port) => ` --loopback-tcp-port=${port}`).join("") : "") + agentLoopbackPorts.map((port) => ` --shared-loopback-tcp-port=${port}`).join("") + (deploymentEnabled ? runnerPublicTcpPorts.map((port) => ` --public-tcp-port=${port}`).join("") : "") : "";
|
|
5503
|
+
const policyProof = `ExecStartPost=${bin} egress-policy-check ${users.map((name) => `--user=${name}`).join(" ")}${portGrant}`;
|
|
5491
5504
|
return `[Unit]
|
|
5492
5505
|
Description=ForgeZero Agent host egress policy
|
|
5493
5506
|
Documentation=https://www.forgezero.net/docs/agent
|
|
@@ -5500,7 +5513,7 @@ Type=notify
|
|
|
5500
5513
|
NotifyAccess=all
|
|
5501
5514
|
User=root
|
|
5502
5515
|
Group=root
|
|
5503
|
-
ExecStart=${bin} egress-policy ${users.map((name) => `--user=${name}`).join(" ")}${
|
|
5516
|
+
ExecStart=${bin} egress-policy ${users.map((name) => `--user=${name}`).join(" ")}${portGrant}
|
|
5504
5517
|
${policyProof}
|
|
5505
5518
|
Restart=on-failure
|
|
5506
5519
|
RestartSec=2
|
|
@@ -5784,7 +5797,7 @@ function agentEnrolmentUnit(options) {
|
|
|
5784
5797
|
Requires=forgezero-agent-egress.service
|
|
5785
5798
|
BindsTo=forgezero-agent-egress.service
|
|
5786
5799
|
` : "";
|
|
5787
|
-
const egressDirectives = options.enforceEgress ? systemdAgentEgressDirectives() : "";
|
|
5800
|
+
const egressDirectives = options.enforceEgress ? systemdAgentEgressDirectives(options.agentLoopbackPorts ?? []) : "";
|
|
5788
5801
|
return `[Unit]
|
|
5789
5802
|
Description=Bind this machine to its ForgeZero compute
|
|
5790
5803
|
After=network-online.target
|
|
@@ -5821,7 +5834,7 @@ WantedBy=multi-user.target
|
|
|
5821
5834
|
function deploymentRunnerUnit(options) {
|
|
5822
5835
|
const bin = options.binPath ?? "fz-agent";
|
|
5823
5836
|
const root = options.deployRoot ?? "/opt/forgezero";
|
|
5824
|
-
const agentUser = options.user ?? "forgezero
|
|
5837
|
+
const agentUser = options.user ?? "forgezero";
|
|
5825
5838
|
const egressDependency = options.enforceEgress ? `After=forgezero-agent-egress.service
|
|
5826
5839
|
Requires=forgezero-agent-egress.service
|
|
5827
5840
|
BindsTo=forgezero-agent-egress.service
|
|
@@ -5862,7 +5875,7 @@ RestrictRealtime=true
|
|
|
5862
5875
|
MemoryDenyWriteExecute=true
|
|
5863
5876
|
LockPersonality=true
|
|
5864
5877
|
${egressDirectives}
|
|
5865
|
-
ReadWritePaths=${root}/releases ${root}/runner-home
|
|
5878
|
+
ReadWritePaths=${root}/releases ${root}/runner-home ${root}/slots /etc/nginx/conf.d /var/log/nginx
|
|
5866
5879
|
|
|
5867
5880
|
[Install]
|
|
5868
5881
|
WantedBy=multi-user.target
|
|
@@ -5997,7 +6010,7 @@ function agentUnit(options) {
|
|
|
5997
6010
|
const supplementaryGroups = [
|
|
5998
6011
|
AGENT_UPDATE_GROUP,
|
|
5999
6012
|
deploymentEnabled ? DEPLOYMENT_GROUP : null,
|
|
6000
|
-
|
|
6013
|
+
SOFTWARE_HELPER_GROUP,
|
|
6001
6014
|
lifecycleEnabled ? LIFECYCLE_GROUP : null
|
|
6002
6015
|
].filter((value) => value !== null);
|
|
6003
6016
|
const deploymentGroup = supplementaryGroups.length > 0 ? `SupplementaryGroups=${supplementaryGroups.join(" ")}` : "";
|
|
@@ -6006,7 +6019,7 @@ function agentUnit(options) {
|
|
|
6006
6019
|
"forgezero-agent-update-helper.service",
|
|
6007
6020
|
options.enforceEgress ? "forgezero-agent-egress.service" : null,
|
|
6008
6021
|
deploymentEnabled ? "forgezero-deploy-runner.service" : null,
|
|
6009
|
-
|
|
6022
|
+
"forgezero-software-helper.service",
|
|
6010
6023
|
lifecycleEnabled ? "forgezero-lifecycle-helper.service" : null,
|
|
6011
6024
|
warpEnabled ? "warp-svc.service" : null,
|
|
6012
6025
|
enrolmentEnabled ? "forgezero-agent-enrol.service" : null
|
|
@@ -6015,7 +6028,7 @@ function agentUnit(options) {
|
|
|
6015
6028
|
"forgezero-agent-update-helper.service",
|
|
6016
6029
|
options.enforceEgress ? "forgezero-agent-egress.service" : null,
|
|
6017
6030
|
deploymentEnabled ? "forgezero-deploy-runner.service" : null,
|
|
6018
|
-
|
|
6031
|
+
"forgezero-software-helper.service",
|
|
6019
6032
|
lifecycleEnabled ? "forgezero-lifecycle-helper.service" : null,
|
|
6020
6033
|
warpEnabled ? "warp-svc.service" : null,
|
|
6021
6034
|
enrolmentEnabled ? "forgezero-agent-enrol.service" : null
|
|
@@ -6030,9 +6043,9 @@ function agentUnit(options) {
|
|
|
6030
6043
|
const snpDevice = options.mode === "attested" ? `DevicePolicy=closed
|
|
6031
6044
|
DeviceAllow=/dev/sev-guest rw` : "";
|
|
6032
6045
|
const snpPrepare = options.mode === "attested" ? `ExecStartPre=+/bin/chgrp ${VAULT_GROUP} /dev/sev-guest
|
|
6033
|
-
ExecStartPre=+/bin/chmod
|
|
6046
|
+
ExecStartPre=+/bin/chmod 0660 /dev/sev-guest
|
|
6034
6047
|
` : "";
|
|
6035
|
-
const egressDirectives = options.enforceEgress ? systemdAgentEgressDirectives() : "";
|
|
6048
|
+
const egressDirectives = options.enforceEgress ? systemdAgentEgressDirectives(options.agentLoopbackPorts ?? []) : "";
|
|
6036
6049
|
return `[Unit]
|
|
6037
6050
|
Description=ForgeZero node agent (${options.mode})
|
|
6038
6051
|
Documentation=https://www.forgezero.net/docs/agent
|
|
@@ -6163,6 +6176,7 @@ function planProvision(options) {
|
|
|
6163
6176
|
const deployRoot = options.deployRoot ?? "/opt/forgezero";
|
|
6164
6177
|
const deploymentEnabled = Boolean(options.repository || options.pullDeployments);
|
|
6165
6178
|
const runnerLoopbackPorts = normalizeEgressTcpPorts(options.runnerLoopbackPorts ?? []);
|
|
6179
|
+
const agentLoopbackPorts = normalizeEgressTcpPorts(options.agentLoopbackPorts ?? []);
|
|
6166
6180
|
const runnerPublicTcpPorts = normalizeEgressTcpPorts(options.runnerPublicTcpPorts ?? DEFAULT_RUNNER_PUBLIC_TCP_PORTS);
|
|
6167
6181
|
const lifecycleEnabled = Boolean(options.pullMigrations && options.lifecycleProfilePath);
|
|
6168
6182
|
if (Boolean(options.pullMigrations) !== Boolean(options.lifecycleProfilePath)) {
|
|
@@ -6211,8 +6225,9 @@ function planProvision(options) {
|
|
|
6211
6225
|
const enabledUnits = [
|
|
6212
6226
|
"forgezero-agent.socket",
|
|
6213
6227
|
"forgezero-agent-update-helper.service",
|
|
6228
|
+
"forgezero-software-helper.service",
|
|
6214
6229
|
...options.enforceEgress ? ["forgezero-agent-egress.service"] : [],
|
|
6215
|
-
...deploymentEnabled ? ["forgezero-deploy-runner.service"
|
|
6230
|
+
...deploymentEnabled ? ["forgezero-deploy-runner.service"] : [],
|
|
6216
6231
|
...lifecycleEnabled ? ["forgezero-lifecycle-helper.service"] : [],
|
|
6217
6232
|
...warpEnabled ? ["forgezero-warp-config.service", "warp-svc.service"] : [],
|
|
6218
6233
|
...enrolmentEnabled ? ["forgezero-agent-enrol.service"] : [],
|
|
@@ -6220,8 +6235,9 @@ function planProvision(options) {
|
|
|
6220
6235
|
];
|
|
6221
6236
|
const restartedUnits = [
|
|
6222
6237
|
"forgezero-agent-update-helper.service",
|
|
6238
|
+
"forgezero-software-helper.service",
|
|
6223
6239
|
...options.enforceEgress ? ["forgezero-agent-egress.service"] : [],
|
|
6224
|
-
...deploymentEnabled ? ["forgezero-deploy-runner.service"
|
|
6240
|
+
...deploymentEnabled ? ["forgezero-deploy-runner.service"] : [],
|
|
6225
6241
|
...lifecycleEnabled ? ["forgezero-lifecycle-helper.service"] : [],
|
|
6226
6242
|
...warpEnabled ? ["forgezero-warp-config.service", "warp-svc.service"] : [],
|
|
6227
6243
|
...enrolmentEnabled ? ["forgezero-agent-enrol.service"] : []
|
|
@@ -6245,9 +6261,9 @@ function planProvision(options) {
|
|
|
6245
6261
|
...options.enforceEgress ? [
|
|
6246
6262
|
{ path: AGENT_EGRESS_UNIT_PATH, unit: agentEgressUnit(options) }
|
|
6247
6263
|
] : [],
|
|
6264
|
+
{ path: SOFTWARE_HELPER_UNIT_PATH, unit: softwareHelperUnit(options) },
|
|
6248
6265
|
...deploymentEnabled ? [
|
|
6249
|
-
{ path: DEPLOYMENT_RUNNER_UNIT_PATH, unit: deploymentRunnerUnit(options) }
|
|
6250
|
-
{ path: SOFTWARE_HELPER_UNIT_PATH, unit: softwareHelperUnit(options) }
|
|
6266
|
+
{ path: DEPLOYMENT_RUNNER_UNIT_PATH, unit: deploymentRunnerUnit(options) }
|
|
6251
6267
|
] : [],
|
|
6252
6268
|
...enrolmentEnabled ? [
|
|
6253
6269
|
{ path: ENROLMENT_UNIT_PATH, unit: agentEnrolmentUnit(options) }
|
|
@@ -6280,24 +6296,29 @@ function planProvision(options) {
|
|
|
6280
6296
|
step("Agent update helper access group", { kind: "commands", commands: [{ argv: ["/usr/sbin/groupadd", "--system", AGENT_UPDATE_GROUP], acceptedExitCodes: [0, 9] }] }),
|
|
6281
6297
|
...sourceBinPath && binPath ? [step("root-owned agent runtime", { kind: "install-runtime", source: sourceBinPath, binary: binPath, version: VERSION2 })] : [],
|
|
6282
6298
|
...warpEnabled ? [step("Cloudflare One client for Ubuntu 26.04", { kind: "install-warp" })] : [],
|
|
6283
|
-
|
|
6284
|
-
{ argv: ["/usr/sbin/groupadd", "--system", DEPLOYMENT_GROUP], acceptedExitCodes: [0, 9] },
|
|
6299
|
+
step("software strategy helper group", { kind: "commands", commands: [
|
|
6285
6300
|
{ argv: ["/usr/sbin/groupadd", "--system", SOFTWARE_HELPER_GROUP], acceptedExitCodes: [0, 9] }
|
|
6301
|
+
] }),
|
|
6302
|
+
...deploymentEnabled ? [step("deployment isolation group", { kind: "commands", commands: [
|
|
6303
|
+
{ argv: ["/usr/sbin/groupadd", "--system", DEPLOYMENT_GROUP], acceptedExitCodes: [0, 9] }
|
|
6286
6304
|
] })] : [],
|
|
6287
6305
|
...lifecycleEnabled ? [step("lifecycle helper access group", { kind: "commands", commands: [{ argv: ["/usr/sbin/groupadd", "--system", LIFECYCLE_GROUP], acceptedExitCodes: [0, 9] }] })] : [],
|
|
6288
6306
|
step("service account", { kind: "commands", commands: [{ argv: ["/usr/sbin/useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", user], acceptedExitCodes: [0, 9] }] }),
|
|
6289
6307
|
step("bind service account to vault group", { kind: "commands", commands: [{ argv: ["/usr/sbin/usermod", "-g", VAULT_GROUP, user] }] }),
|
|
6290
6308
|
step("grant verified Agent update access", { kind: "commands", commands: [{ argv: ["/usr/sbin/usermod", "-a", "-G", AGENT_UPDATE_GROUP, user] }] }),
|
|
6309
|
+
step("grant software helper socket access", { kind: "commands", commands: [
|
|
6310
|
+
{ argv: ["/usr/sbin/usermod", "-a", "-G", SOFTWARE_HELPER_GROUP, user] }
|
|
6311
|
+
] }),
|
|
6291
6312
|
...lifecycleEnabled ? [step("grant lifecycle helper socket access", { kind: "commands", commands: [{ argv: ["/usr/sbin/usermod", "-a", "-G", LIFECYCLE_GROUP, user] }] })] : [],
|
|
6292
6313
|
...deploymentEnabled ? [step("credential-free deployment account", { kind: "commands", commands: [
|
|
6293
6314
|
{ argv: ["/usr/sbin/useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", "--gid", DEPLOYMENT_GROUP, DEPLOYMENT_RUNNER_USER], acceptedExitCodes: [0, 9] },
|
|
6294
6315
|
{ argv: ["/usr/sbin/useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", "--gid", VAULT_GROUP, APPLICATION_RUNTIME_USER], acceptedExitCodes: [0, 9] },
|
|
6295
6316
|
{ argv: ["/usr/sbin/usermod", "-a", "-G", DEPLOYMENT_GROUP, APPLICATION_RUNTIME_USER] },
|
|
6296
|
-
{ argv: ["/usr/sbin/usermod", "-a", "-G",
|
|
6317
|
+
{ argv: ["/usr/sbin/usermod", "-a", "-G", DEPLOYMENT_GROUP, user] }
|
|
6297
6318
|
] })] : [],
|
|
6298
6319
|
step("credential and state directories", { kind: "directories", directories: [
|
|
6299
6320
|
{ path: credentialDir, mode: 448, owner: "root", group: "root" },
|
|
6300
|
-
{ path: "/var/lib/forgezero", mode: 488, owner: "root", group:
|
|
6321
|
+
{ path: "/var/lib/forgezero", mode: 488, owner: "root", group: VAULT_GROUP }
|
|
6301
6322
|
] }),
|
|
6302
6323
|
step("encrypted node identity", { kind: "ensure-seed", credential: seedCredentialPath }),
|
|
6303
6324
|
...options.generateGitIdentity && gitCredentialPath && gitPublicKeyPath ? [
|
|
@@ -6348,19 +6369,26 @@ function planProvision(options) {
|
|
|
6348
6369
|
{ argv: ["/usr/bin/systemctl", "enable", ...enabledUnits] },
|
|
6349
6370
|
{ argv: ["/usr/bin/systemctl", "reset-failed", "forgezero-agent.service"], acceptedExitCodes: [0, 1] },
|
|
6350
6371
|
...restartedUnits.length ? [{ argv: ["/usr/bin/systemctl", "restart", ...restartedUnits] }] : [],
|
|
6372
|
+
{ argv: ["/usr/bin/systemctl", "stop", "forgezero-agent-proxy.service"], acceptedExitCodes: [0, 1, 5] },
|
|
6351
6373
|
{ argv: ["/usr/bin/systemctl", "restart", "forgezero-agent.socket"] },
|
|
6352
6374
|
{ argv: ["/usr/bin/systemctl", "reset-failed", "forgezero-agent.service"], acceptedExitCodes: [0, 1] },
|
|
6353
6375
|
{ argv: ["/usr/bin/systemctl", "restart", "forgezero-agent.service"] }
|
|
6354
6376
|
] }),
|
|
6355
6377
|
...enrolmentEnabled ? [step("prove the compute binding is durable", { kind: "verify-file", path: enrolStatePath })] : [],
|
|
6356
|
-
...options.enforceEgress ? [step("prove the Agent egress policy is active", {
|
|
6378
|
+
...options.enforceEgress ? [step("prove the Agent egress policy is active", {
|
|
6379
|
+
kind: "verify-egress",
|
|
6380
|
+
deploymentEnabled,
|
|
6381
|
+
runnerPublicTcpPorts,
|
|
6382
|
+
runnerLoopbackPorts,
|
|
6383
|
+
agentLoopbackPorts
|
|
6384
|
+
})] : [],
|
|
6357
6385
|
step("prove it is running", { kind: "commands", commands: [{ argv: ["/usr/bin/systemctl", "is-active", "forgezero-agent.service"] }] }),
|
|
6358
6386
|
step("prove the public Vault socket exists", { kind: "wait-socket", path: options.socketPath, attempts: 100, intervalMs: 100 }),
|
|
6359
6387
|
step("prove the Agent Vault backend exists", { kind: "wait-socket", path: agentBackendSocketPath(options.socketPath), attempts: 100, intervalMs: 100 }),
|
|
6360
6388
|
step("prove the Agent update helper exists", { kind: "wait-socket", path: DEFAULT_AGENT_UPDATE_SOCKET, attempts: 100, intervalMs: 100 }),
|
|
6389
|
+
step("prove the software strategy helper socket exists", { kind: "wait-socket", path: DEFAULT_SOFTWARE_HELPER_SOCKET, attempts: 100, intervalMs: 100 }),
|
|
6361
6390
|
...deploymentEnabled ? [
|
|
6362
|
-
step("prove the deployment runner socket exists", { kind: "wait-socket", path: DEPLOYMENT_RUNNER_SOCKET, attempts: 100, intervalMs: 100 })
|
|
6363
|
-
step("prove the software strategy helper socket exists", { kind: "wait-socket", path: DEFAULT_SOFTWARE_HELPER_SOCKET, attempts: 100, intervalMs: 100 })
|
|
6391
|
+
step("prove the deployment runner socket exists", { kind: "wait-socket", path: DEPLOYMENT_RUNNER_SOCKET, attempts: 100, intervalMs: 100 })
|
|
6364
6392
|
] : [],
|
|
6365
6393
|
...lifecycleEnabled ? [step("prove the lifecycle helper socket exists", { kind: "wait-socket", path: lifecycleHelperSocketPath, attempts: 100, intervalMs: 100 })] : [],
|
|
6366
6394
|
...warpEnabled ? [step("prove Cloudflare WARP is connected", { kind: "verify-warp" })] : [],
|
|
@@ -6385,6 +6413,25 @@ import {
|
|
|
6385
6413
|
writeFileSync as writeFileSync3
|
|
6386
6414
|
} from "fs";
|
|
6387
6415
|
import { dirname as dirname3 } from "path";
|
|
6416
|
+
function normalizeEd25519PublicKey(output) {
|
|
6417
|
+
const line = output.trim();
|
|
6418
|
+
if (!/^ssh-ed25519 [A-Za-z0-9+/]+={0,3}(?: [^\r\n]{1,256})?$/.test(line))
|
|
6419
|
+
return;
|
|
6420
|
+
const [algorithm, encoded] = line.split(" ", 3);
|
|
6421
|
+
if (algorithm !== "ssh-ed25519" || !encoded)
|
|
6422
|
+
return;
|
|
6423
|
+
let blob;
|
|
6424
|
+
try {
|
|
6425
|
+
blob = Buffer.from(encoded, "base64");
|
|
6426
|
+
} catch {
|
|
6427
|
+
return;
|
|
6428
|
+
}
|
|
6429
|
+
if (blob.length !== 51 || blob.readUInt32BE(0) !== 11 || blob.subarray(4, 15).toString("ascii") !== "ssh-ed25519" || blob.readUInt32BE(15) !== 32)
|
|
6430
|
+
return;
|
|
6431
|
+
if (blob.toString("base64") !== encoded)
|
|
6432
|
+
return;
|
|
6433
|
+
return `${algorithm} ${encoded}`;
|
|
6434
|
+
}
|
|
6388
6435
|
function parseAssignments(value) {
|
|
6389
6436
|
if (!value?.trim())
|
|
6390
6437
|
return;
|
|
@@ -6557,7 +6604,7 @@ var runProvisionOperation = async (operation) => {
|
|
|
6557
6604
|
return result;
|
|
6558
6605
|
}
|
|
6559
6606
|
result = await fixed(["/usr/bin/ssh-keygen", "-y", "-f", key]);
|
|
6560
|
-
if (result.exitCode !== 0 ||
|
|
6607
|
+
if (result.exitCode !== 0 || !normalizeEd25519PublicKey(result.stdout)) {
|
|
6561
6608
|
return { stdout: "bootstrap SSH private key is not a valid Ed25519 OpenSSH key", exitCode: 1 };
|
|
6562
6609
|
}
|
|
6563
6610
|
result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=bootstrap-ssh-key", key, operation.credential]);
|
|
@@ -6573,11 +6620,12 @@ var runProvisionOperation = async (operation) => {
|
|
|
6573
6620
|
chmodSync(key, 384);
|
|
6574
6621
|
}
|
|
6575
6622
|
const derived = await fixed(["/usr/bin/ssh-keygen", "-y", "-f", key]);
|
|
6576
|
-
|
|
6623
|
+
const publicKey = normalizeEd25519PublicKey(derived.stdout);
|
|
6624
|
+
if (derived.exitCode !== 0 || !publicKey) {
|
|
6577
6625
|
return { stdout: "bootstrap SSH public key derivation failed", exitCode: 1 };
|
|
6578
6626
|
}
|
|
6579
6627
|
mkdirSync3(dirname3(operation.publicKey), { recursive: true, mode: 493 });
|
|
6580
|
-
writeFileSync3(operation.publicKey, `${
|
|
6628
|
+
writeFileSync3(operation.publicKey, `${publicKey} forgezero-bootstrap-runner
|
|
6581
6629
|
`, { mode: 292 });
|
|
6582
6630
|
chmodSync(operation.publicKey, 292);
|
|
6583
6631
|
}
|
|
@@ -6620,8 +6668,9 @@ var runProvisionOperation = async (operation) => {
|
|
|
6620
6668
|
const policy = await fixed(["/usr/sbin/nft", "--numeric", "list", "table", "inet", "forgezero_agent_egress"]);
|
|
6621
6669
|
const required = [
|
|
6622
6670
|
"forgezero-agent-egress-v1",
|
|
6623
|
-
`
|
|
6624
|
-
...operation.
|
|
6671
|
+
...operation.agentLoopbackPorts.length ? [`shared-loopback=${operation.agentLoopbackPorts.join(",")}`] : [],
|
|
6672
|
+
...operation.deploymentEnabled && operation.runnerPublicTcpPorts.length ? [`public-tcp=${operation.runnerPublicTcpPorts.join(",")}`] : [],
|
|
6673
|
+
...operation.deploymentEnabled && operation.runnerLoopbackPorts.length ? [`:${operation.runnerLoopbackPorts.join(",")}`] : []
|
|
6625
6674
|
];
|
|
6626
6675
|
return policy.exitCode === 0 && required.every((part) => policy.stdout.includes(part)) ? { stdout: policy.stdout, exitCode: 0 } : { stdout: policy.stdout, exitCode: 1 };
|
|
6627
6676
|
}
|
|
@@ -6697,7 +6746,9 @@ async function applyPlan(plan, run) {
|
|
|
6697
6746
|
const result = await run(step2.operation);
|
|
6698
6747
|
transcript.push({ label: step2.label, command: step2.command, exitCode: result.exitCode });
|
|
6699
6748
|
if (result.exitCode !== 0 && !step2.optional) {
|
|
6700
|
-
|
|
6749
|
+
const safeDetail = step2.operation.kind === "ensure-bootstrap-ssh-identity" ? result.stdout.trim().slice(0, 1000) : "";
|
|
6750
|
+
throw new Error(`${step2.label} failed (exit ${result.exitCode}): ${step2.command}${safeDetail ? `
|
|
6751
|
+
${safeDetail}` : ""}`);
|
|
6701
6752
|
}
|
|
6702
6753
|
}
|
|
6703
6754
|
return transcript;
|
|
@@ -12509,12 +12560,26 @@ map $limit_conn_status $forgezero_retry_after { default ''; REJECTED 1; REJECTED
|
|
|
12509
12560
|
server {
|
|
12510
12561
|
listen 127.0.0.1:${input.publicPort};
|
|
12511
12562
|
server_name _;
|
|
12563
|
+
server_tokens off;
|
|
12564
|
+
more_clear_headers Server X-Powered-By;
|
|
12512
12565
|
limit_conn forgezero_admission ${concurrencyLimit};
|
|
12513
12566
|
limit_conn_status 503;
|
|
12514
12567
|
add_header Retry-After $forgezero_retry_after always;
|
|
12515
|
-
|
|
12516
|
-
location
|
|
12517
|
-
location / {
|
|
12568
|
+
error_page 400 403 404 405 408 413 414 429 500 502 503 504 = @transport_failure;
|
|
12569
|
+
location @transport_failure { internal; return 444; }
|
|
12570
|
+
location / {
|
|
12571
|
+
proxy_pass http://forgezero;
|
|
12572
|
+
proxy_http_version 1.1;
|
|
12573
|
+
proxy_intercept_errors off;
|
|
12574
|
+
proxy_hide_header Server;
|
|
12575
|
+
proxy_hide_header X-Powered-By;
|
|
12576
|
+
proxy_set_header Host $host;
|
|
12577
|
+
proxy_set_header X-Forwarded-Proto https;
|
|
12578
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
12579
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
12580
|
+
proxy_set_header Connection $forgezero_connection;
|
|
12581
|
+
proxy_read_timeout 3600s;
|
|
12582
|
+
}
|
|
12518
12583
|
}
|
|
12519
12584
|
`
|
|
12520
12585
|
};
|
|
@@ -14362,7 +14427,7 @@ Type=oneshot
|
|
|
14362
14427
|
User=arangodb
|
|
14363
14428
|
Group=arangodb
|
|
14364
14429
|
LoadCredentialEncrypted=arangodb-jwt:${JWT_CREDENTIAL}
|
|
14365
|
-
ExecStart=/usr/bin/arangosh --server.endpoint tcp://${unitEscape(address)}:8529 --server.jwt-secret-keyfile %d/arangodb-jwt --javascript.execute-string 'const c=require("@arangodb").db._connection;let last;for(let i=0;i<90;i++){try{const v=c.GET("/_api/version?details=true");const s=c.GET("/_admin/status");const m=c.GET("/_admin/server/mode");if(!v.error&&v.version==="3.11.14"&&v.details&&v.details.license==="community"&&!s.error&&s.serverInfo&&s.serverInfo.role==="COORDINATOR"&&!m.error&&m.mode==="default")
|
|
14430
|
+
ExecStart=/usr/bin/arangosh --server.endpoint tcp://${unitEscape(address)}:8529 --server.jwt-secret-keyfile %d/arangodb-jwt --javascript.execute-string 'const c=require("@arangodb").db._connection;let last;let ok=false;for(let i=0;i<90;i++){try{const v=c.GET("/_api/version?details=true");const s=c.GET("/_admin/status");const m=c.GET("/_admin/server/mode");if(!v.error&&v.version==="3.11.14"&&v.details&&v.details.license==="community"&&!s.error&&s.serverInfo&&s.serverInfo.role==="COORDINATOR"&&!m.error&&m.mode==="default"){ok=true;break;}last={v,s,m};}catch(e){last=String(e);}require("internal").wait(2);}if(!ok)throw new Error("writable Community Coordinator verification failed: "+JSON.stringify(last));'
|
|
14366
14431
|
RemainAfterExit=yes
|
|
14367
14432
|
TimeoutStartSec=200
|
|
14368
14433
|
NoNewPrivileges=true
|
|
@@ -14488,11 +14553,11 @@ async function seal(host, name, destination2, value) {
|
|
|
14488
14553
|
async function verifyBootstrapBundleOnHost(host, config, verifyGit = false) {
|
|
14489
14554
|
const manifestMetadata = host.inspect?.(config.bootstrapBundle.manifestFile);
|
|
14490
14555
|
const bundleMetadata = host.inspect?.(config.bootstrapBundle.bundleFile);
|
|
14491
|
-
if (manifestMetadata && (!manifestMetadata.regular || manifestMetadata.symbolic || manifestMetadata.uid !== 0 || manifestMetadata.links !== 1 || (manifestMetadata.mode &
|
|
14492
|
-
throw new Error("bootstrap bundle manifest must be a root-owned
|
|
14556
|
+
if (manifestMetadata && (!manifestMetadata.regular || manifestMetadata.symbolic || manifestMetadata.uid !== 0 || manifestMetadata.links !== 1 || (manifestMetadata.mode & 18) !== 0 || manifestMetadata.size > 16 * 1024)) {
|
|
14557
|
+
throw new Error("bootstrap bundle manifest must be a root-owned non-writable regular file");
|
|
14493
14558
|
}
|
|
14494
|
-
if (bundleMetadata && (!bundleMetadata.regular || bundleMetadata.symbolic || bundleMetadata.uid !== 0 || bundleMetadata.links !== 1 || (bundleMetadata.mode &
|
|
14495
|
-
throw new Error("bootstrap bundle must be a root-owned
|
|
14559
|
+
if (bundleMetadata && (!bundleMetadata.regular || bundleMetadata.symbolic || bundleMetadata.uid !== 0 || bundleMetadata.links !== 1 || (bundleMetadata.mode & 18) !== 0 || bundleMetadata.size < 1 || bundleMetadata.size > 512 * 1024 * 1024)) {
|
|
14560
|
+
throw new Error("bootstrap bundle must be a root-owned non-writable regular file within 512 MiB");
|
|
14496
14561
|
}
|
|
14497
14562
|
if (!host.exists(config.bootstrapBundle.bundleFile) || !host.exists(config.bootstrapBundle.manifestFile)) {
|
|
14498
14563
|
throw new Error("attended bootstrap bundle and manifest are required for release generation one");
|
|
@@ -14511,11 +14576,25 @@ async function verifyBootstrapBundleOnHost(host, config, verifyGit = false) {
|
|
|
14511
14576
|
if (digest !== manifest.sha256)
|
|
14512
14577
|
throw new Error("bootstrap bundle digest does not match its manifest");
|
|
14513
14578
|
if (verifyGit) {
|
|
14514
|
-
|
|
14579
|
+
const verificationRepository = "/run/forgezero-bootstrap-bundle-verify";
|
|
14580
|
+
await checked3(host, ["/usr/bin/rm", "-rf", "--", verificationRepository], "stale bootstrap bundle verifier cleanup");
|
|
14581
|
+
await checked3(host, ["/usr/bin/git", "init", "--bare", "--quiet", verificationRepository], "bootstrap bundle verifier repository");
|
|
14582
|
+
try {
|
|
14583
|
+
await checked3(host, [
|
|
14584
|
+
"/usr/bin/git",
|
|
14585
|
+
"-C",
|
|
14586
|
+
verificationRepository,
|
|
14587
|
+
"bundle",
|
|
14588
|
+
"verify",
|
|
14589
|
+
config.bootstrapBundle.bundleFile
|
|
14590
|
+
], "bootstrap Git bundle verification");
|
|
14591
|
+
} finally {
|
|
14592
|
+
await checked3(host, ["/usr/bin/rm", "-rf", "--", verificationRepository], "bootstrap bundle verifier cleanup");
|
|
14593
|
+
}
|
|
14515
14594
|
}
|
|
14516
14595
|
return manifest;
|
|
14517
14596
|
}
|
|
14518
|
-
function bootstrapIdentity(config) {
|
|
14597
|
+
function bootstrapIdentity(config, includeDeploymentEvidence = false) {
|
|
14519
14598
|
if (config.kind === "enrolled-compute")
|
|
14520
14599
|
return {
|
|
14521
14600
|
kind: config.kind,
|
|
@@ -14534,7 +14613,13 @@ function bootstrapIdentity(config) {
|
|
|
14534
14613
|
bootstrapRunner: config.bootstrapRunner ? { targetTelemetryEndpoint: config.bootstrapRunner.targetTelemetryEndpoint } : null
|
|
14535
14614
|
};
|
|
14536
14615
|
const environment = config.runtime.environment;
|
|
14537
|
-
const { cloudflare: _cloudflare, realtime: _realtime, ...
|
|
14616
|
+
const { cloudflare: _cloudflare, realtime: _realtime, initialInventory, ...stableEnvironmentRest } = environment;
|
|
14617
|
+
const stableEnvironment = {
|
|
14618
|
+
...stableEnvironmentRest,
|
|
14619
|
+
...initialInventory ? {
|
|
14620
|
+
initialInventory: { ...initialInventory, deployment: includeDeploymentEvidence ? initialInventory.deployment : undefined }
|
|
14621
|
+
} : {}
|
|
14622
|
+
};
|
|
14538
14623
|
return {
|
|
14539
14624
|
kind: config.kind,
|
|
14540
14625
|
environment: config.environment,
|
|
@@ -14575,6 +14660,26 @@ function bootstrapIdentity(config) {
|
|
|
14575
14660
|
function bootstrapIdentityDigest(config) {
|
|
14576
14661
|
return createHash4("sha256").update(JSON.stringify(bootstrapIdentity(config))).digest("hex");
|
|
14577
14662
|
}
|
|
14663
|
+
var legacyBootstrapIdentityDigest = (config) => createHash4("sha256").update(JSON.stringify(bootstrapIdentity(config, true))).digest("hex");
|
|
14664
|
+
function interruptedPlatformReleaseIdentityDigests(config, releaseEvidence, bootstrapManifest) {
|
|
14665
|
+
if (!config.runtime.environment.initialInventory)
|
|
14666
|
+
return [];
|
|
14667
|
+
const previous = structuredClone(config);
|
|
14668
|
+
previous.runtime.environment.initialInventory.deployment = {
|
|
14669
|
+
source: "bootstrap-bundle",
|
|
14670
|
+
branch: releaseEvidence.branch,
|
|
14671
|
+
revision: releaseEvidence.revision,
|
|
14672
|
+
bundleSha256: releaseEvidence.sha256
|
|
14673
|
+
};
|
|
14674
|
+
const expectedCurrentEpoch = bootstrapManifest ? `${config.environment}-${bootstrapManifest.revision.slice(0, 16)}` : undefined;
|
|
14675
|
+
if (expectedCurrentEpoch && config.runtime.environment.seedSyncEpoch === expectedCurrentEpoch) {
|
|
14676
|
+
previous.runtime.environment.seedSyncEpoch = `${config.environment}-${releaseEvidence.revision.slice(0, 16)}`;
|
|
14677
|
+
}
|
|
14678
|
+
return [bootstrapIdentityDigest(previous), legacyBootstrapIdentityDigest(previous)];
|
|
14679
|
+
}
|
|
14680
|
+
function storedPlatformCoordinatesMatch(state, config) {
|
|
14681
|
+
return state.kind === "platform" && state.profile === config.profile && state.nodeHostname === config.nodeHostname && state.apiUrl === config.apiUrl && state.environment === config.environment && state.databaseRole === config.database.role && state.databaseAgency === config.database.agency && state.databaseReadPreferred === config.database.readPreferred && state.databaseServerMode === config.database.serverMode && (state.databaseAddress ?? null) === (config.database.address ?? null) && JSON.stringify(state.databaseCoordinators ?? []) === JSON.stringify(config.database.coordinators) && state.collectorUnit === config.runtime.environment.otlpCollectorUnit && state.publicApiPort === config.runtime.environment.publicApiPort && state.healthPath === config.runtime.healthPath;
|
|
14682
|
+
}
|
|
14578
14683
|
function parseStoredState(raw) {
|
|
14579
14684
|
let value;
|
|
14580
14685
|
try {
|
|
@@ -14605,11 +14710,21 @@ function parseStoredIntent(raw) {
|
|
|
14605
14710
|
}
|
|
14606
14711
|
return intent;
|
|
14607
14712
|
}
|
|
14608
|
-
function bindBootstrapIntent(host, config) {
|
|
14713
|
+
function bindBootstrapIntent(host, config, previousReleaseDigests = []) {
|
|
14609
14714
|
const identityDigest = bootstrapIdentityDigest(config);
|
|
14610
14715
|
if (host.exists(INTENT_PATH)) {
|
|
14611
14716
|
const intent = parseStoredIntent(host.read(INTENT_PATH));
|
|
14612
14717
|
if (intent.kind !== config.kind || intent.identityDigest !== identityDigest) {
|
|
14718
|
+
if (previousReleaseDigests.includes(intent.identityDigest) && intent.kind === "platform" && config.kind === "platform") {
|
|
14719
|
+
host.write(INTENT_PATH, `${JSON.stringify({
|
|
14720
|
+
format: 1,
|
|
14721
|
+
kind: config.kind,
|
|
14722
|
+
identityDigest,
|
|
14723
|
+
createdAt: new Date().toISOString()
|
|
14724
|
+
}, null, 2)}
|
|
14725
|
+
`, 384);
|
|
14726
|
+
return identityDigest;
|
|
14727
|
+
}
|
|
14613
14728
|
throw new Error("bootstrap resume coordinates do not match the interrupted host intent");
|
|
14614
14729
|
}
|
|
14615
14730
|
} else if (!host.exists(STATE_PATH)) {
|
|
@@ -14777,7 +14892,42 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
14777
14892
|
let installed;
|
|
14778
14893
|
if (host.exists(STATE_PATH))
|
|
14779
14894
|
installed = parseStoredState(host.read(STATE_PATH));
|
|
14780
|
-
|
|
14895
|
+
let bootstrapManifest;
|
|
14896
|
+
let releaseEvidence;
|
|
14897
|
+
if (config.kind === "platform") {
|
|
14898
|
+
const staged = host.exists(config.bootstrapBundle.bundleFile) || host.exists(config.bootstrapBundle.manifestFile);
|
|
14899
|
+
if (staged && !(host.exists(config.bootstrapBundle.bundleFile) && host.exists(config.bootstrapBundle.manifestFile))) {
|
|
14900
|
+
throw new Error("bootstrap bundle and manifest must be staged together");
|
|
14901
|
+
}
|
|
14902
|
+
const reviewed = staged ? await verifyBootstrapBundleOnHost(host, config) : undefined;
|
|
14903
|
+
if (!host.exists(BOOTSTRAP_RELEASE_EVIDENCE)) {
|
|
14904
|
+
if (!reviewed)
|
|
14905
|
+
throw new Error("attended bootstrap bundle and manifest are required for release generation one");
|
|
14906
|
+
bootstrapManifest = reviewed;
|
|
14907
|
+
} else if (reviewed) {
|
|
14908
|
+
let evidence;
|
|
14909
|
+
try {
|
|
14910
|
+
evidence = JSON.parse(host.read(BOOTSTRAP_RELEASE_EVIDENCE));
|
|
14911
|
+
} catch {
|
|
14912
|
+
throw new Error("bootstrap release evidence is malformed");
|
|
14913
|
+
}
|
|
14914
|
+
if (typeof evidence.revision !== "string" || !/^[a-f0-9]{40}$/.test(evidence.revision) || typeof evidence.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(evidence.sha256) || typeof evidence.branch !== "string")
|
|
14915
|
+
throw new Error("bootstrap release evidence is malformed");
|
|
14916
|
+
releaseEvidence = { revision: evidence.revision, sha256: evidence.sha256, branch: evidence.branch };
|
|
14917
|
+
if (evidence.revision !== reviewed.revision || evidence.sha256 !== reviewed.sha256 || evidence.branch !== reviewed.branch)
|
|
14918
|
+
bootstrapManifest = reviewed;
|
|
14919
|
+
} else if (host.exists(BOOTSTRAP_RELEASE_EVIDENCE)) {
|
|
14920
|
+
let evidence;
|
|
14921
|
+
try {
|
|
14922
|
+
evidence = JSON.parse(host.read(BOOTSTRAP_RELEASE_EVIDENCE));
|
|
14923
|
+
} catch {
|
|
14924
|
+
throw new Error("bootstrap release evidence is malformed");
|
|
14925
|
+
}
|
|
14926
|
+
if (typeof evidence.revision !== "string" || !/^[a-f0-9]{40}$/.test(evidence.revision) || typeof evidence.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(evidence.sha256) || typeof evidence.branch !== "string")
|
|
14927
|
+
throw new Error("bootstrap release evidence is malformed");
|
|
14928
|
+
releaseEvidence = { revision: evidence.revision, sha256: evidence.sha256, branch: evidence.branch };
|
|
14929
|
+
}
|
|
14930
|
+
}
|
|
14781
14931
|
let cloudflare;
|
|
14782
14932
|
if (config.cloudflareHandoff) {
|
|
14783
14933
|
if (host.exists(config.cloudflareHandoff.handoffFile)) {
|
|
@@ -14827,10 +14977,14 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
14827
14977
|
throw new Error("platform realtime coordinates require a realtime-enabled Cloudflare handoff");
|
|
14828
14978
|
}
|
|
14829
14979
|
}
|
|
14980
|
+
let allowInterruptedReleaseRebind = false;
|
|
14830
14981
|
if (installed) {
|
|
14831
|
-
|
|
14982
|
+
const exactIdentity = installed.kind === config.kind && installed.identityDigest === bootstrapIdentityDigest(config);
|
|
14983
|
+
const boundedReleaseResume = config.kind === "platform" && !host.exists("/var/lib/forgezero/enrolment.json") && host.exists(BOOTSTRAP_RELEASE_EVIDENCE) && storedPlatformCoordinatesMatch(installed, config);
|
|
14984
|
+
if (!exactIdentity && !boundedReleaseResume) {
|
|
14832
14985
|
throw new Error("bootstrap repair coordinates do not match the installed host identity");
|
|
14833
14986
|
}
|
|
14987
|
+
allowInterruptedReleaseRebind = !exactIdentity && boundedReleaseResume;
|
|
14834
14988
|
}
|
|
14835
14989
|
const platformPrivate = config.kind === "platform" ? (() => {
|
|
14836
14990
|
if (!secrets)
|
|
@@ -14850,10 +15004,14 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
14850
15004
|
if (config.kind === "enrolled-compute" && config.bootstrapRunner?.sshPrivateKeyFile && !host.exists(BOOTSTRAP_SSH_CREDENTIAL)) {
|
|
14851
15005
|
privateFile(host, config.bootstrapRunner.sshPrivateKeyFile, "bootstrap runner SSH private key");
|
|
14852
15006
|
}
|
|
14853
|
-
|
|
15007
|
+
let previousReleaseDigests = [];
|
|
15008
|
+
if (!installed && config.kind === "platform" && releaseEvidence && !host.exists("/var/lib/forgezero/enrolment.json")) {
|
|
15009
|
+
previousReleaseDigests = [...interruptedPlatformReleaseIdentityDigests(config, releaseEvidence, bootstrapManifest)];
|
|
15010
|
+
}
|
|
15011
|
+
bindBootstrapIntent(host, config, allowInterruptedReleaseRebind && installed?.identityDigest ? [installed.identityDigest] : previousReleaseDigests);
|
|
14854
15012
|
const plan = planBootstrap(config, host.exists(STATE_PATH));
|
|
14855
15013
|
await checked3(host, ["hostnamectl", "set-hostname", "--static", config.nodeHostname], "compute hostname");
|
|
14856
|
-
|
|
15014
|
+
let alreadyEnrolled = host.exists("/var/lib/forgezero/enrolment.json");
|
|
14857
15015
|
host.mkdir(CREDS, 448);
|
|
14858
15016
|
host.mkdir("/var/lib/forgezero", 448);
|
|
14859
15017
|
if (!alreadyEnrolled && !host.exists(ENROL_CREDENTIAL)) {
|
|
@@ -14882,14 +15040,29 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
14882
15040
|
if (connectorCapabilities?.meshConnectorToken && !host.exists(WARP_CONNECTOR_CREDENTIAL)) {
|
|
14883
15041
|
await seal(host, "CF_WARP_CONNECTOR_TOKEN", WARP_CONNECTOR_CREDENTIAL, connectorCapabilities.meshConnectorToken);
|
|
14884
15042
|
}
|
|
15043
|
+
let installedAgentPlan;
|
|
14885
15044
|
if (alreadyEnrolled) {
|
|
14886
|
-
await host.installAgent(config, "bound");
|
|
15045
|
+
installedAgentPlan = await host.installAgent(config, "bound");
|
|
14887
15046
|
} else if (config.kind === "platform") {
|
|
14888
15047
|
if (bootstrapManifest)
|
|
14889
|
-
await host.installAgent(config, "bootstrap");
|
|
15048
|
+
installedAgentPlan = await host.installAgent(config, "bootstrap");
|
|
15049
|
+
else if (host.exists(BOOTSTRAP_RELEASE_EVIDENCE)) {
|
|
15050
|
+
await checked3(host, [
|
|
15051
|
+
"curl",
|
|
15052
|
+
"--fail",
|
|
15053
|
+
"--silent",
|
|
15054
|
+
"--show-error",
|
|
15055
|
+
"--max-time",
|
|
15056
|
+
"10",
|
|
15057
|
+
`http://127.0.0.1:${config.runtime.environment.publicApiPort}${config.runtime.healthPath}`
|
|
15058
|
+
], "persisted release-one API health");
|
|
15059
|
+
await host.installAgent(config, "enrol");
|
|
15060
|
+
installedAgentPlan = await host.installAgent(config, "bound");
|
|
15061
|
+
alreadyEnrolled = true;
|
|
15062
|
+
}
|
|
14890
15063
|
} else {
|
|
14891
15064
|
await host.installAgent(config, "enrol");
|
|
14892
|
-
await host.installAgent(config, "bound");
|
|
15065
|
+
installedAgentPlan = await host.installAgent(config, "bound");
|
|
14893
15066
|
}
|
|
14894
15067
|
if (config.kind === "platform" && config.firewall.enabled) {
|
|
14895
15068
|
await host.ensureSoftware(plan.software.filter(({ id: id2 }) => id2 === "ufw"));
|
|
@@ -14969,6 +15142,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
14969
15142
|
await checked3(host, ["useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", runtime.serviceUser], "API service account").catch(async () => {
|
|
14970
15143
|
await checked3(host, ["id", runtime.serviceUser], "existing API service account");
|
|
14971
15144
|
});
|
|
15145
|
+
await checked3(host, ["usermod", "-a", "-G", DEPLOYMENT_GROUP, runtime.serviceUser], "API deployment group");
|
|
14972
15146
|
host.mkdir(runtime.environment.sharedDirectory, 488);
|
|
14973
15147
|
host.mkdir(runtime.slotsDirectory, 493);
|
|
14974
15148
|
host.write(envPath, renderPlatformSharedEnvironment(runtime.environment), 416);
|
|
@@ -14987,7 +15161,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
14987
15161
|
host.write("/etc/sudoers.d/forgezero-runner", activation.sudoers, 288);
|
|
14988
15162
|
await checked3(host, ["visudo", "-cf", "/etc/sudoers.d/forgezero-runner"], "activation sudo policy");
|
|
14989
15163
|
const telemetry = planLocalOtlpProof(runtime.environment.otlpEndpoint, runtime.environment.otlpCollectorUnit);
|
|
14990
|
-
await checked3(host, telemetry.unitCheck.argv, "OTLP collector supervision");
|
|
15164
|
+
await checked3(host, [telemetry.unitCheck.command, ...telemetry.unitCheck.argv], "OTLP collector supervision");
|
|
14991
15165
|
const otlpStatus = (await checked3(host, [telemetry.receiverCheck.command, ...telemetry.receiverCheck.argv], "OTLP receiver")).trim();
|
|
14992
15166
|
if (!/^2\d\d$/.test(otlpStatus))
|
|
14993
15167
|
throw new Error(`OTLP receiver returned HTTP ${otlpStatus || "unknown"}`);
|
|
@@ -15020,10 +15194,12 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
15020
15194
|
`, 384);
|
|
15021
15195
|
}
|
|
15022
15196
|
if (bootstrapManifest) {
|
|
15197
|
+
if (!installedAgentPlan?.user)
|
|
15198
|
+
throw new Error("initial Agent deployment requires the converged Agent user");
|
|
15023
15199
|
await checked3(host, [
|
|
15024
15200
|
"runuser",
|
|
15025
15201
|
"-u",
|
|
15026
|
-
|
|
15202
|
+
installedAgentPlan.user,
|
|
15027
15203
|
"--",
|
|
15028
15204
|
"/usr/local/bin/fz-agent",
|
|
15029
15205
|
"deploy",
|
|
@@ -15050,7 +15226,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
15050
15226
|
"--show-error",
|
|
15051
15227
|
"--max-time",
|
|
15052
15228
|
"10",
|
|
15053
|
-
`http://127.0.0.1
|
|
15229
|
+
`http://127.0.0.1:${runtime.environment.publicApiPort}${runtime.healthPath}`
|
|
15054
15230
|
], "release-one API health");
|
|
15055
15231
|
await host.installAgent(config, "enrol");
|
|
15056
15232
|
await host.installAgent(config, "bound");
|
|
@@ -15291,6 +15467,8 @@ function planBootstrapAgentInstall(config, phase, context) {
|
|
|
15291
15467
|
bootstrapTargetTelemetryEndpoint: bound && bootstrapRunner ? platformBootstrapRunner(config) ? config.telemetryEndpoint : config.kind === "enrolled-compute" ? config.bootstrapRunner?.targetTelemetryEndpoint : undefined : undefined,
|
|
15292
15468
|
lifecycleProfilePath: bound && config.kind === "platform" ? LIFECYCLE_PROFILE : undefined,
|
|
15293
15469
|
enforceEgress: true,
|
|
15470
|
+
runnerLoopbackPorts: config.kind === "platform" ? [config.runtime.environment.publicApiPort, config.runtime.bluePort, config.runtime.greenPort] : [],
|
|
15471
|
+
agentLoopbackPorts: config.kind === "platform" ? [config.runtime.environment.publicApiPort] : [],
|
|
15294
15472
|
nodeHostname: config.nodeHostname,
|
|
15295
15473
|
telemetryEndpoint: config.telemetryEndpoint,
|
|
15296
15474
|
binPath: "/usr/local/lib/forgezero/agent/fz-agent",
|
|
@@ -15300,7 +15478,7 @@ function planBootstrapAgentInstall(config, phase, context) {
|
|
|
15300
15478
|
enrolStatePath: "/var/lib/forgezero/enrolment.json"
|
|
15301
15479
|
} : bound ? { enrolStatePath: "/var/lib/forgezero/enrolment.json" } : {},
|
|
15302
15480
|
...authenticated ? {
|
|
15303
|
-
apiUrl: config.apiUrl,
|
|
15481
|
+
apiUrl: config.kind === "platform" ? `http://127.0.0.1:${config.runtime.environment.publicApiPort}` : config.apiUrl,
|
|
15304
15482
|
project: config.kind === "enrolled-compute" ? config.realm : "platform",
|
|
15305
15483
|
environment: config.kind === "enrolled-compute" ? undefined : config.environment,
|
|
15306
15484
|
nodeLabel: config.kind === "enrolled-compute" ? config.nodeHostname : config.computeReference
|
|
@@ -15309,6 +15487,7 @@ function planBootstrapAgentInstall(config, phase, context) {
|
|
|
15309
15487
|
return planInstall(options);
|
|
15310
15488
|
}
|
|
15311
15489
|
function localBootstrapHost() {
|
|
15490
|
+
let softwareClientUser;
|
|
15312
15491
|
const execute = async (argv2, options = {}) => {
|
|
15313
15492
|
const child = Bun.spawn([...argv2], { stdin: options.stdin === undefined ? "ignore" : "pipe", stdout: "pipe", stderr: "pipe" });
|
|
15314
15493
|
if (options.stdin !== undefined && child.stdin && typeof child.stdin !== "number") {
|
|
@@ -15341,10 +15520,12 @@ function localBootstrapHost() {
|
|
|
15341
15520
|
exec: execute,
|
|
15342
15521
|
sleep: (milliseconds) => Bun.sleep(milliseconds),
|
|
15343
15522
|
async ensureSoftware(requirements) {
|
|
15523
|
+
if (!softwareClientUser)
|
|
15524
|
+
throw new Error("Agent must be converged before software requirements are applied");
|
|
15344
15525
|
const result = await execute([
|
|
15345
15526
|
"runuser",
|
|
15346
15527
|
"-u",
|
|
15347
|
-
|
|
15528
|
+
softwareClientUser,
|
|
15348
15529
|
"--",
|
|
15349
15530
|
"/usr/local/bin/fz-agent",
|
|
15350
15531
|
"software-ensure",
|
|
@@ -15358,7 +15539,7 @@ function localBootstrapHost() {
|
|
|
15358
15539
|
const capabilities = await readCapabilities(localRunner);
|
|
15359
15540
|
const hasBinding = existsSync9("/var/lib/forgezero/enrolment.json");
|
|
15360
15541
|
const hasEnrolCredential = existsSync9(ENROL_CREDENTIAL);
|
|
15361
|
-
const initialBundle = config.kind === "platform" &&
|
|
15542
|
+
const initialBundle = config.kind === "platform" && phase === "bootstrap" && existsSync9(config.bootstrapBundle.bundleFile) && existsSync9(config.bootstrapBundle.manifestFile) ? {
|
|
15362
15543
|
path: config.bootstrapBundle.bundleFile,
|
|
15363
15544
|
manifestPath: config.bootstrapBundle.manifestFile,
|
|
15364
15545
|
manifest: parseBootstrapBundleManifest(JSON.parse(readFileSync9(config.bootstrapBundle.manifestFile, "utf8")))
|
|
@@ -15389,6 +15570,7 @@ function localBootstrapHost() {
|
|
|
15389
15570
|
writeFileSync9(unit.path, unit.unit, { mode: 420 });
|
|
15390
15571
|
}
|
|
15391
15572
|
await applyPlan(plan, localRunner);
|
|
15573
|
+
softwareClientUser = plan.user;
|
|
15392
15574
|
return plan;
|
|
15393
15575
|
}
|
|
15394
15576
|
};
|
|
@@ -15658,7 +15840,7 @@ async function runCloudflareBootstrapFinalizeCommand(configPath, dependencies =
|
|
|
15658
15840
|
|
|
15659
15841
|
// src/operator-bootstrap.ts
|
|
15660
15842
|
import { createHash as createHash6, randomBytes as randomBytes9 } from "crypto";
|
|
15661
|
-
import { chmodSync as chmodSync6, lstatSync as lstatSync7, mkdirSync as mkdirSync12, mkdtempSync as mkdtempSync2, readFileSync as readFileSync12, rmSync as rmSync7, writeFileSync as writeFileSync12 } from "fs";
|
|
15843
|
+
import { chmodSync as chmodSync6, existsSync as existsSync11, lstatSync as lstatSync7, mkdirSync as mkdirSync12, mkdtempSync as mkdtempSync2, readFileSync as readFileSync12, rmSync as rmSync7, writeFileSync as writeFileSync12 } from "fs";
|
|
15662
15844
|
import { isIP as isIP6 } from "net";
|
|
15663
15845
|
import { tmpdir as tmpdir2 } from "os";
|
|
15664
15846
|
import { basename as basename2, dirname as dirname13, isAbsolute as isAbsolute5, join as join11, resolve as resolve9 } from "path";
|
|
@@ -16630,6 +16812,21 @@ async function metalBootstrapStatus(exec = defaultExec2) {
|
|
|
16630
16812
|
}
|
|
16631
16813
|
|
|
16632
16814
|
// src/operator-bootstrap.ts
|
|
16815
|
+
function operatorPackagedBinary(name) {
|
|
16816
|
+
const candidates = [
|
|
16817
|
+
fileURLToPath3(new URL(`./${name}`, import.meta.url)),
|
|
16818
|
+
fileURLToPath3(new URL(`../dist/${name}`, import.meta.url))
|
|
16819
|
+
];
|
|
16820
|
+
const found = candidates.find(existsSync11);
|
|
16821
|
+
if (!found)
|
|
16822
|
+
throw new Error(`packaged ${name} is missing; checked ${candidates.join(" and ")}`);
|
|
16823
|
+
return found;
|
|
16824
|
+
}
|
|
16825
|
+
var operatorPackagedFzCliPath = () => operatorPackagedBinary("fz.js");
|
|
16826
|
+
var operatorPackagedAgentPath = () => operatorPackagedBinary("fz-agent.js");
|
|
16827
|
+
var operatorPackagedGitSshPath = () => operatorPackagedBinary("fz-git-ssh.js");
|
|
16828
|
+
var PLATFORM_CLUSTER_CREDENTIAL_NAME = "forgezero-platform-cluster-bootstrap-code";
|
|
16829
|
+
var PLATFORM_CLUSTER_CREDENTIAL_PATH = `/etc/forgezero/creds/${PLATFORM_CLUSTER_CREDENTIAL_NAME}.cred`;
|
|
16633
16830
|
var REQUEST_LIMIT = 64 * 1024;
|
|
16634
16831
|
var SECRET_LIMIT = 256 * 1024;
|
|
16635
16832
|
var REMOTE_STAGE = "/run/forgezero-operator-bootstrap";
|
|
@@ -17072,9 +17269,11 @@ function operatorPlatformFleetCoordinates(fleet) {
|
|
|
17072
17269
|
function planOperatorPlatformFleetBootstrap(fleet, mode) {
|
|
17073
17270
|
const { requests, configs, environment } = validatedPlatformFleet(fleet);
|
|
17074
17271
|
const secretInputs = mode === "apply" ? attendedSecretNames(configs[0]) : [];
|
|
17075
|
-
|
|
17076
|
-
|
|
17077
|
-
|
|
17272
|
+
if (mode === "apply") {
|
|
17273
|
+
for (const config of configs.slice(1)) {
|
|
17274
|
+
if (JSON.stringify(attendedSecretNames(config)) !== JSON.stringify(secretInputs)) {
|
|
17275
|
+
throw new Error("operator fleet configs do not share one attended secret schema");
|
|
17276
|
+
}
|
|
17078
17277
|
}
|
|
17079
17278
|
}
|
|
17080
17279
|
return {
|
|
@@ -17112,9 +17311,33 @@ var defaultExec3 = async (argv2, options = {}) => {
|
|
|
17112
17311
|
]);
|
|
17113
17312
|
return {
|
|
17114
17313
|
exitCode,
|
|
17115
|
-
output: options.secret ?
|
|
17314
|
+
output: options.secret ? exitCode === 0 ? options.safeStdout ? stdout.slice(0, 65536) : "" : redactOperatorSecretDiagnostic(stderr, options.stdin) : `${stdout}${stderr}`.slice(0, 65536)
|
|
17116
17315
|
};
|
|
17117
17316
|
};
|
|
17317
|
+
function redactOperatorSecretDiagnostic(stderr, stdin) {
|
|
17318
|
+
if (!stdin)
|
|
17319
|
+
return "";
|
|
17320
|
+
let parsed;
|
|
17321
|
+
try {
|
|
17322
|
+
parsed = JSON.parse(stdin);
|
|
17323
|
+
} catch {
|
|
17324
|
+
return "";
|
|
17325
|
+
}
|
|
17326
|
+
const values = [];
|
|
17327
|
+
const visit = (value) => {
|
|
17328
|
+
if (typeof value === "string" && value)
|
|
17329
|
+
values.push(value);
|
|
17330
|
+
else if (Array.isArray(value))
|
|
17331
|
+
value.forEach(visit);
|
|
17332
|
+
else if (value && typeof value === "object")
|
|
17333
|
+
Object.values(value).forEach(visit);
|
|
17334
|
+
};
|
|
17335
|
+
visit(parsed);
|
|
17336
|
+
let safe = stderr.slice(0, 16384);
|
|
17337
|
+
for (const value of values.sort((left, right) => right.length - left.length))
|
|
17338
|
+
safe = safe.replaceAll(value, "[REDACTED]");
|
|
17339
|
+
return safe.slice(0, 8192);
|
|
17340
|
+
}
|
|
17118
17341
|
var checked5 = async (exec, argv2, label, options) => {
|
|
17119
17342
|
const result = await exec(argv2, options);
|
|
17120
17343
|
if (result.exitCode !== 0)
|
|
@@ -17157,7 +17380,7 @@ var sshOptions = (request, knownHosts, scp = false) => [
|
|
|
17157
17380
|
"-o",
|
|
17158
17381
|
"ClearAllForwardings=yes",
|
|
17159
17382
|
"-o",
|
|
17160
|
-
"ConnectTimeout=
|
|
17383
|
+
"ConnectTimeout=30",
|
|
17161
17384
|
"-o",
|
|
17162
17385
|
request.target.jump ? `ProxyJump=${destination2(request.target.jump)}:${request.target.jump.port}` : "ProxyJump=none",
|
|
17163
17386
|
scp ? "-P" : "-p",
|
|
@@ -17192,6 +17415,93 @@ var remoteRegularFileExists = async (exec, request, knownHosts, path) => {
|
|
|
17192
17415
|
return false;
|
|
17193
17416
|
throw new Error(`remote initialized-state probe failed${result.output ? `: ${result.output.trim()}` : ""}`);
|
|
17194
17417
|
};
|
|
17418
|
+
async function operatorMetalClusterBootstrapCode(requestInput, options = {}) {
|
|
17419
|
+
const request = validateMetalRequestValue(requestInput);
|
|
17420
|
+
publicIdentity(request.target.identityPublicKeyFile);
|
|
17421
|
+
socketPath(request.target.agentSocket);
|
|
17422
|
+
const exec = options.exec ?? defaultExec3;
|
|
17423
|
+
const directory = mkdtempSync2(join11(tmpdir2(), "forgezero-operator-metal-credential-"));
|
|
17424
|
+
try {
|
|
17425
|
+
const knownHosts = writeKnownHosts(request, directory);
|
|
17426
|
+
const probe = await exec([
|
|
17427
|
+
"ssh",
|
|
17428
|
+
...sshOptions(request, knownHosts),
|
|
17429
|
+
destination2(request.target),
|
|
17430
|
+
"--",
|
|
17431
|
+
"/usr/bin/sudo",
|
|
17432
|
+
"-n",
|
|
17433
|
+
"/usr/bin/test",
|
|
17434
|
+
"-f",
|
|
17435
|
+
PLATFORM_CLUSTER_CREDENTIAL_PATH
|
|
17436
|
+
]);
|
|
17437
|
+
if (probe.exitCode !== 0 && probe.exitCode !== 1) {
|
|
17438
|
+
throw new Error("Metal cluster credential probe failed");
|
|
17439
|
+
}
|
|
17440
|
+
if (probe.exitCode === 1) {
|
|
17441
|
+
const value2 = randomBytes9(32).toString("hex");
|
|
17442
|
+
await remote(exec, request, knownHosts, [
|
|
17443
|
+
"/usr/bin/sudo",
|
|
17444
|
+
"-n",
|
|
17445
|
+
"/usr/bin/install",
|
|
17446
|
+
"-d",
|
|
17447
|
+
"-o",
|
|
17448
|
+
"root",
|
|
17449
|
+
"-g",
|
|
17450
|
+
"root",
|
|
17451
|
+
"-m",
|
|
17452
|
+
"0700",
|
|
17453
|
+
dirname13(PLATFORM_CLUSTER_CREDENTIAL_PATH)
|
|
17454
|
+
], "Metal credential directory");
|
|
17455
|
+
await remote(exec, request, knownHosts, [
|
|
17456
|
+
"/usr/bin/sudo",
|
|
17457
|
+
"-n",
|
|
17458
|
+
"/usr/bin/systemd-creds",
|
|
17459
|
+
"encrypt",
|
|
17460
|
+
`--name=${PLATFORM_CLUSTER_CREDENTIAL_NAME}`,
|
|
17461
|
+
"-",
|
|
17462
|
+
PLATFORM_CLUSTER_CREDENTIAL_PATH
|
|
17463
|
+
], "seal Metal cluster credential", true, `${value2}
|
|
17464
|
+
`);
|
|
17465
|
+
await remote(exec, request, knownHosts, [
|
|
17466
|
+
"/usr/bin/sudo",
|
|
17467
|
+
"-n",
|
|
17468
|
+
"/usr/bin/chown",
|
|
17469
|
+
"root:root",
|
|
17470
|
+
PLATFORM_CLUSTER_CREDENTIAL_PATH
|
|
17471
|
+
], "own Metal cluster credential");
|
|
17472
|
+
await remote(exec, request, knownHosts, [
|
|
17473
|
+
"/usr/bin/sudo",
|
|
17474
|
+
"-n",
|
|
17475
|
+
"/usr/bin/chmod",
|
|
17476
|
+
"0600",
|
|
17477
|
+
PLATFORM_CLUSTER_CREDENTIAL_PATH
|
|
17478
|
+
], "protect Metal cluster credential");
|
|
17479
|
+
}
|
|
17480
|
+
const metadata = await remote(exec, request, knownHosts, [
|
|
17481
|
+
"/usr/bin/sudo",
|
|
17482
|
+
"-n",
|
|
17483
|
+
"/usr/bin/stat",
|
|
17484
|
+
"--format=%u:%g:%a:%h",
|
|
17485
|
+
PLATFORM_CLUSTER_CREDENTIAL_PATH
|
|
17486
|
+
], "verify Metal cluster credential");
|
|
17487
|
+
if (metadata !== "0:0:600:1")
|
|
17488
|
+
throw new Error("Metal cluster credential is not one root-owned 0600 file");
|
|
17489
|
+
const value = await remote(exec, request, knownHosts, [
|
|
17490
|
+
"/usr/bin/sudo",
|
|
17491
|
+
"-n",
|
|
17492
|
+
"/usr/bin/systemd-creds",
|
|
17493
|
+
"decrypt",
|
|
17494
|
+
`--name=${PLATFORM_CLUSTER_CREDENTIAL_NAME}`,
|
|
17495
|
+
PLATFORM_CLUSTER_CREDENTIAL_PATH,
|
|
17496
|
+
"-"
|
|
17497
|
+
], "open Metal cluster credential", true, undefined, true);
|
|
17498
|
+
if (!/^[a-f0-9]{64}$/i.test(value))
|
|
17499
|
+
throw new Error("Metal cluster credential is invalid");
|
|
17500
|
+
return value;
|
|
17501
|
+
} finally {
|
|
17502
|
+
rmSync7(directory, { recursive: true, force: true });
|
|
17503
|
+
}
|
|
17504
|
+
}
|
|
17195
17505
|
var copy = async (exec, request, knownHosts, local, remotePath, secret = false) => {
|
|
17196
17506
|
safeRemoteArg(remotePath);
|
|
17197
17507
|
await checked5(exec, [
|
|
@@ -17308,9 +17618,9 @@ async function verifiedBunArchive(directory, fetcher) {
|
|
|
17308
17618
|
}
|
|
17309
17619
|
async function installPackagedAgent(request, knownHosts, exec, directory, remoteTemp, options) {
|
|
17310
17620
|
const artifacts = [
|
|
17311
|
-
[options.fzCliPath ??
|
|
17312
|
-
[options.fzAgentPath ??
|
|
17313
|
-
[options.fzGitSshPath ??
|
|
17621
|
+
[options.fzCliPath ?? operatorPackagedFzCliPath(), "fz.js"],
|
|
17622
|
+
[options.fzAgentPath ?? operatorPackagedAgentPath(), "fz-agent.js"],
|
|
17623
|
+
[options.fzGitSshPath ?? operatorPackagedGitSshPath(), "fz-git-ssh.js"]
|
|
17314
17624
|
];
|
|
17315
17625
|
for (const [artifact] of artifacts) {
|
|
17316
17626
|
if (!readFileSync12(artifact).length)
|
|
@@ -17347,6 +17657,7 @@ async function installPackagedAgent(request, knownHosts, exec, directory, remote
|
|
|
17347
17657
|
for (const [, name] of artifacts)
|
|
17348
17658
|
await remote(exec, request, knownHosts, ["/usr/bin/sudo", "-n", "/usr/bin/install", "-m", "0755", `${remoteTemp}/${name}`, `/usr/local/lib/forgezero/agent/${name}`], "remote Agent install");
|
|
17349
17659
|
await remote(exec, request, knownHosts, ["/usr/bin/sudo", "-n", "/usr/bin/ln", "-sfn", "/usr/local/lib/forgezero/agent/fz.js", "/usr/local/bin/fz"], "remote fz link");
|
|
17660
|
+
await remote(exec, request, knownHosts, ["/usr/bin/sudo", "-n", "/usr/bin/ln", "-sfn", "/usr/local/lib/forgezero/agent/fz-agent", "/usr/local/bin/fz-agent"], "remote fz-agent link");
|
|
17350
17661
|
}
|
|
17351
17662
|
async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
|
|
17352
17663
|
const plan = planOperatorPlatformBootstrap(request, mode);
|
|
@@ -17373,8 +17684,10 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
|
|
|
17373
17684
|
await copy(exec, request, knownHosts, join11(directory, name), `${remoteTemp}/${name}`, true);
|
|
17374
17685
|
for (const bundle of staged.bundleFiles)
|
|
17375
17686
|
await copy(exec, request, knownHosts, bundle.source, `${remoteTemp}/${bundle.name}`, true);
|
|
17376
|
-
for (const name of ["platform-config.json", ...staged.files
|
|
17687
|
+
for (const name of ["platform-config.json", ...staged.files])
|
|
17377
17688
|
await remote(exec, request, knownHosts, ["/usr/bin/sudo", "-n", "/usr/bin/install", "-m", "0600", `${remoteTemp}/${name}`, `${REMOTE_STAGE}/${name}`], "remote bootstrap handoff", true);
|
|
17689
|
+
for (const { name } of staged.bundleFiles)
|
|
17690
|
+
await remote(exec, request, knownHosts, ["/usr/bin/sudo", "-n", "/usr/bin/install", "-m", "0644", `${remoteTemp}/${name}`, `${REMOTE_STAGE}/${name}`], "remote bootstrap bundle", true);
|
|
17378
17691
|
const command = ["/usr/bin/sudo", "-n", "/usr/local/bin/fz", "bootstrap", "platform", "credentials-stdin"];
|
|
17379
17692
|
command.push("--bootstrap-config", `${REMOTE_STAGE}/platform-config.json`, "--apply");
|
|
17380
17693
|
const output = await remote(exec, request, knownHosts, command, "remote typed bootstrap", mode === "apply", secrets ? `${JSON.stringify(secrets)}
|
|
@@ -17596,7 +17909,7 @@ var PROFILES = {
|
|
|
17596
17909
|
appOrigin: "https://dev.forgezero.net",
|
|
17597
17910
|
apiOrigin: "https://dev-api.forgezero.net",
|
|
17598
17911
|
zoneName: "forgezero.net",
|
|
17599
|
-
kvNamespaceTitle: "forgezero
|
|
17912
|
+
kvNamespaceTitle: "forgezero dev nodes",
|
|
17600
17913
|
workerScriptName: "forgezero-api-edge-development",
|
|
17601
17914
|
region: { key: "in-south", label: "India South", country: "IN" },
|
|
17602
17915
|
agentOtlpEndpoint: "https://otel.forgezero.net",
|
|
@@ -17608,7 +17921,7 @@ var PROFILES = {
|
|
|
17608
17921
|
appOrigin: "https://www.forgezero.net",
|
|
17609
17922
|
apiOrigin: "https://api.forgezero.net",
|
|
17610
17923
|
zoneName: "forgezero.net",
|
|
17611
|
-
kvNamespaceTitle: "forgezero
|
|
17924
|
+
kvNamespaceTitle: "forgezero nodes",
|
|
17612
17925
|
workerScriptName: "forgezero-api-edge-production",
|
|
17613
17926
|
region: { key: "in-south", label: "India South", country: "IN" },
|
|
17614
17927
|
agentOtlpEndpoint: "https://otel.forgezero.net",
|
|
@@ -17707,7 +18020,7 @@ function forgeZeroLaunchTemplate(profile, guests, bundle, owner) {
|
|
|
17707
18020
|
// src/host-maintenance.ts
|
|
17708
18021
|
import { lstatSync as lstatSync8, readFileSync as readFileSync13 } from "fs";
|
|
17709
18022
|
var ROOT = "/opt/forgezero";
|
|
17710
|
-
var SLOT_FILE = `${ROOT}/.
|
|
18023
|
+
var SLOT_FILE = `${ROOT}/slots/.active`;
|
|
17711
18024
|
var SHARED_ENV = `${ROOT}/shared/.env`;
|
|
17712
18025
|
var JWT = "/etc/forgezero/creds/arangodb-jwt.cred";
|
|
17713
18026
|
var FZ = "/usr/local/bin/fz";
|
|
@@ -17836,7 +18149,7 @@ async function applyHostMaintenance(request, runtime = localRuntime()) {
|
|
|
17836
18149
|
}
|
|
17837
18150
|
|
|
17838
18151
|
// src/cli/maintenance.ts
|
|
17839
|
-
import { existsSync as
|
|
18152
|
+
import { existsSync as existsSync12, lstatSync as lstatSync9, readFileSync as readFileSync14, realpathSync as realpathSync5 } from "fs";
|
|
17840
18153
|
import { isAbsolute as isAbsolute6, join as join12, relative as relative2, resolve as resolve10 } from "path";
|
|
17841
18154
|
var API_OPERATION_ENTRYPOINTS = {
|
|
17842
18155
|
"dev-reset": ["src", "server", "maintenance", "dev-reset.ts"],
|
|
@@ -17907,7 +18220,7 @@ function unsupportedRepositoryCliOption(argv2) {
|
|
|
17907
18220
|
}
|
|
17908
18221
|
function manifestName(root) {
|
|
17909
18222
|
const manifestPath = join12(root, "package.json");
|
|
17910
|
-
if (!
|
|
18223
|
+
if (!existsSync12(manifestPath)) {
|
|
17911
18224
|
throw new Error(`No package.json exists at repository root ${root}.`);
|
|
17912
18225
|
}
|
|
17913
18226
|
let parsed;
|
|
@@ -17923,7 +18236,7 @@ function manifestName(root) {
|
|
|
17923
18236
|
}
|
|
17924
18237
|
function checkedEntrypoint(root, parts) {
|
|
17925
18238
|
const candidate = join12(root, ...parts);
|
|
17926
|
-
if (!
|
|
18239
|
+
if (!existsSync12(candidate) || !lstatSync9(candidate).isFile()) {
|
|
17927
18240
|
throw new Error(`The reviewed operation entrypoint is missing: ${candidate}`);
|
|
17928
18241
|
}
|
|
17929
18242
|
if (lstatSync9(candidate).isSymbolicLink()) {
|
|
@@ -18046,7 +18359,7 @@ function planAppBuild(input) {
|
|
|
18046
18359
|
|
|
18047
18360
|
// src/cli/index.ts
|
|
18048
18361
|
var DEFAULT_MODE = (THRESHOLD_MODES.find((mode) => mode.threshold === 1 && mode.total === 1) ?? THRESHOLD_MODES[0]).id;
|
|
18049
|
-
var PACKAGED_AGENT_BIN2 =
|
|
18362
|
+
var PACKAGED_AGENT_BIN2 = operatorPackagedAgentPath();
|
|
18050
18363
|
function parseOptions(argv2) {
|
|
18051
18364
|
const options = {
|
|
18052
18365
|
api: process.env.FZ_API ?? "http://localhost:8787",
|
|
@@ -18792,7 +19105,7 @@ async function cmdAgent(options, args) {
|
|
|
18792
19105
|
out.ok(`Wrote ${auxiliary.path}`);
|
|
18793
19106
|
}
|
|
18794
19107
|
if (options.enrol) {
|
|
18795
|
-
if (!
|
|
19108
|
+
if (!existsSync13(enrolStatePath) && !existsSync13(enrolTokenCredentialPath)) {
|
|
18796
19109
|
const token = await bootstrapSecret("ForgeZero one-time enrolment token");
|
|
18797
19110
|
if (!/^fze_[A-Za-z0-9_-]{40,100}$/.test(token))
|
|
18798
19111
|
throw new Error("A valid fze_ enrolment token was not provided.");
|
|
@@ -18811,7 +19124,7 @@ async function cmdAgent(options, args) {
|
|
|
18811
19124
|
for (const step3 of transcript)
|
|
18812
19125
|
out.ok(step3.label);
|
|
18813
19126
|
out.ok("Agent service and socket verified");
|
|
18814
|
-
if (gitIdentity.gitPublicKeyPath &&
|
|
19127
|
+
if (gitIdentity.gitPublicKeyPath && existsSync13(gitIdentity.gitPublicKeyPath)) {
|
|
18815
19128
|
out.line();
|
|
18816
19129
|
out.line(" Compatibility SSH deploy public key:");
|
|
18817
19130
|
out.line();
|
|
@@ -18855,7 +19168,6 @@ async function bootstrapSecret(question) {
|
|
|
18855
19168
|
throw new Error(`${question} was not provided`);
|
|
18856
19169
|
return value;
|
|
18857
19170
|
}
|
|
18858
|
-
var PLATFORM_CLUSTER_CREDENTIAL = "forgezero-platform-cluster-bootstrap-code";
|
|
18859
19171
|
function readPlatformLaunchEnvironment(path) {
|
|
18860
19172
|
const absolute2 = resolve12(path);
|
|
18861
19173
|
const stat2 = lstatSync10(absolute2);
|
|
@@ -18915,37 +19227,6 @@ function readPlatformLaunchEnvironment(path) {
|
|
|
18915
19227
|
cloudflareApiToken: requiredValue("CF_API_TOKEN")
|
|
18916
19228
|
};
|
|
18917
19229
|
}
|
|
18918
|
-
async function platformLaunchClusterBootstrapCode(outputDirectory) {
|
|
18919
|
-
const path = join13(dirname14(outputDirectory), `${PLATFORM_CLUSTER_CREDENTIAL}.cred`);
|
|
18920
|
-
if (existsSync12(path)) {
|
|
18921
|
-
const stat2 = lstatSync10(path);
|
|
18922
|
-
if (!stat2.isFile() || stat2.isSymbolicLink() || stat2.nlink !== 1 || typeof process.getuid === "function" && stat2.uid !== process.getuid() || (stat2.mode & 63) !== 0) {
|
|
18923
|
-
throw new Error(`platform cluster credential must be one owner-only regular file: ${path}`);
|
|
18924
|
-
}
|
|
18925
|
-
const child2 = Bun.spawn(["systemd-creds", "decrypt", `--name=${PLATFORM_CLUSTER_CREDENTIAL}`, path, "-"], { stdin: "ignore", stdout: "pipe", stderr: "pipe" });
|
|
18926
|
-
const [stdout, stderr2, exitCode2] = await Promise.all([
|
|
18927
|
-
new Response(child2.stdout).text(),
|
|
18928
|
-
new Response(child2.stderr).text(),
|
|
18929
|
-
child2.exited
|
|
18930
|
-
]);
|
|
18931
|
-
if (exitCode2 !== 0)
|
|
18932
|
-
throw new Error(`cannot open the retained platform cluster credential: ${stderr2.trim()}`);
|
|
18933
|
-
const value2 = stdout.trim();
|
|
18934
|
-
if (!/^[a-f0-9]{64}$/i.test(value2))
|
|
18935
|
-
throw new Error("retained platform cluster credential is invalid");
|
|
18936
|
-
return value2;
|
|
18937
|
-
}
|
|
18938
|
-
mkdirSync13(dirname14(path), { recursive: true, mode: 448 });
|
|
18939
|
-
const value = randomBytes10(32).toString("hex");
|
|
18940
|
-
const child = Bun.spawn(["systemd-creds", "encrypt", `--name=${PLATFORM_CLUSTER_CREDENTIAL}`, "-", path], { stdin: "pipe", stdout: "pipe", stderr: "pipe" });
|
|
18941
|
-
await writeAndCloseProcessInput(child.stdin, `${value}
|
|
18942
|
-
`);
|
|
18943
|
-
const [stderr, exitCode] = await Promise.all([new Response(child.stderr).text(), child.exited]);
|
|
18944
|
-
if (exitCode !== 0)
|
|
18945
|
-
throw new Error(`cannot retain the platform cluster credential: ${stderr.trim()}`);
|
|
18946
|
-
chmodSync7(path, 384);
|
|
18947
|
-
return value;
|
|
18948
|
-
}
|
|
18949
19230
|
async function promptPlatformBootstrapSecrets(config, generatedClusterBootstrapCode) {
|
|
18950
19231
|
return validatePlatformBootstrapSecrets(config, {
|
|
18951
19232
|
clusterBootstrapCode: generatedClusterBootstrapCode ?? await bootstrapSecret("Shared 64-hex cluster bootstrap code"),
|
|
@@ -19082,7 +19363,7 @@ async function interactiveCloudflareBootstrap() {
|
|
|
19082
19363
|
function genesisOutputDirectory(path) {
|
|
19083
19364
|
if (!isAbsolute7(path) || resolve12(path) !== path)
|
|
19084
19365
|
throw new Error("--output must be a canonical absolute directory");
|
|
19085
|
-
if (!
|
|
19366
|
+
if (!existsSync13(path))
|
|
19086
19367
|
mkdirSync13(path, { recursive: true, mode: 448 });
|
|
19087
19368
|
const metadata = lstatSync10(path);
|
|
19088
19369
|
const uid = process.getuid?.();
|
|
@@ -19260,14 +19541,14 @@ async function prepareForgeZeroPlatformLaunch(environment, outputDirectory, proj
|
|
|
19260
19541
|
[metalRequestFile, "reviewed Metal request"],
|
|
19261
19542
|
[guestHostKeysFile, "pinned genesis guest host-key evidence"]
|
|
19262
19543
|
]) {
|
|
19263
|
-
if (!
|
|
19544
|
+
if (!existsSync13(path))
|
|
19264
19545
|
throw new Error(`${label} is missing at ${path}`);
|
|
19265
19546
|
}
|
|
19266
19547
|
const root = resolve12(projectRoot);
|
|
19267
19548
|
const nestedApi = join13(root, "api");
|
|
19268
|
-
const repositoryRoot =
|
|
19549
|
+
const repositoryRoot = existsSync13(join13(nestedApi, "package.json")) ? nestedApi : root;
|
|
19269
19550
|
const bundlePath = join13(base, "api.bundle");
|
|
19270
|
-
const bundle =
|
|
19551
|
+
const bundle = existsSync13(bundlePath) || existsSync13(`${bundlePath}.json`) ? await readBootstrapBundle(bundlePath) : await buildBootstrapBundle({ repositoryRoot, outputPath: bundlePath, branch: profile.branch });
|
|
19271
19552
|
if (bundle.manifest.branch !== profile.branch) {
|
|
19272
19553
|
throw new Error(`existing bootstrap bundle is for ${bundle.manifest.branch}, expected ${profile.branch}`);
|
|
19273
19554
|
}
|
|
@@ -19476,14 +19757,17 @@ async function cmdBootstrap(options, args) {
|
|
|
19476
19757
|
if (environment === "production" && options.bootstrapSecretsEnvFile) {
|
|
19477
19758
|
throw new Error("production platform launch requires interactive hidden secret prompts");
|
|
19478
19759
|
}
|
|
19479
|
-
const outputDirectory = options.outputPath ??
|
|
19760
|
+
const outputDirectory = options.outputPath ?? join13(homedir2(), ".forgezero-secure", environment, "genesis");
|
|
19480
19761
|
const environmentInput = options.bootstrapSecretsEnvFile ? readPlatformLaunchEnvironment(options.bootstrapSecretsEnvFile) : undefined;
|
|
19481
|
-
const
|
|
19482
|
-
const fleet = readOperatorPlatformBootstrapFleetRequest(
|
|
19762
|
+
const existingFleetPath = join13(outputDirectory, "platform-fleet-remote.json");
|
|
19763
|
+
const fleet = existsSync13(existingFleetPath) ? readOperatorPlatformBootstrapFleetRequest(existingFleetPath) : readOperatorPlatformBootstrapFleetRequest((await prepareForgeZeroPlatformLaunch(environment, outputDirectory, options.projectRoot, environmentInput?.owner)).fleetRequest);
|
|
19764
|
+
if (planOperatorPlatformFleetBootstrap(fleet, "apply").environment !== environment) {
|
|
19765
|
+
throw new Error("existing platform fleet does not match the selected launch profile");
|
|
19766
|
+
}
|
|
19483
19767
|
if (options.apply && !environmentInput && bootstrapAnswer("Type APPLY to run the reviewed Cloudflare and three-compute plan") !== "APPLY") {
|
|
19484
19768
|
throw new Error("attended platform bootstrap was not confirmed");
|
|
19485
19769
|
}
|
|
19486
|
-
return await runAttendedPlatformFleet(fleet, options.apply, options.apply ? await
|
|
19770
|
+
return await runAttendedPlatformFleet(fleet, options.apply, options.apply ? await operatorMetalClusterBootstrapCode(readOperatorMetalBootstrapRequest(join13(dirname14(outputDirectory), "metal", "metal-remote.json"), { validateMetalConfig: false })) : undefined, environmentInput);
|
|
19487
19771
|
}
|
|
19488
19772
|
if (operation === "platform" && args[1] === "fleet") {
|
|
19489
19773
|
const mode = args[2];
|
|
@@ -19606,8 +19890,8 @@ async function cmdBootstrap(options, args) {
|
|
|
19606
19890
|
if (credentialStdin && args[2] !== undefined)
|
|
19607
19891
|
throw new Error("platform credential stdin accepts no additional positional arguments");
|
|
19608
19892
|
const installedBootstrapKind = () => resolveInstalledBootstrapKind({
|
|
19609
|
-
metal:
|
|
19610
|
-
compute:
|
|
19893
|
+
metal: existsSync13(METAL_BOOTSTRAP_STATE_PATH),
|
|
19894
|
+
compute: existsSync13(BOOTSTRAP_STATE_PATH)
|
|
19611
19895
|
});
|
|
19612
19896
|
if (operation === "status") {
|
|
19613
19897
|
if (installedBootstrapKind() === "metal") {
|
|
@@ -19978,7 +20262,7 @@ function projectFromCheckout(options) {
|
|
|
19978
20262
|
try {
|
|
19979
20263
|
return loadConfig({
|
|
19980
20264
|
cwd: options.projectRoot,
|
|
19981
|
-
readFile: (path) =>
|
|
20265
|
+
readFile: (path) => existsSync13(path) ? readFileSync16(path, "utf8") : undefined
|
|
19982
20266
|
}).config.project;
|
|
19983
20267
|
} catch (cause) {
|
|
19984
20268
|
throw new Error(`No --project was given and the checkout has no usable .fz/config.json: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
@@ -20100,7 +20384,7 @@ async function cmdDeploy(options, args) {
|
|
|
20100
20384
|
return problems.length === 0 ? 0 : 1;
|
|
20101
20385
|
}
|
|
20102
20386
|
if (operation === "check" || operation === "sync") {
|
|
20103
|
-
if (
|
|
20387
|
+
if (existsSync13(resolve12(options.projectRoot, DEPLOY_SOURCE_FILE))) {
|
|
20104
20388
|
const expected = await compileDeploymentProject(options.projectRoot, { write: false });
|
|
20105
20389
|
const actual = inspectCompiledDeployment(options.projectRoot);
|
|
20106
20390
|
const current2 = canonicalJson(expected.plan) === canonicalJson(actual.plan);
|