underpost 3.2.30 → 3.2.70
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 +87 -0
- package/.github/workflows/npmpkg.ci.yml +9 -6
- package/.github/workflows/pwa-microservices-template-page.cd.yml +0 -7
- package/CHANGELOG.md +121 -1
- package/CLI-HELP.md +12 -6
- package/README.md +2 -2
- package/bin/build.js +30 -0
- package/bin/deploy.js +2 -2
- package/bump.config.js +1 -0
- package/docker-compose.yml +26 -26
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/package.json +10 -10
- package/scripts/disk-clean.sh +85 -60
- package/scripts/test-monitor.sh +1 -1
- package/src/cli/cluster.js +77 -4
- package/src/cli/deploy.js +9 -2
- package/src/cli/docker-compose.js +58 -1
- package/src/cli/fs.js +45 -24
- package/src/cli/image.js +129 -51
- package/src/cli/index.js +21 -6
- package/src/cli/release.js +46 -4
- package/src/cli/repository.js +85 -22
- package/src/cli/run.js +237 -108
- package/src/cli/secrets.js +75 -46
- package/src/index.js +1 -1
- package/src/server/catalog.js +2 -0
- package/src/server/conf.js +0 -3
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"type": "module",
|
|
3
3
|
"main": "src/index.js",
|
|
4
4
|
"name": "underpost",
|
|
5
|
-
"version": "3.2.
|
|
5
|
+
"version": "3.2.70",
|
|
6
6
|
"description": "Underpost Platform — end-to-end CI/CD and application-delivery toolchain CLI. Covers bare metal, Kubernetes, K3s, kubeadm, LXD, container/image orchestration, secrets, databases, cron jobs, monitoring, SSH, runners, PWA + Workbox delivery, and release orchestration. Extensible via downstream CLIs.",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"start": "node --max-old-space-size=8192 src/server",
|
|
@@ -71,14 +71,14 @@
|
|
|
71
71
|
},
|
|
72
72
|
"homepage": "https://github.com/underpostnet/pwa-microservices-template#readme",
|
|
73
73
|
"dependencies": {
|
|
74
|
-
"@fortawesome/fontawesome-free": "^7.
|
|
74
|
+
"@fortawesome/fontawesome-free": "^7.3.0",
|
|
75
75
|
"@fullcalendar/rrule": "^6.1.21",
|
|
76
76
|
"@grpc/grpc-js": "^1.14.4",
|
|
77
77
|
"@grpc/proto-loader": "^0.8.1",
|
|
78
78
|
"@neodrag/vanilla": "^2.3.1",
|
|
79
|
-
"adm-zip": "^0.5.
|
|
80
|
-
"ag-grid-community": "^
|
|
81
|
-
"axios": "^1.18.
|
|
79
|
+
"adm-zip": "^0.5.18",
|
|
80
|
+
"ag-grid-community": "^36.0.0",
|
|
81
|
+
"axios": "^1.18.1",
|
|
82
82
|
"bumpp": "^11.1.0",
|
|
83
83
|
"chai": "^6.2.2",
|
|
84
84
|
"clipboardy": "^5.3.1",
|
|
@@ -100,11 +100,11 @@
|
|
|
100
100
|
"express-slow-down": "^3.1.0",
|
|
101
101
|
"fast-json-stable-stringify": "^2.1.0",
|
|
102
102
|
"favicons": "^7.3.0",
|
|
103
|
-
"fs-extra": "^11.3.
|
|
103
|
+
"fs-extra": "^11.3.6",
|
|
104
104
|
"fullcalendar": "^6.1.21",
|
|
105
105
|
"helmet": "^8.2.0",
|
|
106
106
|
"html-minifier-terser": "^7.2.0",
|
|
107
|
-
"http-proxy-middleware": "^4.
|
|
107
|
+
"http-proxy-middleware": "^4.2.0",
|
|
108
108
|
"ignore-walk": "^9.0.0",
|
|
109
109
|
"iovalkey": "^0.3.3",
|
|
110
110
|
"json-colorizer": "^3.0.1",
|
|
@@ -112,9 +112,9 @@
|
|
|
112
112
|
"mariadb": "^3.5.3",
|
|
113
113
|
"marked": "^18.0.5",
|
|
114
114
|
"mocha": "^11.7.6",
|
|
115
|
-
"mongoose": "^9.7.
|
|
115
|
+
"mongoose": "^9.7.3",
|
|
116
116
|
"morgan": "^1.11.0",
|
|
117
|
-
"nodemailer": "^9.0.
|
|
117
|
+
"nodemailer": "^9.0.3",
|
|
118
118
|
"nodemon": "^3.0.1",
|
|
119
119
|
"peer": "^1.0.2",
|
|
120
120
|
"peerjs": "^1.5.5",
|
|
@@ -128,7 +128,7 @@
|
|
|
128
128
|
"split-file": "^2.3.0",
|
|
129
129
|
"swagger-autogen": "^2.23.7",
|
|
130
130
|
"swagger-ui-express": "^5.0.0",
|
|
131
|
-
"typedoc": "^0.28.
|
|
131
|
+
"typedoc": "^0.28.20",
|
|
132
132
|
"typescript": "^6.0.3",
|
|
133
133
|
"validator": "^13.15.35",
|
|
134
134
|
"vanilla-jsoneditor": "^3.12.0",
|
package/scripts/disk-clean.sh
CHANGED
|
@@ -7,20 +7,20 @@ set -euo pipefail
|
|
|
7
7
|
|
|
8
8
|
VACUUM_SIZE="200M" # journalctl vacuum target
|
|
9
9
|
LARGE_LOG_SIZE="+100M" # threshold for truncation (find syntax)
|
|
10
|
-
TMP_AGE_DAYS=
|
|
11
|
-
CACHE_AGE_DAYS=
|
|
10
|
+
TMP_AGE_DAYS=1 # remove files older than this from /tmp and /var/tmp
|
|
11
|
+
CACHE_AGE_DAYS=5 # remove caches older than this from /var/cache
|
|
12
12
|
KEEP_KERNELS=2 # keep this many latest kernels
|
|
13
13
|
REMOVE_OPT=0 # ALWAYS allow removing /opt entries (user requested)
|
|
14
14
|
|
|
15
15
|
timestamp() { date +"%F %T"; }
|
|
16
16
|
log() {
|
|
17
|
-
|
|
17
|
+
echo "$(timestamp) $*"
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
# must be root
|
|
21
21
|
if [ "$EUID" -ne 0 ]; then
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
echo "This script must be run as root. Exiting."
|
|
23
|
+
exit 1
|
|
24
24
|
fi
|
|
25
25
|
|
|
26
26
|
log "=== Starting Rocky Linux 9 automated cleanup (batch mode) ==="
|
|
@@ -46,8 +46,8 @@ journalctl --vacuum-size="$VACUUM_SIZE"
|
|
|
46
46
|
# 3) Truncate very large logs in /var/log (safer than delete)
|
|
47
47
|
log "Truncating logs in /var/log larger than ${LARGE_LOG_SIZE} (keeps zero-sized file so services don't break)..."
|
|
48
48
|
find /var/log -type f -size "$LARGE_LOG_SIZE" -print0 2>/dev/null | while IFS= read -r -d '' f; do
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
echo "Truncating $f"
|
|
50
|
+
: > "$f" || echo "Failed to truncate $f"
|
|
51
51
|
done
|
|
52
52
|
|
|
53
53
|
# 4) Clean /tmp and /var/tmp older than TMP_AGE_DAYS
|
|
@@ -62,87 +62,110 @@ find /var/cache -mindepth 1 -mtime +"$CACHE_AGE_DAYS" -print0 2>/dev/null | xarg
|
|
|
62
62
|
# 6) Clean user caches (~/.cache) and Downloads older than CACHE_AGE_DAYS
|
|
63
63
|
log "Cleaning user caches and old downloads (home dirs)..."
|
|
64
64
|
for homedir in /home/* /root; do
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
65
|
+
[ -d "$homedir" ] || continue
|
|
66
|
+
usercache="$homedir/.cache"
|
|
67
|
+
userdownloads="$homedir/Downloads"
|
|
68
|
+
usertrash="$homedir/.local/share/Trash"
|
|
69
|
+
|
|
70
|
+
if [ -d "$usercache" ]; then
|
|
71
|
+
log "Removing contents of $usercache"
|
|
72
|
+
rm -rf "${usercache:?}/"* 2>/dev/null
|
|
73
|
+
fi
|
|
74
|
+
if [ -d "$userdownloads" ]; then
|
|
75
|
+
log "Removing files older than ${CACHE_AGE_DAYS} days from $userdownloads"
|
|
76
|
+
find "$userdownloads" -type f -mtime +"$CACHE_AGE_DAYS" -print0 2>/dev/null | xargs -0r rm -f -- 2>/dev/null
|
|
77
|
+
fi
|
|
78
|
+
if [ -d "$usertrash" ]; then
|
|
79
|
+
log "Emptying trash in $usertrash"
|
|
80
|
+
rm -rf "${usertrash:?}/"* 2>/dev/null
|
|
81
|
+
fi
|
|
82
82
|
done
|
|
83
83
|
|
|
84
|
-
# 7) Docker / Podman cleanup (if present)
|
|
84
|
+
# 7) Docker / Podman cleanup (if present) — this is what actually reclaims the
|
|
85
|
+
# overlay storage behind those duplicated `overlay` df lines.
|
|
85
86
|
if command -v docker >/dev/null 2>&1; then
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
log "Docker detected: pruning images/containers/volumes/build cache (aggressive)..."
|
|
88
|
+
docker system df || true
|
|
89
|
+
docker system prune -a --volumes -f || true
|
|
90
|
+
docker builder prune -a -f || true
|
|
89
91
|
fi
|
|
90
92
|
|
|
91
93
|
if command -v podman >/dev/null 2>&1; then
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
94
|
+
log "Podman detected: pruning images/containers/volumes (aggressive)..."
|
|
95
|
+
podman system df || true
|
|
96
|
+
podman system prune -a --volumes -f || true
|
|
95
97
|
fi
|
|
96
98
|
|
|
99
|
+
if command -v buildah >/dev/null 2>&1; then
|
|
100
|
+
log "Buildah detected: removing leftover working containers..."
|
|
101
|
+
buildah rm --all || true
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
# 7b) Unmount leaked container overlay 'merged' mounts. Pruning frees the data
|
|
105
|
+
# but leaves these mountpoints attached, so they keep flooding `df -h` with
|
|
106
|
+
# identical `overlay ... /merged` rows. Only unmount overlays not backing a
|
|
107
|
+
# still-existing container, so running workloads are never disturbed.
|
|
108
|
+
log "Unmounting orphaned container overlay mounts..."
|
|
109
|
+
if command -v podman >/dev/null 2>&1; then
|
|
110
|
+
podman umount --all >/dev/null 2>&1 || true
|
|
111
|
+
fi
|
|
112
|
+
active_merged="$(podman ps -aq 2>/dev/null | xargs -r -I{} podman inspect --format '{{.GraphDriver.Data.MergedDir}}' {} 2>/dev/null || true)"
|
|
113
|
+
findmnt -rn -o TARGET 2>/dev/null | grep -E '/var/lib/(containers/storage|docker)/overlay.*/merged' | sort -r | while IFS= read -r m; do
|
|
114
|
+
if ! printf '%s\n' "$active_merged" | grep -qxF "$m"; then
|
|
115
|
+
echo "Unmounting orphaned overlay: $m"
|
|
116
|
+
umount -l "$m" 2>/dev/null || true
|
|
117
|
+
fi
|
|
118
|
+
done
|
|
119
|
+
|
|
97
120
|
# 8) Flatpak unused runtimes/apps (if present)
|
|
98
121
|
if command -v flatpak >/dev/null 2>&1; then
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
122
|
+
log "Flatpak detected: trying to uninstall unused runtimes/apps..."
|
|
123
|
+
flatpak uninstall --unused -y || flatpak uninstall --unused
|
|
124
|
+
flatpak repair
|
|
102
125
|
fi
|
|
103
126
|
|
|
104
127
|
# 9) Python Pip cleanup
|
|
105
128
|
if command -v pip3 >/dev/null 2>&1 || command -v pip >/dev/null 2>&1; then
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
129
|
+
log "Python Pip detected: cleaning cache..."
|
|
130
|
+
if command -v pip3 >/dev/null 2>&1; then
|
|
131
|
+
pip3 cache purge
|
|
132
|
+
elif command -v pip >/dev/null 2>&1; then
|
|
133
|
+
pip cache purge
|
|
134
|
+
fi
|
|
112
135
|
fi
|
|
113
136
|
|
|
114
137
|
# 10) Conda cleanup
|
|
115
138
|
if command -v conda >/dev/null 2>&1; then
|
|
116
|
-
|
|
117
|
-
|
|
139
|
+
log "Conda detected: cleaning all..."
|
|
140
|
+
conda clean --all -y
|
|
118
141
|
fi
|
|
119
142
|
|
|
120
143
|
# 11) Remove old kernels but keep the last KEEP_KERNELS (safe)
|
|
121
144
|
log "Removing old kernels, keeping the last $KEEP_KERNELS kernels..."
|
|
122
145
|
OLD_KERNELS=$(dnf repoquery --installonly --latest-limit=-$KEEP_KERNELS -q 2>/dev/null)
|
|
123
146
|
if [ -n "$OLD_KERNELS" ]; then
|
|
124
|
-
|
|
125
|
-
|
|
147
|
+
log "Old kernels to remove: $OLD_KERNELS"
|
|
148
|
+
dnf -y remove $OLD_KERNELS
|
|
126
149
|
else
|
|
127
|
-
|
|
150
|
+
log "No old kernels found by DNF repoquery. Only $KEEP_KERNELS or fewer are currently installed."
|
|
128
151
|
fi
|
|
129
152
|
|
|
130
153
|
# 12) /opt review and removal (ON by default now)
|
|
131
154
|
if [ -d /opt ]; then
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
155
|
+
log "/opt usage (top entries):"
|
|
156
|
+
du -sh /opt/* 2>/dev/null | sort -h | head -n 20
|
|
157
|
+
|
|
158
|
+
if [ "$REMOVE_OPT" = "1" ]; then
|
|
159
|
+
OPT_TARGETS=(/opt/local-path-provisioner)
|
|
160
|
+
for t in "${OPT_TARGETS[@]}"; do
|
|
161
|
+
if [ -d "$t" ]; then
|
|
162
|
+
log "Removing $t as REMOVE_OPT=1"
|
|
163
|
+
rm -rf "$t"
|
|
164
|
+
fi
|
|
165
|
+
done
|
|
166
|
+
else
|
|
167
|
+
log "Automatic /opt removals disabled (set REMOVE_OPT=1 to enable)."
|
|
168
|
+
fi
|
|
146
169
|
fi
|
|
147
170
|
|
|
148
171
|
# 13) Find large files > 100MB (report only)
|
|
@@ -150,8 +173,10 @@ log "Listing files larger than 100MB (report only):"
|
|
|
150
173
|
find / -xdev -type f -size +100M -printf '%s\t%p\n' 2>/dev/null | sort -nr | head -n 50 | awk '{printf "%.1fMB\t%s\n",$1/1024/1024,$2}'
|
|
151
174
|
|
|
152
175
|
# Final disk usage
|
|
153
|
-
log "Disk usage
|
|
176
|
+
log "Disk usage cleanup:"
|
|
154
177
|
df -h
|
|
178
|
+
log "Top-level usage ( / ):"
|
|
179
|
+
du -xh --max-depth=1 / | sort -rh | head -n 20
|
|
155
180
|
|
|
156
181
|
log "=== Cleanup finished ==="
|
|
157
182
|
exit 0
|
package/scripts/test-monitor.sh
CHANGED
|
@@ -24,7 +24,7 @@ MODE=runtime # runtime | instance
|
|
|
24
24
|
ENV=development # development | production
|
|
25
25
|
DEPLOY_ID=dd-test # deploy id (instance mode: parent of conf.instances.json)
|
|
26
26
|
INSTANCE_IDS= # instance mode: csv of ids (default: all in conf.instances.json)
|
|
27
|
-
IMAGE=underpost/wp:v3.2.
|
|
27
|
+
IMAGE=underpost/wp:v3.2.70 # runtime mode image (instance mode reads image from conf)
|
|
28
28
|
VERSIONS=green # csv of blue/green versions
|
|
29
29
|
REPLICAS=1 # replicas per deployment
|
|
30
30
|
NAMESPACE=default # k8s namespace
|
package/src/cli/cluster.js
CHANGED
|
@@ -876,6 +876,74 @@ net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/99-k3s.conf > /dev/null`,
|
|
|
876
876
|
}
|
|
877
877
|
},
|
|
878
878
|
|
|
879
|
+
/**
|
|
880
|
+
* @method _pruneContainerCaches
|
|
881
|
+
* @description Reclaims container-runtime disk left behind after a cluster
|
|
882
|
+
* teardown: stopped containers, unused images, build cache and anonymous
|
|
883
|
+
* volumes. Best-effort across every runtime present on the host (docker,
|
|
884
|
+
* podman, and optionally the CRI runtime via crictl). Each block is guarded
|
|
885
|
+
* by a command-existence check, so it is safe to call when a runtime is
|
|
886
|
+
* absent. This is what turns a "cluster deleted" into "disk actually freed":
|
|
887
|
+
* `kind delete` / `kubeadm reset` remove the cluster but leave gigabytes of
|
|
888
|
+
* images and overlay layers under /var/lib/{docker,containers}.
|
|
889
|
+
* @param {object} [options]
|
|
890
|
+
* @param {boolean} [options.all=true] - Remove all unused images, not just dangling ones.
|
|
891
|
+
* @param {boolean} [options.crictl=false] - Also prune the CRI runtime via crictl.
|
|
892
|
+
* @param {string} [options.criSocket] - Optional crictl --runtime-endpoint override.
|
|
893
|
+
* @private
|
|
894
|
+
*/
|
|
895
|
+
_pruneContainerCaches(options = {}) {
|
|
896
|
+
const all = options.all !== false;
|
|
897
|
+
const a = all ? '-a ' : '';
|
|
898
|
+
logger.info(` -> Pruning container-runtime caches (all=${all})...`);
|
|
899
|
+
// Docker (also matches the podman-docker shim when docker is symlinked to podman).
|
|
900
|
+
shellExec(
|
|
901
|
+
`if command -v docker >/dev/null 2>&1; then sudo docker system prune ${a}--volumes -f; sudo docker builder prune ${a}-f; fi`,
|
|
902
|
+
{ silentOnError: true },
|
|
903
|
+
);
|
|
904
|
+
// Podman native — on this host images/overlays live under /var/lib/containers/storage.
|
|
905
|
+
shellExec(
|
|
906
|
+
`if command -v podman >/dev/null 2>&1; then sudo podman system prune ${a}--volumes -f; fi`,
|
|
907
|
+
{ silentOnError: true },
|
|
908
|
+
);
|
|
909
|
+
if (options.crictl) {
|
|
910
|
+
const ep = options.criSocket ? `--runtime-endpoint ${options.criSocket} ` : '';
|
|
911
|
+
shellExec(
|
|
912
|
+
`if command -v crictl >/dev/null 2>&1; then sudo env PATH="$PATH:/usr/local/bin:/usr/bin" crictl ${ep}rmi --prune; fi`,
|
|
913
|
+
{ silentOnError: true },
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
Underpost.cluster._unmountOrphanContainerOverlays();
|
|
917
|
+
},
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* @method _unmountOrphanContainerOverlays
|
|
921
|
+
* @description Unmounts leaked container overlay 'merged' mounts under
|
|
922
|
+
* /var/lib/{containers/storage,docker}/overlay. Pruning images/containers
|
|
923
|
+
* frees the backing data but leaves these mountpoints attached, so they
|
|
924
|
+
* keep showing up as identical `overlay ... /merged` rows in `df -h` long
|
|
925
|
+
* after the containers are gone. Only overlays NOT backing a still-existing
|
|
926
|
+
* container are unmounted, so running workloads are never disturbed.
|
|
927
|
+
* Best-effort; safe to call when no runtime is present.
|
|
928
|
+
* @private
|
|
929
|
+
*/
|
|
930
|
+
_unmountOrphanContainerOverlays() {
|
|
931
|
+
logger.info(' -> Unmounting orphaned container overlay mounts...');
|
|
932
|
+
shellExec(`if command -v podman >/dev/null 2>&1; then sudo podman umount --all >/dev/null 2>&1 || true; fi`, {
|
|
933
|
+
silentOnError: true,
|
|
934
|
+
});
|
|
935
|
+
shellExec(
|
|
936
|
+
`active="$(sudo podman ps -aq 2>/dev/null | xargs -r -I{} sudo podman inspect --format '{{.GraphDriver.Data.MergedDir}}' {} 2>/dev/null || true)"
|
|
937
|
+
findmnt -rn -o TARGET 2>/dev/null | grep -E '/var/lib/(containers/storage|docker)/overlay.*/merged' | sort -r | while IFS= read -r m; do
|
|
938
|
+
if ! printf '%s\\n' "$active" | grep -qxF "$m"; then
|
|
939
|
+
echo "Unmounting orphaned overlay: $m"
|
|
940
|
+
sudo umount -l "$m" 2>/dev/null || true
|
|
941
|
+
fi
|
|
942
|
+
done`,
|
|
943
|
+
{ silentOnError: true },
|
|
944
|
+
);
|
|
945
|
+
},
|
|
946
|
+
|
|
879
947
|
/**
|
|
880
948
|
* @method _lazyUmountKubeletMounts
|
|
881
949
|
* @description Lazy-unmounts every mount under /var/lib/kubelet so a
|
|
@@ -910,7 +978,7 @@ net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/99-k3s.conf > /dev/null`,
|
|
|
910
978
|
if (options.removeVolumeHostPaths) Underpost.cluster._cleanHostPathPvs();
|
|
911
979
|
else logger.info(' -> Skipping (pass --remove-volume-host-paths to enable).');
|
|
912
980
|
|
|
913
|
-
logger.info('Phase 3/
|
|
981
|
+
logger.info('Phase 3/6: Deleting all Kind clusters...');
|
|
914
982
|
shellExec(`clusters=$(kind get clusters)
|
|
915
983
|
if [ -n "$clusters" ]; then
|
|
916
984
|
for c in $clusters; do
|
|
@@ -919,11 +987,14 @@ if [ -n "$clusters" ]; then
|
|
|
919
987
|
done
|
|
920
988
|
fi`);
|
|
921
989
|
|
|
922
|
-
logger.info('Phase 4/
|
|
990
|
+
logger.info('Phase 4/6: Cleaning kubeconfig and Kind Docker networks...');
|
|
923
991
|
shellExec(`rm -rf "$HOME/.kube"`);
|
|
924
992
|
Underpost.cluster.recoverKindDockerNetworks();
|
|
925
993
|
|
|
926
|
-
logger.info('Phase 5/
|
|
994
|
+
logger.info('Phase 5/6: Pruning container-runtime caches (kindest/node images, build cache, volumes)...');
|
|
995
|
+
Underpost.cluster._pruneContainerCaches({ all: true });
|
|
996
|
+
|
|
997
|
+
logger.info('Phase 6/6: Re-applying host configuration (Docker, containerd, sysctl).');
|
|
927
998
|
Underpost.cluster.config();
|
|
928
999
|
|
|
929
1000
|
logger.info('=== KIND SAFE RESET COMPLETE ===');
|
|
@@ -995,7 +1066,7 @@ fi`);
|
|
|
995
1066
|
shellExec(`if ip link show tunl0 >/dev/null 2>&1; then sudo ip link del tunl0; fi`);
|
|
996
1067
|
shellExec(`sudo iptables -F`);
|
|
997
1068
|
shellExec(`sudo iptables -t nat -F`);
|
|
998
|
-
|
|
1069
|
+
Underpost.cluster._pruneContainerCaches({ all: true, crictl: true });
|
|
999
1070
|
|
|
1000
1071
|
logger.info('Phase 7/7: Re-applying host configuration (Docker, containerd, sysctl).');
|
|
1001
1072
|
Underpost.cluster.config();
|
|
@@ -1042,6 +1113,8 @@ fi`);
|
|
|
1042
1113
|
shellExec(`if [ -d /etc/rancher/k3s ]; then sudo rm -rf /etc/rancher/k3s; fi`);
|
|
1043
1114
|
shellExec(`if ip link show flannel.1 >/dev/null 2>&1; then sudo ip link del flannel.1; fi`);
|
|
1044
1115
|
shellExec(`if ip link show cni0 >/dev/null 2>&1; then sudo ip link del cni0; fi`);
|
|
1116
|
+
// k3s-uninstall.sh removes /var/lib/rancher/k3s; still prune any host docker/podman leftovers.
|
|
1117
|
+
Underpost.cluster._pruneContainerCaches({ all: true });
|
|
1045
1118
|
|
|
1046
1119
|
logger.info('Phase 5/5: Re-applying minimal K3s host config.');
|
|
1047
1120
|
Underpost.cluster.configMinimalK3s();
|
package/src/cli/deploy.js
CHANGED
|
@@ -562,7 +562,7 @@ ${Underpost.deploy
|
|
|
562
562
|
const yamlPath = `./engine-private/conf/${deployId}/build/${env}/secret.yaml`;
|
|
563
563
|
fs.writeFileSync(yamlPath, secretYaml, 'utf8');
|
|
564
564
|
} else {
|
|
565
|
-
const deploymentsFiles = ['Dockerfile', 'proxy.yaml', 'deployment.yaml', 'pv-pvc.yaml'];
|
|
565
|
+
const deploymentsFiles = ['Dockerfile', 'proxy.yaml', 'deployment.yaml', 'pv-pvc.yaml', 'grpc-service.yaml'];
|
|
566
566
|
for (const file of deploymentsFiles) {
|
|
567
567
|
if (fs.existsSync(`./engine-private/conf/${deployId}/build/${env}/${file}`)) {
|
|
568
568
|
fs.copyFileSync(
|
|
@@ -1009,10 +1009,17 @@ EOF`);
|
|
|
1009
1009
|
*/
|
|
1010
1010
|
configMap(env, namespace = 'default') {
|
|
1011
1011
|
const cronDeployId = cronDeployIdResolve() || 'dd-cron';
|
|
1012
|
+
const envFilePath = `/home/dd/engine/engine-private/conf/${cronDeployId}/.env.${env}`;
|
|
1013
|
+
// `--from-env-file` turns every KEY=VALUE into a secret key that the Deployment injects via
|
|
1014
|
+
// `envFrom`. Strip shell/runtime-critical keys (notably PATH) first — an injected PATH
|
|
1015
|
+
// overrides the image's own and breaks coreutils/sudo resolution inside the pod.
|
|
1016
|
+
const sanitizedEnvPath = `${envFilePath}.secret`;
|
|
1017
|
+
fs.writeFileSync(sanitizedEnvPath, Underpost.secret.sanitizeSecretEnvFile(fs.readFileSync(envFilePath, 'utf8')));
|
|
1012
1018
|
shellExec(`kubectl delete secret underpost-config -n ${namespace} --ignore-not-found`);
|
|
1013
1019
|
shellExec(
|
|
1014
|
-
`kubectl create secret generic underpost-config --from-env-file
|
|
1020
|
+
`kubectl create secret generic underpost-config --from-env-file=${sanitizedEnvPath} --dry-run=client -o yaml | kubectl apply -f - -n ${namespace}`,
|
|
1015
1021
|
);
|
|
1022
|
+
fs.removeSync(sanitizedEnvPath);
|
|
1016
1023
|
},
|
|
1017
1024
|
/**
|
|
1018
1025
|
* Switches the traffic for a deployment.
|
|
@@ -77,14 +77,42 @@ class UnderpostDockerCompose {
|
|
|
77
77
|
return nodePath.isAbsolute(relPath) ? relPath : nodePath.join(getRootDirectory(), relPath);
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Resolves the canonical directory for a custom docker-compose workflow,
|
|
82
|
+
* keyed by `--deploy-id` + `--docker-compose-id`:
|
|
83
|
+
* `engine-private/conf/<deploy-id>/docker-compose/<docker-compose-id>`.
|
|
84
|
+
* This directory ships its own `docker-compose.yml`, `compose.env`, and
|
|
85
|
+
* `nginx.conf` (used as-is, never generated). Returns null when
|
|
86
|
+
* `--docker-compose-id` is not set (the default, self-generating workflow).
|
|
87
|
+
* @param {object} options - CLI options.
|
|
88
|
+
* @returns {string|null} Repo-root-relative canonical dir, or null.
|
|
89
|
+
* @memberof UnderpostDockerCompose
|
|
90
|
+
*/
|
|
91
|
+
static composeIdBase(options = {}) {
|
|
92
|
+
if (!options.dockerComposeId) return null;
|
|
93
|
+
const deployId = options.deployId || DEFAULT_DEPLOY_ID;
|
|
94
|
+
return `engine-private/conf/${deployId}/docker-compose/${options.dockerComposeId}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
80
97
|
/**
|
|
81
98
|
* Builds the base `docker compose` invocation with explicit file and env-file,
|
|
82
99
|
* so behavior is independent of the caller's working directory.
|
|
100
|
+
*
|
|
101
|
+
* Custom workflow (`--docker-compose-id`): the compose file, env-file, and
|
|
102
|
+
* bind-mounted config (nginx.conf, mongodb/) all live in the canonical dir, so
|
|
103
|
+
* compose runs with `--project-directory` pinned there and no app-override.
|
|
83
104
|
* @param {object} options - CLI options.
|
|
84
105
|
* @returns {string} The base command string (without a subcommand).
|
|
85
106
|
* @memberof UnderpostDockerCompose
|
|
86
107
|
*/
|
|
87
108
|
static baseCmd(options = {}) {
|
|
109
|
+
const base = UnderpostDockerCompose.composeIdBase(options);
|
|
110
|
+
if (base) {
|
|
111
|
+
const projectDir = UnderpostDockerCompose.resolve(base);
|
|
112
|
+
const composeFile = UnderpostDockerCompose.resolve(options.composeFile || `${base}/docker-compose.yml`);
|
|
113
|
+
const envFile = UnderpostDockerCompose.resolve(options.envFile || `${base}/compose.env`);
|
|
114
|
+
return `docker compose --project-directory ${projectDir} --env-file ${envFile} -f ${composeFile}`;
|
|
115
|
+
}
|
|
88
116
|
const composeFile = UnderpostDockerCompose.resolve(options.composeFile || 'docker-compose.yml');
|
|
89
117
|
const envFile = UnderpostDockerCompose.resolve(options.envFile || 'docker/compose.env');
|
|
90
118
|
const overrideFile = UnderpostDockerCompose.resolve(options.appOverride || 'docker/compose.app.yml');
|
|
@@ -163,7 +191,7 @@ services:
|
|
|
163
191
|
MONGO_IMAGE=mongo:latest
|
|
164
192
|
VALKEY_IMAGE=valkey/valkey:latest
|
|
165
193
|
APP_IMAGE=underpost/underpost-engine
|
|
166
|
-
APP_TAG=v3.2.
|
|
194
|
+
APP_TAG=v3.2.70
|
|
167
195
|
PROXY_IMAGE=nginx:stable-alpine
|
|
168
196
|
PROMETHEUS_IMAGE=prom/prometheus:latest
|
|
169
197
|
GRAFANA_IMAGE=grafana/grafana:latest
|
|
@@ -343,6 +371,20 @@ datasources:
|
|
|
343
371
|
* @memberof UnderpostDockerCompose
|
|
344
372
|
*/
|
|
345
373
|
static generate(options = {}) {
|
|
374
|
+
// Custom workflow: docker-compose.yml, compose.env, and nginx.conf are
|
|
375
|
+
// pre-authored in the canonical dir and used as-is — do NOT generate them.
|
|
376
|
+
// Only (re)write the required MongoDB entrypoint (replica-set bootstrap),
|
|
377
|
+
// the single generated infra artifact, into <canonical>/mongodb so the
|
|
378
|
+
// stack stays self-contained under `--project-directory`.
|
|
379
|
+
const composeIdBase = UnderpostDockerCompose.composeIdBase(options);
|
|
380
|
+
if (composeIdBase) {
|
|
381
|
+
const mongoEntrypointPath = UnderpostDockerCompose.resolve(`${composeIdBase}/mongodb/entrypoint.sh`);
|
|
382
|
+
fs.mkdirpSync(nodePath.dirname(mongoEntrypointPath));
|
|
383
|
+
fs.writeFileSync(mongoEntrypointPath, UnderpostDockerCompose.mongoEntrypointContent(), { mode: 0o755 });
|
|
384
|
+
logger.info('mongodb entrypoint written (custom workflow)', { path: mongoEntrypointPath });
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
346
388
|
Nginx.removeRouter();
|
|
347
389
|
for (const { host, routes } of PROXY_HOSTS) Nginx.createApp({ host, routes });
|
|
348
390
|
Nginx.createDefaultServer(DEFAULT_UPSTREAM);
|
|
@@ -449,6 +491,20 @@ datasources:
|
|
|
449
491
|
silentOnError: true,
|
|
450
492
|
});
|
|
451
493
|
|
|
494
|
+
// Custom workflow: the compose file, compose.env, and nginx.conf are
|
|
495
|
+
// hand-authored source — never prune them. Only drop the one generated
|
|
496
|
+
// artifact (the mongo entrypoint), regenerated on the next --up/--generate.
|
|
497
|
+
const composeIdBase = UnderpostDockerCompose.composeIdBase(options);
|
|
498
|
+
if (composeIdBase) {
|
|
499
|
+
const mongoEntrypointPath = UnderpostDockerCompose.resolve(`${composeIdBase}/mongodb/entrypoint.sh`);
|
|
500
|
+
if (fs.existsSync(mongoEntrypointPath)) {
|
|
501
|
+
fs.removeSync(mongoEntrypointPath);
|
|
502
|
+
logger.info('removed generated artifact', { path: mongoEntrypointPath });
|
|
503
|
+
}
|
|
504
|
+
logger.info('Docker Compose reset complete. Run `--up` to recreate the stack.');
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
|
|
452
508
|
// Phase 2: prune generated artifacts (regenerated on the next --generate/--up).
|
|
453
509
|
const artifacts = options.force
|
|
454
510
|
? [...GENERATED_ARTIFACTS, options.envFile || 'docker/compose.env']
|
|
@@ -489,6 +545,7 @@ datasources:
|
|
|
489
545
|
* @param {boolean} [options.shell] - Open an interactive shell in `target` (default: app).
|
|
490
546
|
* @param {string} [options.exec] - General-purpose passthrough docker compose subcommand.
|
|
491
547
|
* @param {string} [options.deployId] - Deployment to run as the app (default: dd-default). `dd-default` self-bootstraps a fresh engine; any other id runs the standard `underpost start` command.
|
|
548
|
+
* @param {string} [options.dockerComposeId] - Custom-workflow selector. When set, use the canonical stack at `engine-private/conf/<deploy-id>/docker-compose/<docker-compose-id>/` (docker-compose.yml + compose.env + nginx.conf, used as-is), skipping nginx/env/app-override generation. Used by the Cyberia MMO ecosystem (`--deploy-id dd-cyberia --docker-compose-id cyberia`).
|
|
492
549
|
* @param {string} [options.env] - Deployment environment for non-default deploy ids (default: development).
|
|
493
550
|
* @param {string} [options.composeFile] - Override compose file path.
|
|
494
551
|
* @param {string} [options.envFile] - Override env-file path.
|
package/src/cli/fs.js
CHANGED
|
@@ -64,6 +64,31 @@ class UnderpostFileStorage {
|
|
|
64
64
|
writeStorageConf(storage, storageConf) {
|
|
65
65
|
if (storage) fs.writeFileSync(storageConf, JSON.stringify(storage, null, 4), 'utf8');
|
|
66
66
|
},
|
|
67
|
+
/**
|
|
68
|
+
* @method gitTrack
|
|
69
|
+
* @description Optional, non-fatal Git tracking layer. Any Git error is logged and swallowed
|
|
70
|
+
* so it can never interrupt or roll back the canonical `storage.*.json` workflow.
|
|
71
|
+
* @param {string} gitPath - The working directory to stage/commit.
|
|
72
|
+
* @param {object} [options] - Tracking options.
|
|
73
|
+
* @param {boolean} [options.init=false] - If true, initialize a local repo before staging.
|
|
74
|
+
* @param {string} [options.message=''] - Explicit commit message; when omitted, `underpost cmt` is used.
|
|
75
|
+
* @memberof UnderpostFileStorage
|
|
76
|
+
*/
|
|
77
|
+
gitTrack(gitPath, options = { init: false, message: '' }) {
|
|
78
|
+
try {
|
|
79
|
+
if (options.init === true) Underpost.repo.initLocalRepo({ path: gitPath });
|
|
80
|
+
shellExec(`cd ${gitPath} && git add .`, { silentOnError: true, silent: true, disableLog: true });
|
|
81
|
+
if (options.message)
|
|
82
|
+
shellExec(`cd ${gitPath} && git commit -m "${options.message}"`, {
|
|
83
|
+
silentOnError: true,
|
|
84
|
+
silent: true,
|
|
85
|
+
disableLog: true,
|
|
86
|
+
});
|
|
87
|
+
else shellExec(`underpost cmt ${gitPath} feat`, { silentOnError: true, silent: true, disableLog: true });
|
|
88
|
+
} catch (error) {
|
|
89
|
+
logger.warn('git tracking skipped (non-fatal)', { gitPath, error: error?.message });
|
|
90
|
+
}
|
|
91
|
+
},
|
|
67
92
|
/**
|
|
68
93
|
* @method recursiveCallback
|
|
69
94
|
* @description Recursively processes files and directories based on the provided options.
|
|
@@ -125,6 +150,7 @@ class UnderpostFileStorage {
|
|
|
125
150
|
|
|
126
151
|
if (hasPathFilter && options.force === true && fs.existsSync(basePath)) fs.removeSync(basePath);
|
|
127
152
|
|
|
153
|
+
// Storage is canonical: persist the removal before any (optional) git tracking runs.
|
|
128
154
|
Underpost.fs.writeStorageConf(storage, storageConf);
|
|
129
155
|
|
|
130
156
|
if (associatedPaths.length === 0)
|
|
@@ -136,13 +162,8 @@ class UnderpostFileStorage {
|
|
|
136
162
|
});
|
|
137
163
|
|
|
138
164
|
if (options.git === true) {
|
|
139
|
-
const gitPath = hasPathFilter ?
|
|
140
|
-
|
|
141
|
-
shellExec(`underpost cmt ${gitPath} feat`, {
|
|
142
|
-
silentOnError: true,
|
|
143
|
-
silent: true,
|
|
144
|
-
disableLog: true,
|
|
145
|
-
});
|
|
165
|
+
const gitPath = !hasPathFilter ? '.' : isSingleFile ? parentDir : basePath;
|
|
166
|
+
Underpost.fs.gitTrack(gitPath);
|
|
146
167
|
}
|
|
147
168
|
|
|
148
169
|
return;
|
|
@@ -151,7 +172,16 @@ class UnderpostFileStorage {
|
|
|
151
172
|
// For single files, run getDeleteFiles against the parent directory to avoid
|
|
152
173
|
// trying to `cd` into a file.
|
|
153
174
|
const gitContextPath = isSingleFile ? parentDir : path;
|
|
154
|
-
|
|
175
|
+
// Detecting locally-deleted files is a best-effort enhancement backed by git; if the path is
|
|
176
|
+
// not a repo (or git is unavailable) it must not block the canonical storage workflow.
|
|
177
|
+
let deleteFiles = [];
|
|
178
|
+
if (options.pull !== true) {
|
|
179
|
+
try {
|
|
180
|
+
deleteFiles = Underpost.repo.getDeleteFiles(gitContextPath);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
logger.warn('delete detection skipped (git unavailable)', { path: gitContextPath, error: error?.message });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
155
185
|
|
|
156
186
|
// When processing a single file, only consider it for deletion
|
|
157
187
|
for (const relativePath of deleteFiles) {
|
|
@@ -172,12 +202,7 @@ class UnderpostFileStorage {
|
|
|
172
202
|
if (pullSkipCount > 0) logger.warn(`Pull skipped ${pullSkipCount} files that already exist`);
|
|
173
203
|
// Only run git init/commit when the caller explicitly requests git tracking (--git flag).
|
|
174
204
|
// For bundle pulls into ./build the git step is unwanted and would error on a non-repo path.
|
|
175
|
-
if (options.git === true) {
|
|
176
|
-
Underpost.repo.initLocalRepo({ path: gitContextPath });
|
|
177
|
-
shellExec(`cd ${gitContextPath} && git add . && git commit -m "Base pull state"`, {
|
|
178
|
-
silentOnError: true,
|
|
179
|
-
});
|
|
180
|
-
}
|
|
205
|
+
if (options.git === true) Underpost.fs.gitTrack(gitContextPath, { init: true, message: 'Base pull state' });
|
|
181
206
|
} else {
|
|
182
207
|
let files;
|
|
183
208
|
if (isSingleFile) {
|
|
@@ -200,15 +225,9 @@ class UnderpostFileStorage {
|
|
|
200
225
|
} else logger.warn('File already exists', _path);
|
|
201
226
|
}
|
|
202
227
|
}
|
|
228
|
+
// Storage is canonical and always persisted; git is an optional layer on top.
|
|
203
229
|
Underpost.fs.writeStorageConf(storage, storageConf);
|
|
204
|
-
if (options.git === true)
|
|
205
|
-
shellExec(`cd ${gitContextPath} && git add .`);
|
|
206
|
-
shellExec(`underpost cmt ${gitContextPath} feat`, {
|
|
207
|
-
silentOnError: true,
|
|
208
|
-
silent: true,
|
|
209
|
-
disableLog: true,
|
|
210
|
-
});
|
|
211
|
-
}
|
|
230
|
+
if (options.git === true) Underpost.fs.gitTrack(gitContextPath);
|
|
212
231
|
},
|
|
213
232
|
/**
|
|
214
233
|
* @method callback
|
|
@@ -230,10 +249,12 @@ class UnderpostFileStorage {
|
|
|
230
249
|
path,
|
|
231
250
|
options = { rm: false, recursive: false, deployId: '', force: false, pull: false, git: false, omitUnzip: false },
|
|
232
251
|
) {
|
|
233
|
-
|
|
252
|
+
// rm always routes through recursiveCallback so storage.*.json is updated regardless of
|
|
253
|
+
// --recursive/--git. The bare `delete` primitive only removes the remote asset and would
|
|
254
|
+
// otherwise leave the tracked storage key orphaned.
|
|
255
|
+
if (options.recursive === true || options.git === true || options.rm === true)
|
|
234
256
|
return await Underpost.fs.recursiveCallback(path, options);
|
|
235
257
|
if (options.pull === true) return await Underpost.fs.pull(path, options);
|
|
236
|
-
if (options.rm === true) return await Underpost.fs.delete(path, options);
|
|
237
258
|
return await Underpost.fs.upload(path, options);
|
|
238
259
|
},
|
|
239
260
|
/**
|