@envsync-cloud/deploy-cli 0.8.15 → 0.9.0

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 +1 -1
  2. package/dist/index.js +251 -10
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -148,4 +148,4 @@ bun run src/index.ts plan-topology /etc/envsync/deploy.yaml
148
148
  - `upgrade` and `deploy` reconcile managed versioned artifacts from `release.version`.
149
149
  - Custom image overrides are still preserved for advanced self-host setups.
150
150
  - Enterprise package publication now targets NPM Packages rather than the public npm registry.
151
- - The full deployment guide lives in [SELFHOSTING.md](/Users/bravo68web/Projects/OSS/EnvSync/monorepo/SELFHOSTING.md).
151
+ - The full deployment guide lives in [SELFHOSTING.md](../../SELFHOSTING.md).
package/dist/index.js CHANGED
@@ -4117,6 +4117,11 @@ var deployConfigSchema = external_exports.object({
4117
4117
  frontend: external_exports.object({
4118
4118
  dashboard_variant: external_exports.enum(["oss", "enterprise"]).optional(),
4119
4119
  include_manage_subtree: external_exports.boolean().optional()
4120
+ }).default({}),
4121
+ vpn: external_exports.object({
4122
+ provider: external_exports.enum(["netbird", "none"]).default("none"),
4123
+ internal_domain: external_exports.string().default("envsync.local"),
4124
+ nb_setup_key: external_exports.string().default("")
4120
4125
  }).default({})
4121
4126
  });
4122
4127
  function readDeployConfigFile(filePath) {
@@ -4475,7 +4480,9 @@ function buildRuntimeEnv2(config, generated) {
4475
4480
  OTEL_SERVICE_NAME: "envsync-api",
4476
4481
  OTEL_SDK_DISABLED: "false",
4477
4482
  CLICKSTACK_URL: publicHttpsUrl(config, hosts.obs),
4478
- KEYCLOAK_IMAGE_TAG: keycloakImageTag(config.images.keycloak)
4483
+ KEYCLOAK_IMAGE_TAG: keycloakImageTag(config.images.keycloak),
4484
+ // Is this too hacky? We go Shawn way.
4485
+ ...config.vpn?.provider && config.vpn.provider !== "none" && config.vpn.nb_setup_key ? { NB_SETUP_KEY: config.vpn.nb_setup_key } : {}
4479
4486
  };
4480
4487
  }
