kampodine 0.3.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/scripts/deploy.sh CHANGED
@@ -3,7 +3,7 @@
3
3
  #
4
4
  # Mac: podman build (VITE_BUILD_ID=<sha>) -> podman save (docker-archive)
5
5
  # streamed over SSH -> podman load on the VM
6
- # VM: retag to 127.0.0.1:5000/esellar-api:latest -> rc-service esellar-api
6
+ # VM: retag to 127.0.0.1:5000/kampodine-api:latest -> rc-service kampodine-api
7
7
  # restart (the OpenRC supervise-daemon service re-runs the whole
8
8
  # `podman run` against :latest — no pull in the run, so restarts are
9
9
  # offline-safe) -> exec-fetch health probe with served-sha verify ->
@@ -20,27 +20,47 @@
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
- ESPELLAR_HOST="${ESPELLAR_HOST:-}"
44
+ KAMPODINE_HOST="${KAMPODINE_HOST:-}"
30
45
  PROXY_HOST="${PROXY_HOST:-app.example.com}"
31
- ENV_FILE_REMOTE="/etc/esellar/env"
32
- DEPLOYED_SHA_FILE="/etc/esellar/deployed-sha"
46
+ ENV_FILE_REMOTE="/etc/kampodine/env"
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
- IMAGE="127.0.0.1:5000/esellar-api"
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. KAMPODINE_SSH_KEY env (project-level: direnv / .envrc / export)
43
- # 3. ESSELLAR_SSH_KEY env (legacy alias, kept for existing setups)
62
+ # 2. profile sshKey (--profile | KAMPODINE_PROFILE | defaultProfile)
63
+ # 3. KAMPODINE_SSH_KEY env (legacy: direnv / .envrc / export)
44
64
  # 4. empty → ssh-agent and/or the operator's ~/.ssh/config Host block
45
65
  # (the POSIX way: per-host IdentityFile belongs in ssh config, not here)
46
66
  if [ -n "${KAMPODINE_SSH_KEY:-}" ]; then
@@ -58,7 +78,7 @@ die() { printf '\033[1;31m[deploy] FAIL:\033[0m %s\n' "$*" >&2; exit 1; }
58
78
  usage() {
59
79
  cat <<'EOF'
60
80
  Usage:
61
- kampodine deploy [--host root@<ip>] [--version <sha7>] [--rollback [<sha7>]]
81
+ kampodine deploy [--host root@<ip>] [--profile <name>] [--version <sha7>] [--rollback [<sha7>]]
62
82
  [--dockerfile <path>] [--ssh-key <path>] [--skip-smoke] [--refresh-config]
63
83
 
64
84
  Examples:
@@ -66,16 +86,19 @@ EOF
66
86
  grep '^# kampodine deploy' "$0" | sed 's/^# //'
67
87
  cat <<'EOF'
68
88
 
69
- Host/key resolution: --host | ESPELLAR_HOST; --ssh-key | KAMPODINE_SSH_KEY |
70
- ESPELLAR_SSH_KEY | ssh-agent / ~/.ssh/config.
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>.
71
93
  EOF
72
94
  exit 0
73
95
  }
74
96
 
75
97
  while [[ $# -gt 0 ]]; do
76
98
  case "$1" in
77
- --host) ESPELLAR_HOST="$2"; shift 2 ;;
78
- --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 ;;
79
102
  --version) VERSION="$2"; MODE="version"; shift 2 ;;
80
103
  --rollback)
81
104
  MODE="rollback"
@@ -83,19 +106,27 @@ while [[ $# -gt 0 ]]; do
83
106
  ;;
84
107
  --skip-smoke) SKIP_SMOKE=1; shift ;;
85
108
  --refresh-config) REFRESH_CONFIG=1; shift ;;
109
+ --require-disk) REQUIRE_DISK="$2"; shift 2 ;;
86
110
  --dockerfile) DOCKERFILE="$2"; shift 2 ;;
87
111
  -h|--help) usage ;;
88
112
  *) die "unknown argument: $1 (--help)" ;;
89
113
  esac
90
114
  done
