kampodine 0.2.2 → 0.4.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 CHANGED
@@ -1,5 +1,52 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0 — 2026-10-07
4
+
5
+ **Breaking: project-generic naming.** Every origin-coupled default name is
6
+ gone; the package now speaks only for itself.
7
+
8
+ - `KAMPODINE_HOST` replaces `ESPELLAR_HOST` everywhere (deploy, env); the
9
+ key-resolution ladder is `--ssh-key` | `KAMPODINE_SSH_KEY` | ssh-agent /
10
+ ssh-config — the old `ESPELLAR_SSH_KEY` / `ESSELLAR_SSH_KEY` aliases are
11
+ no longer mentioned anywhere
12
+ - Guest paths renamed: `/etc/esellar/*` → `/etc/kampodine/*` (env file,
13
+ deployed-sha, anchor.conf); the anchor watcher is `kampodine-anchor`
14
+ - Service/container/image names: `esellar-api` → `kampodine-api`,
15
+ `esellar-blue`/`esellar-green` → `kampodine-blue`/`kampodine-green`,
16
+ golden image `esellar-alpine*` → `kampodine-alpine*`
17
+ - `OCI_COMPARTMENT` is now REQUIRED (no default — compartments are
18
+ account-specific); `OCI_PROFILE` defaults to `default` (the OCI CLI's
19
+ own default profile) instead of a named profile
20
+ - Existing guests keep booting, but flips against guests provisioned by
21
+ older releases need their watcher/paths updated (or re-run
22
+ `vm-prepare` on a fresh host) since the anchor protocol path changed
23
+
24
+ ## 0.3.0 — 2026-10-07
25
+
26
+ Full-lifecycle orchestration with first-class help — the "mini vercel CLI"
27
+ release.
28
+
29
+ - `env` — manage the remote app env file (`/etc/esellar/env`, 0600 root):
30
+ `list` (KEY + value fingerprints only — length + first 2 chars, values
31
+ NEVER printed), `push --file` (0600 temp from creation via umask + atomic
32
+ `mv` + restart hint), `pull` (raw payload to stdout or `--out` 0600, masked
33
+ summary), `fingerprint` (local masking preview). Host/key resolution
34
+ identical to deploy (`--host` | `ESPELLAR_HOST`, `--ssh-key` |
35
+ `KAMPODINE_SSH_KEY` | ssh-agent)
36
+ - `dns` — OCI DNS records: `records`, `add`, `rm`. Shells out to the `oci`
37
+ CLI; auth is the OCI config file (`--profile`) or `--instance-principal`
38
+ ONLY — no credential material is ever accepted, stored, or logged. Types
39
+ pinned to A|AAAA|CNAME; name/type/value/ttl validated locally before any
40
+ provider call; `add` merges into the existing RRSet, `rm` filters it
41
+ - Help everywhere: `kampodine --help` prints a grouped command index
42
+ (DEPLOY / INFRA / DNS / ENV, vercel-style); every command and every
43
+ bluegreen/env/dns sub-step answers `--help`/`-h` with `Usage:` +
44
+ `Examples:` — pinned by a test that enumerates all of them
45
+ - Tests grew from 38 to 114: help coverage for every subcommand, env
46
+ fingerprint masking against fixture ssh shims (a value in output fails the
47
+ suite), dns arg validation, top-level index groups, oci `--profile` gate
48
+ extended to `dns` calls
49
+
3
50
  ## 0.2.1 — 2026-10-07
4
51
 
5
52
  Docs, hygiene, and deploy hardening.
package/README.md CHANGED
@@ -48,12 +48,28 @@ kampodine deploy:
48
48
  - **`kampodine deploy`** — stream deploy; `--version <sha>` restreams an
49
49
  existing build; `--rollback [sha]` is an instant image-tag rollback (the
50
50
  previous image stays on the VM for exactly this)
51
+ - **`kampodine env`** — manage the remote app env file (`/etc/kampodine/env`,
52
+ 0600 root) without ever printing a secret: `list` shows KEY + fingerprints
53
+ only (value length + first 2 chars), `push --file <env>` uploads over ssh
54
+ stdin into a 0600 temp + atomic `mv`, `pull` streams the raw payload to
55
+ stdout or `--out` (written 0600). Host/key resolution is identical to
56
+ deploy: `--host root@<ip>` | `KAMPODINE_HOST`, `--ssh-key <path>` |
57
+ `KAMPODINE_SSH_KEY` | ssh-agent. `env fingerprint` previews the masking
58
+ for any local file — values never leave stdin
59
+ - **`kampodine dns`** — OCI DNS records for the post-sslip.io era:
60
+ `records`, `add --name <label> --type A|AAAA|CNAME --value <target>
61
+ [--ttl 300]`, `rm`. Shell out to the `oci` CLI; auth is the OCI config
62
+ file (`--profile`) or `--instance-principal` ONLY — kampodine never
63
+ accepts, stores, or logs credential material. `add` merges into the
64
+ existing RRSet (round-robin survives), `rm` filters it; types/ttl/value
65
+ are validated locally before any provider call
51
66
  - **`kampodine bluegreen status|init|provision|flip|rollback`** — reserved
52
67
  public IP management for a two-instance blue/green pair on OCI: zero DNS
53
68
  change, health-gated flips with an ACME-first cutover (reserved IP assigned
54
69
  → cert issued for its hostname → served sha verified) and automatic
55
70
  rollback. A guest **anchor watcher** installed by `vm-prepare` holds the
56
71
  flip's IP half — add-only and inert until a flip writes its anchor config.