4481
4488
  function renderEnvFile(env) {
@@ -4789,6 +4796,104 @@ function renderNetutilsService(config) {
4789
4796
  ${ports}
4790
4797
  networks: [envsync]`;
4791
4798
  }
4799
+ var VPN_SERVICE_RENDERERS = {
4800
+ none: () => null,
4801
+ netbird: renderNetbirdService
4802
+ };
4803
+ function renderNetbirdService(config, paths, runtimeEnv) {
4804
+ const nbSetupKey = runtimeEnv.NB_SETUP_KEY ?? "";
4805
+ return {
4806
+ serviceBlock: `
4807
+ netbird_nginx:
4808
+ image: ghcr.io/envsync-cloud/netbird-nginx
4809
+ cap_add:
4810
+ - NET_ADMIN
4811
+ - SYS_ADMIN
4812
+ - SYS_RESOURCE
4813
+ networks: [envsync]
4814
+ environment:
4815
+ ${renderEnvList({ NB_SETUP_KEY: nbSetupKey })}
4816
+ volumes:
4817
+ - netbird_client:/var/lib/netbird
4818
+ - ${paths.nginxVpnConf}:/etc/nginx/nginx.conf:ro`,
4819
+ // Adding comment for readability, extra volume below. We gotta do something about too many ` bhaiya.
4820
+ extraVolumes: ["netbird_client"]
4821
+ };
4822
+ }
4823
+ function renderVpnService(config, paths, runtimeEnv) {
4824
+ const provider = config.vpn?.provider ?? "none";
4825
+ const renderer = VPN_SERVICE_RENDERERS[provider];
4826
+ if (!renderer) return null;
4827
+ return renderer(config, paths, runtimeEnv);
4828
+ }
4829
+ function renderNginxVpnConf(config) {
4830
+ const domain = config.vpn?.internal_domain ?? "envsync.local";
4831
+ return `events {}
4832
+
4833
+ stream {
4834
+ server {
4835
+ listen 5432;
4836
+ proxy_pass postgres:5432;
4837
+ }
4838
+ server {
4839
+ listen 6379;
4840
+ proxy_pass redis:6379;
4841
+ }
4842
+ server {
4843
+ listen 50051;
4844
+ proxy_pass minikms:50051;
4845
+ }
4846
+ server {
4847
+ listen 5544;
4848
+ proxy_pass minikms_db:5432;
4849
+ }
4850
+ server {
4851
+ listen 8091;
4852
+ proxy_pass openfga:8091;
4853
+ }
4854
+ }
4855
+
4856
+ http {
4857
+ server {
4858
+ listen 80;
4859
+ server_name keycloak.${domain};
4860
+ location / {
4861
+ proxy_pass http://keycloak:8080/;
4862
+ proxy_set_header Host $host;
4863
+ proxy_set_header X-Real-IP $remote_addr;
4864
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
4865
+ }
4866
+ }
4867
+ server {
4868
+ listen 80;
4869
+ server_name openfga.${domain};
4870
+ location / {
4871
+ proxy_pass http://openfga:8090/;
4872
+ proxy_set_header Host $host;
4873
+ proxy_set_header X-Real-IP $remote_addr;
4874
+ }
4875
+ }
4876
+ server {
4877
+ listen 80;
4878
+ server_name clickstack.${domain};
4879
+ location / {
4880
+ proxy_pass http://clickstack:8080/;
4881
+ proxy_set_header Host $host;
4882
+ proxy_set_header X-Real-IP $remote_addr;
4883
+ }
4884
+ }
4885
+ server {
4886
+ listen 80;
4887
+ server_name rustfs.${domain};
4888
+ location / {
4889
+ proxy_pass http://rustfs:9001/;
4890
+ proxy_set_header Host $host;
4891
+ proxy_set_header X-Real-IP $remote_addr;
4892
+ }
4893
+ }
4894
+ }
4895
+ `;
4896
+ }
4792
4897
  function renderOtelAgentConfig(config) {
4793
4898
  return [
4794
4899
  "receivers:",
@@ -4846,6 +4951,7 @@ function renderStack(config, runtimeEnv, generated, mode, paths) {
4846
4951
  const s3ConsoleRouterName = `${stackName}-s3-console-router`;
4847
4952
  const s3ConsoleServiceName = `${stackName}-s3-console-service`;
4848
4953
  const netutilsService = renderNetutilsService(config);
4954
+ const vpnResult = renderVpnService(config, paths, runtimeEnv);
4849
4955
  const apiEnvironment = {
4850
4956
  ...runtimeEnv,
4851
4957
  OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-agent:4318",
@@ -5113,7 +5219,7 @@ ${renderEnvList({
5113
5219
  ${apiLicenseVolume}
5114
5220
  networks: [envsync]
5115
5221
  deploy:
5116
- replicas: ${slotHasApiDeployment(deployment.slots.green) ? 1 : 0}` : ""}${netutilsService}
5222
+ replicas: ${slotHasApiDeployment(deployment.slots.green) ? 1 : 0}` : ""}${netutilsService}${vpnResult?.serviceBlock ?? ""}
5117
5223
 
5118
5224
  networks:
5119
5225
  envsync:
@@ -5130,6 +5236,8 @@ volumes:
5130
5236
  clickstack_data:
5131
5237
  clickstack_ch_data:
5132
5238
  clickstack_ch_logs:
5239
+ ${vpnResult?.extraVolumes.map((v) => `
5240
+ ${v}:`).join("") ?? ""}
5133
5241
 
5134
5242
  configs:
5135
5243
  keycloak_realm:
@@ -5181,6 +5289,30 @@ function validateStaticBundle(kind, targetDir) {
5181
5289
  }
5182
5290
 
5183
5291
  // src/index.ts
5292
+ var NETUTILS_EXPOSE_ALL_START_PORT = 15e3;
5293
+ var NETUTILS_EXPOSE_ALL_PORTS = [
5294
+ { service: "postgres", service_port: 5432, protocol: "tcp" },
5295
+ { service: "envsync_api_blue", service_port: 4e3, protocol: "tcp" },
5296
+ { service: "envsync_api_green", service_port: 4e3, protocol: "tcp" },
5297
+ { service: "envsync-management-api", service_port: 4001, protocol: "tcp" },
5298
+ { service: "keycloak", service_port: 8080, protocol: "tcp" },
5299
+ { service: "openfga", service_port: 8090, protocol: "tcp" },
5300
+ { service: "minikms", service_port: 50051, protocol: "tcp" },
5301
+ { service: "keycloak_db", service_port: 5432, protocol: "tcp" },
5302
+ { service: "openfga_db", service_port: 5432, protocol: "tcp" },
5303
+ { service: "minikms_db", service_port: 5432, protocol: "tcp" },
5304
+ { service: "redis", service_port: 6379, protocol: "tcp" },
5305
+ { service: "rustfs", service_port: 9e3, protocol: "tcp" },
5306
+ { service: "rustfs", service_port: 9001, protocol: "tcp" },
5307
+ { service: "clickstack", service_port: 8080, protocol: "tcp" },
5308
+ { service: "otel-agent", service_port: 4317, protocol: "tcp" },
5309
+ { service: "otel-agent", service_port: 4318, protocol: "tcp" },
5310
+ { service: "traefik", service_port: 80, protocol: "tcp" },
5311
+ { service: "traefik", service_port: 443, protocol: "tcp" },
5312
+ { service: "web_nginx", service_port: 80, protocol: "tcp" },
5313
+ { service: "landing_nginx", service_port: 80, protocol: "tcp" },
5314
+ { service: "api_maintenance", service_port: 80, protocol: "tcp" }
5315
+ ];
5184
5316
  var HOST_ROOT = process.env.ENVSYNC_HOST_ROOT ?? "/opt/envsync";
5185
5317
  var ETC_ROOT = process.env.ENVSYNC_ETC_ROOT ?? "/etc/envsync";
5186
5318
  var TRAEFIK_STATE_ROOT = process.env.ENVSYNC_TRAEFIK_STATE_ROOT ?? "/var/lib/envsync/traefik";
@@ -5207,6 +5339,7 @@ var KEYCLOAK_REALM_FILE = path3.join(DEPLOY_ROOT, "keycloak-realm.envsync.json")
5207
5339
  var NGINX_WEB_CONF = path3.join(DEPLOY_ROOT, "nginx-web.conf");
5208
5340
  var NGINX_LANDING_CONF = path3.join(DEPLOY_ROOT, "nginx-landing.conf");
5209
5341
  var NGINX_API_MAINTENANCE_CONF = path3.join(DEPLOY_ROOT, "nginx-api-maintenance.conf");
5342
+ var NGINX_VPN_CONF = path3.join(DEPLOY_ROOT, "nginx-vpn.conf");
5210
5343
  var OTEL_AGENT_CONF = path3.join(DEPLOY_ROOT, "otel-agent.yaml");
5211
5344
  var CLICKSTACK_CLICKHOUSE_CONF = path3.join(DEPLOY_ROOT, "clickhouse-listen.xml");
5212
5345
  var INTERNAL_CONFIG_JSON = path3.join(DEPLOY_ROOT, "config.json");
@@ -5224,7 +5357,8 @@ var DEPLOY_RENDER_PATHS = {
5224
5357
  otelAgentConf: OTEL_AGENT_CONF,
5225
5358
  nginxLandingConf: NGINX_LANDING_CONF,
5226
5359
  nginxWebConf: NGINX_WEB_CONF,
5227
- nginxApiMaintenanceConf: NGINX_API_MAINTENANCE_CONF
5360
+ nginxApiMaintenanceConf: NGINX_API_MAINTENANCE_CONF,
5361
+ nginxVpnConf: NGINX_VPN_CONF
5228
5362
  };
5229
5363
  var REMOVE_TARGETS = [
5230
5364
  DEPLOY_ROOT,
@@ -5386,6 +5520,69 @@ ${stderr}` : ""}`);
5386
5520
  }
5387
5521
  return (result.stdout ?? "").toString().trim();
5388
5522
  }
5523
+ function chunkByMax(items, max) {
5524
+ if (items.length === 0) return [];
5525
+ const chunks = [];
5526
+ for (let i = 0; i < items.length; i += max) {
5527
+ chunks.push(items.slice(i, i + max));
5528
+ }
5529
+ return chunks;
5530
+ }
5531
+ function formatPortRangesForIptables(ports) {
5532
+ const sortedPorts = Array.from(new Set(ports)).map((port) => parseInt(String(port), 10)).filter((port) => Number.isInteger(port) && port >= 1 && port <= 65535).sort((a, b) => a - b);
5533
+ if (sortedPorts.length === 0) return [];
5534
+ const ranges = [];
5535
+ let rangeStart = sortedPorts[0];
5536
+ let previous = sortedPorts[0];
5537
+ for (let i = 1; i < sortedPorts.length; i++) {
5538
+ const current = sortedPorts[i];
5539
+ if (current === previous + 1) {
5540
+ previous = current;
5541
+ continue;
5542
+ }
5543
+ ranges.push(rangeStart === previous ? `${rangeStart}` : `${rangeStart}:${previous}`);
5544
+ rangeStart = current;
5545
+ previous = current;
5546
+ }
5547
+ ranges.push(rangeStart === previous ? `${rangeStart}` : `${rangeStart}:${previous}`);
5548
+ return ranges;
5549
+ }
5550
+ function formatNetutilsFirewallRules(config) {
5551
+ if (!config?.netutils?.enabled || !Array.isArray(config.netutils.forwards) || config.netutils.forwards.length === 0) {
5552
+ return [];
5553
+ }
5554
+ const tcpPorts = config.netutils.forwards.filter((item) => item.protocol === "tcp").map((item) => item.host_port);
5555
+ const udpPorts = config.netutils.forwards.filter((item) => item.protocol === "udp").map((item) => item.host_port);
5556
+ const tcpRanges = formatPortRangesForIptables(tcpPorts);
5557
+ const udpRanges = formatPortRangesForIptables(udpPorts);
5558
+ if (tcpRanges.length === 0 && udpRanges.length === 0) return [];
5559
+ const lines = [];
5560
+ const addRules = (protocol, ranges) => {
5561
+ for (const chunk of chunkByMax(ranges, 15)) {
5562
+ const portSpec = chunk.join(",");
5563
+ lines.push(
5564
+ `PostUp = iptables -I DOCKER-USER -i envsync-wg -p ${protocol} -m multiport --dports ${portSpec} -j ACCEPT`
5565
+ );
5566
+ lines.push(`PostUp = iptables -A DOCKER-USER -p ${protocol} -m multiport --dports ${portSpec} -j DROP`);
5567
+ lines.push(
5568
+ `PostDown = iptables -D DOCKER-USER -p ${protocol} -m multiport --dports ${portSpec} -j DROP`
5569
+ );
5570
+ lines.push(
5571
+ `PostDown = iptables -D DOCKER-USER -i envsync-wg -p ${protocol} -m multiport --dports ${portSpec} -j ACCEPT`
5572
+ );
5573
+ }
5574
+ };
5575
+ addRules("tcp", tcpRanges);
5576
+ addRules("udp", udpRanges);
5577
+ return lines;
5578
+ }
5579
+ function tryLoadOptionalConfig() {
5580
+ try {
5581
+ return loadConfig();
5582
+ } catch {
5583
+ return null;
5584
+ }
5585
+ }
5389
5586
  function parseJsonOrDefault(value, fallback) {
5390
5587
  try {
5391
5588
  return JSON.parse(value);
@@ -5510,6 +5707,15 @@ function parseNetutilsForwardsFromInput(raw, stackName) {
5510
5707
  });
5511
5708
  return normalizeNetutilsForwards(candidates, stackName, true).forwards;
5512
5709
  }
5710
+ function buildNetutilsExposeAllForwards(stackName) {
5711
+ const candidates = NETUTILS_EXPOSE_ALL_PORTS.map((forward, index) => ({
5712
+ service: forward.service,
5713
+ service_port: forward.service_port,
5714
+ host_port: NETUTILS_EXPOSE_ALL_START_PORT + index,
5715
+ protocol: forward.protocol
5716
+ }));
5717
+ return normalizeNetutilsForwards(candidates, stackName, true).forwards;
5718
+ }
5513
5719
  function normalizeNetutilsProtocol(raw) {
5514
5720
  if (typeof raw === "string") {
5515
5721
  const protocol = raw.toLowerCase();
@@ -5635,6 +5841,8 @@ function wireguardPeerAllowedIp(state) {
5635
5841
  throw new Error("WireGuard client IP pool exhausted in subnet 10.66.0.0/24.");
5636
5842
  }
5637
5843
  function writeWireguardConfig(state) {
5844
+ const config = tryLoadOptionalConfig();
5845
+ const netutilsFirewallRules = formatNetutilsFirewallRules(config);
5638
5846
  const peersSection = state.peers.map((peer) => `
5639
5847
  [Peer]
5640
5848
  PublicKey = ${peer.public_key}
@@ -5647,6 +5855,7 @@ PostUp = iptables -A FORWARD -i envsync-wg -j ACCEPT
5647
5855
  PostDown = iptables -D FORWARD -i envsync-wg -j ACCEPT
5648
5856
  PostUp = iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
5649
5857
  PostDown = iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
5858
+ ${netutilsFirewallRules.join("\n")}
5650
5859
  ${peersSection}
5651
5860
  `;
5652
5861
  writeFileMaybe(WIREGUARD_CONFIG_FILE, content);
@@ -6038,6 +6247,11 @@ function normalizeConfig(raw) {
6038
6247
  keep_failed_upgrade_db_snapshot: raw.upgrade?.keep_failed_upgrade_db_snapshot ?? true
6039
6248
  },
6040
6249
  netutils: normalizeNetutilsForwards(raw.netutils?.forwards, stackName, raw.netutils?.enabled ?? false),
6250
+ vpn: {
6251
+ provider: raw.vpn?.provider ?? "none",
6252
+ internal_domain: raw.vpn?.internal_domain ?? "envsync.local",
6253
+ nb_setup_key: raw.vpn?.nb_setup_key ?? ""
6254
+ },
6041
6255
  license: raw.license ? {
6042
6256
  server_url: raw.license.server_url,
6043
6257
  key: raw.license.key,
@@ -6371,6 +6585,9 @@ function writeDeployArtifacts(config, generated) {
6371
6585
  writeFileMaybe(NGINX_API_MAINTENANCE_CONF, renderApiMaintenanceConf());
6372
6586
  writeFileMaybe(OTEL_AGENT_CONF, renderOtelAgentConfig(config));
6373
6587
  writeFileMaybe(CLICKSTACK_CLICKHOUSE_CONF, renderClickstackClickHouseConfig());
6588
+ if (config.vpn?.provider && config.vpn.provider !== "none") {
6589
+ writeFileMaybe(NGINX_VPN_CONF, renderNginxVpnConf(config));
6590
+ }
6374
6591
  logSuccess(currentOptions.dryRun ? "Deploy artifacts previewed" : "Deploy artifacts written");
6375
6592
  }
6376
6593
  function saveDesiredConfig(config) {
@@ -7466,14 +7683,33 @@ async function cmdSetup() {
7466
7683
  const publicObs = await ask("Expose obs.<domain> publicly (true/false)", "true") === "true";
7467
7684
  const mailpitEnabled = await ask("Enable mailpit (true/false)", "false") === "true";
7468
7685
  const netutilsEnabled = await ask("Enable WireGuard jump host access (true/false)", "false") === "true";
7469
- const netutilsForwards = netutilsEnabled ? parseNetutilsForwardsFromInput(
7470
- await ask(
7471
- "Netutils forwards (format service:service_port:host_port[:protocol], comma-separated)",
7472
- "postgres:5432:15432:tcp,keycloak:8080:18080:tcp"
7473
- ),
7474
- "envsync"
7475
- ) : [];
7686
+ const netutilsExposeAll = netutilsEnabled ? await ask("Expose all EnvSync service ports over WireGuard (true/false)", "false") === "true" : false;
7687
+ let netutilsForwards = [];
7688
+ if (netutilsEnabled) {
7689
+ if (netutilsExposeAll) {
7690
+ netutilsForwards = buildNetutilsExposeAllForwards("envsync");
7691
+ logInfo("Netutils exposes all preset (auto-generated):");
7692
+ for (const forward of netutilsForwards) {
7693
+ console.log(` ${forward.service}:${forward.service_port} -> ${forward.host_port} (${forward.protocol})`);
7694
+ }
7695
+ } else {
7696
+ netutilsForwards = parseNetutilsForwardsFromInput(
7697
+ await ask(
7698
+ "Netutils forwards (format service:service_port:host_port[:protocol], comma-separated)",
7699
+ "postgres:5432:15432:tcp,keycloak:8080:18080:tcp"
7700
+ ),
7701
+ "envsync"
7702
+ );
7703
+ }
7704
+ }
7476
7705
  const licenseServerUrl = await ask("Enterprise license server URL", DEFAULT_ENTERPRISE_LICENSE_SERVER_URL);
7706
+ const vpnProvider = (await ask("VPN provider for internal service access (netbird/none)", "none")).toLowerCase();
7707
+ let vpnInternalDomain = "envsync.local";
7708
+ let vpnNbSetupKey = "";
7709
+ if (vpnProvider !== "none") {
7710
+ vpnInternalDomain = await ask("Internal root domain", "envsync.local");
7711
+ vpnNbSetupKey = await ask("Netbird setup key", "");
7712
+ }
7477
7713
  const licenseKey = licenseServerUrl ? await ask("Enterprise license key", "") : "";
7478
7714
  const certificateBundleFile = licenseServerUrl ? "" : await ask("Enterprise certificate bundle file (optional)", "");
7479
7715
  const installFingerprint = deterministicInstallFingerprint(rootDomain, "envsync");
@@ -7553,6 +7789,11 @@ async function cmdSetup() {
7553
7789
  netutils: {
7554
7790
  enabled: netutilsEnabled,
7555
7791
  forwards: netutilsForwards
7792
+ },
7793
+ vpn: {
7794
+ provider: vpnProvider,
7795
+ internal_domain: vpnInternalDomain,
7796
+ nb_setup_key: vpnNbSetupKey
7556
7797
  }
7557
7798
  };
7558
7799
  saveDesiredConfig(config);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@envsync-cloud/deploy-cli",
3
- "version": "0.8.15",
3
+ "version": "0.9.0",
4
4
  "description": "CLI for self-hosted EnvSync deployment on Docker Swarm",
5
5
  "type": "module",
6
6
  "bin": {