kampodine 0.4.0 → 0.6.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/CHANGELOG.md +118 -0
- package/README.md +72 -9
- package/cli.js +21 -1
- package/kampodine.md +13 -3
- package/package.json +1 -1
- package/scripts/backup.sh +290 -0
- package/scripts/bluegreen.sh +21 -1
- package/scripts/common.sh +644 -0
- package/scripts/config.sh +247 -0
- package/scripts/deploy-lifecycle.sh +266 -0
- package/scripts/deploy.sh +136 -18
- package/scripts/dns.sh +17 -0
- package/scripts/env.sh +23 -9
- package/scripts/image-import.sh +9 -0
- package/scripts/metrics.sh +127 -0
- package/scripts/migrate.sh +39 -5
- package/scripts/status.sh +132 -12
- package/scripts/vm-prepare.sh +20 -6
package/scripts/deploy.sh
CHANGED
|
@@ -20,11 +20,26 @@
|
|
|
20
20
|
# kampodine deploy --version <sha7> # stream an existing local build
|
|
21
21
|
# kampodine deploy --rollback [<sha7>] # default: previous
|
|
22
22
|
# kampodine deploy --host root@<ip> ... # target VM override
|
|
23
|
+
# kampodine deploy --profile <name> ... # per-instance profile (config.json)
|
|
23
24
|
# kampodine deploy --dockerfile apps/api-go/Dockerfile.cutover # Go api image
|
|
25
|
+
# kampodine deploy list|prune|logs|restart|shell # lifecycle ops (each has --help)
|
|
24
26
|
# --skip-smoke # skip the public smoke (behind a not-yet-switched proxy)
|
|
25
27
|
# --refresh-config # ansible container-service refresh BEFORE restart
|
|
28
|
+
# --require-disk <pct> # fail closed when VM disk usage >= pct (default: warn above 90)
|
|
26
29
|
set -euo pipefail
|
|
27
30
|
|
|
31
|
+
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
32
|
+
# Lifecycle subcommands (list/prune/logs/restart/shell) are read-mostly
|
|
33
|
+
# VM/ledger ops — they never build or stream, must work OUTSIDE the app repo,
|
|
34
|
+
# and must not pay the deploy preflight (git/podman). Delegate first.
|
|
35
|
+
case "${1:-}" in
|
|
36
|
+
list|prune|logs|restart|shell)
|
|
37
|
+
exec bash "$HERE/deploy-lifecycle.sh" "$@"
|
|
38
|
+
;;
|
|
39
|
+
esac
|
|
40
|
+
# shellcheck source=common.sh
|
|
41
|
+
source "$HERE/common.sh"
|
|
42
|
+
|
|
28
43
|
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
|
29
44
|
KAMPODINE_HOST="${KAMPODINE_HOST:-}"
|
|
30
45
|
PROXY_HOST="${PROXY_HOST:-app.example.com}"
|
|
@@ -32,15 +47,21 @@ ENV_FILE_REMOTE="/etc/kampodine/env"
|
|
|
32
47
|
DEPLOYED_SHA_FILE="/etc/kampodine/deployed-sha"
|
|
33
48
|
ENV_CLEAR_KEYS='^(NODE_ENV|PORT|LIBSQL_TENANT_DIR|LIBSQL_API_MOUNT|STATIC_SPA_MOUNT)='
|
|
34
49
|
IMAGE="127.0.0.1:5000/kampodine-api"
|
|
50
|
+
DISK_PATH="/var/lib/containers"
|
|
35
51
|
|
|
36
52
|
MODE="deploy"
|
|
37
53
|
VERSION=""
|
|
38
54
|
SKIP_SMOKE=0
|
|
39
55
|
REFRESH_CONFIG=0
|
|
56
|
+
REQUIRE_DISK=""
|
|
57
|
+
PROFILE_FLAG=""
|
|
58
|
+
HOST_FLAG=""
|
|
59
|
+
KEY_FLAG=""
|
|
40
60
|
# SSH key resolution — GENERIC, no hardcoded personal paths:
|
|
41
61
|
# 1. --ssh-key flag (explicit, per-invocation)
|
|
42
|
-
# 2.
|
|
43
|
-
# 3.
|
|
62
|
+
# 2. profile sshKey (--profile | KAMPODINE_PROFILE | defaultProfile)
|
|
63
|
+
# 3. KAMPODINE_SSH_KEY env (legacy: direnv / .envrc / export)
|
|
64
|
+
# 4. empty → ssh-agent and/or the operator's ~/.ssh/config Host block
|
|
44
65
|
# (the POSIX way: per-host IdentityFile belongs in ssh config, not here)
|
|
45
66
|
if [ -n "${KAMPODINE_SSH_KEY:-}" ]; then
|
|
46
67
|
SSH_KEY="$KAMPODINE_SSH_KEY"
|
|
@@ -57,7 +78,7 @@ die() { printf '\033[1;31m[deploy] FAIL:\033[0m %s\n' "$*" >&2; exit 1; }
|
|
|
57
78
|
usage() {
|
|
58
79
|
cat <<'EOF'
|
|
59
80
|
Usage:
|
|
60
|
-
kampodine deploy [--host root@<ip>] [--version <sha7>] [--rollback [<sha7>]]
|
|
81
|
+
kampodine deploy [--host root@<ip>] [--profile <name>] [--version <sha7>] [--rollback [<sha7>]]
|
|
61
82
|
[--dockerfile <path>] [--ssh-key <path>] [--skip-smoke] [--refresh-config]
|
|
62
83
|
|
|
63
84
|
Examples:
|
|
@@ -65,16 +86,19 @@ EOF
|
|
|
65
86
|
grep '^# kampodine deploy' "$0" | sed 's/^# //'
|
|
66
87
|
cat <<'EOF'
|
|
67
88
|
|
|
68
|
-
Host/key resolution: --host |
|
|
69
|
-
|
|
89
|
+
Host/key resolution: --host | --profile <name> | KAMPODINE_PROFILE |
|
|
90
|
+
config defaultProfile | KAMPODINE_HOST; --ssh-key | profile sshKey |
|
|
91
|
+
KAMPODINE_SSH_KEY | ssh-agent / ~/.ssh/config. Profiles:
|
|
92
|
+
kampodine config init --name prod --host root@<ip> --ssh-key <path>.
|
|
70
93
|
EOF
|
|
71
94
|
exit 0
|
|
72
95
|
}
|
|
73
96
|
|
|
74
97
|
while [[ $# -gt 0 ]]; do
|
|
75
98
|
case "$1" in
|
|
76
|
-
--host) KAMPODINE_HOST="$2"; shift 2 ;;
|
|
77
|
-
--ssh-key) SSH_KEY="$2"; shift 2 ;;
|
|
99
|
+
--host) HOST_FLAG="$2"; KAMPODINE_HOST="$2"; shift 2 ;;
|
|
100
|
+
--ssh-key) KEY_FLAG="$2"; SSH_KEY="$2"; shift 2 ;;
|
|
101
|
+
--profile) PROFILE_FLAG="$2"; shift 2 ;;
|
|
78
102
|
--version) VERSION="$2"; MODE="version"; shift 2 ;;
|
|
79
103
|
--rollback)
|
|
80
104
|
MODE="rollback"
|
|
@@ -82,13 +106,21 @@ while [[ $# -gt 0 ]]; do
|
|
|
82
106
|
;;
|
|
83
107
|
--skip-smoke) SKIP_SMOKE=1; shift ;;
|
|
84
108
|
--refresh-config) REFRESH_CONFIG=1; shift ;;
|
|
109
|
+
--require-disk) REQUIRE_DISK="$2"; shift 2 ;;
|
|
85
110
|
--dockerfile) DOCKERFILE="$2"; shift 2 ;;
|
|
86
111
|
-h|--help) usage ;;
|
|
87
112
|
*) die "unknown argument: $1 (--help)" ;;
|
|
88
113
|
esac
|
|
89
114
|
done
|
|
90
115
|
[[ "$VERSION" != *..* && "$VERSION" =~ ^[0-9a-f]{4,40}$|^$ ]] || die "--version must be a git sha fragment"
|
|
91
|
-
[ -
|
|
116
|
+
[[ -z "$REQUIRE_DISK" || "$REQUIRE_DISK" =~ ^[0-9]+$ && "$REQUIRE_DISK" -le 100 ]] || die "--require-disk must be a percentage 0-100 (got: $REQUIRE_DISK)"
|
|
117
|
+
# profile resolution (flag > KAMPODINE_PROFILE > defaultProfile); an explicit
|
|
118
|
+
# --host/--ssh-key flag always beats the profile, the profile beats the
|
|
119
|
+
# legacy env; proxyHost from the profile REPLACES the PROXY_HOST env
|
|
120
|
+
profile_resolve "$PROFILE_FLAG" "$HOST_FLAG" "$KEY_FLAG"
|
|
121
|
+
SSH_KEY="${KEY_FLAG:-${KAMPODINE_SSH_KEY:-}}"
|
|
122
|
+
PROXY_HOST="${PROFILE_PROXY_HOST:-${PROXY_HOST:-app.example.com}}"
|
|
123
|
+
[ -n "$KAMPODINE_HOST" ] || die "target required: --host root@<vm-ip>, --profile <name>, KAMPODINE_PROFILE, config defaultProfile, or KAMPODINE_HOST=root@<vm-ip>"
|
|
92
124
|
|
|
93
125
|
SSH_ARGS=(-o ConnectTimeout=10 -o BatchMode=yes)
|
|
94
126
|
[[ -n "$SSH_KEY" ]] && SSH_ARGS+=(-i "$SSH_KEY")
|
|
@@ -102,6 +134,67 @@ if [[ "$(uname -s)" == "Darwin" ]]; then
|
|
|
102
134
|
export SSH_AUTH_SOCK
|
|
103
135
|
fi
|
|
104
136
|
|
|
137
|
+
# --- host init detection (first connect): openrc vs systemd + http client ----
|
|
138
|
+
# Groundwork for non-Alpine targets; on Alpine the verdict is openrc and every
|
|
139
|
+
# svc_action output is byte-identical to the historical strings. The verdict
|
|
140
|
+
# is cached in the active profile entry ("init" field) when one resolves.
|
|
141
|
+
init_detect vm >/dev/null 2>&1 || true
|
|
142
|
+
|
|
143
|
+
# --- deployment ledger: every run appends to ~/.kampodine/deployments.jsonl ---
|
|
144
|
+
# --- {ts, host, sha, tag, result, duration_ms, subject} — shas/tags/hosts ----
|
|
145
|
+
# --- only, NEVER secrets. The EXIT trap records failures + prints the footer --
|
|
146
|
+
# --- so EVERY deploy run ends with the total deployments count. --
|
|
147
|
+
DEPLOY_START_SECONDS="$SECONDS"
|
|
148
|
+
DEPLOY_RECORDING=0
|
|
149
|
+
DEPLOY_APPENDED=0
|
|
150
|
+
DEPLOY_RESULT="success"
|
|
151
|
+
ENV_TMP=""
|
|
152
|
+
deploy_on_exit() {
|
|
153
|
+
local rc=$?
|
|
154
|
+
[[ -n "$ENV_TMP" ]] && rm -f "$ENV_TMP" 2>/dev/null
|
|
155
|
+
if [[ "$DEPLOY_RECORDING" -eq 1 && "$DEPLOY_APPENDED" -ne 1 ]]; then
|
|
156
|
+
# never made it to the append: this run FAILED
|
|
157
|
+
ledger_append "$KAMPODINE_HOST" "$VER" "$VER" "failed" \
|
|
158
|
+
$(( (SECONDS - DEPLOY_START_SECONDS) * 1000 )) "" || true
|
|
159
|
+
DEPLOY_RESULT="failed"
|
|
160
|
+
fi
|
|
161
|
+
if [[ "$DEPLOY_RECORDING" -eq 1 ]]; then
|
|
162
|
+
if [[ "$DEPLOY_RESULT" == "failed" ]]; then
|
|
163
|
+
say "Total deployments: $(ledger_count "$KAMPODINE_HOST") · FAILED — attempted tag: $VER (not activated)"
|
|
164
|
+
else
|
|
165
|
+
say "Total deployments: $(ledger_count "$KAMPODINE_HOST") · current tag: $VER"
|
|
166
|
+
fi
|
|
167
|
+
fi
|
|
168
|
+
exit "$rc"
|
|
169
|
+
}
|
|
170
|
+
trap deploy_on_exit EXIT
|
|
171
|
+
|
|
172
|
+
# --- disk guard (2026-10-08 incident class: old sha-tagged deploy images ------
|
|
173
|
+
# --- silently filled the VM disk until sqlite writes failed with I/O errors). -
|
|
174
|
+
# --- Read-only VM df; LOUD warning above 90%; --require-disk <pct> fails closed
|
|
175
|
+
# --- BEFORE the build. phase = gate (pre-deploy, may fail) | report (warn-only)
|
|
176
|
+
vm_disk_check() {
|
|
177
|
+
local phase="${1:-report}" df_out pct verdict
|
|
178
|
+
df_out="$(vm "df -P $DISK_PATH 2>/dev/null" || true)"
|
|
179
|
+
pct="$(disk_pct_from_df "$df_out")"
|
|
180
|
+
verdict="$(disk_verdict "$pct" "$REQUIRE_DISK")"
|
|
181
|
+
case "$verdict" in
|
|
182
|
+
fail)
|
|
183
|
+
die "VM disk at ${pct}% (>= --require-disk ${REQUIRE_DISK}%) — free space first: kampodine deploy prune --dry-run"
|
|
184
|
+
;;
|
|
185
|
+
warn)
|
|
186
|
+
say "WARNING: VM disk $DISK_PATH at ${pct}% — old sha-tagged deploy images pile up (~1GB each)"
|
|
187
|
+
say "WARNING: reclaim space: kampodine deploy prune --dry-run (this deploy continues)"
|
|
188
|
+
;;
|
|
189
|
+
unknown)
|
|
190
|
+
if [[ "$phase" == "gate" && -n "$REQUIRE_DISK" ]]; then
|
|
191
|
+
die "cannot read VM disk usage (df $DISK_PATH) — --require-disk is fail-closed"
|
|
192
|
+
fi
|
|
193
|
+
[[ "$phase" == "gate" ]] && say "disk usage unknown (df unreadable) — continuing"
|
|
194
|
+
;;
|
|
195
|
+
esac
|
|
196
|
+
}
|
|
197
|
+
|
|
105
198
|
# --- preflight -----------------------------------------------------------------
|
|
106
199
|
cd "$REPO_ROOT"
|
|
107
200
|
if [[ "$MODE" == "deploy" ]]; then
|
|
@@ -109,10 +202,15 @@ if [[ "$MODE" == "deploy" ]]; then
|
|
|
109
202
|
fi
|
|
110
203
|
command -v podman >/dev/null 2>&1 || die "podman not found on the Mac (podman machine must run: podman machine start)"
|
|
111
204
|
podman info >/dev/null 2>&1 || die "podman machine not reachable (podman machine start)"
|
|
205
|
+
# disk gate: BEFORE the build — failing fast beats streaming 1GB into a full disk
|
|
206
|
+
vm_disk_check gate
|
|
112
207
|
# version mode: stream an EXISTING local build (no rebuild, no clean-tree gate —
|
|
113
208
|
# the build already happened when that sha was HEAD)
|
|
114
209
|
[[ "$MODE" == "version" ]] && VER="${VERSION:-$(git rev-parse --short HEAD)}"
|
|
115
210
|
[[ "$MODE" != "version" ]] && VER="$(git rev-parse --short HEAD)"
|
|
211
|
+
# from here on, every outcome is recorded in the ledger (success via the
|
|
212
|
+
# append at the end; anything else via the EXIT trap)
|
|
213
|
+
DEPLOY_RECORDING=1
|
|
116
214
|
|
|
117
215
|
# --- build (deploy mode always builds HEAD — cached layers keep it minutes) ----
|
|
118
216
|
if [[ "$MODE" == "deploy" ]]; then
|
|
@@ -141,7 +239,7 @@ vm "podman tag $IMAGE:$VER $IMAGE:latest" || die "stream retag failed"
|
|
|
141
239
|
# --- env file from varlock (schema-only from HEAD; secrets from pass) ----------
|
|
142
240
|
say "generating $ENV_FILE_REMOTE from varlock (schema-only from HEAD; secrets resolve from pass — no plaintext secret files)…"
|
|
143
241
|
ENV_TMP="$(mktemp /tmp/kampodine-env.XXXXXX)"
|
|
144
|
-
trap
|
|
242
|
+
# (the EXIT trap — deploy_on_exit — removes $ENV_TMP on every exit path)
|
|
145
243
|
# Schema-only resolution: extract the COMMITTED schema into a scratch dir
|
|
146
244
|
# inside the repo (plugin resolution needs node_modules; local
|
|
147
245
|
# .env/.env.local never enter the pipeline — varlock sees only the extracted
|
|
@@ -169,17 +267,20 @@ if [[ $REFRESH_CONFIG -eq 1 ]]; then
|
|
|
169
267
|
|| die "ansible playbook failed"
|
|
170
268
|
fi
|
|
171
269
|
|
|
172
|
-
|
|
173
|
-
|
|
270
|
+
RESTART_CMD="$(svc_action kampodine-api restart)" \
|
|
271
|
+
|| die "neither rc-service nor systemctl found on $KAMPODINE_HOST — cannot restart the service (host init auto-detection failed)"
|
|
272
|
+
STATUS_CMD="$(svc_action kampodine-api status 2>/dev/null || true)"
|
|
273
|
+
say "$RESTART_CMD (supervise-daemon on Alpine: stop-old/start-new podman run on the retagged :latest)…"
|
|
274
|
+
vm "$RESTART_CMD" || die "kampodine-api restart failed (${STATUS_CMD:-check the service})"
|
|
174
275
|
|
|
175
|
-
say "health probe (host-side
|
|
276
|
+
say "health probe (host-side fetch of /api/auth/ok + served-sha — busybox wget default, curl fallback on GNU-only hosts)…"
|
|
176
277
|
HEALTH_OK=0
|
|
177
278
|
for _ in $(seq 1 30); do
|
|
178
|
-
#
|
|
179
|
-
#
|
|
180
|
-
# (busybox BRE alternation + nested quotes →
|
|
181
|
-
# grep -E keeps the alternation portable
|
|
182
|
-
if vm "
|
|
279
|
+
# the fetch runs on the VM (busybox wget by default; curl when the host has
|
|
280
|
+
# none — remote_http_cmd in common.sh); the served-sha grep runs LOCALLY —
|
|
281
|
+
# remote grep quoting is fragile (busybox BRE alternation + nested quotes →
|
|
282
|
+
# permanent false-negative). grep -E keeps the alternation portable.
|
|
283
|
+
if vm "$(remote_http_cmd "http://127.0.0.1:8080/api/auth/ok")" \
|
|
183
284
|
| grep -qE "\"(git|build)\":\"$VER"; then
|
|
184
285
|
HEALTH_OK=1
|
|
185
286
|
break
|
|
@@ -188,7 +289,7 @@ for _ in $(seq 1 30); do
|
|
|
188
289
|
done
|
|
189
290
|
if [[ $HEALTH_OK -ne 1 ]]; then
|
|
190
291
|
vm "podman logs --tail 30 kampodine-api 2>&1" || true
|
|
191
|
-
vm "
|
|
292
|
+
vm "$(svc_action kampodine-api status 2>/dev/null || echo 'true') 2>&1" || true
|
|
192
293
|
PREV_HINT="$(vm "test -f $DEPLOYED_SHA_FILE && cat $DEPLOYED_SHA_FILE" 2>/dev/null || true)"
|
|
193
294
|
die "container never became healthy — rollback: kampodine deploy --rollback${PREV_HINT:+ $PREV_HINT}"
|
|
194
295
|
fi
|
|
@@ -219,4 +320,21 @@ vm "podman image prune -f >/dev/null 2>&1 || true"
|
|
|
219
320
|
# busybox head lacks -n -N — tail -n +4 after a newest-first sort keeps 3 tags.
|
|
220
321
|
vm "podman images --format '{{.Tag}} {{.CreatedAt}}' $IMAGE 2>/dev/null | grep -E '^[0-9a-f]{7,40} ' | sort -k2,2r | tail -n +4 | awk '{print \$1}' | while read -r old; do podman rmi \"$IMAGE:\$old\" >/dev/null 2>&1 || true; done" || true
|
|
221
322
|
[[ "$MODE" == "rollback" ]] || vm "echo $VER > $DEPLOYED_SHA_FILE" || true
|
|
323
|
+
|
|
324
|
+
# --- post-deploy disk report (warn-only — the deploy already succeeded) --------
|
|
325
|
+
vm_disk_check report
|
|
326
|
+
|
|
327
|
+
# --- deployment ledger append: this run made it all the way through ------------
|
|
328
|
+
DEPLOY_RESULT="success"
|
|
329
|
+
[[ "$MODE" == "rollback" ]] && DEPLOY_RESULT="rollback"
|
|
330
|
+
if [[ "$MODE" == "rollback" ]]; then
|
|
331
|
+
# the subject of a rollback is the ORIGINAL deploy's subject — the ledger
|
|
332
|
+
# knows it; git HEAD would be the wrong commit's message
|
|
333
|
+
SUBJECT="$(ledger_subject_for_tag "$KAMPODINE_HOST" "$VER")"
|
|
334
|
+
else
|
|
335
|
+
SUBJECT="$(git -C "$REPO_ROOT" log -1 --pretty=%s 2>/dev/null || true)"
|
|
336
|
+
fi
|
|
337
|
+
ledger_append "$KAMPODINE_HOST" "$VER" "$VER" "$DEPLOY_RESULT" \
|
|
338
|
+
$(( (SECONDS - DEPLOY_START_SECONDS) * 1000 )) "$SUBJECT" || true
|
|
339
|
+
DEPLOY_APPENDED=1
|
|
222
340
|
say "done (version: $VER). instant rollback: kampodine deploy --rollback"
|
package/scripts/dns.sh
CHANGED
|
@@ -23,9 +23,21 @@
|
|
|
23
23
|
# (all: optional --zone <id-or-name>; --instance-principal for instance auth)
|
|
24
24
|
set -euo pipefail
|
|
25
25
|
|
|
26
|
+
# NOTE: no $(dirname) here — some hermetic test PATHs strip coreutils;
|
|
27
|
+
# parameter expansion keeps HERE resolution dependency-free.
|
|
28
|
+
HERE="${BASH_SOURCE[0]%/*}"
|
|
29
|
+
# shellcheck source=common.sh
|
|
30
|
+
source "$HERE/common.sh"
|
|
31
|
+
|
|
32
|
+
# NOTE on --profile: dns.sh follows the OCI auth convention — PROFILE below is
|
|
33
|
+
# the OCI CONFIG profile (OCI_PROFILE). The kampodine INSTANCE profile flag
|
|
34
|
+
# (--profile <name>) is parsed into PROFILE_FLAG and validated via
|
|
35
|
+
# profile_resolve (uniform resolution across every subcommand); dns consumes
|
|
36
|
+
# no instance host/sshKey fields.
|
|
26
37
|
PROFILE="${OCI_PROFILE:-default}"
|
|
27
38
|
COMPARTMENT_NAME="${OCI_COMPARTMENT:-kampodine}"
|
|
28
39
|
AUTH_ARGS=()
|
|
40
|
+
PROFILE_FLAG=""
|
|
29
41
|
|
|
30
42
|
say() { printf '\033[1;34m[dns]\033[0m %s\n' "$*"; }
|
|
31
43
|
die() { printf '\033[1;31m[dns] FAIL:\033[0m %s\n' "$*" >&2; exit 1; }
|
|
@@ -100,12 +112,17 @@ while [[ $# -gt 0 ]]; do
|
|
|
100
112
|
--type) RTYPE="$2"; shift 2 ;;
|
|
101
113
|
--value) VALUE="$2"; shift 2 ;;
|
|
102
114
|
--ttl) TTL="$2"; shift 2 ;;
|
|
115
|
+
--profile) PROFILE_FLAG="$2"; shift 2 ;;
|
|
103
116
|
--instance-principal) AUTH_ARGS=(--auth instance_principal); shift ;;
|
|
104
117
|
-h|--help) usage 0 ;;
|
|
105
118
|
*) die "unknown argument: $1 (--help)" ;;
|
|
106
119
|
esac
|
|
107
120
|
done
|
|
108
121
|
|
|
122
|
+
# validate the kampodine instance profile when selected (uniform resolution);
|
|
123
|
+
# dns consumes none of its host/sshKey fields
|
|
124
|
+
profile_resolve "$PROFILE_FLAG" "" ""
|
|
125
|
+
|
|
109
126
|
need_provider() {
|
|
110
127
|
command -v oci >/dev/null 2>&1 \
|
|
111
128
|
|| die "oci CLI not found — brew install oci-cli (https://docs.oracle.com/en-us/iaas/Content/API/SDKDocs/cliinstall.htm), then: oci setup config"
|
package/scripts/env.sh
CHANGED
|
@@ -13,16 +13,20 @@
|
|
|
13
13
|
# ssh-agent (the POSIX default; per-host IdentityFile belongs in ssh config)
|
|
14
14
|
#
|
|
15
15
|
# Usage:
|
|
16
|
-
# kampodine env list [--host <user@ip>]
|
|
17
|
-
# kampodine env push --file <local-env-file> [...]
|
|
18
|
-
# kampodine env pull [--out <file>] [...]
|
|
19
|
-
# kampodine env fingerprint [--file <f>]
|
|
16
|
+
# kampodine env list [--host <user@ip>] [--profile <name>] # KEY + fingerprint table (NEVER values)
|
|
17
|
+
# kampodine env push --file <local-env-file> [...] [--profile <name>] # upload: 0600 temp + atomic mv + restart hint
|
|
18
|
+
# kampodine env pull [--out <file>] [...] [--profile <name>] # raw payload to stdout/--out (0600); masked summary follows it
|
|
19
|
+
# kampodine env fingerprint [--file <f>] # preview the masking for a LOCAL file / stdin (never values)
|
|
20
20
|
#
|
|
21
21
|
# push sends the file VERBATIM (what you push is what lands). deploy's env
|
|
22
22
|
# step additionally drops rc-script-owned CLEAR keys — keep those out of the
|
|
23
23
|
# file you push unless you mean to own them here.
|
|
24
24
|
set -euo pipefail
|
|
25
25
|
|
|
26
|
+
HERE="${BASH_SOURCE[0]%/*}"
|
|
27
|
+
# shellcheck source=common.sh
|
|
28
|
+
source "$HERE/common.sh"
|
|
29
|
+
|
|
26
30
|
ENV_FILE_REMOTE="${KAMPODINE_ENV_REMOTE:-/etc/kampodine/env}"
|
|
27
31
|
ENV_TMP_BASE="$(dirname "$ENV_FILE_REMOTE")"
|
|
28
32
|
|
|
@@ -47,8 +51,9 @@ usage() {
|
|
|
47
51
|
|
|
48
52
|
Fingerprints NEVER leak values: every line is KEY + value length + first 2 chars.
|
|
49
53
|
Raw values move only in push's upload stream and pull's stdout/--out payload.
|
|
50
|
-
Host/key resolution matches deploy.sh: --host |
|
|
51
|
-
|
|
54
|
+
Host/key resolution matches deploy.sh: --host | --profile <name> |
|
|
55
|
+
KAMPODINE_PROFILE | config defaultProfile | KAMPODINE_HOST; --ssh-key |
|
|
56
|
+
profile sshKey | KAMPODINE_SSH_KEY | ssh-agent / ~/.ssh/config.
|
|
52
57
|
|
|
53
58
|
Examples:
|
|
54
59
|
kampodine env list --host root@203.0.113.10
|
|
@@ -70,10 +75,14 @@ esac
|
|
|
70
75
|
|
|
71
76
|
FILE=""
|
|
72
77
|
OUT=""
|
|
78
|
+
PROFILE_FLAG=""
|
|
79
|
+
HOST_FLAG=""
|
|
80
|
+
KEY_FLAG=""
|
|
73
81
|
while [[ $# -gt 0 ]]; do
|
|
74
82
|
case "$1" in
|
|
75
|
-
--host) KAMPODINE_HOST="$2"; shift 2 ;;
|
|
76
|
-
--ssh-key) SSH_KEY="$2"; shift 2 ;;
|
|
83
|
+
--host) HOST_FLAG="$2"; KAMPODINE_HOST="$2"; shift 2 ;;
|
|
84
|
+
--ssh-key) KEY_FLAG="$2"; SSH_KEY="$2"; shift 2 ;;
|
|
85
|
+
--profile) PROFILE_FLAG="$2"; shift 2 ;;
|
|
77
86
|
--file) FILE="$2"; shift 2 ;;
|
|
78
87
|
--out) OUT="$2"; shift 2 ;;
|
|
79
88
|
-h|--help) usage ;;
|
|
@@ -81,6 +90,11 @@ while [[ $# -gt 0 ]]; do
|
|
|
81
90
|
esac
|
|
82
91
|
done
|
|
83
92
|
|
|
93
|
+
# profile resolution (flag > KAMPODINE_PROFILE > defaultProfile); an explicit
|
|
94
|
+
# --host/--ssh-key flag always beats the profile
|
|
95
|
+
profile_resolve "$PROFILE_FLAG" "$HOST_FLAG" "$KEY_FLAG"
|
|
96
|
+
SSH_KEY="${KEY_FLAG:-${KAMPODINE_SSH_KEY:-}}"
|
|
97
|
+
|
|
84
98
|
SSH_ARGS=(-o ConnectTimeout=10 -o BatchMode=yes)
|
|
85
99
|
[[ -n "$SSH_KEY" ]] && SSH_ARGS+=(-i "$SSH_KEY")
|
|
86
100
|
# $1 is a composed remote command — client-side expansion is the design.
|
|
@@ -94,7 +108,7 @@ if [[ "$(uname -s)" == "Darwin" ]]; then
|
|
|
94
108
|
fi
|
|
95
109
|
|
|
96
110
|
require_host() {
|
|
97
|
-
[[ -n "$KAMPODINE_HOST" ]] || die "target required: --host root@<ip
|
|
111
|
+
[[ -n "$KAMPODINE_HOST" ]] || die "target required: --host root@<ip>, --profile <name>, KAMPODINE_PROFILE, config defaultProfile, or KAMPODINE_HOST=root@<ip> (resolution matches deploy.sh)"
|
|
98
112
|
}
|
|
99
113
|
|
|
100
114
|
# env_fp_lines — env content on stdin -> fingerprint table on stdout.
|
package/scripts/image-import.sh
CHANGED
|
@@ -36,6 +36,7 @@ COMPARTMENT_NAME="${OCI_COMPARTMENT:-kampodine}"
|
|
|
36
36
|
PROFILE="${OCI_PROFILE:-default}"
|
|
37
37
|
FROM_PASS=0
|
|
38
38
|
KEEP_OBJECT=0
|
|
39
|
+
PROFILE_FLAG=""
|
|
39
40
|
|
|
40
41
|
say() { printf '[import] %s\n' "$*"; }
|
|
41
42
|
die() { printf '[import][FAIL] %s\n' "$*" >&2; exit 1; }
|
|
@@ -57,6 +58,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
57
58
|
--bucket) BUCKET="$2"; shift 2 ;;
|
|
58
59
|
--name-prefix) NAME_PREFIX="$2"; shift 2 ;;
|
|
59
60
|
--compartment) COMPARTMENT_NAME="$2"; shift 2 ;;
|
|
61
|
+
--profile) PROFILE_FLAG="$2"; shift 2 ;;
|
|
60
62
|
--from-pass) FROM_PASS=1; shift ;;
|
|
61
63
|
--keep-object) KEEP_OBJECT=1; shift ;;
|
|
62
64
|
-h|--help) usage ;;
|
|
@@ -64,6 +66,13 @@ while [[ $# -gt 0 ]]; do
|
|
|
64
66
|
esac
|
|
65
67
|
done
|
|
66
68
|
|
|
69
|
+
# validate the kampodine instance profile when selected (uniform resolution);
|
|
70
|
+
# image-import consumes none of its host/sshKey fields (OCI-only surface)
|
|
71
|
+
HERE="${BASH_SOURCE[0]%/*}"
|
|
72
|
+
# shellcheck source=common.sh
|
|
73
|
+
source "$HERE/common.sh"
|
|
74
|
+
profile_resolve "$PROFILE_FLAG" "" ""
|
|
75
|
+
|
|
67
76
|
command -v oci >/dev/null 2>&1 || die "oci CLI not found — brew install oci-cli, then: oci setup config (infra/oci/README.md § CLI setup)"
|
|
68
77
|
[[ -n "${OCI_COMPARTMENT:-}" ]] || die "set OCI_COMPARTMENT=<compartment name or ocid> — no default: compartments are account-specific"
|
|
69
78
|
command -v pass >/dev/null 2>&1 || true
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# metrics.sh — ONE-SHOT VM snapshot over SSH (no continuous monitoring —
|
|
3
|
+
# Beszel owns that; this is the CLI triage view). One SSH round-trip per
|
|
4
|
+
# snapshot: load + CPUs, memory, disk WITH an images/data/other breakdown,
|
|
5
|
+
# per-container CPU/MEM (podman stats), uptime, top-5 processes by RSS.
|
|
6
|
+
#
|
|
7
|
+
# The remote command is busybox-safe (cat/echo/nproc/df/du/podman/ps/sort/
|
|
8
|
+
# head — see METRICS_REMOTE_CMD in common.sh); all parsing/rendering happens
|
|
9
|
+
# client-side (metrics_* helpers in common.sh — the SAME snapshot folds into
|
|
10
|
+
# `status --verbose`). Disk threshold reuses the deploy gate's verdict logic
|
|
11
|
+
# (disk_verdict): --warn-disk <pct> (default 90) exits 1 when usage >= pct.
|
|
12
|
+
#
|
|
13
|
+
# Usage:
|
|
14
|
+
# kampodine metrics [--profile <name>] [--host <user@ip>] [--ssh-key <path>]
|
|
15
|
+
# [--warn-disk <pct>] [--watch <sec>] [--count <n>]
|
|
16
|
+
set -euo pipefail
|
|
17
|
+
|
|
18
|
+
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
19
|
+
# shellcheck source=common.sh
|
|
20
|
+
source "$HERE/common.sh"
|
|
21
|
+
|
|
22
|
+
KAMPODINE_HOST="${KAMPODINE_HOST:-}"
|
|
23
|
+
if [ -n "${KAMPODINE_SSH_KEY:-}" ]; then
|
|
24
|
+
SSH_KEY="$KAMPODINE_SSH_KEY"
|
|
25
|
+
else
|
|
26
|
+
SSH_KEY=""
|
|
27
|
+
fi
|
|
28
|
+
PROFILE_FLAG=""
|
|
29
|
+
HOST_FLAG=""
|
|
30
|
+
KEY_FLAG=""
|
|
31
|
+
WARN_DISK="90"
|
|
32
|
+
WATCH=""
|
|
33
|
+
COUNT=""
|
|
34
|
+
|
|
35
|
+
say() { printf '\033[1;36m[metrics]\033[0m %s\n' "$*"; }
|
|
36
|
+
die() { printf '\033[1;31m[metrics] FAIL:\033[0m %s\n' "$*" >&2; exit 1; }
|
|
37
|
+
usage() {
|
|
38
|
+
local code="${1:-0}"
|
|
39
|
+
printf 'Usage:\n'
|
|
40
|
+
grep '^# kampodine metrics' "$0" | sed 's/^# //'
|
|
41
|
+
cat <<'EOF'
|
|
42
|
+
|
|
43
|
+
One SSH round-trip per snapshot; busybox-safe remote commands; rendering is
|
|
44
|
+
client-side. NO continuous monitoring (Beszel owns that) — one-shot CLI view;
|
|
45
|
+
--watch N re-snapshots every N seconds (Ctrl-C ends), --count M bounds the
|
|
46
|
+
iterations. --warn-disk <pct> (default 90) exits 1 when root disk usage is at
|
|
47
|
+
or above the threshold — the SAME verdict logic as deploy's fail-closed gate.
|
|
48
|
+
Host resolution: --host | --profile <name> | KAMPODINE_PROFILE |
|
|
49
|
+
config defaultProfile | KAMPODINE_HOST.
|
|
50
|
+
|
|
51
|
+
Examples:
|
|
52
|
+
kampodine metrics # profile/host from config or env
|
|
53
|
+
kampodine metrics --host root@203.0.113.10 # explicit target
|
|
54
|
+
kampodine metrics --profile prod # per-instance profile
|
|
55
|
+
kampodine metrics --warn-disk 85 # exit 1 at >= 85% (default 90)
|
|
56
|
+
kampodine metrics --watch 10 --count 6 # snapshot every 10s, 6 times
|
|
57
|
+
kampodine status --verbose # the same snapshot inside status
|
|
58
|
+
EOF
|
|
59
|
+
exit "$code"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
while [[ $# -gt 0 ]]; do
|
|
63
|
+
case "$1" in
|
|
64
|
+
--profile) PROFILE_FLAG="$2"; shift 2 ;;
|
|
65
|
+
--host) HOST_FLAG="$2"; KAMPODINE_HOST="$2"; shift 2 ;;
|
|
66
|
+
--ssh-key) KEY_FLAG="$2"; SSH_KEY="$2"; shift 2 ;;
|
|
67
|
+
--warn-disk) WARN_DISK="$2"; shift 2 ;;
|
|
68
|
+
--watch) WATCH="$2"; shift 2 ;;
|
|
69
|
+
--count) COUNT="$2"; shift 2 ;;
|
|
70
|
+
-h|--help) usage 0 ;;
|
|
71
|
+
*) die "unknown argument: $1 (--help)" ;;
|
|
72
|
+
esac
|
|
73
|
+
done
|
|
74
|
+
|
|
75
|
+
# profile resolution (flag > KAMPODINE_PROFILE > defaultProfile); an explicit
|
|
76
|
+
# --host/--ssh-key always beats the profile
|
|
77
|
+
profile_resolve "$PROFILE_FLAG" "$HOST_FLAG" "$KEY_FLAG"
|
|
78
|
+
SSH_KEY="${KEY_FLAG:-${KAMPODINE_SSH_KEY:-}}"
|
|
79
|
+
|
|
80
|
+
[[ "$WARN_DISK" =~ ^[0-9]+$ && "$WARN_DISK" -ge 1 && "$WARN_DISK" -le 100 ]] \
|
|
81
|
+
|| die "--warn-disk must be a percentage 1-100 (got: $WARN_DISK)"
|
|
82
|
+
[[ -z "$WATCH" || "$WATCH" =~ ^[1-9][0-9]*$ ]] || die "--watch must be a positive-integer number of seconds (got: $WATCH)"
|
|
83
|
+
[[ -z "$COUNT" || "$COUNT" =~ ^[1-9][0-9]*$ ]] || die "--count must be a positive integer (got: $COUNT)"
|
|
84
|
+
[[ -n "$KAMPODINE_HOST" ]] || die "target required: --host root@<ip>, --profile <name>, KAMPODINE_PROFILE, config defaultProfile, or KAMPODINE_HOST=root@<ip>"
|
|
85
|
+
|
|
86
|
+
SSH_ARGS=(-o ConnectTimeout=10 -o BatchMode=yes)
|
|
87
|
+
[[ -n "$SSH_KEY" ]] && SSH_ARGS+=(-i "$SSH_KEY")
|
|
88
|
+
# $1 is a composed remote command — client-side expansion is the design.
|
|
89
|
+
# shellcheck disable=SC2029
|
|
90
|
+
vm() { ssh "${SSH_ARGS[@]}" "$KAMPODINE_HOST" "$1"; }
|
|
91
|
+
|
|
92
|
+
# --- macOS ssh-agent quirk (first run from a fresh machine) -------------------
|
|
93
|
+
if [[ "$(uname -s)" == "Darwin" ]]; then
|
|
94
|
+
SSH_AUTH_SOCK="$(launchctl getenv SSH_AUTH_SOCK 2>/dev/null || true)"
|
|
95
|
+
export SSH_AUTH_SOCK
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
snapshot() {
|
|
99
|
+
local raw pct verdict
|
|
100
|
+
raw="$(vm "$METRICS_REMOTE_CMD")" || die "cannot reach VM $KAMPODINE_HOST (ssh failed) — nothing to report"
|
|
101
|
+
say "== metrics ($KAMPODINE_HOST) =="
|
|
102
|
+
metrics_render "$raw"
|
|
103
|
+
pct="$(disk_pct_from_df "$M_DISK")"
|
|
104
|
+
verdict="$(disk_verdict "$pct" "$WARN_DISK")"
|
|
105
|
+
case "$verdict" in
|
|
106
|
+
fail)
|
|
107
|
+
printf 'WARNING: root disk at %s%% (>= --warn-disk %s%%) — free space first: kampodine deploy prune --dry-run\n' \
|
|
108
|
+
"${pct:-?}" "$WARN_DISK"
|
|
109
|
+
return 1
|
|
110
|
+
;;
|
|
111
|
+
warn)
|
|
112
|
+
printf 'WARNING: root disk above 90%% (%s%%) — old sha-tagged deploy images pile up; reclaim: kampodine deploy prune --dry-run\n' "$pct"
|
|
113
|
+
;;
|
|
114
|
+
esac
|
|
115
|
+
return 0
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
n=0
|
|
119
|
+
while :; do
|
|
120
|
+
n=$(( n + 1 ))
|
|
121
|
+
if ! snapshot; then
|
|
122
|
+
exit 1
|
|
123
|
+
fi
|
|
124
|
+
[[ -n "$COUNT" && "$n" -ge "$COUNT" ]] && exit 0
|
|
125
|
+
[[ -z "$WATCH" ]] && exit 0
|
|
126
|
+
sleep "$WATCH"
|
|
127
|
+
done
|
package/scripts/migrate.sh
CHANGED
|
@@ -9,13 +9,16 @@
|
|
|
9
9
|
# are COLLECTED (files are independent; one bad tenant must not block the others)
|
|
10
10
|
# and the script exits 1 if any failed — the deploy's smoke gate catches a red migration.
|
|
11
11
|
# Runs ON the VM from the repo checkout (also fine locally against .tmp data):
|
|
12
|
-
# kampodine migrate [--allow-running]
|
|
12
|
+
# kampodine migrate [--allow-running] [--profile <name>]
|
|
13
13
|
set -euo pipefail
|
|
14
14
|
|
|
15
15
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
16
16
|
REPO_ROOT="${REPO_ROOT:-$(cd "${SCRIPT_DIR}/../../.." && pwd)}"
|
|
17
17
|
TENANT_DIR="${TENANT_DIR:-/data/tenants}"
|
|
18
18
|
SERVICE="${SERVICE:-kampodine-api}"
|
|
19
|
+
HERE="$SCRIPT_DIR"
|
|
20
|
+
# shellcheck source=common.sh
|
|
21
|
+
source "$HERE/common.sh"
|
|
19
22
|
|
|
20
23
|
log() { printf '[migrate-all] %s\n' "$*"; }
|
|
21
24
|
fail() { printf '[migrate-all][FAIL] %s\n' "$*" >&2; exit 1; }
|
|
@@ -37,13 +40,25 @@ EOF
|
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
ALLOW_RUNNING=0
|
|
43
|
+
PROFILE_FLAG=""
|
|
44
|
+
PENDING_PROFILE=0
|
|
40
45
|
for arg in "$@"; do
|
|
46
|
+
if [[ "$PENDING_PROFILE" -eq 1 ]]; then
|
|
47
|
+
PROFILE_FLAG="$arg"
|
|
48
|
+
PENDING_PROFILE=0
|
|
49
|
+
continue
|
|
50
|
+
fi
|
|
41
51
|
case "$arg" in
|
|
42
52
|
--allow-running) ALLOW_RUNNING=1 ;;
|
|
43
|
-
|
|
53
|
+
--profile) PENDING_PROFILE=1 ;;
|
|
54
|
+
--help|-h) usage ;;
|
|
44
55
|
*) fail "unknown argument: $arg (--help)" ;;
|
|
45
56
|
esac
|
|
46
57
|
done
|
|
58
|
+
[[ "$PENDING_PROFILE" -eq 0 ]] || fail "--profile requires a value"
|
|
59
|
+
# profile selection is validated for uniformity — migrate consumes no
|
|
60
|
+
# host/sshKey fields (it runs ON the VM from the repo checkout)
|
|
61
|
+
profile_resolve "$PROFILE_FLAG" "" ""
|
|
47
62
|
|
|
48
63
|
[[ -f "${REPO_ROOT}/apps/api/scripts/libsql-migrate/migrate-db.ts" ]] \
|
|
49
64
|
|| fail "repo root not found at ${REPO_ROOT} (set REPO_ROOT=... if the checkout lives elsewhere)"
|
|
@@ -51,11 +66,30 @@ command -v pnpm >/dev/null 2>&1 || fail "pnpm not found in PATH"
|
|
|
51
66
|
[[ -d "$TENANT_DIR" ]] || fail "tenant dir ${TENANT_DIR} does not exist"
|
|
52
67
|
|
|
53
68
|
# Writing schema while the API serves traffic risks SQLITE_BUSY on a single-writer
|
|
54
|
-
# engine — the service must be stopped first (rc-service
|
|
69
|
+
# engine — the service must be stopped first (rc-service ${SERVICE} stop). Override exists
|
|
55
70
|
# for deliberate local use.
|
|
56
|
-
|
|
71
|
+
#
|
|
72
|
+
# Init-aware detection (fixes the DEAD guard, 2026-10-08): this script runs ON
|
|
73
|
+
# the VM, whose init is OpenRC (Alpine ships no systemd) — a bare `systemctl
|
|
74
|
+
# is-active` always failed there, so the stop-first guard NEVER fired and
|
|
75
|
+
# migrations could run under the serving API. Mirror common.sh's detection
|
|
76
|
+
# order: rc-service (openrc) first, then systemctl (systemd hosts). On a host
|
|
77
|
+
# with neither (local dev machines), state is unknowable — proceed with a
|
|
78
|
+
# loud warning rather than blocking local runs.
|
|
79
|
+
service_running() {
|
|
80
|
+
if command -v rc-service >/dev/null 2>&1; then
|
|
81
|
+
rc-service "$SERVICE" status >/dev/null 2>&1
|
|
82
|
+
elif command -v systemctl >/dev/null 2>&1; then
|
|
83
|
+
systemctl is-active --quiet "$SERVICE"
|
|
84
|
+
else
|
|
85
|
+
log "service state unknown (neither rc-service nor systemctl found) — assuming ${SERVICE} is not running"
|
|
86
|
+
return 1
|
|
87
|
+
fi
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if service_running; then
|
|
57
91
|
if [[ $ALLOW_RUNNING -ne 1 ]]; then
|
|
58
|
-
fail "${SERVICE} is running — stop it first (rc-service
|
|
92
|
+
fail "${SERVICE} is running — stop it first (rc-service ${SERVICE} stop) or pass --allow-running"
|
|
59
93
|
fi
|
|
60
94
|
log "WARNING: migrating while ${SERVICE} is running (--allow-running)"
|
|
61
95
|
fi
|