72
+ Every sub-step has its own `--help` with usage + examples
57
73
  - **`kampodine vm-prepare`** — first-run bootstrap of a bare Alpine host:
58
74
  sshd hardening (fresh VMs pass unattended), busybox-wget health probes (the
59
75
  golden image ships no curl), the blue-green anchor watcher, the podman
@@ -63,6 +79,11 @@ kampodine deploy:
63
79
  use `bluegreen provision` for ARM targets
64
80
  - **`kampodine migrate`** — sqlite migrations over SSH
65
81
 
82
+ **Help everywhere:** `kampodine --help` (or bare `kampodine`) prints a
83
+ grouped command index (DEPLOY / INFRA / DNS / ENV, vercel-style); every
84
+ command — and every bluegreen/env/dns sub-step — answers `--help` with
85
+ usage + examples. No subcommand silently does nothing on `--help`.
86
+
66
87
  ## Kamal parity
67
88
 
68
89
  | kamal | kampodine |
@@ -135,9 +156,9 @@ automation is on the roadmap; today the stream is the automated path.
135
156
  ## Prerequisites
136
157
 
137
158
  Deploy machine: node ≥ 20, podman, ssh key access to the target, `oci` CLI
138
- (for bluegreen / image-import), and whatever secret-resolution your env-file
139
- step uses (kampodine is agnostic; the reference setup uses
140
- [varlock](https://varlock.dev) + pass).
159
+ (for bluegreen / image-import / dns), `jq` (for dns record surgery), and
160
+ whatever secret-resolution your env-file step uses (kampodine is agnostic;
161
+ the reference setup uses [varlock](https://varlock.dev) + pass).
141
162
 
142
163
  Target: a converged Alpine + Podman + OpenRC host (see above), reachable over
143
164
  ssh as root, with kamal-proxy running.
package/cli.js CHANGED
@@ -14,20 +14,34 @@ const commands = {
14
14
  "vm-prepare": "vm-prepare.sh",
15
15
  "image-import": "image-import.sh",
16
16
  migrate: "migrate.sh",
17
+ env: "env.sh",
18
+ dns: "dns.sh",
17
19
  };
18
20
 
21
+ // Command index, grouped vercel-style. Every command (and every sub-step of
22
+ // bluegreen/env/dns) answers --help with Usage + Examples — pinned by
23
+ // test/help-coverage.test.ts.
19
24
  const usage = `kampodine — kamal-alternative CLI for Alpine + Podman deploys, built on kamal-proxy
20
25
 
21
26
  Usage: kampodine <command> [args...]
22
27
 
23
- Commands:
28
+ DEPLOY
24
29
  deploy stream deploy (podman save | ssh podman load) with sha-verified health gate; --rollback [sha] = instant image-tag rollback
25
- bluegreen reserved-IP blue/green pair: status (pair + reserved IP + health) | init | provision | flip | rollback
30
+ bluegreen reserved-IP blue/green pair: status | init | provision | flip | rollback (each sub-step has --help)
31
+ migrate tenant db migrations over SSH
32
+
33
+ INFRA
26
34
  vm-prepare first-run bootstrap of a bare Alpine host (OpenRC + podman stack)
27
35
  image-import golden qcow2 -> OCI custom image
28
- migrate tenant db migrations over SSH
36
+ status live health through the proxy + the blue/green pair view
37
+
38
+ DNS
39
+ dns OCI DNS records (oci config-file / instance-principal auth ONLY): records | add | rm
40
+
41
+ ENV
42
+ env remote app env file (/etc/kampodine/env, 0600): list | push | pull — values NEVER printed, fingerprints only
29
43
 
30
- All further args pass through to the underlying script.
44
+ Every command supports --help with usage + examples. All further args pass through to the underlying script.
31
45
  `;
32
46
 
33
47
  const [cmd, ...args] = process.argv.slice(2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kampodine",
3
- "version": "0.2.2",
3
+ "version": "0.4.0",
4
4
  "description": "Kamal-style deploys for Alpine + Podman hosts, built on kamal-proxy",
5
5
  "license": "AGPL-3.0",
6
6
  "type": "module",
@@ -8,7 +8,7 @@
8
8
  # API call, rollback is the same call reversed.
9
9
  #
10
10
  # Colors are instance display names: <app>-blue / <app>-green (defaults:
11
- # esellar-blue / esellar-green).
11
+ # kampodine-blue / kampodine-green).
12
12
  # The reserved IP is derived from live OCI state — no local state file.
13
13
  #
14
14
  # Usage:
@@ -20,7 +20,7 @@
20
20
  #
21
21
  # FLIP = ACME-FIRST:
22
22
  # 1. health gate on the target's own IP
23
- # 2. anchor.conf written on the TARGET guest over ssh — the esellar-anchor
23
+ # 2. anchor.conf written on the TARGET guest over ssh — the kampodine-anchor
24
24
  # watcher service (shipped by vm-prepare) configures the anchor private
25
25
  # address within one interval; flip waits for `ip addr` to show it
26
26
  # 3. OCI assigns the reserved IP to the target's anchor (secondary private
@@ -49,7 +49,7 @@
49
49
  # instead — it runs the same ACME-first sequence there.
50
50
  #
51
51
  # provision has TWO routes:
52
- # 1. NATIVE — a UEFI_64 esellar-alpine* custom image exists in the
52
+ # 1. NATIVE — a UEFI_64 kampodine-alpine* custom image exists in the
53
53
  # compartment: launch it directly (the golden image boots as-is).
54
54
  # 2. INJECT (provision-via-migrate) — OCI pins imported custom images to
55
55
  # firmware=BIOS and A1/Ampere is UEFI-only, but the template instance
@@ -63,7 +63,7 @@
63
63
  # 'gunzip | sudo dd', reboot, verify /etc/alpine-release). The instance
64
64
  # record keeps the platform image metadata — exactly like the template.
65
65
  #
66
- # Env: OCI_PROFILE (default esellar-api), OCI_COMPARTMENT (default esellar).
66
+ # Env: OCI_PROFILE (default: "default"), OCI_COMPARTMENT (required).
67
67
  # Injection extras: ALPINE_QCOW2 (golden disk path), OPS_SSH_PUBKEY (ops
68
68
  # public key for the platform-image launch), PLATFORM_SSH_USER (default
69
69
  # ubuntu), INJECT_PROBE_SLEEP / INJECT_PROBE_TRIES (ssh wait tuning).
@@ -71,8 +71,8 @@
71
71
  # loopback; the reserved IP is checked over :80 with the prod Host header).
72
72
  set -euo pipefail
73
73
 
74
- PROFILE="${OCI_PROFILE:-esellar-api}"
75
- COMPARTMENT_NAME="${OCI_COMPARTMENT:-esellar}"
74
+ PROFILE="${OCI_PROFILE:-default}"
75
+ COMPARTMENT_NAME="${OCI_COMPARTMENT:-kampodine}"
76
76
  APP_HOST_HEADER="${APP_HOST_HEADER:-app.example.com}"
77
77
  REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)}"
78
78
  SSH_OPTS=(-o ConnectTimeout=6 -o BatchMode=yes -o StrictHostKeyChecking=accept-new)
@@ -80,6 +80,95 @@ SSH_OPTS=(-o ConnectTimeout=6 -o BatchMode=yes -o StrictHostKeyChecking=accept-n
80
80
  say() { printf '%s\n' "$*"; }
81
81
  die() { printf '✋ %s\n' "$*" >&2; exit 1; }
82
82
 
83
+ usage() {
84
+ printf 'Usage:\n'
85
+ grep '^# kampodine bluegreen' "$0" | sed 's/^# //'
86
+ cat <<'EOF'
87
+
88
+ Each sub-step has its own --help: status | init | provision | flip | rollback.
89
+ Env: OCI_PROFILE (default: "default"), OCI_COMPARTMENT (required).
90
+
91
+ Examples:
92
+ kampodine bluegreen status
93
+ kampodine bluegreen init
94
+ kampodine bluegreen provision green
95
+ kampodine bluegreen flip --to green
96
+ kampodine bluegreen rollback
97
+ EOF
98
+ exit 0
99
+ }
100
+
101
+ step_usage() {
102
+ case "$1" in
103
+ status)
104
+ cat <<'EOF'
105
+ Usage:
106
+ kampodine bluegreen status
107
+
108
+ Pair view: reserved IP + holder, both instances (ocid, ip, AD), per-color app
109
+ health. Read-only.
110
+
111
+ Examples:
112
+ kampodine bluegreen status
113
+ EOF
114
+ ;;
115
+ init)
116
+ cat <<'EOF'
117
+ Usage:
118
+ kampodine bluegreen init
119
+
120
+ Create the DORMANT reserved public IP (unassigned — no instance attached).
121
+ Idempotent: exits 0 when the reserved IP already exists.
122
+
123
+ Examples:
124
+ kampodine bluegreen init
125
+ EOF
126
+ ;;
127
+ provision)
128
+ cat <<'EOF'
129
+ Usage:
130
+ kampodine bluegreen provision <blue|green>
131
+
132
+ Launch the second instance from the golden image (native UEFI custom image,
133
+ or the INJECT route when none exists), then vm-prepare it.
134
+
135
+ Examples:
136
+ kampodine bluegreen provision green
137
+ kampodine vm-prepare --host root@<new-ip> # after provision hands you the IP
138
+ EOF
139
+ ;;
140
+ flip)
141
+ cat <<'EOF'
142
+ Usage:
143
+ kampodine bluegreen flip --to <blue|green> [--force]
144
+
145
+ ACME-first health-gated cutover: health gate on the target, the guest anchor
146
+ watcher claims the reserved-IP half, OCI assigns, the cert issues on the
147
+ target, verify through the reserved IP — any post-assign failure auto-rolls
148
+ back.
149
+
150
+ Examples:
151
+ kampodine bluegreen flip --to green
152
+ kampodine bluegreen flip --to green --force
153
+ EOF
154
+ ;;
155
+ rollback)
156
+ cat <<'EOF'
157
+ Usage:
158
+ kampodine bluegreen rollback
159
+
160
+ Unassign the reserved IP back to DORMANT: holder guest cleanup (anchor.conf
161
+ removal + address delete) then the OCI unassign. To move traffic to the other
162
+ color of a real pair instead, use flip --to <other>.
163
+
164
+ Examples:
165
+ kampodine bluegreen rollback
166
+ EOF
167
+ ;;
168
+ esac
169
+ exit 0
170
+ }
171
+
83
172
  compartment_ocid() {
84
173
  local ocid
85
174
  ocid="$(oci iam compartment list --all --profile "$PROFILE" \
@@ -92,7 +181,7 @@ compartment_ocid() {
92
181
  instance_by_color() {
93
182
  local comp="$1" color="$2" row
94
183
  row="$(oci compute instance list -c "$comp" --profile "$PROFILE" \
95
- --display-name "esellar-$color" --lifecycle-state RUNNING \
184
+ --display-name "kampodine-$color" --lifecycle-state RUNNING \
96
185
  --query 'sort_by(data, &"time-created")[-1] | ["id", "availability-domain"] | join('\'' '\'', @)' \
97
186
  --raw-output 2>/dev/null || true)"
