@envsync-cloud/deploy-cli 0.6.10 → 0.6.12

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 +138 -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
@@ -25,6 +25,7 @@ var KEYCLOAK_REALM_FILE = "/opt/envsync/deploy/keycloak-realm.envsync.json";
25
25
  var NGINX_WEB_CONF = "/opt/envsync/deploy/nginx-web.conf";
26
26
  var NGINX_LANDING_CONF = "/opt/envsync/deploy/nginx-landing.conf";
27
27
  var OTEL_AGENT_CONF = "/opt/envsync/deploy/otel-agent.yaml";
28
+ var CLICKSTACK_CLICKHOUSE_CONF = "/opt/envsync/deploy/clickhouse-listen.xml";
28
29
  var INTERNAL_CONFIG_JSON = "/opt/envsync/deploy/config.json";
29
30
  var STACK_VOLUMES = [
30
31
  "postgres_data",
@@ -611,6 +612,19 @@ function renderTraefikDynamicConfig(config) {
611
612
  " stsSeconds: 31536000",
612
613
  " gzip:",
613
614
  " compress: {}",
615
+ " otel-cors:",
616
+ " headers:",
617
+ " accessControlAllowOriginList:",
618
+ ` - https://${hosts.landing}`,
619
+ ` - https://${hosts.app}`,
620
+ " accessControlAllowMethods:",
621
+ " - POST",
622
+ " - OPTIONS",
623
+ " accessControlAllowHeaders:",
624
+ " - Content-Type",
625
+ " - Authorization",
626
+ " accessControlAllowCredentials: false",
627
+ " addVaryHeader: true",
614
628
  " services:",
615
629
  " envsync-api:",
616
630
  " weighted:",
@@ -635,7 +649,15 @@ function renderTraefikDynamicConfig(config) {
635
649
  " loadBalancer:",
636
650
  " servers:",
637
651
  " - url: http://web_nginx:8080",
638
- " browser-otlp:",
652
+ " clickstack-ui:",
653
+ " loadBalancer:",
654
+ " servers:",
655
+ " - url: http://clickstack:8080",
656
+ " clickstack-api:",
657
+ " loadBalancer:",
658
+ " servers:",
659
+ " - url: http://clickstack:8000",
660
+ " clickstack-otlp:",
639
661
  " loadBalancer:",
640
662
  " servers:",
641
663
  ` - url: http://clickstack:${config.services.clickstack_otlp_http_port}`,
@@ -650,16 +672,23 @@ function renderTraefikDynamicConfig(config) {
650
672
  " service: web",
651
673
  " entryPoints: [websecure]",
652
674
  " tls: {}",
653
- " landing-otlp-router:",
654
- ` rule: Host(\`${hosts.landing}\`) && (PathPrefix(\`/v1/traces\`) || PathPrefix(\`/v1/logs\`) || PathPrefix(\`/v1/metrics\`))`,
655
- " service: browser-otlp",
675
+ " obs-otlp-router:",
676
+ ` rule: Host(\`${hosts.obs}\`) && (PathPrefix(\`/v1/traces\`) || PathPrefix(\`/v1/logs\`) || PathPrefix(\`/v1/metrics\`))`,
677
+ " service: clickstack-otlp",
678
+ " middlewares: [otel-cors]",
656
679
  " priority: 100",
657
680
  " entryPoints: [websecure]",
658
681
  " 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",
682
+ " obs-api-router:",
683
+ ` rule: Host(\`${hosts.obs}\`) && PathPrefix(\`/api\`)`,
684
+ " service: clickstack-api",
685
+ " priority: 90",
686
+ " entryPoints: [websecure]",
687
+ " tls: {}",
688
+ " obs-ui-router:",
689
+ ` rule: Host(\`${hosts.obs}\`)`,
690
+ " service: clickstack-ui",
691
+ " priority: 10",
663
692
  " entryPoints: [websecure]",
664
693
  " tls: {}",
665
694
  " api-router:",
@@ -684,7 +713,7 @@ function renderNginxConf(kind) {
684
713
  }
685
714
  function renderFrontendRuntimeConfig(config, kind) {
686
715
  const hosts = domainMap(config.domain.root_domain);
687
- const otelEndpoint = kind === "web" ? `https://${hosts.app}` : `https://${hosts.landing}`;
716
+ const otelEndpoint = `https://${hosts.obs}`;
688
717
  return `window.__ENVSYNC_RUNTIME_CONFIG__ = ${JSON.stringify({
689
718
  apiBaseUrl: `https://${hosts.api}`,
690
719
  appBaseUrl: `https://${hosts.app}`,
@@ -729,10 +758,23 @@ function renderOtelAgentConfig(config) {
729
758
  " exporters: [otlphttp/clickstack]"
730
759
  ].join("\n") + "\n";
731
760
  }
761
+ function renderClickstackClickHouseConfig() {
762
+ return [
763
+ "<clickhouse>",
764
+ " <listen_host>0.0.0.0</listen_host>",
765
+ " <listen_try>1</listen_try>",
766
+ "</clickhouse>"
767
+ ].join("\n") + "\n";
768
+ }
732
769
  function renderStack(config, runtimeEnv, mode) {
733
770
  const hosts = domainMap(config.domain.root_domain);
734
771
  const includeRuntimeInfra = mode !== "base";
735
772
  const includeAppServices = mode === "full";
773
+ const stackName = config.services.stack_name;
774
+ const s3RouterName = `${stackName}-s3-router`;
775
+ const s3ServiceName = `${stackName}-s3-service`;
776
+ const s3ConsoleRouterName = `${stackName}-s3-console-router`;
777
+ const s3ConsoleServiceName = `${stackName}-s3-console-service`;
736
778
  const apiEnvironment = {
737
779
  ...runtimeEnv,
738
780
  OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-agent:4318",
@@ -809,16 +851,16 @@ ${renderEnvList({
809
851
  deploy:
810
852
  labels:
811
853
  - 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
854
+ - traefik.http.routers.${s3RouterName}.rule=Host(\`${hosts.s3}\`)
855
+ - traefik.http.routers.${s3RouterName}.entrypoints=websecure
856
+ - traefik.http.routers.${s3RouterName}.tls.certresolver=letsencrypt
857
+ - traefik.http.routers.${s3RouterName}.service=${s3ServiceName}
858
+ - traefik.http.services.${s3ServiceName}.loadbalancer.server.port=9000
859
+ - traefik.http.routers.${s3ConsoleRouterName}.rule=Host(\`${hosts.s3Console}\`)
860
+ - traefik.http.routers.${s3ConsoleRouterName}.entrypoints=websecure
861
+ - traefik.http.routers.${s3ConsoleRouterName}.tls.certresolver=letsencrypt
862
+ - traefik.http.routers.${s3ConsoleRouterName}.service=${s3ConsoleServiceName}
863
+ - traefik.http.services.${s3ConsoleServiceName}.loadbalancer.server.port=9001
822
864
 
823
865
  keycloak_db:
824
866
  image: postgres:17
@@ -911,18 +953,26 @@ ${renderEnvList({
911
953
 
912
954
  clickstack:
913
955
  image: ${config.images.clickstack}
956
+ environment:
957
+ ${renderEnvList({
958
+ HYPERDX_APP_URL: `https://${hosts.obs}`,
959
+ HYPERDX_APP_PORT: "443",
960
+ HYPERDX_API_URL: `https://${hosts.obs}`,
961
+ HYPERDX_API_PORT: "443",
962
+ FRONTEND_URL: `https://${hosts.obs}`
963
+ })}
914
964
  volumes:
915
965
  - clickstack_data:/data/db
916
966
  - clickstack_ch_data:/var/lib/clickhouse
917
967
  - clickstack_ch_logs:/var/log/clickhouse-server
968
+ - ${CLICKSTACK_CLICKHOUSE_CONF}:/etc/clickhouse-server/config.d/envsync-listen-host.xml:ro
918
969
  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
970
+ healthcheck:
971
+ test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1:8080 || exit 1"]
972
+ interval: 30s
973
+ timeout: 10s
974
+ retries: 10
975
+ start_period: 180s
926
976
 
927
977
  otel-agent:
928
978
  image: ${config.images.otel_agent}
@@ -992,6 +1042,7 @@ function writeDeployArtifacts(config, generated) {
992
1042
  writeFileMaybe(NGINX_WEB_CONF, renderNginxConf("web"));
993
1043
  writeFileMaybe(NGINX_LANDING_CONF, renderNginxConf("landing"));
994
1044
  writeFileMaybe(OTEL_AGENT_CONF, renderOtelAgentConfig(config));
1045
+ writeFileMaybe(CLICKSTACK_CLICKHOUSE_CONF, renderClickstackClickHouseConfig());
995
1046
  logSuccess(currentOptions.dryRun ? "Deploy artifacts previewed" : "Deploy artifacts written");
996
1047
  }
997
1048
  function saveDesiredConfig(config) {
@@ -1265,6 +1316,32 @@ function runBootstrapInit(config) {
1265
1316
  openfgaModelId: result.openfgaModelId
1266
1317
  };
1267
1318
  }
1319
+ function runClickstackBootstrap(config) {
1320
+ logStep("Running ClickStack self-host bootstrap");
1321
+ if (currentOptions.dryRun) {
1322
+ logDryRun("Would bootstrap ClickStack sources and dashboards");
1323
+ logCommand("node", [path.join(REPO_ROOT, "scripts/bootstrap-clickstack-selfhost.mjs")]);
1324
+ return;
1325
+ }
1326
+ const deadline = Date.now() + 180 * 1e3;
1327
+ let lastError = "unknown error";
1328
+ while (Date.now() < deadline) {
1329
+ try {
1330
+ run("node", [path.join(REPO_ROOT, "scripts/bootstrap-clickstack-selfhost.mjs")], {
1331
+ env: {
1332
+ ENVSYNC_STACK_NAME: config.services.stack_name,
1333
+ ENVSYNC_ROOT_DOMAIN: config.domain.root_domain
1334
+ }
1335
+ });
1336
+ logSuccess("ClickStack self-host bootstrap completed");
1337
+ return;
1338
+ } catch (error) {
1339
+ lastError = error instanceof Error ? error.message : String(error);
1340
+ sleepSeconds(3);
1341
+ }
1342
+ }
1343
+ throw new Error(`Timed out bootstrapping ClickStack: ${lastError}`);
1344
+ }
1268
1345
  function parseBootstrapInitJson(output) {
1269
1346
  const trimmed = output.trim();
1270
1347
  if (!trimmed) {
@@ -1591,7 +1668,20 @@ async function cmdBootstrap() {
1591
1668
  waitForHttpService(config, "keycloak management readiness", "http://keycloak:9000/health/ready", 180);
1592
1669
  waitForHttpService(config, "openfga", "http://openfga:8090/stores");
1593
1670
  waitForTcpService(config, "minikms", "minikms", 50051);
1671
+ waitForHttpService(config, "clickstack ui", "http://clickstack:8080", 180);
1594
1672
  const initResult = runBootstrapInit(config);
1673
+ const persistedGenerated = normalizeGeneratedState({
1674
+ openfga: {
1675
+ store_id: initResult.openfgaStoreId,
1676
+ model_id: initResult.openfgaModelId
1677
+ },
1678
+ secrets: nextGenerated.secrets,
1679
+ bootstrap: nextGenerated.bootstrap
1680
+ });
1681
+ if (!currentOptions.dryRun) {
1682
+ writeDeployArtifacts(config, persistedGenerated);
1683
+ }
1684
+ runClickstackBootstrap(config);
1595
1685
  if (currentOptions.dryRun) {
1596
1686
  logDryRun("Skipping generated OpenFGA ID persistence in preview mode");
1597
1687
  logSuccess("Bootstrap dry-run completed");
@@ -1654,6 +1744,7 @@ async function cmdHealth(asJson) {
1654
1744
  const hosts = domainMap(config.domain.root_domain);
1655
1745
  const services = listStackServices(config);
1656
1746
  const stackName = config.services.stack_name;
1747
+ const traefikDynamic = exists(TRAEFIK_DYNAMIC_FILE) ? fs.readFileSync(TRAEFIK_DYNAMIC_FILE, "utf8") : "";
1657
1748
  const bootstrapServices = {
1658
1749
  postgres: serviceHealth(services, `${stackName}_postgres`),
1659
1750
  redis: serviceHealth(services, `${stackName}_redis`),
@@ -1673,6 +1764,25 @@ async function cmdHealth(asJson) {
1673
1764
  web: serviceHealth(services, `${stackName}_web_nginx`),
1674
1765
  landing: serviceHealth(services, `${stackName}_landing_nginx`)
1675
1766
  },
1767
+ observability: {
1768
+ service: serviceHealth(services, `${stackName}_clickstack`),
1769
+ obs_ui: {
1770
+ url: `https://${hosts.obs}`,
1771
+ configured: traefikDynamic.includes("obs-ui-router")
1772
+ },
1773
+ obs_api: {
1774
+ url: `https://${hosts.obs}/api`,
1775
+ configured: traefikDynamic.includes("obs-api-router")
1776
+ },
1777
+ obs_otlp: {
1778
+ url: `https://${hosts.obs}/v1/traces`,
1779
+ configured: traefikDynamic.includes("obs-otlp-router")
1780
+ },
1781
+ frontend_otel_endpoint: {
1782
+ web: `https://${hosts.obs}`,
1783
+ landing: `https://${hosts.obs}`
1784
+ }
1785
+ },
1676
1786
  public: {
1677
1787
  landing: `https://${hosts.landing}`,
1678
1788
  app: `https://${hosts.app}`,
@@ -1794,6 +1904,7 @@ async function cmdBackup() {
1794
1904
  writeFile(path.join(staged, "traefik-dynamic.yaml"), fs.readFileSync(TRAEFIK_DYNAMIC_FILE, "utf8"));
1795
1905
  writeFile(path.join(staged, "keycloak-realm.envsync.json"), fs.readFileSync(KEYCLOAK_REALM_FILE, "utf8"));
1796
1906
  writeFile(path.join(staged, "otel-agent.yaml"), fs.readFileSync(OTEL_AGENT_CONF, "utf8"));
1907
+ writeFile(path.join(staged, "clickhouse-listen.xml"), fs.readFileSync(CLICKSTACK_CLICKHOUSE_CONF, "utf8"));
1797
1908
  const volumesDir = path.join(staged, "volumes");
1798
1909
  for (const volume of STACK_VOLUMES) {
1799
1910
  backupDockerVolume(stackVolumeName(config, volume), path.join(volumesDir, volume));
@@ -1834,6 +1945,7 @@ async function cmdRestore(archivePath) {
1834
1945
  writeFile(TRAEFIK_DYNAMIC_FILE, fs.readFileSync(path.join(restoreRoot, "traefik-dynamic.yaml"), "utf8"));
1835
1946
  writeFile(KEYCLOAK_REALM_FILE, fs.readFileSync(path.join(restoreRoot, "keycloak-realm.envsync.json"), "utf8"));
1836
1947
  writeFile(OTEL_AGENT_CONF, fs.readFileSync(path.join(restoreRoot, "otel-agent.yaml"), "utf8"));
1948
+ writeFile(CLICKSTACK_CLICKHOUSE_CONF, fs.readFileSync(path.join(restoreRoot, "clickhouse-listen.xml"), "utf8"));
1837
1949
  const config = loadConfig();
1838
1950
  for (const volume of STACK_VOLUMES) {
1839
1951
  restoreDockerVolume(stackVolumeName(config, volume), path.join(restoreRoot, "volumes", volume));
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.12",
4
4
  "description": "CLI for self-hosted EnvSync deployment on Docker Swarm",
5
5
  "type": "module",
6
6
  "bin": {