@envsync-cloud/deploy-cli 0.6.4 → 0.6.6
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 +5 -2
- package/dist/index.js +151 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,7 +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 [--dry-run]
|
|
43
|
+
envsync-deploy bootstrap [--dry-run] [--force]
|
|
44
44
|
envsync-deploy deploy [--dry-run]
|
|
45
45
|
envsync-deploy health [--json]
|
|
46
46
|
envsync-deploy upgrade [--dry-run]
|
|
@@ -71,6 +71,8 @@ Bootstrap infra, migrations, RustFS, and OpenFGA:
|
|
|
71
71
|
npx @envsync-cloud/deploy-cli bootstrap
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
+
`bootstrap` is destructive. It removes the existing EnvSync stack, matching containers, network, and managed volumes before rebuilding, and requires typing `yes` to continue. Use `--force` to bypass the prompt in automation or other non-interactive environments.
|
|
75
|
+
|
|
74
76
|
Deploy the pending API and frontend services:
|
|
75
77
|
|
|
76
78
|
```bash
|
|
@@ -79,7 +81,7 @@ npx @envsync-cloud/deploy-cli deploy
|
|
|
79
81
|
|
|
80
82
|
The staged flow is:
|
|
81
83
|
- `setup` writes desired config
|
|
82
|
-
- `bootstrap` starts base infra, runs OpenFGA and miniKMS migrations, starts runtime infra, and persists generated runtime env state
|
|
84
|
+
- `bootstrap` resets the existing EnvSync deployment, then starts base infra, runs OpenFGA and miniKMS migrations, starts runtime infra, and persists generated runtime env state
|
|
83
85
|
- `deploy` starts the pending API and frontend services
|
|
84
86
|
|
|
85
87
|
Check service health:
|
|
@@ -104,6 +106,7 @@ Preview mutating commands without changing the host:
|
|
|
104
106
|
|
|
105
107
|
```bash
|
|
106
108
|
npx @envsync-cloud/deploy-cli bootstrap --dry-run
|
|
109
|
+
npx @envsync-cloud/deploy-cli bootstrap --force
|
|
107
110
|
npx @envsync-cloud/deploy-cli deploy --dry-run
|
|
108
111
|
```
|
|
109
112
|
|
package/dist/index.js
CHANGED
|
@@ -48,7 +48,7 @@ var REQUIRED_BOOTSTRAP_ENV_KEYS = [
|
|
|
48
48
|
"OPENFGA_MODEL_ID"
|
|
49
49
|
];
|
|
50
50
|
var SEMVER_VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
51
|
-
var currentOptions = { dryRun: false };
|
|
51
|
+
var currentOptions = { dryRun: false, force: false };
|
|
52
52
|
function formatShellArg(arg) {
|
|
53
53
|
if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(arg)) return arg;
|
|
54
54
|
return JSON.stringify(arg);
|
|
@@ -69,6 +69,9 @@ function logInfo(message) {
|
|
|
69
69
|
function logSuccess(message) {
|
|
70
70
|
console.log(`${chalk.green("[ok]")} ${message}`);
|
|
71
71
|
}
|
|
72
|
+
function logWarn(message) {
|
|
73
|
+
console.log(`${chalk.yellow("[warn]")} ${message}`);
|
|
74
|
+
}
|
|
72
75
|
function logDryRun(message) {
|
|
73
76
|
console.log(`${chalk.magenta("[dry-run]")} ${message}`);
|
|
74
77
|
}
|
|
@@ -216,6 +219,18 @@ async function ask(question, fallback = "") {
|
|
|
216
219
|
});
|
|
217
220
|
});
|
|
218
221
|
}
|
|
222
|
+
async function askRequired(question) {
|
|
223
|
+
if (!process.stdin.isTTY) {
|
|
224
|
+
throw new Error("Bootstrap confirmation requires an interactive terminal. Re-run with --force to bypass the prompt.");
|
|
225
|
+
}
|
|
226
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
227
|
+
return await new Promise((resolve) => {
|
|
228
|
+
rl.question(`${question} `, (answer) => {
|
|
229
|
+
rl.close();
|
|
230
|
+
resolve(answer.trim());
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
}
|
|
219
234
|
function sleepSeconds(seconds) {
|
|
220
235
|
spawnSync("sleep", [`${seconds}`], { stdio: "ignore" });
|
|
221
236
|
}
|
|
@@ -364,6 +379,7 @@ function emptyGeneratedState() {
|
|
|
364
379
|
},
|
|
365
380
|
secrets: {
|
|
366
381
|
s3_secret_key: "",
|
|
382
|
+
keycloak_db_password: "",
|
|
367
383
|
keycloak_web_client_secret: "",
|
|
368
384
|
keycloak_api_client_secret: "",
|
|
369
385
|
openfga_db_password: "",
|
|
@@ -384,6 +400,7 @@ function normalizeGeneratedState(raw) {
|
|
|
384
400
|
},
|
|
385
401
|
secrets: {
|
|
386
402
|
s3_secret_key: raw?.secrets?.s3_secret_key ?? defaults.secrets.s3_secret_key,
|
|
403
|
+
keycloak_db_password: raw?.secrets?.keycloak_db_password ?? defaults.secrets.keycloak_db_password,
|
|
387
404
|
keycloak_web_client_secret: raw?.secrets?.keycloak_web_client_secret ?? defaults.secrets.keycloak_web_client_secret,
|
|
388
405
|
keycloak_api_client_secret: raw?.secrets?.keycloak_api_client_secret ?? defaults.secrets.keycloak_api_client_secret,
|
|
389
406
|
openfga_db_password: raw?.secrets?.openfga_db_password ?? defaults.secrets.openfga_db_password,
|
|
@@ -426,6 +443,7 @@ function mergeGeneratedState(env, generated) {
|
|
|
426
443
|
},
|
|
427
444
|
secrets: {
|
|
428
445
|
s3_secret_key: env.S3_SECRET_KEY ?? normalized.secrets.s3_secret_key,
|
|
446
|
+
keycloak_db_password: env.KEYCLOAK_DB_PASSWORD ?? normalized.secrets.keycloak_db_password,
|
|
429
447
|
keycloak_web_client_secret: env.KEYCLOAK_WEB_CLIENT_SECRET ?? normalized.secrets.keycloak_web_client_secret,
|
|
430
448
|
keycloak_api_client_secret: env.KEYCLOAK_API_CLIENT_SECRET ?? normalized.secrets.keycloak_api_client_secret,
|
|
431
449
|
openfga_db_password: env.OPENFGA_DB_PASSWORD ?? normalized.secrets.openfga_db_password,
|
|
@@ -446,6 +464,7 @@ function ensureGeneratedRuntimeState(generated) {
|
|
|
446
464
|
openfga: generated.openfga,
|
|
447
465
|
secrets: {
|
|
448
466
|
s3_secret_key: generated.secrets.s3_secret_key || randomSecret(16),
|
|
467
|
+
keycloak_db_password: generated.secrets.keycloak_db_password || "",
|
|
449
468
|
keycloak_web_client_secret: generated.secrets.keycloak_web_client_secret || randomSecret(),
|
|
450
469
|
keycloak_api_client_secret: generated.secrets.keycloak_api_client_secret || randomSecret(),
|
|
451
470
|
openfga_db_password: generated.secrets.openfga_db_password || randomSecret(),
|
|
@@ -488,6 +507,7 @@ function buildRuntimeEnv(config, generated) {
|
|
|
488
507
|
KEYCLOAK_REALM: config.auth.keycloak_realm,
|
|
489
508
|
KEYCLOAK_ADMIN_USER: config.auth.admin_user,
|
|
490
509
|
KEYCLOAK_ADMIN_PASSWORD: config.auth.admin_password,
|
|
510
|
+
KEYCLOAK_DB_PASSWORD: generated.secrets.keycloak_db_password || config.auth.admin_password,
|
|
491
511
|
KEYCLOAK_WEB_CLIENT_ID: config.auth.web_client_id,
|
|
492
512
|
KEYCLOAK_WEB_CLIENT_SECRET: generated.secrets.keycloak_web_client_secret,
|
|
493
513
|
KEYCLOAK_CLI_CLIENT_ID: config.auth.cli_client_id,
|
|
@@ -781,7 +801,7 @@ ${renderEnvList({
|
|
|
781
801
|
environment:
|
|
782
802
|
${renderEnvList({
|
|
783
803
|
POSTGRES_USER: "keycloak",
|
|
784
|
-
POSTGRES_PASSWORD: runtimeEnv.
|
|
804
|
+
POSTGRES_PASSWORD: runtimeEnv.KEYCLOAK_DB_PASSWORD,
|
|
785
805
|
POSTGRES_DB: "keycloak"
|
|
786
806
|
})}
|
|
787
807
|
volumes:
|
|
@@ -798,7 +818,7 @@ ${renderEnvList({
|
|
|
798
818
|
KC_DB: "postgres",
|
|
799
819
|
KC_DB_URL: "jdbc:postgresql://keycloak_db:5432/keycloak",
|
|
800
820
|
KC_DB_USERNAME: "keycloak",
|
|
801
|
-
KC_DB_PASSWORD: runtimeEnv.
|
|
821
|
+
KC_DB_PASSWORD: runtimeEnv.KEYCLOAK_DB_PASSWORD,
|
|
802
822
|
KC_BOOTSTRAP_ADMIN_USERNAME: config.auth.admin_user,
|
|
803
823
|
KC_BOOTSTRAP_ADMIN_PASSWORD: config.auth.admin_password,
|
|
804
824
|
KC_HTTP_ENABLED: "true",
|
|
@@ -1070,7 +1090,29 @@ function waitForTcpService(config, label, host, port, timeoutSeconds = 120) {
|
|
|
1070
1090
|
throw new Error(`Timed out waiting for ${label} at ${host}:${port}`);
|
|
1071
1091
|
}
|
|
1072
1092
|
function waitForHttpService(config, label, url, timeoutSeconds = 120) {
|
|
1073
|
-
|
|
1093
|
+
if (currentOptions.dryRun) {
|
|
1094
|
+
logDryRun(`Would wait for ${label} at ${url}`);
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
logStep(`Waiting for ${label} on ${url}`);
|
|
1098
|
+
const deadline = Date.now() + timeoutSeconds * 1e3;
|
|
1099
|
+
while (Date.now() < deadline) {
|
|
1100
|
+
if (commandSucceeds("docker", [
|
|
1101
|
+
"run",
|
|
1102
|
+
"--rm",
|
|
1103
|
+
"--network",
|
|
1104
|
+
stackNetworkName(config),
|
|
1105
|
+
"alpine:3.20",
|
|
1106
|
+
"sh",
|
|
1107
|
+
"-lc",
|
|
1108
|
+
`wget -q -O /dev/null ${JSON.stringify(url)}`
|
|
1109
|
+
])) {
|
|
1110
|
+
logSuccess(`${label} is ready`);
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
sleepSeconds(2);
|
|
1114
|
+
}
|
|
1115
|
+
throw new Error(`Timed out waiting for ${label} at ${url}`);
|
|
1074
1116
|
}
|
|
1075
1117
|
function runOpenFgaMigrate(config, runtimeEnv) {
|
|
1076
1118
|
logStep("Running OpenFGA datastore migrations");
|
|
@@ -1251,6 +1293,100 @@ function listStackServices(config) {
|
|
|
1251
1293
|
}
|
|
1252
1294
|
return services;
|
|
1253
1295
|
}
|
|
1296
|
+
function listManagedContainers(config) {
|
|
1297
|
+
const output = tryRun("docker", ["ps", "-aq", "--filter", `name=^/${config.services.stack_name}_`], { quiet: true });
|
|
1298
|
+
return output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
1299
|
+
}
|
|
1300
|
+
function stackExists(config) {
|
|
1301
|
+
const output = tryRun("docker", ["stack", "ls", "--format", "{{.Name}}"], { quiet: true });
|
|
1302
|
+
return output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).includes(config.services.stack_name);
|
|
1303
|
+
}
|
|
1304
|
+
function waitForStackRemoval(config, timeoutSeconds = 60) {
|
|
1305
|
+
if (currentOptions.dryRun) {
|
|
1306
|
+
logDryRun(`Would wait for stack ${config.services.stack_name} to be removed`);
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
const deadline = Date.now() + timeoutSeconds * 1e3;
|
|
1310
|
+
while (Date.now() < deadline) {
|
|
1311
|
+
if (!stackExists(config)) {
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
sleepSeconds(2);
|
|
1315
|
+
}
|
|
1316
|
+
throw new Error(`Timed out waiting for stack ${config.services.stack_name} to be removed`);
|
|
1317
|
+
}
|
|
1318
|
+
async function confirmBootstrapReset(config) {
|
|
1319
|
+
const volumeNames = STACK_VOLUMES.map((volume) => stackVolumeName(config, volume));
|
|
1320
|
+
const containerIds = listManagedContainers(config);
|
|
1321
|
+
const networkName = stackNetworkName(config);
|
|
1322
|
+
logWarn("Bootstrap will delete existing EnvSync Docker resources before rebuilding infra.");
|
|
1323
|
+
logWarn(`Stack: ${config.services.stack_name}`);
|
|
1324
|
+
logWarn(`Network: ${networkName}`);
|
|
1325
|
+
logWarn(`Volumes: ${volumeNames.join(", ")}`);
|
|
1326
|
+
if (containerIds.length > 0) {
|
|
1327
|
+
logWarn(`Containers: ${containerIds.join(", ")}`);
|
|
1328
|
+
} else {
|
|
1329
|
+
logWarn("Containers: none currently matched");
|
|
1330
|
+
}
|
|
1331
|
+
logWarn("This removes existing deployment data for the managed EnvSync services.");
|
|
1332
|
+
if (currentOptions.force) {
|
|
1333
|
+
logWarn("Skipping confirmation because --force was provided.");
|
|
1334
|
+
logSuccess("Destructive bootstrap reset confirmed");
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
const response = await askRequired(chalk.bold.red('Type "yes" to continue:'));
|
|
1338
|
+
if (response !== "yes") {
|
|
1339
|
+
throw new Error("Bootstrap aborted. Confirmation did not match 'yes'.");
|
|
1340
|
+
}
|
|
1341
|
+
logSuccess("Destructive bootstrap reset confirmed");
|
|
1342
|
+
}
|
|
1343
|
+
function cleanupBootstrapState(config) {
|
|
1344
|
+
const volumeNames = STACK_VOLUMES.map((volume) => stackVolumeName(config, volume));
|
|
1345
|
+
const containerIds = listManagedContainers(config);
|
|
1346
|
+
const networkName = stackNetworkName(config);
|
|
1347
|
+
logStep("Removing existing EnvSync deployment resources");
|
|
1348
|
+
if (currentOptions.dryRun) {
|
|
1349
|
+
if (stackExists(config)) {
|
|
1350
|
+
logDryRun(`Would remove stack ${config.services.stack_name}`);
|
|
1351
|
+
logCommand("docker", ["stack", "rm", config.services.stack_name]);
|
|
1352
|
+
} else {
|
|
1353
|
+
logDryRun(`No existing stack named ${config.services.stack_name} found`);
|
|
1354
|
+
}
|
|
1355
|
+
if (containerIds.length > 0) {
|
|
1356
|
+
logDryRun(`Would remove containers: ${containerIds.join(", ")}`);
|
|
1357
|
+
logCommand("docker", ["rm", "-f", ...containerIds]);
|
|
1358
|
+
}
|
|
1359
|
+
logDryRun(`Would remove network ${networkName} if present`);
|
|
1360
|
+
logCommand("docker", ["network", "rm", networkName]);
|
|
1361
|
+
for (const volumeName of volumeNames) {
|
|
1362
|
+
logDryRun(`Would remove volume ${volumeName}`);
|
|
1363
|
+
logCommand("docker", ["volume", "rm", "-f", volumeName]);
|
|
1364
|
+
}
|
|
1365
|
+
logSuccess("Bootstrap cleanup preview completed");
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
if (stackExists(config)) {
|
|
1369
|
+
logCommand("docker", ["stack", "rm", config.services.stack_name]);
|
|
1370
|
+
run("docker", ["stack", "rm", config.services.stack_name]);
|
|
1371
|
+
waitForStackRemoval(config);
|
|
1372
|
+
}
|
|
1373
|
+
const refreshedContainers = listManagedContainers(config);
|
|
1374
|
+
if (refreshedContainers.length > 0) {
|
|
1375
|
+
logCommand("docker", ["rm", "-f", ...refreshedContainers]);
|
|
1376
|
+
run("docker", ["rm", "-f", ...refreshedContainers]);
|
|
1377
|
+
}
|
|
1378
|
+
if (commandSucceeds("docker", ["network", "inspect", networkName])) {
|
|
1379
|
+
logCommand("docker", ["network", "rm", networkName]);
|
|
1380
|
+
run("docker", ["network", "rm", networkName]);
|
|
1381
|
+
}
|
|
1382
|
+
for (const volumeName of volumeNames) {
|
|
1383
|
+
if (commandSucceeds("docker", ["volume", "inspect", volumeName])) {
|
|
1384
|
+
logCommand("docker", ["volume", "rm", "-f", volumeName]);
|
|
1385
|
+
run("docker", ["volume", "rm", "-f", volumeName]);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
logSuccess("Existing EnvSync deployment resources removed");
|
|
1389
|
+
}
|
|
1254
1390
|
function serviceHealth(services, name) {
|
|
1255
1391
|
return services.get(`${name}`) ?? "missing";
|
|
1256
1392
|
}
|
|
@@ -1369,6 +1505,11 @@ async function cmdBootstrap() {
|
|
|
1369
1505
|
const runtimeEnv = buildRuntimeEnv(config, nextGenerated);
|
|
1370
1506
|
logInfo(`Release version: ${config.release.version}`);
|
|
1371
1507
|
assertSwarmManager();
|
|
1508
|
+
if (currentOptions.dryRun) {
|
|
1509
|
+
logWarn("Dry-run mode: bootstrap reset will be previewed but not executed.");
|
|
1510
|
+
}
|
|
1511
|
+
await confirmBootstrapReset(config);
|
|
1512
|
+
cleanupBootstrapState(config);
|
|
1372
1513
|
ensureRepoCheckout(config);
|
|
1373
1514
|
writeDeployArtifacts(config, nextGenerated);
|
|
1374
1515
|
buildKeycloakImage(config.images.keycloak);
|
|
@@ -1384,7 +1525,7 @@ async function cmdBootstrap() {
|
|
|
1384
1525
|
waitForPostgresService(config, "postgres", "postgres", "postgres", "envsync-postgres");
|
|
1385
1526
|
waitForRedisService(config);
|
|
1386
1527
|
waitForTcpService(config, "rustfs", "rustfs", 9e3);
|
|
1387
|
-
waitForPostgresService(config, "keycloak", "keycloak_db", "keycloak", runtimeEnv.
|
|
1528
|
+
waitForPostgresService(config, "keycloak", "keycloak_db", "keycloak", runtimeEnv.KEYCLOAK_DB_PASSWORD);
|
|
1388
1529
|
waitForPostgresService(config, "openfga", "openfga_db", "openfga", runtimeEnv.OPENFGA_DB_PASSWORD);
|
|
1389
1530
|
waitForPostgresService(config, "minikms", "minikms_db", "postgres", runtimeEnv.MINIKMS_DB_PASSWORD);
|
|
1390
1531
|
runOpenFgaMigrate(config, runtimeEnv);
|
|
@@ -1398,7 +1539,7 @@ async function cmdBootstrap() {
|
|
|
1398
1539
|
run("docker", ["stack", "deploy", "-c", BOOTSTRAP_STACK_FILE, config.services.stack_name]);
|
|
1399
1540
|
logSuccess("Runtime bootstrap stack deployed");
|
|
1400
1541
|
}
|
|
1401
|
-
waitForHttpService(config, "keycloak", "http://keycloak:
|
|
1542
|
+
waitForHttpService(config, "keycloak management readiness", "http://keycloak:9000/health/ready", 180);
|
|
1402
1543
|
waitForHttpService(config, "openfga", "http://openfga:8090/stores");
|
|
1403
1544
|
waitForTcpService(config, "minikms", "minikms", 50051);
|
|
1404
1545
|
const initResult = runBootstrapInit(config);
|
|
@@ -1655,9 +1796,10 @@ async function main() {
|
|
|
1655
1796
|
const command = argv[0];
|
|
1656
1797
|
const args = argv.slice(1);
|
|
1657
1798
|
currentOptions = {
|
|
1658
|
-
dryRun: args.includes("--dry-run")
|
|
1799
|
+
dryRun: args.includes("--dry-run"),
|
|
1800
|
+
force: args.includes("--force")
|
|
1659
1801
|
};
|
|
1660
|
-
const positionals = args.filter((arg) => arg !== "--dry-run");
|
|
1802
|
+
const positionals = args.filter((arg) => arg !== "--dry-run" && arg !== "--force");
|
|
1661
1803
|
switch (command) {
|
|
1662
1804
|
case "preinstall":
|
|
1663
1805
|
await cmdPreinstall();
|
|
@@ -1688,7 +1830,7 @@ async function main() {
|
|
|
1688
1830
|
break;
|
|
1689
1831
|
default:
|
|
1690
1832
|
console.log(
|
|
1691
|
-
"Usage: envsync-deploy <preinstall|setup|bootstrap|deploy|health|upgrade|upgrade-deps|backup|restore> [--dry-run]"
|
|
1833
|
+
"Usage: envsync-deploy <preinstall|setup|bootstrap|deploy|health|upgrade|upgrade-deps|backup|restore> [--dry-run] [--force]"
|
|
1692
1834
|
);
|
|
1693
1835
|
process.exit(command ? 1 : 0);
|
|
1694
1836
|
}
|