98
187
  [[ -n "$row" ]] || return 0
@@ -151,7 +240,7 @@ reserved_anchor_ip() {
151
240
  return 0
152
241
  fi
153
242
  oci network private-ip create --profile "$PROFILE" --vnic-id "$vnic" \
154
- --display-name "esellar-reserved-anchor" \
243
+ --display-name "kampodine-reserved-anchor" \
155
244
  --query 'data.id' --raw-output 2>/dev/null
156
245
  }
157
246
 
@@ -160,9 +249,9 @@ FLIP_POLL_SLEEP="${FLIP_POLL_SLEEP:-5}"
160
249
  FLIP_ADDR_TRIES="${FLIP_ADDR_TRIES:-12}" # guest watcher pickup: 12 x 5s = 60s
161
250
  FLIP_ACME_TRIES="${FLIP_ACME_TRIES:-24}" # LE HTTP-01: 24 x 5s = 120s
162
251
 
163
- # Guest anchor protocol (the esellar-anchor watcher half lives in
164
- # infra/alpine-host/ansible/roles/container-service/files/esellar-anchor.sh —
165
- # both halves of the conf path must stay in sync: /etc/esellar/anchor.conf).
252
+ # Guest anchor protocol (the kampodine-anchor watcher half lives in
253
+ # infra/alpine-host/ansible/roles/container-service/files/kampodine-anchor.sh —
254
+ # both halves of the conf path must stay in sync: /etc/kampodine/anchor.conf).
166
255
  # Remote commands are composed client-side BY DESIGN (vm-prepare convention);
