@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.
- package/dist/index.js +250 -4
- 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
|
|
8401
|
-
|
|
8402
|
-
|
|
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";
|