@envsync-cloud/deploy-cli 0.6.1 → 0.6.3
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 +16 -2
- package/dist/index.js +682 -140
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,6 +40,7 @@ bunx @envsync-cloud/deploy-cli <command>
|
|
|
40
40
|
```text
|
|
41
41
|
envsync-deploy preinstall
|
|
42
42
|
envsync-deploy setup
|
|
43
|
+
envsync-deploy bootstrap
|
|
43
44
|
envsync-deploy deploy
|
|
44
45
|
envsync-deploy health [--json]
|
|
45
46
|
envsync-deploy upgrade
|
|
@@ -56,18 +57,31 @@ Prepare the host:
|
|
|
56
57
|
npx @envsync-cloud/deploy-cli preinstall
|
|
57
58
|
```
|
|
58
59
|
|
|
59
|
-
|
|
60
|
+
Write the desired self-hosted config:
|
|
60
61
|
|
|
61
62
|
```bash
|
|
62
63
|
npx @envsync-cloud/deploy-cli setup
|
|
63
64
|
```
|
|
64
65
|
|
|
65
|
-
|
|
66
|
+
`setup` requires an exact release version such as `0.6.2`. Channel names like `stable` and `latest` are not accepted for self-hosted installs.
|
|
67
|
+
|
|
68
|
+
Bootstrap infra, migrations, RustFS, and OpenFGA:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npx @envsync-cloud/deploy-cli bootstrap
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Deploy the pending API and frontend services:
|
|
66
75
|
|
|
67
76
|
```bash
|
|
68
77
|
npx @envsync-cloud/deploy-cli deploy
|
|
69
78
|
```
|
|
70
79
|
|
|
80
|
+
The staged flow is:
|
|
81
|
+
- `setup` writes desired config
|
|
82
|
+
- `bootstrap` starts base infra, runs OpenFGA and miniKMS migrations, starts runtime infra, and persists generated runtime env state
|
|
83
|
+
- `deploy` starts the pending API and frontend services
|
|
84
|
+
|
|
71
85
|
Check service health:
|
|
72
86
|
|
|
73
87
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -17,6 +17,8 @@ var DEPLOY_ENV = "/etc/envsync/deploy.env";
|
|
|
17
17
|
var DEPLOY_YAML = "/etc/envsync/deploy.yaml";
|
|
18
18
|
var VERSIONS_LOCK = "/opt/envsync/deploy/versions.lock.json";
|
|
19
19
|
var STACK_FILE = "/opt/envsync/deploy/docker-stack.yaml";
|
|
20
|
+
var BOOTSTRAP_BASE_STACK_FILE = "/opt/envsync/deploy/docker-stack.bootstrap.base.yaml";
|
|
21
|
+
var BOOTSTRAP_STACK_FILE = "/opt/envsync/deploy/docker-stack.bootstrap.yaml";
|
|
20
22
|
var TRAEFIK_DYNAMIC_FILE = "/opt/envsync/deploy/traefik-dynamic.yaml";
|
|
21
23
|
var KEYCLOAK_REALM_FILE = "/opt/envsync/deploy/keycloak-realm.envsync.json";
|
|
22
24
|
var NGINX_WEB_CONF = "/opt/envsync/deploy/nginx-web.conf";
|
|
@@ -34,6 +36,17 @@ var STACK_VOLUMES = [
|
|
|
34
36
|
"clickstack_ch_data",
|
|
35
37
|
"clickstack_ch_logs"
|
|
36
38
|
];
|
|
39
|
+
var REQUIRED_BOOTSTRAP_ENV_KEYS = [
|
|
40
|
+
"S3_SECRET_KEY",
|
|
41
|
+
"KEYCLOAK_WEB_CLIENT_SECRET",
|
|
42
|
+
"KEYCLOAK_API_CLIENT_SECRET",
|
|
43
|
+
"OPENFGA_DB_PASSWORD",
|
|
44
|
+
"MINIKMS_ROOT_KEY",
|
|
45
|
+
"MINIKMS_DB_PASSWORD",
|
|
46
|
+
"OPENFGA_STORE_ID",
|
|
47
|
+
"OPENFGA_MODEL_ID"
|
|
48
|
+
];
|
|
49
|
+
var SEMVER_VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
37
50
|
function run(cmd, args, opts = {}) {
|
|
38
51
|
const result = spawnSync(cmd, args, {
|
|
39
52
|
cwd: opts.cwd,
|
|
@@ -48,6 +61,22 @@ ${stderr}` : ""}`);
|
|
|
48
61
|
}
|
|
49
62
|
return result.stdout?.toString() ?? "";
|
|
50
63
|
}
|
|
64
|
+
function tryRun(cmd, args, opts = {}) {
|
|
65
|
+
try {
|
|
66
|
+
return run(cmd, args, opts);
|
|
67
|
+
} catch {
|
|
68
|
+
return "";
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function commandSucceeds(cmd, args, opts = {}) {
|
|
72
|
+
const result = spawnSync(cmd, args, {
|
|
73
|
+
cwd: opts.cwd,
|
|
74
|
+
env: { ...process.env, ...opts.env },
|
|
75
|
+
stdio: "ignore",
|
|
76
|
+
encoding: "utf8"
|
|
77
|
+
});
|
|
78
|
+
return result.status === 0;
|
|
79
|
+
}
|
|
51
80
|
function ensureDir(dir) {
|
|
52
81
|
fs.mkdirSync(dir, { recursive: true });
|
|
53
82
|
}
|
|
@@ -126,9 +155,21 @@ function parseSimpleYamlObject(input) {
|
|
|
126
155
|
}
|
|
127
156
|
return root;
|
|
128
157
|
}
|
|
129
|
-
function
|
|
130
|
-
const
|
|
131
|
-
|
|
158
|
+
function parseEnvFile(content) {
|
|
159
|
+
const out = {};
|
|
160
|
+
for (const line of content.split(/\r?\n/)) {
|
|
161
|
+
const trimmed = line.trim();
|
|
162
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
163
|
+
const eq = trimmed.indexOf("=");
|
|
164
|
+
if (eq === -1) continue;
|
|
165
|
+
const key = trimmed.slice(0, eq).trim();
|
|
166
|
+
let value = trimmed.slice(eq + 1).trim();
|
|
167
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
168
|
+
value = value.slice(1, -1).replace(/\\n/g, "\n").replace(/\\"/g, '"');
|
|
169
|
+
}
|
|
170
|
+
out[key] = value;
|
|
171
|
+
}
|
|
172
|
+
return out;
|
|
132
173
|
}
|
|
133
174
|
async function ask(question, fallback = "") {
|
|
134
175
|
if (!process.stdin.isTTY) return fallback;
|
|
@@ -140,6 +181,9 @@ async function ask(question, fallback = "") {
|
|
|
140
181
|
});
|
|
141
182
|
});
|
|
142
183
|
}
|
|
184
|
+
function sleepSeconds(seconds) {
|
|
185
|
+
spawnSync("sleep", [`${seconds}`], { stdio: "ignore" });
|
|
186
|
+
}
|
|
143
187
|
function domainMap(rootDomain) {
|
|
144
188
|
return {
|
|
145
189
|
landing: rootDomain,
|
|
@@ -152,29 +196,234 @@ function domainMap(rootDomain) {
|
|
|
152
196
|
s3Console: `console.s3.${rootDomain}`
|
|
153
197
|
};
|
|
154
198
|
}
|
|
199
|
+
function getDeployCliVersion() {
|
|
200
|
+
try {
|
|
201
|
+
const packageJsonPath = new URL("../package.json", import.meta.url);
|
|
202
|
+
const raw = fs.readFileSync(packageJsonPath, "utf8");
|
|
203
|
+
return JSON.parse(raw).version ?? "0.0.0";
|
|
204
|
+
} catch {
|
|
205
|
+
return process.env.npm_package_version ?? "0.0.0";
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function assertSemverVersion(version, label = "release version") {
|
|
209
|
+
if (!SEMVER_VERSION_RE.test(version)) {
|
|
210
|
+
throw new Error(`Invalid ${label} '${version}'. Expected an exact semver like 0.6.2.`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function versionedImages(version) {
|
|
214
|
+
assertSemverVersion(version);
|
|
215
|
+
return {
|
|
216
|
+
api: `ghcr.io/envsync-cloud/envsync-api:${version}`,
|
|
217
|
+
keycloak: `envsync-keycloak:${version}`,
|
|
218
|
+
web: `ghcr.io/envsync-cloud/envsync-web-static:${version}`,
|
|
219
|
+
landing: `ghcr.io/envsync-cloud/envsync-landing-static:${version}`
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function defaultSourceConfig(version) {
|
|
223
|
+
return {
|
|
224
|
+
repo_url: "https://github.com/EnvSync-Cloud/envsync.git",
|
|
225
|
+
ref: `v${version}`
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
function resolveReleaseVersion(raw) {
|
|
229
|
+
const releaseVersion = raw.release?.version;
|
|
230
|
+
if (releaseVersion) {
|
|
231
|
+
assertSemverVersion(releaseVersion);
|
|
232
|
+
return releaseVersion;
|
|
233
|
+
}
|
|
234
|
+
if (typeof raw.release_channel === "string" && raw.release_channel.length > 0) {
|
|
235
|
+
if (SEMVER_VERSION_RE.test(raw.release_channel)) {
|
|
236
|
+
return raw.release_channel;
|
|
237
|
+
}
|
|
238
|
+
if (raw.release_channel === "stable" || raw.release_channel === "latest") {
|
|
239
|
+
throw new Error(
|
|
240
|
+
"Legacy release channel config is no longer supported for self-hosted installs. Set an exact release version in /etc/envsync/deploy.yaml."
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
throw new Error(`Invalid legacy release channel '${raw.release_channel}'. Set an exact release version in /etc/envsync/deploy.yaml.`);
|
|
244
|
+
}
|
|
245
|
+
return getDeployCliVersion();
|
|
246
|
+
}
|
|
247
|
+
function requireDefined(value, label) {
|
|
248
|
+
if (value === void 0) {
|
|
249
|
+
throw new Error(`Missing ${label} in ${DEPLOY_YAML}. Run setup again.`);
|
|
250
|
+
}
|
|
251
|
+
return value;
|
|
252
|
+
}
|
|
253
|
+
function normalizeConfig(raw) {
|
|
254
|
+
const version = resolveReleaseVersion(raw);
|
|
255
|
+
const derivedImages = versionedImages(version);
|
|
256
|
+
const { release_channel: _legacyReleaseChannel, ...rest } = raw;
|
|
257
|
+
const rootDomain = requireDefined(raw.domain?.root_domain, "domain.root_domain");
|
|
258
|
+
const acmeEmail = requireDefined(raw.domain?.acme_email, "domain.acme_email");
|
|
259
|
+
return {
|
|
260
|
+
...rest,
|
|
261
|
+
source: {
|
|
262
|
+
repo_url: raw.source?.repo_url ?? "https://github.com/EnvSync-Cloud/envsync.git",
|
|
263
|
+
ref: `v${version}`
|
|
264
|
+
},
|
|
265
|
+
release: {
|
|
266
|
+
version
|
|
267
|
+
},
|
|
268
|
+
domain: {
|
|
269
|
+
root_domain: rootDomain,
|
|
270
|
+
acme_email: acmeEmail
|
|
271
|
+
},
|
|
272
|
+
images: {
|
|
273
|
+
api: derivedImages.api,
|
|
274
|
+
keycloak: derivedImages.keycloak,
|
|
275
|
+
web: derivedImages.web,
|
|
276
|
+
landing: derivedImages.landing,
|
|
277
|
+
clickstack: raw.images?.clickstack ?? "clickhouse/clickstack-all-in-one:latest",
|
|
278
|
+
traefik: raw.images?.traefik ?? "traefik:v3.1",
|
|
279
|
+
otel_agent: raw.images?.otel_agent ?? "otel/opentelemetry-collector-contrib:0.111.0"
|
|
280
|
+
},
|
|
281
|
+
services: {
|
|
282
|
+
stack_name: requireDefined(raw.services?.stack_name, "services.stack_name"),
|
|
283
|
+
api_port: requireDefined(raw.services?.api_port, "services.api_port"),
|
|
284
|
+
clickstack_ui_port: requireDefined(raw.services?.clickstack_ui_port, "services.clickstack_ui_port"),
|
|
285
|
+
clickstack_otlp_http_port: requireDefined(raw.services?.clickstack_otlp_http_port, "services.clickstack_otlp_http_port"),
|
|
286
|
+
clickstack_otlp_grpc_port: requireDefined(raw.services?.clickstack_otlp_grpc_port, "services.clickstack_otlp_grpc_port"),
|
|
287
|
+
keycloak_port: requireDefined(raw.services?.keycloak_port, "services.keycloak_port"),
|
|
288
|
+
rustfs_port: requireDefined(raw.services?.rustfs_port, "services.rustfs_port"),
|
|
289
|
+
rustfs_console_port: requireDefined(raw.services?.rustfs_console_port, "services.rustfs_console_port")
|
|
290
|
+
},
|
|
291
|
+
auth: {
|
|
292
|
+
keycloak_realm: requireDefined(raw.auth?.keycloak_realm, "auth.keycloak_realm"),
|
|
293
|
+
admin_user: requireDefined(raw.auth?.admin_user, "auth.admin_user"),
|
|
294
|
+
admin_password: requireDefined(raw.auth?.admin_password, "auth.admin_password"),
|
|
295
|
+
web_client_id: requireDefined(raw.auth?.web_client_id, "auth.web_client_id"),
|
|
296
|
+
api_client_id: requireDefined(raw.auth?.api_client_id, "auth.api_client_id"),
|
|
297
|
+
cli_client_id: requireDefined(raw.auth?.cli_client_id, "auth.cli_client_id")
|
|
298
|
+
},
|
|
299
|
+
observability: {
|
|
300
|
+
retention_days: requireDefined(raw.observability?.retention_days, "observability.retention_days"),
|
|
301
|
+
public_obs: requireDefined(raw.observability?.public_obs, "observability.public_obs")
|
|
302
|
+
},
|
|
303
|
+
backup: {
|
|
304
|
+
output_dir: requireDefined(raw.backup?.output_dir, "backup.output_dir"),
|
|
305
|
+
encrypted: requireDefined(raw.backup?.encrypted, "backup.encrypted")
|
|
306
|
+
},
|
|
307
|
+
smtp: {
|
|
308
|
+
host: requireDefined(raw.smtp?.host, "smtp.host"),
|
|
309
|
+
port: requireDefined(raw.smtp?.port, "smtp.port"),
|
|
310
|
+
secure: requireDefined(raw.smtp?.secure, "smtp.secure"),
|
|
311
|
+
user: requireDefined(raw.smtp?.user, "smtp.user"),
|
|
312
|
+
pass: requireDefined(raw.smtp?.pass, "smtp.pass"),
|
|
313
|
+
from: requireDefined(raw.smtp?.from, "smtp.from")
|
|
314
|
+
},
|
|
315
|
+
exposure: {
|
|
316
|
+
public_auth: requireDefined(raw.exposure?.public_auth, "exposure.public_auth"),
|
|
317
|
+
public_obs: requireDefined(raw.exposure?.public_obs, "exposure.public_obs"),
|
|
318
|
+
mailpit_enabled: requireDefined(raw.exposure?.mailpit_enabled, "exposure.mailpit_enabled"),
|
|
319
|
+
s3_public: requireDefined(raw.exposure?.s3_public, "exposure.s3_public"),
|
|
320
|
+
s3_console_public: requireDefined(raw.exposure?.s3_console_public, "exposure.s3_console_public")
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
function emptyGeneratedState() {
|
|
325
|
+
return {
|
|
326
|
+
openfga: {
|
|
327
|
+
store_id: "",
|
|
328
|
+
model_id: ""
|
|
329
|
+
},
|
|
330
|
+
secrets: {
|
|
331
|
+
s3_secret_key: "",
|
|
332
|
+
keycloak_web_client_secret: "",
|
|
333
|
+
keycloak_api_client_secret: "",
|
|
334
|
+
openfga_db_password: "",
|
|
335
|
+
minikms_root_key: "",
|
|
336
|
+
minikms_db_password: ""
|
|
337
|
+
},
|
|
338
|
+
bootstrap: {
|
|
339
|
+
completed_at: ""
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
function normalizeGeneratedState(raw) {
|
|
344
|
+
const defaults = emptyGeneratedState();
|
|
345
|
+
return {
|
|
346
|
+
openfga: {
|
|
347
|
+
store_id: raw?.openfga?.store_id ?? defaults.openfga.store_id,
|
|
348
|
+
model_id: raw?.openfga?.model_id ?? defaults.openfga.model_id
|
|
349
|
+
},
|
|
350
|
+
secrets: {
|
|
351
|
+
s3_secret_key: raw?.secrets?.s3_secret_key ?? defaults.secrets.s3_secret_key,
|
|
352
|
+
keycloak_web_client_secret: raw?.secrets?.keycloak_web_client_secret ?? defaults.secrets.keycloak_web_client_secret,
|
|
353
|
+
keycloak_api_client_secret: raw?.secrets?.keycloak_api_client_secret ?? defaults.secrets.keycloak_api_client_secret,
|
|
354
|
+
openfga_db_password: raw?.secrets?.openfga_db_password ?? defaults.secrets.openfga_db_password,
|
|
355
|
+
minikms_root_key: raw?.secrets?.minikms_root_key ?? defaults.secrets.minikms_root_key,
|
|
356
|
+
minikms_db_password: raw?.secrets?.minikms_db_password ?? defaults.secrets.minikms_db_password
|
|
357
|
+
},
|
|
358
|
+
bootstrap: {
|
|
359
|
+
completed_at: raw?.bootstrap?.completed_at ?? defaults.bootstrap.completed_at
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
function readInternalState() {
|
|
364
|
+
if (!exists(INTERNAL_CONFIG_JSON)) return null;
|
|
365
|
+
const raw = JSON.parse(fs.readFileSync(INTERNAL_CONFIG_JSON, "utf8"));
|
|
366
|
+
return {
|
|
367
|
+
config: raw.config ? normalizeConfig(raw.config) : void 0,
|
|
368
|
+
generated: normalizeGeneratedState(raw.generated)
|
|
369
|
+
};
|
|
370
|
+
}
|
|
155
371
|
function loadConfig() {
|
|
156
372
|
if (!exists(DEPLOY_YAML)) {
|
|
157
373
|
throw new Error(`Missing deploy config at ${DEPLOY_YAML}. Run setup first.`);
|
|
158
374
|
}
|
|
159
375
|
const raw = fs.readFileSync(DEPLOY_YAML, "utf8");
|
|
160
376
|
if (raw.trimStart().startsWith("{")) {
|
|
161
|
-
return JSON.parse(raw);
|
|
377
|
+
return normalizeConfig(JSON.parse(raw));
|
|
162
378
|
}
|
|
163
|
-
return parseSimpleYamlObject(raw);
|
|
379
|
+
return normalizeConfig(parseSimpleYamlObject(raw));
|
|
164
380
|
}
|
|
165
|
-
function
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
writeFile(DEPLOY_ENV, renderEnv(config), 384);
|
|
169
|
-
writeFile(VERSIONS_LOCK, JSON.stringify(config.images, null, 2) + "\n");
|
|
170
|
-
writeFile(KEYCLOAK_REALM_FILE, renderKeycloakRealm(config));
|
|
171
|
-
writeFile(TRAEFIK_DYNAMIC_FILE, renderTraefikDynamicConfig(config));
|
|
172
|
-
writeFile(STACK_FILE, renderStack(config));
|
|
173
|
-
writeFile(NGINX_WEB_CONF, renderNginxConf("web"));
|
|
174
|
-
writeFile(NGINX_LANDING_CONF, renderNginxConf("landing"));
|
|
175
|
-
writeFile(OTEL_AGENT_CONF, renderOtelAgentConfig(config));
|
|
381
|
+
function loadGeneratedEnv() {
|
|
382
|
+
if (!exists(DEPLOY_ENV)) return {};
|
|
383
|
+
return parseEnvFile(fs.readFileSync(DEPLOY_ENV, "utf8"));
|
|
176
384
|
}
|
|
177
|
-
function
|
|
385
|
+
function mergeGeneratedState(env, generated) {
|
|
386
|
+
const normalized = normalizeGeneratedState(generated);
|
|
387
|
+
return normalizeGeneratedState({
|
|
388
|
+
openfga: {
|
|
389
|
+
store_id: env.OPENFGA_STORE_ID ?? normalized.openfga.store_id,
|
|
390
|
+
model_id: env.OPENFGA_MODEL_ID ?? normalized.openfga.model_id
|
|
391
|
+
},
|
|
392
|
+
secrets: {
|
|
393
|
+
s3_secret_key: env.S3_SECRET_KEY ?? normalized.secrets.s3_secret_key,
|
|
394
|
+
keycloak_web_client_secret: env.KEYCLOAK_WEB_CLIENT_SECRET ?? normalized.secrets.keycloak_web_client_secret,
|
|
395
|
+
keycloak_api_client_secret: env.KEYCLOAK_API_CLIENT_SECRET ?? normalized.secrets.keycloak_api_client_secret,
|
|
396
|
+
openfga_db_password: env.OPENFGA_DB_PASSWORD ?? normalized.secrets.openfga_db_password,
|
|
397
|
+
minikms_root_key: env.MINIKMS_ROOT_KEY ?? normalized.secrets.minikms_root_key,
|
|
398
|
+
minikms_db_password: env.MINIKMS_DB_PASSWORD ?? normalized.secrets.minikms_db_password
|
|
399
|
+
},
|
|
400
|
+
bootstrap: normalized.bootstrap
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
function loadState() {
|
|
404
|
+
const config = loadConfig();
|
|
405
|
+
const internal = readInternalState();
|
|
406
|
+
const generated = mergeGeneratedState(loadGeneratedEnv(), internal?.generated);
|
|
407
|
+
return { config, generated };
|
|
408
|
+
}
|
|
409
|
+
function ensureGeneratedRuntimeState(generated) {
|
|
410
|
+
return normalizeGeneratedState({
|
|
411
|
+
openfga: generated.openfga,
|
|
412
|
+
secrets: {
|
|
413
|
+
s3_secret_key: generated.secrets.s3_secret_key || randomSecret(16),
|
|
414
|
+
keycloak_web_client_secret: generated.secrets.keycloak_web_client_secret || randomSecret(),
|
|
415
|
+
keycloak_api_client_secret: generated.secrets.keycloak_api_client_secret || randomSecret(),
|
|
416
|
+
openfga_db_password: generated.secrets.openfga_db_password || randomSecret(),
|
|
417
|
+
minikms_root_key: generated.secrets.minikms_root_key || randomBytes(32).toString("hex"),
|
|
418
|
+
minikms_db_password: generated.secrets.minikms_db_password || randomSecret()
|
|
419
|
+
},
|
|
420
|
+
bootstrap: generated.bootstrap
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
function keycloakImageTag(image) {
|
|
424
|
+
return image.split(":").slice(1).join(":") || "local";
|
|
425
|
+
}
|
|
426
|
+
function buildRuntimeEnv(config, generated) {
|
|
178
427
|
const hosts = domainMap(config.domain.root_domain);
|
|
179
428
|
return {
|
|
180
429
|
NODE_ENV: "production",
|
|
@@ -190,7 +439,7 @@ function buildEnvMap(config) {
|
|
|
190
439
|
S3_BUCKET: "envsync-bucket",
|
|
191
440
|
S3_REGION: "us-east-1",
|
|
192
441
|
S3_ACCESS_KEY: "envsync-rustfs",
|
|
193
|
-
S3_SECRET_KEY:
|
|
442
|
+
S3_SECRET_KEY: generated.secrets.s3_secret_key,
|
|
194
443
|
S3_BUCKET_URL: `https://${hosts.s3}`,
|
|
195
444
|
S3_ENDPOINT: "http://rustfs:9000",
|
|
196
445
|
REDIS_URL: "redis://redis:6379",
|
|
@@ -205,43 +454,40 @@ function buildEnvMap(config) {
|
|
|
205
454
|
KEYCLOAK_ADMIN_USER: config.auth.admin_user,
|
|
206
455
|
KEYCLOAK_ADMIN_PASSWORD: config.auth.admin_password,
|
|
207
456
|
KEYCLOAK_WEB_CLIENT_ID: config.auth.web_client_id,
|
|
208
|
-
KEYCLOAK_WEB_CLIENT_SECRET:
|
|
457
|
+
KEYCLOAK_WEB_CLIENT_SECRET: generated.secrets.keycloak_web_client_secret,
|
|
209
458
|
KEYCLOAK_CLI_CLIENT_ID: config.auth.cli_client_id,
|
|
210
459
|
KEYCLOAK_API_CLIENT_ID: config.auth.api_client_id,
|
|
211
|
-
KEYCLOAK_API_CLIENT_SECRET:
|
|
460
|
+
KEYCLOAK_API_CLIENT_SECRET: generated.secrets.keycloak_api_client_secret,
|
|
212
461
|
KEYCLOAK_WEB_REDIRECT_URI: `https://${hosts.api}/api/access/web/callback`,
|
|
213
462
|
KEYCLOAK_WEB_CALLBACK_URL: `https://${hosts.app}/auth/callback`,
|
|
214
463
|
KEYCLOAK_API_REDIRECT_URI: `https://${hosts.api}/api/access/api/callback`,
|
|
215
464
|
LANDING_PAGE_URL: `https://${hosts.landing}`,
|
|
216
465
|
DASHBOARD_URL: `https://${hosts.app}`,
|
|
217
466
|
OPENFGA_API_URL: "http://openfga:8090",
|
|
218
|
-
OPENFGA_STORE_ID:
|
|
219
|
-
OPENFGA_MODEL_ID:
|
|
220
|
-
OPENFGA_DB_PASSWORD:
|
|
467
|
+
OPENFGA_STORE_ID: generated.openfga.store_id,
|
|
468
|
+
OPENFGA_MODEL_ID: generated.openfga.model_id,
|
|
469
|
+
OPENFGA_DB_PASSWORD: generated.secrets.openfga_db_password,
|
|
221
470
|
MINIKMS_GRPC_ADDR: "minikms:50051",
|
|
222
471
|
MINIKMS_TLS_ENABLED: "false",
|
|
223
|
-
MINIKMS_ROOT_KEY:
|
|
472
|
+
MINIKMS_ROOT_KEY: generated.secrets.minikms_root_key,
|
|
224
473
|
MINIKMS_DB_USER: "postgres",
|
|
225
|
-
MINIKMS_DB_PASSWORD:
|
|
474
|
+
MINIKMS_DB_PASSWORD: generated.secrets.minikms_db_password,
|
|
226
475
|
OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-agent:4318",
|
|
227
476
|
OTEL_SERVICE_NAME: "envsync-api",
|
|
228
477
|
OTEL_SDK_DISABLED: "false",
|
|
229
|
-
CLICKSTACK_URL: `https://${hosts.obs}
|
|
478
|
+
CLICKSTACK_URL: `https://${hosts.obs}`,
|
|
479
|
+
KEYCLOAK_IMAGE_TAG: keycloakImageTag(config.images.keycloak)
|
|
230
480
|
};
|
|
231
481
|
}
|
|
232
|
-
function
|
|
233
|
-
return Object.entries({
|
|
234
|
-
...buildEnvMap(config),
|
|
235
|
-
KEYCLOAK_IMAGE_TAG: config.images.keycloak.split(":").slice(1).join(":") || "local"
|
|
236
|
-
}).map(([k, v]) => `${k}=${v}`).join("\n") + "\n";
|
|
482
|
+
function renderEnvFile(env) {
|
|
483
|
+
return Object.entries(env).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${key}=${value}`).join("\n") + "\n";
|
|
237
484
|
}
|
|
238
|
-
function
|
|
239
|
-
|
|
485
|
+
function renderEnvList(values, indent = 6) {
|
|
486
|
+
const prefix = " ".repeat(indent);
|
|
487
|
+
return Object.entries(values).map(([key, value]) => `${prefix}- ${JSON.stringify(`${key}=${String(value)}`)}`).join("\n");
|
|
240
488
|
}
|
|
241
|
-
function renderKeycloakRealm(config) {
|
|
489
|
+
function renderKeycloakRealm(config, runtimeEnv) {
|
|
242
490
|
const hosts = domainMap(config.domain.root_domain);
|
|
243
|
-
const webSecret = extractEnvValue("KEYCLOAK_WEB_CLIENT_SECRET");
|
|
244
|
-
const apiSecret = extractEnvValue("KEYCLOAK_API_CLIENT_SECRET");
|
|
245
491
|
return JSON.stringify(
|
|
246
492
|
{
|
|
247
493
|
realm: config.auth.keycloak_realm,
|
|
@@ -254,7 +500,7 @@ function renderKeycloakRealm(config) {
|
|
|
254
500
|
name: "EnvSync Web",
|
|
255
501
|
protocol: "openid-connect",
|
|
256
502
|
publicClient: false,
|
|
257
|
-
secret:
|
|
503
|
+
secret: runtimeEnv.KEYCLOAK_WEB_CLIENT_SECRET,
|
|
258
504
|
standardFlowEnabled: true,
|
|
259
505
|
directAccessGrantsEnabled: false,
|
|
260
506
|
redirectUris: [
|
|
@@ -273,7 +519,7 @@ function renderKeycloakRealm(config) {
|
|
|
273
519
|
name: "EnvSync API",
|
|
274
520
|
protocol: "openid-connect",
|
|
275
521
|
publicClient: false,
|
|
276
|
-
secret:
|
|
522
|
+
secret: runtimeEnv.KEYCLOAK_API_CLIENT_SECRET,
|
|
277
523
|
standardFlowEnabled: true,
|
|
278
524
|
redirectUris: [`https://${hosts.api}/api/access/api/callback`],
|
|
279
525
|
webOrigins: [`https://${hosts.api}`],
|
|
@@ -297,11 +543,6 @@ function renderKeycloakRealm(config) {
|
|
|
297
543
|
2
|
|
298
544
|
) + "\n";
|
|
299
545
|
}
|
|
300
|
-
function extractEnvValue(key) {
|
|
301
|
-
const env = exists(DEPLOY_ENV) ? fs.readFileSync(DEPLOY_ENV, "utf8") : "";
|
|
302
|
-
const line = env.split(/\r?\n/).find((entry) => entry.startsWith(`${key}=`));
|
|
303
|
-
return line?.slice(key.length + 1) ?? "";
|
|
304
|
-
}
|
|
305
546
|
function renderTraefikDynamicConfig(config) {
|
|
306
547
|
const hosts = domainMap(config.domain.root_domain);
|
|
307
548
|
return [
|
|
@@ -340,17 +581,17 @@ function renderTraefikDynamicConfig(config) {
|
|
|
340
581
|
" servers:",
|
|
341
582
|
" - url: http://web_nginx:8080",
|
|
342
583
|
" routers:",
|
|
343
|
-
|
|
584
|
+
" landing-router:",
|
|
344
585
|
` rule: Host(\`${hosts.landing}\`)`,
|
|
345
586
|
" service: landing",
|
|
346
587
|
" entryPoints: [websecure]",
|
|
347
588
|
" tls: {}",
|
|
348
|
-
|
|
589
|
+
" web-router:",
|
|
349
590
|
` rule: Host(\`${hosts.app}\`)`,
|
|
350
591
|
" service: web",
|
|
351
592
|
" entryPoints: [websecure]",
|
|
352
593
|
" tls: {}",
|
|
353
|
-
|
|
594
|
+
" api-router:",
|
|
354
595
|
` rule: Host(\`${hosts.api}\`)`,
|
|
355
596
|
" service: envsync-api",
|
|
356
597
|
" entryPoints: [websecure]",
|
|
@@ -415,16 +656,19 @@ function renderOtelAgentConfig(config) {
|
|
|
415
656
|
" exporters: [otlphttp/clickstack]"
|
|
416
657
|
].join("\n") + "\n";
|
|
417
658
|
}
|
|
418
|
-
function renderStack(config) {
|
|
659
|
+
function renderStack(config, runtimeEnv, mode) {
|
|
419
660
|
const hosts = domainMap(config.domain.root_domain);
|
|
420
|
-
const
|
|
661
|
+
const includeRuntimeInfra = mode !== "base";
|
|
662
|
+
const includeAppServices = mode === "full";
|
|
663
|
+
const apiEnvironment = {
|
|
664
|
+
...runtimeEnv,
|
|
421
665
|
OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-agent:4318",
|
|
422
666
|
KEYCLOAK_URL: "http://keycloak:8080",
|
|
423
667
|
OPENFGA_API_URL: "http://openfga:8090",
|
|
424
668
|
MINIKMS_GRPC_ADDR: "minikms:50051",
|
|
425
669
|
S3_ENDPOINT: "http://rustfs:9000",
|
|
426
670
|
S3_BUCKET_URL: `https://${hosts.s3}`
|
|
427
|
-
}
|
|
671
|
+
};
|
|
428
672
|
return `
|
|
429
673
|
version: "3.9"
|
|
430
674
|
services:
|
|
@@ -458,9 +702,11 @@ services:
|
|
|
458
702
|
postgres:
|
|
459
703
|
image: postgres:17
|
|
460
704
|
environment:
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
705
|
+
${renderEnvList({
|
|
706
|
+
POSTGRES_USER: "postgres",
|
|
707
|
+
POSTGRES_PASSWORD: "envsync-postgres",
|
|
708
|
+
POSTGRES_DB: "envsync"
|
|
709
|
+
})}
|
|
464
710
|
volumes:
|
|
465
711
|
- postgres_data:/var/lib/postgresql/data
|
|
466
712
|
networks: [envsync]
|
|
@@ -474,10 +720,12 @@ services:
|
|
|
474
720
|
rustfs:
|
|
475
721
|
image: rustfs/rustfs:latest
|
|
476
722
|
environment:
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
723
|
+
${renderEnvList({
|
|
724
|
+
RUSTFS_DATA_DIR: "/data",
|
|
725
|
+
RUSTFS_ACCESS_KEY: "envsync-rustfs",
|
|
726
|
+
RUSTFS_SECRET_KEY: runtimeEnv.S3_SECRET_KEY,
|
|
727
|
+
RUSTFS_CONSOLE_ENABLE: "true"
|
|
728
|
+
})}
|
|
481
729
|
volumes:
|
|
482
730
|
- rustfs_data:/data
|
|
483
731
|
networks: [envsync]
|
|
@@ -496,30 +744,34 @@ services:
|
|
|
496
744
|
keycloak_db:
|
|
497
745
|
image: postgres:17
|
|
498
746
|
environment:
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
747
|
+
${renderEnvList({
|
|
748
|
+
POSTGRES_USER: "keycloak",
|
|
749
|
+
POSTGRES_PASSWORD: runtimeEnv.KEYCLOAK_ADMIN_PASSWORD,
|
|
750
|
+
POSTGRES_DB: "keycloak"
|
|
751
|
+
})}
|
|
502
752
|
volumes:
|
|
503
753
|
- keycloak_db_data:/var/lib/postgresql/data
|
|
504
754
|
networks: [envsync]
|
|
505
|
-
|
|
755
|
+
${includeRuntimeInfra ? `
|
|
506
756
|
keycloak:
|
|
507
757
|
image: ${config.images.keycloak}
|
|
508
758
|
entrypoint: ["/bin/sh", "-lc"]
|
|
509
759
|
command:
|
|
510
760
|
- /opt/keycloak/bin/kc.sh import --dir /opt/keycloak/data/import --override true && exec /opt/keycloak/bin/kc.sh start-dev
|
|
511
761
|
environment:
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
762
|
+
${renderEnvList({
|
|
763
|
+
KC_DB: "postgres",
|
|
764
|
+
KC_DB_URL: "jdbc:postgresql://keycloak_db:5432/keycloak",
|
|
765
|
+
KC_DB_USERNAME: "keycloak",
|
|
766
|
+
KC_DB_PASSWORD: runtimeEnv.KEYCLOAK_ADMIN_PASSWORD,
|
|
767
|
+
KC_BOOTSTRAP_ADMIN_USERNAME: config.auth.admin_user,
|
|
768
|
+
KC_BOOTSTRAP_ADMIN_PASSWORD: config.auth.admin_password,
|
|
769
|
+
KC_HTTP_ENABLED: "true",
|
|
770
|
+
KC_PROXY_HEADERS: "xforwarded",
|
|
771
|
+
KC_HOSTNAME: hosts.auth
|
|
772
|
+
})}
|
|
521
773
|
volumes:
|
|
522
|
-
- ${
|
|
774
|
+
- ${KEYCLOAK_REALM_FILE}:/opt/keycloak/data/import/realm.json:ro
|
|
523
775
|
networks: [envsync]
|
|
524
776
|
deploy:
|
|
525
777
|
labels:
|
|
@@ -527,46 +779,55 @@ services:
|
|
|
527
779
|
- traefik.http.routers.keycloak.rule=Host(\`${hosts.auth}\`)
|
|
528
780
|
- traefik.http.routers.keycloak.entrypoints=websecure
|
|
529
781
|
- traefik.http.routers.keycloak.tls.certresolver=letsencrypt
|
|
530
|
-
- traefik.http.services.keycloak.loadbalancer.server.port=8080
|
|
782
|
+
- traefik.http.services.keycloak.loadbalancer.server.port=8080` : ""}
|
|
531
783
|
|
|
532
784
|
openfga_db:
|
|
533
785
|
image: postgres:17
|
|
534
786
|
environment:
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
787
|
+
${renderEnvList({
|
|
788
|
+
POSTGRES_USER: "openfga",
|
|
789
|
+
POSTGRES_PASSWORD: runtimeEnv.OPENFGA_DB_PASSWORD,
|
|
790
|
+
POSTGRES_DB: "openfga"
|
|
791
|
+
})}
|
|
538
792
|
volumes:
|
|
539
793
|
- openfga_db_data:/var/lib/postgresql/data
|
|
540
794
|
networks: [envsync]
|
|
541
|
-
|
|
795
|
+
${includeRuntimeInfra ? `
|
|
542
796
|
openfga:
|
|
543
797
|
image: openfga/openfga:v1.12.0
|
|
544
798
|
command: run
|
|
545
799
|
environment:
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
800
|
+
${renderEnvList({
|
|
801
|
+
OPENFGA_DATASTORE_ENGINE: "postgres",
|
|
802
|
+
OPENFGA_DATASTORE_URI: `postgres://openfga:${runtimeEnv.OPENFGA_DB_PASSWORD}@openfga_db:5432/openfga?sslmode=disable`,
|
|
803
|
+
OPENFGA_HTTP_ADDR: "0.0.0.0:8090",
|
|
804
|
+
OPENFGA_GRPC_ADDR: "0.0.0.0:8091"
|
|
805
|
+
})}
|
|
806
|
+
networks: [envsync]` : ""}
|
|
551
807
|
|
|
552
808
|
minikms_db:
|
|
553
809
|
image: postgres:17
|
|
554
810
|
environment:
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
811
|
+
${renderEnvList({
|
|
812
|
+
POSTGRES_USER: "postgres",
|
|
813
|
+
POSTGRES_PASSWORD: runtimeEnv.MINIKMS_DB_PASSWORD,
|
|
814
|
+
POSTGRES_DB: "minikms"
|
|
815
|
+
})}
|
|
558
816
|
volumes:
|
|
559
817
|
- minikms_db_data:/var/lib/postgresql/data
|
|
560
818
|
networks: [envsync]
|
|
561
|
-
|
|
819
|
+
${includeRuntimeInfra ? `
|
|
562
820
|
minikms:
|
|
563
821
|
image: ghcr.io/envsync-cloud/minikms:sha-735dfe8
|
|
564
822
|
environment:
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
823
|
+
${renderEnvList({
|
|
824
|
+
MINIKMS_ROOT_KEY: runtimeEnv.MINIKMS_ROOT_KEY,
|
|
825
|
+
MINIKMS_DB_URL: `postgres://postgres:${runtimeEnv.MINIKMS_DB_PASSWORD}@minikms_db:5432/minikms?sslmode=disable`,
|
|
826
|
+
MINIKMS_REDIS_URL: "redis://redis:6379",
|
|
827
|
+
MINIKMS_GRPC_ADDR: "0.0.0.0:50051",
|
|
828
|
+
MINIKMS_TLS_ENABLED: "false"
|
|
829
|
+
})}
|
|
830
|
+
networks: [envsync]` : ""}
|
|
570
831
|
|
|
571
832
|
clickstack:
|
|
572
833
|
image: ${config.images.clickstack}
|
|
@@ -589,6 +850,7 @@ services:
|
|
|
589
850
|
volumes:
|
|
590
851
|
- ${OTEL_AGENT_CONF}:/etc/otel-agent.yaml:ro
|
|
591
852
|
networks: [envsync]
|
|
853
|
+
${includeAppServices ? `
|
|
592
854
|
|
|
593
855
|
landing_nginx:
|
|
594
856
|
image: nginx:1.27-alpine
|
|
@@ -607,14 +869,14 @@ services:
|
|
|
607
869
|
envsync_api_blue:
|
|
608
870
|
image: ${config.images.api}
|
|
609
871
|
environment:
|
|
610
|
-
${
|
|
872
|
+
${renderEnvList(apiEnvironment)}
|
|
611
873
|
networks: [envsync]
|
|
612
874
|
|
|
613
875
|
envsync_api_green:
|
|
614
876
|
image: ${config.images.api}
|
|
615
877
|
environment:
|
|
616
|
-
${
|
|
617
|
-
networks: [envsync]
|
|
878
|
+
${renderEnvList(apiEnvironment)}
|
|
879
|
+
networks: [envsync]` : ""}
|
|
618
880
|
|
|
619
881
|
networks:
|
|
620
882
|
envsync:
|
|
@@ -633,6 +895,232 @@ volumes:
|
|
|
633
895
|
clickstack_ch_logs:
|
|
634
896
|
`.trimStart();
|
|
635
897
|
}
|
|
898
|
+
function writeDeployArtifacts(config, generated) {
|
|
899
|
+
const runtimeEnv = buildRuntimeEnv(config, generated);
|
|
900
|
+
writeFile(DEPLOY_ENV, renderEnvFile(runtimeEnv), 384);
|
|
901
|
+
writeFile(
|
|
902
|
+
INTERNAL_CONFIG_JSON,
|
|
903
|
+
JSON.stringify({ config, generated: mergeGeneratedState(runtimeEnv, generated) }, null, 2) + "\n"
|
|
904
|
+
);
|
|
905
|
+
writeFile(VERSIONS_LOCK, JSON.stringify(config.images, null, 2) + "\n");
|
|
906
|
+
writeFile(KEYCLOAK_REALM_FILE, renderKeycloakRealm(config, runtimeEnv));
|
|
907
|
+
writeFile(TRAEFIK_DYNAMIC_FILE, renderTraefikDynamicConfig(config));
|
|
908
|
+
writeFile(BOOTSTRAP_BASE_STACK_FILE, renderStack(config, runtimeEnv, "base"));
|
|
909
|
+
writeFile(BOOTSTRAP_STACK_FILE, renderStack(config, runtimeEnv, "bootstrap"));
|
|
910
|
+
writeFile(STACK_FILE, renderStack(config, runtimeEnv, "full"));
|
|
911
|
+
writeFile(NGINX_WEB_CONF, renderNginxConf("web"));
|
|
912
|
+
writeFile(NGINX_LANDING_CONF, renderNginxConf("landing"));
|
|
913
|
+
writeFile(OTEL_AGENT_CONF, renderOtelAgentConfig(config));
|
|
914
|
+
}
|
|
915
|
+
function saveDesiredConfig(config) {
|
|
916
|
+
const internal = readInternalState();
|
|
917
|
+
const generated = mergeGeneratedState(loadGeneratedEnv(), internal?.generated);
|
|
918
|
+
writeFile(DEPLOY_YAML, toYaml(config) + "\n");
|
|
919
|
+
writeFile(
|
|
920
|
+
INTERNAL_CONFIG_JSON,
|
|
921
|
+
JSON.stringify({ config, generated }, null, 2) + "\n"
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
function ensureRepoCheckout(config) {
|
|
925
|
+
ensureDir(REPO_ROOT);
|
|
926
|
+
if (!exists(path.join(REPO_ROOT, ".git"))) {
|
|
927
|
+
run("git", ["clone", config.source.repo_url, REPO_ROOT]);
|
|
928
|
+
}
|
|
929
|
+
run("git", ["remote", "set-url", "origin", config.source.repo_url], { cwd: REPO_ROOT });
|
|
930
|
+
run("git", ["fetch", "--tags", "--force", "origin"], { cwd: REPO_ROOT });
|
|
931
|
+
run("git", ["checkout", "--force", config.source.ref], { cwd: REPO_ROOT });
|
|
932
|
+
}
|
|
933
|
+
function extractStaticBundle(image, targetDir) {
|
|
934
|
+
ensureDir(targetDir);
|
|
935
|
+
const containerId = run("docker", ["create", image], { quiet: true }).trim();
|
|
936
|
+
try {
|
|
937
|
+
run("docker", ["cp", `${containerId}:/app/dist/.`, targetDir]);
|
|
938
|
+
} finally {
|
|
939
|
+
run("docker", ["rm", "-f", containerId], { quiet: true });
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
function buildKeycloakImage(imageTag, repoRoot = REPO_ROOT) {
|
|
943
|
+
const buildContext = path.join(repoRoot, "packages/envsync-keycloak-theme");
|
|
944
|
+
if (!exists(path.join(buildContext, "Dockerfile"))) {
|
|
945
|
+
throw new Error(`Missing Keycloak Docker build context at ${buildContext}`);
|
|
946
|
+
}
|
|
947
|
+
run("docker", ["build", "-t", imageTag, buildContext]);
|
|
948
|
+
}
|
|
949
|
+
function stackNetworkName(config) {
|
|
950
|
+
return `${config.services.stack_name}_envsync`;
|
|
951
|
+
}
|
|
952
|
+
function assertSwarmManager() {
|
|
953
|
+
const state = tryRun("docker", ["info", "--format", "{{.Swarm.LocalNodeState}}|{{.Swarm.ControlAvailable}}"], { quiet: true }).trim();
|
|
954
|
+
if (state !== "active|true") {
|
|
955
|
+
throw new Error("Docker Swarm is not initialized on this node. Run 'docker swarm init' or 'envsync-deploy preinstall' first.");
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
function waitForCommand(config, label, image, command, timeoutSeconds = 120, env = {}, volumes = []) {
|
|
959
|
+
const deadline = Date.now() + timeoutSeconds * 1e3;
|
|
960
|
+
while (Date.now() < deadline) {
|
|
961
|
+
const args = ["run", "--rm", "--network", stackNetworkName(config)];
|
|
962
|
+
for (const volume of volumes) {
|
|
963
|
+
args.push("-v", volume);
|
|
964
|
+
}
|
|
965
|
+
for (const [key, value] of Object.entries(env)) {
|
|
966
|
+
args.push("-e", `${key}=${value}`);
|
|
967
|
+
}
|
|
968
|
+
args.push(image, "sh", "-lc", command);
|
|
969
|
+
if (commandSucceeds("docker", args)) {
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
sleepSeconds(2);
|
|
973
|
+
}
|
|
974
|
+
throw new Error(`Timed out waiting for ${label}`);
|
|
975
|
+
}
|
|
976
|
+
function waitForPostgresService(config, label, host, user, password) {
|
|
977
|
+
waitForCommand(config, `${label} database readiness`, "postgres:17", `pg_isready -h ${host} -U ${user}`, 120, {
|
|
978
|
+
PGPASSWORD: password
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
function waitForRedisService(config) {
|
|
982
|
+
waitForCommand(config, "redis readiness", "redis:7", "redis-cli -h redis ping | grep PONG");
|
|
983
|
+
}
|
|
984
|
+
function waitForTcpService(config, label, host, port, timeoutSeconds = 120) {
|
|
985
|
+
const deadline = Date.now() + timeoutSeconds * 1e3;
|
|
986
|
+
while (Date.now() < deadline) {
|
|
987
|
+
if (commandSucceeds(
|
|
988
|
+
"docker",
|
|
989
|
+
["run", "--rm", "--network", stackNetworkName(config), "alpine:3.20", "sh", "-lc", `nc -z -w 2 ${host} ${port}`]
|
|
990
|
+
)) {
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
sleepSeconds(2);
|
|
994
|
+
}
|
|
995
|
+
throw new Error(`Timed out waiting for ${label} at ${host}:${port}`);
|
|
996
|
+
}
|
|
997
|
+
function waitForHttpService(config, label, url, timeoutSeconds = 120) {
|
|
998
|
+
waitForCommand(config, `${label} HTTP readiness`, "alpine:3.20", `wget -q -O /dev/null ${JSON.stringify(url)}`, timeoutSeconds);
|
|
999
|
+
}
|
|
1000
|
+
function runOpenFgaMigrate(config, runtimeEnv) {
|
|
1001
|
+
run("docker", [
|
|
1002
|
+
"run",
|
|
1003
|
+
"--rm",
|
|
1004
|
+
"--network",
|
|
1005
|
+
stackNetworkName(config),
|
|
1006
|
+
"-e",
|
|
1007
|
+
"OPENFGA_DATASTORE_ENGINE=postgres",
|
|
1008
|
+
"-e",
|
|
1009
|
+
`OPENFGA_DATASTORE_URI=postgres://openfga:${runtimeEnv.OPENFGA_DB_PASSWORD}@openfga_db:5432/openfga?sslmode=disable`,
|
|
1010
|
+
"openfga/openfga:v1.12.0",
|
|
1011
|
+
"migrate"
|
|
1012
|
+
]);
|
|
1013
|
+
}
|
|
1014
|
+
function runMiniKmsMigrate(config, runtimeEnv) {
|
|
1015
|
+
run("docker", [
|
|
1016
|
+
"run",
|
|
1017
|
+
"--rm",
|
|
1018
|
+
"--network",
|
|
1019
|
+
stackNetworkName(config),
|
|
1020
|
+
"-e",
|
|
1021
|
+
`PGPASSWORD=${runtimeEnv.MINIKMS_DB_PASSWORD}`,
|
|
1022
|
+
"-v",
|
|
1023
|
+
`${path.join(REPO_ROOT, "docker/minikms/migrations")}:/migrations:ro`,
|
|
1024
|
+
"postgres:17",
|
|
1025
|
+
"sh",
|
|
1026
|
+
"-lc",
|
|
1027
|
+
"psql -h minikms_db -U postgres -d minikms -f /migrations/001_initial_schema.sql && psql -h minikms_db -U postgres -d minikms -f /migrations/002_vault_storage.sql"
|
|
1028
|
+
]);
|
|
1029
|
+
}
|
|
1030
|
+
function runBootstrapInit(config) {
|
|
1031
|
+
const output = run(
|
|
1032
|
+
"docker",
|
|
1033
|
+
[
|
|
1034
|
+
"run",
|
|
1035
|
+
"--rm",
|
|
1036
|
+
"--network",
|
|
1037
|
+
stackNetworkName(config),
|
|
1038
|
+
"--env-file",
|
|
1039
|
+
DEPLOY_ENV,
|
|
1040
|
+
"-e",
|
|
1041
|
+
"SKIP_ROOT_ENV=1",
|
|
1042
|
+
"-e",
|
|
1043
|
+
"SKIP_ROOT_ENV_WRITE=1",
|
|
1044
|
+
config.images.api,
|
|
1045
|
+
"bun",
|
|
1046
|
+
"run",
|
|
1047
|
+
"scripts/prod-init.ts",
|
|
1048
|
+
"--json",
|
|
1049
|
+
"--no-write-root-env"
|
|
1050
|
+
],
|
|
1051
|
+
{ quiet: true }
|
|
1052
|
+
).trim();
|
|
1053
|
+
const result = JSON.parse(output);
|
|
1054
|
+
if (!result.openfgaStoreId || !result.openfgaModelId) {
|
|
1055
|
+
throw new Error("Bootstrap init did not return OpenFGA IDs");
|
|
1056
|
+
}
|
|
1057
|
+
return {
|
|
1058
|
+
openfgaStoreId: result.openfgaStoreId,
|
|
1059
|
+
openfgaModelId: result.openfgaModelId
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
function hasCompleteBootstrapState(generated) {
|
|
1063
|
+
return REQUIRED_BOOTSTRAP_ENV_KEYS.every((key) => {
|
|
1064
|
+
switch (key) {
|
|
1065
|
+
case "S3_SECRET_KEY":
|
|
1066
|
+
return generated.secrets.s3_secret_key.length > 0;
|
|
1067
|
+
case "KEYCLOAK_WEB_CLIENT_SECRET":
|
|
1068
|
+
return generated.secrets.keycloak_web_client_secret.length > 0;
|
|
1069
|
+
case "KEYCLOAK_API_CLIENT_SECRET":
|
|
1070
|
+
return generated.secrets.keycloak_api_client_secret.length > 0;
|
|
1071
|
+
case "OPENFGA_DB_PASSWORD":
|
|
1072
|
+
return generated.secrets.openfga_db_password.length > 0;
|
|
1073
|
+
case "MINIKMS_ROOT_KEY":
|
|
1074
|
+
return generated.secrets.minikms_root_key.length > 0;
|
|
1075
|
+
case "MINIKMS_DB_PASSWORD":
|
|
1076
|
+
return generated.secrets.minikms_db_password.length > 0;
|
|
1077
|
+
case "OPENFGA_STORE_ID":
|
|
1078
|
+
return generated.openfga.store_id.length > 0;
|
|
1079
|
+
case "OPENFGA_MODEL_ID":
|
|
1080
|
+
return generated.openfga.model_id.length > 0;
|
|
1081
|
+
default:
|
|
1082
|
+
return false;
|
|
1083
|
+
}
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
function assertBootstrapState(generated) {
|
|
1087
|
+
if (!hasCompleteBootstrapState(generated)) {
|
|
1088
|
+
throw new Error("Missing bootstrap state. Run 'envsync-deploy bootstrap' first.");
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
function parseReplicaHealth(raw) {
|
|
1092
|
+
const match = raw.match(/^(\d+)\/(\d+)$/);
|
|
1093
|
+
if (!match) return raw.trim() ? "degraded" : "missing";
|
|
1094
|
+
const current = Number(match[1]);
|
|
1095
|
+
const desired = Number(match[2]);
|
|
1096
|
+
if (desired === 0) return "missing";
|
|
1097
|
+
if (current === desired) return "healthy";
|
|
1098
|
+
return "degraded";
|
|
1099
|
+
}
|
|
1100
|
+
function listStackServices(config) {
|
|
1101
|
+
const output = tryRun(
|
|
1102
|
+
"docker",
|
|
1103
|
+
["stack", "services", config.services.stack_name, "--format", "{{.Name}}|{{.Replicas}}"],
|
|
1104
|
+
{ quiet: true }
|
|
1105
|
+
);
|
|
1106
|
+
const services = /* @__PURE__ */ new Map();
|
|
1107
|
+
for (const line of output.split(/\r?\n/)) {
|
|
1108
|
+
if (!line.trim()) continue;
|
|
1109
|
+
const [name, replicas] = line.split("|");
|
|
1110
|
+
services.set(name, parseReplicaHealth(replicas ?? ""));
|
|
1111
|
+
}
|
|
1112
|
+
return services;
|
|
1113
|
+
}
|
|
1114
|
+
function serviceHealth(services, name) {
|
|
1115
|
+
return services.get(`${name}`) ?? "missing";
|
|
1116
|
+
}
|
|
1117
|
+
function apiHealth(services, stackName) {
|
|
1118
|
+
const blue = serviceHealth(services, `${stackName}_envsync_api_blue`);
|
|
1119
|
+
const green = serviceHealth(services, `${stackName}_envsync_api_green`);
|
|
1120
|
+
if (blue === "missing" && green === "missing") return "missing";
|
|
1121
|
+
if (blue === "healthy" || green === "healthy") return "healthy";
|
|
1122
|
+
return "degraded";
|
|
1123
|
+
}
|
|
636
1124
|
async function cmdPreinstall() {
|
|
637
1125
|
ensureDir(HOST_ROOT);
|
|
638
1126
|
ensureDir(DEPLOY_ROOT);
|
|
@@ -655,7 +1143,9 @@ async function cmdPreinstall() {
|
|
|
655
1143
|
async function cmdSetup() {
|
|
656
1144
|
const rootDomain = await ask("Root domain", "example.com");
|
|
657
1145
|
const acmeEmail = await ask("ACME email", `admin@${rootDomain}`);
|
|
658
|
-
const
|
|
1146
|
+
const releaseVersion = await ask("Release version", getDeployCliVersion());
|
|
1147
|
+
assertSemverVersion(releaseVersion, "release version");
|
|
1148
|
+
const releaseImages = versionedImages(releaseVersion);
|
|
659
1149
|
const adminUser = await ask("Keycloak admin user", "admin");
|
|
660
1150
|
const adminPassword = await ask("Keycloak admin password", randomSecret(12));
|
|
661
1151
|
const smtpHost = await ask("SMTP host", "smtp.example.com");
|
|
@@ -669,12 +1159,16 @@ async function cmdSetup() {
|
|
|
669
1159
|
const publicObs = await ask("Expose obs.<domain> publicly (true/false)", "true") === "true";
|
|
670
1160
|
const mailpitEnabled = await ask("Enable mailpit (true/false)", "false") === "true";
|
|
671
1161
|
const config = {
|
|
1162
|
+
source: defaultSourceConfig(releaseVersion),
|
|
1163
|
+
release: {
|
|
1164
|
+
version: releaseVersion
|
|
1165
|
+
},
|
|
672
1166
|
domain: { root_domain: rootDomain, acme_email: acmeEmail },
|
|
673
1167
|
images: {
|
|
674
|
-
api:
|
|
675
|
-
keycloak:
|
|
676
|
-
web:
|
|
677
|
-
landing:
|
|
1168
|
+
api: releaseImages.api,
|
|
1169
|
+
keycloak: releaseImages.keycloak,
|
|
1170
|
+
web: releaseImages.web,
|
|
1171
|
+
landing: releaseImages.landing,
|
|
678
1172
|
clickstack: "clickhouse/clickstack-all-in-one:latest",
|
|
679
1173
|
traefik: "traefik:v3.1",
|
|
680
1174
|
otel_agent: "otel/opentelemetry-collector-contrib:0.111.0"
|
|
@@ -719,36 +1213,59 @@ async function cmdSetup() {
|
|
|
719
1213
|
mailpit_enabled: mailpitEnabled,
|
|
720
1214
|
s3_public: true,
|
|
721
1215
|
s3_console_public: true
|
|
722
|
-
}
|
|
723
|
-
release_channel: channel
|
|
1216
|
+
}
|
|
724
1217
|
};
|
|
725
|
-
|
|
726
|
-
if (!exists(path.join(REPO_ROOT, ".git"))) {
|
|
727
|
-
run("git", ["clone", "https://github.com/EnvSync-Cloud/envsync.git", REPO_ROOT]);
|
|
728
|
-
}
|
|
729
|
-
saveConfig(config);
|
|
1218
|
+
saveDesiredConfig(config);
|
|
730
1219
|
console.log(`Config written to ${DEPLOY_YAML}`);
|
|
1220
|
+
console.log(`Pinned source checkout: ${config.source.repo_url} @ ${config.source.ref}`);
|
|
731
1221
|
console.log("Create these DNS records:");
|
|
732
1222
|
console.log(JSON.stringify(domainMap(rootDomain), null, 2));
|
|
733
1223
|
}
|
|
734
|
-
function
|
|
735
|
-
|
|
736
|
-
const
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
1224
|
+
async function cmdBootstrap() {
|
|
1225
|
+
const { config, generated } = loadState();
|
|
1226
|
+
const nextGenerated = ensureGeneratedRuntimeState(generated);
|
|
1227
|
+
const runtimeEnv = buildRuntimeEnv(config, nextGenerated);
|
|
1228
|
+
assertSwarmManager();
|
|
1229
|
+
ensureRepoCheckout(config);
|
|
1230
|
+
writeDeployArtifacts(config, nextGenerated);
|
|
1231
|
+
buildKeycloakImage(config.images.keycloak);
|
|
1232
|
+
run("docker", ["stack", "deploy", "-c", BOOTSTRAP_BASE_STACK_FILE, config.services.stack_name]);
|
|
1233
|
+
waitForPostgresService(config, "postgres", "postgres", "postgres", "envsync-postgres");
|
|
1234
|
+
waitForRedisService(config);
|
|
1235
|
+
waitForTcpService(config, "rustfs", "rustfs", 9e3);
|
|
1236
|
+
waitForPostgresService(config, "keycloak", "keycloak_db", "keycloak", runtimeEnv.KEYCLOAK_ADMIN_PASSWORD);
|
|
1237
|
+
waitForPostgresService(config, "openfga", "openfga_db", "openfga", runtimeEnv.OPENFGA_DB_PASSWORD);
|
|
1238
|
+
waitForPostgresService(config, "minikms", "minikms_db", "postgres", runtimeEnv.MINIKMS_DB_PASSWORD);
|
|
1239
|
+
runOpenFgaMigrate(config, runtimeEnv);
|
|
1240
|
+
runMiniKmsMigrate(config, runtimeEnv);
|
|
1241
|
+
run("docker", ["stack", "deploy", "-c", BOOTSTRAP_STACK_FILE, config.services.stack_name]);
|
|
1242
|
+
waitForHttpService(config, "keycloak", "http://keycloak:8080/realms/master");
|
|
1243
|
+
waitForHttpService(config, "openfga", "http://openfga:8090/stores");
|
|
1244
|
+
waitForTcpService(config, "minikms", "minikms", 50051);
|
|
1245
|
+
const initResult = runBootstrapInit(config);
|
|
1246
|
+
const bootstrappedGenerated = normalizeGeneratedState({
|
|
1247
|
+
openfga: {
|
|
1248
|
+
store_id: initResult.openfgaStoreId,
|
|
1249
|
+
model_id: initResult.openfgaModelId
|
|
1250
|
+
},
|
|
1251
|
+
secrets: nextGenerated.secrets,
|
|
1252
|
+
bootstrap: {
|
|
1253
|
+
completed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1254
|
+
}
|
|
1255
|
+
});
|
|
1256
|
+
writeDeployArtifacts(config, bootstrappedGenerated);
|
|
1257
|
+
console.log("Bootstrap completed.");
|
|
749
1258
|
}
|
|
750
1259
|
async function cmdDeploy() {
|
|
751
|
-
const config =
|
|
1260
|
+
const { config, generated } = loadState();
|
|
1261
|
+
assertSwarmManager();
|
|
1262
|
+
assertBootstrapState(generated);
|
|
1263
|
+
const services = listStackServices(config);
|
|
1264
|
+
if (serviceHealth(services, `${config.services.stack_name}_keycloak`) === "missing" || serviceHealth(services, `${config.services.stack_name}_openfga`) === "missing" || serviceHealth(services, `${config.services.stack_name}_minikms`) === "missing") {
|
|
1265
|
+
throw new Error("Bootstrap has not completed successfully. Run 'envsync-deploy bootstrap' again.");
|
|
1266
|
+
}
|
|
1267
|
+
ensureRepoCheckout(config);
|
|
1268
|
+
writeDeployArtifacts(config, generated);
|
|
752
1269
|
buildKeycloakImage(config.images.keycloak);
|
|
753
1270
|
ensureDir(`${RELEASES_ROOT}/web/current`);
|
|
754
1271
|
ensureDir(`${RELEASES_ROOT}/landing/current`);
|
|
@@ -759,12 +1276,29 @@ async function cmdDeploy() {
|
|
|
759
1276
|
run("docker", ["stack", "deploy", "-c", STACK_FILE, config.services.stack_name]);
|
|
760
1277
|
}
|
|
761
1278
|
async function cmdHealth(asJson) {
|
|
762
|
-
const config =
|
|
1279
|
+
const { config, generated } = loadState();
|
|
763
1280
|
const hosts = domainMap(config.domain.root_domain);
|
|
764
|
-
const services =
|
|
1281
|
+
const services = listStackServices(config);
|
|
1282
|
+
const stackName = config.services.stack_name;
|
|
1283
|
+
const bootstrapServices = {
|
|
1284
|
+
postgres: serviceHealth(services, `${stackName}_postgres`),
|
|
1285
|
+
redis: serviceHealth(services, `${stackName}_redis`),
|
|
1286
|
+
rustfs: serviceHealth(services, `${stackName}_rustfs`),
|
|
1287
|
+
keycloak: serviceHealth(services, `${stackName}_keycloak`),
|
|
1288
|
+
openfga: serviceHealth(services, `${stackName}_openfga`),
|
|
1289
|
+
minikms: serviceHealth(services, `${stackName}_minikms`)
|
|
1290
|
+
};
|
|
765
1291
|
const checks = {
|
|
766
|
-
|
|
767
|
-
|
|
1292
|
+
bootstrap: {
|
|
1293
|
+
completed: hasCompleteBootstrapState(generated) && generated.bootstrap.completed_at.length > 0,
|
|
1294
|
+
completed_at: generated.bootstrap.completed_at || null,
|
|
1295
|
+
services: bootstrapServices
|
|
1296
|
+
},
|
|
1297
|
+
deploy: {
|
|
1298
|
+
api: apiHealth(services, stackName),
|
|
1299
|
+
web: serviceHealth(services, `${stackName}_web_nginx`),
|
|
1300
|
+
landing: serviceHealth(services, `${stackName}_landing_nginx`)
|
|
1301
|
+
},
|
|
768
1302
|
public: {
|
|
769
1303
|
landing: `https://${hosts.landing}`,
|
|
770
1304
|
app: `https://${hosts.app}`,
|
|
@@ -773,25 +1307,27 @@ async function cmdHealth(asJson) {
|
|
|
773
1307
|
obs: `https://${hosts.obs}`
|
|
774
1308
|
}
|
|
775
1309
|
};
|
|
776
|
-
if (asJson)
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
console.log(JSON.stringify(checks.public, null, 2));
|
|
1310
|
+
if (asJson) {
|
|
1311
|
+
console.log(JSON.stringify(checks, null, 2));
|
|
1312
|
+
return;
|
|
780
1313
|
}
|
|
1314
|
+
console.log(JSON.stringify(checks, null, 2));
|
|
781
1315
|
}
|
|
782
1316
|
async function cmdUpgrade() {
|
|
783
|
-
const config =
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
1317
|
+
const { config } = loadState();
|
|
1318
|
+
config.images = {
|
|
1319
|
+
...config.images,
|
|
1320
|
+
...versionedImages(config.release.version)
|
|
1321
|
+
};
|
|
1322
|
+
saveDesiredConfig(config);
|
|
787
1323
|
await cmdDeploy();
|
|
788
1324
|
}
|
|
789
1325
|
async function cmdUpgradeDeps() {
|
|
790
|
-
const config =
|
|
1326
|
+
const { config } = loadState();
|
|
791
1327
|
config.images.traefik = "traefik:v3.1";
|
|
792
1328
|
config.images.clickstack = "clickhouse/clickstack-all-in-one:latest";
|
|
793
1329
|
config.images.otel_agent = "otel/opentelemetry-collector-contrib:0.111.0";
|
|
794
|
-
|
|
1330
|
+
saveDesiredConfig(config);
|
|
795
1331
|
await cmdDeploy();
|
|
796
1332
|
}
|
|
797
1333
|
function sha256File(filePath) {
|
|
@@ -833,7 +1369,7 @@ function restoreDockerVolume(volumeName, sourceDir) {
|
|
|
833
1369
|
]);
|
|
834
1370
|
}
|
|
835
1371
|
async function cmdBackup() {
|
|
836
|
-
const config =
|
|
1372
|
+
const { config } = loadState();
|
|
837
1373
|
ensureDir(config.backup.output_dir);
|
|
838
1374
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:]/g, "-");
|
|
839
1375
|
const archiveBase = path.join(config.backup.output_dir, `envsync-backup-${timestamp}`);
|
|
@@ -845,14 +1381,15 @@ async function cmdBackup() {
|
|
|
845
1381
|
writeFile(path.join(staged, "deploy.yaml"), fs.readFileSync(DEPLOY_YAML, "utf8"));
|
|
846
1382
|
writeFile(path.join(staged, "config.json"), fs.readFileSync(INTERNAL_CONFIG_JSON, "utf8"));
|
|
847
1383
|
writeFile(path.join(staged, "versions.lock.json"), fs.readFileSync(VERSIONS_LOCK, "utf8"));
|
|
1384
|
+
writeFile(path.join(staged, "docker-stack.bootstrap.base.yaml"), fs.readFileSync(BOOTSTRAP_BASE_STACK_FILE, "utf8"));
|
|
1385
|
+
writeFile(path.join(staged, "docker-stack.bootstrap.yaml"), fs.readFileSync(BOOTSTRAP_STACK_FILE, "utf8"));
|
|
848
1386
|
writeFile(path.join(staged, "docker-stack.yaml"), fs.readFileSync(STACK_FILE, "utf8"));
|
|
849
1387
|
writeFile(path.join(staged, "traefik-dynamic.yaml"), fs.readFileSync(TRAEFIK_DYNAMIC_FILE, "utf8"));
|
|
850
1388
|
writeFile(path.join(staged, "keycloak-realm.envsync.json"), fs.readFileSync(KEYCLOAK_REALM_FILE, "utf8"));
|
|
851
1389
|
writeFile(path.join(staged, "otel-agent.yaml"), fs.readFileSync(OTEL_AGENT_CONF, "utf8"));
|
|
852
1390
|
const volumesDir = path.join(staged, "volumes");
|
|
853
1391
|
for (const volume of STACK_VOLUMES) {
|
|
854
|
-
|
|
855
|
-
backupDockerVolume(stackVolumeName(config, volume), target);
|
|
1392
|
+
backupDockerVolume(stackVolumeName(config, volume), path.join(volumesDir, volume));
|
|
856
1393
|
}
|
|
857
1394
|
run("bash", ["-lc", `tar -czf ${JSON.stringify(tarPath)} -C ${JSON.stringify(staged)} .`]);
|
|
858
1395
|
const manifest = {
|
|
@@ -867,7 +1404,6 @@ async function cmdBackup() {
|
|
|
867
1404
|
}
|
|
868
1405
|
async function cmdRestore(archivePath) {
|
|
869
1406
|
if (!archivePath) throw new Error("restore requires a .tar.gz path");
|
|
870
|
-
const config = loadConfig();
|
|
871
1407
|
const restoreRoot = path.join(BACKUPS_ROOT, `restore-${Date.now()}`);
|
|
872
1408
|
ensureDir(restoreRoot);
|
|
873
1409
|
run("bash", ["-lc", `tar -xzf ${JSON.stringify(archivePath)} -C ${JSON.stringify(restoreRoot)}`]);
|
|
@@ -875,14 +1411,17 @@ async function cmdRestore(archivePath) {
|
|
|
875
1411
|
writeFile(DEPLOY_YAML, fs.readFileSync(path.join(restoreRoot, "deploy.yaml"), "utf8"));
|
|
876
1412
|
writeFile(INTERNAL_CONFIG_JSON, fs.readFileSync(path.join(restoreRoot, "config.json"), "utf8"));
|
|
877
1413
|
writeFile(VERSIONS_LOCK, fs.readFileSync(path.join(restoreRoot, "versions.lock.json"), "utf8"));
|
|
1414
|
+
writeFile(BOOTSTRAP_BASE_STACK_FILE, fs.readFileSync(path.join(restoreRoot, "docker-stack.bootstrap.base.yaml"), "utf8"));
|
|
1415
|
+
writeFile(BOOTSTRAP_STACK_FILE, fs.readFileSync(path.join(restoreRoot, "docker-stack.bootstrap.yaml"), "utf8"));
|
|
878
1416
|
writeFile(STACK_FILE, fs.readFileSync(path.join(restoreRoot, "docker-stack.yaml"), "utf8"));
|
|
879
1417
|
writeFile(TRAEFIK_DYNAMIC_FILE, fs.readFileSync(path.join(restoreRoot, "traefik-dynamic.yaml"), "utf8"));
|
|
880
1418
|
writeFile(KEYCLOAK_REALM_FILE, fs.readFileSync(path.join(restoreRoot, "keycloak-realm.envsync.json"), "utf8"));
|
|
881
1419
|
writeFile(OTEL_AGENT_CONF, fs.readFileSync(path.join(restoreRoot, "otel-agent.yaml"), "utf8"));
|
|
1420
|
+
const config = loadConfig();
|
|
882
1421
|
for (const volume of STACK_VOLUMES) {
|
|
883
1422
|
restoreDockerVolume(stackVolumeName(config, volume), path.join(restoreRoot, "volumes", volume));
|
|
884
1423
|
}
|
|
885
|
-
|
|
1424
|
+
console.log("Restore completed. Run 'envsync-deploy deploy' to start services.");
|
|
886
1425
|
}
|
|
887
1426
|
async function main() {
|
|
888
1427
|
const command = process.argv[2];
|
|
@@ -894,6 +1433,9 @@ async function main() {
|
|
|
894
1433
|
case "setup":
|
|
895
1434
|
await cmdSetup();
|
|
896
1435
|
break;
|
|
1436
|
+
case "bootstrap":
|
|
1437
|
+
await cmdBootstrap();
|
|
1438
|
+
break;
|
|
897
1439
|
case "deploy":
|
|
898
1440
|
await cmdDeploy();
|
|
899
1441
|
break;
|
|
@@ -913,7 +1455,7 @@ async function main() {
|
|
|
913
1455
|
await cmdRestore(flag ?? "");
|
|
914
1456
|
break;
|
|
915
1457
|
default:
|
|
916
|
-
console.log("Usage: envsync-deploy <preinstall|setup|deploy|health|upgrade|upgrade-deps|backup|restore>");
|
|
1458
|
+
console.log("Usage: envsync-deploy <preinstall|setup|bootstrap|deploy|health|upgrade|upgrade-deps|backup|restore>");
|
|
917
1459
|
process.exit(command ? 1 : 0);
|
|
918
1460
|
}
|
|
919
1461
|
}
|