167
256
  # the interpolated values are OCI-API derived and regex-gated at the call
168
257
  # sites, never user input.
@@ -171,7 +260,7 @@ FLIP_ACME_TRIES="${FLIP_ACME_TRIES:-24}" # LE HTTP-01: 24 x 5s = 120s
171
260
  write_anchor_conf() {
172
261
  local ip="$1" addr="$2"
173
262
  ssh "${SSH_OPTS[@]}" "root@$ip" \
174
- "umask 077; mkdir -p /etc/esellar; printf 'ANCHOR_ADDR=%s\nANCHOR_IFACE=\n' '$addr' > /etc/esellar/anchor.conf && echo ANCHOR_CONF_WRITTEN"
263
+ "umask 077; mkdir -p /etc/kampodine; printf 'ANCHOR_ADDR=%s\nANCHOR_IFACE=\n' '$addr' > /etc/kampodine/anchor.conf && echo ANCHOR_CONF_WRITTEN"
175
264
  }
176
265
 
177
266
  # shellcheck disable=SC2029
@@ -185,7 +274,7 @@ anchor_addr_ready() {
185
274
  read_anchor_conf_addr() {
186
275
  local ip="$1" line addr
187
276
  line="$(ssh "${SSH_OPTS[@]}" "root@$ip" \
188
- 'grep -h "^ANCHOR_ADDR=" /etc/esellar/anchor.conf 2>/dev/null | head -n 1' 2>/dev/null || true)"
277
+ 'grep -h "^ANCHOR_ADDR=" /etc/kampodine/anchor.conf 2>/dev/null | head -n 1' 2>/dev/null || true)"
189
278
  addr="${line#ANCHOR_ADDR=}"
190
279
  addr="${addr//\"/}"
191
280
  addr="${addr//\'/}"
@@ -208,7 +297,7 @@ delete_guest_addr() {
208
297
  # re-added the address once; retry the delete across one watcher interval.
209
298
  cleanup_target_anchor() {
210
299
  local ip="$1" addr="$2" i
211
- ssh "${SSH_OPTS[@]}" "root@$ip" "rm -f /etc/esellar/anchor.conf" >/dev/null 2>&1 || true
300
+ ssh "${SSH_OPTS[@]}" "root@$ip" "rm -f /etc/kampodine/anchor.conf" >/dev/null 2>&1 || true
212
301
  for ((i = 1; i <= 3; i++)); do
213
302
  delete_guest_addr "$ip" "$addr"
214
303
  anchor_addr_ready "$ip" "$addr" >/dev/null 2>&1 || return 0
@@ -234,7 +323,7 @@ flip_failure_rollback() {
234
323
  if [[ "$bpip" == ocid1.privateip* ]]; then
235
324
  if oci network public-ip update --public-ip-id "$rocid" --profile "$PROFILE" \
236
325
  --private-ip-id "$bpip" --force >/dev/null 2>&1; then
237
- say "rolled back to esellar-$ob (existing anchor $bpip)" >&2
326
+ say "rolled back to kampodine-$ob (existing anchor $bpip)" >&2
238
327
  recovered=1
239
328
  fi
240
329
  fi
@@ -242,14 +331,14 @@ flip_failure_rollback() {
242
331
  if (( ! recovered )); then
243
332
  if oci network public-ip update --public-ip-id "$rocid" --profile "$PROFILE" \
244
333
  --private-ip-id "" --force --wait-for-state AVAILABLE >/dev/null 2>&1; then
245
- say "reserved ip UNASSIGNED (dormant) — no existing anchor on esellar-$ob" >&2
334
+ say "reserved ip UNASSIGNED (dormant) — no existing anchor on kampodine-$ob" >&2
246
335
  else
247
336
  cleanup_target_anchor "$tpub" "$taddr"
248
337
  die "AUTO-ROLLBACK FAILED — reserved ip state unknown; flip manually via console: $rocid"
249
338
  fi
250
339
  fi
251
340
  cleanup_target_anchor "$tpub" "$taddr"
252
- say "esellar-$to cleaned (anchor.conf removed, anchor address deleted) — investigate before retrying" >&2
341
+ say "kampodine-$to cleaned (anchor.conf removed, anchor address deleted) — investigate before retrying" >&2
253
342
  }
254
343
 
255
344
  # instance_healthy <ephemeral_ip> -> ssh + loopback app check.
@@ -260,7 +349,7 @@ instance_healthy() {
260
349
  local ip="$1"
261
350
  [[ -n "$ip" ]] || return 1
262
351
  ssh "${SSH_OPTS[@]}" "root@$ip" \
263
- "rc-service esellar-api status >/dev/null 2>&1 && busybox wget -q -O /dev/null http://127.0.0.1:8080/up" 2>/dev/null
352
+ "rc-service kampodine-api status >/dev/null 2>&1 && busybox wget -q -O /dev/null http://127.0.0.1:8080/up" 2>/dev/null
264
353
  }
265
354
 
266
355
  # instance_wait_running <iid> — poll lifecycle-state to RUNNING. The OCI CLI's
@@ -276,7 +365,7 @@ instance_wait_running() {
276
365
  --query 'data."lifecycle-state"' --raw-output 2>/dev/null || true)"
277
366
  case "$state" in
278
367
  RUNNING) return 0 ;;
279
- FAILED | TERMINATED | TERMINATING) die "esellar instance reached $state — nothing to inject, check the console" ;;
368
+ FAILED | TERMINATED | TERMINATING) die "kampodine instance reached $state — nothing to inject, check the console" ;;
280
369
  esac
