@envsync-cloud/deploy-cli 0.6.3 → 0.6.4
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 +13 -6
- package/dist/index.js +269 -35
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -40,13 +40,13 @@ bunx @envsync-cloud/deploy-cli <command>
|
|
|
40
40
|
```text
|
|
41
41
|
envsync-deploy preinstall
|
|
42
42
|
envsync-deploy setup
|
|
43
|
-
envsync-deploy bootstrap
|
|
44
|
-
envsync-deploy deploy
|
|
43
|
+
envsync-deploy bootstrap [--dry-run]
|
|
44
|
+
envsync-deploy deploy [--dry-run]
|
|
45
45
|
envsync-deploy health [--json]
|
|
46
|
-
envsync-deploy upgrade
|
|
47
|
-
envsync-deploy upgrade-deps
|
|
48
|
-
envsync-deploy backup
|
|
49
|
-
envsync-deploy restore <archive>
|
|
46
|
+
envsync-deploy upgrade [--dry-run]
|
|
47
|
+
envsync-deploy upgrade-deps [--dry-run]
|
|
48
|
+
envsync-deploy backup [--dry-run]
|
|
49
|
+
envsync-deploy restore <archive> [--dry-run]
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
## Quick Start
|
|
@@ -100,6 +100,13 @@ Restore from an existing backup archive:
|
|
|
100
100
|
npx @envsync-cloud/deploy-cli restore /path/to/envsync-backup.tar.gz
|
|
101
101
|
```
|
|
102
102
|
|
|
103
|
+
Preview mutating commands without changing the host:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npx @envsync-cloud/deploy-cli bootstrap --dry-run
|
|
107
|
+
npx @envsync-cloud/deploy-cli deploy --dry-run
|
|
108
|
+
```
|
|
109
|
+
|
|
103
110
|
## Links
|
|
104
111
|
|
|
105
112
|
- Repository: https://github.com/EnvSync-Cloud/envsync
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { spawnSync } from "child_process";
|
|
|
6
6
|
import fs from "fs";
|
|
7
7
|
import path from "path";
|
|
8
8
|
import readline from "readline";
|
|
9
|
+
import chalk from "chalk";
|
|
9
10
|
var HOST_ROOT = "/opt/envsync";
|
|
10
11
|
var DEPLOY_ROOT = "/opt/envsync/deploy";
|
|
11
12
|
var RELEASES_ROOT = "/opt/envsync/releases";
|
|
@@ -47,6 +48,33 @@ var REQUIRED_BOOTSTRAP_ENV_KEYS = [
|
|
|
47
48
|
"OPENFGA_MODEL_ID"
|
|
48
49
|
];
|
|
49
50
|
var SEMVER_VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
51
|
+
var currentOptions = { dryRun: false };
|
|
52
|
+
function formatShellArg(arg) {
|
|
53
|
+
if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(arg)) return arg;
|
|
54
|
+
return JSON.stringify(arg);
|
|
55
|
+
}
|
|
56
|
+
function formatCommand(cmd, args) {
|
|
57
|
+
return [cmd, ...args].map(formatShellArg).join(" ");
|
|
58
|
+
}
|
|
59
|
+
function logSection(title) {
|
|
60
|
+
console.log(`
|
|
61
|
+
${chalk.bold.blue(title)}`);
|
|
62
|
+
}
|
|
63
|
+
function logStep(message) {
|
|
64
|
+
console.log(`${chalk.cyan("[step]")} ${message}`);
|
|
65
|
+
}
|
|
66
|
+
function logInfo(message) {
|
|
67
|
+
console.log(`${chalk.blue("[info]")} ${message}`);
|
|
68
|
+
}
|
|
69
|
+
function logSuccess(message) {
|
|
70
|
+
console.log(`${chalk.green("[ok]")} ${message}`);
|
|
71
|
+
}
|
|
72
|
+
function logDryRun(message) {
|
|
73
|
+
console.log(`${chalk.magenta("[dry-run]")} ${message}`);
|
|
74
|
+
}
|
|
75
|
+
function logCommand(cmd, args) {
|
|
76
|
+
console.log(chalk.dim(`$ ${formatCommand(cmd, args)}`));
|
|
77
|
+
}
|
|
50
78
|
function run(cmd, args, opts = {}) {
|
|
51
79
|
const result = spawnSync(cmd, args, {
|
|
52
80
|
cwd: opts.cwd,
|
|
@@ -85,6 +113,13 @@ function writeFile(target, content, mode) {
|
|
|
85
113
|
fs.writeFileSync(target, content, "utf8");
|
|
86
114
|
if (mode != null) fs.chmodSync(target, mode);
|
|
87
115
|
}
|
|
116
|
+
function writeFileMaybe(target, content, mode) {
|
|
117
|
+
if (currentOptions.dryRun) {
|
|
118
|
+
logDryRun(`Would write ${target}`);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
writeFile(target, content, mode);
|
|
122
|
+
}
|
|
88
123
|
function exists(target) {
|
|
89
124
|
return fs.existsSync(target);
|
|
90
125
|
}
|
|
@@ -767,8 +802,9 @@ ${renderEnvList({
|
|
|
767
802
|
KC_BOOTSTRAP_ADMIN_USERNAME: config.auth.admin_user,
|
|
768
803
|
KC_BOOTSTRAP_ADMIN_PASSWORD: config.auth.admin_password,
|
|
769
804
|
KC_HTTP_ENABLED: "true",
|
|
805
|
+
KC_HEALTH_ENABLED: "true",
|
|
770
806
|
KC_PROXY_HEADERS: "xforwarded",
|
|
771
|
-
|
|
807
|
+
KC_HOSTNAME_STRICT: "false"
|
|
772
808
|
})}
|
|
773
809
|
volumes:
|
|
774
810
|
- ${KEYCLOAK_REALM_FILE}:/opt/keycloak/data/import/realm.json:ro
|
|
@@ -897,40 +933,59 @@ volumes:
|
|
|
897
933
|
}
|
|
898
934
|
function writeDeployArtifacts(config, generated) {
|
|
899
935
|
const runtimeEnv = buildRuntimeEnv(config, generated);
|
|
900
|
-
|
|
901
|
-
|
|
936
|
+
logStep("Rendering deploy artifacts");
|
|
937
|
+
writeFileMaybe(DEPLOY_ENV, renderEnvFile(runtimeEnv), 384);
|
|
938
|
+
writeFileMaybe(
|
|
902
939
|
INTERNAL_CONFIG_JSON,
|
|
903
940
|
JSON.stringify({ config, generated: mergeGeneratedState(runtimeEnv, generated) }, null, 2) + "\n"
|
|
904
941
|
);
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
942
|
+
writeFileMaybe(VERSIONS_LOCK, JSON.stringify(config.images, null, 2) + "\n");
|
|
943
|
+
writeFileMaybe(KEYCLOAK_REALM_FILE, renderKeycloakRealm(config, runtimeEnv));
|
|
944
|
+
writeFileMaybe(TRAEFIK_DYNAMIC_FILE, renderTraefikDynamicConfig(config));
|
|
945
|
+
writeFileMaybe(BOOTSTRAP_BASE_STACK_FILE, renderStack(config, runtimeEnv, "base"));
|
|
946
|
+
writeFileMaybe(BOOTSTRAP_STACK_FILE, renderStack(config, runtimeEnv, "bootstrap"));
|
|
947
|
+
writeFileMaybe(STACK_FILE, renderStack(config, runtimeEnv, "full"));
|
|
948
|
+
writeFileMaybe(NGINX_WEB_CONF, renderNginxConf("web"));
|
|
949
|
+
writeFileMaybe(NGINX_LANDING_CONF, renderNginxConf("landing"));
|
|
950
|
+
writeFileMaybe(OTEL_AGENT_CONF, renderOtelAgentConfig(config));
|
|
951
|
+
logSuccess(currentOptions.dryRun ? "Deploy artifacts previewed" : "Deploy artifacts written");
|
|
914
952
|
}
|
|
915
953
|
function saveDesiredConfig(config) {
|
|
916
954
|
const internal = readInternalState();
|
|
917
955
|
const generated = mergeGeneratedState(loadGeneratedEnv(), internal?.generated);
|
|
918
|
-
|
|
919
|
-
|
|
956
|
+
logStep(`Saving desired config to ${DEPLOY_YAML}`);
|
|
957
|
+
writeFileMaybe(DEPLOY_YAML, toYaml(config) + "\n");
|
|
958
|
+
writeFileMaybe(
|
|
920
959
|
INTERNAL_CONFIG_JSON,
|
|
921
960
|
JSON.stringify({ config, generated }, null, 2) + "\n"
|
|
922
961
|
);
|
|
962
|
+
logSuccess(currentOptions.dryRun ? "Desired config previewed" : "Desired config saved");
|
|
923
963
|
}
|
|
924
964
|
function ensureRepoCheckout(config) {
|
|
965
|
+
logStep(`Ensuring pinned repo checkout at ${config.source.ref}`);
|
|
966
|
+
if (currentOptions.dryRun) {
|
|
967
|
+
logDryRun(`Would ensure repo checkout at ${REPO_ROOT}`);
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
925
970
|
ensureDir(REPO_ROOT);
|
|
926
971
|
if (!exists(path.join(REPO_ROOT, ".git"))) {
|
|
972
|
+
logCommand("git", ["clone", config.source.repo_url, REPO_ROOT]);
|
|
927
973
|
run("git", ["clone", config.source.repo_url, REPO_ROOT]);
|
|
928
974
|
}
|
|
975
|
+
logCommand("git", ["remote", "set-url", "origin", config.source.repo_url]);
|
|
929
976
|
run("git", ["remote", "set-url", "origin", config.source.repo_url], { cwd: REPO_ROOT });
|
|
977
|
+
logCommand("git", ["fetch", "--tags", "--force", "origin"]);
|
|
930
978
|
run("git", ["fetch", "--tags", "--force", "origin"], { cwd: REPO_ROOT });
|
|
979
|
+
logCommand("git", ["checkout", "--force", config.source.ref]);
|
|
931
980
|
run("git", ["checkout", "--force", config.source.ref], { cwd: REPO_ROOT });
|
|
981
|
+
logSuccess(`Pinned repo checkout ready at ${config.source.ref}`);
|
|
932
982
|
}
|
|
933
983
|
function extractStaticBundle(image, targetDir) {
|
|
984
|
+
logStep(`Extracting static bundle from ${image}`);
|
|
985
|
+
if (currentOptions.dryRun) {
|
|
986
|
+
logDryRun(`Would extract ${image} into ${targetDir}`);
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
934
989
|
ensureDir(targetDir);
|
|
935
990
|
const containerId = run("docker", ["create", image], { quiet: true }).trim();
|
|
936
991
|
try {
|
|
@@ -938,24 +993,43 @@ function extractStaticBundle(image, targetDir) {
|
|
|
938
993
|
} finally {
|
|
939
994
|
run("docker", ["rm", "-f", containerId], { quiet: true });
|
|
940
995
|
}
|
|
996
|
+
logSuccess(`Static bundle extracted to ${targetDir}`);
|
|
941
997
|
}
|
|
942
998
|
function buildKeycloakImage(imageTag, repoRoot = REPO_ROOT) {
|
|
943
999
|
const buildContext = path.join(repoRoot, "packages/envsync-keycloak-theme");
|
|
944
1000
|
if (!exists(path.join(buildContext, "Dockerfile"))) {
|
|
945
1001
|
throw new Error(`Missing Keycloak Docker build context at ${buildContext}`);
|
|
946
1002
|
}
|
|
1003
|
+
logStep(`Building Keycloak image ${imageTag}`);
|
|
1004
|
+
if (currentOptions.dryRun) {
|
|
1005
|
+
logDryRun(`Would build ${imageTag} from ${buildContext}`);
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
logCommand("docker", ["build", "-t", imageTag, buildContext]);
|
|
947
1009
|
run("docker", ["build", "-t", imageTag, buildContext]);
|
|
1010
|
+
logSuccess(`Built Keycloak image ${imageTag}`);
|
|
948
1011
|
}
|
|
949
1012
|
function stackNetworkName(config) {
|
|
950
1013
|
return `${config.services.stack_name}_envsync`;
|
|
951
1014
|
}
|
|
952
1015
|
function assertSwarmManager() {
|
|
1016
|
+
if (currentOptions.dryRun) {
|
|
1017
|
+
logDryRun("Skipping Docker Swarm manager validation");
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
logStep("Validating Docker Swarm manager state");
|
|
953
1021
|
const state = tryRun("docker", ["info", "--format", "{{.Swarm.LocalNodeState}}|{{.Swarm.ControlAvailable}}"], { quiet: true }).trim();
|
|
954
1022
|
if (state !== "active|true") {
|
|
955
1023
|
throw new Error("Docker Swarm is not initialized on this node. Run 'docker swarm init' or 'envsync-deploy preinstall' first.");
|
|
956
1024
|
}
|
|
1025
|
+
logSuccess("Docker Swarm manager is ready");
|
|
957
1026
|
}
|
|
958
1027
|
function waitForCommand(config, label, image, command, timeoutSeconds = 120, env = {}, volumes = []) {
|
|
1028
|
+
if (currentOptions.dryRun) {
|
|
1029
|
+
logDryRun(`Would wait for ${label}`);
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
logStep(`Waiting for ${label}`);
|
|
959
1033
|
const deadline = Date.now() + timeoutSeconds * 1e3;
|
|
960
1034
|
while (Date.now() < deadline) {
|
|
961
1035
|
const args = ["run", "--rm", "--network", stackNetworkName(config)];
|
|
@@ -967,6 +1041,7 @@ function waitForCommand(config, label, image, command, timeoutSeconds = 120, env
|
|
|
967
1041
|
}
|
|
968
1042
|
args.push(image, "sh", "-lc", command);
|
|
969
1043
|
if (commandSucceeds("docker", args)) {
|
|
1044
|
+
logSuccess(`${label} is ready`);
|
|
970
1045
|
return;
|
|
971
1046
|
}
|
|
972
1047
|
sleepSeconds(2);
|
|
@@ -998,6 +1073,23 @@ function waitForHttpService(config, label, url, timeoutSeconds = 120) {
|
|
|
998
1073
|
waitForCommand(config, `${label} HTTP readiness`, "alpine:3.20", `wget -q -O /dev/null ${JSON.stringify(url)}`, timeoutSeconds);
|
|
999
1074
|
}
|
|
1000
1075
|
function runOpenFgaMigrate(config, runtimeEnv) {
|
|
1076
|
+
logStep("Running OpenFGA datastore migrations");
|
|
1077
|
+
if (currentOptions.dryRun) {
|
|
1078
|
+
logDryRun("Would run OpenFGA datastore migrations");
|
|
1079
|
+
logCommand("docker", [
|
|
1080
|
+
"run",
|
|
1081
|
+
"--rm",
|
|
1082
|
+
"--network",
|
|
1083
|
+
stackNetworkName(config),
|
|
1084
|
+
"-e",
|
|
1085
|
+
"OPENFGA_DATASTORE_ENGINE=postgres",
|
|
1086
|
+
"-e",
|
|
1087
|
+
`OPENFGA_DATASTORE_URI=postgres://openfga:${runtimeEnv.OPENFGA_DB_PASSWORD}@openfga_db:5432/openfga?sslmode=disable`,
|
|
1088
|
+
"openfga/openfga:v1.12.0",
|
|
1089
|
+
"migrate"
|
|
1090
|
+
]);
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1001
1093
|
run("docker", [
|
|
1002
1094
|
"run",
|
|
1003
1095
|
"--rm",
|
|
@@ -1010,8 +1102,28 @@ function runOpenFgaMigrate(config, runtimeEnv) {
|
|
|
1010
1102
|
"openfga/openfga:v1.12.0",
|
|
1011
1103
|
"migrate"
|
|
1012
1104
|
]);
|
|
1105
|
+
logSuccess("OpenFGA datastore migrations completed");
|
|
1013
1106
|
}
|
|
1014
1107
|
function runMiniKmsMigrate(config, runtimeEnv) {
|
|
1108
|
+
logStep("Running miniKMS datastore migrations");
|
|
1109
|
+
if (currentOptions.dryRun) {
|
|
1110
|
+
logDryRun("Would run miniKMS datastore migrations");
|
|
1111
|
+
logCommand("docker", [
|
|
1112
|
+
"run",
|
|
1113
|
+
"--rm",
|
|
1114
|
+
"--network",
|
|
1115
|
+
stackNetworkName(config),
|
|
1116
|
+
"-e",
|
|
1117
|
+
`PGPASSWORD=${runtimeEnv.MINIKMS_DB_PASSWORD}`,
|
|
1118
|
+
"-v",
|
|
1119
|
+
`${path.join(REPO_ROOT, "docker/minikms/migrations")}:/migrations:ro`,
|
|
1120
|
+
"postgres:17",
|
|
1121
|
+
"sh",
|
|
1122
|
+
"-lc",
|
|
1123
|
+
"psql -h minikms_db -U postgres -d minikms -f /migrations/001_initial_schema.sql && psql -h minikms_db -U postgres -d minikms -f /migrations/002_vault_storage.sql"
|
|
1124
|
+
]);
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1015
1127
|
run("docker", [
|
|
1016
1128
|
"run",
|
|
1017
1129
|
"--rm",
|
|
@@ -1026,8 +1138,35 @@ function runMiniKmsMigrate(config, runtimeEnv) {
|
|
|
1026
1138
|
"-lc",
|
|
1027
1139
|
"psql -h minikms_db -U postgres -d minikms -f /migrations/001_initial_schema.sql && psql -h minikms_db -U postgres -d minikms -f /migrations/002_vault_storage.sql"
|
|
1028
1140
|
]);
|
|
1141
|
+
logSuccess("miniKMS datastore migrations completed");
|
|
1029
1142
|
}
|
|
1030
1143
|
function runBootstrapInit(config) {
|
|
1144
|
+
logStep("Running API bootstrap init");
|
|
1145
|
+
if (currentOptions.dryRun) {
|
|
1146
|
+
logDryRun("Would run API bootstrap init and persist generated OpenFGA IDs");
|
|
1147
|
+
logCommand("docker", [
|
|
1148
|
+
"run",
|
|
1149
|
+
"--rm",
|
|
1150
|
+
"--network",
|
|
1151
|
+
stackNetworkName(config),
|
|
1152
|
+
"--env-file",
|
|
1153
|
+
DEPLOY_ENV,
|
|
1154
|
+
"-e",
|
|
1155
|
+
"SKIP_ROOT_ENV=1",
|
|
1156
|
+
"-e",
|
|
1157
|
+
"SKIP_ROOT_ENV_WRITE=1",
|
|
1158
|
+
config.images.api,
|
|
1159
|
+
"bun",
|
|
1160
|
+
"run",
|
|
1161
|
+
"scripts/prod-init.ts",
|
|
1162
|
+
"--json",
|
|
1163
|
+
"--no-write-root-env"
|
|
1164
|
+
]);
|
|
1165
|
+
return {
|
|
1166
|
+
openfgaStoreId: "",
|
|
1167
|
+
openfgaModelId: ""
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1031
1170
|
const output = run(
|
|
1032
1171
|
"docker",
|
|
1033
1172
|
[
|
|
@@ -1054,6 +1193,7 @@ function runBootstrapInit(config) {
|
|
|
1054
1193
|
if (!result.openfgaStoreId || !result.openfgaModelId) {
|
|
1055
1194
|
throw new Error("Bootstrap init did not return OpenFGA IDs");
|
|
1056
1195
|
}
|
|
1196
|
+
logSuccess("API bootstrap init completed");
|
|
1057
1197
|
return {
|
|
1058
1198
|
openfgaStoreId: result.openfgaStoreId,
|
|
1059
1199
|
openfgaModelId: result.openfgaModelId
|
|
@@ -1141,6 +1281,7 @@ async function cmdPreinstall() {
|
|
|
1141
1281
|
run("bash", ["-lc", "curl -fsSL https://acme-v02.api.letsencrypt.org/directory >/dev/null"]);
|
|
1142
1282
|
}
|
|
1143
1283
|
async function cmdSetup() {
|
|
1284
|
+
logSection("Setup");
|
|
1144
1285
|
const rootDomain = await ask("Root domain", "example.com");
|
|
1145
1286
|
const acmeEmail = await ask("ACME email", `admin@${rootDomain}`);
|
|
1146
1287
|
const releaseVersion = await ask("Release version", getDeployCliVersion());
|
|
@@ -1216,20 +1357,30 @@ async function cmdSetup() {
|
|
|
1216
1357
|
}
|
|
1217
1358
|
};
|
|
1218
1359
|
saveDesiredConfig(config);
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1360
|
+
logSuccess(`Config written to ${DEPLOY_YAML}`);
|
|
1361
|
+
logInfo(`Pinned source checkout: ${config.source.repo_url} @ ${config.source.ref}`);
|
|
1362
|
+
logInfo("Create these DNS records:");
|
|
1222
1363
|
console.log(JSON.stringify(domainMap(rootDomain), null, 2));
|
|
1223
1364
|
}
|
|
1224
1365
|
async function cmdBootstrap() {
|
|
1366
|
+
logSection("Bootstrap");
|
|
1225
1367
|
const { config, generated } = loadState();
|
|
1226
1368
|
const nextGenerated = ensureGeneratedRuntimeState(generated);
|
|
1227
1369
|
const runtimeEnv = buildRuntimeEnv(config, nextGenerated);
|
|
1370
|
+
logInfo(`Release version: ${config.release.version}`);
|
|
1228
1371
|
assertSwarmManager();
|
|
1229
1372
|
ensureRepoCheckout(config);
|
|
1230
1373
|
writeDeployArtifacts(config, nextGenerated);
|
|
1231
1374
|
buildKeycloakImage(config.images.keycloak);
|
|
1232
|
-
|
|
1375
|
+
if (currentOptions.dryRun) {
|
|
1376
|
+
logDryRun(`Would deploy base bootstrap stack for ${config.services.stack_name}`);
|
|
1377
|
+
logCommand("docker", ["stack", "deploy", "-c", BOOTSTRAP_BASE_STACK_FILE, config.services.stack_name]);
|
|
1378
|
+
} else {
|
|
1379
|
+
logStep("Deploying base bootstrap stack");
|
|
1380
|
+
logCommand("docker", ["stack", "deploy", "-c", BOOTSTRAP_BASE_STACK_FILE, config.services.stack_name]);
|
|
1381
|
+
run("docker", ["stack", "deploy", "-c", BOOTSTRAP_BASE_STACK_FILE, config.services.stack_name]);
|
|
1382
|
+
logSuccess("Base bootstrap stack deployed");
|
|
1383
|
+
}
|
|
1233
1384
|
waitForPostgresService(config, "postgres", "postgres", "postgres", "envsync-postgres");
|
|
1234
1385
|
waitForRedisService(config);
|
|
1235
1386
|
waitForTcpService(config, "rustfs", "rustfs", 9e3);
|
|
@@ -1238,11 +1389,24 @@ async function cmdBootstrap() {
|
|
|
1238
1389
|
waitForPostgresService(config, "minikms", "minikms_db", "postgres", runtimeEnv.MINIKMS_DB_PASSWORD);
|
|
1239
1390
|
runOpenFgaMigrate(config, runtimeEnv);
|
|
1240
1391
|
runMiniKmsMigrate(config, runtimeEnv);
|
|
1241
|
-
|
|
1242
|
-
|
|
1392
|
+
if (currentOptions.dryRun) {
|
|
1393
|
+
logDryRun(`Would deploy runtime bootstrap stack for ${config.services.stack_name}`);
|
|
1394
|
+
logCommand("docker", ["stack", "deploy", "-c", BOOTSTRAP_STACK_FILE, config.services.stack_name]);
|
|
1395
|
+
} else {
|
|
1396
|
+
logStep("Deploying runtime bootstrap stack");
|
|
1397
|
+
logCommand("docker", ["stack", "deploy", "-c", BOOTSTRAP_STACK_FILE, config.services.stack_name]);
|
|
1398
|
+
run("docker", ["stack", "deploy", "-c", BOOTSTRAP_STACK_FILE, config.services.stack_name]);
|
|
1399
|
+
logSuccess("Runtime bootstrap stack deployed");
|
|
1400
|
+
}
|
|
1401
|
+
waitForHttpService(config, "keycloak", "http://keycloak:8080/health/ready", 180);
|
|
1243
1402
|
waitForHttpService(config, "openfga", "http://openfga:8090/stores");
|
|
1244
1403
|
waitForTcpService(config, "minikms", "minikms", 50051);
|
|
1245
1404
|
const initResult = runBootstrapInit(config);
|
|
1405
|
+
if (currentOptions.dryRun) {
|
|
1406
|
+
logDryRun("Skipping generated OpenFGA ID persistence in preview mode");
|
|
1407
|
+
logSuccess("Bootstrap dry-run completed");
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1246
1410
|
const bootstrappedGenerated = normalizeGeneratedState({
|
|
1247
1411
|
openfga: {
|
|
1248
1412
|
store_id: initResult.openfgaStoreId,
|
|
@@ -1254,26 +1418,46 @@ async function cmdBootstrap() {
|
|
|
1254
1418
|
}
|
|
1255
1419
|
});
|
|
1256
1420
|
writeDeployArtifacts(config, bootstrappedGenerated);
|
|
1257
|
-
|
|
1421
|
+
logSuccess("Bootstrap completed");
|
|
1258
1422
|
}
|
|
1259
1423
|
async function cmdDeploy() {
|
|
1424
|
+
logSection("Deploy");
|
|
1260
1425
|
const { config, generated } = loadState();
|
|
1426
|
+
logInfo(`Release version: ${config.release.version}`);
|
|
1261
1427
|
assertSwarmManager();
|
|
1262
1428
|
assertBootstrapState(generated);
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1429
|
+
if (!currentOptions.dryRun) {
|
|
1430
|
+
const services = listStackServices(config);
|
|
1431
|
+
if (serviceHealth(services, `${config.services.stack_name}_keycloak`) === "missing" || serviceHealth(services, `${config.services.stack_name}_openfga`) === "missing" || serviceHealth(services, `${config.services.stack_name}_minikms`) === "missing") {
|
|
1432
|
+
throw new Error("Bootstrap has not completed successfully. Run 'envsync-deploy bootstrap' again.");
|
|
1433
|
+
}
|
|
1434
|
+
} else {
|
|
1435
|
+
logDryRun("Skipping runtime bootstrap service validation");
|
|
1266
1436
|
}
|
|
1267
1437
|
ensureRepoCheckout(config);
|
|
1268
1438
|
writeDeployArtifacts(config, generated);
|
|
1269
1439
|
buildKeycloakImage(config.images.keycloak);
|
|
1270
|
-
|
|
1271
|
-
|
|
1440
|
+
if (currentOptions.dryRun) {
|
|
1441
|
+
logDryRun(`Would ensure ${RELEASES_ROOT}/web/current exists`);
|
|
1442
|
+
logDryRun(`Would ensure ${RELEASES_ROOT}/landing/current exists`);
|
|
1443
|
+
} else {
|
|
1444
|
+
ensureDir(`${RELEASES_ROOT}/web/current`);
|
|
1445
|
+
ensureDir(`${RELEASES_ROOT}/landing/current`);
|
|
1446
|
+
}
|
|
1272
1447
|
extractStaticBundle(config.images.web, `${RELEASES_ROOT}/web/current`);
|
|
1273
1448
|
extractStaticBundle(config.images.landing, `${RELEASES_ROOT}/landing/current`);
|
|
1274
|
-
|
|
1275
|
-
|
|
1449
|
+
writeFileMaybe(`${RELEASES_ROOT}/web/current/runtime-config.js`, renderFrontendRuntimeConfig(config));
|
|
1450
|
+
writeFileMaybe(`${RELEASES_ROOT}/landing/current/runtime-config.js`, renderFrontendRuntimeConfig(config));
|
|
1451
|
+
if (currentOptions.dryRun) {
|
|
1452
|
+
logDryRun(`Would deploy full stack for ${config.services.stack_name}`);
|
|
1453
|
+
logCommand("docker", ["stack", "deploy", "-c", STACK_FILE, config.services.stack_name]);
|
|
1454
|
+
logSuccess("Deploy dry-run completed");
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
logStep("Deploying full stack");
|
|
1458
|
+
logCommand("docker", ["stack", "deploy", "-c", STACK_FILE, config.services.stack_name]);
|
|
1276
1459
|
run("docker", ["stack", "deploy", "-c", STACK_FILE, config.services.stack_name]);
|
|
1460
|
+
logSuccess("Deploy completed");
|
|
1277
1461
|
}
|
|
1278
1462
|
async function cmdHealth(asJson) {
|
|
1279
1463
|
const { config, generated } = loadState();
|
|
@@ -1314,20 +1498,28 @@ async function cmdHealth(asJson) {
|
|
|
1314
1498
|
console.log(JSON.stringify(checks, null, 2));
|
|
1315
1499
|
}
|
|
1316
1500
|
async function cmdUpgrade() {
|
|
1501
|
+
logSection("Upgrade");
|
|
1317
1502
|
const { config } = loadState();
|
|
1318
1503
|
config.images = {
|
|
1319
1504
|
...config.images,
|
|
1320
1505
|
...versionedImages(config.release.version)
|
|
1321
1506
|
};
|
|
1322
1507
|
saveDesiredConfig(config);
|
|
1508
|
+
if (currentOptions.dryRun) {
|
|
1509
|
+
logDryRun(`Would upgrade stack to release ${config.release.version}`);
|
|
1510
|
+
}
|
|
1323
1511
|
await cmdDeploy();
|
|
1324
1512
|
}
|
|
1325
1513
|
async function cmdUpgradeDeps() {
|
|
1514
|
+
logSection("Upgrade Dependencies");
|
|
1326
1515
|
const { config } = loadState();
|
|
1327
1516
|
config.images.traefik = "traefik:v3.1";
|
|
1328
1517
|
config.images.clickstack = "clickhouse/clickstack-all-in-one:latest";
|
|
1329
1518
|
config.images.otel_agent = "otel/opentelemetry-collector-contrib:0.111.0";
|
|
1330
1519
|
saveDesiredConfig(config);
|
|
1520
|
+
if (currentOptions.dryRun) {
|
|
1521
|
+
logDryRun("Would refresh dependency image tags and redeploy");
|
|
1522
|
+
}
|
|
1331
1523
|
await cmdDeploy();
|
|
1332
1524
|
}
|
|
1333
1525
|
function sha256File(filePath) {
|
|
@@ -1339,6 +1531,11 @@ function stackVolumeName(config, name) {
|
|
|
1339
1531
|
return `${config.services.stack_name}_${name}`;
|
|
1340
1532
|
}
|
|
1341
1533
|
function backupDockerVolume(volumeName, targetDir) {
|
|
1534
|
+
logStep(`Backing up Docker volume ${volumeName}`);
|
|
1535
|
+
if (currentOptions.dryRun) {
|
|
1536
|
+
logDryRun(`Would back up ${volumeName} into ${targetDir}`);
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1342
1539
|
ensureDir(targetDir);
|
|
1343
1540
|
run("docker", [
|
|
1344
1541
|
"run",
|
|
@@ -1352,8 +1549,14 @@ function backupDockerVolume(volumeName, targetDir) {
|
|
|
1352
1549
|
"-lc",
|
|
1353
1550
|
"cd /from && tar -czf /to/volume.tar.gz ."
|
|
1354
1551
|
]);
|
|
1552
|
+
logSuccess(`Backed up Docker volume ${volumeName}`);
|
|
1355
1553
|
}
|
|
1356
1554
|
function restoreDockerVolume(volumeName, sourceDir) {
|
|
1555
|
+
logStep(`Restoring Docker volume ${volumeName}`);
|
|
1556
|
+
if (currentOptions.dryRun) {
|
|
1557
|
+
logDryRun(`Would restore ${volumeName} from ${sourceDir}`);
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1357
1560
|
run("docker", ["volume", "create", volumeName], { quiet: true });
|
|
1358
1561
|
run("docker", [
|
|
1359
1562
|
"run",
|
|
@@ -1367,15 +1570,29 @@ function restoreDockerVolume(volumeName, sourceDir) {
|
|
|
1367
1570
|
"-lc",
|
|
1368
1571
|
"cd /to && tar -xzf /from/volume.tar.gz"
|
|
1369
1572
|
]);
|
|
1573
|
+
logSuccess(`Restored Docker volume ${volumeName}`);
|
|
1370
1574
|
}
|
|
1371
1575
|
async function cmdBackup() {
|
|
1576
|
+
logSection("Backup");
|
|
1372
1577
|
const { config } = loadState();
|
|
1373
|
-
ensureDir(config.backup.output_dir);
|
|
1374
1578
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:]/g, "-");
|
|
1375
1579
|
const archiveBase = path.join(config.backup.output_dir, `envsync-backup-${timestamp}`);
|
|
1376
1580
|
const manifestPath = `${archiveBase}.manifest.json`;
|
|
1377
1581
|
const tarPath = `${archiveBase}.tar.gz`;
|
|
1378
1582
|
const staged = path.join(BACKUPS_ROOT, `staging-${timestamp}`);
|
|
1583
|
+
logInfo(`Backup archive target: ${tarPath}`);
|
|
1584
|
+
if (currentOptions.dryRun) {
|
|
1585
|
+
logDryRun(`Would stage backup files in ${staged}`);
|
|
1586
|
+
for (const volume of STACK_VOLUMES) {
|
|
1587
|
+
backupDockerVolume(stackVolumeName(config, volume), path.join(staged, "volumes", volume));
|
|
1588
|
+
}
|
|
1589
|
+
logDryRun(`Would write manifest ${manifestPath}`);
|
|
1590
|
+
logDryRun(`Would create archive ${tarPath}`);
|
|
1591
|
+
logSuccess("Backup dry-run completed");
|
|
1592
|
+
console.log(tarPath);
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
ensureDir(config.backup.output_dir);
|
|
1379
1596
|
ensureDir(staged);
|
|
1380
1597
|
writeFile(path.join(staged, "deploy.env"), fs.readFileSync(DEPLOY_ENV, "utf8"));
|
|
1381
1598
|
writeFile(path.join(staged, "deploy.yaml"), fs.readFileSync(DEPLOY_YAML, "utf8"));
|
|
@@ -1400,11 +1617,21 @@ async function cmdBackup() {
|
|
|
1400
1617
|
volumes: STACK_VOLUMES.map((volume) => stackVolumeName(config, volume))
|
|
1401
1618
|
};
|
|
1402
1619
|
writeFile(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
|
|
1620
|
+
logSuccess("Backup completed");
|
|
1403
1621
|
console.log(tarPath);
|
|
1404
1622
|
}
|
|
1405
1623
|
async function cmdRestore(archivePath) {
|
|
1406
1624
|
if (!archivePath) throw new Error("restore requires a .tar.gz path");
|
|
1625
|
+
logSection("Restore");
|
|
1407
1626
|
const restoreRoot = path.join(BACKUPS_ROOT, `restore-${Date.now()}`);
|
|
1627
|
+
logInfo(`Restore archive: ${archivePath}`);
|
|
1628
|
+
if (currentOptions.dryRun) {
|
|
1629
|
+
logDryRun(`Would extract ${archivePath} into ${restoreRoot}`);
|
|
1630
|
+
logDryRun(`Would restore deploy files into ${DEPLOY_ROOT} and ${ETC_ROOT}`);
|
|
1631
|
+
logDryRun("Would restore all managed Docker volumes from the archive");
|
|
1632
|
+
logSuccess("Restore dry-run completed");
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1408
1635
|
ensureDir(restoreRoot);
|
|
1409
1636
|
run("bash", ["-lc", `tar -xzf ${JSON.stringify(archivePath)} -C ${JSON.stringify(restoreRoot)}`]);
|
|
1410
1637
|
writeFile(DEPLOY_ENV, fs.readFileSync(path.join(restoreRoot, "deploy.env"), "utf8"), 384);
|
|
@@ -1421,11 +1648,16 @@ async function cmdRestore(archivePath) {
|
|
|
1421
1648
|
for (const volume of STACK_VOLUMES) {
|
|
1422
1649
|
restoreDockerVolume(stackVolumeName(config, volume), path.join(restoreRoot, "volumes", volume));
|
|
1423
1650
|
}
|
|
1424
|
-
|
|
1651
|
+
logSuccess("Restore completed. Run 'envsync-deploy deploy' to start services.");
|
|
1425
1652
|
}
|
|
1426
1653
|
async function main() {
|
|
1427
|
-
const
|
|
1428
|
-
const
|
|
1654
|
+
const argv = process.argv.slice(2);
|
|
1655
|
+
const command = argv[0];
|
|
1656
|
+
const args = argv.slice(1);
|
|
1657
|
+
currentOptions = {
|
|
1658
|
+
dryRun: args.includes("--dry-run")
|
|
1659
|
+
};
|
|
1660
|
+
const positionals = args.filter((arg) => arg !== "--dry-run");
|
|
1429
1661
|
switch (command) {
|
|
1430
1662
|
case "preinstall":
|
|
1431
1663
|
await cmdPreinstall();
|
|
@@ -1440,7 +1672,7 @@ async function main() {
|
|
|
1440
1672
|
await cmdDeploy();
|
|
1441
1673
|
break;
|
|
1442
1674
|
case "health":
|
|
1443
|
-
await cmdHealth(
|
|
1675
|
+
await cmdHealth(positionals[0] === "--json");
|
|
1444
1676
|
break;
|
|
1445
1677
|
case "upgrade":
|
|
1446
1678
|
await cmdUpgrade();
|
|
@@ -1452,10 +1684,12 @@ async function main() {
|
|
|
1452
1684
|
await cmdBackup();
|
|
1453
1685
|
break;
|
|
1454
1686
|
case "restore":
|
|
1455
|
-
await cmdRestore(
|
|
1687
|
+
await cmdRestore(positionals[0] ?? "");
|
|
1456
1688
|
break;
|
|
1457
1689
|
default:
|
|
1458
|
-
console.log(
|
|
1690
|
+
console.log(
|
|
1691
|
+
"Usage: envsync-deploy <preinstall|setup|bootstrap|deploy|health|upgrade|upgrade-deps|backup|restore> [--dry-run]"
|
|
1692
|
+
);
|
|
1459
1693
|
process.exit(command ? 1 : 0);
|
|
1460
1694
|
}
|
|
1461
1695
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@envsync-cloud/deploy-cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
4
4
|
"description": "CLI for self-hosted EnvSync deployment on Docker Swarm",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -39,6 +39,9 @@
|
|
|
39
39
|
"cli",
|
|
40
40
|
"secrets"
|
|
41
41
|
],
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"chalk": "^5.6.2"
|
|
44
|
+
},
|
|
42
45
|
"devDependencies": {
|
|
43
46
|
"tsup": "^8.5.0",
|
|
44
47
|
"typescript": "^5.9.2"
|