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
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# config.sh — per-instance profiles in ~/.kampodine/config.json.
|
|
3
|
+
#
|
|
4
|
+
# FLAT by owner decision: every profile carries host/sshKey/proxyHost/group
|
|
5
|
+
# — NO inheritance between profiles (the schema only LEAVES ROOM for a
|
|
6
|
+
# future "defaults" block; nothing reads one). `group` is cosmetic: it feeds
|
|
7
|
+
# the `config list --group` filter and nothing else. The file NEVER holds
|
|
8
|
+
# secrets — sshKey is a PATH; hosts are addresses; that is the whole schema.
|
|
9
|
+
# (The init-system auto-detection may add an "init" field per profile —
|
|
10
|
+
# written by common.sh's init_detect, never by hand.)
|
|
11
|
+
#
|
|
12
|
+
# File contract: dir 0700, file 0600, atomic writes (same-dir tmp + mv).
|
|
13
|
+
# jq is required for every subcommand here (safe JSON read + rewrite).
|
|
14
|
+
#
|
|
15
|
+
# Usage:
|
|
16
|
+
# kampodine config init --name <name> --host <user@ip> --ssh-key <path> [--proxy-host <host>] [--group <g>] [--set-default] [--force]
|
|
17
|
+
# kampodine config list [--group <g>]
|
|
18
|
+
# kampodine config show [--profile <name>]
|
|
19
|
+
# kampodine config set-default <name>
|
|
20
|
+
# kampodine config remove <name> [--force]
|
|
21
|
+
#
|
|
22
|
+
# The FIRST profile automatically becomes the default; later profiles need
|
|
23
|
+
# --set-default (or `config set-default`) to take it. An explicit --profile
|
|
24
|
+
# flag or KAMPODINE_PROFILE env beats the default everywhere.
|
|
25
|
+
set -euo pipefail
|
|
26
|
+
|
|
27
|
+
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
28
|
+
# shellcheck source=common.sh
|
|
29
|
+
source "$HERE/common.sh"
|
|
30
|
+
|
|
31
|
+
say() { printf '\033[1;35m[config]\033[0m %s\n' "$*"; }
|
|
32
|
+
die() { printf '\033[1;31m[config] FAIL:\033[0m %s\n' "$*" >&2; exit 1; }
|
|
33
|
+
usage() {
|
|
34
|
+
local code="${1:-0}"
|
|
35
|
+
printf 'Usage:\n'
|
|
36
|
+
grep '^# kampodine config' "$0" | sed 's/^# //'
|
|
37
|
+
cat <<'EOF'
|
|
38
|
+
|
|
39
|
+
Per-instance profiles (~/.kampodine/config.json, dir 0700 / file 0600).
|
|
40
|
+
FLAT — no inheritance; `group` is a cosmetic list filter. sshKey is stored
|
|
41
|
+
as a PATH only; secrets never live in the config. Resolution everywhere:
|
|
42
|
+
explicit flag > KAMPODINE_PROFILE env > defaultProfile > legacy env.
|
|
43
|
+
jq is required (safe JSON rewrites). The first profile auto-becomes the
|
|
44
|
+
default; removing the LAST profile requires --force.
|
|
45
|
+
|
|
46
|
+
Examples:
|
|
47
|
+
kampodine config init --name prod --host root@84.13.128.216 --ssh-key ~/.ssh/id_ed25519-esellar --proxy-host 84-13-128-216.sslip.io --set-default
|
|
48
|
+
kampodine config init --name staging --host root@203.0.113.8 --ssh-key ~/.ssh/id_ed25519 --group preview
|
|
49
|
+
kampodine config list # all profiles (default marked *)
|
|
50
|
+
kampodine config list --group main # cosmetic group filter
|
|
51
|
+
kampodine config show --profile prod
|
|
52
|
+
kampodine config set-default staging
|
|
53
|
+
kampodine config remove staging
|
|
54
|
+
kampodine deploy --profile prod # profiles feed every subcommand
|
|
55
|
+
EOF
|
|
56
|
+
exit "$code"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# config_atomic_write <json> — 0700 dir / 0600 file, same-dir tmp + mv.
|
|
60
|
+
config_atomic_write() {
|
|
61
|
+
local json="$1" f tmp
|
|
62
|
+
f="$(config_file)"
|
|
63
|
+
tmp="$f.tmp.$$"
|
|
64
|
+
umask 077
|
|
65
|
+
printf '%s\n' "$json" > "$tmp" || die "cannot write $tmp"
|
|
66
|
+
chmod 600 "$tmp" && mv -f "$tmp" "$f" || die "atomic install failed (tmp left at: $tmp)"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# expand_tilde <path> -> $HOME-spelled-out form (config stores absolute paths).
|
|
70
|
+
expand_tilde() {
|
|
71
|
+
case "$1" in
|
|
72
|
+
"~") printf '%s' "$HOME" ;;
|
|
73
|
+
"~"/*) printf '%s%s' "$HOME" "${1#\~}" ;;
|
|
74
|
+
*) printf '%s' "$1" ;;
|
|
75
|
+
esac
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# reject_config_value <value> <what> — no whitespace/quote/backslash: the
|
|
79
|
+
# config is hand-greppable and jq-round-trippable; nothing legit needs them.
|
|
80
|
+
reject_config_value() {
|
|
81
|
+
local value="$1" what="$2"
|
|
82
|
+
[[ -n "$value" ]] || die "$what must not be empty"
|
|
83
|
+
case "$value" in
|
|
84
|
+
*[[:space:]\"\\]*)
|
|
85
|
+
die "$what must not contain whitespace, quotes, or backslashes (got it with — value not echoed)"
|
|
86
|
+
;;
|
|
87
|
+
esac
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
CMD="${1:-}"
|
|
91
|
+
case "$CMD" in
|
|
92
|
+
init|list|show|set-default|remove) shift ;;
|
|
93
|
+
-h|--help|help) usage 0 ;;
|
|
94
|
+
"") usage 2 ;;
|
|
95
|
+
*)
|
|
96
|
+
printf 'kampodine config: unknown subcommand: %s\n\n' "$CMD" >&2
|
|
97
|
+
usage 2
|
|
98
|
+
;;
|
|
99
|
+
esac
|
|
100
|
+
|
|
101
|
+
NAME="" HOST_ARG="" KEY_ARG="" PROXY_ARG="" GROUP_ARG="" SET_DEFAULT=0 FORCE=0
|
|
102
|
+
PROFILE_ARG=""
|
|
103
|
+
POSITIONAL=()
|
|
104
|
+
while [[ $# -gt 0 ]]; do
|
|
105
|
+
case "$1" in
|
|
106
|
+
--name) NAME="$2"; shift 2 ;;
|
|
107
|
+
--host) HOST_ARG="$2"; shift 2 ;;
|
|
108
|
+
--ssh-key) KEY_ARG="$2"; shift 2 ;;
|
|
109
|
+
--proxy-host) PROXY_ARG="$2"; shift 2 ;;
|
|
110
|
+
--group) GROUP_ARG="$2"; shift 2 ;;
|
|
111
|
+
--profile) PROFILE_ARG="$2"; shift 2 ;;
|
|
112
|
+
--set-default) SET_DEFAULT=1; shift ;;
|
|
113
|
+
--force) FORCE=1; shift ;;
|
|
114
|
+
-h|--help) usage 0 ;;
|
|
115
|
+
--) shift; POSITIONAL+=("$@"); break ;;
|
|
116
|
+
*) POSITIONAL+=("$1"); shift ;;
|
|
117
|
+
esac
|
|
118
|
+
done
|
|
119
|
+
|
|
120
|
+
require_jq() {
|
|
121
|
+
command -v jq >/dev/null 2>&1 \
|
|
122
|
+
|| die "jq not found — brew install jq (config.sh rewrites JSON safely)"
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
case "$CMD" in
|
|
126
|
+
init)
|
|
127
|
+
[[ -n "$NAME" ]] || die "--name is required (--help)"
|
|
128
|
+
is_profile_name "$NAME" || die "invalid --name '$NAME' — use [A-Za-z0-9][A-Za-z0-9_-]* (max 64)"
|
|
129
|
+
f="$(config_file)"
|
|
130
|
+
if [[ -f "$f" ]] && [[ "$(jq -r --arg n "$NAME" '.profiles // {} | has($n)' "$f" 2>/dev/null || echo false)" == "true" ]] && [[ $FORCE -ne 1 ]]; then
|
|
131
|
+
die "profile '$NAME' already exists — pass --force to overwrite (re-init also refreshes fields; the cached init field is kept unless you change host/ssh-key)"
|
|
132
|
+
fi
|
|
133
|
+
reject_config_value "$HOST_ARG" "--host"
|
|
134
|
+
reject_config_value "$KEY_ARG" "--ssh-key"
|
|
135
|
+
[[ -z "$PROXY_ARG" ]] || reject_config_value "$PROXY_ARG" "--proxy-host"
|
|
136
|
+
[[ -z "$GROUP_ARG" ]] || reject_config_value "$GROUP_ARG" "--group"
|
|
137
|
+
KEY_ABS="$(expand_tilde "$KEY_ARG")"
|
|
138
|
+
require_jq
|
|
139
|
+
if [[ ! -f "$KEY_ABS" ]]; then
|
|
140
|
+
say "note: ssh key file does not exist (yet): $KEY_ABS — stored as a path, deploy will fail until it does"
|
|
141
|
+
fi
|
|
142
|
+
if [[ -f "$f" ]]; then
|
|
143
|
+
# upsert + default rules: explicit --set-default wins; a config without
|
|
144
|
+
# any default (fresh defaultProfile, or cleared by a --force remove)
|
|
145
|
+
# adopts this profile.
|
|
146
|
+
new="$(jq --arg n "$NAME" --arg h "$HOST_ARG" --arg k "$KEY_ABS" --arg p "$PROXY_ARG" --arg g "$GROUP_ARG" --arg sd "$SET_DEFAULT" '
|
|
147
|
+
.profiles[$n] = {host: $h, sshKey: $k, proxyHost: $p, group: $g}
|
|
148
|
+
| if $sd == "1" or (.defaultProfile // "") == "" then .defaultProfile = $n else . end
|
|
149
|
+
' "$f")"
|
|
150
|
+
config_atomic_write "$new"
|
|
151
|
+
else
|
|
152
|
+
new="$(jq -n --arg n "$NAME" --arg h "$HOST_ARG" --arg k "$KEY_ABS" --arg p "$PROXY_ARG" --arg g "$GROUP_ARG" '
|
|
153
|
+
{defaultProfile: $n, profiles: {($n): {host: $h, sshKey: $k, proxyHost: $p, group: $g}}}
|
|
154
|
+
')"
|
|
155
|
+
config_atomic_write "$new"
|
|
156
|
+
fi
|
|
157
|
+
default="$(config_default_profile)"
|
|
158
|
+
say "saved profile '$NAME' -> $f (0600)$([ "$default" == "$NAME" ] && printf ' — default' || true)"
|
|
159
|
+
[[ $SET_DEFAULT -eq 1 ]] || say "make default later: kampodine config set-default $NAME"
|
|
160
|
+
;;
|
|
161
|
+
|
|
162
|
+
list)
|
|
163
|
+
require_jq
|
|
164
|
+
f="$(config_file)"
|
|
165
|
+
[[ -f "$f" ]] || die "no kampodine config yet — create one: kampodine config init --name <name> --host root@<ip> (--help)"
|
|
166
|
+
default="$(config_default_profile)"
|
|
167
|
+
printf '%-12s %-10s %-8s %-26s %-30s %-26s\n' "NAME" "GROUP" "INIT" "HOST" "SSH-KEY (path)" "PROXY-HOST"
|
|
168
|
+
rows="$(jq -r --arg g "$GROUP_ARG" '
|
|
169
|
+
.profiles // {} | to_entries[]
|
|
170
|
+
| select($g == "" or ((.value.group // "") == $g))
|
|
171
|
+
| [.key,
|
|
172
|
+
(if (.value.group // "") == "" then "-" else .value.group end),
|
|
173
|
+
(if (.value.init // "") == "" then "-" else .value.init end),
|
|
174
|
+
(if (.value.host // "") == "" then "-" else .value.host end),
|
|
175
|
+
(if (.value.sshKey // "") == "" then "-" else .value.sshKey end),
|
|
176
|
+
(if (.value.proxyHost // "") == "" then "-" else .value.proxyHost end)]
|
|
177
|
+
| @tsv' "$f")"
|
|
178
|
+
if [[ -z "$rows" ]]; then
|
|
179
|
+
say "(no profiles${GROUP_ARG:+ in group '$GROUP_ARG'})"
|
|
180
|
+
exit 0
|
|
181
|
+
fi
|
|
182
|
+
while IFS=$'\t' read -r name group init host key proxy; do
|
|
183
|
+
marker=""
|
|
184
|
+
[[ "$name" == "$default" ]] && marker=" * default"
|
|
185
|
+
printf '%-12s %-10s %-8s %-26s %-30s %-26s%s\n' \
|
|
186
|
+
"$name" "$group" "$init" "$host" "$key" "$proxy" "$marker"
|
|
187
|
+
done <<<"$rows"
|
|
188
|
+
;;
|
|
189
|
+
|
|
190
|
+
show)
|
|
191
|
+
require_jq
|
|
192
|
+
f="$(config_file)"
|
|
193
|
+
[[ -f "$f" ]] || die "no kampodine config yet — create one: kampodine config init --name <name> --host root@<ip> (--help)"
|
|
194
|
+
name="${PROFILE_ARG:-$(config_default_profile)}"
|
|
195
|
+
[[ -n "$name" ]] || die "no default profile — pass --profile <name> (known: $(config_profile_names | paste -sd' ' -))"
|
|
196
|
+
host="$(profile_field "$name" host)" || die "unknown profile '$name' — known: $(config_profile_names | paste -sd' ' -)"
|
|
197
|
+
say "profile: $name$([ "$name" == "$(config_default_profile)" ] && printf ' (default)' || true)"
|
|
198
|
+
printf 'host : %s\n' "$host"
|
|
199
|
+
printf 'sshKey : %s # path only — secrets never live in the config\n' "$(profile_field "$name" sshKey)"
|
|
200
|
+
printf 'proxyHost : %s\n' "$(profile_field "$name" proxyHost)"
|
|
201
|
+
printf 'group : %s\n' "$(profile_field "$name" group)"
|
|
202
|
+
cached_init="$(profile_field "$name" init)"
|
|
203
|
+
[[ -z "$cached_init" ]] || printf 'init : %s # cached by host auto-detection\n' "$cached_init"
|
|
204
|
+
;;
|
|
205
|
+
|
|
206
|
+
set-default)
|
|
207
|
+
target="${POSITIONAL[0]:-}"
|
|
208
|
+
[[ -n "$target" ]] || die "usage: kampodine config set-default <name>"
|
|
209
|
+
require_jq
|
|
210
|
+
f="$(config_file)"
|
|
211
|
+
[[ -f "$f" ]] || die "no kampodine config yet — create one: kampodine config init --name <name> --host root@<ip> (--help)"
|
|
212
|
+
profile_field "$target" host >/dev/null \
|
|
213
|
+
|| die "unknown profile '$target' — known: $(config_profile_names | paste -sd' ' -)"
|
|
214
|
+
new="$(jq --arg n "$target" '.defaultProfile = $n' "$f")"
|
|
215
|
+
config_atomic_write "$new"
|
|
216
|
+
say "default profile: $target"
|
|
217
|
+
;;
|
|
218
|
+
|
|
219
|
+
remove)
|
|
220
|
+
target="${POSITIONAL[0]:-}"
|
|
221
|
+
[[ -n "$target" ]] || die "usage: kampodine config remove <name> [--force]"
|
|
222
|
+
require_jq
|
|
223
|
+
f="$(config_file)"
|
|
224
|
+
[[ -f "$f" ]] || die "no kampodine config at $f — nothing to remove"
|
|
225
|
+
profile_field "$target" host >/dev/null \
|
|
226
|
+
|| die "unknown profile '$target' — known: $(config_profile_names | paste -sd' ' -)"
|
|
227
|
+
count="$(jq '.profiles // {} | length' "$f")"
|
|
228
|
+
if [[ "$count" -eq 1 && $FORCE -ne 1 ]]; then
|
|
229
|
+
die "refusing to remove the last profile ('$target') — pass --force if you really mean it"
|
|
230
|
+
fi
|
|
231
|
+
new="$(jq --arg n "$target" '
|
|
232
|
+
.profiles |= del(.[$n])
|
|
233
|
+
| if .defaultProfile == $n
|
|
234
|
+
then .defaultProfile = ((.profiles | keys | sort) | .[0] // "")
|
|
235
|
+
else . end
|
|
236
|
+
' "$f")"
|
|
237
|
+
config_atomic_write "$new"
|
|
238
|
+
remaining="$(jq -r '.profiles // {} | keys | length' "$f")"
|
|
239
|
+
say "removed profile '$target' ($remaining remaining)"
|
|
240
|
+
new_default="$(config_default_profile)"
|
|
241
|
+
if [[ "$target" == "$new_default" || -z "$new_default" ]]; then
|
|
242
|
+
:
|
|
243
|
+
else
|
|
244
|
+
say "default profile is now: $new_default"
|
|
245
|
+
fi
|
|
246
|
+
;;
|
|
247
|
+
esac
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# deploy-lifecycle.sh — deployment lifecycle ops for the target VM, reached
|
|
3
|
+
# as `kampodine deploy <subcommand>` (deploy.sh delegates before any build
|
|
4
|
+
# preflight, so these work outside the app repo, offline, and without podman
|
|
5
|
+
# on the deploy machine). Read-mostly by design: nothing here builds or
|
|
6
|
+
# streams images.
|
|
7
|
+
#
|
|
8
|
+
# The local deployment ledger (~/.kampodine/deployments.jsonl, appended by
|
|
9
|
+
# EVERY deploy/rollback) is the deployment HISTORY; the VM's sha-tagged
|
|
10
|
+
# images are the deployable REALITY. `deploy list` merges both, marks the
|
|
11
|
+
# running tag, and prints the total — no more counting `podman images` by
|
|
12
|
+
# hand after an outage.
|
|
13
|
+
#
|
|
14
|
+
# Usage:
|
|
15
|
+
# kampodine deploy list [--host root@<ip>] [--profile <name>] [--ssh-key <path>]
|
|
16
|
+
# kampodine deploy prune [--host root@<ip>] [--profile <name>] [--keep N] [--dry-run]
|
|
17
|
+
# kampodine deploy logs [--host root@<ip>] [--profile <name>] [--lines N]
|
|
18
|
+
# kampodine deploy restart [--host root@<ip>] [--profile <name>]
|
|
19
|
+
# kampodine deploy shell [--host root@<ip>] [--profile <name>] [exec -- <cmd>...]
|
|
20
|
+
set -euo pipefail
|
|
21
|
+
|
|
22
|
+
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
23
|
+
# shellcheck source=common.sh
|
|
24
|
+
source "$HERE/common.sh"
|
|
25
|
+
|
|
26
|
+
IMAGE="127.0.0.1:5000/kampodine-api"
|
|
27
|
+
CONTAINER="kampodine-api"
|
|
28
|
+
DISK_PATH="/var/lib/containers"
|
|
29
|
+
|
|
30
|
+
KAMPODINE_HOST="${KAMPODINE_HOST:-}"
|
|
31
|
+
# SSH key resolution — same ladder as deploy.sh:
|
|
32
|
+
# 1. --ssh-key flag (explicit, per-invocation)
|
|
33
|
+
# 2. KAMPODINE_SSH_KEY env (project-level: direnv / .envrc / export)
|
|
34
|
+
# 3. empty → ssh-agent and/or the operator's ~/.ssh/config Host block
|
|
35
|
+
if [ -n "${KAMPODINE_SSH_KEY:-}" ]; then
|
|
36
|
+
SSH_KEY="$KAMPODINE_SSH_KEY"
|
|
37
|
+
else
|
|
38
|
+
SSH_KEY=""
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
SUB="${1:-}"
|
|
42
|
+
say() { printf '\033[1;35m[deploy %s]\033[0m %s\n' "$SUB" "$*"; }
|
|
43
|
+
warn() { printf '\033[1;33m[deploy %s] WARNING:\033[0m %s\n' "$SUB" "$*" >&2; }
|
|
44
|
+
die() { printf '\033[1;31m[deploy %s] FAIL:\033[0m %s\n' "$SUB" "$*" >&2; exit 1; }
|
|
45
|
+
usage() {
|
|
46
|
+
local code="${1:-0}"
|
|
47
|
+
printf 'Usage:\n'
|
|
48
|
+
grep '^# kampodine deploy' "$0" | sed 's/^# //'
|
|
49
|
+
cat <<'EOF'
|
|
50
|
+
|
|
51
|
+
Lifecycle subcommands are --host-aware with the SAME resolution as deploy:
|
|
52
|
+
--host | --profile <name> | KAMPODINE_PROFILE | config defaultProfile |
|
|
53
|
+
KAMPODINE_HOST; --ssh-key | profile sshKey | KAMPODINE_SSH_KEY | ssh-agent /
|
|
54
|
+
~/.ssh/config.
|
|
55
|
+
|
|
56
|
+
list deployment history: the VM's sha-tagged images (running one marked)
|
|
57
|
+
merged with the local ledger (~/.kampodine/deployments.jsonl —
|
|
58
|
+
every deploy/rollback appends {ts, host, sha, tag, result,
|
|
59
|
+
duration_ms, subject}; shas/tags/hosts only, NEVER secrets) with a
|
|
60
|
+
"Total deployments" footer. VM unreachable degrades to a
|
|
61
|
+
ledger-only view.
|
|
62
|
+
prune reclaim VM disk: removes OLD sha-tagged deploy images. ALWAYS kept:
|
|
63
|
+
the currently-running image, ts-rollback, and the newest --keep N
|
|
64
|
+
(default 2). --dry-run prints the exact podman rmi commands and
|
|
65
|
+
removes nothing; images in use by running containers are refused.
|
|
66
|
+
logs tail the running api container's logs (podman logs --tail).
|
|
67
|
+
restart restart the api service via rc-service (deploy's restart path:
|
|
68
|
+
supervise-daemon re-runs the whole podman run against :latest).
|
|
69
|
+
shell interactive sh inside the running api container; flags must come
|
|
70
|
+
before `exec -- <cmd>...` for one-shot commands.
|
|
71
|
+
|
|
72
|
+
Examples:
|
|
73
|
+
kampodine deploy list --host root@203.0.113.10
|
|
74
|
+
kampodine deploy prune --host root@203.0.113.10 --dry-run
|
|
75
|
+
kampodine deploy prune --host root@203.0.113.10 --keep 3
|
|
76
|
+
kampodine deploy logs --host root@203.0.113.10 --lines 200
|
|
77
|
+
kampodine deploy restart --host root@203.0.113.10
|
|
78
|
+
kampodine deploy shell --host root@203.0.113.10
|
|
79
|
+
kampodine deploy shell --host root@203.0.113.10 exec -- df -h /var/lib/containers
|
|
80
|
+
EOF
|
|
81
|
+
exit "$code"
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
case "$SUB" in
|
|
85
|
+
list|prune|logs|restart|shell) shift ;;
|
|
86
|
+
-h|--help|help) usage 0 ;;
|
|
87
|
+
"") usage 2 ;;
|
|
88
|
+
*)
|
|
89
|
+
printf 'kampodine deploy: unknown lifecycle subcommand: %s\n\n' "$SUB" >&2
|
|
90
|
+
usage 2
|
|
91
|
+
;;
|
|
92
|
+
esac
|
|
93
|
+
|
|
94
|
+
# shell exec: `deploy shell exec -- <cmd>...` — capture the command before the
|
|
95
|
+
# shared flag loop (flags for shell must come BEFORE `exec`).
|
|
96
|
+
SHELL_EXEC=()
|
|
97
|
+
if [[ "$SUB" == "shell" && "${1:-}" == "exec" ]]; then
|
|
98
|
+
shift
|
|
99
|
+
[[ "${1:-}" == "--" ]] || die "shell exec requires -- before the command: kampodine deploy shell exec -- <cmd>..."
|
|
100
|
+
shift
|
|
101
|
+
[[ $# -gt 0 ]] || die "shell exec -- requires a command"
|
|
102
|
+
SHELL_EXEC=("$@")
|
|
103
|
+
set --
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
KEEP="2"
|
|
107
|
+
DRY_RUN=0
|
|
108
|
+
LINES="100"
|
|
109
|
+
PROFILE_FLAG=""
|
|
110
|
+
HOST_FLAG=""
|
|
111
|
+
KEY_FLAG=""
|
|
112
|
+
while [[ $# -gt 0 ]]; do
|
|
113
|
+
case "$1" in
|
|
114
|
+
--host) HOST_FLAG="$2"; KAMPODINE_HOST="$2"; shift 2 ;;
|
|
115
|
+
--ssh-key) KEY_FLAG="$2"; SSH_KEY="$2"; shift 2 ;;
|
|
116
|
+
--profile) PROFILE_FLAG="$2"; shift 2 ;;
|
|
117
|
+
--keep) KEEP="$2"; shift 2 ;;
|
|
118
|
+
--dry-run) DRY_RUN=1; shift ;;
|
|
119
|
+
--lines) LINES="$2"; shift 2 ;;
|
|
120
|
+
-h|--help) usage 0 ;;
|
|
121
|
+
--) shift; break ;;
|
|
122
|
+
*) die "unknown argument: $1 (--help)" ;;
|
|
123
|
+
esac
|
|
124
|
+
done
|
|
125
|
+
|
|
126
|
+
case "$SUB" in
|
|
127
|
+
prune) [[ "$KEEP" =~ ^[0-9]+$ ]] || die "--keep must be a non-negative integer (got: $KEEP)" ;;
|
|
128
|
+
logs) [[ "$LINES" =~ ^[1-9][0-9]*$ ]] || die "--lines must be a positive integer (got: $LINES)" ;;
|
|
129
|
+
esac
|
|
130
|
+
|
|
131
|
+
# profile resolution (flag > KAMPODINE_PROFILE > defaultProfile); an explicit
|
|
132
|
+
# --host/--ssh-key flag always beats the profile
|
|
133
|
+
profile_resolve "$PROFILE_FLAG" "$HOST_FLAG" "$KEY_FLAG"
|
|
134
|
+
SSH_KEY="${KEY_FLAG:-${KAMPODINE_SSH_KEY:-}}"
|
|
135
|
+
|
|
136
|
+
SSH_ARGS=(-o ConnectTimeout=10 -o BatchMode=yes)
|
|
137
|
+
[[ -n "$SSH_KEY" ]] && SSH_ARGS+=(-i "$SSH_KEY")
|
|
138
|
+
# $1 is a composed remote command — client-side expansion is the design.
|
|
139
|
+
# shellcheck disable=SC2029
|
|
140
|
+
vm() { ssh "${SSH_ARGS[@]}" "$KAMPODINE_HOST" "$1"; }
|
|
141
|
+
|
|
142
|
+
# --- macOS ssh-agent quirk (first run from a fresh machine) -------------------
|
|
143
|
+
if [[ "$(uname -s)" == "Darwin" ]]; then
|
|
144
|
+
SSH_AUTH_SOCK="$(launchctl getenv SSH_AUTH_SOCK 2>/dev/null || true)"
|
|
145
|
+
export SSH_AUTH_SOCK
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
require_host() {
|
|
149
|
+
[[ -n "$KAMPODINE_HOST" ]] || die "target required: --host root@<ip>, --profile <name>, KAMPODINE_PROFILE, config defaultProfile, or KAMPODINE_HOST=root@<ip> (resolution matches deploy.sh)"
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
# fetch_image_listing -> "tag|CreatedAt|Size" lines for sha-tagged deploy
|
|
153
|
+
# images; returns 1 only when the VM itself is unreachable (podman's own
|
|
154
|
+
# "no images" exits 0 — a reachable VM with zero images is not an error).
|
|
155
|
+
fetch_image_listing() {
|
|
156
|
+
local raw
|
|
157
|
+
raw="$(vm "podman images --format '{{.Tag}}|{{.CreatedAt}}|{{.Size}}' $IMAGE 2>/dev/null")" || return 1
|
|
158
|
+
printf '%s\n' "$raw" | grep -E '^[0-9a-f]{7,40}\|' || true
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
fetch_running_images() {
|
|
162
|
+
vm "podman ps --format '{{.Image}}' 2>/dev/null" || return 1
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# sh_quote_args <args...> -> the args POSIX-single-quoted for safe composition
|
|
166
|
+
# into one remote sh command string (busybox-safe: '\'' escaping only).
|
|
167
|
+
sh_quote_args() {
|
|
168
|
+
local out="" arg
|
|
169
|
+
for arg in "$@"; do
|
|
170
|
+
arg="${arg//\'/\'\\\'\'}"
|
|
171
|
+
out+="'$arg' "
|
|
172
|
+
done
|
|
173
|
+
printf '%s' "${out% }"
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
case "$SUB" in
|
|
177
|
+
list)
|
|
178
|
+
require_host
|
|
179
|
+
say "deployment history for $KAMPODINE_HOST (ledger: $(ledger_file))"
|
|
180
|
+
images="$(fetch_image_listing)" || images=""
|
|
181
|
+
running="$(fetch_running_images)" || running=""
|
|
182
|
+
if [[ -z "$images" && -z "$running" ]]; then
|
|
183
|
+
if ! vm "podman images >/dev/null 2>&1"; then
|
|
184
|
+
warn "cannot reach VM $KAMPODINE_HOST — showing ledger-only history (no VM truth)"
|
|
185
|
+
fi
|
|
186
|
+
fi
|
|
187
|
+
ledger_lines="$(ledger_entries "$KAMPODINE_HOST")"
|
|
188
|
+
rows="$(render_deploy_list "$images" "$running" "$ledger_lines")"
|
|
189
|
+
printf '%-9s %-17s %-9s %-9s %s\n' "TAG" "CREATED" "STATE" "RESULT" "SUBJECT"
|
|
190
|
+
if [[ -n "$rows" ]]; then
|
|
191
|
+
printf '%s\n' "$rows"
|
|
192
|
+
else
|
|
193
|
+
say "(no deployments recorded yet — the ledger fills as you deploy)"
|
|
194
|
+
fi
|
|
195
|
+
count="$(printf '%s\n' "$rows" | grep -c . || true)"
|
|
196
|
+
ledger_total_line "${count:-0}" "on $KAMPODINE_HOST"
|
|
197
|
+
;;
|
|
198
|
+
|
|
199
|
+
prune)
|
|
200
|
+
require_host
|
|
201
|
+
images="$(fetch_image_listing)" || die "cannot list images on $KAMPODINE_HOST (ssh failed — refusing to prune blind)"
|
|
202
|
+
running="$(fetch_running_images)" || running=""
|
|
203
|
+
removals="$(prune_select "$images" "$running" "$KEEP")" || die "prune selection failed"
|
|
204
|
+
if [[ -z "$removals" ]]; then
|
|
205
|
+
say "nothing to prune (keep set: running + ts-rollback + newest $KEEP)"
|
|
206
|
+
exit 0
|
|
207
|
+
fi
|
|
208
|
+
reclaim="$(sum_sizes_human "$(printf '%s\n' "$removals" | cut -d'|' -f2)")"
|
|
209
|
+
plan_n="$(printf '%s\n' "$removals" | grep -c . || true)"
|
|
210
|
+
say "plan: ${plan_n} image(s), ${reclaim} reclaimed (keep: running + ts-rollback + newest $KEEP)"
|
|
211
|
+
while IFS= read -r line; do
|
|
212
|
+
[[ -n "$line" ]] || continue
|
|
213
|
+
tag="${line%%|*}"
|
|
214
|
+
say " podman rmi $IMAGE:$tag # ${line#*|}"
|
|
215
|
+
done <<<"$removals"
|
|
216
|
+
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
217
|
+
say "dry-run: nothing removed — re-run without --dry-run to apply"
|
|
218
|
+
exit 0
|
|
219
|
+
fi
|
|
220
|
+
while IFS= read -r line; do
|
|
221
|
+
[[ -n "$line" ]] || continue
|
|
222
|
+
tag="${line%%|*}"
|
|
223
|
+
say "removing $IMAGE:$tag"
|
|
224
|
+
vm "podman rmi $IMAGE:$tag" || die "podman rmi $IMAGE:$tag failed (in use? remove manually after checking podman ps)"
|
|
225
|
+
done <<<"$removals"
|
|
226
|
+
disk_pct="$(disk_pct_from_df "$(vm "df -P $DISK_PATH 2>/dev/null" || true)")"
|
|
227
|
+
if [[ -n "$disk_pct" ]]; then
|
|
228
|
+
say "VM disk after prune: ${disk_pct}% used on $DISK_PATH"
|
|
229
|
+
if [[ "$disk_pct" -gt 90 ]]; then
|
|
230
|
+
warn "disk still above 90% — investigate before the next deploy"
|
|
231
|
+
fi
|
|
232
|
+
fi
|
|
233
|
+
;;
|
|
234
|
+
|
|
235
|
+
logs)
|
|
236
|
+
require_host
|
|
237
|
+
say "tailing $CONTAINER on $KAMPODINE_HOST (last $LINES lines)"
|
|
238
|
+
vm "podman logs --tail $LINES $CONTAINER"
|
|
239
|
+
;;
|
|
240
|
+
|
|
241
|
+
restart)
|
|
242
|
+
require_host
|
|
243
|
+
init_detect vm >/dev/null 2>&1 || true
|
|
244
|
+
RESTART_CMD="$(svc_action "$CONTAINER" restart)" \
|
|
245
|
+
|| die "neither rc-service nor systemctl found on $KAMPODINE_HOST — cannot restart (host init auto-detection detected neither; groundwork supports openrc + systemd)"
|
|
246
|
+
say "$RESTART_CMD (deploy's restart path: supervise-daemon on Alpine re-runs podman run on :latest)"
|
|
247
|
+
vm "$RESTART_CMD"
|
|
248
|
+
STATUS_CMD="$(svc_action "$CONTAINER" status 2>/dev/null || true)"
|
|
249
|
+
if [[ -n "$STATUS_CMD" ]]; then
|
|
250
|
+
say "service status:"
|
|
251
|
+
vm "$STATUS_CMD" || say "status command failed — check: kampodine deploy logs"
|
|
252
|
+
fi
|
|
253
|
+
say "done — verify: kampodine deploy logs --lines 50"
|
|
254
|
+
;;
|
|
255
|
+
|
|
256
|
+
shell)
|
|
257
|
+
require_host
|
|
258
|
+
if [[ ${#SHELL_EXEC[@]} -gt 0 ]]; then
|
|
259
|
+
vm "podman exec $CONTAINER $(sh_quote_args "${SHELL_EXEC[@]}")"
|
|
260
|
+
else
|
|
261
|
+
say "interactive sh in $CONTAINER on $KAMPODINE_HOST (exit to leave)"
|
|
262
|
+
say "one-shot instead: kampodine deploy shell exec -- <cmd>"
|
|
263
|
+
ssh "${SSH_ARGS[@]}" -t "$KAMPODINE_HOST" "podman exec -it $CONTAINER sh"
|
|
264
|
+
fi
|
|
265
|
+
;;
|
|
266
|
+
esac
|