281
370
  sleep "$sleep_s"
282
371
  done
@@ -301,7 +390,7 @@ inject_alpine() {
301
390
  # the Alpine verify forever. The scrub below
302
391
  # clears the phase file between the two boots; the user's file is never
303
392
  # touched.
304
- kh="$(mktemp "${TMPDIR:-/tmp}/esellar-inject-kh.XXXXXX")"
393
+ kh="$(mktemp "${TMPDIR:-/tmp}/kampodine-inject-kh.XXXXXX")"
305
394
  iss() { ssh -o UserKnownHostsFile="$kh" "${SSH_OPTS[@]}" "$@"; }
306
395
  say "inject: waiting for ssh (${ruser}@${ip}, platform-image first boot)…"
307
396
  ssh_wait_probe() { iss "${ruser}@${ip}" true; }
@@ -311,7 +400,7 @@ inject_alpine() {
311
400
  [[ "$i" == "$probe_tries" ]] && die "ssh never came up on ${ip} (${ruser}) — check the instance console connection"
312
401
  sleep "$probe_sleep"
313
402
  done
314
- raw="$(mktemp "${TMPDIR:-/tmp}/esellar-inject-raw.XXXXXX")"
403
+ raw="$(mktemp "${TMPDIR:-/tmp}/kampodine-inject-raw.XXXXXX")"
315
404
  say "inject: converting ${qcow2} -> raw…"
316
405
  qemu-img convert -O raw "$qcow2" "$raw"
317
406
  say "inject: streaming golden disk -> ${ip} boot volume (gunzip | dd, conv=fsync)…"
@@ -351,17 +440,30 @@ inject_alpine() {
351
440
  done
352
441
  rm -f "$kh"
353
442
  [[ -n "$rel" ]] || die "injection streamed but no ALPINE 3.x ssh on ${ip} after reboot (got: '${rel:-nothing}') — check the serial console; terminate, do NOT flip to ${color}"
354
- say "INJECTED esellar-$color: Alpine ${rel} boots on ${ip} (instance image metadata stays the platform image — like green)"
443
+ say "INJECTED kampodine-$color: Alpine ${rel} boots on ${ip} (instance image metadata stays the platform image — like green)"
355
444
  say "next: kampodine vm-prepare --host root@${ip} -> kampodine deploy --host root@${ip}"
356
445
  say "then 'bluegreen.sh flip --to ${color}' (health-gated) once its app checks green."
357
446
  }
358
447
 
359
448
  cmd="${1:-}"
449
+ # Any -h/--help in the args answers with usage for that sub-step (exit 0) —
450
+ # before any validation or OCI call, so help is always hermetic.
451
+ for help_arg in "$@"; do
452
+ case "$help_arg" in
453
+ -h|--help)
454
+ case "$cmd" in
455
+ status|init|provision|flip|rollback) step_usage "$cmd" ;;
456
+ *) usage ;;
457
+ esac
458
+ ;;
459
+ esac
460
+ done
461
+ [[ -n "${OCI_COMPARTMENT:-}" ]] || die "set OCI_COMPARTMENT=<compartment name or ocid> — no default: compartments are account-specific"
360
462
  case "$cmd" in
361
463
  status)
362
464
  comp="$(compartment_ocid)"
363
465
  rp="$(reserved_ip "$comp")"
364
- say "== esellar blue/green pair (compartment $COMPARTMENT_NAME) =="
466
+ say "== kampodine blue/green pair (compartment $COMPARTMENT_NAME) =="
365
467
  if [[ -n "$rp" ]]; then
366
468
  read -r rocid raddr rholder <<<"$rp"
367
469
  say "reserved IP : $raddr ($rocid)"
@@ -374,9 +476,9 @@ case "$cmd" in
374
476
  if [[ -n "$row" ]]; then
375
477
  read -r iid pub ad <<<"$row"