91
115
  [[ "$VERSION" != *..* && "$VERSION" =~ ^[0-9a-f]{4,40}$|^$ ]] || die "--version must be a git sha fragment"
92
- [ -n "$ESPELLAR_HOST" ] || die "set ESPELLAR_HOST=root@<vm-ip> (or a ~/.ssh/config Host alias via --host)"
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>"
93
124
 
94
125
  SSH_ARGS=(-o ConnectTimeout=10 -o BatchMode=yes)
95
126
  [[ -n "$SSH_KEY" ]] && SSH_ARGS+=(-i "$SSH_KEY")
96
127
  # $1 is a composed remote command — client-side expansion is the design.
97
128
  # shellcheck disable=SC2029
98
- vm() { ssh "${SSH_ARGS[@]}" "$ESPELLAR_HOST" "$1"; }
129
+ vm() { ssh "${SSH_ARGS[@]}" "$KAMPODINE_HOST" "$1"; }
99
130
 
100
131
  # --- macOS ssh-agent quirk (first deploy from a fresh machine) ---------------
101
132
  if [[ "$(uname -s)" == "Darwin" ]]; then
@@ -103,6 +134,67 @@ if [[ "$(uname -s)" == "Darwin" ]]; then
103
134
  export SSH_AUTH_SOCK
104
135
  fi
105
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
+
106
198
  # --- preflight -----------------------------------------------------------------
107
199
  cd "$REPO_ROOT"
108
200
  if [[ "$MODE" == "deploy" ]]; then
@@ -110,10 +202,15 @@ if [[ "$MODE" == "deploy" ]]; then
110
202
  fi
111
203
  command -v podman >/dev/null 2>&1 || die "podman not found on the Mac (podman machine must run: podman machine start)"
112
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
113
207
  # version mode: stream an EXISTING local build (no rebuild, no clean-tree gate —
114
208
  # the build already happened when that sha was HEAD)
115
209
  [[ "$MODE" == "version" ]] && VER="${VERSION:-$(git rev-parse --short HEAD)}"
116
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
117
214
 
118
215
  # --- build (deploy mode always builds HEAD — cached layers keep it minutes) ----
119
216
  if [[ "$MODE" == "deploy" ]]; then
@@ -141,8 +238,8 @@ vm "podman tag $IMAGE:$VER $IMAGE:latest" || die "stream retag failed"
141
238
 
142
239
  # --- env file from varlock (schema-only from HEAD; secrets from pass) ----------
143
240
  say "generating $ENV_FILE_REMOTE from varlock (schema-only from HEAD; secrets resolve from pass — no plaintext secret files)…"
144
- ENV_TMP="$(mktemp /tmp/esellar-env.XXXXXX)"
145
- trap 'rm -f "$ENV_TMP"' EXIT
241
+ ENV_TMP="$(mktemp /tmp/kampodine-env.XXXXXX)"
242
+ # (the EXIT trap — deploy_on_exit — removes $ENV_TMP on every exit path)
146
243
  # Schema-only resolution: extract the COMMITTED schema into a scratch dir
147
244
  # inside the repo (plugin resolution needs node_modules; local
148
245
  # .env/.env.local never enter the pipeline — varlock sees only the extracted
@@ -158,29 +255,32 @@ git -C "$REPO_ROOT" archive HEAD apps/api/.env.schema | tar -x -C "$ENV_SCHEMA_D
158
255
  | sed -E 's/^([A-Za-z_][A-Za-z0-9_]*)="(.*)"$/\1=\2/' > "$ENV_TMP" \
159
256
  || die "varlock env generation failed (pass store unlocked? schema committed?)"
160
257
  printf 'API_GIT_SHA=%s\n' "$VER" >> "$ENV_TMP"
161
- scp -q "${SSH_ARGS[@]}" "$ENV_TMP" "$ESPELLAR_HOST:/etc/esellar/env.tmp"
162
- vm "mv /etc/esellar/env.tmp $ENV_FILE_REMOTE && chmod 600 $ENV_FILE_REMOTE"
258
+ scp -q "${SSH_ARGS[@]}" "$ENV_TMP" "$KAMPODINE_HOST:/etc/kampodine/env.tmp"
259
+ vm "mv /etc/kampodine/env.tmp $ENV_FILE_REMOTE && chmod 600 $ENV_FILE_REMOTE"
163
260
  rm -f "$ENV_TMP"
