@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.
Files changed (3) hide show
  1. package/README.md +8 -1
  2. package/dist/index.js +153 -24
  3. 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
- " browser-otlp:",
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
- " landing-otlp-router:",
654
- ` rule: Host(\`${hosts.landing}\`) && (PathPrefix(\`/v1/traces\`) || PathPrefix(\`/v1/logs\`) || PathPrefix(\`/v1/metrics\`))`,
655
- " service: browser-otlp",
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
- " web-otlp-router:",
660
- ` rule: Host(\`${hosts.app}\`) && (PathPrefix(\`/v1/traces\`) || PathPrefix(\`/v1/logs\`) || PathPrefix(\`/v1/metrics\`))`,
661
- " service: browser-otlp",
662
- " priority: 100",
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 = kind === "web" ? `https://${hosts.app}` : `https://${hosts.landing}`;
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.s3.rule=Host(\`${hosts.s3}\`)
813
- - traefik.http.routers.s3.entrypoints=websecure
814
- - traefik.http.routers.s3.tls.certresolver=letsencrypt
815
- - traefik.http.services.s3.loadbalancer.server.port=9000
816
- - traefik.http.routers.s3-console.rule=Host(\`${hosts.s3Console}\`)
817
- - traefik.http.routers.s3-console.entrypoints=websecure
818
- - traefik.http.routers.s3-console.tls.certresolver=letsencrypt
819
- - traefik.http.services.s3-console.loadbalancer.server.port=9001
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}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@envsync-cloud/deploy-cli",
3
- "version": "0.6.9",
3
+ "version": "0.6.11",
4
4
  "description": "CLI for self-hosted EnvSync deployment on Docker Swarm",
5
5
  "type": "module",
6
6
  "bin": {