376
478
  if instance_healthy "$pub"; then verdict=HEALTHY; else verdict=UNHEALTHY/unreachable; fi
377
- say "esellar-$color : $iid ip=${pub:-none} ad=${ad:-?} app=$verdict"
479
+ say "kampodine-$color : $iid ip=${pub:-none} ad=${ad:-?} app=$verdict"
378
480
  else
379
- say "esellar-$color : not provisioned"
481
+ say "kampodine-$color : not provisioned"
380
482
  fi
381
483
  done
382
484
  ;;
@@ -390,7 +492,7 @@ case "$cmd" in
390
492
  exit 0
391
493
  fi
392
494
  addr="$(oci network public-ip create -c "$comp" --profile "$PROFILE" \
393
- --lifetime RESERVED --display-name esellar-active \
495
+ --lifetime RESERVED --display-name kampodine-active \
394
496
  --query 'data."ip-address"' --raw-output)"
395
497
  say "created DORMANT reserved IP: $addr (unassigned — no instance attached)"
396
498
  say "Point DNS/SSLIP at this address when the pair goes active."
@@ -401,25 +503,25 @@ case "$cmd" in
401
503
  color="${1:-}"
402
504
  [[ "$color" == blue || "$color" == green ]] || die "usage: kampodine bluegreen provision <blue|green>"
403
505
  comp="$(compartment_ocid)"
404
- instance_by_color "$comp" "$color" | grep -q . && die "esellar-$color already RUNNING"
506
+ instance_by_color "$comp" "$color" | grep -q . && die "kampodine-$color already RUNNING"
405
507
  # base: the OTHER color's AD + subnet (same fault domain layout), golden image
406
508
  other=green; [[ "$color" == green ]] && other=blue
407
509
  orow="$(instance_by_color "$comp" "$other")"
408
- [[ -n "$orow" ]] || die "esellar-$other not RUNNING — need its AD/subnet as the pair template"
510
+ [[ -n "$orow" ]] || die "kampodine-$other not RUNNING — need its AD/subnet as the pair template"
409
511
  # instance_by_color rows are "<ocid> <ephemeral_public_ip> <ad>" — field 2
410
512
  # is the IP, the AD is field 3 (launching with the IP as AD fails).
411
513
  read -r oiid _ oad <<<"$orow"
412
514
  subnet="$(oci compute vnic-attachment list -c "$comp" --profile "$PROFILE" \
413
515
  --instance-id "$oiid" --query 'data[0]."subnet-id"' --raw-output)"
414
516
 
415
- # Route selection. NATIVE only with a UEFI_64 esellar-alpine* custom image:
517
+ # Route selection. NATIVE only with a UEFI_64 kampodine-alpine* custom image:
416
518
  # OCI pins IMPORTED images to firmware=BIOS and A1 is UEFI-only, so a BIOS
417
519
  # verdict means the import would die at launch (Shape ... is not valid for
418
520
  # image) — skip it. Otherwise take the template's LIVE image-id from its
419
521
  # instance record and inject the golden disk (provision-via-migrate).
420
522
  mode="" image=""
421
523
  custom="$(oci compute image list -c "$comp" --profile "$PROFILE" --all --sort-by TIMECREATED \
422
- --query "data[?\"display-name\" != null && starts_with(\"display-name\", 'esellar-alpine')] | [0].id" \
524
+ --query "data[?\"display-name\" != null && starts_with(\"display-name\", 'kampodine-alpine')] | [0].id" \
423
525
  --raw-output 2>/dev/null || true)"
424
526
  if [[ "$custom" == ocid1.image* ]]; then
425
527
  fw="$(oci compute image get --image-id "$custom" --profile "$PROFILE" \
@@ -427,7 +529,7 @@ case "$cmd" in
427
529
  if [[ "$fw" == "UEFI_64" ]]; then
428
530
  image="$custom" mode="native"
429
531
  else
430
- say "note: newest esellar-alpine* custom image is firmware=${fw:-unknown} — A1 rejects BIOS-pinned imports, skipping to the platform-image + injection route"
532
+ say "note: newest kampodine-alpine* custom image is firmware=${fw:-unknown} — A1 rejects BIOS-pinned imports, skipping to the platform-image + injection route"
431
533
  fi
432
534
  fi
433
535
  if [[ -z "$image" ]]; then
@@ -435,7 +537,7 @@ case "$cmd" in
435
537
  --query 'data."image-id"' --raw-output 2>/dev/null || true)"
436
538
  [[ "$image" == ocid1.image* ]] || die "template instance has no resolvable image-id — cannot launch or inject"
437
539
  mode="inject"
438
- qcow2="${ALPINE_QCOW2:-${REPO_ROOT}/infra/alpine-host/build/esellar-alpine-3.22.6-aarch64.qcow2}"
540
+ qcow2="${ALPINE_QCOW2:-${REPO_ROOT}/infra/alpine-host/build/kampodine-alpine-3.22.6-aarch64.qcow2}"
439
541
  [[ -f "$qcow2" ]] || die "golden qcow2 not found: $qcow2 (build via packer, or set ALPINE_QCOW2)"
440
542
  command -v qemu-img >/dev/null 2>&1 || die "qemu-img not found in PATH (brew install qemu) — required for the qcow2 -> raw conversion"
441
543
  # ops ssh public key for the platform-image first boot. OPS_SSH_PUBKEY
@@ -448,22 +550,22 @@ case "$cmd" in
448
550
  keyfile="$OPS_SSH_PUBKEY"
449
551
  else
