@forgezero/agent 0.1.33 → 0.1.35
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 +21 -0
- package/dist/agent-heartbeat.js +1 -1
- package/dist/bootstrap.d.ts +128 -0
- package/dist/bootstrap.js +3361 -0
- package/dist/capacity-calibration.d.ts +51 -0
- package/dist/capacity-calibration.js +119 -0
- package/dist/cli/cloudflare-bootstrap.d.ts +11 -0
- package/dist/cloudflare-bootstrap.d.ts +218 -0
- package/dist/cloudflare-bootstrap.js +1196 -0
- package/dist/cloudflare-edge.d.ts +265 -0
- package/dist/cloudflare-edge.js +424 -0
- package/dist/definition.d.ts +3 -0
- package/dist/definition.js +174 -2
- package/dist/deploy-file.js +174 -2
- package/dist/deployment.d.ts +11 -0
- package/dist/fz-agent.js +1855 -3370
- package/dist/fz.js +8092 -6437
- package/dist/index.d.ts +2 -0
- package/dist/metal-bootstrap.d.ts +52 -0
- package/dist/metal-bootstrap.js +942 -0
- package/dist/platform-bootstrap-runtime.d.ts +161 -0
- package/dist/platform-bootstrap-runtime.js +462 -0
- package/dist/provision.js +5 -4
- package/dist/version.d.ts +1 -1
- package/package.json +26 -2
- package/schema/deploy-v2.json +14 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/** Typed, side-effect-free host runtime plans used by `fz bootstrap platform`. */
|
|
2
|
+
export type PlatformSoftwareProfile = 'platform-db-api' | 'platform-api';
|
|
3
|
+
export type PlatformDatabaseRole = 'master' | 'joiner' | 'none';
|
|
4
|
+
export type ApiSlot = 'blue' | 'green';
|
|
5
|
+
export interface PlatformSharedEnvironment {
|
|
6
|
+
softwareProfile: PlatformSoftwareProfile;
|
|
7
|
+
databaseRole: PlatformDatabaseRole;
|
|
8
|
+
databaseCoordinators: string[];
|
|
9
|
+
databaseAddress?: string;
|
|
10
|
+
databaseMaster?: string;
|
|
11
|
+
databaseNetworkMode: 'private-lan' | 'cloudflare-warp';
|
|
12
|
+
databaseReplicationFactor: number;
|
|
13
|
+
databaseWriteConcern: number;
|
|
14
|
+
databaseUser: string;
|
|
15
|
+
nodeHostname: string;
|
|
16
|
+
nodeRegion: string;
|
|
17
|
+
nodeRole: 'guest';
|
|
18
|
+
appOrigin: string;
|
|
19
|
+
apiOrigin: string;
|
|
20
|
+
publicApiPort: number;
|
|
21
|
+
sharedDirectory: string;
|
|
22
|
+
seedSyncPeers: string[];
|
|
23
|
+
seedSyncMembers: number;
|
|
24
|
+
seedSyncEpoch: string;
|
|
25
|
+
concurrencyLimit: number;
|
|
26
|
+
drainDeadlineMs: number;
|
|
27
|
+
otlpEndpoint: 'http://127.0.0.1:4318';
|
|
28
|
+
otlpCollectorUnit: string;
|
|
29
|
+
agentOtlpEndpoint: string;
|
|
30
|
+
otlpFlushIntervalMs: number;
|
|
31
|
+
otlpTraceSampleRatio: number;
|
|
32
|
+
custodianEmail?: string;
|
|
33
|
+
smtp?: {
|
|
34
|
+
host: string;
|
|
35
|
+
port: number;
|
|
36
|
+
user?: string;
|
|
37
|
+
from: string;
|
|
38
|
+
};
|
|
39
|
+
backup?: {
|
|
40
|
+
endpoint: string;
|
|
41
|
+
region: string;
|
|
42
|
+
bucket: string;
|
|
43
|
+
accessKeyId: string;
|
|
44
|
+
};
|
|
45
|
+
cloudflare?: {
|
|
46
|
+
accountId: string;
|
|
47
|
+
zoneId: string;
|
|
48
|
+
kvNamespaceId: string;
|
|
49
|
+
tunnelId: string;
|
|
50
|
+
tunnelService: string;
|
|
51
|
+
warp?: {
|
|
52
|
+
organization: string;
|
|
53
|
+
virtualNetworkId: string;
|
|
54
|
+
deviceProfileId: string;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
repository: string;
|
|
58
|
+
branch: string;
|
|
59
|
+
deployProfile: string;
|
|
60
|
+
}
|
|
61
|
+
export declare function validatePlatformSharedEnvironment(input: PlatformSharedEnvironment, options?: {
|
|
62
|
+
allowPendingCloudflareHandoff?: boolean;
|
|
63
|
+
}): PlatformSharedEnvironment;
|
|
64
|
+
/** Render only non-secret runtime coordinates. Passwords/tokens have no field in this contract. */
|
|
65
|
+
export declare function renderPlatformSharedEnvironment(input: PlatformSharedEnvironment): string;
|
|
66
|
+
export interface SystemdCredentialSpec {
|
|
67
|
+
name: 'arangodb-jwt' | 'seed-sync-root' | 'bootstrap-smtp-password' | 'cloudflare-kv-token' | 'cloudflare-network-token';
|
|
68
|
+
encryptedPath: string;
|
|
69
|
+
required: boolean;
|
|
70
|
+
}
|
|
71
|
+
export declare function platformApiCredentialSpecs(options: {
|
|
72
|
+
smtp: boolean;
|
|
73
|
+
cloudflareKv: boolean;
|
|
74
|
+
cloudflareNetwork: boolean;
|
|
75
|
+
}): SystemdCredentialSpec[];
|
|
76
|
+
export interface ApiRuntimeRenderOptions {
|
|
77
|
+
serviceUser: string;
|
|
78
|
+
sharedDirectory: string;
|
|
79
|
+
sharedEnvironmentFile: string;
|
|
80
|
+
slotsDirectory: string;
|
|
81
|
+
bluePort: number;
|
|
82
|
+
greenPort: number;
|
|
83
|
+
collectorUnit: string;
|
|
84
|
+
credentials: SystemdCredentialSpec[];
|
|
85
|
+
}
|
|
86
|
+
export declare function renderPlatformApiUnits(input: ApiRuntimeRenderOptions): {
|
|
87
|
+
template: string;
|
|
88
|
+
dropIns: Record<ApiSlot, string>;
|
|
89
|
+
};
|
|
90
|
+
export declare function renderPlatformNginx(input: {
|
|
91
|
+
publicPort: number;
|
|
92
|
+
initialSlotPort: number;
|
|
93
|
+
}): {
|
|
94
|
+
upstream: string;
|
|
95
|
+
site: string;
|
|
96
|
+
};
|
|
97
|
+
export interface ActivationBoundary {
|
|
98
|
+
command: '/usr/local/libexec/forgezero-activate';
|
|
99
|
+
argv: [string];
|
|
100
|
+
runAs: 'root';
|
|
101
|
+
invoker: 'forgezero-runner';
|
|
102
|
+
}
|
|
103
|
+
/** The caller passes an immutable release path, never a shell command. */
|
|
104
|
+
export declare function planPlatformActivation(releasesDirectory: string, releasePath: string): ActivationBoundary;
|
|
105
|
+
export interface PlatformActivationConfig {
|
|
106
|
+
root: string;
|
|
107
|
+
serviceUser: string;
|
|
108
|
+
bluePort: number;
|
|
109
|
+
greenPort: number;
|
|
110
|
+
healthPath: string;
|
|
111
|
+
keepReleases: number;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Render the fixed privilege boundary used by the credential-free deployment
|
|
115
|
+
* runner. Repository data supplies one release path; it never supplies shell.
|
|
116
|
+
*/
|
|
117
|
+
export declare function renderPlatformActivationFiles(input: PlatformActivationConfig): {
|
|
118
|
+
environment: string;
|
|
119
|
+
helper: string;
|
|
120
|
+
sudoers: string;
|
|
121
|
+
};
|
|
122
|
+
export interface LocalOtlpProofPlan {
|
|
123
|
+
unitCheck: {
|
|
124
|
+
command: 'systemctl';
|
|
125
|
+
argv: ['is-active', '--quiet', string];
|
|
126
|
+
};
|
|
127
|
+
receiverCheck: {
|
|
128
|
+
command: 'curl';
|
|
129
|
+
argv: string[];
|
|
130
|
+
acceptedStatus: '2xx';
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
export declare function planLocalOtlpProof(endpoint: string, collectorUnit: string): LocalOtlpProofPlan;
|
|
134
|
+
export type BringupStage = 'runtime-ready' | 'invite-prepared' | 'deploy-requested' | 'deploy-healthy' | 'invite-presented' | 'complete' | 'failed';
|
|
135
|
+
export interface PlatformBringupState {
|
|
136
|
+
requiresFirstInvite: boolean;
|
|
137
|
+
stage: BringupStage;
|
|
138
|
+
inviteTokenFilePresent: boolean;
|
|
139
|
+
lastError?: string;
|
|
140
|
+
}
|
|
141
|
+
export type PlatformBringupEvent = {
|
|
142
|
+
type: 'invite-prepared';
|
|
143
|
+
tokenFilePresent: boolean;
|
|
144
|
+
} | {
|
|
145
|
+
type: 'deploy-requested';
|
|
146
|
+
} | {
|
|
147
|
+
type: 'deploy-healthy';
|
|
148
|
+
} | {
|
|
149
|
+
type: 'invite-presented';
|
|
150
|
+
} | {
|
|
151
|
+
type: 'complete';
|
|
152
|
+
} | {
|
|
153
|
+
type: 'failed';
|
|
154
|
+
error: string;
|
|
155
|
+
};
|
|
156
|
+
export declare function initialPlatformBringupState(requiresFirstInvite: boolean): PlatformBringupState;
|
|
157
|
+
/** Strict persisted sequencing: genesis invite before deploy, presentation only after health. */
|
|
158
|
+
export declare function advancePlatformBringup(state: PlatformBringupState, event: PlatformBringupEvent): PlatformBringupState;
|
|
159
|
+
export type PlatformBringupAction = 'prepare-invite' | 'request-deploy' | 'await-deploy-health' | 'present-invite' | 'mark-complete' | 'none';
|
|
160
|
+
export declare function nextPlatformBringupAction(state: PlatformBringupState): PlatformBringupAction;
|
|
161
|
+
export declare function repairPlatformBringup(state: PlatformBringupState): PlatformBringupState;
|
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
// src/platform-bootstrap-runtime.ts
|
|
2
|
+
var safeAtom = (name, value) => {
|
|
3
|
+
if (!value || /[\0\r\n]/.test(value))
|
|
4
|
+
throw new Error(`${name} must be non-empty and single-line.`);
|
|
5
|
+
return value;
|
|
6
|
+
};
|
|
7
|
+
var boundedInteger = (name, value, minimum, maximum) => {
|
|
8
|
+
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
|
|
9
|
+
throw new Error(`${name} must be an integer from ${minimum} through ${maximum}.`);
|
|
10
|
+
}
|
|
11
|
+
return value;
|
|
12
|
+
};
|
|
13
|
+
var privateCoordinator = (raw) => {
|
|
14
|
+
const url = new URL(raw);
|
|
15
|
+
if (url.protocol !== "http:" || url.username || url.password || url.search || url.hash || url.pathname !== "/") {
|
|
16
|
+
throw new Error("ArangoDB coordinator URLs must be credential-free private HTTP origins.");
|
|
17
|
+
}
|
|
18
|
+
const host = url.hostname.replace(/^\[|\]$/g, "");
|
|
19
|
+
const privateHost = host === "localhost" || host === "::1" || host.startsWith("fd") || host.startsWith("fc") || /^127\./.test(host) || /^10\./.test(host) || /^192\.168\./.test(host) || /^172\.(1[6-9]|2\d|3[01])\./.test(host);
|
|
20
|
+
if (!privateHost || url.port && url.port !== "8529") {
|
|
21
|
+
throw new Error("ArangoDB coordinators must use private addresses and port 8529.");
|
|
22
|
+
}
|
|
23
|
+
return url.origin;
|
|
24
|
+
};
|
|
25
|
+
var httpsOrigin = (name, raw) => {
|
|
26
|
+
const value = new URL(raw);
|
|
27
|
+
if (value.protocol !== "https:" || value.username || value.password || value.search || value.hash || value.pathname !== "/") {
|
|
28
|
+
throw new Error(`${name} must be a credential-free HTTPS origin.`);
|
|
29
|
+
}
|
|
30
|
+
return value.origin;
|
|
31
|
+
};
|
|
32
|
+
var systemdValue = (name, raw) => {
|
|
33
|
+
if (/[\0\r\n]/.test(raw))
|
|
34
|
+
throw new Error(`${name} must be single-line.`);
|
|
35
|
+
const value = raw;
|
|
36
|
+
return `"${value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"").replaceAll("$", "\\$")}"`;
|
|
37
|
+
};
|
|
38
|
+
function validatePlatformSharedEnvironment(input, options = {}) {
|
|
39
|
+
if (input.softwareProfile === "platform-api" !== (input.databaseRole === "none")) {
|
|
40
|
+
throw new Error("platform-api requires database role none; platform-db-api requires master or joiner.");
|
|
41
|
+
}
|
|
42
|
+
if (input.databaseCoordinators.length < 1 || input.databaseCoordinators.length > 16) {
|
|
43
|
+
throw new Error("databaseCoordinators must contain 1 through 16 endpoints.");
|
|
44
|
+
}
|
|
45
|
+
const coordinators = input.databaseCoordinators.map(privateCoordinator);
|
|
46
|
+
if (new Set(coordinators).size !== coordinators.length)
|
|
47
|
+
throw new Error("databaseCoordinators must be unique.");
|
|
48
|
+
if (!["private-lan", "cloudflare-warp"].includes(input.databaseNetworkMode)) {
|
|
49
|
+
throw new Error("Database networking must be private-lan or cloudflare-warp.");
|
|
50
|
+
}
|
|
51
|
+
boundedInteger("databaseReplicationFactor", input.databaseReplicationFactor, 1, 16);
|
|
52
|
+
boundedInteger("databaseWriteConcern", input.databaseWriteConcern, 1, 16);
|
|
53
|
+
if (input.databaseWriteConcern > input.databaseReplicationFactor) {
|
|
54
|
+
throw new Error("databaseWriteConcern cannot exceed databaseReplicationFactor.");
|
|
55
|
+
}
|
|
56
|
+
boundedInteger("publicApiPort", input.publicApiPort, 1024, 65533);
|
|
57
|
+
boundedInteger("seedSyncMembers", input.seedSyncMembers, 1, 64);
|
|
58
|
+
boundedInteger("concurrencyLimit", input.concurrencyLimit, 1, 1e6);
|
|
59
|
+
boundedInteger("drainDeadlineMs", input.drainDeadlineMs, 1000, 300000);
|
|
60
|
+
boundedInteger("otlpFlushIntervalMs", input.otlpFlushIntervalMs, 1000, 300000);
|
|
61
|
+
if (!Number.isFinite(input.otlpTraceSampleRatio) || input.otlpTraceSampleRatio < 0 || input.otlpTraceSampleRatio > 1) {
|
|
62
|
+
throw new Error("otlpTraceSampleRatio must be from 0 through 1.");
|
|
63
|
+
}
|
|
64
|
+
if (input.otlpEndpoint !== "http://127.0.0.1:4318")
|
|
65
|
+
throw new Error("OTLP must use the exact local collector endpoint.");
|
|
66
|
+
validateCollectorUnit(input.otlpCollectorUnit);
|
|
67
|
+
httpsOrigin("agentOtlpEndpoint", input.agentOtlpEndpoint);
|
|
68
|
+
for (const [name, value] of Object.entries({
|
|
69
|
+
nodeHostname: input.nodeHostname,
|
|
70
|
+
nodeRegion: input.nodeRegion,
|
|
71
|
+
databaseUser: input.databaseUser,
|
|
72
|
+
sharedDirectory: input.sharedDirectory,
|
|
73
|
+
seedSyncEpoch: input.seedSyncEpoch,
|
|
74
|
+
repository: input.repository,
|
|
75
|
+
branch: input.branch,
|
|
76
|
+
deployProfile: input.deployProfile
|
|
77
|
+
}))
|
|
78
|
+
safeAtom(name, value);
|
|
79
|
+
if (!input.sharedDirectory.startsWith("/"))
|
|
80
|
+
throw new Error("sharedDirectory must be absolute.");
|
|
81
|
+
for (const peer of input.seedSyncPeers) {
|
|
82
|
+
const url = new URL(peer);
|
|
83
|
+
if (url.protocol !== "ws:" && url.protocol !== "wss:")
|
|
84
|
+
throw new Error("Seed peers must be WebSocket URLs.");
|
|
85
|
+
if (url.username || url.password || url.hash)
|
|
86
|
+
throw new Error("Seed peers cannot contain credentials or fragments.");
|
|
87
|
+
}
|
|
88
|
+
if (input.smtp) {
|
|
89
|
+
safeAtom("smtp.host", input.smtp.host);
|
|
90
|
+
boundedInteger("smtp.port", input.smtp.port, 1, 65535);
|
|
91
|
+
safeAtom("smtp.from", input.smtp.from);
|
|
92
|
+
if (input.smtp.user)
|
|
93
|
+
safeAtom("smtp.user", input.smtp.user);
|
|
94
|
+
}
|
|
95
|
+
if (input.backup) {
|
|
96
|
+
httpsOrigin("backup.endpoint", input.backup.endpoint);
|
|
97
|
+
for (const [name, value] of Object.entries(input.backup))
|
|
98
|
+
safeAtom(`backup.${name}`, value);
|
|
99
|
+
}
|
|
100
|
+
if (input.cloudflare) {
|
|
101
|
+
if (![input.cloudflare.accountId, input.cloudflare.zoneId, input.cloudflare.kvNamespaceId].every((item) => /^[a-f0-9]{32}$/i.test(item)) || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(input.cloudflare.tunnelId)) {
|
|
102
|
+
throw new Error("Cloudflare account, zone, KV and Tunnel ids are malformed.");
|
|
103
|
+
}
|
|
104
|
+
const service = new URL(input.cloudflare.tunnelService);
|
|
105
|
+
if (service.protocol !== "http:" || !["127.0.0.1", "localhost", "::1"].includes(service.hostname) || service.username || service.password || service.search || service.hash)
|
|
106
|
+
throw new Error("Cloudflare Tunnel service must be loopback HTTP.");
|
|
107
|
+
if (input.cloudflare.warp) {
|
|
108
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9-]{0,62}$/.test(input.cloudflare.warp.organization) || !/^[0-9a-f-]{36}$/i.test(input.cloudflare.warp.virtualNetworkId) || !/^[A-Za-z0-9_-]{1,128}$/.test(input.cloudflare.warp.deviceProfileId)) {
|
|
109
|
+
throw new Error("Cloudflare WARP organization, VNET or device profile is malformed.");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (!options.allowPendingCloudflareHandoff && input.databaseNetworkMode === "cloudflare-warp" !== Boolean(input.cloudflare?.warp)) {
|
|
114
|
+
throw new Error("cloudflare-warp networking requires its exact enrolled Cloudflare coordinates.");
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
...input,
|
|
118
|
+
databaseCoordinators: coordinators,
|
|
119
|
+
appOrigin: httpsOrigin("appOrigin", input.appOrigin),
|
|
120
|
+
apiOrigin: httpsOrigin("apiOrigin", input.apiOrigin),
|
|
121
|
+
agentOtlpEndpoint: httpsOrigin("agentOtlpEndpoint", input.agentOtlpEndpoint)
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function renderPlatformSharedEnvironment(input) {
|
|
125
|
+
const value = validatePlatformSharedEnvironment(input);
|
|
126
|
+
const appHost = new URL(value.appOrigin).hostname.split(".").slice(-2).join(".");
|
|
127
|
+
const apiHost = new URL(value.apiOrigin).hostname.split(".").slice(-2).join(".");
|
|
128
|
+
const entries = {
|
|
129
|
+
ARANGO_URL: value.databaseCoordinators[0],
|
|
130
|
+
ARANGO_URLS: value.databaseCoordinators.join(","),
|
|
131
|
+
ARANGO_DB: "fz",
|
|
132
|
+
FZ_DATABASE_MODE: "platform",
|
|
133
|
+
ARANGO_USER: value.databaseUser,
|
|
134
|
+
ARANGO_REPLICATION_FACTOR: String(value.databaseReplicationFactor),
|
|
135
|
+
ARANGO_WRITE_CONCERN: String(value.databaseWriteConcern),
|
|
136
|
+
FZ_DB_ROLE: value.databaseRole,
|
|
137
|
+
FZ_SOFTWARE_PROFILE: value.softwareProfile,
|
|
138
|
+
FZ_ROLE: value.nodeRole,
|
|
139
|
+
FZ_DB_ADDRESS: value.databaseAddress ?? "",
|
|
140
|
+
FZ_DB_MASTER: value.databaseMaster ?? "",
|
|
141
|
+
FZ_DB_NETWORK_MODE: value.databaseNetworkMode,
|
|
142
|
+
FZ_SEED_SYNC_PEERS: value.seedSyncPeers.join(","),
|
|
143
|
+
FZ_SEED_SYNC_MEMBERS: String(value.seedSyncMembers),
|
|
144
|
+
FZ_SEED_SYNC_EPOCH: value.seedSyncEpoch,
|
|
145
|
+
FZ_SEED_SYNC_CREDENTIAL: "seed-sync-root",
|
|
146
|
+
FZ_SHARED_DIR: value.sharedDirectory,
|
|
147
|
+
FZ_PUBLIC_API_PORT: String(value.publicApiPort),
|
|
148
|
+
ORIGIN: value.appOrigin,
|
|
149
|
+
API_ORIGIN: value.apiOrigin,
|
|
150
|
+
HOST: "127.0.0.1",
|
|
151
|
+
APP_ORIGINS: value.appOrigin,
|
|
152
|
+
TRUST_CLOUDFLARE_IP: "1",
|
|
153
|
+
SESSION_COOKIE_SAMESITE: appHost === apiHost ? "lax" : "none",
|
|
154
|
+
SESSION_COOKIE_DOMAIN: "",
|
|
155
|
+
FZ_NODE_HOSTNAME: value.nodeHostname,
|
|
156
|
+
FZ_NODE_REGION: value.nodeRegion,
|
|
157
|
+
FZ_CONCURRENCY_LIMIT: String(value.concurrencyLimit),
|
|
158
|
+
FZ_DRAIN_DEADLINE_MS: String(value.drainDeadlineMs),
|
|
159
|
+
OTEL_EXPORTER_OTLP_ENDPOINT: value.otlpEndpoint,
|
|
160
|
+
FZ_OTLP_COLLECTOR_UNIT: value.otlpCollectorUnit,
|
|
161
|
+
OTEL_SERVICE_NAME: "forgezero-api",
|
|
162
|
+
FZ_OTLP_FLUSH_INTERVAL_MS: String(value.otlpFlushIntervalMs),
|
|
163
|
+
FZ_OTLP_TRACE_SAMPLE_RATIO: String(value.otlpTraceSampleRatio),
|
|
164
|
+
FZ_AGENT_OTLP_ENDPOINT: value.agentOtlpEndpoint,
|
|
165
|
+
FZ_CUSTODIAN_EMAIL: value.custodianEmail ?? "",
|
|
166
|
+
FZ_PROFILE: value.deployProfile,
|
|
167
|
+
FZ_REPO: value.repository,
|
|
168
|
+
FZ_BRANCH: value.branch,
|
|
169
|
+
FZ_SMTP_HOST: value.smtp?.host ?? "",
|
|
170
|
+
FZ_SMTP_PORT: value.smtp ? String(value.smtp.port) : "",
|
|
171
|
+
FZ_SMTP_USER: value.smtp?.user ?? "",
|
|
172
|
+
FZ_SMTP_FROM: value.smtp?.from ?? "",
|
|
173
|
+
BACKUP_S3_ENDPOINT: value.backup?.endpoint ?? "",
|
|
174
|
+
BACKUP_S3_REGION: value.backup?.region ?? "",
|
|
175
|
+
BACKUP_S3_BUCKET: value.backup?.bucket ?? "",
|
|
176
|
+
BACKUP_S3_ACCESS_KEY_ID: value.backup?.accessKeyId ?? "",
|
|
177
|
+
FZ_CF_ACCOUNT_ID: value.cloudflare?.accountId ?? "",
|
|
178
|
+
FZ_CF_ZONE_ID: value.cloudflare?.zoneId ?? "",
|
|
179
|
+
FZ_CF_KV_NAMESPACE_ID: value.cloudflare?.kvNamespaceId ?? "",
|
|
180
|
+
FZ_CF_TUNNEL_ID: value.cloudflare?.tunnelId ?? "",
|
|
181
|
+
FZ_CF_TUNNEL_SERVICE: value.cloudflare?.tunnelService ?? "",
|
|
182
|
+
FZ_WARP_ORGANIZATION: value.cloudflare?.warp?.organization ?? "",
|
|
183
|
+
FZ_CF_VIRTUAL_NETWORK_ID: value.cloudflare?.warp?.virtualNetworkId ?? "",
|
|
184
|
+
FZ_CF_WARP_POLICY_ID: value.cloudflare?.warp?.deviceProfileId ?? ""
|
|
185
|
+
};
|
|
186
|
+
return `# Generated by fz bootstrap platform. Non-secret coordinates only.
|
|
187
|
+
` + Object.entries(entries).map(([key, entry]) => `${key}=${systemdValue(key, entry)}`).join(`
|
|
188
|
+
`) + `
|
|
189
|
+
`;
|
|
190
|
+
}
|
|
191
|
+
function platformApiCredentialSpecs(options) {
|
|
192
|
+
const optional = [
|
|
193
|
+
["bootstrap-smtp-password", options.smtp],
|
|
194
|
+
["cloudflare-kv-token", options.cloudflareKv],
|
|
195
|
+
["cloudflare-network-token", options.cloudflareNetwork]
|
|
196
|
+
];
|
|
197
|
+
return [
|
|
198
|
+
{ name: "arangodb-jwt", encryptedPath: "/etc/forgezero/creds/arangodb-jwt.cred", required: true },
|
|
199
|
+
{ name: "seed-sync-root", encryptedPath: "/etc/forgezero/creds/seed-sync-root.cred", required: true },
|
|
200
|
+
...optional.filter(([, present]) => present).map(([name]) => ({
|
|
201
|
+
name,
|
|
202
|
+
encryptedPath: `/etc/forgezero/creds/${name}.cred`,
|
|
203
|
+
required: false
|
|
204
|
+
}))
|
|
205
|
+
];
|
|
206
|
+
}
|
|
207
|
+
function renderPlatformApiUnits(input) {
|
|
208
|
+
for (const path of [input.sharedDirectory, input.sharedEnvironmentFile, input.slotsDirectory]) {
|
|
209
|
+
if (!path.startsWith("/") || /[\r\n]/.test(path))
|
|
210
|
+
throw new Error("Runtime paths must be absolute and single-line.");
|
|
211
|
+
}
|
|
212
|
+
if (!/^[a-z_][a-z0-9_-]{0,31}$/.test(input.serviceUser))
|
|
213
|
+
throw new Error("Invalid service user.");
|
|
214
|
+
validateCollectorUnit(input.collectorUnit);
|
|
215
|
+
boundedInteger("bluePort", input.bluePort, 1024, 65535);
|
|
216
|
+
boundedInteger("greenPort", input.greenPort, 1024, 65535);
|
|
217
|
+
if (input.bluePort === input.greenPort)
|
|
218
|
+
throw new Error("Blue and green ports must differ.");
|
|
219
|
+
const credentials = input.credentials.map((credential) => `LoadCredentialEncrypted=${credential.name}:${credential.encryptedPath}`).join(`
|
|
220
|
+
`);
|
|
221
|
+
const template = `[Unit]
|
|
222
|
+
Description=ForgeZero (%i slot)
|
|
223
|
+
After=network-online.target ${input.collectorUnit}
|
|
224
|
+
Wants=network-online.target ${input.collectorUnit}
|
|
225
|
+
|
|
226
|
+
[Service]
|
|
227
|
+
Type=simple
|
|
228
|
+
User=${input.serviceUser}
|
|
229
|
+
WorkingDirectory=${input.slotsDirectory}/%i
|
|
230
|
+
Environment=NODE_ENV=production
|
|
231
|
+
Environment=FZ_SLOT=%i
|
|
232
|
+
EnvironmentFile=${input.sharedEnvironmentFile}
|
|
233
|
+
${credentials}
|
|
234
|
+
ExecStart=/usr/local/bin/bun run ${input.slotsDirectory}/%i/src/index.ts
|
|
235
|
+
Restart=always
|
|
236
|
+
RestartSec=2
|
|
237
|
+
TimeoutStopSec=35s
|
|
238
|
+
LimitCORE=0
|
|
239
|
+
UMask=0077
|
|
240
|
+
NoNewPrivileges=yes
|
|
241
|
+
PrivateTmp=yes
|
|
242
|
+
PrivateDevices=yes
|
|
243
|
+
ProtectSystem=strict
|
|
244
|
+
ProtectHome=yes
|
|
245
|
+
ReadOnlyPaths=${input.sharedDirectory}
|
|
246
|
+
ProtectKernelTunables=yes
|
|
247
|
+
ProtectKernelModules=yes
|
|
248
|
+
ProtectControlGroups=yes
|
|
249
|
+
RestrictSUIDSGID=yes
|
|
250
|
+
RestrictRealtime=yes
|
|
251
|
+
LockPersonality=yes
|
|
252
|
+
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
|
253
|
+
|
|
254
|
+
[Install]
|
|
255
|
+
WantedBy=multi-user.target
|
|
256
|
+
`;
|
|
257
|
+
return { template, dropIns: {
|
|
258
|
+
blue: `[Service]
|
|
259
|
+
Environment=PORT=${input.bluePort}
|
|
260
|
+
`,
|
|
261
|
+
green: `[Service]
|
|
262
|
+
Environment=PORT=${input.greenPort}
|
|
263
|
+
`
|
|
264
|
+
} };
|
|
265
|
+
}
|
|
266
|
+
function renderPlatformNginx(input) {
|
|
267
|
+
boundedInteger("publicPort", input.publicPort, 1024, 65535);
|
|
268
|
+
boundedInteger("initialSlotPort", input.initialSlotPort, 1024, 65535);
|
|
269
|
+
if (input.publicPort === input.initialSlotPort)
|
|
270
|
+
throw new Error("Edge and slot ports must differ.");
|
|
271
|
+
return {
|
|
272
|
+
upstream: `upstream forgezero { server 127.0.0.1:${input.initialSlotPort}; }
|
|
273
|
+
`,
|
|
274
|
+
site: `server {
|
|
275
|
+
listen 127.0.0.1:${input.publicPort};
|
|
276
|
+
server_name _;
|
|
277
|
+
location ^~ /api/ { proxy_pass http://forgezero; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto https; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }
|
|
278
|
+
location ^~ /v1/ { proxy_pass http://forgezero; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto https; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }
|
|
279
|
+
location / { return 404; }
|
|
280
|
+
}
|
|
281
|
+
`
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
function planPlatformActivation(releasesDirectory, releasePath) {
|
|
285
|
+
if (!releasesDirectory.startsWith("/") || !releasePath.startsWith("/") || /[\0\r\n]/.test(releasePath)) {
|
|
286
|
+
throw new Error("Activation paths must be absolute and single-line.");
|
|
287
|
+
}
|
|
288
|
+
const root = releasesDirectory.replace(/\/+$/, "");
|
|
289
|
+
if (!releasePath.startsWith(`${root}/`) || releasePath === root || releasePath.includes("/../")) {
|
|
290
|
+
throw new Error("Release must be an immutable child of the releases directory.");
|
|
291
|
+
}
|
|
292
|
+
return { command: "/usr/local/libexec/forgezero-activate", argv: [releasePath], runAs: "root", invoker: "forgezero-runner" };
|
|
293
|
+
}
|
|
294
|
+
function renderPlatformActivationFiles(input) {
|
|
295
|
+
if (!input.root.startsWith("/") || /[\0\r\n]/.test(input.root))
|
|
296
|
+
throw new Error("Activation root must be absolute and single-line.");
|
|
297
|
+
if (!/^[a-z_][a-z0-9_-]{0,31}$/.test(input.serviceUser))
|
|
298
|
+
throw new Error("Invalid activation service user.");
|
|
299
|
+
boundedInteger("bluePort", input.bluePort, 1024, 65535);
|
|
300
|
+
boundedInteger("greenPort", input.greenPort, 1024, 65535);
|
|
301
|
+
if (input.bluePort === input.greenPort)
|
|
302
|
+
throw new Error("Activation slot ports must differ.");
|
|
303
|
+
boundedInteger("keepReleases", input.keepReleases, 2, 100);
|
|
304
|
+
if (!/^\/[A-Za-z0-9/_-]{1,128}$/.test(input.healthPath) || input.healthPath.includes("..")) {
|
|
305
|
+
throw new Error("Activation health path is malformed.");
|
|
306
|
+
}
|
|
307
|
+
const environment = [
|
|
308
|
+
`FZ_DIR=${input.root}`,
|
|
309
|
+
`FZ_USER=${input.serviceUser}`,
|
|
310
|
+
`FZ_BLUE_PORT=${input.bluePort}`,
|
|
311
|
+
`FZ_GREEN_PORT=${input.greenPort}`,
|
|
312
|
+
`FZ_HEALTH_PATH=${input.healthPath}`,
|
|
313
|
+
`FZ_KEEP_RELEASES=${input.keepReleases}`
|
|
314
|
+
].join(`
|
|
315
|
+
`) + `
|
|
316
|
+
`;
|
|
317
|
+
const helper = `#!/usr/bin/env bash
|
|
318
|
+
set -Eeuo pipefail
|
|
319
|
+
source /etc/forgezero/deploy.env
|
|
320
|
+
[[ $# == 1 ]] || { echo "usage: forgezero-activate <release>" >&2; exit 2; }
|
|
321
|
+
release="$(realpath -e "$1")"; releases="$(realpath -e "$FZ_DIR/releases")"; slots="$FZ_DIR/slots"
|
|
322
|
+
install -d -o root -g root -m 0755 "$slots"
|
|
323
|
+
case "$release/" in "$releases"/*/) ;; *) echo "release is outside $releases" >&2; exit 2 ;; esac
|
|
324
|
+
[[ -f "$release/.fz/deploy.json" && -s "$release/src/index.ts" && -s "$release/bun.lock" ]] || { echo "release is incomplete" >&2; exit 2; }
|
|
325
|
+
slot_file="$FZ_DIR/.forge-slot"; previous_slot="$(cat "$slot_file" 2>/dev/null || true)"
|
|
326
|
+
if [[ "$previous_slot" == blue ]]; then target=green; port="$FZ_GREEN_PORT"; else target=blue; port="$FZ_BLUE_PORT"; fi
|
|
327
|
+
target_link="$slots/$target"; previous_target_link="$(readlink -f "$target_link" 2>/dev/null || true)"
|
|
328
|
+
chown -R root:"$FZ_USER" "$release"; chmod -R a-w "$release"; find "$release" -type d -exec chmod a+rx {} +; find "$release" -type f -exec chmod a+r {} +
|
|
329
|
+
ln -sfn "$release" "$target_link"; systemctl restart "forgezero@\${target}.service"
|
|
330
|
+
healthy=0; for _ in $(seq 1 30); do curl -fsS --max-time 2 "http://127.0.0.1:\${port}\${FZ_HEALTH_PATH}" >/dev/null 2>&1 && { healthy=1; break; }; sleep 1; done
|
|
331
|
+
if (( ! healthy )); then systemctl stop "forgezero@\${target}.service" || true; [[ -n "$previous_target_link" && -d "$previous_target_link" ]] && ln -sfn "$previous_target_link" "$target_link" || rm -f "$target_link"; exit 1; fi
|
|
332
|
+
upstream=/etc/nginx/conf.d/forgezero-upstream.conf; backup="$(mktemp -p /run forgezero-upstream.XXXXXX)"; [[ -f "$upstream" ]] && cp "$upstream" "$backup" || : >"$backup"
|
|
333
|
+
printf 'upstream forgezero { server 127.0.0.1:%s; }\\n' "$port" >"$upstream"
|
|
334
|
+
if ! nginx -t || ! nginx -s reload; then [[ -s "$backup" ]] && cp "$backup" "$upstream" || rm -f "$upstream"; rm -f "$backup"; systemctl stop "forgezero@\${target}.service" || true; [[ -n "$previous_target_link" && -d "$previous_target_link" ]] && ln -sfn "$previous_target_link" "$target_link" || rm -f "$target_link"; nginx -t >/dev/null 2>&1 && nginx -s reload || true; exit 1; fi
|
|
335
|
+
rm -f "$backup"; printf '%s\\n' "$target" >"$slot_file"; [[ -n "$previous_slot" && "$previous_slot" != "$target" ]] && systemctl stop "forgezero@\${previous_slot}.service" || true
|
|
336
|
+
mapfile -t old < <(find "$releases" -mindepth 1 -maxdepth 1 -type d -printf '%T@ %p\\n' | sort -rn | tail -n "+$((FZ_KEEP_RELEASES + 1))" | cut -d' ' -f2-)
|
|
337
|
+
for path in "\${old[@]}"; do [[ "$path" == "$release" ]] || rm -rf -- "$path"; done
|
|
338
|
+
printf 'promoted %s on %s\\n' "$release" "$target"
|
|
339
|
+
`;
|
|
340
|
+
return {
|
|
341
|
+
environment,
|
|
342
|
+
helper,
|
|
343
|
+
sudoers: `forgezero-runner ALL=(root) NOPASSWD: /usr/local/libexec/forgezero-activate *
|
|
344
|
+
`
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
function validateCollectorUnit(unit) {
|
|
348
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.@-]{0,127}\.service$/.test(unit))
|
|
349
|
+
throw new Error("Invalid OTLP collector service unit.");
|
|
350
|
+
if (/^(forgezero@.*|forgezero-agent|forgezero-metal-agent|forgezero-db)\.service$/.test(unit)) {
|
|
351
|
+
throw new Error("OTLP collector must be independently supervised.");
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
function planLocalOtlpProof(endpoint, collectorUnit) {
|
|
355
|
+
if (endpoint !== "http://127.0.0.1:4318")
|
|
356
|
+
throw new Error("OTLP proof requires exact loopback endpoint http://127.0.0.1:4318.");
|
|
357
|
+
validateCollectorUnit(collectorUnit);
|
|
358
|
+
return {
|
|
359
|
+
unitCheck: { command: "systemctl", argv: ["is-active", "--quiet", collectorUnit] },
|
|
360
|
+
receiverCheck: {
|
|
361
|
+
command: "curl",
|
|
362
|
+
acceptedStatus: "2xx",
|
|
363
|
+
argv: [
|
|
364
|
+
"--silent",
|
|
365
|
+
"--show-error",
|
|
366
|
+
"--max-time",
|
|
367
|
+
"5",
|
|
368
|
+
"--output",
|
|
369
|
+
"/dev/null",
|
|
370
|
+
"--write-out",
|
|
371
|
+
"%{http_code}",
|
|
372
|
+
"--request",
|
|
373
|
+
"POST",
|
|
374
|
+
"--header",
|
|
375
|
+
"Content-Type: application/json",
|
|
376
|
+
"--data-binary",
|
|
377
|
+
"{}",
|
|
378
|
+
`${endpoint}/v1/metrics`
|
|
379
|
+
]
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
function initialPlatformBringupState(requiresFirstInvite) {
|
|
384
|
+
return { requiresFirstInvite, stage: "runtime-ready", inviteTokenFilePresent: false };
|
|
385
|
+
}
|
|
386
|
+
function advancePlatformBringup(state, event) {
|
|
387
|
+
if (state.stage === "failed")
|
|
388
|
+
throw new Error("A failed bring-up must be explicitly repaired before resuming.");
|
|
389
|
+
if (event.type === "failed")
|
|
390
|
+
return { ...state, stage: "failed", lastError: safeAtom("error", event.error) };
|
|
391
|
+
if (event.type === state.stage)
|
|
392
|
+
return state;
|
|
393
|
+
if (event.type === "invite-prepared") {
|
|
394
|
+
if (!state.requiresFirstInvite || state.stage !== "runtime-ready" || !event.tokenFilePresent) {
|
|
395
|
+
throw new Error("The first invite must be durably present before it is recorded.");
|
|
396
|
+
}
|
|
397
|
+
return { ...state, stage: "invite-prepared", inviteTokenFilePresent: true };
|
|
398
|
+
}
|
|
399
|
+
if (event.type === "deploy-requested") {
|
|
400
|
+
const expected = state.requiresFirstInvite ? "invite-prepared" : "runtime-ready";
|
|
401
|
+
if (state.stage !== expected || state.requiresFirstInvite && !state.inviteTokenFilePresent) {
|
|
402
|
+
throw new Error("Deployment cannot start before required invite preparation.");
|
|
403
|
+
}
|
|
404
|
+
return { ...state, stage: "deploy-requested" };
|
|
405
|
+
}
|
|
406
|
+
if (event.type === "deploy-healthy") {
|
|
407
|
+
if (state.stage !== "deploy-requested")
|
|
408
|
+
throw new Error("Deployment health requires a requested deployment.");
|
|
409
|
+
return { ...state, stage: "deploy-healthy" };
|
|
410
|
+
}
|
|
411
|
+
if (event.type === "invite-presented") {
|
|
412
|
+
if (!state.requiresFirstInvite || state.stage !== "deploy-healthy" || !state.inviteTokenFilePresent) {
|
|
413
|
+
throw new Error("Invite presentation requires a healthy deployment and the original token file.");
|
|
414
|
+
}
|
|
415
|
+
return { ...state, stage: "invite-presented" };
|
|
416
|
+
}
|
|
417
|
+
if (event.type === "complete") {
|
|
418
|
+
const expected = state.requiresFirstInvite ? "invite-presented" : "deploy-healthy";
|
|
419
|
+
if (state.stage !== expected)
|
|
420
|
+
throw new Error("Bring-up cannot complete before its ordered ceremony.");
|
|
421
|
+
return { ...state, stage: "complete" };
|
|
422
|
+
}
|
|
423
|
+
throw new Error("Unsupported bring-up transition.");
|
|
424
|
+
}
|
|
425
|
+
function nextPlatformBringupAction(state) {
|
|
426
|
+
if (state.stage === "failed" || state.stage === "complete")
|
|
427
|
+
return "none";
|
|
428
|
+
if (state.stage === "runtime-ready")
|
|
429
|
+
return state.requiresFirstInvite ? "prepare-invite" : "request-deploy";
|
|
430
|
+
if (state.stage === "invite-prepared") {
|
|
431
|
+
if (!state.inviteTokenFilePresent)
|
|
432
|
+
throw new Error("Recorded invite is missing; refusing implicit rotation.");
|
|
433
|
+
return "request-deploy";
|
|
434
|
+
}
|
|
435
|
+
if (state.stage === "deploy-requested")
|
|
436
|
+
return "await-deploy-health";
|
|
437
|
+
if (state.stage === "deploy-healthy")
|
|
438
|
+
return state.requiresFirstInvite ? "present-invite" : "mark-complete";
|
|
439
|
+
return "mark-complete";
|
|
440
|
+
}
|
|
441
|
+
function repairPlatformBringup(state) {
|
|
442
|
+
if (state.stage !== "failed")
|
|
443
|
+
return state;
|
|
444
|
+
if (state.requiresFirstInvite && !state.inviteTokenFilePresent) {
|
|
445
|
+
throw new Error("Missing issued invite cannot be repaired by implicit rotation.");
|
|
446
|
+
}
|
|
447
|
+
return { ...state, stage: state.inviteTokenFilePresent ? "invite-prepared" : "runtime-ready", lastError: undefined };
|
|
448
|
+
}
|
|
449
|
+
export {
|
|
450
|
+
validatePlatformSharedEnvironment,
|
|
451
|
+
repairPlatformBringup,
|
|
452
|
+
renderPlatformSharedEnvironment,
|
|
453
|
+
renderPlatformNginx,
|
|
454
|
+
renderPlatformApiUnits,
|
|
455
|
+
renderPlatformActivationFiles,
|
|
456
|
+
platformApiCredentialSpecs,
|
|
457
|
+
planPlatformActivation,
|
|
458
|
+
planLocalOtlpProof,
|
|
459
|
+
nextPlatformBringupAction,
|
|
460
|
+
initialPlatformBringupState,
|
|
461
|
+
advancePlatformBringup
|
|
462
|
+
};
|
package/dist/provision.js
CHANGED
|
@@ -884,7 +884,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
|
|
|
884
884
|
}
|
|
885
885
|
|
|
886
886
|
// src/version.ts
|
|
887
|
-
var VERSION3 = "0.1.
|
|
887
|
+
var VERSION3 = "0.1.35";
|
|
888
888
|
|
|
889
889
|
// src/egress-policy.ts
|
|
890
890
|
import { realpathSync } from "node:fs";
|
|
@@ -1476,6 +1476,7 @@ function agentUnit(options) {
|
|
|
1476
1476
|
options.profile ? `FZ_DEPLOY_PROFILE=${options.profile}` : null,
|
|
1477
1477
|
options.repository && options.branch ? `FZ_DEPLOY_KEY=${options.project ?? "platform"}:${options.environment ?? "production"}` : null,
|
|
1478
1478
|
deploymentEnabled ? `FZ_DEPLOY_ROOT=${deployRoot}` : null,
|
|
1479
|
+
deploymentEnabled ? `FZ_CAPACITY_EVIDENCE_DIR=${deployRoot}/capacity` : null,
|
|
1479
1480
|
deploymentEnabled ? `FZ_DEPLOY_RUNNER_SOCKET=${DEPLOYMENT_RUNNER_SOCKET}` : null,
|
|
1480
1481
|
deploymentEnabled ? `FZ_SOFTWARE_HELPER_SOCKET=${DEFAULT_SOFTWARE_HELPER_SOCKET}` : null,
|
|
1481
1482
|
Object.keys(deploymentCredentials).length > 0 ? `FZ_DEPLOY_SYSTEMD_SECRETS=${Object.keys(deploymentCredentials).join(",")}` : null,
|
|
@@ -1494,7 +1495,7 @@ function agentUnit(options) {
|
|
|
1494
1495
|
` : "";
|
|
1495
1496
|
const projectCredentials = Object.entries(deploymentCredentials).map(([name, path]) => `LoadCredentialEncrypted=${name}:${path}`).join(`
|
|
1496
1497
|
`);
|
|
1497
|
-
const deploymentWrites = deploymentEnabled ? `ReadWritePaths=${deployRoot}/releases ${deployRoot}/agent-home ${deployRoot}/cache` : "";
|
|
1498
|
+
const deploymentWrites = deploymentEnabled ? `ReadWritePaths=${deployRoot}/releases ${deployRoot}/agent-home ${deployRoot}/cache ${deployRoot}/capacity` : "";
|
|
1498
1499
|
const supplementaryGroups = [
|
|
1499
1500
|
AGENT_UPDATE_GROUP,
|
|
1500
1501
|
deploymentEnabled ? DEPLOYMENT_GROUP : null,
|
|
@@ -1742,12 +1743,12 @@ function planProvision(options) {
|
|
|
1742
1743
|
},
|
|
1743
1744
|
{
|
|
1744
1745
|
label: "encrypted one-time enrolment capability",
|
|
1745
|
-
command: `test -s ${enrolTokenCredentialPath} || { test -r ${enrolTokenSourcePath}; ` + `systemd-creds encrypt --name=enrol-token ${enrolTokenSourcePath} ${enrolTokenCredentialPath}; ` + `chmod 0400 ${enrolTokenCredentialPath}; rm -f ${enrolTokenSourcePath}; }`
|
|
1746
|
+
command: `test -s ${enrolStatePath} || test -s ${enrolTokenCredentialPath} || { test -r ${enrolTokenSourcePath}; ` + `systemd-creds encrypt --name=enrol-token ${enrolTokenSourcePath} ${enrolTokenCredentialPath}; ` + `chmod 0400 ${enrolTokenCredentialPath}; rm -f ${enrolTokenSourcePath}; }`
|
|
1746
1747
|
}
|
|
1747
1748
|
] : [],
|
|
1748
1749
|
...deploymentEnabled ? [{
|
|
1749
1750
|
label: "deployment directories",
|
|
1750
|
-
command: `install -d -o root -g root -m 0755 ${deployRoot} && ` + `install -d -o root -g ${DEPLOYMENT_GROUP} -m 3770 ${deployRoot}/releases && ` + `install -d -o ${user} -g ${user} -m 0750 ${deployRoot}/cache && ` + `install -d -o ${user} -g ${user} -m 0700 ${deployRoot}/agent-home && ` + `install -d -o ${DEPLOYMENT_RUNNER_USER} -g ${DEPLOYMENT_GROUP} -m 0700 ${deployRoot}/runner-home ${deployRoot}/runner-home/cache`
|
|
1751
|
+
command: `install -d -o root -g root -m 0755 ${deployRoot} && ` + `install -d -o root -g ${DEPLOYMENT_GROUP} -m 3770 ${deployRoot}/releases && ` + `install -d -o ${user} -g ${user} -m 0750 ${deployRoot}/cache && ` + `install -d -o ${user} -g ${user} -m 0700 ${deployRoot}/capacity && ` + `install -d -o ${user} -g ${user} -m 0700 ${deployRoot}/agent-home && ` + `install -d -o ${DEPLOYMENT_RUNNER_USER} -g ${DEPLOYMENT_GROUP} -m 0700 ${deployRoot}/runner-home ${deployRoot}/runner-home/cache`
|
|
1751
1752
|
}] : [],
|
|
1752
1753
|
{ label: "reload units", command: "systemctl daemon-reload" },
|
|
1753
1754
|
...deploymentEnabled ? [{
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** One package version shared by both public binaries. Pinned to package.json by tests. */
|
|
2
|
-
export declare const VERSION = "0.1.
|
|
2
|
+
export declare const VERSION = "0.1.35";
|