@envsync-cloud/deploy-cli 0.8.14 → 0.8.16

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 (2) hide show
  1. package/dist/index.js +250 -4
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4756,6 +4756,39 @@ function renderFrontendRuntimeConfig(config, generated) {
4756
4756
  }, null, 2)};
4757
4757
  `;
4758
4758
  }
4759
+ function renderNetutilsService(config) {
4760
+ const netutils = config.netutils;
4761
+ if (!netutils?.enabled) return "";
4762
+ const forwards = Array.isArray(netutils.forwards) ? netutils.forwards : [];
4763
+ if (forwards.length === 0) return "";
4764
+ const commands = forwards.map((forward) => {
4765
+ const protocol = forward.protocol ?? "tcp";
4766
+ if (protocol === "udp") {
4767
+ return `socat UDP-LISTEN:${forward.host_port},fork,reuseaddr UDP:${forward.service}:${forward.service_port} &`;
4768
+ }
4769
+ return `socat TCP-LISTEN:${forward.host_port},fork,reuseaddr TCP:${forward.service}:${forward.service_port} &`;
4770
+ }).join("\n ");
4771
+ const ports = forwards.map((forward) => {
4772
+ const protocol = forward.protocol ?? "tcp";
4773
+ return ` - target: ${forward.host_port}
4774
+ published: ${forward.host_port}
4775
+ protocol: ${protocol}
4776
+ mode: ingress`;
4777
+ }).join("\n");
4778
+ return `
4779
+ netutils:
4780
+ image: alpine/socat
4781
+ command:
4782
+ - sh
4783
+ - -c
4784
+ - |
4785
+ set -eu
4786
+ ${commands}
4787
+ wait
4788
+ ports:
4789
+ ${ports}
4790
+ networks: [envsync]`;
4791
+ }
4759
4792
  function renderOtelAgentConfig(config) {
4760
4793
  return [
4761
4794
  "receivers:",
@@ -4812,6 +4845,7 @@ function renderStack(config, runtimeEnv, generated, mode, paths) {
4812
4845
  const s3ServiceName = `${stackName}-s3-service`;
4813
4846
  const s3ConsoleRouterName = `${stackName}-s3-console-router`;
4814
4847
  const s3ConsoleServiceName = `${stackName}-s3-console-service`;
4848
+ const netutilsService = renderNetutilsService(config);
4815
4849
  const apiEnvironment = {
4816
4850
  ...runtimeEnv,
4817
4851
  OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-agent:4318",
@@ -5079,7 +5113,7 @@ ${renderEnvList({
5079
5113
  ${apiLicenseVolume}
5080
5114
  networks: [envsync]
5081
5115
  deploy:
5082
- replicas: ${slotHasApiDeployment(deployment.slots.green) ? 1 : 0}` : ""}
5116
+ replicas: ${slotHasApiDeployment(deployment.slots.green) ? 1 : 0}` : ""}${netutilsService}
5083
5117
 
5084
5118
  networks:
5085
5119
  envsync:
@@ -5147,6 +5181,30 @@ function validateStaticBundle(kind, targetDir) {
5147
5181
  }
5148
5182
 
5149
5183
  // src/index.ts
5184
+ var NETUTILS_EXPOSE_ALL_START_PORT = 15e3;
5185
+ var NETUTILS_EXPOSE_ALL_PORTS = [
5186
+ { service: "postgres", service_port: 5432, protocol: "tcp" },
5187
+ { service: "envsync_api_blue", service_port: 4e3, protocol: "tcp" },
5188
+ { service: "envsync_api_green", service_port: 4e3, protocol: "tcp" },
5189
+ { service: "envsync-management-api", service_port: 4001, protocol: "tcp" },
5190
+ { service: "keycloak", service_port: 8080, protocol: "tcp" },
5191
+ { service: "openfga", service_port: 8090, protocol: "tcp" },
5192
+ { service: "minikms", service_port: 50051, protocol: "tcp" },
5193
+ { service: "keycloak_db", service_port: 5432, protocol: "tcp" },
5194
+ { service: "openfga_db", service_port: 5432, protocol: "tcp" },
5195
+ { service: "minikms_db", service_port: 5432, protocol: "tcp" },
5196
+ { service: "redis", service_port: 6379, protocol: "tcp" },
5197
+ { service: "rustfs", service_port: 9e3, protocol: "tcp" },
5198
+ { service: "rustfs", service_port: 9001, protocol: "tcp" },
5199
+ { service: "clickstack", service_port: 8080, protocol: "tcp" },
5200
+ { service: "otel-agent", service_port: 4317, protocol: "tcp" },
5201
+ { service: "otel-agent", service_port: 4318, protocol: "tcp" },
5202
+ { service: "traefik", service_port: 80, protocol: "tcp" },
5203
+ { service: "traefik", service_port: 443, protocol: "tcp" },
5204
+ { service: "web_nginx", service_port: 80, protocol: "tcp" },
5205
+ { service: "landing_nginx", service_port: 80, protocol: "tcp" },
5206
+ { service: "api_maintenance", service_port: 80, protocol: "tcp" }
5207
+ ];
5150
5208
  var HOST_ROOT = process.env.ENVSYNC_HOST_ROOT ?? "/opt/envsync";
5151
5209
  var ETC_ROOT = process.env.ENVSYNC_ETC_ROOT ?? "/etc/envsync";
5152
5210
  var TRAEFIK_STATE_ROOT = process.env.ENVSYNC_TRAEFIK_STATE_ROOT ?? "/var/lib/envsync/traefik";
@@ -5352,6 +5410,69 @@ ${stderr}` : ""}`);
5352
5410
  }
5353
5411
  return (result.stdout ?? "").toString().trim();
5354
5412
  }
5413
+ function chunkByMax(items, max) {
5414
+ if (items.length === 0) return [];
5415
+ const chunks = [];
5416
+ for (let i = 0; i < items.length; i += max) {
5417
+ chunks.push(items.slice(i, i + max));
5418
+ }
5419
+ return chunks;
5420
+ }
5421
+ function formatPortRangesForIptables(ports) {
5422
+ 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);
5423
+ if (sortedPorts.length === 0) return [];
5424
+ const ranges = [];
5425
+ let rangeStart = sortedPorts[0];
5426
+ let previous = sortedPorts[0];
5427
+ for (let i = 1; i < sortedPorts.length; i++) {
5428
+ const current = sortedPorts[i];
5429
+ if (current === previous + 1) {
5430
+ previous = current;
5431
+ continue;
5432
+ }
5433
+ ranges.push(rangeStart === previous ? `${rangeStart}` : `${rangeStart}:${previous}`);
5434
+ rangeStart = current;
5435
+ previous = current;
5436
+ }
5437
+ ranges.push(rangeStart === previous ? `${rangeStart}` : `${rangeStart}:${previous}`);
5438
+ return ranges;
5439
+ }
5440
+ function formatNetutilsFirewallRules(config) {
5441
+ if (!config?.netutils?.enabled || !Array.isArray(config.netutils.forwards) || config.netutils.forwards.length === 0) {
5442
+ return [];
5443
+ }
5444
+ const tcpPorts = config.netutils.forwards.filter((item) => item.protocol === "tcp").map((item) => item.host_port);
5445
+ const udpPorts = config.netutils.forwards.filter((item) => item.protocol === "udp").map((item) => item.host_port);
5446
+ const tcpRanges = formatPortRangesForIptables(tcpPorts);
5447
+ const udpRanges = formatPortRangesForIptables(udpPorts);
5448
+ if (tcpRanges.length === 0 && udpRanges.length === 0) return [];
5449
+ const lines = [];
5450
+ const addRules = (protocol, ranges) => {
5451
+ for (const chunk of chunkByMax(ranges, 15)) {
5452
+ const portSpec = chunk.join(",");
5453
+ lines.push(
5454
+ `PostUp = iptables -I DOCKER-USER -i envsync-wg -p ${protocol} -m multiport --dports ${portSpec} -j ACCEPT`
5455
+ );
5456
+ lines.push(`PostUp = iptables -A DOCKER-USER -p ${protocol} -m multiport --dports ${portSpec} -j DROP`);
5457
+ lines.push(
5458
+ `PostDown = iptables -D DOCKER-USER -p ${protocol} -m multiport --dports ${portSpec} -j DROP`
5459
+ );
5460
+ lines.push(
5461
+ `PostDown = iptables -D DOCKER-USER -i envsync-wg -p ${protocol} -m multiport --dports ${portSpec} -j ACCEPT`
5462
+ );
5463
+ }
5464
+ };
5465
+ addRules("tcp", tcpRanges);
5466
+ addRules("udp", udpRanges);
5467
+ return lines;
5468
+ }
5469
+ function tryLoadOptionalConfig() {
5470
+ try {
5471
+ return loadConfig();
5472
+ } catch {
5473
+ return null;
5474
+ }
5475
+ }
5355
5476
  function parseJsonOrDefault(value, fallback) {
5356
5477
  try {
5357
5478
  return JSON.parse(value);
@@ -5448,6 +5569,100 @@ function randomStrongPassword() {
5448
5569
  function randomWireguardPort() {
5449
5570
  return randomInt(5e4, 65535);
5450
5571
  }
5572
+ function parsePort(value, label) {
5573
+ const numericValue = typeof value === "string" ? Number(value.trim()) : Number(value);
5574
+ if (!Number.isInteger(numericValue) || numericValue < 1 || numericValue > 65535) {
5575
+ throw new Error(`Invalid ${label}: expected an integer from 1 to 65535.`);
5576
+ }
5577
+ return numericValue;
5578
+ }
5579
+ function parseNetutilsForwardsFromInput(raw, stackName) {
5580
+ const chunks = asStringList(raw);
5581
+ if (chunks.length === 0) {
5582
+ return [];
5583
+ }
5584
+ const candidates = chunks.map((chunk) => {
5585
+ const parts = chunk.split(":");
5586
+ if (parts.length < 3 || parts.length > 4) {
5587
+ throw new Error(
5588
+ `Invalid netutils forward '${chunk}'. Expected format service:service_port:host_port[:protocol].`
5589
+ );
5590
+ }
5591
+ return {
5592
+ service: parts[0].trim(),
5593
+ service_port: parts[1].trim(),
5594
+ host_port: parts[2].trim(),
5595
+ protocol: parts[3] ?? "tcp"
5596
+ };
5597
+ });
5598
+ return normalizeNetutilsForwards(candidates, stackName, true).forwards;
5599
+ }
5600
+ function buildNetutilsExposeAllForwards(stackName) {
5601
+ const candidates = NETUTILS_EXPOSE_ALL_PORTS.map((forward, index) => ({
5602
+ service: forward.service,
5603
+ service_port: forward.service_port,
5604
+ host_port: NETUTILS_EXPOSE_ALL_START_PORT + index,
5605
+ protocol: forward.protocol
5606
+ }));
5607
+ return normalizeNetutilsForwards(candidates, stackName, true).forwards;
5608
+ }
5609
+ function normalizeNetutilsProtocol(raw) {
5610
+ if (typeof raw === "string") {
5611
+ const protocol = raw.toLowerCase();
5612
+ if (protocol === "tcp" || protocol === "udp") return protocol;
5613
+ throw new Error(`Invalid protocol '${raw}'. Supported values are tcp or udp.`);
5614
+ }
5615
+ return "tcp";
5616
+ }
5617
+ function normalizeNetutilsForwards(raw, stackName, enabled) {
5618
+ if (!Array.isArray(raw)) {
5619
+ return {
5620
+ enabled,
5621
+ forwards: []
5622
+ };
5623
+ }
5624
+ const usedHostPorts = /* @__PURE__ */ new Set();
5625
+ const unique = /* @__PURE__ */ new Set();
5626
+ const forwards = [];
5627
+ for (const entry of raw) {
5628
+ const candidate = entry ?? {};
5629
+ const rawService = typeof candidate.service === "string" ? candidate.service.trim() : "";
5630
+ if (!rawService) {
5631
+ throw new Error("Invalid netutils forward: missing service");
5632
+ }
5633
+ const service = rawService.includes(".") || rawService.includes(":") || rawService.startsWith(`${stackName}_`) ? rawService : rawService;
5634
+ const servicePort = parsePort(candidate.service_port, "service_port");
5635
+ const hostPort = parsePort(candidate.host_port, "host_port");
5636
+ const protocol = normalizeNetutilsProtocol(candidate.protocol);
5637
+ const mapKey = `${service}:${servicePort}:${hostPort}:${protocol}`;
5638
+ if (unique.has(mapKey)) {
5639
+ continue;
5640
+ }
5641
+ const hostKey = `${protocol}:${hostPort}`;
5642
+ if (usedHostPorts.has(hostKey)) {
5643
+ throw new Error(
5644
+ `Duplicate netutils host port mapping detected for ${protocol.toUpperCase()} ${hostPort}. Use unique host ports per protocol.`
5645
+ );
5646
+ }
5647
+ unique.add(mapKey);
5648
+ usedHostPorts.add(hostKey);
5649
+ forwards.push({
5650
+ service,
5651
+ service_port: servicePort,
5652
+ host_port: hostPort,
5653
+ protocol
5654
+ });
5655
+ }
5656
+ forwards.sort((a, b) => {
5657
+ if (a.protocol !== b.protocol) return a.protocol.localeCompare(b.protocol);
5658
+ if (a.host_port !== b.host_port) return a.host_port - b.host_port;
5659
+ return a.service.localeCompare(b.service);
5660
+ });
5661
+ return {
5662
+ enabled,
5663
+ forwards
5664
+ };
5665
+ }
5451
5666
  function loadWireguardState() {
5452
5667
  if (!exists2(WIREGUARD_STATE_FILE)) return null;
5453
5668
  const raw = JSON.parse(fs3.readFileSync(WIREGUARD_STATE_FILE, "utf8"));
@@ -5516,6 +5731,8 @@ function wireguardPeerAllowedIp(state) {
5516
5731
  throw new Error("WireGuard client IP pool exhausted in subnet 10.66.0.0/24.");
5517
5732
  }
5518
5733
  function writeWireguardConfig(state) {
5734
+ const config = tryLoadOptionalConfig();
5735
+ const netutilsFirewallRules = formatNetutilsFirewallRules(config);
5519
5736
  const peersSection = state.peers.map((peer) => `
