@envsync-cloud/deploy-cli 0.8.14 → 0.8.15

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 +139 -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:
@@ -5448,6 +5482,91 @@ function randomStrongPassword() {
5448
5482
  function randomWireguardPort() {
5449
5483
  return randomInt(5e4, 65535);
5450
5484
  }
5485
+ function parsePort(value, label) {
5486
+ const numericValue = typeof value === "string" ? Number(value.trim()) : Number(value);
5487
+ if (!Number.isInteger(numericValue) || numericValue < 1 || numericValue > 65535) {
5488
+ throw new Error(`Invalid ${label}: expected an integer from 1 to 65535.`);
5489
+ }
5490
+ return numericValue;
5491
+ }
5492
+ function parseNetutilsForwardsFromInput(raw, stackName) {
5493
+ const chunks = asStringList(raw);
5494
+ if (chunks.length === 0) {
5495
+ return [];
5496
+ }
5497
+ const candidates = chunks.map((chunk) => {
5498
+ const parts = chunk.split(":");
5499
+ if (parts.length < 3 || parts.length > 4) {
5500
+ throw new Error(
5501
+ `Invalid netutils forward '${chunk}'. Expected format service:service_port:host_port[:protocol].`
5502
+ );
5503
+ }
5504
+ return {
5505
+ service: parts[0].trim(),
5506
+ service_port: parts[1].trim(),
5507
+ host_port: parts[2].trim(),
5508
+ protocol: parts[3] ?? "tcp"
5509
+ };
5510
+ });
5511
+ return normalizeNetutilsForwards(candidates, stackName, true).forwards;
5512
+ }
5513
+ function normalizeNetutilsProtocol(raw) {
5514
+ if (typeof raw === "string") {
5515
+ const protocol = raw.toLowerCase();
5516
+ if (protocol === "tcp" || protocol === "udp") return protocol;
5517
+ throw new Error(`Invalid protocol '${raw}'. Supported values are tcp or udp.`);
5518
+ }
5519
+ return "tcp";
5520
+ }
5521
+ function normalizeNetutilsForwards(raw, stackName, enabled) {
5522
+ if (!Array.isArray(raw)) {
5523
+ return {
5524
+ enabled,
5525
+ forwards: []
5526
+ };
5527
+ }
5528
+ const usedHostPorts = /* @__PURE__ */ new Set();
5529
+ const unique = /* @__PURE__ */ new Set();
5530
+ const forwards = [];
5531
+ for (const entry of raw) {
5532
+ const candidate = entry ?? {};
5533
+ const rawService = typeof candidate.service === "string" ? candidate.service.trim() : "";
5534
+ if (!rawService) {
5535
+ throw new Error("Invalid netutils forward: missing service");
5536
+ }
5537
+ const service = rawService.includes(".") || rawService.includes(":") || rawService.startsWith(`${stackName}_`) ? rawService : rawService;
5538
+ const servicePort = parsePort(candidate.service_port, "service_port");
5539
+ const hostPort = parsePort(candidate.host_port, "host_port");
5540
+ const protocol = normalizeNetutilsProtocol(candidate.protocol);
5541
+ const mapKey = `${service}:${servicePort}:${hostPort}:${protocol}`;
5542
+ if (unique.has(mapKey)) {
5543
+ continue;
5544
+ }
5545
+ const hostKey = `${protocol}:${hostPort}`;
5546
+ if (usedHostPorts.has(hostKey)) {
5547
+ throw new Error(
5548
+ `Duplicate netutils host port mapping detected for ${protocol.toUpperCase()} ${hostPort}. Use unique host ports per protocol.`
5549
+ );
5550
+ }
5551
+ unique.add(mapKey);
5552
+ usedHostPorts.add(hostKey);
5553
+ forwards.push({
5554
+ service,
5555
+ service_port: servicePort,
5556
+ host_port: hostPort,
5557
+ protocol
5558
+ });
5559
+ }
5560
+ forwards.sort((a, b) => {
5561
+ if (a.protocol !== b.protocol) return a.protocol.localeCompare(b.protocol);
5562
+ if (a.host_port !== b.host_port) return a.host_port - b.host_port;
5563
+ return a.service.localeCompare(b.service);
5564
+ });
5565
+ return {
5566
+ enabled,
5567
+ forwards
5568
+ };
5569
+ }
5451
5570
  function loadWireguardState() {
5452
5571
  if (!exists2(WIREGUARD_STATE_FILE)) return null;
5453
5572
  const raw = JSON.parse(fs3.readFileSync(WIREGUARD_STATE_FILE, "utf8"));
@@ -5581,6 +5700,9 @@ function parseEnvFile(content) {
5581
5700
  }
5582
5701
  return out;
5583
5702
  }
5703
+ function asStringList(value) {
5704
+ return value.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
5705
+ }
5584
5706
  async function ask(question, fallback = "") {
5585
5707
  if (!process.stdin.isTTY) return fallback;
5586
5708
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
@@ -5915,6 +6037,7 @@ function normalizeConfig(raw) {
5915
6037
  db_snapshot_on_api_upgrade: raw.upgrade?.db_snapshot_on_api_upgrade ?? true,
5916
6038
  keep_failed_upgrade_db_snapshot: raw.upgrade?.keep_failed_upgrade_db_snapshot ?? true
5917
6039
  },
6040
+ netutils: normalizeNetutilsForwards(raw.netutils?.forwards, stackName, raw.netutils?.enabled ?? false),
5918
6041
  license: raw.license ? {
5919
6042
  server_url: raw.license.server_url,
5920
6043
  key: raw.license.key,
@@ -7342,6 +7465,14 @@ async function cmdSetup() {
7342
7465
  const publicAuth = await ask("Expose auth.<domain> publicly (true/false)", "true") === "true";
7343
7466
  const publicObs = await ask("Expose obs.<domain> publicly (true/false)", "true") === "true";
7344
7467
  const mailpitEnabled = await ask("Enable mailpit (true/false)", "false") === "true";
7468
+ 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
+ ) : [];
7345
7476
  const licenseServerUrl = await ask("Enterprise license server URL", DEFAULT_ENTERPRISE_LICENSE_SERVER_URL);
7346
7477
  const licenseKey = licenseServerUrl ? await ask("Enterprise license key", "") : "";
7347
7478
  const certificateBundleFile = licenseServerUrl ? "" : await ask("Enterprise certificate bundle file (optional)", "");
@@ -7418,6 +7549,10 @@ async function cmdSetup() {
7418
7549
  install_fingerprint: installFingerprint,
7419
7550
  certificate_bundle_file: certificateBundleFile || void 0,
7420
7551
  certificate_validity_days: 1095
7552
+ },
7553
+ netutils: {
7554
+ enabled: netutilsEnabled,
7555
+ forwards: netutilsForwards
7421
7556
  }
7422
7557
  };
7423
7558
  saveDesiredConfig(config);
@@ -8397,9 +8532,9 @@ function listRunningContainerEndpoints(serviceName) {
8397
8532
  for (const taskId of taskIds) {
8398
8533
  const taskRaw = tryRun("docker", ["inspect", taskId, "--format", "{{json .}}"], { quiet: true });
8399
8534
  if (!taskRaw) continue;
8400
- const inspections = parseJsonOrDefault(taskRaw, []);
8401
- if (!Array.isArray(inspections) || inspections.length === 0) continue;
8402
- const inspect = inspections[0];
8535
+ const parsedTaskInspect = parseJsonOrDefault(taskRaw, null);
8536
+ const inspect = Array.isArray(parsedTaskInspect) ? parsedTaskInspect[0] : parsedTaskInspect;
8537
+ if (!inspect || typeof inspect !== "object") continue;
8403
8538
  const containerId = inspect.Status?.ContainerStatus?.ContainerID ?? "";
8404
8539
  const addresses = Array.isArray(inspect.NetworksAttachments) ? inspect.NetworksAttachments.flatMap((attachment) => {
8405
8540
  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.15",
4
4
  "description": "CLI for self-hosted EnvSync deployment on Docker Swarm",
5
5
  "type": "module",
6
6
  "bin": {