@envsync-cloud/deploy-cli 0.6.0 → 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 +495 -128
- 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,11 +394,18 @@ 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
|
-
redirectUris: [
|
|
400
|
+
redirectUris: [
|
|
401
|
+
`https://${hosts.api}/api/access/web/callback`,
|
|
402
|
+
`https://${hosts.app}/auth/callback`,
|
|
403
|
+
`https://${hosts.app}`
|
|
404
|
+
],
|
|
261
405
|
webOrigins: [`https://${hosts.app}`],
|
|
406
|
+
attributes: {
|
|
407
|
+
"post.logout.redirect.uris": "+"
|
|
408
|
+
},
|
|
262
409
|
defaultClientScopes: ["basic", "web-origins", "profile", "email", "roles"]
|
|
263
410
|
},
|
|
264
411
|
{
|
|
@@ -266,7 +413,7 @@ function renderKeycloakRealm(config) {
|
|
|
266
413
|
name: "EnvSync API",
|
|
267
414
|
protocol: "openid-connect",
|
|
268
415
|
publicClient: false,
|
|
269
|
-
secret:
|
|
416
|
+
secret: runtimeEnv.KEYCLOAK_API_CLIENT_SECRET,
|
|
270
417
|
standardFlowEnabled: true,
|
|
271
418
|
redirectUris: [`https://${hosts.api}/api/access/api/callback`],
|
|
272
419
|
webOrigins: [`https://${hosts.api}`],
|
|
@@ -290,11 +437,6 @@ function renderKeycloakRealm(config) {
|
|
|
290
437
|
2
|
|
291
438
|
) + "\n";
|
|
292
439
|
}
|
|
293
|
-
function extractEnvValue(key) {
|
|
294
|
-
const env = exists(DEPLOY_ENV) ? fs.readFileSync(DEPLOY_ENV, "utf8") : "";
|
|
295
|
-
const line = env.split(/\r?\n/).find((entry) => entry.startsWith(`${key}=`));
|
|
296
|
-
return line?.slice(key.length + 1) ?? "";
|
|
297
|
-
}
|
|
298
440
|
function renderTraefikDynamicConfig(config) {
|
|
299
441
|
const hosts = domainMap(config.domain.root_domain);
|
|
300
442
|
return [
|
|
@@ -333,17 +475,17 @@ function renderTraefikDynamicConfig(config) {
|
|
|
333
475
|
" servers:",
|
|
334
476
|
" - url: http://web_nginx:8080",
|
|
335
477
|
" routers:",
|
|
336
|
-
|
|
478
|
+
" landing-router:",
|
|
337
479
|
` rule: Host(\`${hosts.landing}\`)`,
|
|
338
480
|
" service: landing",
|
|
339
481
|
" entryPoints: [websecure]",
|
|
340
482
|
" tls: {}",
|
|
341
|
-
|
|
483
|
+
" web-router:",
|
|
342
484
|
` rule: Host(\`${hosts.app}\`)`,
|
|
343
485
|
" service: web",
|
|
344
486
|
" entryPoints: [websecure]",
|
|
345
487
|
" tls: {}",
|
|
346
|
-
|
|
488
|
+
" api-router:",
|
|
347
489
|
` rule: Host(\`${hosts.api}\`)`,
|
|
348
490
|
" service: envsync-api",
|
|
349
491
|
" entryPoints: [websecure]",
|
|
@@ -363,6 +505,18 @@ function renderNginxConf(kind) {
|
|
|
363
505
|
"}"
|
|
364
506
|
].join("\n") + "\n";
|
|
365
507
|
}
|
|
508
|
+
function renderFrontendRuntimeConfig(config) {
|
|
509
|
+
const hosts = domainMap(config.domain.root_domain);
|
|
510
|
+
return `window.__ENVSYNC_RUNTIME_CONFIG__ = ${JSON.stringify({
|
|
511
|
+
apiBaseUrl: `https://${hosts.api}`,
|
|
512
|
+
appBaseUrl: `https://${hosts.app}`,
|
|
513
|
+
authBaseUrl: `https://${hosts.auth}`,
|
|
514
|
+
keycloakRealm: config.auth.keycloak_realm,
|
|
515
|
+
webClientId: config.auth.web_client_id,
|
|
516
|
+
apiDocsUrl: `https://${hosts.api}/docs`
|
|
517
|
+
}, null, 2)};
|
|
518
|
+
`;
|
|
519
|
+
}
|
|
366
520
|
function renderOtelAgentConfig(config) {
|
|
367
521
|
return [
|
|
368
522
|
"receivers:",
|
|
@@ -396,16 +550,17 @@ function renderOtelAgentConfig(config) {
|
|
|
396
550
|
" exporters: [otlphttp/clickstack]"
|
|
397
551
|
].join("\n") + "\n";
|
|
398
552
|
}
|
|
399
|
-
function renderStack(config) {
|
|
553
|
+
function renderStack(config, runtimeEnv, includeAppServices) {
|
|
400
554
|
const hosts = domainMap(config.domain.root_domain);
|
|
401
|
-
const apiEnvironment =
|
|
555
|
+
const apiEnvironment = {
|
|
556
|
+
...runtimeEnv,
|
|
402
557
|
OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-agent:4318",
|
|
403
558
|
KEYCLOAK_URL: "http://keycloak:8080",
|
|
404
559
|
OPENFGA_API_URL: "http://openfga:8090",
|
|
405
560
|
MINIKMS_GRPC_ADDR: "minikms:50051",
|
|
406
561
|
S3_ENDPOINT: "http://rustfs:9000",
|
|
407
562
|
S3_BUCKET_URL: `https://${hosts.s3}`
|
|
408
|
-
}
|
|
563
|
+
};
|
|
409
564
|
return `
|
|
410
565
|
version: "3.9"
|
|
411
566
|
services:
|
|
@@ -439,9 +594,11 @@ services:
|
|
|
439
594
|
postgres:
|
|
440
595
|
image: postgres:17
|
|
441
596
|
environment:
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
597
|
+
${renderEnvList({
|
|
598
|
+
POSTGRES_USER: "postgres",
|
|
599
|
+
POSTGRES_PASSWORD: "envsync-postgres",
|
|
600
|
+
POSTGRES_DB: "envsync"
|
|
601
|
+
})}
|
|
445
602
|
volumes:
|
|
446
603
|
- postgres_data:/var/lib/postgresql/data
|
|
447
604
|
networks: [envsync]
|
|
@@ -455,10 +612,12 @@ services:
|
|
|
455
612
|
rustfs:
|
|
456
613
|
image: rustfs/rustfs:latest
|
|
457
614
|
environment:
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
+
})}
|
|
462
621
|
volumes:
|
|
463
622
|
- rustfs_data:/data
|
|
464
623
|
networks: [envsync]
|
|
@@ -477,9 +636,11 @@ services:
|
|
|
477
636
|
keycloak_db:
|
|
478
637
|
image: postgres:17
|
|
479
638
|
environment:
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
639
|
+
${renderEnvList({
|
|
640
|
+
POSTGRES_USER: "keycloak",
|
|
641
|
+
POSTGRES_PASSWORD: runtimeEnv.KEYCLOAK_ADMIN_PASSWORD,
|
|
642
|
+
POSTGRES_DB: "keycloak"
|
|
643
|
+
})}
|
|
483
644
|
volumes:
|
|
484
645
|
- keycloak_db_data:/var/lib/postgresql/data
|
|
485
646
|
networks: [envsync]
|
|
@@ -490,17 +651,19 @@ services:
|
|
|
490
651
|
command:
|
|
491
652
|
- /opt/keycloak/bin/kc.sh import --dir /opt/keycloak/data/import --override true && exec /opt/keycloak/bin/kc.sh start-dev
|
|
492
653
|
environment:
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
+
})}
|
|
502
665
|
volumes:
|
|
503
|
-
- ${
|
|
666
|
+
- ${KEYCLOAK_REALM_FILE}:/opt/keycloak/data/import/realm.json:ro
|
|
504
667
|
networks: [envsync]
|
|
505
668
|
deploy:
|
|
506
669
|
labels:
|
|
@@ -513,9 +676,11 @@ services:
|
|
|
513
676
|
openfga_db:
|
|
514
677
|
image: postgres:17
|
|
515
678
|
environment:
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
679
|
+
${renderEnvList({
|
|
680
|
+
POSTGRES_USER: "openfga",
|
|
681
|
+
POSTGRES_PASSWORD: runtimeEnv.OPENFGA_DB_PASSWORD,
|
|
682
|
+
POSTGRES_DB: "openfga"
|
|
683
|
+
})}
|
|
519
684
|
volumes:
|
|
520
685
|
- openfga_db_data:/var/lib/postgresql/data
|
|
521
686
|
networks: [envsync]
|
|
@@ -524,18 +689,22 @@ services:
|
|
|
524
689
|
image: openfga/openfga:v1.12.0
|
|
525
690
|
command: run
|
|
526
691
|
environment:
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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
|
+
})}
|
|
531
698
|
networks: [envsync]
|
|
532
699
|
|
|
533
700
|
minikms_db:
|
|
534
701
|
image: postgres:17
|
|
535
702
|
environment:
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
703
|
+
${renderEnvList({
|
|
704
|
+
POSTGRES_USER: "postgres",
|
|
705
|
+
POSTGRES_PASSWORD: runtimeEnv.MINIKMS_DB_PASSWORD,
|
|
706
|
+
POSTGRES_DB: "minikms"
|
|
707
|
+
})}
|
|
539
708
|
volumes:
|
|
540
709
|
- minikms_db_data:/var/lib/postgresql/data
|
|
541
710
|
networks: [envsync]
|
|
@@ -543,10 +712,13 @@ services:
|
|
|
543
712
|
minikms:
|
|
544
713
|
image: ghcr.io/envsync-cloud/minikms:sha-735dfe8
|
|
545
714
|
environment:
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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
|
+
})}
|
|
550
722
|
networks: [envsync]
|
|
551
723
|
|
|
552
724
|
clickstack:
|
|
@@ -570,6 +742,7 @@ services:
|
|
|
570
742
|
volumes:
|
|
571
743
|
- ${OTEL_AGENT_CONF}:/etc/otel-agent.yaml:ro
|
|
572
744
|
networks: [envsync]
|
|
745
|
+
${includeAppServices ? `
|
|
573
746
|
|
|
574
747
|
landing_nginx:
|
|
575
748
|
image: nginx:1.27-alpine
|
|
@@ -588,14 +761,14 @@ services:
|
|
|
588
761
|
envsync_api_blue:
|
|
589
762
|
image: ${config.images.api}
|
|
590
763
|
environment:
|
|
591
|
-
${
|
|
764
|
+
${renderEnvList(apiEnvironment)}
|
|
592
765
|
networks: [envsync]
|
|
593
766
|
|
|
594
767
|
envsync_api_green:
|
|
595
768
|
image: ${config.images.api}
|
|
596
769
|
environment:
|
|
597
|
-
${
|
|
598
|
-
networks: [envsync]
|
|
770
|
+
${renderEnvList(apiEnvironment)}
|
|
771
|
+
networks: [envsync]` : ""}
|
|
599
772
|
|
|
600
773
|
networks:
|
|
601
774
|
envsync:
|
|
@@ -614,6 +787,166 @@ volumes:
|
|
|
614
787
|
clickstack_ch_logs:
|
|
615
788
|
`.trimStart();
|
|
616
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
|
+
}
|
|
617
950
|
async function cmdPreinstall() {
|
|
618
951
|
ensureDir(HOST_ROOT);
|
|
619
952
|
ensureDir(DEPLOY_ROOT);
|
|
@@ -650,6 +983,7 @@ async function cmdSetup() {
|
|
|
650
983
|
const publicObs = await ask("Expose obs.<domain> publicly (true/false)", "true") === "true";
|
|
651
984
|
const mailpitEnabled = await ask("Enable mailpit (true/false)", "false") === "true";
|
|
652
985
|
const config = {
|
|
986
|
+
source: defaultSourceConfig(),
|
|
653
987
|
domain: { root_domain: rootDomain, acme_email: acmeEmail },
|
|
654
988
|
images: {
|
|
655
989
|
api: `ghcr.io/envsync-cloud/envsync-api:${channel}`,
|
|
@@ -703,47 +1037,77 @@ async function cmdSetup() {
|
|
|
703
1037
|
},
|
|
704
1038
|
release_channel: channel
|
|
705
1039
|
};
|
|
706
|
-
|
|
707
|
-
if (!exists(path.join(REPO_ROOT, ".git"))) {
|
|
708
|
-
run("git", ["clone", "https://github.com/EnvSync-Cloud/envsync.git", REPO_ROOT]);
|
|
709
|
-
}
|
|
710
|
-
saveConfig(config);
|
|
1040
|
+
saveDesiredConfig(config);
|
|
711
1041
|
console.log(`Config written to ${DEPLOY_YAML}`);
|
|
1042
|
+
console.log(`Pinned source checkout: ${config.source.repo_url} @ ${config.source.ref}`);
|
|
712
1043
|
console.log("Create these DNS records:");
|
|
713
1044
|
console.log(JSON.stringify(domainMap(rootDomain), null, 2));
|
|
714
1045
|
}
|
|
715
|
-
function
|
|
716
|
-
|
|
717
|
-
const
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
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.");
|
|
730
1072
|
}
|
|
731
1073
|
async function cmdDeploy() {
|
|
732
|
-
const config =
|
|
1074
|
+
const { config, generated } = loadState();
|
|
1075
|
+
assertBootstrapState(generated);
|
|
1076
|
+
ensureRepoCheckout(config);
|
|
1077
|
+
writeDeployArtifacts(config, generated);
|
|
733
1078
|
buildKeycloakImage(config.images.keycloak);
|
|
734
1079
|
ensureDir(`${RELEASES_ROOT}/web/current`);
|
|
735
1080
|
ensureDir(`${RELEASES_ROOT}/landing/current`);
|
|
736
1081
|
extractStaticBundle(config.images.web, `${RELEASES_ROOT}/web/current`);
|
|
737
1082
|
extractStaticBundle(config.images.landing, `${RELEASES_ROOT}/landing/current`);
|
|
1083
|
+
writeFile(`${RELEASES_ROOT}/web/current/runtime-config.js`, renderFrontendRuntimeConfig(config));
|
|
1084
|
+
writeFile(`${RELEASES_ROOT}/landing/current/runtime-config.js`, renderFrontendRuntimeConfig(config));
|
|
738
1085
|
run("docker", ["stack", "deploy", "-c", STACK_FILE, config.services.stack_name]);
|
|
739
1086
|
}
|
|
740
1087
|
async function cmdHealth(asJson) {
|
|
741
|
-
const config =
|
|
1088
|
+
const { config, generated } = loadState();
|
|
742
1089
|
const hosts = domainMap(config.domain.root_domain);
|
|
743
|
-
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
|
+
};
|
|
744
1100
|
const checks = {
|
|
745
|
-
|
|
746
|
-
|
|
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
|
+
},
|
|
747
1111
|
public: {
|
|
748
1112
|
landing: `https://${hosts.landing}`,
|
|
749
1113
|
app: `https://${hosts.app}`,
|
|
@@ -752,25 +1116,24 @@ async function cmdHealth(asJson) {
|
|
|
752
1116
|
obs: `https://${hosts.obs}`
|
|
753
1117
|
}
|
|
754
1118
|
};
|
|
755
|
-
if (asJson)
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
console.log(JSON.stringify(checks.public, null, 2));
|
|
1119
|
+
if (asJson) {
|
|
1120
|
+
console.log(JSON.stringify(checks, null, 2));
|
|
1121
|
+
return;
|
|
759
1122
|
}
|
|
1123
|
+
console.log(JSON.stringify(checks, null, 2));
|
|
760
1124
|
}
|
|
761
1125
|
async function cmdUpgrade() {
|
|
762
|
-
const config =
|
|
763
|
-
|
|
764
|
-
config
|
|
765
|
-
saveConfig(config);
|
|
1126
|
+
const { config } = loadState();
|
|
1127
|
+
config.images.api = `ghcr.io/envsync-cloud/envsync-api:${config.release_channel}`;
|
|
1128
|
+
saveDesiredConfig(config);
|
|
766
1129
|
await cmdDeploy();
|
|
767
1130
|
}
|
|
768
1131
|
async function cmdUpgradeDeps() {
|
|
769
|
-
const config =
|
|
1132
|
+
const { config } = loadState();
|
|
770
1133
|
config.images.traefik = "traefik:v3.1";
|
|
771
1134
|
config.images.clickstack = "clickhouse/clickstack-all-in-one:latest";
|
|
772
1135
|
config.images.otel_agent = "otel/opentelemetry-collector-contrib:0.111.0";
|
|
773
|
-
|
|
1136
|
+
saveDesiredConfig(config);
|
|
774
1137
|
await cmdDeploy();
|
|
775
1138
|
}
|
|
776
1139
|
function sha256File(filePath) {
|
|
@@ -812,7 +1175,7 @@ function restoreDockerVolume(volumeName, sourceDir) {
|
|
|
812
1175
|
]);
|
|
813
1176
|
}
|
|
814
1177
|
async function cmdBackup() {
|
|
815
|
-
const config =
|
|
1178
|
+
const { config } = loadState();
|
|
816
1179
|
ensureDir(config.backup.output_dir);
|
|
817
1180
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:]/g, "-");
|
|
818
1181
|
const archiveBase = path.join(config.backup.output_dir, `envsync-backup-${timestamp}`);
|
|
@@ -824,14 +1187,14 @@ async function cmdBackup() {
|
|
|
824
1187
|
writeFile(path.join(staged, "deploy.yaml"), fs.readFileSync(DEPLOY_YAML, "utf8"));
|
|
825
1188
|
writeFile(path.join(staged, "config.json"), fs.readFileSync(INTERNAL_CONFIG_JSON, "utf8"));
|
|
826
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"));
|
|
827
1191
|
writeFile(path.join(staged, "docker-stack.yaml"), fs.readFileSync(STACK_FILE, "utf8"));
|
|
828
1192
|
writeFile(path.join(staged, "traefik-dynamic.yaml"), fs.readFileSync(TRAEFIK_DYNAMIC_FILE, "utf8"));
|
|
829
1193
|
writeFile(path.join(staged, "keycloak-realm.envsync.json"), fs.readFileSync(KEYCLOAK_REALM_FILE, "utf8"));
|
|
830
1194
|
writeFile(path.join(staged, "otel-agent.yaml"), fs.readFileSync(OTEL_AGENT_CONF, "utf8"));
|
|
831
1195
|
const volumesDir = path.join(staged, "volumes");
|
|
832
1196
|
for (const volume of STACK_VOLUMES) {
|
|
833
|
-
|
|
834
|
-
backupDockerVolume(stackVolumeName(config, volume), target);
|
|
1197
|
+
backupDockerVolume(stackVolumeName(config, volume), path.join(volumesDir, volume));
|
|
835
1198
|
}
|
|
836
1199
|
run("bash", ["-lc", `tar -czf ${JSON.stringify(tarPath)} -C ${JSON.stringify(staged)} .`]);
|
|
837
1200
|
const manifest = {
|
|
@@ -846,7 +1209,6 @@ async function cmdBackup() {
|
|
|
846
1209
|
}
|
|
847
1210
|
async function cmdRestore(archivePath) {
|
|
848
1211
|
if (!archivePath) throw new Error("restore requires a .tar.gz path");
|
|
849
|
-
const config = loadConfig();
|
|
850
1212
|
const restoreRoot = path.join(BACKUPS_ROOT, `restore-${Date.now()}`);
|
|
851
1213
|
ensureDir(restoreRoot);
|
|
852
1214
|
run("bash", ["-lc", `tar -xzf ${JSON.stringify(archivePath)} -C ${JSON.stringify(restoreRoot)}`]);
|
|
@@ -854,14 +1216,16 @@ async function cmdRestore(archivePath) {
|
|
|
854
1216
|
writeFile(DEPLOY_YAML, fs.readFileSync(path.join(restoreRoot, "deploy.yaml"), "utf8"));
|
|
855
1217
|
writeFile(INTERNAL_CONFIG_JSON, fs.readFileSync(path.join(restoreRoot, "config.json"), "utf8"));
|
|
856
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"));
|
|
857
1220
|
writeFile(STACK_FILE, fs.readFileSync(path.join(restoreRoot, "docker-stack.yaml"), "utf8"));
|
|
858
1221
|
writeFile(TRAEFIK_DYNAMIC_FILE, fs.readFileSync(path.join(restoreRoot, "traefik-dynamic.yaml"), "utf8"));
|
|
859
1222
|
writeFile(KEYCLOAK_REALM_FILE, fs.readFileSync(path.join(restoreRoot, "keycloak-realm.envsync.json"), "utf8"));
|
|
860
1223
|
writeFile(OTEL_AGENT_CONF, fs.readFileSync(path.join(restoreRoot, "otel-agent.yaml"), "utf8"));
|
|
1224
|
+
const config = loadConfig();
|
|
861
1225
|
for (const volume of STACK_VOLUMES) {
|
|
862
1226
|
restoreDockerVolume(stackVolumeName(config, volume), path.join(restoreRoot, "volumes", volume));
|
|
863
1227
|
}
|
|
864
|
-
|
|
1228
|
+
console.log("Restore completed. Run 'envsync-deploy deploy' to start services.");
|
|
865
1229
|
}
|
|
866
1230
|
async function main() {
|
|
867
1231
|
const command = process.argv[2];
|
|
@@ -873,6 +1237,9 @@ async function main() {
|
|
|
873
1237
|
case "setup":
|
|
874
1238
|
await cmdSetup();
|
|
875
1239
|
break;
|
|
1240
|
+
case "bootstrap":
|
|
1241
|
+
await cmdBootstrap();
|
|
1242
|
+
break;
|
|
876
1243
|
case "deploy":
|
|
877
1244
|
await cmdDeploy();
|
|
878
1245
|
break;
|
|
@@ -892,7 +1259,7 @@ async function main() {
|
|
|
892
1259
|
await cmdRestore(flag ?? "");
|
|
893
1260
|
break;
|
|
894
1261
|
default:
|
|
895
|
-
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>");
|
|
896
1263
|
process.exit(command ? 1 : 0);
|
|
897
1264
|
}
|
|
898
1265
|
}
|