5520
5737
  [Peer]
5521
5738
  PublicKey = ${peer.public_key}
@@ -5528,6 +5745,7 @@ PostUp = iptables -A FORWARD -i envsync-wg -j ACCEPT
5528
5745
  PostDown = iptables -D FORWARD -i envsync-wg -j ACCEPT
5529
5746
  PostUp = iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
5530
5747
  PostDown = iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
5748
+ ${netutilsFirewallRules.join("\n")}
5531
5749
  ${peersSection}
5532
5750
  `;
5533
5751
  writeFileMaybe(WIREGUARD_CONFIG_FILE, content);
@@ -5581,6 +5799,9 @@ function parseEnvFile(content) {
5581
5799
  }
5582
5800
  return out;
5583
5801
  }
5802
+ function asStringList(value) {
5803
+ return value.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
5804
+ }
5584
5805
  async function ask(question, fallback = "") {
5585
5806
  if (!process.stdin.isTTY) return fallback;
5586
5807
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
@@ -5915,6 +6136,7 @@ function normalizeConfig(raw) {
5915
6136
  db_snapshot_on_api_upgrade: raw.upgrade?.db_snapshot_on_api_upgrade ?? true,
5916
6137
  keep_failed_upgrade_db_snapshot: raw.upgrade?.keep_failed_upgrade_db_snapshot ?? true
5917
6138
  },
6139
+ netutils: normalizeNetutilsForwards(raw.netutils?.forwards, stackName, raw.netutils?.enabled ?? false),
5918
6140
  license: raw.license ? {
5919
6141
  server_url: raw.license.server_url,
5920
6142
  key: raw.license.key,
@@ -7342,6 +7564,26 @@ async function cmdSetup() {
7342
7564
  const publicAuth = await ask("Expose auth.<domain> publicly (true/false)", "true") === "true";
7343
7565
  const publicObs = await ask("Expose obs.<domain> publicly (true/false)", "true") === "true";
7344
7566
  const mailpitEnabled = await ask("Enable mailpit (true/false)", "false") === "true";
7567
+ const netutilsEnabled = await ask("Enable WireGuard jump host access (true/false)", "false") === "true";
7568
+ const netutilsExposeAll = netutilsEnabled ? await ask("Expose all EnvSync service ports over WireGuard (true/false)", "false") === "true" : false;
7569
+ let netutilsForwards = [];
7570
+ if (netutilsEnabled) {
7571
+ if (netutilsExposeAll) {
7572
+ netutilsForwards = buildNetutilsExposeAllForwards("envsync");
7573
+ logInfo("Netutils exposes all preset (auto-generated):");
7574
+ for (const forward of netutilsForwards) {
7575
+ console.log(` ${forward.service}:${forward.service_port} -> ${forward.host_port} (${forward.protocol})`);
7576
+ }
7577
+ } else {
7578
+ netutilsForwards = parseNetutilsForwardsFromInput(
7579
+ await ask(
7580
+ "Netutils forwards (format service:service_port:host_port[:protocol], comma-separated)",
7581
+ "postgres:5432:15432:tcp,keycloak:8080:18080:tcp"
7582
+ ),
7583
+ "envsync"
7584
+ );
7585
+ }
7586
+ }
7345
7587
  const licenseServerUrl = await ask("Enterprise license server URL", DEFAULT_ENTERPRISE_LICENSE_SERVER_URL);
7346
7588
  const licenseKey = licenseServerUrl ? await ask("Enterprise license key", "") : "";
7347
7589
  const certificateBundleFile = licenseServerUrl ? "" : await ask("Enterprise certificate bundle file (optional)", "");
@@ -7418,6 +7660,10 @@ async function cmdSetup() {
7418
7660
  install_fingerprint: installFingerprint,
7419
7661
  certificate_bundle_file: certificateBundleFile || void 0,
7420
7662
  certificate_validity_days: 1095
7663
+ },
7664
+ netutils: {
7665
+ enabled: netutilsEnabled,
7666
+ forwards: netutilsForwards
7421
7667
  }
7422
7668
  };
7423
7669
  saveDesiredConfig(config);
@@ -8397,9 +8643,9 @@ function listRunningContainerEndpoints(serviceName) {
8397
8643
  for (const taskId of taskIds) {
8398
8644
  const taskRaw = tryRun("docker", ["inspect", taskId, "--format", "{{json .}}"], { quiet: true });
8399
8645
  if (!taskRaw) continue;
8400
- const inspections = parseJsonOrDefault(taskRaw, []);
8401
- if (!Array.isArray(inspections) || inspections.length === 0) continue;
8402
- const inspect = inspections[0];
8646
+ const parsedTaskInspect = parseJsonOrDefault(taskRaw, null);
8647
+ const inspect = Array.isArray(parsedTaskInspect) ? parsedTaskInspect[0] : parsedTaskInspect;
8648
+ if (!inspect || typeof inspect !== "object") continue;
8403
8649
  const containerId = inspect.Status?.ContainerStatus?.ContainerID ?? "";
8404
8650
  const addresses = Array.isArray(inspect.NetworksAttachments) ? inspect.NetworksAttachments.flatMap((attachment) => {
8405
8651
  const networkName = attachment.Network?.Spec?.Name ?? "overlay";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@envsync-cloud/deploy-cli",
3
- "version": "0.8.14",
3
+ "version": "0.8.16",
4
4
  "description": "CLI for self-hosted EnvSync deployment on Docker Swarm",
5
5
  "type": "module",
6
6
  "bin": {