underpost 3.2.90 → 3.3.0
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/.github/workflows/ghpkg.ci.yml +7 -1
- package/.github/workflows/pwa-microservices-template-page.cd.yml +1 -16
- package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
- package/.github/workflows/release.cd.yml +1 -9
- package/CHANGELOG.md +110 -1
- package/CLI-HELP.md +139 -9
- package/README.md +5 -2
- package/bin/build.js +7 -5
- package/bin/deploy.js +1 -1
- package/deploy/lib/logging.sh +96 -0
- package/deploy/pwa-microservices-template/deploy.sh +72 -0
- package/deploy/release/deploy.sh +62 -0
- package/docker-compose.yml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +5 -1
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +52 -0
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/package.json +5 -5
- package/scripts/audit-selinux.sh +64 -0
- package/scripts/coverall-test.sh +24 -0
- package/scripts/gpu-diag.sh +0 -0
- package/scripts/ip-info.sh +0 -0
- package/scripts/k3s-node-setup.sh +18 -15
- package/scripts/kubeadm-node-setup.sh +12 -23
- package/scripts/link-local-underpost-cli.sh +0 -0
- package/scripts/lxd-vm-setup.sh +0 -0
- package/scripts/maas-nat-firewalld.sh +0 -0
- package/scripts/nat-iptables.sh +2 -0
- package/scripts/rhel-grpc-setup.sh +0 -0
- package/scripts/rocky-kickstart.sh +25 -9
- package/scripts/test-monitor.sh +1 -1
- package/src/cli/baremetal.js +1 -2
- package/src/cli/cloud-init.js +1 -1
- package/src/cli/cluster.js +73 -68
- package/src/cli/db.js +9 -2
- package/src/cli/deploy.js +21 -5
- package/src/cli/docker-compose.js +1 -1
- package/src/cli/env.js +1 -1
- package/src/cli/image.js +0 -1
- package/src/cli/index.js +121 -9
- package/src/cli/lxd.js +1 -1
- package/src/cli/monitor.js +1 -1
- package/src/cli/release.js +57 -22
- package/src/cli/repository.js +11 -9
- package/src/cli/run.js +36 -9
- package/src/cli/ssh.js +198 -77
- package/src/cli/system.js +26 -13
- package/src/cli/test.js +1 -1
- package/src/cli/vultr.js +583 -0
- package/src/cli/wireguard.js +2125 -0
- package/src/client-builder/client-build.js +20 -14
- package/src/db/mongo/MongooseDB.js +4 -0
- package/src/index.js +25 -1
- package/src/projects/underpost/catalog-underpost.js +4 -1
- package/src/server/backup.js +1 -1
- package/src/server/conf.js +18 -108
- package/src/server/cron.js +249 -51
- package/src/server/dns.js +100 -6
- package/src/server/environment.js +98 -0
- package/src/server/forward-proxy.js +549 -0
- package/src/server/middlewares.js +56 -1
- package/src/server/process.js +0 -1
- package/src/server/selinux.js +185 -0
- package/src/server/systemd.js +205 -0
- package/src/server/underpost-compression.js +186 -0
- package/src/server/underpost-gateway.js +20 -10
- package/src/server/underpost-ingress.js +18 -2
- package/test/selinux.test.js +71 -0
- package/test/underpost-gateway.test.js +41 -0
- package/test/underpost-ingress.test.js +52 -0
- package/test/wireguard-edge.test.js +1177 -0
package/src/cli/cluster.js
CHANGED
|
@@ -4,10 +4,18 @@
|
|
|
4
4
|
* @namespace UnderpostCluster
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { clusterTypeFactory, gatewayApiEnabledFactory,
|
|
7
|
+
import { clusterTypeFactory, gatewayApiEnabledFactory, resolveReplicaCount } from '../server/conf.js';
|
|
8
|
+
import { getNpmRootPath, HOST_VOLUME_ROOT } from '../server/environment.js';
|
|
8
9
|
import { loggerFactory } from '../server/logger.js';
|
|
9
10
|
import { shellExec } from '../server/process.js';
|
|
10
11
|
import { crictlCommandFactory, resolveCriSocket } from '../server/cri.js';
|
|
12
|
+
import {
|
|
13
|
+
runSELinuxCommands,
|
|
14
|
+
selinuxContainerSharedContextCommandsFactory,
|
|
15
|
+
selinuxEnforcingCommandsFactory,
|
|
16
|
+
selinuxPackagesCommandFactory,
|
|
17
|
+
selinuxRestoreconCommandFactory,
|
|
18
|
+
} from '../server/selinux.js';
|
|
11
19
|
import { UNDERPOST_GATEWAY, seedDefaultStatusPage } from '../server/underpost-gateway.js';
|
|
12
20
|
import {
|
|
13
21
|
UNDERPOST_INGRESS,
|
|
@@ -24,17 +32,31 @@ import Underpost from '../index.js';
|
|
|
24
32
|
|
|
25
33
|
const logger = loggerFactory(import.meta);
|
|
26
34
|
|
|
27
|
-
// Pinned Gateway API control plane. The CRD release and the implementation are
|
|
28
|
-
// upgraded together, and the pairing is not free choice: Envoy Gateway builds
|
|
29
|
-
// against one `sigs.k8s.io/gateway-api` version (v1.8.3 -> v1.5.1) and reads
|
|
30
|
-
// fields the older CRD schemas do not define, which the API
|
|
31
|
-
// server silently strips. Check the implementation's go.mod before moving
|
|
32
|
-
// either pin.
|
|
33
35
|
const GATEWAY_API_RELEASE = 'v1.5.1';
|
|
34
36
|
const ENVOY_GATEWAY_VERSION = 'v1.8.3';
|
|
35
|
-
// Namespace the upstream Contour render deploys into, and the only one where an
|
|
36
|
-
// `app=envoy` selector resolves to its DaemonSet.
|
|
37
37
|
const CONTOUR_NAMESPACE = 'projectcontour';
|
|
38
|
+
const KUBEADM_CONTAINER_MOUNT_PATHS = ['/etc/kubernetes', '/var/lib/etcd', '/var/lib/calico'];
|
|
39
|
+
const HOST_VOLUME_PATHS = ['/data', '/opt/local-path-provisioner', HOST_VOLUME_ROOT];
|
|
40
|
+
const K3S_SELINUX_PATHS = [
|
|
41
|
+
'/usr/local/bin/k3s',
|
|
42
|
+
'/etc/rancher',
|
|
43
|
+
'/var/lib/rancher',
|
|
44
|
+
'/var/lib/kubelet',
|
|
45
|
+
'/var/lib/cni',
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const shareWithContainers = (paths = []) => {
|
|
49
|
+
for (const path of paths) shellExec(`sudo mkdir -p ${path}`);
|
|
50
|
+
runSELinuxCommands(selinuxContainerSharedContextCommandsFactory(paths), { execute: shellExec });
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const enforceSELinux = (paths = []) => {
|
|
54
|
+
shellExec(
|
|
55
|
+
`if [ -f /etc/redhat-release ] && command -v dnf >/dev/null 2>&1 && { ! command -v restorecon >/dev/null 2>&1 || ! command -v semanage >/dev/null 2>&1; }; then ${selinuxPackagesCommandFactory()}; fi`,
|
|
56
|
+
);
|
|
57
|
+
const commands = selinuxEnforcingCommandsFactory({ restorePaths: paths });
|
|
58
|
+
runSELinuxCommands(commands, { execute: shellExec });
|
|
59
|
+
};
|
|
38
60
|
|
|
39
61
|
/**
|
|
40
62
|
* @class UnderpostCluster
|
|
@@ -227,8 +249,11 @@ class UnderpostCluster {
|
|
|
227
249
|
// balancer. The platform exposes services explicitly via Project
|
|
228
250
|
// Contour / Envoy and NodePort services (see --node-port); leaving the
|
|
229
251
|
// K3s built-ins enabled would bind the same host ports and conflict.
|
|
230
|
-
shellExec(
|
|
252
|
+
shellExec(
|
|
253
|
+
`curl -sfL https://get.k3s.io | sh -s - $(if command -v selinuxenabled >/dev/null 2>&1 && selinuxenabled; then printf '%s' '--selinux'; fi) --disable=traefik --disable=servicelb`,
|
|
254
|
+
);
|
|
231
255
|
logger.info('K3s installation completed.');
|
|
256
|
+
runSELinuxCommands([selinuxRestoreconCommandFactory(K3S_SELINUX_PATHS)], { execute: shellExec });
|
|
232
257
|
|
|
233
258
|
Underpost.cluster.chown('k3s');
|
|
234
259
|
|
|
@@ -240,6 +265,7 @@ class UnderpostCluster {
|
|
|
240
265
|
} else if (options.kubeadm) {
|
|
241
266
|
Underpost.cluster.config();
|
|
242
267
|
Underpost.cluster.natSetup({ underpostRoot });
|
|
268
|
+
shareWithContainers([...KUBEADM_CONTAINER_MOUNT_PATHS, ...HOST_VOLUME_PATHS]);
|
|
243
269
|
logger.info('Initializing Kubeadm control plane...');
|
|
244
270
|
// Set default values if not provided
|
|
245
271
|
const podNetworkCidr = options.podNetworkCidr || '192.168.0.0/16';
|
|
@@ -272,6 +298,13 @@ class UnderpostCluster {
|
|
|
272
298
|
shellExec(
|
|
273
299
|
`kubectl apply -f https://cdn.jsdelivr.net/gh/rancher/local-path-provisioner@master/deploy/local-path-storage.yaml`,
|
|
274
300
|
);
|
|
301
|
+
|
|
302
|
+
// The CNI lands asynchronously, and the node stays NotReady until it
|
|
303
|
+
// does. Block here so the deploy steps that follow (MongoDB and the
|
|
304
|
+
// application workloads) schedule onto a node that can actually run
|
|
305
|
+
// them, instead of racing the Calico rollout.
|
|
306
|
+
logger.info('Waiting for the node to become Ready (Calico rollout)...');
|
|
307
|
+
shellExec(`kubectl wait --for=condition=Ready node --all --timeout=300s`);
|
|
275
308
|
} else {
|
|
276
309
|
Underpost.cluster.config();
|
|
277
310
|
Underpost.cluster.natSetup({ underpostRoot });
|
|
@@ -1198,11 +1231,13 @@ EOF
|
|
|
1198
1231
|
}
|
|
1199
1232
|
}
|
|
1200
1233
|
const liveConf = canHotReload
|
|
1201
|
-
? `${
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1234
|
+
? `${
|
|
1235
|
+
execIngress('cat /tmp/nginx.conf', {
|
|
1236
|
+
stdout: true,
|
|
1237
|
+
silent: true,
|
|
1238
|
+
silentOnError: true,
|
|
1239
|
+
}) || ''
|
|
1240
|
+
}`
|
|
1206
1241
|
: '';
|
|
1207
1242
|
const shouldHotReload = canHotReload && liveConf.trimEnd() !== `${conf}`.trimEnd();
|
|
1208
1243
|
|
|
@@ -1222,10 +1257,10 @@ EOF
|
|
|
1222
1257
|
{ silent: true },
|
|
1223
1258
|
);
|
|
1224
1259
|
} catch (error) {
|
|
1225
|
-
execIngress(
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
);
|
|
1260
|
+
execIngress(`sh -c 'cp /tmp/nginx.previous.conf /tmp/nginx.conf && nginx -s reload -c /tmp/nginx.conf'`, {
|
|
1261
|
+
silent: true,
|
|
1262
|
+
silentOnError: true,
|
|
1263
|
+
});
|
|
1229
1264
|
throw error;
|
|
1230
1265
|
}
|
|
1231
1266
|
}
|
|
@@ -1241,10 +1276,10 @@ EOF
|
|
|
1241
1276
|
`);
|
|
1242
1277
|
} catch (error) {
|
|
1243
1278
|
if (shouldHotReload)
|
|
1244
|
-
execIngress(
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
);
|
|
1279
|
+
execIngress(`sh -c 'cp /tmp/nginx.previous.conf /tmp/nginx.conf && nginx -s reload -c /tmp/nginx.conf'`, {
|
|
1280
|
+
silent: true,
|
|
1281
|
+
silentOnError: true,
|
|
1282
|
+
});
|
|
1248
1283
|
throw error;
|
|
1249
1284
|
}
|
|
1250
1285
|
if (shouldHotReload)
|
|
@@ -1257,18 +1292,16 @@ EOF
|
|
|
1257
1292
|
// ready replica that made canHotReload true. Do not let the caller delete
|
|
1258
1293
|
// the old route kind until the replacement edge is actually Available.
|
|
1259
1294
|
if (!canHotReload || changesNode)
|
|
1260
|
-
shellExec(
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
);
|
|
1295
|
+
shellExec(`kubectl rollout status deployment/${UNDERPOST_INGRESS.name} -n ${namespace} --timeout=5m`, {
|
|
1296
|
+
silent: true,
|
|
1297
|
+
});
|
|
1264
1298
|
else if (shouldHotReload)
|
|
1265
1299
|
// `nginx -s reload` returns after signalling the master. Give it one
|
|
1266
1300
|
// scheduling turn to start the new workers before the caller removes
|
|
1267
1301
|
// the old stack's route object.
|
|
1268
1302
|
shellExec('sleep 1', { silent: true });
|
|
1269
1303
|
shellExec(
|
|
1270
|
-
`kubectl wait --for=condition=Available deployment/${UNDERPOST_INGRESS.name} ` +
|
|
1271
|
-
`-n ${namespace} --timeout=5m`,
|
|
1304
|
+
`kubectl wait --for=condition=Available deployment/${UNDERPOST_INGRESS.name} ` + `-n ${namespace} --timeout=5m`,
|
|
1272
1305
|
{ silent: true },
|
|
1273
1306
|
);
|
|
1274
1307
|
execIngress('nginx -t -c /tmp/nginx.conf', { silent: true });
|
|
@@ -1350,26 +1383,17 @@ EOF
|
|
|
1350
1383
|
config(options = { underpostRoot: '.' }) {
|
|
1351
1384
|
const { underpostRoot } = options;
|
|
1352
1385
|
console.log('Applying host configuration: SELinux, Docker, Containerd, and Sysctl settings.');
|
|
1353
|
-
|
|
1354
|
-
shellExec(`sudo setenforce 0`, {
|
|
1355
|
-
silentOnError: true,
|
|
1356
|
-
});
|
|
1357
|
-
shellExec(`sudo sed -i 's/^SELINUX=enforcing$/SELINUX=permissive/' /etc/selinux/config`, {
|
|
1358
|
-
silentOnError: true,
|
|
1359
|
-
});
|
|
1386
|
+
enforceSELinux(['/var/lib/kubelet', '/var/lib/containerd']);
|
|
1360
1387
|
|
|
1361
1388
|
// Enable and start Docker and Kubelet services
|
|
1362
1389
|
shellExec(`sudo systemctl enable --now docker`); // Docker might not be needed for K3s
|
|
1363
1390
|
shellExec(`sudo systemctl enable --now kubelet`); // Kubelet might not be needed for K3s (K3s uses its own agent)
|
|
1364
1391
|
|
|
1365
|
-
// Configure containerd for
|
|
1366
|
-
// This is crucial for kubelet/k3s to interact correctly with containerd
|
|
1392
|
+
// Configure containerd for systemd cgroups and SELinux labeling.
|
|
1367
1393
|
shellExec(`containerd config default | sudo tee /etc/containerd/config.toml > /dev/null`);
|
|
1368
1394
|
shellExec(`sudo sed -i -e "s/SystemdCgroup = false/SystemdCgroup = true/g" /etc/containerd/config.toml`);
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
// `sudo sed -i '/SystemdCgroup = true/a selinux_disabled = true' /etc/containerd/config.toml`,
|
|
1372
|
-
// );
|
|
1395
|
+
shellExec(`sudo sed -i -e "s/enable_selinux = false/enable_selinux = true/g" /etc/containerd/config.toml`);
|
|
1396
|
+
runSELinuxCommands([selinuxRestoreconCommandFactory('/etc/containerd')], { execute: shellExec });
|
|
1373
1397
|
// Restart docker after containerd config changes. Rocky 9 uses systemctl,
|
|
1374
1398
|
// not the legacy service command.
|
|
1375
1399
|
shellExec(`sudo systemctl restart docker || sudo service docker restart || true`);
|
|
@@ -1400,7 +1424,7 @@ EOF
|
|
|
1400
1424
|
* This applies only what K3s genuinely requires, and every step is guarded
|
|
1401
1425
|
* so it is a no-op when the relevant tooling is absent (e.g. minimal images
|
|
1402
1426
|
* without SELinux userspace):
|
|
1403
|
-
* - SELinux
|
|
1427
|
+
* - SELinux remains Enforcing.
|
|
1404
1428
|
* - swap off (Kubernetes best practice).
|
|
1405
1429
|
* - br_netfilter + bridge/forward sysctls (pod networking).
|
|
1406
1430
|
* - inotify limits.
|
|
@@ -1415,16 +1439,8 @@ EOF
|
|
|
1415
1439
|
// forever (the upstream unit ships TimeoutStartSec=0).
|
|
1416
1440
|
shellExec(`if systemctl is-active --quiet firewalld; then sudo systemctl disable --now firewalld; fi`);
|
|
1417
1441
|
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
// mode, so this is a best-effort dev convenience, not a hard requirement.
|
|
1421
|
-
shellExec(`if command -v setenforce >/dev/null 2>&1; then sudo setenforce 0; fi`, {
|
|
1422
|
-
silentOnError: true,
|
|
1423
|
-
});
|
|
1424
|
-
shellExec(
|
|
1425
|
-
`if [ -f /etc/selinux/config ]; then sudo sed -i 's/^SELINUX=enforcing$/SELINUX=permissive/' /etc/selinux/config; fi`,
|
|
1426
|
-
{ silentOnError: true },
|
|
1427
|
-
);
|
|
1442
|
+
enforceSELinux(K3S_SELINUX_PATHS);
|
|
1443
|
+
shareWithContainers(HOST_VOLUME_PATHS);
|
|
1428
1444
|
|
|
1429
1445
|
// Disable swap. `swapoff -a` is a no-op without swap; the sed only edits
|
|
1430
1446
|
// fstab when a swap line is present.
|
|
@@ -1697,17 +1713,8 @@ fi`);
|
|
|
1697
1713
|
if (options.removeVolumeHostPaths) Underpost.cluster._cleanHostPathPvs();
|
|
1698
1714
|
else logger.info(' -> Skipping (pass --remove-volume-host-paths to enable).');
|
|
1699
1715
|
|
|
1700
|
-
logger.info('Phase 2/7: SELinux
|
|
1701
|
-
|
|
1702
|
-
shellExec(
|
|
1703
|
-
`if [ -f /etc/selinux/config ]; then sudo sed -i 's/^SELINUX=enforcing$/SELINUX=permissive/' /etc/selinux/config; fi`,
|
|
1704
|
-
);
|
|
1705
|
-
shellExec(
|
|
1706
|
-
`if command -v restorecon >/dev/null 2>&1 && [ -d /var/lib/kubelet ]; then sudo restorecon -Rv /var/lib/kubelet; fi`,
|
|
1707
|
-
);
|
|
1708
|
-
shellExec(
|
|
1709
|
-
`if command -v restorecon >/dev/null 2>&1 && [ -d /var/lib/containerd ]; then sudo restorecon -Rv /var/lib/containerd; fi`,
|
|
1710
|
-
);
|
|
1716
|
+
logger.info('Phase 2/7: Enforcing SELinux and restoring runtime contexts...');
|
|
1717
|
+
enforceSELinux(['/var/lib/kubelet', '/var/lib/containerd']);
|
|
1711
1718
|
|
|
1712
1719
|
logger.info('Phase 3/7: Stopping host kubelet and container runtimes (kubeadm-scope only)...');
|
|
1713
1720
|
shellExec(`if systemctl is-active --quiet kubelet; then sudo systemctl stop kubelet; fi`);
|
|
@@ -1981,10 +1988,8 @@ EOF`);
|
|
|
1981
1988
|
shellExec(`sudo rm -f /etc/sysctl.d/99-k8s-ipforward.conf`);
|
|
1982
1989
|
shellExec(`sudo rm -f /etc/sysctl.d/99-k8s.conf`);
|
|
1983
1990
|
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
// shellExec(`sudo setenforce 1`);
|
|
1987
|
-
// shellExec(`sudo sed -i 's/^SELINUX=permissive$/SELINUX=enforcing/' /etc/selinux/config`);
|
|
1991
|
+
console.log('Keeping SELinux in enforcing mode...');
|
|
1992
|
+
enforceSELinux();
|
|
1988
1993
|
|
|
1989
1994
|
console.log('Uninstall process completed.');
|
|
1990
1995
|
},
|
package/src/cli/db.js
CHANGED
|
@@ -6,12 +6,19 @@
|
|
|
6
6
|
* Supports MariaDB and MongoDB with import/export capabilities, Git integration, and multi-pod operations.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
loadConfInstances,
|
|
11
|
+
loadConfServerJson,
|
|
12
|
+
loadReplicas,
|
|
13
|
+
mergeFile,
|
|
14
|
+
pathPortAssignmentFactory,
|
|
15
|
+
splitFileFactory,
|
|
16
|
+
} from '../server/conf.js';
|
|
10
17
|
import { loggerFactory } from '../server/logger.js';
|
|
11
18
|
import { shellExec } from '../server/process.js';
|
|
12
19
|
import fs from 'fs-extra';
|
|
13
20
|
import { DataBaseProviderService } from '../db/DataBaseProvider.js';
|
|
14
|
-
import {
|
|
21
|
+
import { loadCronDeployEnv } from '../server/cron.js';
|
|
15
22
|
import { MongoBootstrap } from '../db/mongo/MongoBootstrap.js';
|
|
16
23
|
import Underpost from '../index.js';
|
|
17
24
|
import { timer } from '../client/components/core/CommonJs.js';
|
package/src/cli/deploy.js
CHANGED
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
buildProxyRouter,
|
|
11
11
|
clusterTypeFactory,
|
|
12
12
|
Config,
|
|
13
|
-
cronDeployIdResolve,
|
|
14
13
|
deployHostsFactory,
|
|
15
14
|
deployRangePortFactory,
|
|
16
15
|
gatewayApiEnabledFactory,
|
|
@@ -24,8 +23,11 @@ import {
|
|
|
24
23
|
schedulableNodeFactory,
|
|
25
24
|
trafficFromRoutingInfoFactory,
|
|
26
25
|
} from '../server/conf.js';
|
|
26
|
+
import { cronDeployIdResolve } from '../server/cron.js';
|
|
27
27
|
import { loggerFactory } from '../server/logger.js';
|
|
28
|
+
import { HOST_VOLUME_ROOT } from '../server/environment.js';
|
|
28
29
|
import { shellExec } from '../server/process.js';
|
|
30
|
+
import { runSELinuxCommands, selinuxRestoreconCommandFactory } from '../server/selinux.js';
|
|
29
31
|
import { INTERNAL_READY_PATH, INTERNAL_HEALTH_PATH } from '../server/runtime-status.js';
|
|
30
32
|
import { staticContextRoutesFactory, statusPageRoutesFactory } from '../client-builder/client-build.js';
|
|
31
33
|
import {
|
|
@@ -152,6 +154,13 @@ const gatewayDurationFactory = (value) => {
|
|
|
152
154
|
|
|
153
155
|
const logger = loggerFactory(import.meta);
|
|
154
156
|
|
|
157
|
+
// hostPath volume trees are written under the operator's home directory, which
|
|
158
|
+
// carries a label no unprivileged container can read. Cluster bring-up registers
|
|
159
|
+
// the persistent mapping for HOST_VOLUME_ROOT; this applies it to what a deploy
|
|
160
|
+
// just wrote. A no-op where SELinux or its userspace is absent.
|
|
161
|
+
const restoreContainerContext = (path) =>
|
|
162
|
+
runSELinuxCommands([selinuxRestoreconCommandFactory(path)], { execute: shellExec });
|
|
163
|
+
|
|
155
164
|
/**
|
|
156
165
|
* @class UnderpostDeploy
|
|
157
166
|
* @description Manages the deployment of applications and services.
|
|
@@ -724,7 +733,7 @@ ${Underpost.deploy
|
|
|
724
733
|
if (!volume.claimName) continue;
|
|
725
734
|
const pvcId = `${volume.claimName}-${deployId}-${env}-${deploymentVersion}`;
|
|
726
735
|
const pvId = pvcId.replace(/^pvc-/, 'pv-');
|
|
727
|
-
const hostPath =
|
|
736
|
+
const hostPath = `${HOST_VOLUME_ROOT}/${pvId}`;
|
|
728
737
|
volumeYaml += `---\n${Underpost.deploy.persistentVolumeFactory({
|
|
729
738
|
pvcId,
|
|
730
739
|
namespace: options.namespace,
|
|
@@ -1697,13 +1706,13 @@ ${rules}`;
|
|
|
1697
1706
|
|
|
1698
1707
|
/**
|
|
1699
1708
|
* Node directory backing the static utility's volume, following the same
|
|
1700
|
-
*
|
|
1709
|
+
* `HOST_VOLUME_ROOT/<pv>` convention as every other hostPath volume.
|
|
1701
1710
|
* @param {object} [options] - Deploy/run options.
|
|
1702
1711
|
* @returns {string} Absolute host path.
|
|
1703
1712
|
* @memberof UnderpostDeploy
|
|
1704
1713
|
*/
|
|
1705
1714
|
underpostGatewayRootFactory(options = {}) {
|
|
1706
|
-
return options.underpostGatewayRoot ||
|
|
1715
|
+
return options.underpostGatewayRoot || `${HOST_VOLUME_ROOT}/${UNDERPOST_GATEWAY.volumeName}`;
|
|
1707
1716
|
},
|
|
1708
1717
|
|
|
1709
1718
|
/**
|
|
@@ -1881,6 +1890,12 @@ ${rules}`;
|
|
|
1881
1890
|
? 'project'
|
|
1882
1891
|
: null,
|
|
1883
1892
|
});
|
|
1893
|
+
// The documents were just written under the operator's home tree, whose
|
|
1894
|
+
// policy label (user_home_t) the unprivileged gateway container cannot
|
|
1895
|
+
// read — it would answer 403 for every one of them. The persistent
|
|
1896
|
+
// mapping is registered at cluster bring-up; restore it here so files
|
|
1897
|
+
// this pass created carry it too.
|
|
1898
|
+
restoreContainerContext(hostRoot);
|
|
1884
1899
|
logger.info('Static edge documents placed', {
|
|
1885
1900
|
deployId,
|
|
1886
1901
|
podName: podName || '(no running workload; placed from this checkout)',
|
|
@@ -2632,7 +2647,7 @@ EOF`);
|
|
|
2632
2647
|
const clusterContext = options.clusterContext || 'kind';
|
|
2633
2648
|
const pvcId = `${volume.claimName}-${deployId}-${env}-${version}`;
|
|
2634
2649
|
const pvId = `${volume.claimName.replace('pvc-', 'pv-')}-${deployId}-${env}-${version}`;
|
|
2635
|
-
const rootVolumeHostPath =
|
|
2650
|
+
const rootVolumeHostPath = `${HOST_VOLUME_ROOT}/${pvId}`;
|
|
2636
2651
|
if (options.gitClean && volume.volumeMountPath) {
|
|
2637
2652
|
Underpost.repo.clean({ paths: [volume.volumeMountPath] });
|
|
2638
2653
|
}
|
|
@@ -2656,6 +2671,7 @@ EOF`);
|
|
|
2656
2671
|
// Target node is the control plane / current host: write directly.
|
|
2657
2672
|
if (!fs.existsSync(rootVolumeHostPath)) fs.mkdirSync(rootVolumeHostPath, { recursive: true });
|
|
2658
2673
|
fs.copySync(volume.volumeMountPath, rootVolumeHostPath);
|
|
2674
|
+
restoreContainerContext(rootVolumeHostPath);
|
|
2659
2675
|
} else {
|
|
2660
2676
|
// Target node is remote: fs.copySync would only write the control-plane
|
|
2661
2677
|
// filesystem, leaving the real node's hostPath empty. Ship the folder to
|
|
@@ -192,7 +192,7 @@ services:
|
|
|
192
192
|
MONGO_IMAGE=mongo:latest
|
|
193
193
|
VALKEY_IMAGE=valkey/valkey:latest
|
|
194
194
|
APP_IMAGE=underpost/underpost-engine
|
|
195
|
-
APP_TAG=v3.
|
|
195
|
+
APP_TAG=v3.3.0
|
|
196
196
|
PROXY_IMAGE=nginx:stable-alpine
|
|
197
197
|
PROMETHEUS_IMAGE=prom/prometheus:latest
|
|
198
198
|
GRAFANA_IMAGE=grafana/grafana:latest
|
package/src/cli/env.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @namespace UnderpostEnv
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { getNpmRootPath, writeEnv } from '../server/
|
|
7
|
+
import { getNpmRootPath, writeEnv } from '../server/environment.js';
|
|
8
8
|
import fs from 'fs-extra';
|
|
9
9
|
import { loggerFactory } from '../server/logger.js';
|
|
10
10
|
import dotenv from 'dotenv';
|
package/src/cli/image.js
CHANGED
|
@@ -10,7 +10,6 @@ import nodePath from 'path';
|
|
|
10
10
|
import crypto from 'crypto';
|
|
11
11
|
import { loggerFactory } from '../server/logger.js';
|
|
12
12
|
import Underpost from '../index.js';
|
|
13
|
-
import { getNpmRootPath } from '../server/conf.js';
|
|
14
13
|
import { shellExec } from '../server/process.js';
|
|
15
14
|
import { crictlCommandFactory } from '../server/cri.js';
|
|
16
15
|
|
package/src/cli/index.js
CHANGED
|
@@ -2,7 +2,8 @@ import dotenv from 'dotenv';
|
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
|
|
4
4
|
import { Command } from 'commander';
|
|
5
|
-
import {
|
|
5
|
+
import { loadConf } from '../server/conf.js';
|
|
6
|
+
import { getNpmRootPath, getUnderpostRootPath } from '../server/environment.js';
|
|
6
7
|
import { commitData } from '../client/components/core/CommonJs.js';
|
|
7
8
|
|
|
8
9
|
import Underpost from '../index.js';
|
|
@@ -51,6 +52,7 @@ program
|
|
|
51
52
|
.option('--merge-zip <build-prefix>', 'Merge split ZIP parts back into a single ZIP file for the given build prefix')
|
|
52
53
|
.option('--lite-build', 'Skip full build (default is full build)')
|
|
53
54
|
.option('--icons-build', 'Build icons')
|
|
55
|
+
.option('--ssr', 'Rebuild only SSR views defined in conf.ssr.json, leaving client assets untouched')
|
|
54
56
|
.description('Builds client assets, single replicas, and/or syncs environment ports.')
|
|
55
57
|
.action(Underpost.repo.client);
|
|
56
58
|
|
|
@@ -143,7 +145,7 @@ program
|
|
|
143
145
|
'--switch-repo <url>',
|
|
144
146
|
'Switches the git remote (origin) to <url> and force-pulls the target branch, overwriting the current working tree (discards local commits and tracked changes). Accepts a full URL or "owner/repo".',
|
|
145
147
|
)
|
|
146
|
-
.option('--target-branch <branch>', 'Target branch for --switch-repo (default:
|
|
148
|
+
.option('--target-branch <branch>', 'Target branch for --switch-repo (default: remote default branch).')
|
|
147
149
|
.description('Manages commits to a GitHub repository, supporting various commit types and options.')
|
|
148
150
|
.action(Underpost.repo.commit);
|
|
149
151
|
|
|
@@ -254,6 +256,10 @@ program
|
|
|
254
256
|
.option('--ban-egress-clear', 'Clears all banned egress IP addresses.')
|
|
255
257
|
.option('--ban-both-add', 'Adds IP addresses to both banned ingress and egress lists.')
|
|
256
258
|
.option('--ban-both-remove', 'Removes IP addresses from both banned ingress and egress lists.')
|
|
259
|
+
.option('--block-all-egress', 'Blocks all outbound traffic from this host (keeps established/related connections).')
|
|
260
|
+
.option('--unblock-all-egress', 'Unblocks all outbound traffic and restores default ACCEPT policy.')
|
|
261
|
+
.option('--block-all-ingress', 'Blocks all new inbound traffic to this host (keeps established/related connections).')
|
|
262
|
+
.option('--unblock-all-ingress', 'Unblocks all inbound traffic and restores default ACCEPT policy.')
|
|
257
263
|
.option('--mac', 'Prints the MAC address of the main network interface.')
|
|
258
264
|
.description('Displays the current public machine IP addresses.')
|
|
259
265
|
.action(Underpost.dns.ipDispatcher);
|
|
@@ -592,19 +598,23 @@ program
|
|
|
592
598
|
|
|
593
599
|
program
|
|
594
600
|
.command('cron')
|
|
595
|
-
.argument(
|
|
601
|
+
.argument(
|
|
602
|
+
'[deploy-list]',
|
|
603
|
+
'A comma-separated list of deployment IDs (e.g., "default-a,default-b"). In manifest modes its first entry is the manifest owner deploy-id.',
|
|
604
|
+
)
|
|
596
605
|
.argument(
|
|
597
606
|
'[job-list]',
|
|
598
|
-
`A comma-separated list of job IDs. Options: ${Underpost.cron.getJobsIDs()}. Defaults to all available jobs.`,
|
|
607
|
+
`A comma-separated list of job IDs. Options: ${Underpost.cron.getJobsIDs()}. Defaults to all available jobs, and restricts which jobs are generated in manifest modes.`,
|
|
599
608
|
)
|
|
600
609
|
.option('--generate-k8s-cronjobs', 'Generates Kubernetes CronJob YAML manifests from cron configuration.')
|
|
601
|
-
.option('--apply', '
|
|
610
|
+
.option('--apply', 'Generates and applies K8s CronJob manifests to the cluster via kubectl (never runs jobs).')
|
|
602
611
|
.option(
|
|
603
|
-
'--setup-start
|
|
604
|
-
'Updates deploy-
|
|
612
|
+
'--setup-start',
|
|
613
|
+
'Updates deploy-list package.json start script and generates+applies its K8s CronJob manifests.',
|
|
605
614
|
)
|
|
606
615
|
.option('--namespace <namespace>', 'Kubernetes namespace for the CronJob resources (default: "default").')
|
|
607
616
|
.option('--image <image>', 'Custom container image for the CronJob pods.')
|
|
617
|
+
.option('--node-name <node-name>', 'Pins the CronJob pods to this node via a kubernetes.io/hostname nodeSelector.')
|
|
608
618
|
.option('--git', 'Pass --git flag to cron job execution.')
|
|
609
619
|
.option('--cmd <cmd>', 'Optional pre-script commands to run before cron execution.')
|
|
610
620
|
.option('--dev', 'Use local ./ base path instead of global underpost installation.')
|
|
@@ -614,7 +624,7 @@ program
|
|
|
614
624
|
.option('--dry-run', 'Preview cron jobs without executing them.')
|
|
615
625
|
.option(
|
|
616
626
|
'--create-job-now',
|
|
617
|
-
'
|
|
627
|
+
'Creates a Job from each CronJob on the cluster now (implies manifest mode; combine with --apply to publish first).',
|
|
618
628
|
)
|
|
619
629
|
.description('Manages cron jobs: execute jobs directly or generate and apply K8s CronJob manifests.')
|
|
620
630
|
.action(Underpost.cron.callback);
|
|
@@ -696,6 +706,108 @@ program
|
|
|
696
706
|
.description('Manages SSH credentials and sessions for remote access to cluster nodes or services.')
|
|
697
707
|
.action(Underpost.ssh.callback);
|
|
698
708
|
|
|
709
|
+
// `underpost wireguard` and `underpost haproxy` are two entrypoints onto one
|
|
710
|
+
// subsystem: the L3 transport and the gateway in front of it are configured from
|
|
711
|
+
// the same deploy state, so they share a single option set and a single action
|
|
712
|
+
// rather than drifting into two half-overlapping command surfaces.
|
|
713
|
+
const edgeCommandFactory = (name, description) =>
|
|
714
|
+
program
|
|
715
|
+
.command(name)
|
|
716
|
+
.option(
|
|
717
|
+
'--deploy-id <deploy-id>',
|
|
718
|
+
'Deploy IDs whose conf.server.json/conf.instances.json define the routes. ' +
|
|
719
|
+
'Accepts one id or a comma-separated list; defaults to "dd", every deploy in dd.router, ' +
|
|
720
|
+
'because the edge holds one pair of map files for the whole cluster.',
|
|
721
|
+
)
|
|
722
|
+
.option('--interface <name>', 'WireGuard interface name (default: "wg0").')
|
|
723
|
+
.option('--wireguard-install', 'Installs the wireguard-tools, haproxy and iptables host packages.')
|
|
724
|
+
.option('--wireguard-setup', 'Generates keys, builds the interface config, and applies local network rules.')
|
|
725
|
+
.option('--server', 'Configures this node as the hub endpoint accepting inbound tunnel traffic.')
|
|
726
|
+
.option('--client', 'Configures this node as a spoke endpoint maintaining an outbound tunnel.')
|
|
727
|
+
.option('--port <port>', 'WireGuard UDP listening port (default: 51820).')
|
|
728
|
+
.option(
|
|
729
|
+
'--cidr <cidr>',
|
|
730
|
+
'Hub interface address with prefix when used with --server (e.g. "10.0.0.1/24"); ' +
|
|
731
|
+
'the overlay subnet a spoke routes back through the hub when used with --client (default: "10.0.0.0/24").',
|
|
732
|
+
)
|
|
733
|
+
.option('--peer-ip <ip>', 'Tunnel address of the target spoke. Required with --client and --peer-add.')
|
|
734
|
+
.option('--endpoint <host:port>', 'Hub host and port a spoke dials. Required with --client.')
|
|
735
|
+
.option('--public-key <key>', 'Hub public key with --client; spoke public key with --peer-add.')
|
|
736
|
+
.option('--peer-add <peer-id>', 'Registers a spoke and applies it to the running hub without a restart.')
|
|
737
|
+
.option('--peer-remove <peer-id>', 'Removes a spoke from the registry and from the running hub.')
|
|
738
|
+
.option('--allowed-ips <cidrs>', 'Comma-separated CIDRs routed to the spoke (e.g. "10.0.0.2/32,192.168.10.0/24").')
|
|
739
|
+
.option('--hosts <hosts>', 'Comma-separated hostnames bound to the spoke, overriding instance resolution.')
|
|
740
|
+
.option('--instances <instances>', 'Comma-separated conf.instances.json ids bound to the spoke.')
|
|
741
|
+
.option('--default', 'Marks the spoke as the fallback for hostnames that match no other binding.')
|
|
742
|
+
.option('--haproxy-setup', 'Installs HAProxy, publishes the current routes, and enables the daemon.')
|
|
743
|
+
.option('--haproxy-sync', 'Recompiles the SNI/Host maps from deploy config and hot-reloads HAProxy.')
|
|
744
|
+
.option(
|
|
745
|
+
'--status',
|
|
746
|
+
'Prints the whole edge context without changing anything: role, interface, tunnel address, ' +
|
|
747
|
+
'public key, daemon states, peers with their bindings and link health, and the resolved routing.',
|
|
748
|
+
)
|
|
749
|
+
.option(
|
|
750
|
+
'--build-conf',
|
|
751
|
+
'Writes only engine-private/deploy/conf.wireguard.json and touches no host state. ' +
|
|
752
|
+
'Combine with --wireguard-setup / --peer-add / --peer-remove to author the topology off-box; ' +
|
|
753
|
+
'alone it normalizes and validates the existing registry.',
|
|
754
|
+
)
|
|
755
|
+
.option(
|
|
756
|
+
'--forward-proxy-server',
|
|
757
|
+
'Ensures the hub HTTP/CONNECT forward proxy runs as the underpost-forward-proxy systemd service, ' +
|
|
758
|
+
'bound to the tunnel address only (default port 1080), and returns. Authenticates every request with ' +
|
|
759
|
+
'FORWARD_PROXY_API_KEY, so spokes can reach the internet through the VPS public address. Idempotent: ' +
|
|
760
|
+
're-running converges on the one service and restarts it only when the unit actually changed.',
|
|
761
|
+
)
|
|
762
|
+
.option(
|
|
763
|
+
'--forward-proxy-server-host <host>',
|
|
764
|
+
'Address the forward proxy binds, overriding the hub tunnel address from the registry.',
|
|
765
|
+
)
|
|
766
|
+
.option('--forward-proxy-server-port <port>', 'Port the forward proxy binds (default: 1080).')
|
|
767
|
+
.option(
|
|
768
|
+
'--ssh-forward-port <port>',
|
|
769
|
+
'Publishes the default spoke SSH port on this public TCP port of the hub, so CI with no fixed ' +
|
|
770
|
+
'address can reach the cluster node (e.g. 2222). "0" closes it. Stored in the registry.',
|
|
771
|
+
)
|
|
772
|
+
.option('--wireguard-start', 'Enables and starts wg-quick@<interface> and the QUIC forward.')
|
|
773
|
+
.option('--wireguard-stop', 'Tears down the interface and removes its transient packet rules.')
|
|
774
|
+
.option('--wireguard-reset', 'Removes generated configs and packet rules, keeping the key pair and registry.')
|
|
775
|
+
.option('--wireguard-reinstall', 'Full purge, package reinstall and re-key; every spoke must re-register.')
|
|
776
|
+
.option('--dry-run', 'Prints the files and commands the run would apply, without touching the host.')
|
|
777
|
+
.description(description)
|
|
778
|
+
.action(Underpost.wireguard.callback);
|
|
779
|
+
|
|
780
|
+
edgeCommandFactory(
|
|
781
|
+
'wireguard',
|
|
782
|
+
'Manages the WireGuard L3 hub-and-spoke transport and the HAProxy edge gateway in front of it.',
|
|
783
|
+
);
|
|
784
|
+
edgeCommandFactory(
|
|
785
|
+
'haproxy',
|
|
786
|
+
'Manages the HAProxy edge gateway over the WireGuard transport (same subsystem as `underpost wireguard`).',
|
|
787
|
+
);
|
|
788
|
+
|
|
789
|
+
program
|
|
790
|
+
.command('vultr')
|
|
791
|
+
.argument('[deploy-list]', 'A comma-separated list of deployment IDs, logged for attribution.')
|
|
792
|
+
.option('--instance-id <instance-id>', 'Vultr instance id to meter (default: VULTR_INSTANCE_ID).')
|
|
793
|
+
.option('--api-key <api-key>', 'Vultr API key (default: VULTR_API_KEY). Prefer the environment over this flag.')
|
|
794
|
+
.option(
|
|
795
|
+
'--threshold <ratio>',
|
|
796
|
+
'Fraction of the plan quota that triggers the egress block; "0.80" and "80" are both accepted (default: 0.80).',
|
|
797
|
+
)
|
|
798
|
+
.option('--metric <metric>', '"total" (incoming + outgoing, default) or "outgoing" for egress alone.')
|
|
799
|
+
.option('--month <yyyy-mm>', 'Billing month to sum (default: the current UTC month).')
|
|
800
|
+
.option('--all-dates', 'Sum every daily bucket the API returns instead of scoping to one month.')
|
|
801
|
+
.option('--host <ip>', 'Edge VPS to block (default: VULTR_VPS_IP, then DEFAULT_SSH_HOST).')
|
|
802
|
+
.option('--user <user>', 'SSH user on the edge VPS (default: VULTR_SSH_USER, then DEFAULT_SSH_USER, then "root").')
|
|
803
|
+
.option('--key-path <path>', 'SSH private key (default: VULTR_SSH_KEY_PATH, then DEFAULT_SSH_KEY_PATH).')
|
|
804
|
+
.option('--port <port>', 'SSH port on the edge VPS (default: VULTR_SSH_PORT, then DEFAULT_SSH_PORT, then 22).')
|
|
805
|
+
.option('--force', 'Re-apply the egress block even if it was already applied for this cycle.')
|
|
806
|
+
.option('--auto-unblock', 'Restore egress automatically once consumption falls back under the threshold.')
|
|
807
|
+
.option('--dry-run', 'Reports the consumption and the action it would take, without touching the edge VPS.')
|
|
808
|
+
.description('Meters the edge VPS bandwidth against its Vultr plan quota and blocks egress before overage accrues.')
|
|
809
|
+
.action(Underpost.vultr.callback);
|
|
810
|
+
|
|
699
811
|
program
|
|
700
812
|
.command('run')
|
|
701
813
|
.argument('<runner-id>', `The runner ID to run. Options: ${Underpost.run.RUNNERS}.`)
|
|
@@ -769,7 +881,7 @@ program
|
|
|
769
881
|
.option('--cmd-cron-jobs <cmd-cron-jobs>', 'Pre-script commands to run before cron job execution.')
|
|
770
882
|
.option(
|
|
771
883
|
'--deploy-id-cron-jobs <deploy-id-cron-jobs>',
|
|
772
|
-
'
|
|
884
|
+
'Cron deploy-id to set up during sync; defaults to dd.cron, "none" skips cron setup entirely.',
|
|
773
885
|
)
|
|
774
886
|
.option('--timezone <timezone>', 'Sets the timezone for the runner execution.')
|
|
775
887
|
.option('--kubeadm', 'Sets the kubeadm cluster context for the runner execution.')
|
package/src/cli/lxd.js
CHANGED
package/src/cli/monitor.js
CHANGED
|
@@ -8,10 +8,10 @@ import {
|
|
|
8
8
|
loadReplicas,
|
|
9
9
|
pathPortAssignmentFactory,
|
|
10
10
|
loadConfServerJson,
|
|
11
|
-
loadCronDeployEnv,
|
|
12
11
|
etcHostFactory,
|
|
13
12
|
deployRangePortFactory,
|
|
14
13
|
} from '../server/conf.js';
|
|
14
|
+
import { loadCronDeployEnv } from '../server/cron.js';
|
|
15
15
|
import { loggerFactory } from '../server/logger.js';
|
|
16
16
|
import { timer } from '../client/components/core/CommonJs.js';
|
|
17
17
|
import {
|