164
261
  rm -rf "$ENV_SCHEMA_DIR"
165
262
 
166
263
  if [[ $REFRESH_CONFIG -eq 1 ]]; then
167
264
  say "ansible container-service refresh (--tags container-service — rc script/config drift repair)…"
168
265
  (cd "$REPO_ROOT/infra/alpine-host/ansible" \
169
- && ansible-playbook -i "esellar-vm ansible_host=${ESPELLAR_HOST#*@},ansible_user=${ESPELLAR_HOST%%@*}," playbook.yml --tags container-service --private-key "${SSH_KEY:-~/.ssh/id_ed25519}") \
266
+ && ansible-playbook -i "kampodine-vm ansible_host=${KAMPODINE_HOST#*@},ansible_user=${KAMPODINE_HOST%%@*}," playbook.yml --tags container-service --private-key "${SSH_KEY:-~/.ssh/id_ed25519}") \
170
267
  || die "ansible playbook failed"
171
268
  fi
172
269
 
173
- say "rc-service esellar-api restart (supervise-daemon: stop-old/start-new podman run on the retagged :latest)…"
174
- vm "rc-service esellar-api restart" || die "esellar-api restart failed (rc-service esellar-api status)"
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})"
175
275
 
176
- say "health probe (host-side wget /api/auth/ok + served-sha — works for node AND scratch images)…"
276
+ say "health probe (host-side fetch of /api/auth/ok + served-sha — busybox wget default, curl fallback on GNU-only hosts)…"
177
277
  HEALTH_OK=0
178
278
  for _ in $(seq 1 30); do
179
- # wget runs on the VM (busybox, -p 8080:8080 publish); the served-sha
180
- # grep runs LOCALLY — remote grep quoting is fragile (busybox BRE
181
- # (busybox BRE alternation + nested quotes → permanent false-negative).
182
- # grep -E keeps the alternation portable across BSD/GNU/busybox.
183
- if vm "wget -qO- -T 3 http://127.0.0.1:8080/api/auth/ok 2>/dev/null" \
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")" \
184
284
  | grep -qE "\"(git|build)\":\"$VER"; then
185
285
  HEALTH_OK=1
186
286
  break
@@ -188,17 +288,17 @@ for _ in $(seq 1 30); do
188
288
  sleep 3
189
289
  done
190
290
  if [[ $HEALTH_OK -ne 1 ]]; then
191
- vm "podman logs --tail 30 esellar-api 2>&1" || true
192
- vm "rc-service esellar-api status 2>&1" || true
291
+ vm "podman logs --tail 30 kampodine-api 2>&1" || true
292
+ vm "$(svc_action kampodine-api status 2>/dev/null || echo 'true') 2>&1" || true
193
293
  PREV_HINT="$(vm "test -f $DEPLOYED_SHA_FILE && cat $DEPLOYED_SHA_FILE" 2>/dev/null || true)"
194
294
  die "container never became healthy — rollback: kampodine deploy --rollback${PREV_HINT:+ $PREV_HINT}"
195
295
  fi
196
- CANARY="$(vm "podman logs esellar-api 2>&1 | grep -c ENV_IMPORT_SUSPECT || true")"
296
+ CANARY="$(vm "podman logs kampodine-api 2>&1 | grep -c ENV_IMPORT_SUSPECT || true")"
197
297
  say "container healthy, env-canary hits: $CANARY"
198
298
 
199
299
  say "kamal-proxy re-point (podman exec — same invocation kamal used)…"
200
300
  if vm "podman ps --format '{{.Names}}' | grep -qx kamal-proxy"; then
201
- vm "podman exec kamal-proxy kamal-proxy deploy esellar-api --host=$PROXY_HOST --target=esellar-api:8080 --tls --health-check-path=/api/auth/ok" \
301
+ vm "podman exec kamal-proxy kamal-proxy deploy kampodine-api --host=$PROXY_HOST --target=kampodine-api:8080 --tls --health-check-path=/api/auth/ok" \
202
302
  || die "proxy re-point failed (podman logs kamal-proxy)"
