@envsync-cloud/deploy-cli 0.6.9 → 0.6.11
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 +8 -1
- package/dist/index.js +153 -24
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -81,9 +81,16 @@ npx @envsync-cloud/deploy-cli deploy
|
|
|
81
81
|
|
|
82
82
|
The staged flow is:
|
|
83
83
|
- `setup` writes desired config
|
|
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
|
|
84
|
+
- `bootstrap` resets the existing EnvSync deployment, then starts base infra, runs OpenFGA and miniKMS migrations, starts runtime infra, initializes ClickStack sources and dashboards, and persists generated runtime env state
|
|
85
85
|
- `deploy` starts the pending API and frontend services
|
|
86
86
|
|
|
87
|
+
Self-hosted observability routing is:
|
|
88
|
+
- `https://obs.<root-domain>/` for ClickStack UI
|
|
89
|
+
- `https://obs.<root-domain>/api/...` for ClickStack API
|
|
90
|
+
- `https://obs.<root-domain>/v1/{traces,logs,metrics}` for browser OTLP
|
|
91
|
+
|
|
92
|
+
Both frontends receive `otelEndpoint = https://obs.<root-domain>` in the generated `runtime-config.js`.
|
|
93
|
+
|
|
87
94
|
Check service health:
|
|
88
95
|
|
|
89
96
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -611,6 +611,19 @@ function renderTraefikDynamicConfig(config) {
|
|
|
611
611
|
" stsSeconds: 31536000",
|
|
612
612
|
" gzip:",
|
|
613
613
|
" compress: {}",
|
|
614
|
+
" otel-cors:",
|
|
615
|
+
" headers:",
|
|
616
|
+
" accessControlAllowOriginList:",
|
|
617
|
+
` - https://${hosts.landing}`,
|
|
618
|
+
` - https://${hosts.app}`,
|
|
619
|
+
" accessControlAllowMethods:",
|
|
620
|
+
" - POST",
|
|
621
|
+
" - OPTIONS",
|
|
622
|
+
" accessControlAllowHeaders:",
|
|
623
|
+
" - Content-Type",
|
|
624
|
+
" - Authorization",
|
|
625
|
+
" accessControlAllowCredentials: false",
|
|
626
|
+
" addVaryHeader: true",
|
|
614
627
|
" services:",
|
|
615
628
|
" envsync-api:",
|
|
616
629
|
" weighted:",
|
|
@@ -635,7 +648,15 @@ function renderTraefikDynamicConfig(config) {
|
|
|
635
648
|
" loadBalancer:",
|
|
636
649
|
" servers:",
|
|
637
650
|
" - url: http://web_nginx:8080",
|
|
638
|
-
"
|
|
651
|
+
" clickstack-ui:",
|
|
652
|
+
" loadBalancer:",
|
|
653
|
+
" servers:",
|
|
654
|
+
" - url: http://clickstack:8080",
|
|
655
|
+
" clickstack-api:",
|
|
656
|
+
" loadBalancer:",
|
|
657
|
+
" servers:",
|
|
658
|
+
" - url: http://clickstack:8000",
|
|
659
|
+
" clickstack-otlp:",
|
|
639
660
|
" loadBalancer:",
|
|
640
661
|
" servers:",
|
|
641
662
|
` - url: http://clickstack:${config.services.clickstack_otlp_http_port}`,
|
|
@@ -650,16 +671,23 @@ function renderTraefikDynamicConfig(config) {
|
|
|
650
671
|
" service: web",
|
|
651
672
|
" entryPoints: [websecure]",
|
|
652
673
|
" tls: {}",
|
|
653
|
-
"
|
|
654
|
-
` rule: Host(\`${hosts.
|
|
655
|
-
" service:
|
|
674
|
+
" obs-otlp-router:",
|
|
675
|
+
` rule: Host(\`${hosts.obs}\`) && (PathPrefix(\`/v1/traces\`) || PathPrefix(\`/v1/logs\`) || PathPrefix(\`/v1/metrics\`))`,
|
|
676
|
+
" service: clickstack-otlp",
|
|
677
|
+
" middlewares: [otel-cors]",
|
|
656
678
|
" priority: 100",
|
|
657
679
|
" entryPoints: [websecure]",
|
|
658
680
|
" tls: {}",
|
|
659
|
-
"
|
|
660
|
-
` rule: Host(\`${hosts.
|
|
661
|
-
" service:
|
|
662
|
-
" priority:
|
|
681
|
+
" obs-api-router:",
|
|
682
|
+
` rule: Host(\`${hosts.obs}\`) && PathPrefix(\`/api\`)`,
|
|
683
|
+
" service: clickstack-api",
|
|
684
|
+
" priority: 90",
|
|
685
|
+
" entryPoints: [websecure]",
|
|
686
|
+
" tls: {}",
|
|
687
|
+
" obs-ui-router:",
|
|
688
|
+
` rule: Host(\`${hosts.obs}\`)`,
|
|
689
|
+
" service: clickstack-ui",
|
|
690
|
+
" priority: 10",
|
|
663
691
|
" entryPoints: [websecure]",
|
|
664
692
|
" tls: {}",
|
|
665
693
|
" api-router:",
|
|
@@ -684,7 +712,7 @@ function renderNginxConf(kind) {
|
|
|
684
712
|
}
|
|
685
713
|
function renderFrontendRuntimeConfig(config, kind) {
|
|
686
714
|
const hosts = domainMap(config.domain.root_domain);
|
|
687
|
-
const otelEndpoint =
|
|
715
|
+
const otelEndpoint = `https://${hosts.obs}`;
|
|
688
716
|
return `window.__ENVSYNC_RUNTIME_CONFIG__ = ${JSON.stringify({
|
|
689
717
|
apiBaseUrl: `https://${hosts.api}`,
|
|
690
718
|
appBaseUrl: `https://${hosts.app}`,
|
|
@@ -733,6 +761,11 @@ function renderStack(config, runtimeEnv, mode) {
|
|
|
733
761
|
const hosts = domainMap(config.domain.root_domain);
|
|
734
762
|
const includeRuntimeInfra = mode !== "base";
|
|
735
763
|
const includeAppServices = mode === "full";
|
|
764
|
+
const stackName = config.services.stack_name;
|
|
765
|
+
const s3RouterName = `${stackName}-s3-router`;
|
|
766
|
+
const s3ServiceName = `${stackName}-s3-service`;
|
|
767
|
+
const s3ConsoleRouterName = `${stackName}-s3-console-router`;
|
|
768
|
+
const s3ConsoleServiceName = `${stackName}-s3-console-service`;
|
|
736
769
|
const apiEnvironment = {
|
|
737
770
|
...runtimeEnv,
|
|
738
771
|
OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-agent:4318",
|
|
@@ -809,14 +842,16 @@ ${renderEnvList({
|
|
|
809
842
|
deploy:
|
|
810
843
|
labels:
|
|
811
844
|
- traefik.enable=true
|
|
812
|
-
- traefik.http.routers.
|
|
813
|
-
- traefik.http.routers.
|
|
814
|
-
- traefik.http.routers.
|
|
815
|
-
- traefik.http.
|
|
816
|
-
- traefik.http.
|
|
817
|
-
- traefik.http.routers.
|
|
818
|
-
- traefik.http.routers.
|
|
819
|
-
- traefik.http.
|
|
845
|
+
- traefik.http.routers.${s3RouterName}.rule=Host(\`${hosts.s3}\`)
|
|
846
|
+
- traefik.http.routers.${s3RouterName}.entrypoints=websecure
|
|
847
|
+
- traefik.http.routers.${s3RouterName}.tls.certresolver=letsencrypt
|
|
848
|
+
- traefik.http.routers.${s3RouterName}.service=${s3ServiceName}
|
|
849
|
+
- traefik.http.services.${s3ServiceName}.loadbalancer.server.port=9000
|
|
850
|
+
- traefik.http.routers.${s3ConsoleRouterName}.rule=Host(\`${hosts.s3Console}\`)
|
|
851
|
+
- traefik.http.routers.${s3ConsoleRouterName}.entrypoints=websecure
|
|
852
|
+
- traefik.http.routers.${s3ConsoleRouterName}.tls.certresolver=letsencrypt
|
|
853
|
+
- traefik.http.routers.${s3ConsoleRouterName}.service=${s3ConsoleServiceName}
|
|
854
|
+
- traefik.http.services.${s3ConsoleServiceName}.loadbalancer.server.port=9001
|
|
820
855
|
|
|
821
856
|
keycloak_db:
|
|
822
857
|
image: postgres:17
|
|
@@ -909,18 +944,19 @@ ${renderEnvList({
|
|
|
909
944
|
|
|
910
945
|
clickstack:
|
|
911
946
|
image: ${config.images.clickstack}
|
|
947
|
+
environment:
|
|
948
|
+
${renderEnvList({
|
|
949
|
+
HYPERDX_APP_URL: `https://${hosts.obs}`,
|
|
950
|
+
HYPERDX_APP_PORT: "443",
|
|
951
|
+
HYPERDX_API_URL: `https://${hosts.obs}`,
|
|
952
|
+
HYPERDX_API_PORT: "443",
|
|
953
|
+
FRONTEND_URL: `https://${hosts.obs}`
|
|
954
|
+
})}
|
|
912
955
|
volumes:
|
|
913
956
|
- clickstack_data:/data/db
|
|
914
957
|
- clickstack_ch_data:/var/lib/clickhouse
|
|
915
958
|
- clickstack_ch_logs:/var/log/clickhouse-server
|
|
916
959
|
networks: [envsync]
|
|
917
|
-
deploy:
|
|
918
|
-
labels:
|
|
919
|
-
- traefik.enable=true
|
|
920
|
-
- traefik.http.routers.obs.rule=Host(\`${hosts.obs}\`)
|
|
921
|
-
- traefik.http.routers.obs.entrypoints=websecure
|
|
922
|
-
- traefik.http.routers.obs.tls.certresolver=letsencrypt
|
|
923
|
-
- traefik.http.services.obs.loadbalancer.server.port=8080
|
|
924
960
|
|
|
925
961
|
otel-agent:
|
|
926
962
|
image: ${config.images.otel_agent}
|
|
@@ -1136,6 +1172,51 @@ function waitForHttpService(config, label, url, timeoutSeconds = 120) {
|
|
|
1136
1172
|
}
|
|
1137
1173
|
throw new Error(`Timed out waiting for ${label} at ${url}`);
|
|
1138
1174
|
}
|
|
1175
|
+
function serviceContainerId(config, serviceName) {
|
|
1176
|
+
const output = tryRun(
|
|
1177
|
+
"docker",
|
|
1178
|
+
[
|
|
1179
|
+
"ps",
|
|
1180
|
+
"--filter",
|
|
1181
|
+
`label=com.docker.swarm.service.name=${config.services.stack_name}_${serviceName}`,
|
|
1182
|
+
"--format",
|
|
1183
|
+
"{{.ID}}"
|
|
1184
|
+
],
|
|
1185
|
+
{ quiet: true }
|
|
1186
|
+
);
|
|
1187
|
+
return output.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? "";
|
|
1188
|
+
}
|
|
1189
|
+
function waitForContainerHealth(config, serviceName, label, timeoutSeconds = 180) {
|
|
1190
|
+
if (currentOptions.dryRun) {
|
|
1191
|
+
logDryRun(`Would wait for ${label}`);
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
logStep(`Waiting for ${label}`);
|
|
1195
|
+
const deadline = Date.now() + timeoutSeconds * 1e3;
|
|
1196
|
+
while (Date.now() < deadline) {
|
|
1197
|
+
const containerId = serviceContainerId(config, serviceName);
|
|
1198
|
+
if (!containerId) {
|
|
1199
|
+
sleepSeconds(2);
|
|
1200
|
+
continue;
|
|
1201
|
+
}
|
|
1202
|
+
const health = tryRun(
|
|
1203
|
+
"docker",
|
|
1204
|
+
[
|
|
1205
|
+
"inspect",
|
|
1206
|
+
"--format",
|
|
1207
|
+
"{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}",
|
|
1208
|
+
containerId
|
|
1209
|
+
],
|
|
1210
|
+
{ quiet: true }
|
|
1211
|
+
).trim();
|
|
1212
|
+
if (health === "healthy" || health === "running") {
|
|
1213
|
+
logSuccess(`${label} is ready`);
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
sleepSeconds(2);
|
|
1217
|
+
}
|
|
1218
|
+
throw new Error(`Timed out waiting for ${label}`);
|
|
1219
|
+
}
|
|
1139
1220
|
function runOpenFgaMigrate(config, runtimeEnv) {
|
|
1140
1221
|
logStep("Running OpenFGA datastore migrations");
|
|
1141
1222
|
if (currentOptions.dryRun) {
|
|
@@ -1263,6 +1344,21 @@ function runBootstrapInit(config) {
|
|
|
1263
1344
|
openfgaModelId: result.openfgaModelId
|
|
1264
1345
|
};
|
|
1265
1346
|
}
|
|
1347
|
+
function runClickstackBootstrap(config) {
|
|
1348
|
+
logStep("Running ClickStack self-host bootstrap");
|
|
1349
|
+
if (currentOptions.dryRun) {
|
|
1350
|
+
logDryRun("Would bootstrap ClickStack sources and dashboards");
|
|
1351
|
+
logCommand("node", [path.join(REPO_ROOT, "scripts/bootstrap-clickstack-selfhost.mjs")]);
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
run("node", [path.join(REPO_ROOT, "scripts/bootstrap-clickstack-selfhost.mjs")], {
|
|
1355
|
+
env: {
|
|
1356
|
+
ENVSYNC_STACK_NAME: config.services.stack_name,
|
|
1357
|
+
ENVSYNC_ROOT_DOMAIN: config.domain.root_domain
|
|
1358
|
+
}
|
|
1359
|
+
});
|
|
1360
|
+
logSuccess("ClickStack self-host bootstrap completed");
|
|
1361
|
+
}
|
|
1266
1362
|
function parseBootstrapInitJson(output) {
|
|
1267
1363
|
const trimmed = output.trim();
|
|
1268
1364
|
if (!trimmed) {
|
|
@@ -1589,7 +1685,20 @@ async function cmdBootstrap() {
|
|
|
1589
1685
|
waitForHttpService(config, "keycloak management readiness", "http://keycloak:9000/health/ready", 180);
|
|
1590
1686
|
waitForHttpService(config, "openfga", "http://openfga:8090/stores");
|
|
1591
1687
|
waitForTcpService(config, "minikms", "minikms", 50051);
|
|
1688
|
+
waitForContainerHealth(config, "clickstack", "clickstack container health");
|
|
1592
1689
|
const initResult = runBootstrapInit(config);
|
|
1690
|
+
const persistedGenerated = normalizeGeneratedState({
|
|
1691
|
+
openfga: {
|
|
1692
|
+
store_id: initResult.openfgaStoreId,
|
|
1693
|
+
model_id: initResult.openfgaModelId
|
|
1694
|
+
},
|
|
1695
|
+
secrets: nextGenerated.secrets,
|
|
1696
|
+
bootstrap: nextGenerated.bootstrap
|
|
1697
|
+
});
|
|
1698
|
+
if (!currentOptions.dryRun) {
|
|
1699
|
+
writeDeployArtifacts(config, persistedGenerated);
|
|
1700
|
+
}
|
|
1701
|
+
runClickstackBootstrap(config);
|
|
1593
1702
|
if (currentOptions.dryRun) {
|
|
1594
1703
|
logDryRun("Skipping generated OpenFGA ID persistence in preview mode");
|
|
1595
1704
|
logSuccess("Bootstrap dry-run completed");
|
|
@@ -1652,6 +1761,7 @@ async function cmdHealth(asJson) {
|
|
|
1652
1761
|
const hosts = domainMap(config.domain.root_domain);
|
|
1653
1762
|
const services = listStackServices(config);
|
|
1654
1763
|
const stackName = config.services.stack_name;
|
|
1764
|
+
const traefikDynamic = exists(TRAEFIK_DYNAMIC_FILE) ? fs.readFileSync(TRAEFIK_DYNAMIC_FILE, "utf8") : "";
|
|
1655
1765
|
const bootstrapServices = {
|
|
1656
1766
|
postgres: serviceHealth(services, `${stackName}_postgres`),
|
|
1657
1767
|
redis: serviceHealth(services, `${stackName}_redis`),
|
|
@@ -1671,6 +1781,25 @@ async function cmdHealth(asJson) {
|
|
|
1671
1781
|
web: serviceHealth(services, `${stackName}_web_nginx`),
|
|
1672
1782
|
landing: serviceHealth(services, `${stackName}_landing_nginx`)
|
|
1673
1783
|
},
|
|
1784
|
+
observability: {
|
|
1785
|
+
service: serviceHealth(services, `${stackName}_clickstack`),
|
|
1786
|
+
obs_ui: {
|
|
1787
|
+
url: `https://${hosts.obs}`,
|
|
1788
|
+
configured: traefikDynamic.includes("obs-ui-router")
|
|
1789
|
+
},
|
|
1790
|
+
obs_api: {
|
|
1791
|
+
url: `https://${hosts.obs}/api`,
|
|
1792
|
+
configured: traefikDynamic.includes("obs-api-router")
|
|
1793
|
+
},
|
|
1794
|
+
obs_otlp: {
|
|
1795
|
+
url: `https://${hosts.obs}/v1/traces`,
|
|
1796
|
+
configured: traefikDynamic.includes("obs-otlp-router")
|
|
1797
|
+
},
|
|
1798
|
+
frontend_otel_endpoint: {
|
|
1799
|
+
web: `https://${hosts.obs}`,
|
|
1800
|
+
landing: `https://${hosts.obs}`
|
|
1801
|
+
}
|
|
1802
|
+
},
|
|
1674
1803
|
public: {
|
|
1675
1804
|
landing: `https://${hosts.landing}`,
|
|
1676
1805
|
app: `https://${hosts.app}`,
|