450
552
  agent_keys="$(ssh-add -L 2>/dev/null | grep -v '\.pub$' || true)"
451
- [[ -n "$agent_keys" ]] || die "no ssh key available: OPS_SSH_PUBKEY unset and ssh-add lists no keys (ssh-add ~/.ssh/id_ed25519-esellar, or set OPS_SSH_PUBKEY)"
452
- keyfile="$(mktemp "${TMPDIR:-/tmp}/esellar-ops-pubkey.XXXXXX")"
553
+ [[ -n "$agent_keys" ]] || die "no ssh key available: OPS_SSH_PUBKEY unset and ssh-add lists no keys (ssh-add ~/.ssh/id_ed25519-kampodine, or set OPS_SSH_PUBKEY)"
554
+ keyfile="$(mktemp "${TMPDIR:-/tmp}/kampodine-ops-pubkey.XXXXXX")"
453
555
  printf '%s\n' "$agent_keys" > "$keyfile"
454
556
  chmod 600 "$keyfile"
455
557
  fi
456
558
  fi
457
559
 
458
- say "launching esellar-$color: image=${image:0:60}… ad=$oad subnet=${subnet:0:60}… mode=$mode"
560
+ say "launching kampodine-$color: image=${image:0:60}… ad=$oad subnet=${subnet:0:60}… mode=$mode"
459
561
  # NOTE: --profile stays ON the launch line (script-gates static scan reads
460
562
  # the invocation line, not array contents).
461
563
  launch_args=(-c "$comp" --availability-domain "$oad" --subnet-id "$subnet"
462
564
  --image-id "$image" --shape VM.Standard.A1.Flex --shape-config '{"ocpus":2,"memoryInGBs":12}'
463
- --assign-public-ip true --display-name "esellar-$color")
565
+ --assign-public-ip true --display-name "kampodine-$color")
464
566
  [[ "$mode" == "inject" ]] && launch_args+=(--ssh-authorized-keys-file "$keyfile")
465
567
  iid="$(oci compute instance launch "${launch_args[@]}" --profile "$PROFILE" --query 'data.id' --raw-output)"
466
- say "LAUNCHED esellar-$color: $iid"
568
+ say "LAUNCHED kampodine-$color: $iid"
467
569
  if [[ "$mode" == "native" ]]; then
468
570
  say "next: wait RUNNING -> ssh in -> kampodine vm-prepare -> kampodine deploy --host root@<ephemeral-ip>"
469
571
  say "then 'bluegreen.sh flip --to $color' (health-gated) once its app checks green."
@@ -493,16 +595,16 @@ case "$cmd" in
493
595
  read -r rocid raddr _ <<<"$rp"
494
596
  rhost="${raddr//./-}.sslip.io"
495
597
  trow="$(instance_by_color "$comp" "$to")"
496
- [[ -n "$trow" ]] || die "esellar-$to is not RUNNING — nothing to flip to"
598
+ [[ -n "$trow" ]] || die "kampodine-$to is not RUNNING — nothing to flip to"
497
599
  read -r tiid tpub _ <<<"$trow"
498
600
  tvnic="$(primary_vnic_of "$tiid")"
499
- [[ -n "$tvnic" ]] || die "no primary VNIC on esellar-$to"
601
+ [[ -n "$tvnic" ]] || die "no primary VNIC on kampodine-$to"
500
602
  tpip="$(reserved_anchor_ip "$tvnic")"
501
- [[ "$tpip" == ocid1.privateip* ]] || die "could not resolve/create the reserved-anchor secondary private ip on esellar-$to"
603
+ [[ "$tpip" == ocid1.privateip* ]] || die "could not resolve/create the reserved-anchor secondary private ip on kampodine-$to"
502
604
  if instance_healthy "$tpub"; then
503
- say "target health: esellar-$to app HEALTHY on its own IP"
605
+ say "target health: kampodine-$to app HEALTHY on its own IP"
504
606
  else
505
- (( force )) || die "esellar-$to app UNHEALTHY — refusing flip (override: --force)"
607
+ (( force )) || die "kampodine-$to app UNHEALTHY — refusing flip (override: --force)"
506
608
  say "target health: UNHEALTHY — flipping anyway (--force)"
507
609
  fi
508
610
 
@@ -510,19 +612,19 @@ case "$cmd" in
510
612
  # once the hostname resolves to the reserved ip AND routes to the target,
511
613
  # so the guest anchor address goes FIRST, then the assign, then the cert.
512
614
  ob=green; [[ "$to" == green ]] && ob=blue
513
- say "flip[1/4]: anchor conf -> esellar-$to guest ($tpub), watcher configures the address"
615
+ say "flip[1/4]: anchor conf -> kampodine-$to guest ($tpub), watcher configures the address"
514
616
  tsubnet="$(oci compute vnic-attachment list -c "$comp" --profile "$PROFILE" \
515
617
  --instance-id "$tiid" --query 'data[0]."subnet-id"' --raw-output 2>/dev/null || true)"
516
- [[ "$tsubnet" == ocid1.subnet* ]] || die "could not resolve esellar-$to's subnet id"
618
+ [[ "$tsubnet" == ocid1.subnet* ]] || die "could not resolve kampodine-$to's subnet id"
517
619
  tcidr="$(oci network subnet get --subnet-id "$tsubnet" --profile "$PROFILE" \
518
620
  --query 'data."cidr-block"' --raw-output 2>/dev/null || true)"