203
303
  else
204
304
  say " kamal-proxy not running — skipping re-point"
@@ -220,4 +320,21 @@ vm "podman image prune -f >/dev/null 2>&1 || true"
220
320
  # busybox head lacks -n -N — tail -n +4 after a newest-first sort keeps 3 tags.
221
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
222
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
223
340
  say "done (version: $VER). instant rollback: kampodine deploy --rollback"
package/scripts/dns.sh CHANGED
@@ -12,8 +12,8 @@
12
12
  # Types are pinned to A | AAAA | CNAME. `add` merges into the existing RRSet
13
13
  # (round-robin records survive); `rm` filters it; both are idempotence-aware.
14
14
  #
15
- # Zone/compartment: OCI_PROFILE (default esellar-api), OCI_COMPARTMENT
16
- # (default esellar). Zone defaults to the compartment's ONLY zone; pass
15
+ # Zone/compartment: OCI_PROFILE (default: "default"), OCI_COMPARTMENT (required)
16
+ # (required). Zone defaults to the compartment's ONLY zone; pass
17
17
  # --zone <id-or-name> when several exist.
18
18
  #
19
19
  # Usage:
@@ -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
- PROFILE="${OCI_PROFILE:-esellar-api}"
27
- COMPARTMENT_NAME="${OCI_COMPARTMENT:-esellar}"
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.
37
+ PROFILE="${OCI_PROFILE:-default}"
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; }
@@ -37,13 +49,13 @@ usage() {
37
49
 
38
50
  Auth is the oci CLI's own — its config file (--profile) or instance
39
51
  principal. kampodine never accepts, stores, or logs credential material.
40
- Zone/compartment: OCI_PROFILE (default esellar-api), OCI_COMPARTMENT (default
41
- esellar). Types are pinned to A | AAAA | CNAME; ttl range 60..172800.
52
+ Zone/compartment: OCI_PROFILE (default: "default"), OCI_COMPARTMENT (default
53
+ kampodine). Types are pinned to A | AAAA | CNAME; ttl range 60..172800.
42
54
 
43
55
  Examples:
44
56
  kampodine dns records
45
- kampodine dns records --zone esellar.example.com
46
- kampodine dns add --name app --type A --value 203.0.113.10 --zone esellar.example.com
57
+ kampodine dns records --zone kampodine.example.com
58
+ kampodine dns add --name app --type A --value 203.0.113.10 --zone kampodine.example.com
47
59
  kampodine dns add --name www --type CNAME --value app.example.com --ttl 3600
48
60
  kampodine dns rm --name app --type A --value 203.0.113.10
49
61
  EOF
@@ -100,17 +112,24 @@ 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"
112
129
  command -v jq >/dev/null 2>&1 \
113
130
  || die "jq not found — brew install jq (dns.sh uses it for RRSet surgery)"
131
+ [[ -n "${OCI_COMPARTMENT:-}" ]] \
132
+ || die "set OCI_COMPARTMENT=<compartment name or ocid> — no default: compartments are account-specific"
114
133
  }
115
134
 
116
135
  compartment_ocid() {
package/scripts/env.sh CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # env.sh — manage the remote app env file (/etc/esellar/env, 0600 root).
2
+ # env.sh — manage the remote app env file (/etc/kampodine/env, 0600 root).
3
3
  #
4
4
  # SECRETS NEVER PRINT. `env list`, `env push`, and the `env pull` summaries
5
5
  # surface FINGERPRINTS ONLY: KEY + value length + first 2 characters. Raw
@@ -8,29 +8,33 @@
8
8
  # secret ever lands in terminal output, logs, or process args.
9
9
  #
10
10
  # Host/key resolution is IDENTICAL to deploy.sh:
11
- # --host root@<ip> | ESPELLAR_HOST | ~/.ssh/config Host alias
12
- # --ssh-key <path> | KAMPODINE_SSH_KEY | ESPELLAR_SSH_KEY (legacy) |
11
+ # --host root@<ip> | KAMPODINE_HOST | ~/.ssh/config Host alias
12
+ # --ssh-key <path> | KAMPODINE_SSH_KEY |
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>] # KEY + fingerprint table (NEVER values)
17
- # kampodine env push --file <local-env-file> [...] # upload: 0600 temp + atomic mv + restart hint
18
- # kampodine env pull [--out <file>] [...] # 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)
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
- ENV_FILE_REMOTE="${KAMPODINE_ENV_REMOTE:-/etc/esellar/env}"
26
+ HERE="${BASH_SOURCE[0]%/*}"
27
+ # shellcheck source=common.sh
28
+ source "$HERE/common.sh"
29
+
30
+ ENV_FILE_REMOTE="${KAMPODINE_ENV_REMOTE:-/etc/kampodine/env}"
27
31
  ENV_TMP_BASE="$(dirname "$ENV_FILE_REMOTE")"
