@envsync-cloud/deploy-cli 0.6.1 → 0.6.2
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 +14 -2
- package/dist/index.js +473 -127
- 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,29 @@ 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
|
+
Bootstrap infra, migrations, RustFS, and OpenFGA:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npx @envsync-cloud/deploy-cli bootstrap
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Deploy the pending API and frontend services:
|
|
66
73
|
|
|
67
74
|
```bash
|
|
68
75
|
npx @envsync-cloud/deploy-cli deploy
|
|
69
76
|
```
|
|
70
77
|
|
|
78
|
+
The staged flow is:
|
|
79
|
+
- `setup` writes desired config
|
|
80
|
+
- `bootstrap` starts infra and persists generated runtime env state
|
|
81
|
+
- `deploy` starts the pending API and frontend services
|
|
82
|
+
|
|
71
83
|
Check service health:
|
|
72
84
|
|
|
73
85
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -17,6 +17,7 @@ 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_STACK_FILE = "/opt/envsync/deploy/docker-stack.bootstrap.yaml";
|
|
20
21
|
var TRAEFIK_DYNAMIC_FILE = "/opt/envsync/deploy/traefik-dynamic.yaml";
|
|
21
22
|
var KEYCLOAK_REALM_FILE = "/opt/envsync/deploy/keycloak-realm.envsync.json";
|
|
22
23
|
var NGINX_WEB_CONF = "/opt/envsync/deploy/nginx-web.conf";
|
|
@@ -34,6 +35,16 @@ var STACK_VOLUMES = [
|
|
|
34
35
|
"clickstack_ch_data",
|
|
35
36
|
"clickstack_ch_logs"
|
|
36
37
|
];
|
|
38
|
+
var REQUIRED_BOOTSTRAP_ENV_KEYS = [
|
|
39
|
+
"S3_SECRET_KEY",
|
|
40
|
+
"KEYCLOAK_WEB_CLIENT_SECRET",
|
|
41
|
+
"KEYCLOAK_API_CLIENT_SECRET",
|
|
42
|
+
"OPENFGA_DB_PASSWORD",
|
|
43
|
+
"MINIKMS_ROOT_KEY",
|
|
44
|
+
"MINIKMS_DB_PASSWORD",
|
|
45
|
+
"OPENFGA_STORE_ID",
|
|
46
|
+
"OPENFGA_MODEL_ID"
|
|
47
|
+
];
|
|
37
48
|
function run(cmd, args, opts = {}) {
|
|
38
49
|
const result = spawnSync(cmd, args, {
|
|
39
50
|
cwd: opts.cwd,
|
|
@@ -48,6 +59,22 @@ ${stderr}` : ""}`);
|
|
|
48
59
|
}
|
|
49
60
|
return result.stdout?.toString() ?? "";
|
|
50
61
|
}
|
|
62
|
+
function tryRun(cmd, args, opts = {}) {
|
|
63
|
+
try {
|
|
64
|
+
return run(cmd, args, opts);
|
|
65
|
+
} catch {
|
|
66
|
+
return "";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function commandSucceeds(cmd, args, opts = {}) {
|
|
70
|
+
const result = spawnSync(cmd, args, {
|
|
71
|
+
cwd: opts.cwd,
|
|
72
|
+
env: { ...process.env, ...opts.env },
|
|
73
|
+
stdio: "ignore",
|
|
74
|
+
encoding: "utf8"
|
|
75
|
+
});
|
|
76
|
+
return result.status === 0;
|
|
77
|
+
}
|
|
51
78
|
function ensureDir(dir) {
|
|
52
79
|
fs.mkdirSync(dir, { recursive: true });
|
|
53
80
|
}
|
|
@@ -126,9 +153,21 @@ function parseSimpleYamlObject(input) {
|
|
|
126
153
|
}
|
|
127
154
|
return root;
|
|
128
155
|
}
|
|
129
|
-
function
|
|
130
|
-
const
|
|
131
|
-
|
|
156
|
+
function parseEnvFile(content) {
|
|
157
|
+
const out = {};
|
|
158
|
+
for (const line of content.split(/\r?\n/)) {
|
|
159
|
+
const trimmed = line.trim();
|
|
160
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
161
|
+
const eq = trimmed.indexOf("=");
|
|
162
|
+
if (eq === -1) continue;
|
|
163
|
+
const key = trimmed.slice(0, eq).trim();
|
|
164
|
+
let value = trimmed.slice(eq + 1).trim();
|
|
165
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
166
|
+
value = value.slice(1, -1).replace(/\\n/g, "\n").replace(/\\"/g, '"');
|
|
167
|
+
}
|
|
168
|
+
out[key] = value;
|
|
169
|
+
}
|
|
170
|
+
return out;
|
|
132
171
|
}
|
|
133
172
|
async function ask(question, fallback = "") {
|
|
134
173
|
if (!process.stdin.isTTY) return fallback;
|
|
@@ -140,6 +179,9 @@ async function ask(question, fallback = "") {
|
|
|
140
179
|
});
|
|
141
180
|
});
|
|
142
181
|
}
|
|
182
|
+
function sleepSeconds(seconds) {
|
|
183
|
+
spawnSync("sleep", [`${seconds}`], { stdio: "ignore" });
|
|
184
|
+
}
|
|
143
185
|
function domainMap(rootDomain) {
|
|
144
186
|
return {
|
|
145
187
|
landing: rootDomain,
|
|
@@ -152,29 +194,130 @@ function domainMap(rootDomain) {
|
|
|
152
194
|
s3Console: `console.s3.${rootDomain}`
|
|
153
195
|
};
|
|
154
196
|
}
|
|
197
|
+
function getDeployCliVersion() {
|
|
198
|
+
try {
|
|
199
|
+
const packageJsonPath = new URL("../package.json", import.meta.url);
|
|
200
|
+
const raw = fs.readFileSync(packageJsonPath, "utf8");
|
|
201
|
+
return JSON.parse(raw).version ?? "0.0.0";
|
|
202
|
+
} catch {
|
|
203
|
+
return process.env.npm_package_version ?? "0.0.0";
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function defaultSourceConfig() {
|
|
207
|
+
return {
|
|
208
|
+
repo_url: "https://github.com/EnvSync-Cloud/envsync.git",
|
|
209
|
+
ref: `v${getDeployCliVersion()}`
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function normalizeConfig(raw) {
|
|
213
|
+
return {
|
|
214
|
+
...raw,
|
|
215
|
+
source: raw.source ?? defaultSourceConfig()
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function emptyGeneratedState() {
|
|
219
|
+
return {
|
|
220
|
+
openfga: {
|
|
221
|
+
store_id: "",
|
|
222
|
+
model_id: ""
|
|
223
|
+
},
|
|
224
|
+
secrets: {
|
|
225
|
+
s3_secret_key: "",
|
|
226
|
+
keycloak_web_client_secret: "",
|
|
227
|
+
keycloak_api_client_secret: "",
|
|
228
|
+
openfga_db_password: "",
|
|
229
|
+
minikms_root_key: "",
|
|
230
|
+
minikms_db_password: ""
|
|
231
|
+
},
|
|
232
|
+
bootstrap: {
|
|
233
|
+
completed_at: ""
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
function normalizeGeneratedState(raw) {
|
|
238
|
+
const defaults = emptyGeneratedState();
|
|
239
|
+
return {
|
|
240
|
+
openfga: {
|
|
241
|
+
store_id: raw?.openfga?.store_id ?? defaults.openfga.store_id,
|
|
242
|
+
model_id: raw?.openfga?.model_id ?? defaults.openfga.model_id
|
|
243
|
+
},
|
|
244
|
+
secrets: {
|
|
245
|
+
s3_secret_key: raw?.secrets?.s3_secret_key ?? defaults.secrets.s3_secret_key,
|
|
246
|
+
keycloak_web_client_secret: raw?.secrets?.keycloak_web_client_secret ?? defaults.secrets.keycloak_web_client_secret,
|
|
247
|
+
keycloak_api_client_secret: raw?.secrets?.keycloak_api_client_secret ?? defaults.secrets.keycloak_api_client_secret,
|
|
248
|
+
openfga_db_password: raw?.secrets?.openfga_db_password ?? defaults.secrets.openfga_db_password,
|
|
249
|
+
minikms_root_key: raw?.secrets?.minikms_root_key ?? defaults.secrets.minikms_root_key,
|
|
250
|
+
minikms_db_password: raw?.secrets?.minikms_db_password ?? defaults.secrets.minikms_db_password
|
|
251
|
+
},
|
|
252
|
+
bootstrap: {
|
|
253
|
+
completed_at: raw?.bootstrap?.completed_at ?? defaults.bootstrap.completed_at
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
function readInternalState() {
|
|
258
|
+
if (!exists(INTERNAL_CONFIG_JSON)) return null;
|
|
259
|
+
const raw = JSON.parse(fs.readFileSync(INTERNAL_CONFIG_JSON, "utf8"));
|
|
260
|
+
return {
|
|
261
|
+
config: raw.config ? normalizeConfig(raw.config) : void 0,
|
|
262
|
+
generated: normalizeGeneratedState(raw.generated)
|
|
263
|
+
};
|
|
264
|
+
}
|
|
155
265
|
function loadConfig() {
|
|
156
266
|
if (!exists(DEPLOY_YAML)) {
|
|
157
267
|
throw new Error(`Missing deploy config at ${DEPLOY_YAML}. Run setup first.`);
|
|
158
268
|
}
|
|
159
269
|
const raw = fs.readFileSync(DEPLOY_YAML, "utf8");
|
|
160
270
|
if (raw.trimStart().startsWith("{")) {
|
|
161
|
-
return JSON.parse(raw);
|
|
271
|
+
return normalizeConfig(JSON.parse(raw));
|
|
162
272
|
}
|
|
163
|
-
return parseSimpleYamlObject(raw);
|
|
273
|
+
return normalizeConfig(parseSimpleYamlObject(raw));
|
|
164
274
|
}
|
|
165
|
-
function
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
275
|
+
function loadGeneratedEnv() {
|
|
276
|
+
if (!exists(DEPLOY_ENV)) return {};
|
|
277
|
+
return parseEnvFile(fs.readFileSync(DEPLOY_ENV, "utf8"));
|
|
278
|
+
}
|
|
279
|
+
function mergeGeneratedState(env, generated) {
|
|
280
|
+
const normalized = normalizeGeneratedState(generated);
|
|
281
|
+
return normalizeGeneratedState({
|
|
282
|
+
openfga: {
|
|
283
|
+
store_id: env.OPENFGA_STORE_ID ?? normalized.openfga.store_id,
|
|
284
|
+
model_id: env.OPENFGA_MODEL_ID ?? normalized.openfga.model_id
|
|
285
|
+
},
|
|
286
|
+
secrets: {
|
|
287
|
+
s3_secret_key: env.S3_SECRET_KEY ?? normalized.secrets.s3_secret_key,
|
|
288
|
+
keycloak_web_client_secret: env.KEYCLOAK_WEB_CLIENT_SECRET ?? normalized.secrets.keycloak_web_client_secret,
|
|
289
|
+
keycloak_api_client_secret: env.KEYCLOAK_API_CLIENT_SECRET ?? normalized.secrets.keycloak_api_client_secret,
|
|
290
|
+
openfga_db_password: env.OPENFGA_DB_PASSWORD ?? normalized.secrets.openfga_db_password,
|
|
291
|
+
minikms_root_key: env.MINIKMS_ROOT_KEY ?? normalized.secrets.minikms_root_key,
|
|
292
|
+
minikms_db_password: env.MINIKMS_DB_PASSWORD ?? normalized.secrets.minikms_db_password
|
|
293
|
+
},
|
|
294
|
+
bootstrap: normalized.bootstrap
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
function loadState() {
|
|
298
|
+
const config = loadConfig();
|
|
299
|
+
const internal = readInternalState();
|
|
300
|
+
const generated = mergeGeneratedState(loadGeneratedEnv(), internal?.generated);
|
|
301
|
+
return { config, generated };
|
|
302
|
+
}
|
|
303
|
+
function ensureGeneratedRuntimeState(generated) {
|
|
304
|
+
return normalizeGeneratedState({
|
|
305
|
+
openfga: generated.openfga,
|
|
306
|
+
secrets: {
|
|
307
|
+
s3_secret_key: generated.secrets.s3_secret_key || randomSecret(16),
|
|
308
|
+
keycloak_web_client_secret: generated.secrets.keycloak_web_client_secret || randomSecret(),
|
|
309
|
+
keycloak_api_client_secret: generated.secrets.keycloak_api_client_secret || randomSecret(),
|
|
310
|
+
openfga_db_password: generated.secrets.openfga_db_password || randomSecret(),
|
|
311
|
+
minikms_root_key: generated.secrets.minikms_root_key || randomBytes(32).toString("hex"),
|
|
312
|
+
minikms_db_password: generated.secrets.minikms_db_password || randomSecret()
|
|
313
|
+
},
|
|
314
|
+
bootstrap: generated.bootstrap
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
function keycloakImageTag(image) {
|
|
318
|
+
return image.split(":").slice(1).join(":") || "local";
|
|
176
319
|
}
|
|
177
|
-
function
|
|
320
|
+
function buildRuntimeEnv(config, generated) {
|
|
178
321
|
const hosts = domainMap(config.domain.root_domain);
|
|
179
322
|
return {
|
|
180
323
|
NODE_ENV: "production",
|
|
@@ -190,7 +333,7 @@ function buildEnvMap(config) {
|
|
|
190
333
|
S3_BUCKET: "envsync-bucket",
|
|
191
334
|
S3_REGION: "us-east-1",
|
|
192
335
|
S3_ACCESS_KEY: "envsync-rustfs",
|
|
193
|
-
S3_SECRET_KEY:
|
|
336
|
+
S3_SECRET_KEY: generated.secrets.s3_secret_key,
|
|
194
337
|
S3_BUCKET_URL: `https://${hosts.s3}`,
|
|
195
338
|
S3_ENDPOINT: "http://rustfs:9000",
|
|
196
339
|
REDIS_URL: "redis://redis:6379",
|
|
@@ -205,43 +348,40 @@ function buildEnvMap(config) {
|
|
|
205
348
|
KEYCLOAK_ADMIN_USER: config.auth.admin_user,
|
|
206
349
|
KEYCLOAK_ADMIN_PASSWORD: config.auth.admin_password,
|
|
207
350
|
KEYCLOAK_WEB_CLIENT_ID: config.auth.web_client_id,
|
|
208
|
-
KEYCLOAK_WEB_CLIENT_SECRET:
|
|
351
|
+
KEYCLOAK_WEB_CLIENT_SECRET: generated.secrets.keycloak_web_client_secret,
|
|
209
352
|
KEYCLOAK_CLI_CLIENT_ID: config.auth.cli_client_id,
|
|
210
353
|
KEYCLOAK_API_CLIENT_ID: config.auth.api_client_id,
|
|
211
|
-
KEYCLOAK_API_CLIENT_SECRET:
|
|
354
|
+
KEYCLOAK_API_CLIENT_SECRET: generated.secrets.keycloak_api_client_secret,
|
|
212
355
|
KEYCLOAK_WEB_REDIRECT_URI: `https://${hosts.api}/api/access/web/callback`,
|
|
213
356
|
KEYCLOAK_WEB_CALLBACK_URL: `https://${hosts.app}/auth/callback`,
|
|
214
357
|
KEYCLOAK_API_REDIRECT_URI: `https://${hosts.api}/api/access/api/callback`,
|
|
215
358
|
LANDING_PAGE_URL: `https://${hosts.landing}`,
|
|
216
359
|
DASHBOARD_URL: `https://${hosts.app}`,
|
|
217
360
|
OPENFGA_API_URL: "http://openfga:8090",
|
|
218
|
-
OPENFGA_STORE_ID:
|
|
219
|
-
OPENFGA_MODEL_ID:
|
|
220
|
-
OPENFGA_DB_PASSWORD:
|
|
361
|
+
OPENFGA_STORE_ID: generated.openfga.store_id,
|
|
362
|
+
OPENFGA_MODEL_ID: generated.openfga.model_id,
|
|
363
|
+
OPENFGA_DB_PASSWORD: generated.secrets.openfga_db_password,
|
|
221
364
|
MINIKMS_GRPC_ADDR: "minikms:50051",
|
|
222
365
|
MINIKMS_TLS_ENABLED: "false",
|
|
223
|
-
MINIKMS_ROOT_KEY:
|
|
366
|
+
MINIKMS_ROOT_KEY: generated.secrets.minikms_root_key,
|
|
224
367
|
MINIKMS_DB_USER: "postgres",
|
|
225
|
-
MINIKMS_DB_PASSWORD:
|
|
368
|
+
MINIKMS_DB_PASSWORD: generated.secrets.minikms_db_password,
|
|
226
369
|
OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-agent:4318",
|
|
227
370
|
OTEL_SERVICE_NAME: "envsync-api",
|
|
228
371
|
OTEL_SDK_DISABLED: "false",
|
|
229
|
-
CLICKSTACK_URL: `https://${hosts.obs}
|
|
372
|
+
CLICKSTACK_URL: `https://${hosts.obs}`,
|
|
373
|
+
KEYCLOAK_IMAGE_TAG: keycloakImageTag(config.images.keycloak)
|
|
230
374
|
};
|
|
231
375
|
}
|
|
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";
|
|
376
|
+
function renderEnvFile(env) {
|
|
377
|
+
return Object.entries(env).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${key}=${value}`).join("\n") + "\n";
|
|
237
378
|
}
|
|
238
|
-
function
|
|
239
|
-
|
|
379
|
+
function renderEnvList(values, indent = 6) {
|
|
380
|
+
const prefix = " ".repeat(indent);
|
|
381
|
+
return Object.entries(values).map(([key, value]) => `${prefix}- ${JSON.stringify(`${key}=${String(value)}`)}`).join("\n");
|
|
240
382
|
}
|
|
241
|
-
function renderKeycloakRealm(config) {
|
|
383
|
+
function renderKeycloakRealm(config, runtimeEnv) {
|
|
242
384
|
const hosts = domainMap(config.domain.root_domain);
|
|
243
|
-
const webSecret = extractEnvValue("KEYCLOAK_WEB_CLIENT_SECRET");
|
|
244
|
-
const apiSecret = extractEnvValue("KEYCLOAK_API_CLIENT_SECRET");
|
|
245
385
|
return JSON.stringify(
|
|
246
386
|
{
|
|
247
387
|
realm: config.auth.keycloak_realm,
|
|
@@ -254,7 +394,7 @@ function renderKeycloakRealm(config) {
|
|
|
254
394
|
name: "EnvSync Web",
|
|
255
395
|
protocol: "openid-connect",
|
|
256
396
|
publicClient: false,
|
|
257
|
-
secret:
|
|
397
|
+
secret: runtimeEnv.KEYCLOAK_WEB_CLIENT_SECRET,
|
|
258
398
|
standardFlowEnabled: true,
|
|
259
399
|
directAccessGrantsEnabled: false,
|
|
260
400
|
redirectUris: [
|
|
@@ -273,7 +413,7 @@ function renderKeycloakRealm(config) {
|
|
|
273
413
|
name: "EnvSync API",
|
|
274
414
|
protocol: "openid-connect",
|
|
275
415
|
publicClient: false,
|
|
276
|
-
secret:
|
|
416
|
+
secret: runtimeEnv.KEYCLOAK_API_CLIENT_SECRET,
|
|
277
417
|
standardFlowEnabled: true,
|
|
278
418
|
redirectUris: [`https://${hosts.api}/api/access/api/callback`],
|
|
279
419
|
webOrigins: [`https://${hosts.api}`],
|
|
@@ -297,11 +437,6 @@ function renderKeycloakRealm(config) {
|
|
|
297
437
|
2
|
|
298
438
|
) + "\n";
|
|
299
439
|
}
|
|
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
440
|
function renderTraefikDynamicConfig(config) {
|
|
306
441
|
const hosts = domainMap(config.domain.root_domain);
|
|
307
442
|
return [
|
|
@@ -340,17 +475,17 @@ function renderTraefikDynamicConfig(config) {
|
|
|
340
475
|
" servers:",
|
|
341
476
|
" - url: http://web_nginx:8080",
|
|
342
477
|
" routers:",
|
|
343
|
-
|
|
478
|
+
" landing-router:",
|
|
344
479
|
` rule: Host(\`${hosts.landing}\`)`,
|
|
345
480
|
" service: landing",
|
|
346
481
|
" entryPoints: [websecure]",
|
|
347
482
|
" tls: {}",
|
|
348
|
-
|
|
483
|
+
" web-router:",
|
|
349
484
|
` rule: Host(\`${hosts.app}\`)`,
|
|
350
485
|
" service: web",
|
|
351
486
|
" entryPoints: [websecure]",
|
|
352
487
|
" tls: {}",
|
|
353
|
-
|
|
488
|
+
" api-router:",
|
|
354
489
|
` rule: Host(\`${hosts.api}\`)`,
|
|
355
490
|
" service: envsync-api",
|
|
356
491
|
" entryPoints: [websecure]",
|
|
@@ -415,16 +550,17 @@ function renderOtelAgentConfig(config) {
|
|
|
415
550
|
" exporters: [otlphttp/clickstack]"
|
|
416
551
|
].join("\n") + "\n";
|
|
417
552
|
}
|
|
418
|
-
function renderStack(config) {
|
|
553
|
+
function renderStack(config, runtimeEnv, includeAppServices) {
|
|
419
554
|
const hosts = domainMap(config.domain.root_domain);
|
|
420
|
-
const apiEnvironment =
|
|
555
|
+
const apiEnvironment = {
|
|
556
|
+
...runtimeEnv,
|
|
421
557
|
OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-agent:4318",
|
|
422
558
|
KEYCLOAK_URL: "http://keycloak:8080",
|
|
423
559
|
OPENFGA_API_URL: "http://openfga:8090",
|
|
424
560
|
MINIKMS_GRPC_ADDR: "minikms:50051",
|
|
425
561
|
S3_ENDPOINT: "http://rustfs:9000",
|
|
426
562
|
S3_BUCKET_URL: `https://${hosts.s3}`
|
|
427
|
-
}
|
|
563
|
+
};
|
|
428
564
|
return `
|
|
429
565
|
version: "3.9"
|
|
430
566
|
services:
|
|
@@ -458,9 +594,11 @@ services:
|
|
|
458
594
|
postgres:
|
|
459
595
|
image: postgres:17
|
|
460
596
|
environment:
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
597
|
+
${renderEnvList({
|
|
598
|
+
POSTGRES_USER: "postgres",
|
|
599
|
+
POSTGRES_PASSWORD: "envsync-postgres",
|
|
600
|
+
POSTGRES_DB: "envsync"
|
|
601
|
+
})}
|
|
464
602
|
volumes:
|
|
465
603
|
- postgres_data:/var/lib/postgresql/data
|
|
466
604
|
networks: [envsync]
|
|
@@ -474,10 +612,12 @@ services:
|
|
|
474
612
|
rustfs:
|
|
475
613
|
image: rustfs/rustfs:latest
|
|
476
614
|
environment:
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
615
|
+
${renderEnvList({
|
|
616
|
+
RUSTFS_DATA_DIR: "/data",
|
|
617
|
+
RUSTFS_ACCESS_KEY: "envsync-rustfs",
|
|
618
|
+
RUSTFS_SECRET_KEY: runtimeEnv.S3_SECRET_KEY,
|
|
619
|
+
RUSTFS_CONSOLE_ENABLE: "true"
|
|
620
|
+
})}
|
|
481
621
|
volumes:
|
|
482
622
|
- rustfs_data:/data
|
|
483
623
|
networks: [envsync]
|
|
@@ -496,9 +636,11 @@ services:
|
|
|
496
636
|
keycloak_db:
|
|
497
637
|
image: postgres:17
|
|
498
638
|
environment:
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
639
|
+
${renderEnvList({
|
|
640
|
+
POSTGRES_USER: "keycloak",
|
|
641
|
+
POSTGRES_PASSWORD: runtimeEnv.KEYCLOAK_ADMIN_PASSWORD,
|
|
642
|
+
POSTGRES_DB: "keycloak"
|
|
643
|
+
})}
|
|
502
644
|
volumes:
|
|
503
645
|
- keycloak_db_data:/var/lib/postgresql/data
|
|
504
646
|
networks: [envsync]
|
|
@@ -509,17 +651,19 @@ services:
|
|
|
509
651
|
command:
|
|
510
652
|
- /opt/keycloak/bin/kc.sh import --dir /opt/keycloak/data/import --override true && exec /opt/keycloak/bin/kc.sh start-dev
|
|
511
653
|
environment:
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
654
|
+
${renderEnvList({
|
|
655
|
+
KC_DB: "postgres",
|
|
656
|
+
KC_DB_URL: "jdbc:postgresql://keycloak_db:5432/keycloak",
|
|
657
|
+
KC_DB_USERNAME: "keycloak",
|
|
658
|
+
KC_DB_PASSWORD: runtimeEnv.KEYCLOAK_ADMIN_PASSWORD,
|
|
659
|
+
KC_BOOTSTRAP_ADMIN_USERNAME: config.auth.admin_user,
|
|
660
|
+
KC_BOOTSTRAP_ADMIN_PASSWORD: config.auth.admin_password,
|
|
661
|
+
KC_HTTP_ENABLED: "true",
|
|
662
|
+
KC_PROXY_HEADERS: "xforwarded",
|
|
663
|
+
KC_HOSTNAME: hosts.auth
|
|
664
|
+
})}
|
|
521
665
|
volumes:
|
|
522
|
-
- ${
|
|
666
|
+
- ${KEYCLOAK_REALM_FILE}:/opt/keycloak/data/import/realm.json:ro
|
|
523
667
|
networks: [envsync]
|
|
524
668
|
deploy:
|
|
525
669
|
labels:
|
|
@@ -532,9 +676,11 @@ services:
|
|
|
532
676
|
openfga_db:
|
|
533
677
|
image: postgres:17
|
|
534
678
|
environment:
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
679
|
+
${renderEnvList({
|
|
680
|
+
POSTGRES_USER: "openfga",
|
|
681
|
+
POSTGRES_PASSWORD: runtimeEnv.OPENFGA_DB_PASSWORD,
|
|
682
|
+
POSTGRES_DB: "openfga"
|
|
683
|
+
})}
|
|
538
684
|
volumes:
|
|
539
685
|
- openfga_db_data:/var/lib/postgresql/data
|
|
540
686
|
networks: [envsync]
|
|
@@ -543,18 +689,22 @@ services:
|
|
|
543
689
|
image: openfga/openfga:v1.12.0
|
|
544
690
|
command: run
|
|
545
691
|
environment:
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
692
|
+
${renderEnvList({
|
|
693
|
+
OPENFGA_DATASTORE_ENGINE: "postgres",
|
|
694
|
+
OPENFGA_DATASTORE_URI: `postgres://openfga:${runtimeEnv.OPENFGA_DB_PASSWORD}@openfga_db:5432/openfga?sslmode=disable`,
|
|
695
|
+
OPENFGA_HTTP_ADDR: "0.0.0.0:8090",
|
|
696
|
+
OPENFGA_GRPC_ADDR: "0.0.0.0:8091"
|
|
697
|
+
})}
|
|
550
698
|
networks: [envsync]
|
|
551
699
|
|
|
552
700
|
minikms_db:
|
|
553
701
|
image: postgres:17
|
|
554
702
|
environment:
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
703
|
+
${renderEnvList({
|
|
704
|
+
POSTGRES_USER: "postgres",
|
|
705
|
+
POSTGRES_PASSWORD: runtimeEnv.MINIKMS_DB_PASSWORD,
|
|
706
|
+
POSTGRES_DB: "minikms"
|
|
707
|
+
})}
|
|
558
708
|
volumes:
|
|
559
709
|
- minikms_db_data:/var/lib/postgresql/data
|
|
560
710
|
networks: [envsync]
|
|
@@ -562,10 +712,13 @@ services:
|
|
|
562
712
|
minikms:
|
|
563
713
|
image: ghcr.io/envsync-cloud/minikms:sha-735dfe8
|
|
564
714
|
environment:
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
715
|
+
${renderEnvList({
|
|
716
|
+
MINIKMS_ROOT_KEY: runtimeEnv.MINIKMS_ROOT_KEY,
|
|
717
|
+
MINIKMS_DB_URL: `postgres://postgres:${runtimeEnv.MINIKMS_DB_PASSWORD}@minikms_db:5432/minikms?sslmode=disable`,
|
|
718
|
+
MINIKMS_REDIS_URL: "redis://redis:6379",
|
|
719
|
+
MINIKMS_GRPC_ADDR: "0.0.0.0:50051",
|
|
720
|
+
MINIKMS_TLS_ENABLED: "false"
|
|
721
|
+
})}
|
|
569
722
|
networks: [envsync]
|
|
570
723
|
|
|
571
724
|
clickstack:
|
|
@@ -589,6 +742,7 @@ services:
|
|
|
589
742
|
volumes:
|
|
590
743
|
- ${OTEL_AGENT_CONF}:/etc/otel-agent.yaml:ro
|
|
591
744
|
networks: [envsync]
|
|
745
|
+
${includeAppServices ? `
|
|
592
746
|
|
|
593
747
|
landing_nginx:
|
|
594
748
|
image: nginx:1.27-alpine
|
|
@@ -607,14 +761,14 @@ services:
|
|
|
607
761
|
envsync_api_blue:
|
|
608
762
|
image: ${config.images.api}
|
|
609
763
|
environment:
|
|
610
|
-
${
|
|
764
|
+
${renderEnvList(apiEnvironment)}
|
|
611
765
|
networks: [envsync]
|
|
612
766
|
|
|
613
767
|
envsync_api_green:
|
|
614
768
|
image: ${config.images.api}
|
|
615
769
|
environment:
|
|
616
|
-
${
|
|
617
|
-
networks: [envsync]
|
|
770
|
+
${renderEnvList(apiEnvironment)}
|
|
771
|
+
networks: [envsync]` : ""}
|
|
618
772
|
|
|
619
773
|
networks:
|
|
620
774
|
envsync:
|
|
@@ -633,6 +787,166 @@ volumes:
|
|
|
633
787
|
clickstack_ch_logs:
|
|
634
788
|
`.trimStart();
|
|
635
789
|
}
|
|
790
|
+
function writeDeployArtifacts(config, generated) {
|
|
791
|
+
const runtimeEnv = buildRuntimeEnv(config, generated);
|
|
792
|
+
writeFile(DEPLOY_ENV, renderEnvFile(runtimeEnv), 384);
|
|
793
|
+
writeFile(
|
|
794
|
+
INTERNAL_CONFIG_JSON,
|
|
795
|
+
JSON.stringify({ config, generated: mergeGeneratedState(runtimeEnv, generated) }, null, 2) + "\n"
|
|
796
|
+
);
|
|
797
|
+
writeFile(VERSIONS_LOCK, JSON.stringify(config.images, null, 2) + "\n");
|
|
798
|
+
writeFile(KEYCLOAK_REALM_FILE, renderKeycloakRealm(config, runtimeEnv));
|
|
799
|
+
writeFile(TRAEFIK_DYNAMIC_FILE, renderTraefikDynamicConfig(config));
|
|
800
|
+
writeFile(STACK_FILE, renderStack(config, runtimeEnv, true));
|
|
801
|
+
writeFile(BOOTSTRAP_STACK_FILE, renderStack(config, runtimeEnv, false));
|
|
802
|
+
writeFile(NGINX_WEB_CONF, renderNginxConf("web"));
|
|
803
|
+
writeFile(NGINX_LANDING_CONF, renderNginxConf("landing"));
|
|
804
|
+
writeFile(OTEL_AGENT_CONF, renderOtelAgentConfig(config));
|
|
805
|
+
}
|
|
806
|
+
function saveDesiredConfig(config) {
|
|
807
|
+
const internal = readInternalState();
|
|
808
|
+
const generated = mergeGeneratedState(loadGeneratedEnv(), internal?.generated);
|
|
809
|
+
writeFile(DEPLOY_YAML, toYaml(config) + "\n");
|
|
810
|
+
writeFile(
|
|
811
|
+
INTERNAL_CONFIG_JSON,
|
|
812
|
+
JSON.stringify({ config, generated }, null, 2) + "\n"
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
function ensureRepoCheckout(config) {
|
|
816
|
+
ensureDir(REPO_ROOT);
|
|
817
|
+
if (!exists(path.join(REPO_ROOT, ".git"))) {
|
|
818
|
+
run("git", ["clone", config.source.repo_url, REPO_ROOT]);
|
|
819
|
+
}
|
|
820
|
+
run("git", ["remote", "set-url", "origin", config.source.repo_url], { cwd: REPO_ROOT });
|
|
821
|
+
run("git", ["fetch", "--tags", "--force", "origin"], { cwd: REPO_ROOT });
|
|
822
|
+
run("git", ["checkout", "--force", config.source.ref], { cwd: REPO_ROOT });
|
|
823
|
+
}
|
|
824
|
+
function extractStaticBundle(image, targetDir) {
|
|
825
|
+
ensureDir(targetDir);
|
|
826
|
+
const containerId = run("docker", ["create", image], { quiet: true }).trim();
|
|
827
|
+
try {
|
|
828
|
+
run("docker", ["cp", `${containerId}:/app/dist/.`, targetDir]);
|
|
829
|
+
} finally {
|
|
830
|
+
run("docker", ["rm", "-f", containerId], { quiet: true });
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
function buildKeycloakImage(imageTag, repoRoot = REPO_ROOT) {
|
|
834
|
+
const buildContext = path.join(repoRoot, "packages/envsync-keycloak-theme");
|
|
835
|
+
if (!exists(path.join(buildContext, "Dockerfile"))) {
|
|
836
|
+
throw new Error(`Missing Keycloak Docker build context at ${buildContext}`);
|
|
837
|
+
}
|
|
838
|
+
run("docker", ["build", "-t", imageTag, buildContext]);
|
|
839
|
+
}
|
|
840
|
+
function stackNetworkName(config) {
|
|
841
|
+
return `${config.services.stack_name}_envsync`;
|
|
842
|
+
}
|
|
843
|
+
function waitForTcpService(config, label, host, port, timeoutSeconds = 120) {
|
|
844
|
+
const deadline = Date.now() + timeoutSeconds * 1e3;
|
|
845
|
+
while (Date.now() < deadline) {
|
|
846
|
+
if (commandSucceeds(
|
|
847
|
+
"docker",
|
|
848
|
+
["run", "--rm", "--network", stackNetworkName(config), "alpine:3.20", "sh", "-lc", `nc -z -w 2 ${host} ${port}`]
|
|
849
|
+
)) {
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
sleepSeconds(2);
|
|
853
|
+
}
|
|
854
|
+
throw new Error(`Timed out waiting for ${label} at ${host}:${port}`);
|
|
855
|
+
}
|
|
856
|
+
function runBootstrapInit(config) {
|
|
857
|
+
const output = run(
|
|
858
|
+
"docker",
|
|
859
|
+
[
|
|
860
|
+
"run",
|
|
861
|
+
"--rm",
|
|
862
|
+
"--network",
|
|
863
|
+
stackNetworkName(config),
|
|
864
|
+
"--env-file",
|
|
865
|
+
DEPLOY_ENV,
|
|
866
|
+
"-e",
|
|
867
|
+
"SKIP_ROOT_ENV=1",
|
|
868
|
+
"-e",
|
|
869
|
+
"SKIP_ROOT_ENV_WRITE=1",
|
|
870
|
+
config.images.api,
|
|
871
|
+
"bun",
|
|
872
|
+
"run",
|
|
873
|
+
"scripts/prod-init.ts",
|
|
874
|
+
"--json",
|
|
875
|
+
"--no-write-root-env"
|
|
876
|
+
],
|
|
877
|
+
{ quiet: true }
|
|
878
|
+
).trim();
|
|
879
|
+
const result = JSON.parse(output);
|
|
880
|
+
if (!result.openfgaStoreId || !result.openfgaModelId) {
|
|
881
|
+
throw new Error("Bootstrap init did not return OpenFGA IDs");
|
|
882
|
+
}
|
|
883
|
+
return {
|
|
884
|
+
openfgaStoreId: result.openfgaStoreId,
|
|
885
|
+
openfgaModelId: result.openfgaModelId
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
function hasCompleteBootstrapState(generated) {
|
|
889
|
+
return REQUIRED_BOOTSTRAP_ENV_KEYS.every((key) => {
|
|
890
|
+
switch (key) {
|
|
891
|
+
case "S3_SECRET_KEY":
|
|
892
|
+
return generated.secrets.s3_secret_key.length > 0;
|
|
893
|
+
case "KEYCLOAK_WEB_CLIENT_SECRET":
|
|
894
|
+
return generated.secrets.keycloak_web_client_secret.length > 0;
|
|
895
|
+
case "KEYCLOAK_API_CLIENT_SECRET":
|
|
896
|
+
return generated.secrets.keycloak_api_client_secret.length > 0;
|
|
897
|
+
case "OPENFGA_DB_PASSWORD":
|
|
898
|
+
return generated.secrets.openfga_db_password.length > 0;
|
|
899
|
+
case "MINIKMS_ROOT_KEY":
|
|
900
|
+
return generated.secrets.minikms_root_key.length > 0;
|
|
901
|
+
case "MINIKMS_DB_PASSWORD":
|
|
902
|
+
return generated.secrets.minikms_db_password.length > 0;
|
|
903
|
+
case "OPENFGA_STORE_ID":
|
|
904
|
+
return generated.openfga.store_id.length > 0;
|
|
905
|
+
case "OPENFGA_MODEL_ID":
|
|
906
|
+
return generated.openfga.model_id.length > 0;
|
|
907
|
+
default:
|
|
908
|
+
return false;
|
|
909
|
+
}
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
function assertBootstrapState(generated) {
|
|
913
|
+
if (!hasCompleteBootstrapState(generated)) {
|
|
914
|
+
throw new Error("Missing bootstrap state. Run 'envsync-deploy bootstrap' first.");
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
function parseReplicaHealth(raw) {
|
|
918
|
+
const match = raw.match(/^(\d+)\/(\d+)$/);
|
|
919
|
+
if (!match) return raw.trim() ? "degraded" : "missing";
|
|
920
|
+
const current = Number(match[1]);
|
|
921
|
+
const desired = Number(match[2]);
|
|
922
|
+
if (desired === 0) return "missing";
|
|
923
|
+
if (current === desired) return "healthy";
|
|
924
|
+
return "degraded";
|
|
925
|
+
}
|
|
926
|
+
function listStackServices(config) {
|
|
927
|
+
const output = tryRun(
|
|
928
|
+
"docker",
|
|
929
|
+
["stack", "services", config.services.stack_name, "--format", "{{.Name}}|{{.Replicas}}"],
|
|
930
|
+
{ quiet: true }
|
|
931
|
+
);
|
|
932
|
+
const services = /* @__PURE__ */ new Map();
|
|
933
|
+
for (const line of output.split(/\r?\n/)) {
|
|
934
|
+
if (!line.trim()) continue;
|
|
935
|
+
const [name, replicas] = line.split("|");
|
|
936
|
+
services.set(name, parseReplicaHealth(replicas ?? ""));
|
|
937
|
+
}
|
|
938
|
+
return services;
|
|
939
|
+
}
|
|
940
|
+
function serviceHealth(services, name) {
|
|
941
|
+
return services.get(`${name}`) ?? "missing";
|
|
942
|
+
}
|
|
943
|
+
function apiHealth(services, stackName) {
|
|
944
|
+
const blue = serviceHealth(services, `${stackName}_envsync_api_blue`);
|
|
945
|
+
const green = serviceHealth(services, `${stackName}_envsync_api_green`);
|
|
946
|
+
if (blue === "missing" && green === "missing") return "missing";
|
|
947
|
+
if (blue === "healthy" || green === "healthy") return "healthy";
|
|
948
|
+
return "degraded";
|
|
949
|
+
}
|
|
636
950
|
async function cmdPreinstall() {
|
|
637
951
|
ensureDir(HOST_ROOT);
|
|
638
952
|
ensureDir(DEPLOY_ROOT);
|
|
@@ -669,6 +983,7 @@ async function cmdSetup() {
|
|
|
669
983
|
const publicObs = await ask("Expose obs.<domain> publicly (true/false)", "true") === "true";
|
|
670
984
|
const mailpitEnabled = await ask("Enable mailpit (true/false)", "false") === "true";
|
|
671
985
|
const config = {
|
|
986
|
+
source: defaultSourceConfig(),
|
|
672
987
|
domain: { root_domain: rootDomain, acme_email: acmeEmail },
|
|
673
988
|
images: {
|
|
674
989
|
api: `ghcr.io/envsync-cloud/envsync-api:${channel}`,
|
|
@@ -722,33 +1037,44 @@ async function cmdSetup() {
|
|
|
722
1037
|
},
|
|
723
1038
|
release_channel: channel
|
|
724
1039
|
};
|
|
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);
|
|
1040
|
+
saveDesiredConfig(config);
|
|
730
1041
|
console.log(`Config written to ${DEPLOY_YAML}`);
|
|
1042
|
+
console.log(`Pinned source checkout: ${config.source.repo_url} @ ${config.source.ref}`);
|
|
731
1043
|
console.log("Create these DNS records:");
|
|
732
1044
|
console.log(JSON.stringify(domainMap(rootDomain), null, 2));
|
|
733
1045
|
}
|
|
734
|
-
function
|
|
735
|
-
|
|
736
|
-
const
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
1046
|
+
async function cmdBootstrap() {
|
|
1047
|
+
const { config, generated } = loadState();
|
|
1048
|
+
const nextGenerated = ensureGeneratedRuntimeState(generated);
|
|
1049
|
+
ensureRepoCheckout(config);
|
|
1050
|
+
writeDeployArtifacts(config, nextGenerated);
|
|
1051
|
+
buildKeycloakImage(config.images.keycloak);
|
|
1052
|
+
run("docker", ["stack", "deploy", "-c", BOOTSTRAP_STACK_FILE, config.services.stack_name]);
|
|
1053
|
+
waitForTcpService(config, "postgres", "postgres", 5432);
|
|
1054
|
+
waitForTcpService(config, "redis", "redis", 6379);
|
|
1055
|
+
waitForTcpService(config, "rustfs", "rustfs", 9e3);
|
|
1056
|
+
waitForTcpService(config, "keycloak", "keycloak", 8080);
|
|
1057
|
+
waitForTcpService(config, "openfga", "openfga", 8090);
|
|
1058
|
+
waitForTcpService(config, "minikms", "minikms", 50051);
|
|
1059
|
+
const initResult = runBootstrapInit(config);
|
|
1060
|
+
const bootstrappedGenerated = normalizeGeneratedState({
|
|
1061
|
+
openfga: {
|
|
1062
|
+
store_id: initResult.openfgaStoreId,
|
|
1063
|
+
model_id: initResult.openfgaModelId
|
|
1064
|
+
},
|
|
1065
|
+
secrets: nextGenerated.secrets,
|
|
1066
|
+
bootstrap: {
|
|
1067
|
+
completed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1068
|
+
}
|
|
1069
|
+
});
|
|
1070
|
+
writeDeployArtifacts(config, bootstrappedGenerated);
|
|
1071
|
+
console.log("Bootstrap completed.");
|
|
749
1072
|
}
|
|
750
1073
|
async function cmdDeploy() {
|
|
751
|
-
const config =
|
|
1074
|
+
const { config, generated } = loadState();
|
|
1075
|
+
assertBootstrapState(generated);
|
|
1076
|
+
ensureRepoCheckout(config);
|
|
1077
|
+
writeDeployArtifacts(config, generated);
|
|
752
1078
|
buildKeycloakImage(config.images.keycloak);
|
|
753
1079
|
ensureDir(`${RELEASES_ROOT}/web/current`);
|
|
754
1080
|
ensureDir(`${RELEASES_ROOT}/landing/current`);
|
|
@@ -759,12 +1085,29 @@ async function cmdDeploy() {
|
|
|
759
1085
|
run("docker", ["stack", "deploy", "-c", STACK_FILE, config.services.stack_name]);
|
|
760
1086
|
}
|
|
761
1087
|
async function cmdHealth(asJson) {
|
|
762
|
-
const config =
|
|
1088
|
+
const { config, generated } = loadState();
|
|
763
1089
|
const hosts = domainMap(config.domain.root_domain);
|
|
764
|
-
const services =
|
|
1090
|
+
const services = listStackServices(config);
|
|
1091
|
+
const stackName = config.services.stack_name;
|
|
1092
|
+
const bootstrapServices = {
|
|
1093
|
+
postgres: serviceHealth(services, `${stackName}_postgres`),
|
|
1094
|
+
redis: serviceHealth(services, `${stackName}_redis`),
|
|
1095
|
+
rustfs: serviceHealth(services, `${stackName}_rustfs`),
|
|
1096
|
+
keycloak: serviceHealth(services, `${stackName}_keycloak`),
|
|
1097
|
+
openfga: serviceHealth(services, `${stackName}_openfga`),
|
|
1098
|
+
minikms: serviceHealth(services, `${stackName}_minikms`)
|
|
1099
|
+
};
|
|
765
1100
|
const checks = {
|
|
766
|
-
|
|
767
|
-
|
|
1101
|
+
bootstrap: {
|
|
1102
|
+
completed: hasCompleteBootstrapState(generated) && generated.bootstrap.completed_at.length > 0,
|
|
1103
|
+
completed_at: generated.bootstrap.completed_at || null,
|
|
1104
|
+
services: bootstrapServices
|
|
1105
|
+
},
|
|
1106
|
+
deploy: {
|
|
1107
|
+
api: apiHealth(services, stackName),
|
|
1108
|
+
web: serviceHealth(services, `${stackName}_web_nginx`),
|
|
1109
|
+
landing: serviceHealth(services, `${stackName}_landing_nginx`)
|
|
1110
|
+
},
|
|
768
1111
|
public: {
|
|
769
1112
|
landing: `https://${hosts.landing}`,
|
|
770
1113
|
app: `https://${hosts.app}`,
|
|
@@ -773,25 +1116,24 @@ async function cmdHealth(asJson) {
|
|
|
773
1116
|
obs: `https://${hosts.obs}`
|
|
774
1117
|
}
|
|
775
1118
|
};
|
|
776
|
-
if (asJson)
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
console.log(JSON.stringify(checks.public, null, 2));
|
|
1119
|
+
if (asJson) {
|
|
1120
|
+
console.log(JSON.stringify(checks, null, 2));
|
|
1121
|
+
return;
|
|
780
1122
|
}
|
|
1123
|
+
console.log(JSON.stringify(checks, null, 2));
|
|
781
1124
|
}
|
|
782
1125
|
async function cmdUpgrade() {
|
|
783
|
-
const config =
|
|
784
|
-
|
|
785
|
-
config
|
|
786
|
-
saveConfig(config);
|
|
1126
|
+
const { config } = loadState();
|
|
1127
|
+
config.images.api = `ghcr.io/envsync-cloud/envsync-api:${config.release_channel}`;
|
|
1128
|
+
saveDesiredConfig(config);
|
|
787
1129
|
await cmdDeploy();
|
|
788
1130
|
}
|
|
789
1131
|
async function cmdUpgradeDeps() {
|
|
790
|
-
const config =
|
|
1132
|
+
const { config } = loadState();
|
|
791
1133
|
config.images.traefik = "traefik:v3.1";
|
|
792
1134
|
config.images.clickstack = "clickhouse/clickstack-all-in-one:latest";
|
|
793
1135
|
config.images.otel_agent = "otel/opentelemetry-collector-contrib:0.111.0";
|
|
794
|
-
|
|
1136
|
+
saveDesiredConfig(config);
|
|
795
1137
|
await cmdDeploy();
|
|
796
1138
|
}
|
|
797
1139
|
function sha256File(filePath) {
|
|
@@ -833,7 +1175,7 @@ function restoreDockerVolume(volumeName, sourceDir) {
|
|
|
833
1175
|
]);
|
|
834
1176
|
}
|
|
835
1177
|
async function cmdBackup() {
|
|
836
|
-
const config =
|
|
1178
|
+
const { config } = loadState();
|
|
837
1179
|
ensureDir(config.backup.output_dir);
|
|
838
1180
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:]/g, "-");
|
|
839
1181
|
const archiveBase = path.join(config.backup.output_dir, `envsync-backup-${timestamp}`);
|
|
@@ -845,14 +1187,14 @@ async function cmdBackup() {
|
|
|
845
1187
|
writeFile(path.join(staged, "deploy.yaml"), fs.readFileSync(DEPLOY_YAML, "utf8"));
|
|
846
1188
|
writeFile(path.join(staged, "config.json"), fs.readFileSync(INTERNAL_CONFIG_JSON, "utf8"));
|
|
847
1189
|
writeFile(path.join(staged, "versions.lock.json"), fs.readFileSync(VERSIONS_LOCK, "utf8"));
|
|
1190
|
+
writeFile(path.join(staged, "docker-stack.bootstrap.yaml"), fs.readFileSync(BOOTSTRAP_STACK_FILE, "utf8"));
|
|
848
1191
|
writeFile(path.join(staged, "docker-stack.yaml"), fs.readFileSync(STACK_FILE, "utf8"));
|
|
849
1192
|
writeFile(path.join(staged, "traefik-dynamic.yaml"), fs.readFileSync(TRAEFIK_DYNAMIC_FILE, "utf8"));
|
|
850
1193
|
writeFile(path.join(staged, "keycloak-realm.envsync.json"), fs.readFileSync(KEYCLOAK_REALM_FILE, "utf8"));
|
|
851
1194
|
writeFile(path.join(staged, "otel-agent.yaml"), fs.readFileSync(OTEL_AGENT_CONF, "utf8"));
|
|
852
1195
|
const volumesDir = path.join(staged, "volumes");
|
|
853
1196
|
for (const volume of STACK_VOLUMES) {
|
|
854
|
-
|
|
855
|
-
backupDockerVolume(stackVolumeName(config, volume), target);
|
|
1197
|
+
backupDockerVolume(stackVolumeName(config, volume), path.join(volumesDir, volume));
|
|
856
1198
|
}
|
|
857
1199
|
run("bash", ["-lc", `tar -czf ${JSON.stringify(tarPath)} -C ${JSON.stringify(staged)} .`]);
|
|
858
1200
|
const manifest = {
|
|
@@ -867,7 +1209,6 @@ async function cmdBackup() {
|
|
|
867
1209
|
}
|
|
868
1210
|
async function cmdRestore(archivePath) {
|
|
869
1211
|
if (!archivePath) throw new Error("restore requires a .tar.gz path");
|
|
870
|
-
const config = loadConfig();
|
|
871
1212
|
const restoreRoot = path.join(BACKUPS_ROOT, `restore-${Date.now()}`);
|
|
872
1213
|
ensureDir(restoreRoot);
|
|
873
1214
|
run("bash", ["-lc", `tar -xzf ${JSON.stringify(archivePath)} -C ${JSON.stringify(restoreRoot)}`]);
|
|
@@ -875,14 +1216,16 @@ async function cmdRestore(archivePath) {
|
|
|
875
1216
|
writeFile(DEPLOY_YAML, fs.readFileSync(path.join(restoreRoot, "deploy.yaml"), "utf8"));
|
|
876
1217
|
writeFile(INTERNAL_CONFIG_JSON, fs.readFileSync(path.join(restoreRoot, "config.json"), "utf8"));
|
|
877
1218
|
writeFile(VERSIONS_LOCK, fs.readFileSync(path.join(restoreRoot, "versions.lock.json"), "utf8"));
|
|
1219
|
+
writeFile(BOOTSTRAP_STACK_FILE, fs.readFileSync(path.join(restoreRoot, "docker-stack.bootstrap.yaml"), "utf8"));
|
|
878
1220
|
writeFile(STACK_FILE, fs.readFileSync(path.join(restoreRoot, "docker-stack.yaml"), "utf8"));
|
|
879
1221
|
writeFile(TRAEFIK_DYNAMIC_FILE, fs.readFileSync(path.join(restoreRoot, "traefik-dynamic.yaml"), "utf8"));
|
|
880
1222
|
writeFile(KEYCLOAK_REALM_FILE, fs.readFileSync(path.join(restoreRoot, "keycloak-realm.envsync.json"), "utf8"));
|
|
881
1223
|
writeFile(OTEL_AGENT_CONF, fs.readFileSync(path.join(restoreRoot, "otel-agent.yaml"), "utf8"));
|
|
1224
|
+
const config = loadConfig();
|
|
882
1225
|
for (const volume of STACK_VOLUMES) {
|
|
883
1226
|
restoreDockerVolume(stackVolumeName(config, volume), path.join(restoreRoot, "volumes", volume));
|
|
884
1227
|
}
|
|
885
|
-
|
|
1228
|
+
console.log("Restore completed. Run 'envsync-deploy deploy' to start services.");
|
|
886
1229
|
}
|
|
887
1230
|
async function main() {
|
|
888
1231
|
const command = process.argv[2];
|
|
@@ -894,6 +1237,9 @@ async function main() {
|
|
|
894
1237
|
case "setup":
|
|
895
1238
|
await cmdSetup();
|
|
896
1239
|
break;
|
|
1240
|
+
case "bootstrap":
|
|
1241
|
+
await cmdBootstrap();
|
|
1242
|
+
break;
|
|
897
1243
|
case "deploy":
|
|
898
1244
|
await cmdDeploy();
|
|
899
1245
|
break;
|
|
@@ -913,7 +1259,7 @@ async function main() {
|
|
|
913
1259
|
await cmdRestore(flag ?? "");
|
|
914
1260
|
break;
|
|
915
1261
|
default:
|
|
916
|
-
console.log("Usage: envsync-deploy <preinstall|setup|deploy|health|upgrade|upgrade-deps|backup|restore>");
|
|
1262
|
+
console.log("Usage: envsync-deploy <preinstall|setup|bootstrap|deploy|health|upgrade|upgrade-deps|backup|restore>");
|
|
917
1263
|
process.exit(command ? 1 : 0);
|
|
918
1264
|
}
|
|
919
1265
|
}
|