@envsync-cloud/deploy-cli 0.6.10 → 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 -26
- 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,16 +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.routers.
|
|
816
|
-
- traefik.http.services.
|
|
817
|
-
- traefik.http.routers.
|
|
818
|
-
- traefik.http.routers.
|
|
819
|
-
- traefik.http.routers.
|
|
820
|
-
- traefik.http.routers.
|
|
821
|
-
- traefik.http.services.
|
|
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
|
|
822
855
|
|
|
823
856
|
keycloak_db:
|
|
824
857
|
image: postgres:17
|
|
@@ -911,18 +944,19 @@ ${renderEnvList({
|
|
|
911
944
|
|
|
912
945
|
clickstack:
|
|
913
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
|
+
})}
|
|
914
955
|
volumes:
|
|
915
956
|
- clickstack_data:/data/db
|
|
916
957
|
- clickstack_ch_data:/var/lib/clickhouse
|
|
917
958
|
- clickstack_ch_logs:/var/log/clickhouse-server
|
|
918
959
|
networks: [envsync]
|
|
919
|
-
deploy:
|
|
920
|
-
labels:
|
|
921
|
-
- traefik.enable=true
|
|
922
|
-
- traefik.http.routers.obs.rule=Host(\`${hosts.obs}\`)
|
|
923
|
-
- traefik.http.routers.obs.entrypoints=websecure
|
|
924
|
-
- traefik.http.routers.obs.tls.certresolver=letsencrypt
|
|
925
|
-
- traefik.http.services.obs.loadbalancer.server.port=8080
|
|
926
960
|
|
|
927
961
|
otel-agent:
|
|
928
962
|
image: ${config.images.otel_agent}
|
|
@@ -1138,6 +1172,51 @@ function waitForHttpService(config, label, url, timeoutSeconds = 120) {
|
|
|
1138
1172
|
}
|
|
1139
1173
|
throw new Error(`Timed out waiting for ${label} at ${url}`);
|
|
1140
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
|
+
}
|
|
1141
1220
|
function runOpenFgaMigrate(config, runtimeEnv) {
|
|
1142
1221
|
logStep("Running OpenFGA datastore migrations");
|
|
1143
1222
|
if (currentOptions.dryRun) {
|
|
@@ -1265,6 +1344,21 @@ function runBootstrapInit(config) {
|
|
|
1265
1344
|
openfgaModelId: result.openfgaModelId
|
|
1266
1345
|
};
|
|
1267
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
|
+
}
|
|
1268
1362
|
function parseBootstrapInitJson(output) {
|
|
1269
1363
|
const trimmed = output.trim();
|
|
1270
1364
|
if (!trimmed) {
|
|
@@ -1591,7 +1685,20 @@ async function cmdBootstrap() {
|
|
|
1591
1685
|
waitForHttpService(config, "keycloak management readiness", "http://keycloak:9000/health/ready", 180);
|
|
1592
1686
|
waitForHttpService(config, "openfga", "http://openfga:8090/stores");
|
|
1593
1687
|
waitForTcpService(config, "minikms", "minikms", 50051);
|
|
1688
|
+
waitForContainerHealth(config, "clickstack", "clickstack container health");
|
|
1594
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);
|
|
1595
1702
|
if (currentOptions.dryRun) {
|
|
1596
1703
|
logDryRun("Skipping generated OpenFGA ID persistence in preview mode");
|
|
1597
1704
|
logSuccess("Bootstrap dry-run completed");
|
|
@@ -1654,6 +1761,7 @@ async function cmdHealth(asJson) {
|
|
|
1654
1761
|
const hosts = domainMap(config.domain.root_domain);
|
|
1655
1762
|
const services = listStackServices(config);
|
|
1656
1763
|
const stackName = config.services.stack_name;
|
|
1764
|
+
const traefikDynamic = exists(TRAEFIK_DYNAMIC_FILE) ? fs.readFileSync(TRAEFIK_DYNAMIC_FILE, "utf8") : "";
|
|
1657
1765
|
const bootstrapServices = {
|
|
1658
1766
|
postgres: serviceHealth(services, `${stackName}_postgres`),
|
|
1659
1767
|
redis: serviceHealth(services, `${stackName}_redis`),
|
|
@@ -1673,6 +1781,25 @@ async function cmdHealth(asJson) {
|
|
|
1673
1781
|
web: serviceHealth(services, `${stackName}_web_nginx`),
|
|
1674
1782
|
landing: serviceHealth(services, `${stackName}_landing_nginx`)
|
|
1675
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
|
+
},
|
|
1676
1803
|
public: {
|
|
1677
1804
|
landing: `https://${hosts.landing}`,
|
|
1678
1805
|
app: `https://${hosts.app}`,
|