@envsync-cloud/deploy-cli 0.8.13 → 0.8.14
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 +304 -63
- 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,
|
|
@@ -5169,6 +5156,7 @@ var BACKUPS_ROOT = path3.join(HOST_ROOT, "backups");
|
|
|
5169
5156
|
var REPO_ROOT = process.env.ENVSYNC_REPO_ROOT ?? path3.join(HOST_ROOT, "repo");
|
|
5170
5157
|
var DEPLOY_ENV = path3.join(ETC_ROOT, "deploy.env");
|
|
5171
5158
|
var DEPLOY_YAML = path3.join(ETC_ROOT, "deploy.yaml");
|
|
5159
|
+
var WIREGUARD_ROOT = path3.join(ETC_ROOT, "wireguard");
|
|
5172
5160
|
var LICENSE_ROOT = path3.join(ETC_ROOT, "license");
|
|
5173
5161
|
var LICENSE_BUNDLE_FILE = path3.join(LICENSE_ROOT, "enterprise-license-bundle.json");
|
|
5174
5162
|
var LICENSE_CERT_FILE = path3.join(LICENSE_ROOT, "enterprise-cert.pem");
|
|
@@ -5189,6 +5177,10 @@ var OTEL_AGENT_CONF = path3.join(DEPLOY_ROOT, "otel-agent.yaml");
|
|
|
5189
5177
|
var CLICKSTACK_CLICKHOUSE_CONF = path3.join(DEPLOY_ROOT, "clickhouse-listen.xml");
|
|
5190
5178
|
var INTERNAL_CONFIG_JSON = path3.join(DEPLOY_ROOT, "config.json");
|
|
5191
5179
|
var UPGRADE_BACKUPS_ROOT = path3.join(BACKUPS_ROOT, "upgrade");
|
|
5180
|
+
var WIREGUARD_STATE_FILE = path3.join(WIREGUARD_ROOT, "state.json");
|
|
5181
|
+
var WIREGUARD_PRIVATE_KEY_FILE = path3.join(WIREGUARD_ROOT, "server-private.key");
|
|
5182
|
+
var WIREGUARD_PUBLIC_KEY_FILE = path3.join(WIREGUARD_ROOT, "server-public.key");
|
|
5183
|
+
var WIREGUARD_CONFIG_FILE = path3.join(WIREGUARD_ROOT, "envsync-wg.conf");
|
|
5192
5184
|
var DEPLOY_RENDER_PATHS = {
|
|
5193
5185
|
traefikStateRoot: TRAEFIK_STATE_ROOT,
|
|
5194
5186
|
deployRoot: DEPLOY_ROOT,
|
|
@@ -5215,6 +5207,11 @@ var REMOVE_TARGETS = [
|
|
|
5215
5207
|
LICENSE_ROOT_CA_FILE,
|
|
5216
5208
|
INTERNAL_CONFIG_JSON,
|
|
5217
5209
|
VERSIONS_LOCK,
|
|
5210
|
+
WIREGUARD_ROOT,
|
|
5211
|
+
WIREGUARD_STATE_FILE,
|
|
5212
|
+
WIREGUARD_PRIVATE_KEY_FILE,
|
|
5213
|
+
WIREGUARD_PUBLIC_KEY_FILE,
|
|
5214
|
+
WIREGUARD_CONFIG_FILE,
|
|
5218
5215
|
STACK_FILE,
|
|
5219
5216
|
BOOTSTRAP_BASE_STACK_FILE,
|
|
5220
5217
|
BOOTSTRAP_STACK_FILE,
|
|
@@ -5340,6 +5337,28 @@ ${stderr}` : ""}`);
|
|
|
5340
5337
|
}
|
|
5341
5338
|
return result.stdout?.toString() ?? "";
|
|
5342
5339
|
}
|
|
5340
|
+
function runCapture(cmd, args, opts = {}) {
|
|
5341
|
+
const result = spawnSync(cmd, args, {
|
|
5342
|
+
cwd: opts.cwd,
|
|
5343
|
+
env: { ...process.env, ...opts.env },
|
|
5344
|
+
input: opts.input,
|
|
5345
|
+
stdio: "pipe",
|
|
5346
|
+
encoding: "utf8"
|
|
5347
|
+
});
|
|
5348
|
+
if (result.status !== 0) {
|
|
5349
|
+
const stderr = typeof result.stderr === "string" ? result.stderr : "";
|
|
5350
|
+
throw new Error(`Command failed: ${cmd} ${args.join(" ")}${stderr ? `
|
|
5351
|
+
${stderr}` : ""}`);
|
|
5352
|
+
}
|
|
5353
|
+
return (result.stdout ?? "").toString().trim();
|
|
5354
|
+
}
|
|
5355
|
+
function parseJsonOrDefault(value, fallback) {
|
|
5356
|
+
try {
|
|
5357
|
+
return JSON.parse(value);
|
|
5358
|
+
} catch {
|
|
5359
|
+
return fallback;
|
|
5360
|
+
}
|
|
5361
|
+
}
|
|
5343
5362
|
function tryRun(cmd, args, opts = {}) {
|
|
5344
5363
|
try {
|
|
5345
5364
|
return run(cmd, args, opts);
|
|
@@ -5426,6 +5445,93 @@ function deterministicInstallFingerprint(rootDomain, stackName) {
|
|
|
5426
5445
|
function randomStrongPassword() {
|
|
5427
5446
|
return `EnvSync!${randomBytes(8).toString("hex")}Aa1`;
|
|
5428
5447
|
}
|
|
5448
|
+
function randomWireguardPort() {
|
|
5449
|
+
return randomInt(5e4, 65535);
|
|
5450
|
+
}
|
|
5451
|
+
function loadWireguardState() {
|
|
5452
|
+
if (!exists2(WIREGUARD_STATE_FILE)) return null;
|
|
5453
|
+
const raw = JSON.parse(fs3.readFileSync(WIREGUARD_STATE_FILE, "utf8"));
|
|
5454
|
+
return {
|
|
5455
|
+
private_key: raw.private_key,
|
|
5456
|
+
public_key: raw.public_key,
|
|
5457
|
+
listen_port: raw.listen_port,
|
|
5458
|
+
network: raw.network,
|
|
5459
|
+
server_ip: raw.server_ip,
|
|
5460
|
+
peers: Array.isArray(raw.peers) ? raw.peers : []
|
|
5461
|
+
};
|
|
5462
|
+
}
|
|
5463
|
+
function saveWireguardState(state) {
|
|
5464
|
+
if (currentOptions.dryRun) {
|
|
5465
|
+
logDryRun(`Would save WireGuard state to ${WIREGUARD_STATE_FILE}`);
|
|
5466
|
+
return;
|
|
5467
|
+
}
|
|
5468
|
+
writeFile(WIREGUARD_STATE_FILE, JSON.stringify(state, null, 2), 384);
|
|
5469
|
+
}
|
|
5470
|
+
function ensureWireguardState() {
|
|
5471
|
+
ensureDir(WIREGUARD_ROOT);
|
|
5472
|
+
let state = loadWireguardState();
|
|
5473
|
+
if (state) {
|
|
5474
|
+
state.private_key = state.private_key?.trim();
|
|
5475
|
+
state.public_key = state.public_key?.trim();
|
|
5476
|
+
state.network = state.network || "10.66.0.0/24";
|
|
5477
|
+
state.server_ip = state.server_ip || "10.66.0.1";
|
|
5478
|
+
state.listen_port = state.listen_port || randomWireguardPort();
|
|
5479
|
+
state.peers = Array.isArray(state.peers) ? state.peers : [];
|
|
5480
|
+
if (state.private_key && !state.public_key) {
|
|
5481
|
+
state.public_key = runCapture("wg", ["pubkey"], { input: `${state.private_key}
|
|
5482
|
+
` });
|
|
5483
|
+
}
|
|
5484
|
+
if (!state.private_key || !state.public_key) {
|
|
5485
|
+
state = null;
|
|
5486
|
+
}
|
|
5487
|
+
}
|
|
5488
|
+
if (!state) {
|
|
5489
|
+
const privateKey = runCapture("wg", ["genkey"]);
|
|
5490
|
+
const publicKey = runCapture("wg", ["pubkey"], { input: `${privateKey}
|
|
5491
|
+
` });
|
|
5492
|
+
state = {
|
|
5493
|
+
private_key: privateKey,
|
|
5494
|
+
public_key: publicKey,
|
|
5495
|
+
listen_port: randomWireguardPort(),
|
|
5496
|
+
network: "10.66.0.0/24",
|
|
5497
|
+
server_ip: "10.66.0.1",
|
|
5498
|
+
peers: []
|
|
5499
|
+
};
|
|
5500
|
+
saveWireguardState(state);
|
|
5501
|
+
}
|
|
5502
|
+
writeFileMaybe(WIREGUARD_PRIVATE_KEY_FILE, `${state.private_key}
|
|
5503
|
+
`, 384);
|
|
5504
|
+
writeFileMaybe(WIREGUARD_PUBLIC_KEY_FILE, `${state.public_key}
|
|
5505
|
+
`);
|
|
5506
|
+
return state;
|
|
5507
|
+
}
|
|
5508
|
+
function wireguardPeerAllowedIp(state) {
|
|
5509
|
+
const usedIps = new Set(state.peers.map((peer) => peer.allowed_ip));
|
|
5510
|
+
for (let i = 2; i <= 254; i++) {
|
|
5511
|
+
const candidate = `${state.server_ip.split(".").slice(0, 3).join(".")}.${i}/32`;
|
|
5512
|
+
if (!usedIps.has(candidate)) {
|
|
5513
|
+
return candidate;
|
|
5514
|
+
}
|
|
5515
|
+
}
|
|
5516
|
+
throw new Error("WireGuard client IP pool exhausted in subnet 10.66.0.0/24.");
|
|
5517
|
+
}
|
|
5518
|
+
function writeWireguardConfig(state) {
|
|
5519
|
+
const peersSection = state.peers.map((peer) => `
|
|
5520
|
+
[Peer]
|
|
5521
|
+
PublicKey = ${peer.public_key}
|
|
5522
|
+
AllowedIPs = ${peer.allowed_ip}`).join("\n");
|
|
5523
|
+
const content = `[Interface]
|
|
5524
|
+
Address = ${state.server_ip}/24
|
|
5525
|
+
ListenPort = ${state.listen_port}
|
|
5526
|
+
PrivateKey = ${state.private_key}
|
|
5527
|
+
PostUp = iptables -A FORWARD -i envsync-wg -j ACCEPT
|
|
5528
|
+
PostDown = iptables -D FORWARD -i envsync-wg -j ACCEPT
|
|
5529
|
+
PostUp = iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
|
|
5530
|
+
PostDown = iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
|
|
5531
|
+
${peersSection}
|
|
5532
|
+
`;
|
|
5533
|
+
writeFileMaybe(WIREGUARD_CONFIG_FILE, content);
|
|
5534
|
+
}
|
|
5429
5535
|
function emptyApiSlotState() {
|
|
5430
5536
|
return {
|
|
5431
5537
|
api_image: "",
|
|
@@ -5475,12 +5581,6 @@ function parseEnvFile(content) {
|
|
|
5475
5581
|
}
|
|
5476
5582
|
return out;
|
|
5477
5583
|
}
|
|
5478
|
-
function asBool(value) {
|
|
5479
|
-
return value.trim().toLowerCase() === "true" || value.trim().toLowerCase() === "y" || value.trim().toLowerCase() === "yes";
|
|
5480
|
-
}
|
|
5481
|
-
function asStringList(value) {
|
|
5482
|
-
return value.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
|
|
5483
|
-
}
|
|
5484
5584
|
async function ask(question, fallback = "") {
|
|
5485
5585
|
if (!process.stdin.isTTY) return fallback;
|
|
5486
5586
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -5567,6 +5667,9 @@ function renderHelpBlock() {
|
|
|
5567
5667
|
" validate-topology Validate Enterprise edition topology rules",
|
|
5568
5668
|
" upgrade [version] Pin a target release and deploy it",
|
|
5569
5669
|
" upgrade-deps Refresh dependency images and redeploy",
|
|
5670
|
+
" vpn pubkey Generate or print WireGuard server public key",
|
|
5671
|
+
" vpn whitelist <key> Add a client public key to allowed peers",
|
|
5672
|
+
" vpn status Show WireGuard server/peer status",
|
|
5570
5673
|
" license issue-cert Issue and install an Enterprise certificate bundle",
|
|
5571
5674
|
" license renew-cert Renew and install the Enterprise certificate bundle",
|
|
5572
5675
|
" license validate-cert Validate the installed Enterprise certificate bundle files",
|
|
@@ -5822,30 +5925,6 @@ function normalizeConfig(raw) {
|
|
|
5822
5925
|
} : raw.edition === "oss" ? void 0 : {
|
|
5823
5926
|
install_fingerprint: deterministicInstallFingerprint(rootDomain, stackName),
|
|
5824
5927
|
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
5928
|
}
|
|
5850
5929
|
};
|
|
5851
5930
|
}
|
|
@@ -7222,7 +7301,19 @@ async function cmdPreinstall() {
|
|
|
7222
7301
|
ensureDir(TRAEFIK_STATE_ROOT);
|
|
7223
7302
|
run("bash", ["-lc", "command -v apt-get >/dev/null"]);
|
|
7224
7303
|
run("sudo", ["apt-get", "update"]);
|
|
7225
|
-
run("sudo", [
|
|
7304
|
+
run("sudo", [
|
|
7305
|
+
"apt-get",
|
|
7306
|
+
"install",
|
|
7307
|
+
"-y",
|
|
7308
|
+
"docker.io",
|
|
7309
|
+
"docker-compose-v2",
|
|
7310
|
+
"git",
|
|
7311
|
+
"curl",
|
|
7312
|
+
"jq",
|
|
7313
|
+
"openssl",
|
|
7314
|
+
"tar",
|
|
7315
|
+
"wireguard-tools"
|
|
7316
|
+
]);
|
|
7226
7317
|
run("sudo", ["systemctl", "enable", "--now", "docker"]);
|
|
7227
7318
|
try {
|
|
7228
7319
|
run("docker", ["swarm", "init"]);
|
|
@@ -7255,14 +7346,6 @@ async function cmdSetup() {
|
|
|
7255
7346
|
const licenseKey = licenseServerUrl ? await ask("Enterprise license key", "") : "";
|
|
7256
7347
|
const certificateBundleFile = licenseServerUrl ? "" : await ask("Enterprise certificate bundle file (optional)", "");
|
|
7257
7348
|
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
7349
|
const config = {
|
|
7267
7350
|
edition: "enterprise",
|
|
7268
7351
|
source: defaultSourceConfig(releaseVersion),
|
|
@@ -7335,16 +7418,6 @@ async function cmdSetup() {
|
|
|
7335
7418
|
install_fingerprint: installFingerprint,
|
|
7336
7419
|
certificate_bundle_file: certificateBundleFile || void 0,
|
|
7337
7420
|
certificate_validity_days: 1095
|
|
7338
|
-
},
|
|
7339
|
-
access_proxy: {
|
|
7340
|
-
enabled: enableAccessProxy,
|
|
7341
|
-
provider: accessProxyProvider,
|
|
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
|
|
7348
7421
|
}
|
|
7349
7422
|
};
|
|
7350
7423
|
saveDesiredConfig(config);
|
|
@@ -8226,6 +8299,171 @@ async function cmdRestore(archivePath, autoDeploy = false) {
|
|
|
8226
8299
|
}
|
|
8227
8300
|
logInfo("Next: envsync-deploy deploy");
|
|
8228
8301
|
}
|
|
8302
|
+
async function cmdVpn(args) {
|
|
8303
|
+
const action = args[0];
|
|
8304
|
+
switch (action) {
|
|
8305
|
+
case "pubkey":
|
|
8306
|
+
await cmdVpnPubkey();
|
|
8307
|
+
break;
|
|
8308
|
+
case "whitelist":
|
|
8309
|
+
await cmdVpnWhitelist(args[1]);
|
|
8310
|
+
break;
|
|
8311
|
+
case "status":
|
|
8312
|
+
await cmdVpnStatus();
|
|
8313
|
+
break;
|
|
8314
|
+
default:
|
|
8315
|
+
throw new Error("Usage: envsync-deploy vpn <pubkey|whitelist|status>");
|
|
8316
|
+
}
|
|
8317
|
+
}
|
|
8318
|
+
async function cmdVpnPubkey() {
|
|
8319
|
+
logSection("WireGuard VPN");
|
|
8320
|
+
const state = ensureWireguardState();
|
|
8321
|
+
writeWireguardConfig(state);
|
|
8322
|
+
logInfo(`WireGuard interface: envsync-wg`);
|
|
8323
|
+
logInfo(`Server config: ${WIREGUARD_CONFIG_FILE}`);
|
|
8324
|
+
logInfo(`Private key: ${WIREGUARD_PRIVATE_KEY_FILE}`);
|
|
8325
|
+
logInfo(`Public key: ${WIREGUARD_PUBLIC_KEY_FILE}`);
|
|
8326
|
+
logInfo(`Server listen port: ${state.listen_port}`);
|
|
8327
|
+
logInfo(`Network: ${state.network}`);
|
|
8328
|
+
console.log("");
|
|
8329
|
+
logSuccess(`Server public key: ${state.public_key}`);
|
|
8330
|
+
}
|
|
8331
|
+
async function cmdVpnWhitelist(clientPublicKeyArg) {
|
|
8332
|
+
if (!clientPublicKeyArg) {
|
|
8333
|
+
throw new Error("usage: envsync-deploy vpn whitelist <client_public_key>");
|
|
8334
|
+
}
|
|
8335
|
+
const clientPublicKey = clientPublicKeyArg.trim();
|
|
8336
|
+
if (!clientPublicKey) {
|
|
8337
|
+
throw new Error("client public key cannot be empty");
|
|
8338
|
+
}
|
|
8339
|
+
const state = ensureWireguardState();
|
|
8340
|
+
const existing = state.peers.find((peer) => peer.public_key === clientPublicKey);
|
|
8341
|
+
if (existing) {
|
|
8342
|
+
logWarn(`Client key already whitelisted for ${existing.allowed_ip}`);
|
|
8343
|
+
return;
|
|
8344
|
+
}
|
|
8345
|
+
const allowedIp = wireguardPeerAllowedIp(state);
|
|
8346
|
+
state.peers.push({
|
|
8347
|
+
public_key: clientPublicKey,
|
|
8348
|
+
allowed_ip: allowedIp,
|
|
8349
|
+
added_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
8350
|
+
});
|
|
8351
|
+
state.peers.sort((a, b) => a.public_key.localeCompare(b.public_key));
|
|
8352
|
+
saveWireguardState(state);
|
|
8353
|
+
writeWireguardConfig(state);
|
|
8354
|
+
logSuccess(`Whitelisted client ${clientPublicKey}`);
|
|
8355
|
+
console.log(`AllowedIP: ${allowedIp}`);
|
|
8356
|
+
console.log(`Append to client config:`);
|
|
8357
|
+
console.log(`[Peer]`);
|
|
8358
|
+
console.log(`PublicKey = ${clientPublicKey}`);
|
|
8359
|
+
console.log(`AllowedIPs = ${allowedIp}`);
|
|
8360
|
+
}
|
|
8361
|
+
function getVpnStackName() {
|
|
8362
|
+
try {
|
|
8363
|
+
return loadConfig().services.stack_name;
|
|
8364
|
+
} catch {
|
|
8365
|
+
return "envsync";
|
|
8366
|
+
}
|
|
8367
|
+
}
|
|
8368
|
+
function listStackServiceNames(stackName) {
|
|
8369
|
+
const raw = tryRun("docker", ["service", "ls", "--filter", `label=com.docker.stack.namespace=${stackName}`, "--format", "{{.Name}}"], {
|
|
8370
|
+
quiet: true
|
|
8371
|
+
});
|
|
8372
|
+
if (!raw.trim()) return [];
|
|
8373
|
+
return raw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
8374
|
+
}
|
|
8375
|
+
function listServicePublishedPorts(serviceName) {
|
|
8376
|
+
const raw = tryRun("docker", ["service", "inspect", "--format", "{{json .Endpoint.Ports}}", serviceName], { quiet: true });
|
|
8377
|
+
const ports = parseJsonOrDefault(raw, []);
|
|
8378
|
+
const list = Array.isArray(ports) ? ports : [];
|
|
8379
|
+
if (list.length === 0) return [];
|
|
8380
|
+
return list.map((port) => {
|
|
8381
|
+
const typed = port;
|
|
8382
|
+
if (!typed || typeof typed !== "object") return "";
|
|
8383
|
+
if (!typed.PublishedPort || !typed.TargetPort || !typed.Protocol) return "";
|
|
8384
|
+
const mode = typed.PublishMode || "ingress";
|
|
8385
|
+
return `${typed.PublishedPort}/${typed.Protocol} -> ${typed.TargetPort}/${typed.Protocol} (${mode})`;
|
|
8386
|
+
}).filter(Boolean);
|
|
8387
|
+
}
|
|
8388
|
+
function listRunningContainerEndpoints(serviceName) {
|
|
8389
|
+
const raw = tryRun(
|
|
8390
|
+
"docker",
|
|
8391
|
+
["service", "ps", serviceName, "--filter", "desired-state=running", "--no-trunc", "--format", "{{.ID}}"],
|
|
8392
|
+
{ quiet: true }
|
|
8393
|
+
);
|
|
8394
|
+
const taskIds = raw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
8395
|
+
if (taskIds.length === 0) return [];
|
|
8396
|
+
const results = [];
|
|
8397
|
+
for (const taskId of taskIds) {
|
|
8398
|
+
const taskRaw = tryRun("docker", ["inspect", taskId, "--format", "{{json .}}"], { quiet: true });
|
|
8399
|
+
if (!taskRaw) continue;
|
|
8400
|
+
const inspections = parseJsonOrDefault(taskRaw, []);
|
|
8401
|
+
if (!Array.isArray(inspections) || inspections.length === 0) continue;
|
|
8402
|
+
const inspect = inspections[0];
|
|
8403
|
+
const containerId = inspect.Status?.ContainerStatus?.ContainerID ?? "";
|
|
8404
|
+
const addresses = Array.isArray(inspect.NetworksAttachments) ? inspect.NetworksAttachments.flatMap((attachment) => {
|
|
8405
|
+
const networkName = attachment.Network?.Spec?.Name ?? "overlay";
|
|
8406
|
+
const ips = Array.isArray(attachment.Addresses) ? attachment.Addresses : [];
|
|
8407
|
+
return ips.map((ip) => {
|
|
8408
|
+
const trimmed = typeof ip === "string" ? ip.trim() : "";
|
|
8409
|
+
return trimmed ? `${networkName}:${trimmed}` : "";
|
|
8410
|
+
}).filter(Boolean);
|
|
8411
|
+
}) : [];
|
|
8412
|
+
const line = `task ${taskId.slice(0, 12)} container=${containerId.slice(0, 12) || "unknown"} ${addresses.length ? `ips=${addresses.join(", ")}` : "ips=none"}`;
|
|
8413
|
+
results.push(line);
|
|
8414
|
+
}
|
|
8415
|
+
return results;
|
|
8416
|
+
}
|
|
8417
|
+
async function cmdVpnStatus() {
|
|
8418
|
+
logSection("WireGuard VPN");
|
|
8419
|
+
const state = loadWireguardState();
|
|
8420
|
+
if (!state) {
|
|
8421
|
+
logWarn("WireGuard state not initialized.");
|
|
8422
|
+
logInfo("Run: envsync-deploy vpn pubkey");
|
|
8423
|
+
return;
|
|
8424
|
+
}
|
|
8425
|
+
logInfo(`Config file: ${WIREGUARD_CONFIG_FILE}`);
|
|
8426
|
+
logInfo(`State file: ${WIREGUARD_STATE_FILE}`);
|
|
8427
|
+
logInfo(`Server endpoint: 0.0.0.0:${state.listen_port}`);
|
|
8428
|
+
logInfo(`Network: ${state.network}`);
|
|
8429
|
+
logInfo(`Server IP: ${state.server_ip}`);
|
|
8430
|
+
if (state.peers.length === 0) {
|
|
8431
|
+
logInfo("Peers: none");
|
|
8432
|
+
} else {
|
|
8433
|
+
logInfo("Peers:");
|
|
8434
|
+
for (const peer of state.peers) {
|
|
8435
|
+
console.log(` ${peer.public_key} -> ${peer.allowed_ip}`);
|
|
8436
|
+
}
|
|
8437
|
+
}
|
|
8438
|
+
console.log("");
|
|
8439
|
+
logInfo("Container endpoints:");
|
|
8440
|
+
const stackName = getVpnStackName();
|
|
8441
|
+
const services = listStackServiceNames(stackName);
|
|
8442
|
+
if (services.length === 0) {
|
|
8443
|
+
logWarn(`No running services found for stack ${stackName}`);
|
|
8444
|
+
} else {
|
|
8445
|
+
for (const service of services) {
|
|
8446
|
+
const ports = listServicePublishedPorts(service);
|
|
8447
|
+
const endpoints = listRunningContainerEndpoints(service);
|
|
8448
|
+
console.log(` ${service}`);
|
|
8449
|
+
console.log(` Published ports: ${ports.length ? ports.join(", ") : "none"}`);
|
|
8450
|
+
if (endpoints.length === 0) {
|
|
8451
|
+
console.log(" Containers: none running");
|
|
8452
|
+
} else {
|
|
8453
|
+
for (const endpoint of endpoints) {
|
|
8454
|
+
console.log(` ${endpoint}`);
|
|
8455
|
+
}
|
|
8456
|
+
}
|
|
8457
|
+
}
|
|
8458
|
+
}
|
|
8459
|
+
try {
|
|
8460
|
+
const ifaceStatus = runCapture("wg", ["show", "envsync-wg"]);
|
|
8461
|
+
console.log("Runtime:");
|
|
8462
|
+
console.log(ifaceStatus);
|
|
8463
|
+
} catch {
|
|
8464
|
+
logWarn("WireGuard interface is not running in kernel yet. Run wg-quick up on the generated config to activate.");
|
|
8465
|
+
}
|
|
8466
|
+
}
|
|
8229
8467
|
async function main() {
|
|
8230
8468
|
const argv = process.argv.slice(2);
|
|
8231
8469
|
const command = argv[0];
|
|
@@ -8273,6 +8511,9 @@ async function main() {
|
|
|
8273
8511
|
case "upgrade-deps":
|
|
8274
8512
|
await cmdUpgradeDeps();
|
|
8275
8513
|
break;
|
|
8514
|
+
case "vpn":
|
|
8515
|
+
await cmdVpn(positionals);
|
|
8516
|
+
break;
|
|
8276
8517
|
case "license":
|
|
8277
8518
|
await cmdLicense(positionals[0]);
|
|
8278
8519
|
break;
|