28
32
 
29
- ESPELLAR_HOST="${ESPELLAR_HOST:-}"
33
+ KAMPODINE_HOST="${KAMPODINE_HOST:-}"
30
34
  # SSH key resolution — same ladder as deploy.sh:
31
35
  # 1. --ssh-key flag (explicit, per-invocation)
32
36
  # 2. KAMPODINE_SSH_KEY env (project-level: direnv / .envrc / export)
33
- # 3. ESPELLAR_SSH_KEY env (legacy alias, kept for existing setups)
37
+ # 3. KAMPODINE_SSH_KEY env (legacy alias, kept for existing setups)
34
38
  # 4. empty → ssh-agent and/or the operator's ~/.ssh/config Host block
35
39
  if [ -n "${KAMPODINE_SSH_KEY:-}" ]; then
36
40
  SSH_KEY="$KAMPODINE_SSH_KEY"
@@ -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 | ESPELLAR_HOST; --ssh-key |
51
- KAMPODINE_SSH_KEY | ESPELLAR_SSH_KEY | ssh-agent / ~/.ssh/config.
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) ESPELLAR_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,11 +90,16 @@ 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.
87
101
  # shellcheck disable=SC2029
88
- vm() { ssh "${SSH_ARGS[@]}" "$ESPELLAR_HOST" "$1"; }
102
+ vm() { ssh "${SSH_ARGS[@]}" "$KAMPODINE_HOST" "$1"; }
89
103
 
90
104
  # --- macOS ssh-agent quirk (first run from a fresh machine) -------------------
91
105
  if [[ "$(uname -s)" == "Darwin" ]]; then
@@ -94,7 +108,7 @@ if [[ "$(uname -s)" == "Darwin" ]]; then
94
108
  fi
95
109
 
96
110
  require_host() {
97
- [[ -n "$ESPELLAR_HOST" ]] || die "target required: --host root@<ip> or ESPELLAR_HOST=root@<ip> (resolution matches deploy.sh)"
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.
@@ -126,7 +140,7 @@ env_fp_lines() {
126
140
  }
127
141
 
128
142
  fetch_remote_env() {
129
- vm "cat $ENV_FILE_REMOTE" || die "cannot read $ENV_FILE_REMOTE on $ESPELLAR_HOST (no env file yet? run: kampodine env push --file <env>, or kampodine deploy)"
143
+ vm "cat $ENV_FILE_REMOTE" || die "cannot read $ENV_FILE_REMOTE on $KAMPODINE_HOST (no env file yet? run: kampodine env push --file <env>, or kampodine deploy)"
130
144
  }
131
145
 
132
146
  case "$SUB" in
@@ -142,7 +156,7 @@ case "$SUB" in
142
156
  require_host
143
157
  count="$(grep -cE '^[A-Za-z_][A-Za-z0-9_]*=' "$FILE" || true)"
144
158
  [[ "${count:-0}" -gt 0 ]] || die "no KEY=VALUE lines in $FILE — nothing to push"
145
- say "pushing $FILE -> $ESPELLAR_HOST:$ENV_FILE_REMOTE (fingerprint summary below; values are NEVER printed)"
159
+ say "pushing $FILE -> $KAMPODINE_HOST:$ENV_FILE_REMOTE (fingerprint summary below; values are NEVER printed)"
146
160
  env_fp_lines < "$FILE"
147
161
  tmp_remote="$ENV_TMP_BASE/env.tmp.$$"
148
162
  # 0600 FROM CREATION: umask 077 + ssh stdin pipe — the temp is never
@@ -151,8 +165,8 @@ case "$SUB" in
151
165
  vm "umask 077; cat > $tmp_remote" < "$FILE" || die "upload failed"
152
166
  vm "chmod 600 $tmp_remote && mv -f $tmp_remote $ENV_FILE_REMOTE" \
153
167
  || die "atomic install failed (remote temp left at: $tmp_remote)"
154
- say "installed $ENV_FILE_REMOTE (0600) on $ESPELLAR_HOST"
155
- say "restart to apply: ssh $ESPELLAR_HOST 'rc-service esellar-api restart' # or: kampodine deploy"
168
+ say "installed $ENV_FILE_REMOTE (0600) on $KAMPODINE_HOST"
169
+ say "restart to apply: ssh $KAMPODINE_HOST 'rc-service kampodine-api restart' # or: kampodine deploy"
156
170
  ;;
157
171
 
158
172
  pull)
