@envsync-cloud/deploy-cli 0.8.13 → 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.
- package/dist/index.js +436 -60
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ var __export = (target, all) => {
|
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
// src/index.ts
|
|
9
|
-
import { createHash, randomBytes } from "crypto";
|
|
9
|
+
import { createHash, randomBytes, randomInt } from "crypto";
|
|
10
10
|
import { spawnSync } from "child_process";
|
|
11
11
|
import fs3 from "fs";
|
|
12
12
|
import path3 from "path";
|
|
@@ -4400,7 +4400,6 @@ function buildRuntimeEnv2(config, generated) {
|
|
|
4400
4400
|
const oss = isOssConfig(config);
|
|
4401
4401
|
const enterprise = !oss;
|
|
4402
4402
|
const license = config.license ?? {};
|
|
4403
|
-
const accessProxy = config.access_proxy;
|
|
4404
4403
|
return {
|
|
4405
4404
|
NODE_ENV: "production",
|
|
4406
4405
|
ENVSYNC_EDITION: oss ? "oss" : "enterprise",
|
|
@@ -4429,13 +4428,6 @@ function buildRuntimeEnv2(config, generated) {
|
|
|
4429
4428
|
POSTGRES_USER: "postgres",
|
|
4430
4429
|
POSTGRES_PASSWORD: "envsync-postgres",
|
|
4431
4430
|
POSTGRES_DB: "envsync",
|
|
4432
|
-
ENVSYNC_ACCESS_PROXY_ENABLED: `${accessProxy?.enabled ?? false}`,
|
|
4433
|
-
ENVSYNC_ACCESS_PROXY_PROVIDER: accessProxy?.provider ?? "",
|
|
4434
|
-
ENVSYNC_ACCESS_PROXY_DOMAIN: accessProxy?.domain ?? "",
|
|
4435
|
-
ENVSYNC_ACCESS_PROXY_SERVICE_SCOPE: accessProxy?.service_scope ?? "all",
|
|
4436
|
-
ENVSYNC_ACCESS_PROXY_SERVICES: Array.isArray(accessProxy?.services) ? accessProxy.services.join(",") : "",
|
|
4437
|
-
ENVSYNC_ACCESS_PROXY_ADVERTISE_ROUTES: `${accessProxy?.advertise_routes ?? false}`,
|
|
4438
|
-
ENVSYNC_ACCESS_PROXY_INCLUDE_ADMIN_SERVICES: `${accessProxy?.include_admin_services ?? false}`,
|
|
4439
4431
|
S3_BUCKET: bucketName,
|
|
4440
4432
|
S3_REGION: "us-east-1",
|
|
4441
4433
|
S3_ACCESS_KEY: "envsync-rustfs",
|
|
@@ -4745,16 +4737,11 @@ function renderFrontendRuntimeConfig(config, generated) {
|
|
|
4745
4737
|
const otelEndpoint = publicHttpsUrl(config, hosts.obs);
|
|
4746
4738
|
const managementApiEnabled = !isOssConfig(config);
|
|
4747
4739
|
const activeReleaseVersion = generated.deployment.slots[generated.deployment.active_slot].release_version || config.release.version;
|
|
4748
|
-
const accessProxy = config.access_proxy;
|
|
4749
4740
|
const managementApiUrl = managementApiEnabled ? publicHttpsUrl(config, hosts.manage_api) : "";
|
|
4750
4741
|
return `window.__ENVSYNC_RUNTIME_CONFIG__ = ${JSON.stringify({
|
|
4751
4742
|
apiBaseUrl: publicHttpsUrl(config, hosts.api),
|
|
4752
4743
|
appBaseUrl: publicHttpsUrl(config, hosts.app),
|
|
4753
4744
|
authBaseUrl: publicHttpsUrl(config, hosts.auth),
|
|
4754
|
-
accessProxyEnabled: accessProxy?.enabled ?? false,
|
|
4755
|
-
accessProxyDomain: accessProxy?.domain ?? "",
|
|
4756
|
-
accessProxyServiceScope: accessProxy?.service_scope ?? "all",
|
|
4757
|
-
accessProxyServices: accessProxy?.services ?? [],
|
|
4758
4745
|
managementApiUrl,
|
|
4759
4746
|
keycloakRealm: config.auth.keycloak_realm,
|
|
4760
4747
|
webClientId: config.auth.web_client_id,
|
|
@@ -4769,6 +4756,39 @@ function renderFrontendRuntimeConfig(config, generated) {
|
|
|
4769
4756
|
}, null, 2)};
|
|
4770
4757
|
`;
|
|
4771
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
|
+
}
|
|
4772
4792
|
function renderOtelAgentConfig(config) {
|
|
4773
4793
|
return [
|
|
4774
4794
|
"receivers:",
|
|
@@ -4825,6 +4845,7 @@ function renderStack(config, runtimeEnv, generated, mode, paths) {
|
|
|
4825
4845
|
const s3ServiceName = `${stackName}-s3-service`;
|
|
4826
4846
|
const s3ConsoleRouterName = `${stackName}-s3-console-router`;
|
|
4827
4847
|
const s3ConsoleServiceName = `${stackName}-s3-console-service`;
|
|
4848
|
+
const netutilsService = renderNetutilsService(config);
|
|
4828
4849
|
const apiEnvironment = {
|
|
4829
4850
|
...runtimeEnv,
|
|
4830
4851
|
OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-agent:4318",
|
|
@@ -5092,7 +5113,7 @@ ${renderEnvList({
|
|
|
5092
5113
|
${apiLicenseVolume}
|
|
5093
5114
|
networks: [envsync]
|
|
5094
5115
|
deploy:
|
|
5095
|
-
replicas: ${slotHasApiDeployment(deployment.slots.green) ? 1 : 0}` : ""}
|
|
5116
|
+
replicas: ${slotHasApiDeployment(deployment.slots.green) ? 1 : 0}` : ""}${netutilsService}
|
|
5096
5117
|
|
|
5097
5118
|
networks:
|
|
5098
5119
|
envsync:
|
|
@@ -5169,6 +5190,7 @@ var BACKUPS_ROOT = path3.join(HOST_ROOT, "backups");
|
|
|
5169
5190
|
var REPO_ROOT = process.env.ENVSYNC_REPO_ROOT ?? path3.join(HOST_ROOT, "repo");
|
|
5170
5191
|
var DEPLOY_ENV = path3.join(ETC_ROOT, "deploy.env");
|
|
5171
5192
|
var DEPLOY_YAML = path3.join(ETC_ROOT, "deploy.yaml");
|
|
5193
|
+
var WIREGUARD_ROOT = path3.join(ETC_ROOT, "wireguard");
|
|
5172
5194
|
var LICENSE_ROOT = path3.join(ETC_ROOT, "license");
|
|
5173
5195
|
var LICENSE_BUNDLE_FILE = path3.join(LICENSE_ROOT, "enterprise-license-bundle.json");
|
|
5174
5196
|
var LICENSE_CERT_FILE = path3.join(LICENSE_ROOT, "enterprise-cert.pem");
|
|
@@ -5189,6 +5211,10 @@ var OTEL_AGENT_CONF = path3.join(DEPLOY_ROOT, "otel-agent.yaml");
|
|
|
5189
5211
|
var CLICKSTACK_CLICKHOUSE_CONF = path3.join(DEPLOY_ROOT, "clickhouse-listen.xml");
|
|
5190
5212
|
var INTERNAL_CONFIG_JSON = path3.join(DEPLOY_ROOT, "config.json");
|
|
5191
5213
|
var UPGRADE_BACKUPS_ROOT = path3.join(BACKUPS_ROOT, "upgrade");
|
|
5214
|
+
var WIREGUARD_STATE_FILE = path3.join(WIREGUARD_ROOT, "state.json");
|
|
5215
|
+
var WIREGUARD_PRIVATE_KEY_FILE = path3.join(WIREGUARD_ROOT, "server-private.key");
|
|
5216
|
+
var WIREGUARD_PUBLIC_KEY_FILE = path3.join(WIREGUARD_ROOT, "server-public.key");
|
|
5217
|
+
var WIREGUARD_CONFIG_FILE = path3.join(WIREGUARD_ROOT, "envsync-wg.conf");
|
|
5192
5218
|
var DEPLOY_RENDER_PATHS = {
|
|
5193
5219
|
traefikStateRoot: TRAEFIK_STATE_ROOT,
|
|
5194
5220
|
deployRoot: DEPLOY_ROOT,
|
|
@@ -5215,6 +5241,11 @@ var REMOVE_TARGETS = [
|
|
|
5215
5241
|
LICENSE_ROOT_CA_FILE,
|
|
5216
5242
|
INTERNAL_CONFIG_JSON,
|
|
5217
5243
|
VERSIONS_LOCK,
|
|
5244
|
+
WIREGUARD_ROOT,
|
|
5245
|
+
WIREGUARD_STATE_FILE,
|
|
5246
|
+
WIREGUARD_PRIVATE_KEY_FILE,
|
|
5247
|
+
WIREGUARD_PUBLIC_KEY_FILE,
|
|
5248
|
+
WIREGUARD_CONFIG_FILE,
|
|
5218
5249
|
STACK_FILE,
|
|
5219
5250
|
BOOTSTRAP_BASE_STACK_FILE,
|
|
5220
5251
|
BOOTSTRAP_STACK_FILE,
|
|
@@ -5340,6 +5371,28 @@ ${stderr}` : ""}`);
|
|
|
5340
5371
|
}
|
|
5341
5372
|
return result.stdout?.toString() ?? "";
|
|
5342
5373
|
}
|
|
5374
|
+
function runCapture(cmd, args, opts = {}) {
|
|
5375
|
+
const result = spawnSync(cmd, args, {
|
|
5376
|
+
cwd: opts.cwd,
|
|
5377
|
+
env: { ...process.env, ...opts.env },
|
|
5378
|
+
input: opts.input,
|
|
5379
|
+
stdio: "pipe",
|
|
5380
|
+
encoding: "utf8"
|
|
5381
|
+
});
|
|
5382
|
+
if (result.status !== 0) {
|
|
5383
|
+
const stderr = typeof result.stderr === "string" ? result.stderr : "";
|
|
5384
|
+
throw new Error(`Command failed: ${cmd} ${args.join(" ")}${stderr ? `
|
|
5385
|
+
${stderr}` : ""}`);
|
|
5386
|
+
}
|
|
5387
|
+
return (result.stdout ?? "").toString().trim();
|
|
5388
|
+
}
|
|
5389
|
+
function parseJsonOrDefault(value, fallback) {
|
|
5390
|
+
try {
|
|
5391
|
+
return JSON.parse(value);
|
|
5392
|
+
} catch {
|
|
5393
|
+
return fallback;
|
|
5394
|
+
}
|
|
5395
|
+
}
|
|
5343
5396
|
function tryRun(cmd, args, opts = {}) {
|
|
5344
5397
|
try {
|
|
5345
5398
|
return run(cmd, args, opts);
|
|
@@ -5426,6 +5479,178 @@ function deterministicInstallFingerprint(rootDomain, stackName) {
|
|
|
5426
5479
|
function randomStrongPassword() {
|
|
5427
5480
|
return `EnvSync!${randomBytes(8).toString("hex")}Aa1`;
|
|
5428
5481
|
}
|
|
5482
|
+
function randomWireguardPort() {
|
|
5483
|
+
return randomInt(5e4, 65535);
|
|
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
|
+
}
|
|
5570
|
+
function loadWireguardState() {
|
|
5571
|
+
if (!exists2(WIREGUARD_STATE_FILE)) return null;
|
|
5572
|
+
const raw = JSON.parse(fs3.readFileSync(WIREGUARD_STATE_FILE, "utf8"));
|
|
5573
|
+
return {
|
|
5574
|
+
private_key: raw.private_key,
|
|
5575
|
+
public_key: raw.public_key,
|
|
5576
|
+
listen_port: raw.listen_port,
|
|
5577
|
+
network: raw.network,
|
|
5578
|
+
server_ip: raw.server_ip,
|
|
5579
|
+
peers: Array.isArray(raw.peers) ? raw.peers : []
|
|
5580
|
+
};
|
|
5581
|
+
}
|
|
5582
|
+
function saveWireguardState(state) {
|
|
5583
|
+
if (currentOptions.dryRun) {
|
|
5584
|
+
logDryRun(`Would save WireGuard state to ${WIREGUARD_STATE_FILE}`);
|
|
5585
|
+
return;
|
|
5586
|
+
}
|
|
5587
|
+
writeFile(WIREGUARD_STATE_FILE, JSON.stringify(state, null, 2), 384);
|
|
5588
|
+
}
|
|
5589
|
+
function ensureWireguardState() {
|
|
5590
|
+
ensureDir(WIREGUARD_ROOT);
|
|
5591
|
+
let state = loadWireguardState();
|
|
5592
|
+
if (state) {
|
|
5593
|
+
state.private_key = state.private_key?.trim();
|
|
5594
|
+
state.public_key = state.public_key?.trim();
|
|
5595
|
+
state.network = state.network || "10.66.0.0/24";
|
|
5596
|
+
state.server_ip = state.server_ip || "10.66.0.1";
|
|
5597
|
+
state.listen_port = state.listen_port || randomWireguardPort();
|
|
5598
|
+
state.peers = Array.isArray(state.peers) ? state.peers : [];
|
|
5599
|
+
if (state.private_key && !state.public_key) {
|
|
5600
|
+
state.public_key = runCapture("wg", ["pubkey"], { input: `${state.private_key}
|
|
5601
|
+
` });
|
|
5602
|
+
}
|
|
5603
|
+
if (!state.private_key || !state.public_key) {
|
|
5604
|
+
state = null;
|
|
5605
|
+
}
|
|
5606
|
+
}
|
|
5607
|
+
if (!state) {
|
|
5608
|
+
const privateKey = runCapture("wg", ["genkey"]);
|
|
5609
|
+
const publicKey = runCapture("wg", ["pubkey"], { input: `${privateKey}
|
|
5610
|
+
` });
|
|
5611
|
+
state = {
|
|
5612
|
+
private_key: privateKey,
|
|
5613
|
+
public_key: publicKey,
|
|
5614
|
+
listen_port: randomWireguardPort(),
|
|
5615
|
+
network: "10.66.0.0/24",
|
|
5616
|
+
server_ip: "10.66.0.1",
|
|
5617
|
+
peers: []
|
|
5618
|
+
};
|
|
5619
|
+
saveWireguardState(state);
|
|
5620
|
+
}
|
|
5621
|
+
writeFileMaybe(WIREGUARD_PRIVATE_KEY_FILE, `${state.private_key}
|
|
5622
|
+
`, 384);
|
|
5623
|
+
writeFileMaybe(WIREGUARD_PUBLIC_KEY_FILE, `${state.public_key}
|
|
5624
|
+
`);
|
|
5625
|
+
return state;
|
|
5626
|
+
}
|
|
5627
|
+
function wireguardPeerAllowedIp(state) {
|
|
5628
|
+
const usedIps = new Set(state.peers.map((peer) => peer.allowed_ip));
|
|
5629
|
+
for (let i = 2; i <= 254; i++) {
|
|
5630
|
+
const candidate = `${state.server_ip.split(".").slice(0, 3).join(".")}.${i}/32`;
|
|
5631
|
+
if (!usedIps.has(candidate)) {
|
|
5632
|
+
return candidate;
|
|
5633
|
+
}
|
|
5634
|
+
}
|
|
5635
|
+
throw new Error("WireGuard client IP pool exhausted in subnet 10.66.0.0/24.");
|
|
5636
|
+
}
|
|
5637
|
+
function writeWireguardConfig(state) {
|
|
5638
|
+
const peersSection = state.peers.map((peer) => `
|
|
5639
|
+
[Peer]
|
|
5640
|
+
PublicKey = ${peer.public_key}
|
|
5641
|
+
AllowedIPs = ${peer.allowed_ip}`).join("\n");
|
|
5642
|
+
const content = `[Interface]
|
|
5643
|
+
Address = ${state.server_ip}/24
|
|
5644
|
+
ListenPort = ${state.listen_port}
|
|
5645
|
+
PrivateKey = ${state.private_key}
|
|
5646
|
+
PostUp = iptables -A FORWARD -i envsync-wg -j ACCEPT
|
|
5647
|
+
PostDown = iptables -D FORWARD -i envsync-wg -j ACCEPT
|
|
5648
|
+
PostUp = iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
|
|
5649
|
+
PostDown = iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
|
|
5650
|
+
${peersSection}
|
|
5651
|
+
`;
|
|
5652
|
+
writeFileMaybe(WIREGUARD_CONFIG_FILE, content);
|
|
5653
|
+
}
|
|
5429
5654
|
function emptyApiSlotState() {
|
|
5430
5655
|
return {
|
|
5431
5656
|
api_image: "",
|
|
@@ -5475,9 +5700,6 @@ function parseEnvFile(content) {
|
|
|
5475
5700
|
}
|
|
5476
5701
|
return out;
|
|
5477
5702
|
}
|
|
5478
|
-
function asBool(value) {
|
|
5479
|
-
return value.trim().toLowerCase() === "true" || value.trim().toLowerCase() === "y" || value.trim().toLowerCase() === "yes";
|
|
5480
|
-
}
|
|
5481
5703
|
function asStringList(value) {
|
|
5482
5704
|
return value.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
|
|
5483
5705
|
}
|
|
@@ -5567,6 +5789,9 @@ function renderHelpBlock() {
|
|
|
5567
5789
|
" validate-topology Validate Enterprise edition topology rules",
|
|
5568
5790
|
" upgrade [version] Pin a target release and deploy it",
|
|
5569
5791
|
" upgrade-deps Refresh dependency images and redeploy",
|
|
5792
|
+
" vpn pubkey Generate or print WireGuard server public key",
|
|
5793
|
+
" vpn whitelist <key> Add a client public key to allowed peers",
|
|
5794
|
+
" vpn status Show WireGuard server/peer status",
|
|
5570
5795
|
" license issue-cert Issue and install an Enterprise certificate bundle",
|
|
5571
5796
|
" license renew-cert Renew and install the Enterprise certificate bundle",
|
|
5572
5797
|
" license validate-cert Validate the installed Enterprise certificate bundle files",
|
|
@@ -5812,6 +6037,7 @@ function normalizeConfig(raw) {
|
|
|
5812
6037
|
db_snapshot_on_api_upgrade: raw.upgrade?.db_snapshot_on_api_upgrade ?? true,
|
|
5813
6038
|
keep_failed_upgrade_db_snapshot: raw.upgrade?.keep_failed_upgrade_db_snapshot ?? true
|
|
5814
6039
|
},
|
|
6040
|
+
netutils: normalizeNetutilsForwards(raw.netutils?.forwards, stackName, raw.netutils?.enabled ?? false),
|
|
5815
6041
|
license: raw.license ? {
|
|
5816
6042
|
server_url: raw.license.server_url,
|
|
5817
6043
|
key: raw.license.key,
|
|
@@ -5822,30 +6048,6 @@ function normalizeConfig(raw) {
|
|
|
5822
6048
|
} : raw.edition === "oss" ? void 0 : {
|
|
5823
6049
|
install_fingerprint: deterministicInstallFingerprint(rootDomain, stackName),
|
|
5824
6050
|
certificate_validity_days: 1095
|
|
5825
|
-
},
|
|
5826
|
-
access_proxy: {
|
|
5827
|
-
enabled: raw.access_proxy?.enabled ?? false,
|
|
5828
|
-
provider: "tsdproxy",
|
|
5829
|
-
auth_token: raw.access_proxy?.auth_token ?? "",
|
|
5830
|
-
domain: raw.access_proxy?.domain ?? `tsdproxy.${rootDomain}`,
|
|
5831
|
-
service_scope: raw.access_proxy?.service_scope === "selected" ? "selected" : "all",
|
|
5832
|
-
services: Array.isArray(raw.access_proxy?.services) ? raw.access_proxy.services.filter((s) => typeof s === "string" && s.trim().length > 0) : [
|
|
5833
|
-
"api",
|
|
5834
|
-
"web",
|
|
5835
|
-
"landing",
|
|
5836
|
-
"keycloak",
|
|
5837
|
-
"openfga",
|
|
5838
|
-
"postgre*",
|
|
5839
|
-
"redis",
|
|
5840
|
-
"rustfs",
|
|
5841
|
-
"clickstack",
|
|
5842
|
-
"obs",
|
|
5843
|
-
"envsync_api_*",
|
|
5844
|
-
"envsync_management-api",
|
|
5845
|
-
"envsync-api"
|
|
5846
|
-
],
|
|
5847
|
-
advertise_routes: raw.access_proxy?.advertise_routes ?? false,
|
|
5848
|
-
include_admin_services: raw.access_proxy?.include_admin_services ?? false
|
|
5849
6051
|
}
|
|
5850
6052
|
};
|
|
5851
6053
|
}
|
|
@@ -7222,7 +7424,19 @@ async function cmdPreinstall() {
|
|
|
7222
7424
|
ensureDir(TRAEFIK_STATE_ROOT);
|
|
7223
7425
|
run("bash", ["-lc", "command -v apt-get >/dev/null"]);
|
|
7224
7426
|
run("sudo", ["apt-get", "update"]);
|
|
7225
|
-
run("sudo", [
|
|
7427
|
+
run("sudo", [
|
|
7428
|
+
"apt-get",
|
|
7429
|
+
"install",
|
|
7430
|
+
"-y",
|
|
7431
|
+
"docker.io",
|
|
7432
|
+
"docker-compose-v2",
|
|
7433
|
+
"git",
|
|
7434
|
+
"curl",
|
|
7435
|
+
"jq",
|
|
7436
|
+
"openssl",
|
|
7437
|
+
"tar",
|
|
7438
|
+
"wireguard-tools"
|
|
7439
|
+
]);
|
|
7226
7440
|
run("sudo", ["systemctl", "enable", "--now", "docker"]);
|
|
7227
7441
|
try {
|
|
7228
7442
|
run("docker", ["swarm", "init"]);
|
|
@@ -7251,18 +7465,18 @@ async function cmdSetup() {
|
|
|
7251
7465
|
const publicAuth = await ask("Expose auth.<domain> publicly (true/false)", "true") === "true";
|
|
7252
7466
|
const publicObs = await ask("Expose obs.<domain> publicly (true/false)", "true") === "true";
|
|
7253
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
|
+
) : [];
|
|
7254
7476
|
const licenseServerUrl = await ask("Enterprise license server URL", DEFAULT_ENTERPRISE_LICENSE_SERVER_URL);
|
|
7255
7477
|
const licenseKey = licenseServerUrl ? await ask("Enterprise license key", "") : "";
|
|
7256
7478
|
const certificateBundleFile = licenseServerUrl ? "" : await ask("Enterprise certificate bundle file (optional)", "");
|
|
7257
7479
|
const installFingerprint = deterministicInstallFingerprint(rootDomain, "envsync");
|
|
7258
|
-
const enableAccessProxy = asBool(await ask("Enable private access proxy for container services (y/N)", "n"));
|
|
7259
|
-
const accessProxyProvider = enableAccessProxy ? await ask("Private access proxy provider", "tsdproxy") : "tsdproxy";
|
|
7260
|
-
const accessProxyAuthToken = enableAccessProxy ? await ask("Proxy auth token or API key", "") : "";
|
|
7261
|
-
const accessProxyDomain = enableAccessProxy ? await ask("Private access domain", `tsdproxy.${rootDomain}`) : `tsdproxy.${rootDomain}`;
|
|
7262
|
-
const accessProxyExposeAll = enableAccessProxy ? asBool(await ask("Expose all services through proxy (Y/n)", "y")) : true;
|
|
7263
|
-
const accessProxyServices = !accessProxyExposeAll ? asStringList(await ask("Service hostnames to expose (comma-separated)", "postgres,api,web,landing,keycloak,minikms,openfga,rustfs,clickstack,obs,manage-api")) : [];
|
|
7264
|
-
const includeAdminServices = enableAccessProxy ? asBool(await ask("Expose admin/maintenance services (y/N)", "n")) : false;
|
|
7265
|
-
const accessProxyAdvertiseRoutes = enableAccessProxy ? asBool(await ask("Advertise overlay subnet routes through the proxy provider (y/N)", "n")) : false;
|
|
7266
7480
|
const config = {
|
|
7267
7481
|
edition: "enterprise",
|
|
7268
7482
|
source: defaultSourceConfig(releaseVersion),
|
|
@@ -7336,15 +7550,9 @@ async function cmdSetup() {
|
|
|
7336
7550
|
certificate_bundle_file: certificateBundleFile || void 0,
|
|
7337
7551
|
certificate_validity_days: 1095
|
|
7338
7552
|
},
|
|
7339
|
-
|
|
7340
|
-
enabled:
|
|
7341
|
-
|
|
7342
|
-
auth_token: accessProxyAuthToken,
|
|
7343
|
-
domain: accessProxyDomain,
|
|
7344
|
-
service_scope: accessProxyExposeAll ? "all" : "selected",
|
|
7345
|
-
services: accessProxyServices,
|
|
7346
|
-
advertise_routes: accessProxyAdvertiseRoutes,
|
|
7347
|
-
include_admin_services: includeAdminServices
|
|
7553
|
+
netutils: {
|
|
7554
|
+
enabled: netutilsEnabled,
|
|
7555
|
+
forwards: netutilsForwards
|
|
7348
7556
|
}
|
|
7349
7557
|
};
|
|
7350
7558
|
saveDesiredConfig(config);
|
|
@@ -8226,6 +8434,171 @@ async function cmdRestore(archivePath, autoDeploy = false) {
|
|
|
8226
8434
|
}
|
|
8227
8435
|
logInfo("Next: envsync-deploy deploy");
|
|
8228
8436
|
}
|
|
8437
|
+
async function cmdVpn(args) {
|
|
8438
|
+
const action = args[0];
|
|
8439
|
+
switch (action) {
|
|
8440
|
+
case "pubkey":
|
|
8441
|
+
await cmdVpnPubkey();
|
|
8442
|
+
break;
|
|
8443
|
+
case "whitelist":
|
|
8444
|
+
await cmdVpnWhitelist(args[1]);
|
|
8445
|
+
break;
|
|
8446
|
+
case "status":
|
|
8447
|
+
await cmdVpnStatus();
|
|
8448
|
+
break;
|
|
8449
|
+
default:
|
|
8450
|
+
throw new Error("Usage: envsync-deploy vpn <pubkey|whitelist|status>");
|
|
8451
|
+
}
|
|
8452
|
+
}
|
|
8453
|
+
async function cmdVpnPubkey() {
|
|
8454
|
+
logSection("WireGuard VPN");
|
|
8455
|
+
const state = ensureWireguardState();
|
|
8456
|
+
writeWireguardConfig(state);
|
|
8457
|
+
logInfo(`WireGuard interface: envsync-wg`);
|
|
8458
|
+
logInfo(`Server config: ${WIREGUARD_CONFIG_FILE}`);
|
|
8459
|
+
logInfo(`Private key: ${WIREGUARD_PRIVATE_KEY_FILE}`);
|
|
8460
|
+
logInfo(`Public key: ${WIREGUARD_PUBLIC_KEY_FILE}`);
|
|
8461
|
+
logInfo(`Server listen port: ${state.listen_port}`);
|
|
8462
|
+
logInfo(`Network: ${state.network}`);
|
|
8463
|
+
console.log("");
|
|
8464
|
+
logSuccess(`Server public key: ${state.public_key}`);
|
|
8465
|
+
}
|
|
8466
|
+
async function cmdVpnWhitelist(clientPublicKeyArg) {
|
|
8467
|
+
if (!clientPublicKeyArg) {
|
|
8468
|
+
throw new Error("usage: envsync-deploy vpn whitelist <client_public_key>");
|
|
8469
|
+
}
|
|
8470
|
+
const clientPublicKey = clientPublicKeyArg.trim();
|
|
8471
|
+
if (!clientPublicKey) {
|
|
8472
|
+
throw new Error("client public key cannot be empty");
|
|
8473
|
+
}
|
|
8474
|
+
const state = ensureWireguardState();
|
|
8475
|
+
const existing = state.peers.find((peer) => peer.public_key === clientPublicKey);
|
|
8476
|
+
if (existing) {
|
|
8477
|
+
logWarn(`Client key already whitelisted for ${existing.allowed_ip}`);
|
|
8478
|
+
return;
|
|
8479
|
+
}
|
|
8480
|
+
const allowedIp = wireguardPeerAllowedIp(state);
|
|
8481
|
+
state.peers.push({
|
|
8482
|
+
public_key: clientPublicKey,
|
|
8483
|
+
allowed_ip: allowedIp,
|
|
8484
|
+
added_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
8485
|
+
});
|
|
8486
|
+
state.peers.sort((a, b) => a.public_key.localeCompare(b.public_key));
|
|
8487
|
+
saveWireguardState(state);
|
|
8488
|
+
writeWireguardConfig(state);
|
|
8489
|
+
logSuccess(`Whitelisted client ${clientPublicKey}`);
|
|
8490
|
+
console.log(`AllowedIP: ${allowedIp}`);
|
|
8491
|
+
console.log(`Append to client config:`);
|
|
8492
|
+
console.log(`[Peer]`);
|
|
8493
|
+
console.log(`PublicKey = ${clientPublicKey}`);
|
|
8494
|
+
console.log(`AllowedIPs = ${allowedIp}`);
|
|
8495
|
+
}
|
|
8496
|
+
function getVpnStackName() {
|
|
8497
|
+
try {
|
|
8498
|
+
return loadConfig().services.stack_name;
|
|
8499
|
+
} catch {
|
|
8500
|
+
return "envsync";
|
|
8501
|
+
}
|
|
8502
|
+
}
|
|
8503
|
+
function listStackServiceNames(stackName) {
|
|
8504
|
+
const raw = tryRun("docker", ["service", "ls", "--filter", `label=com.docker.stack.namespace=${stackName}`, "--format", "{{.Name}}"], {
|
|
8505
|
+
quiet: true
|
|
8506
|
+
});
|
|
8507
|
+
if (!raw.trim()) return [];
|
|
8508
|
+
return raw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
8509
|
+
}
|
|
8510
|
+
function listServicePublishedPorts(serviceName) {
|
|
8511
|
+
const raw = tryRun("docker", ["service", "inspect", "--format", "{{json .Endpoint.Ports}}", serviceName], { quiet: true });
|
|
8512
|
+
const ports = parseJsonOrDefault(raw, []);
|
|
8513
|
+
const list = Array.isArray(ports) ? ports : [];
|
|
8514
|
+
if (list.length === 0) return [];
|
|
8515
|
+
return list.map((port) => {
|
|
8516
|
+
const typed = port;
|
|
8517
|
+
if (!typed || typeof typed !== "object") return "";
|
|
8518
|
+
if (!typed.PublishedPort || !typed.TargetPort || !typed.Protocol) return "";
|
|
8519
|
+
const mode = typed.PublishMode || "ingress";
|
|
8520
|
+
return `${typed.PublishedPort}/${typed.Protocol} -> ${typed.TargetPort}/${typed.Protocol} (${mode})`;
|
|
8521
|
+
}).filter(Boolean);
|
|
8522
|
+
}
|
|
8523
|
+
function listRunningContainerEndpoints(serviceName) {
|
|
8524
|
+
const raw = tryRun(
|
|
8525
|
+
"docker",
|
|
8526
|
+
["service", "ps", serviceName, "--filter", "desired-state=running", "--no-trunc", "--format", "{{.ID}}"],
|
|
8527
|
+
{ quiet: true }
|
|
8528
|
+
);
|
|
8529
|
+
const taskIds = raw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
8530
|
+
if (taskIds.length === 0) return [];
|
|
8531
|
+
const results = [];
|
|
8532
|
+
for (const taskId of taskIds) {
|
|
8533
|
+
const taskRaw = tryRun("docker", ["inspect", taskId, "--format", "{{json .}}"], { quiet: true });
|
|
8534
|
+
if (!taskRaw) continue;
|
|
8535
|
+
const parsedTaskInspect = parseJsonOrDefault(taskRaw, null);
|
|
8536
|
+
const inspect = Array.isArray(parsedTaskInspect) ? parsedTaskInspect[0] : parsedTaskInspect;
|
|
8537
|
+
if (!inspect || typeof inspect !== "object") continue;
|
|
8538
|
+
const containerId = inspect.Status?.ContainerStatus?.ContainerID ?? "";
|
|
8539
|
+
const addresses = Array.isArray(inspect.NetworksAttachments) ? inspect.NetworksAttachments.flatMap((attachment) => {
|
|
8540
|
+
const networkName = attachment.Network?.Spec?.Name ?? "overlay";
|
|
8541
|
+
const ips = Array.isArray(attachment.Addresses) ? attachment.Addresses : [];
|
|
8542
|
+
return ips.map((ip) => {
|
|
8543
|
+
const trimmed = typeof ip === "string" ? ip.trim() : "";
|
|
8544
|
+
return trimmed ? `${networkName}:${trimmed}` : "";
|
|
8545
|
+
}).filter(Boolean);
|
|
8546
|
+
}) : [];
|
|
8547
|
+
const line = `task ${taskId.slice(0, 12)} container=${containerId.slice(0, 12) || "unknown"} ${addresses.length ? `ips=${addresses.join(", ")}` : "ips=none"}`;
|
|
8548
|
+
results.push(line);
|
|
8549
|
+
}
|
|
8550
|
+
return results;
|
|
8551
|
+
}
|
|
8552
|
+
async function cmdVpnStatus() {
|
|
8553
|
+
logSection("WireGuard VPN");
|
|
8554
|
+
const state = loadWireguardState();
|
|
8555
|
+
if (!state) {
|
|
8556
|
+
logWarn("WireGuard state not initialized.");
|
|
8557
|
+
logInfo("Run: envsync-deploy vpn pubkey");
|
|
8558
|
+
return;
|
|
8559
|
+
}
|
|
8560
|
+
logInfo(`Config file: ${WIREGUARD_CONFIG_FILE}`);
|
|
8561
|
+
logInfo(`State file: ${WIREGUARD_STATE_FILE}`);
|
|
8562
|
+
logInfo(`Server endpoint: 0.0.0.0:${state.listen_port}`);
|
|
8563
|
+
logInfo(`Network: ${state.network}`);
|
|
8564
|
+
logInfo(`Server IP: ${state.server_ip}`);
|
|
8565
|
+
if (state.peers.length === 0) {
|
|
8566
|
+
logInfo("Peers: none");
|
|
8567
|
+
} else {
|
|
8568
|
+
logInfo("Peers:");
|
|
8569
|
+
for (const peer of state.peers) {
|
|
8570
|
+
console.log(` ${peer.public_key} -> ${peer.allowed_ip}`);
|
|
8571
|
+
}
|
|
8572
|
+
}
|
|
8573
|
+
console.log("");
|
|
8574
|
+
logInfo("Container endpoints:");
|
|
8575
|
+
const stackName = getVpnStackName();
|
|
8576
|
+
const services = listStackServiceNames(stackName);
|
|
8577
|
+
if (services.length === 0) {
|
|
8578
|
+
logWarn(`No running services found for stack ${stackName}`);
|
|
8579
|
+
} else {
|
|
8580
|
+
for (const service of services) {
|
|
8581
|
+
const ports = listServicePublishedPorts(service);
|
|
8582
|
+
const endpoints = listRunningContainerEndpoints(service);
|
|
8583
|
+
console.log(` ${service}`);
|
|
8584
|
+
console.log(` Published ports: ${ports.length ? ports.join(", ") : "none"}`);
|
|
8585
|
+
if (endpoints.length === 0) {
|
|
8586
|
+
console.log(" Containers: none running");
|
|
8587
|
+
} else {
|
|
8588
|
+
for (const endpoint of endpoints) {
|
|
8589
|
+
console.log(` ${endpoint}`);
|
|
8590
|
+
}
|
|
8591
|
+
}
|
|
8592
|
+
}
|
|
8593
|
+
}
|
|
8594
|
+
try {
|
|
8595
|
+
const ifaceStatus = runCapture("wg", ["show", "envsync-wg"]);
|
|
8596
|
+
console.log("Runtime:");
|
|
8597
|
+
console.log(ifaceStatus);
|
|
8598
|
+
} catch {
|
|
8599
|
+
logWarn("WireGuard interface is not running in kernel yet. Run wg-quick up on the generated config to activate.");
|
|
8600
|
+
}
|
|
8601
|
+
}
|
|
8229
8602
|
async function main() {
|
|
8230
8603
|
const argv = process.argv.slice(2);
|
|
8231
8604
|
const command = argv[0];
|
|
@@ -8273,6 +8646,9 @@ async function main() {
|
|
|
8273
8646
|
case "upgrade-deps":
|
|
8274
8647
|
await cmdUpgradeDeps();
|
|
8275
8648
|
break;
|
|
8649
|
+
case "vpn":
|
|
8650
|
+
await cmdVpn(positionals);
|
|
8651
|
+
break;
|
|
8276
8652
|
case "license":
|
|
8277
8653
|
await cmdLicense(positionals[0]);
|
|
8278
8654
|
break;
|