519
- [[ "$tcidr" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/[0-9]+$ ]] || die "could not resolve esellar-$to's subnet cidr (got: ${tcidr:-none})"
621
+ [[ "$tcidr" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/[0-9]+$ ]] || die "could not resolve kampodine-$to's subnet cidr (got: ${tcidr:-none})"
520
622
  tanchor_addr="$(oci network private-ip get --private-ip-id "$tpip" --profile "$PROFILE" \
521
623
  --query 'data."ip-address"' --raw-output 2>/dev/null || true)"
522
- [[ "$tanchor_addr" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]] || die "could not resolve the anchor private address on esellar-$to"
624
+ [[ "$tanchor_addr" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]] || die "could not resolve the anchor private address on kampodine-$to"
523
625
  taddr_cidr="${tanchor_addr}/${tcidr##*/}"
524
626
  if ! write_anchor_conf "$tpub" "$taddr_cidr" >/dev/null 2>&1; then
525
- die "anchor.conf write failed on esellar-$to ($tpub) — nothing mutated, flip aborted"
627
+ die "anchor.conf write failed on kampodine-$to ($tpub) — nothing mutated, flip aborted"
526
628
  fi
527
629
  addr_ok=0
528
630
  for ((i = 1; i <= FLIP_ADDR_TRIES; i++)); do
@@ -531,15 +633,15 @@ case "$cmd" in
531
633
  done
532
634
  if (( ! addr_ok )); then
533
635
  cleanup_target_anchor "$tpub" "$taddr_cidr"
534
- die "guest watcher never configured $taddr_cidr on esellar-$to — is rc-service esellar-anchor running? (conf removed, NOTHING mutated)"
636
+ die "guest watcher never configured $taddr_cidr on kampodine-$to — is rc-service kampodine-anchor running? (conf removed, NOTHING mutated)"
535
637
  fi
536
- say "flip[2/4]: guest answers on $taddr_cidr — reserved $raddr -> esellar-$to (anchor $tpip)"
638
+ say "flip[2/4]: guest answers on $taddr_cidr — reserved $raddr -> kampodine-$to (anchor $tpip)"
537
639
  oci network public-ip update --public-ip-id "$rocid" --profile "$PROFILE" \
538
640
  --private-ip-id "$tpip" --force --wait-for-state ASSIGNED >/dev/null \
539
641
  || { cleanup_target_anchor "$tpub" "$taddr_cidr"; die "OCI flip call failed — guest conf removed; run 'kampodine bluegreen status'"; }
540
- say "flip[3/4]: reserved IP ASSIGNED — registering $rhost on esellar-$to's kamal-proxy (ACME HTTP-01 through the reserved ip)…"
642
+ say "flip[3/4]: reserved IP ASSIGNED — registering $rhost on kampodine-$to's kamal-proxy (ACME HTTP-01 through the reserved ip)…"
541
643
  if ! ssh "${SSH_OPTS[@]}" "root@$tpub" \
542
- "podman exec kamal-proxy kamal-proxy deploy esellar-api --host=$rhost --target=esellar-api:8080 --tls --health-check-path=/api/auth/ok"; then
644
+ "podman exec kamal-proxy kamal-proxy deploy kampodine-api --host=$rhost --target=kampodine-api:8080 --tls --health-check-path=/api/auth/ok"; then
543
645
  flip_failure_rollback "$to" "$ob" "$comp" "$rocid" "$tpub" "$taddr_cidr"
544
646
  exit 1
545
647
  fi
@@ -555,14 +657,14 @@ case "$cmd" in
555
657
  fi
556
658
  served="$(curl -s -m 8 --resolve "$rhost:443:$raddr" "https://$rhost/api/auth/ok" || true)"
557
659
  say "flip[4/4]: cert for $rhost VALID + serving through $raddr"
558
- say "FLIPPED: https://$raddr/ (https://$rhost/) now serves from esellar-$to"
660
+ say "FLIPPED: https://$raddr/ (https://$rhost/) now serves from kampodine-$to"
559
661
  say "served /api/auth/ok: ${served:-<no body>}"
560
662
  ;;
561
663
 
562
664
  rollback)
563
665
  # DORMANT rollback: holder guest cleanup
564
666
  # (anchor.conf removal + anchor address delete — the flip tool's explicit
565
- # job; the esellar-anchor watcher is add-only) then the OCI unassign
667
+ # job; the kampodine-anchor watcher is add-only) then the OCI unassign
566
668
  # (documented CLI semantics: an empty --private-ip-id unassigns). For a
567
669
  # real pair where traffic must land on the other color, use
568
670
  # 'flip --to <other>' — it runs the full ACME-first sequence there.
@@ -591,10 +693,10 @@ case "$cmd" in
591
693
  if [[ -z "$holder_color" ]]; then
592
694
  say "warning: reserved $raddr held by an anchor of neither RUNNING color (terminated instance?) — unassigning without guest cleanup"
593
695
  else
594
- say "current holder: esellar-$holder_color ($holder_ip) — cleaning the guest anchor, then unassigning"
696
+ say "current holder: kampodine-$holder_color ($holder_ip) — cleaning the guest anchor, then unassigning"
595
697
  conf_addr="$(read_anchor_conf_addr "$holder_ip")"
596
698
  cleanup_target_anchor "$holder_ip" "$conf_addr"
597
- say "esellar-$holder_color guest cleaned (anchor.conf removed${conf_addr:+, address $conf_addr deleted})"
699
+ say "kampodine-$holder_color guest cleaned (anchor.conf removed${conf_addr:+, address $conf_addr deleted})"
598
700
  fi
599
701
  say "unassigning reserved $raddr (-> dormant)…"
600
702
  oci network public-ip update --public-ip-id "$rocid" --profile "$PROFILE" \