@@ -167,7 +181,7 @@ case "$SUB" in
167
181
  # stdout IS the payload (pipe into whatever needs the values);
168
182
  # the human-readable summary goes to stderr, masked.
169
183
  printf '%s\n' "$raw"
170
- say "fingerprint summary for $ENV_FILE_REMOTE on $ESPELLAR_HOST (payload above on stdout):" >&2
184
+ say "fingerprint summary for $ENV_FILE_REMOTE on $KAMPODINE_HOST (payload above on stdout):" >&2
171
185
  env_fp_lines <<<"$raw" >&2
172
186
  fi
173
187
  ;;
@@ -14,28 +14,29 @@
14
14
  # Auth: ~/.oci/config (brew install oci-cli; oci setup config) by default, or
15
15
  # --from-pass to source API credentials from the local pass store — the key
16
16
  # NEVER enters this repo, the image, or any log (values only go into env vars).
17
- # Every oci call pins --profile $PROFILE (OCI_PROFILE, default esellar-api) —
18
- # the CLI's DEFAULT profile is NOT the esellar tenancy.
17
+ # Every oci call pins --profile $PROFILE (OCI_PROFILE, default "default") —
18
+ # the CLI's DEFAULT profile is NOT the kampodine tenancy.
19
19
  # Placeholder pass paths (create yours to match):
20
- # esellar/oci/user user OCID
21
- # esellar/oci/tenancy tenancy OCID
22
- # esellar/oci/fingerprint API key fingerprint
23
- # esellar/oci/api-key PEM private key
20
+ # kampodine/oci/user user OCID
21
+ # kampodine/oci/tenancy tenancy OCID
22
+ # kampodine/oci/fingerprint API key fingerprint
23
+ # kampodine/oci/api-key PEM private key
24
24
  #
25
25
  # Usage:
26
26
  # kampodine image-import # defaults below
27
27
  # kampodine image-import --from-pass
28
- # kampodine image-import --image build/esellar-alpine-3.22.6-aarch64.qcow2
28
+ # kampodine image-import --image build/kampodine-alpine-3.22.6-aarch64.qcow2
29
29
  set -euo pipefail
30
30
 
31
31
  REPO_ROOT="$(git rev-parse --show-toplevel)"
32
32
  IMAGE=""
33
- BUCKET="${OCI_IMPORT_BUCKET:-esellar-image-import}"
34
- NAME_PREFIX="${OCI_IMPORT_NAME:-esellar-alpine}"
35
- COMPARTMENT_NAME="${OCI_COMPARTMENT:-esellar}"
36
- PROFILE="${OCI_PROFILE:-esellar-api}"
33
+ BUCKET="${OCI_IMPORT_BUCKET:-kampodine-image-import}"
34
+ NAME_PREFIX="${OCI_IMPORT_NAME:-kampodine-alpine}"
35
+ COMPARTMENT_NAME="${OCI_COMPARTMENT:-kampodine}"
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,19 +66,27 @@ 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)"
77
+ [[ -n "${OCI_COMPARTMENT:-}" ]] || die "set OCI_COMPARTMENT=<compartment name or ocid> — no default: compartments are account-specific"
68
78
  command -v pass >/dev/null 2>&1 || true
