@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.
Files changed (3) hide show
  1. package/README.md +8 -1
  2. package/dist/index.js +153 -26
  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,16 +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.routers.s3.service=s3
816
- - traefik.http.services.s3.loadbalancer.server.port=9000
817
- - traefik.http.routers.s3-console.rule=Host(\`${hosts.s3Console}\`)
818
- - traefik.http.routers.s3-console.entrypoints=websecure
819
- - traefik.http.routers.s3-console.tls.certresolver=letsencrypt
820
- - traefik.http.routers.s3-console.service=s3-console
821
- - 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
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}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@envsync-cloud/deploy-cli",
3
- "version": "0.6.10",
3
+ "version": "0.6.11",
4
4
  "description": "CLI for self-hosted EnvSync deployment on Docker Swarm",
5
5
  "type": "module",
6
6
  "bin": {