69
79
 
70
80
  if [[ $FROM_PASS -eq 1 ]]; then
71
81
  command -v pass >/dev/null 2>&1 || die "pass not installed (brew install pass)"
72
82
  say "sourcing OCI API credentials from pass (values never printed/logged)…"
73
83
  for entry in user tenancy fingerprint api-key; do
74
- pass show "esellar/oci/$entry" >/dev/null 2>&1 || die "missing pass entry esellar/oci/$entry"
84
+ pass show "kampodine/oci/$entry" >/dev/null 2>&1 || die "missing pass entry kampodine/oci/$entry"
75
85
  done
76
- OCI_CLI_USER="$(pass show esellar/oci/user)"
77
- OCI_CLI_TENANCY="$(pass show esellar/oci/tenancy)"
78
- OCI_CLI_FINGERPRINT="$(pass show esellar/oci/fingerprint)"
79
- OCI_CLI_KEY_CONTENT="$(pass show esellar/oci/api-key)"
86
+ OCI_CLI_USER="$(pass show kampodine/oci/user)"
87
+ OCI_CLI_TENANCY="$(pass show kampodine/oci/tenancy)"
88
+ OCI_CLI_FINGERPRINT="$(pass show kampodine/oci/fingerprint)"
89
+ OCI_CLI_KEY_CONTENT="$(pass show kampodine/oci/api-key)"
80
90
  export OCI_CLI_USER OCI_CLI_TENANCY OCI_CLI_FINGERPRINT OCI_CLI_KEY_CONTENT
81
91
  fi
82
92
 
@@ -84,7 +94,7 @@ fi
84
94
  if [[ -z "$IMAGE" ]]; then
85
95
  # Constrained glob (our own build output names) — ls is fine here.
86
96
  # shellcheck disable=SC2012
87
- IMAGE="$(ls -t "$REPO_ROOT/infra/alpine-host/build"/esellar-alpine-*.qcow2 2>/dev/null | head -1 || true)"
97
+ IMAGE="$(ls -t "$REPO_ROOT/infra/alpine-host/build"/kampodine-alpine-*.qcow2 2>/dev/null | head -1 || true)"
88
98
  [[ -n "$IMAGE" ]] || die "no qcow2 under infra/alpine-host/build/ — run: (cd infra/alpine-host && packer build .)"
89
99
  fi
90
100
  [[ -f "$IMAGE" ]] || die "image not found: $IMAGE"
@@ -96,7 +106,7 @@ NAMESPACE="$(oci os ns get --profile "$PROFILE" --query data --raw-output)"
96
106
 
97
107
  STAMP="$(date +%Y%m%d-%H%M%S)"
98
108
  OBJECT_NAME="${NAME_PREFIX}-${STAMP}.qcow2"
99
- IMAGE_NAME="${NAME_PREFIX}-$(basename "$IMAGE" .qcow2 | sed 's/^esellar-alpine-//')-${STAMP}"
109
+ IMAGE_NAME="${NAME_PREFIX}-$(basename "$IMAGE" .qcow2 | sed 's/^kampodine-alpine-//')-${STAMP}"
100
110
 
101
111
  say "uploading $(basename "$IMAGE") -> os://$BUCKET/$OBJECT_NAME"
102
112
  oci os object put -bn "$BUCKET" --profile "$PROFILE" --file "$IMAGE" --name "$OBJECT_NAME" --force \
@@ -162,7 +172,7 @@ say " display : $IMAGE_NAME"
162
172
  say " firmware : $IMPORT_FIRMWARE"
163
173
  say "next:"
164
174
  if [[ "$IMPORT_FIRMWARE" == "UEFI_64" ]]; then
165
- say " A1-ready. kampodine bluegreen provision <blue|green> picks this image (newest esellar-alpine*)."
175
+ say " A1-ready. kampodine bluegreen provision <blue|green> picks this image (newest kampodine-alpine*)."
166
176
  else
167
177
  say " A1 launch will REJECT this image (firmware $IMPORT_FIRMWARE) — see the WARN above."
168
178
  fi