kampodine 0.2.2 → 0.3.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 +26 -0
- package/README.md +24 -3
- package/cli.js +18 -4
- package/package.json +1 -1
- package/scripts/bluegreen.sh +101 -0
- package/scripts/deploy.sh +16 -1
- package/scripts/dns.sh +264 -0
- package/scripts/env.sh +183 -0
- package/scripts/image-import.sh +11 -1
- package/scripts/migrate.sh +18 -2
- package/scripts/status.sh +23 -0
- package/scripts/vm-prepare.sh +15 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0 — 2026-10-07
|
|
4
|
+
|
|
5
|
+
Full-lifecycle orchestration with first-class help — the "mini vercel CLI"
|
|
6
|
+
release.
|
|
7
|
+
|
|
8
|
+
- `env` — manage the remote app env file (`/etc/esellar/env`, 0600 root):
|
|
9
|
+
`list` (KEY + value fingerprints only — length + first 2 chars, values
|
|
10
|
+
NEVER printed), `push --file` (0600 temp from creation via umask + atomic
|
|
11
|
+
`mv` + restart hint), `pull` (raw payload to stdout or `--out` 0600, masked
|
|
12
|
+
summary), `fingerprint` (local masking preview). Host/key resolution
|
|
13
|
+
identical to deploy (`--host` | `ESPELLAR_HOST`, `--ssh-key` |
|
|
14
|
+
`KAMPODINE_SSH_KEY` | ssh-agent)
|
|
15
|
+
- `dns` — OCI DNS records: `records`, `add`, `rm`. Shells out to the `oci`
|
|
16
|
+
CLI; auth is the OCI config file (`--profile`) or `--instance-principal`
|
|
17
|
+
ONLY — no credential material is ever accepted, stored, or logged. Types
|
|
18
|
+
pinned to A|AAAA|CNAME; name/type/value/ttl validated locally before any
|
|
19
|
+
provider call; `add` merges into the existing RRSet, `rm` filters it
|
|
20
|
+
- Help everywhere: `kampodine --help` prints a grouped command index
|
|
21
|
+
(DEPLOY / INFRA / DNS / ENV, vercel-style); every command and every
|
|
22
|
+
bluegreen/env/dns sub-step answers `--help`/`-h` with `Usage:` +
|
|
23
|
+
`Examples:` — pinned by a test that enumerates all of them
|
|
24
|
+
- Tests grew from 38 to 114: help coverage for every subcommand, env
|
|
25
|
+
fingerprint masking against fixture ssh shims (a value in output fails the
|
|
26
|
+
suite), dns arg validation, top-level index groups, oci `--profile` gate
|
|
27
|
+
extended to `dns` calls
|
|
28
|
+
|
|
3
29
|
## 0.2.1 — 2026-10-07
|
|
4
30
|
|
|
5
31
|
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/esellar/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>` | `ESPELLAR_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),
|
|
139
|
-
step uses (kampodine is agnostic;
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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/esellar/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
package/scripts/bluegreen.sh
CHANGED
|
@@ -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 esellar-api), OCI_COMPARTMENT (default esellar).
|
|
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" \
|
|
@@ -357,6 +446,18 @@ inject_alpine() {
|
|
|
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
|
|
360
461
|
case "$cmd" in
|
|
361
462
|
status)
|
|
362
463
|
comp="$(compartment_ocid)"
|
package/scripts/deploy.sh
CHANGED
|
@@ -55,7 +55,22 @@ DOCKERFILE="${KAMPODINE_DOCKERFILE:-apps/api/Containerfile}"
|
|
|
55
55
|
|
|
56
56
|
say() { printf '\033[1;34m[deploy]\033[0m %s\n' "$*"; }
|
|
57
57
|
die() { printf '\033[1;31m[deploy] FAIL:\033[0m %s\n' "$*" >&2; exit 1; }
|
|
58
|
-
usage() {
|
|
58
|
+
usage() {
|
|
59
|
+
cat <<'EOF'
|
|
60
|
+
Usage:
|
|
61
|
+
kampodine deploy [--host root@<ip>] [--version <sha7>] [--rollback [<sha7>]]
|
|
62
|
+
[--dockerfile <path>] [--ssh-key <path>] [--skip-smoke] [--refresh-config]
|
|
63
|
+
|
|
64
|
+
Examples:
|
|
65
|
+
EOF
|
|
66
|
+
grep '^# kampodine deploy' "$0" | sed 's/^# //'
|
|
67
|
+
cat <<'EOF'
|
|
68
|
+
|
|
69
|
+
Host/key resolution: --host | ESPELLAR_HOST; --ssh-key | KAMPODINE_SSH_KEY |
|
|
70
|
+
ESPELLAR_SSH_KEY | ssh-agent / ~/.ssh/config.
|
|
71
|
+
EOF
|
|
72
|
+
exit 0
|
|
73
|
+
}
|
|
59
74
|
|
|
60
75
|
while [[ $# -gt 0 ]]; do
|
|
61
76
|
case "$1" in
|
package/scripts/dns.sh
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# dns.sh — OCI DNS record management: the single surface for the coming DNS
|
|
3
|
+
# era (today the stack is raw IP + sslip.io; when real zones land, records
|
|
4
|
+
# are managed here, never by hand in the console).
|
|
5
|
+
#
|
|
6
|
+
# AUTH RULE (hard): the `oci` CLI's own auth ONLY — its config file
|
|
7
|
+
# (~/.oci/config, selected via --profile) or instance principal. kampodine
|
|
8
|
+
# NEVER accepts, stores, or logs credential material: no key flags, no
|
|
9
|
+
# credential env vars, nothing token-shaped in args or output. The OCI config
|
|
10
|
+
# file stays the single credential store.
|
|
11
|
+
#
|
|
12
|
+
# Types are pinned to A | AAAA | CNAME. `add` merges into the existing RRSet
|
|
13
|
+
# (round-robin records survive); `rm` filters it; both are idempotence-aware.
|
|
14
|
+
#
|
|
15
|
+
# Zone/compartment: OCI_PROFILE (default esellar-api), OCI_COMPARTMENT
|
|
16
|
+
# (default esellar). Zone defaults to the compartment's ONLY zone; pass
|
|
17
|
+
# --zone <id-or-name> when several exist.
|
|
18
|
+
#
|
|
19
|
+
# Usage:
|
|
20
|
+
# kampodine dns records [--zone <id-or-name>] # list records: domain / type / ttl / value
|
|
21
|
+
# kampodine dns add --name <label> --type A|AAAA|CNAME --value <target> [--ttl 300]
|
|
22
|
+
# kampodine dns rm --name <label> --type <A|AAAA|CNAME> --value <target>
|
|
23
|
+
# (all: optional --zone <id-or-name>; --instance-principal for instance auth)
|
|
24
|
+
set -euo pipefail
|
|
25
|
+
|
|
26
|
+
PROFILE="${OCI_PROFILE:-esellar-api}"
|
|
27
|
+
COMPARTMENT_NAME="${OCI_COMPARTMENT:-esellar}"
|
|
28
|
+
AUTH_ARGS=()
|
|
29
|
+
|
|
30
|
+
say() { printf '\033[1;34m[dns]\033[0m %s\n' "$*"; }
|
|
31
|
+
die() { printf '\033[1;31m[dns] FAIL:\033[0m %s\n' "$*" >&2; exit 1; }
|
|
32
|
+
usage() {
|
|
33
|
+
local code="${1:-0}"
|
|
34
|
+
printf 'Usage:\n'
|
|
35
|
+
grep '^# kampodine dns' "$0" | sed 's/^# //'
|
|
36
|
+
cat <<'EOF'
|
|
37
|
+
|
|
38
|
+
Auth is the oci CLI's own — its config file (--profile) or instance
|
|
39
|
+
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.
|
|
42
|
+
|
|
43
|
+
Examples:
|
|
44
|
+
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
|
|
47
|
+
kampodine dns add --name www --type CNAME --value app.example.com --ttl 3600
|
|
48
|
+
kampodine dns rm --name app --type A --value 203.0.113.10
|
|
49
|
+
EOF
|
|
50
|
+
exit "$code"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# --- pure validators (no side effects, run before any provider call) ----------
|
|
54
|
+
is_ipv4() {
|
|
55
|
+
local ip="$1" i octet
|
|
56
|
+
[[ "$ip" =~ ^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$ ]] || return 1
|
|
57
|
+
for i in 1 2 3 4; do
|
|
58
|
+
octet="${BASH_REMATCH[i]}"
|
|
59
|
+
[[ $((10#$octet)) -le 255 ]] || return 1
|
|
60
|
+
done
|
|
61
|
+
return 0
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
is_ipv6() {
|
|
65
|
+
local ip="$1" colons
|
|
66
|
+
[[ "$ip" =~ ^[0-9A-Fa-f:]+$ ]] || return 1
|
|
67
|
+
[[ -n "${ip//:/}" ]] || return 1 # at least one hex digit
|
|
68
|
+
[[ "$ip" != *":::"* ]] || return 1 # no triple colons
|
|
69
|
+
colons="${ip//[0-9A-Fa-f]/}"
|
|
70
|
+
[[ ${#colons} -le 7 ]] || return 1 # at most 8 groups
|
|
71
|
+
[[ "${ip/::/}" != *"::"* ]] || return 1 # at most one "::"
|
|
72
|
+
return 0
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
is_hostname() {
|
|
76
|
+
[[ "$1" =~ ^([A-Za-z0-9_]([A-Za-z0-9_-]{0,61}[A-Za-z0-9_])?\.)+[A-Za-z0-9_]([A-Za-z0-9_-]{0,61}[A-Za-z0-9_])?\.?$ ]]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# --- dispatch -----------------------------------------------------------------
|
|
80
|
+
CMD="${1:-}"
|
|
81
|
+
case "$CMD" in
|
|
82
|
+
records|add|rm) shift ;;
|
|
83
|
+
-h|--help|help) usage 0 ;;
|
|
84
|
+
"") usage 2 ;;
|
|
85
|
+
*)
|
|
86
|
+
printf 'kampodine dns: unknown subcommand: %s\n\n' "$CMD" >&2
|
|
87
|
+
usage 2
|
|
88
|
+
;;
|
|
89
|
+
esac
|
|
90
|
+
|
|
91
|
+
ZONE_ARG=""
|
|
92
|
+
NAME=""
|
|
93
|
+
RTYPE=""
|
|
94
|
+
VALUE=""
|
|
95
|
+
TTL="300"
|
|
96
|
+
while [[ $# -gt 0 ]]; do
|
|
97
|
+
case "$1" in
|
|
98
|
+
--zone) ZONE_ARG="$2"; shift 2 ;;
|
|
99
|
+
--name) NAME="$2"; shift 2 ;;
|
|
100
|
+
--type) RTYPE="$2"; shift 2 ;;
|
|
101
|
+
--value) VALUE="$2"; shift 2 ;;
|
|
102
|
+
--ttl) TTL="$2"; shift 2 ;;
|
|
103
|
+
--instance-principal) AUTH_ARGS=(--auth instance_principal); shift ;;
|
|
104
|
+
-h|--help) usage 0 ;;
|
|
105
|
+
*) die "unknown argument: $1 (--help)" ;;
|
|
106
|
+
esac
|
|
107
|
+
done
|
|
108
|
+
|
|
109
|
+
need_provider() {
|
|
110
|
+
command -v oci >/dev/null 2>&1 \
|
|
111
|
+
|| 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
|
+
command -v jq >/dev/null 2>&1 \
|
|
113
|
+
|| die "jq not found — brew install jq (dns.sh uses it for RRSet surgery)"
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
compartment_ocid() {
|
|
117
|
+
local ocid
|
|
118
|
+
ocid="$(oci iam compartment list --all --profile "$PROFILE" ${AUTH_ARGS[@]+"${AUTH_ARGS[@]}"} \
|
|
119
|
+
--query "data[?name=='$COMPARTMENT_NAME'].id | [0]" --raw-output 2>/dev/null || true)"
|
|
120
|
+
[[ "$ocid" == ocid1.compartment* ]] || die "compartment '$COMPARTMENT_NAME' not found (profile $PROFILE)"
|
|
121
|
+
printf '%s' "$ocid"
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# resolve_zone -> ZONE_NAME + ZONE_ID (explicit --zone wins; otherwise the
|
|
125
|
+
# compartment must hold exactly one zone — never guess between several).
|
|
126
|
+
resolve_zone() {
|
|
127
|
+
local zj zl count
|
|
128
|
+
COMPARTMENT_OCID="$(compartment_ocid)"
|
|
129
|
+
if [[ -n "$ZONE_ARG" ]]; then
|
|
130
|
+
if [[ "$ZONE_ARG" == ocid1.dns-zone* ]]; then
|
|
131
|
+
zj="$(oci dns zone get --zone-name-or-id "$ZONE_ARG" -c "$COMPARTMENT_OCID" --profile "$PROFILE" ${AUTH_ARGS[@]+"${AUTH_ARGS[@]}"} 2>/dev/null)" \
|
|
132
|
+
|| die "zone not found: $ZONE_ARG"
|
|
133
|
+
ZONE_NAME="$(jq -r '.data.name' <<<"$zj")"
|
|
134
|
+
ZONE_ID="$ZONE_ARG"
|
|
135
|
+
else
|
|
136
|
+
ZONE_NAME="${ZONE_ARG%.}"
|
|
137
|
+
ZONE_ID="$ZONE_ARG"
|
|
138
|
+
fi
|
|
139
|
+
return 0
|
|
140
|
+
fi
|
|
141
|
+
zl="$(oci dns zone list --all -c "$COMPARTMENT_OCID" --profile "$PROFILE" ${AUTH_ARGS[@]+"${AUTH_ARGS[@]}"} 2>/dev/null)" \
|
|
142
|
+
|| die "zone list failed (profile $PROFILE, compartment $COMPARTMENT_NAME)"
|
|
143
|
+
count="$(jq '.data.items | length' <<<"$zl")"
|
|
144
|
+
if [[ "$count" -eq 0 ]]; then
|
|
145
|
+
die "no DNS zones in compartment '$COMPARTMENT_NAME' — create one in the OCI console first (or pass --zone <id-or-name>)"
|
|
146
|
+
fi
|
|
147
|
+
if [[ "$count" -gt 1 ]]; then
|
|
148
|
+
printf '\033[1;31m[dns] FAIL:\033[0m multiple zones in %s — pass --zone with one of:\n' "$COMPARTMENT_NAME" >&2
|
|
149
|
+
jq -r '.data.items[].name' <<<"$zl" | sed 's/^/ /' >&2
|
|
150
|
+
exit 1
|
|
151
|
+
fi
|
|
152
|
+
ZONE_NAME="$(jq -r '.data.items[0].name' <<<"$zl")"
|
|
153
|
+
ZONE_ID="$(jq -r '.data.items[0].id' <<<"$zl")"
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
# turn --name into the record domain: plain label -> label.zone; fqdn must
|
|
157
|
+
# live inside the zone (never write into someone else's zone by accident).
|
|
158
|
+
validate_name_shape() {
|
|
159
|
+
[[ -n "$NAME" ]] || die "--name is required (--help)"
|
|
160
|
+
[[ "$NAME" =~ ^[A-Za-z0-9_-]{1,63}$ ]] && return 0
|
|
161
|
+
is_hostname "$NAME" || die "invalid --name '$NAME' — use a DNS label (app) or an fqdn inside the zone (app.example.com)"
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
resolve_domain() {
|
|
165
|
+
if [[ "$NAME" =~ ^[A-Za-z0-9_-]{1,63}$ ]]; then
|
|
166
|
+
DOMAIN="$NAME.$ZONE_NAME"
|
|
167
|
+
return 0
|
|
168
|
+
fi
|
|
169
|
+
DOMAIN="${NAME%.}"
|
|
170
|
+
[[ "$DOMAIN" == *".$ZONE_NAME" || "$DOMAIN" == "$ZONE_NAME" ]] \
|
|
171
|
+
|| die "--name '$NAME' is outside zone '$ZONE_NAME' — pass --zone <that-zone> explicitly if intended"
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
validate_type() {
|
|
175
|
+
case "$RTYPE" in
|
|
176
|
+
A|AAAA|CNAME) return 0 ;;
|
|
177
|
+
"") die "--type is required: A, AAAA, or CNAME (--help)" ;;
|
|
178
|
+
*) die "--type must be one of: A, AAAA, CNAME (got: $RTYPE)" ;;
|
|
179
|
+
esac
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
validate_value() {
|
|
183
|
+
[[ -n "$VALUE" ]] || die "--value is required (--help)"
|
|
184
|
+
case "$VALUE" in *[\"\\]*) die "invalid --value — quotes/backslashes are never valid in a record value" ;; esac
|
|
185
|
+
case "$RTYPE" in
|
|
186
|
+
A) is_ipv4 "$VALUE" || die "invalid --value '$VALUE' for A — need dotted-quad IPv4 (203.0.113.10)" ;;
|
|
187
|
+
AAAA) is_ipv6 "$VALUE" || die "invalid --value '$VALUE' for AAAA — need an IPv6 literal (fd00::1)" ;;
|
|
188
|
+
CNAME) is_hostname "$VALUE" || die "invalid --value '$VALUE' for CNAME — need a hostname (app.example.com)" ;;
|
|
189
|
+
esac
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
validate_ttl() {
|
|
193
|
+
[[ "$TTL" =~ ^[0-9]+$ ]] || die "invalid --ttl '$TTL' — seconds (60..172800)"
|
|
194
|
+
[[ "$TTL" -ge 60 && "$TTL" -le 172800 ]] || die "invalid --ttl $TTL — OCI allows 60..172800 seconds"
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
# fetch_rrset -> ITEMS (canonical JSON array of {domain,rtype,ttl,rdata};
|
|
198
|
+
# a missing RRSet reads as empty — first add must not 404).
|
|
199
|
+
fetch_rrset() {
|
|
200
|
+
local rj
|
|
201
|
+
rj="$(oci dns record rrset get -c "$COMPARTMENT_OCID" --zone-name-or-id "$ZONE_ID" --domain "$DOMAIN" --rtype "$RTYPE" --profile "$PROFILE" ${AUTH_ARGS[@]+"${AUTH_ARGS[@]}"} 2>/dev/null || true)"
|
|
202
|
+
if [[ -n "$rj" ]]; then
|
|
203
|
+
ITEMS="$(jq -c '.data.items | map({domain, rtype, ttl, rdata})' <<<"$rj")" || ITEMS="[]"
|
|
204
|
+
else
|
|
205
|
+
ITEMS="[]"
|
|
206
|
+
fi
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
case "$CMD" in
|
|
210
|
+
records)
|
|
211
|
+
need_provider
|
|
212
|
+
resolve_zone
|
|
213
|
+
say "DNS records — zone $ZONE_NAME ($ZONE_ID), profile $PROFILE:"
|
|
214
|
+
oci dns record zone get -c "$COMPARTMENT_OCID" --zone-name-or-id "$ZONE_ID" --profile "$PROFILE" ${AUTH_ARGS[@]+"${AUTH_ARGS[@]}"} --all \
|
|
215
|
+
| jq -r '.data.items[] | [.domain, .rtype, (.ttl|tostring), .rdata] | @tsv'
|
|
216
|
+
;;
|
|
217
|
+
|
|
218
|
+
add)
|
|
219
|
+
[[ -n "$NAME" ]] || die "--name is required (--help)"
|
|
220
|
+
validate_type
|
|
221
|
+
validate_value
|
|
222
|
+
validate_ttl
|
|
223
|
+
validate_name_shape
|
|
224
|
+
need_provider
|
|
225
|
+
resolve_zone
|
|
226
|
+
resolve_domain
|
|
227
|
+
fetch_rrset
|
|
228
|
+
norm_value="${VALUE%.}"
|
|
229
|
+
present="$(jq -r --arg v "$norm_value" 'any(.[]; (.rdata | sub("\\.$"; "")) == $v)' <<<"$ITEMS")"
|
|
230
|
+
if [[ "$present" == "true" ]]; then
|
|
231
|
+
say "already present: $DOMAIN $RTYPE $VALUE — nothing to do"
|
|
232
|
+
exit 0
|
|
233
|
+
fi
|
|
234
|
+
new_items="$(jq -c --arg d "$DOMAIN" --arg t "$RTYPE" --arg v "$VALUE" --argjson ttl "$TTL" \
|
|
235
|
+
'. + [{domain: $d, rtype: $t, ttl: $ttl, rdata: $v}]' <<<"$ITEMS")"
|
|
236
|
+
# NOTE: --profile stays ON the first line (script-gates static scan reads it there)
|
|
237
|
+
oci dns record rrset update --profile "$PROFILE" -c "$COMPARTMENT_OCID" --zone-name-or-id "$ZONE_ID" \
|
|
238
|
+
--domain "$DOMAIN" --rtype "$RTYPE" --items "$new_items" --force ${AUTH_ARGS[@]+"${AUTH_ARGS[@]}"} >/dev/null \
|
|
239
|
+
|| die "RRSet update failed for $DOMAIN $RTYPE (zone $ZONE_NAME)"
|
|
240
|
+
say "added: $DOMAIN $RTYPE $VALUE (ttl $TTL)"
|
|
241
|
+
;;
|
|
242
|
+
|
|
243
|
+
rm)
|
|
244
|
+
[[ -n "$NAME" ]] || die "--name is required (--help)"
|
|
245
|
+
validate_type
|
|
246
|
+
validate_value
|
|
247
|
+
validate_name_shape
|
|
248
|
+
need_provider
|
|
249
|
+
resolve_zone
|
|
250
|
+
resolve_domain
|
|
251
|
+
fetch_rrset
|
|
252
|
+
[[ "$ITEMS" == "[]" ]] && die "no matching record: $DOMAIN $RTYPE $VALUE (see: kampodine dns records)"
|
|
253
|
+
norm_value="${VALUE%.}"
|
|
254
|
+
remaining="$(jq -c --arg v "$norm_value" '[.[] | select((.rdata | sub("\\.$"; "")) != $v)]' <<<"$ITEMS")"
|
|
255
|
+
[[ "$(jq 'length' <<<"$remaining")" -eq "$(jq 'length' <<<"$ITEMS")" ]] \
|
|
256
|
+
&& die "no matching record: $DOMAIN $RTYPE $VALUE (see: kampodine dns records)"
|
|
257
|
+
# an emptied RRSet is legal: --items '[]' removes it entirely
|
|
258
|
+
# NOTE: --profile stays ON the first line (script-gates static scan reads it there)
|
|
259
|
+
oci dns record rrset update --profile "$PROFILE" -c "$COMPARTMENT_OCID" --zone-name-or-id "$ZONE_ID" \
|
|
260
|
+
--domain "$DOMAIN" --rtype "$RTYPE" --items "$remaining" --force ${AUTH_ARGS[@]+"${AUTH_ARGS[@]}"} >/dev/null \
|
|
261
|
+
|| die "RRSet update failed for $DOMAIN $RTYPE (zone $ZONE_NAME)"
|
|
262
|
+
say "removed: $DOMAIN $RTYPE $VALUE"
|
|
263
|
+
;;
|
|
264
|
+
esac
|
package/scripts/env.sh
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# env.sh — manage the remote app env file (/etc/esellar/env, 0600 root).
|
|
3
|
+
#
|
|
4
|
+
# SECRETS NEVER PRINT. `env list`, `env push`, and the `env pull` summaries
|
|
5
|
+
# surface FINGERPRINTS ONLY: KEY + value length + first 2 characters. Raw
|
|
6
|
+
# values move exactly twice — push uploads the local file over ssh stdin,
|
|
7
|
+
# pull streams the remote file to stdout (payload) or --out (0600). Nothing
|
|
8
|
+
# secret ever lands in terminal output, logs, or process args.
|
|
9
|
+
#
|
|
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) |
|
|
13
|
+
# ssh-agent (the POSIX default; per-host IdentityFile belongs in ssh config)
|
|
14
|
+
#
|
|
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)
|
|
20
|
+
#
|
|
21
|
+
# push sends the file VERBATIM (what you push is what lands). deploy's env
|
|
22
|
+
# step additionally drops rc-script-owned CLEAR keys — keep those out of the
|
|
23
|
+
# file you push unless you mean to own them here.
|
|
24
|
+
set -euo pipefail
|
|
25
|
+
|
|
26
|
+
ENV_FILE_REMOTE="${KAMPODINE_ENV_REMOTE:-/etc/esellar/env}"
|
|
27
|
+
ENV_TMP_BASE="$(dirname "$ENV_FILE_REMOTE")"
|
|
28
|
+
|
|
29
|
+
ESPELLAR_HOST="${ESPELLAR_HOST:-}"
|
|
30
|
+
# SSH key resolution — same ladder as deploy.sh:
|
|
31
|
+
# 1. --ssh-key flag (explicit, per-invocation)
|
|
32
|
+
# 2. KAMPODINE_SSH_KEY env (project-level: direnv / .envrc / export)
|
|
33
|
+
# 3. ESPELLAR_SSH_KEY env (legacy alias, kept for existing setups)
|
|
34
|
+
# 4. 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
|
+
say() { printf '\033[1;36m[env]\033[0m %s\n' "$*"; }
|
|
42
|
+
die() { printf '\033[1;31m[env] FAIL:\033[0m %s\n' "$*" >&2; exit 1; }
|
|
43
|
+
usage() {
|
|
44
|
+
printf 'Usage:\n'
|
|
45
|
+
grep '^# kampodine env' "$0" | sed 's/^# //'
|
|
46
|
+
cat <<'EOF'
|
|
47
|
+
|
|
48
|
+
Fingerprints NEVER leak values: every line is KEY + value length + first 2 chars.
|
|
49
|
+
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.
|
|
52
|
+
|
|
53
|
+
Examples:
|
|
54
|
+
kampodine env list --host root@203.0.113.10
|
|
55
|
+
kampodine env push --file ./ops/env.production --host root@203.0.113.10
|
|
56
|
+
kampodine env pull --out ./env.snapshot --host root@203.0.113.10 # written 0600
|
|
57
|
+
kampodine env pull --host root@203.0.113.10 | wc -l # raw payload on stdout, summary on stderr
|
|
58
|
+
kampodine env fingerprint --file ./.env.local # preview masking, values never leave stdin
|
|
59
|
+
EOF
|
|
60
|
+
exit 0
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
SUB="${1:-}"
|
|
64
|
+
[[ -n "$SUB" ]] || usage
|
|
65
|
+
case "$SUB" in
|
|
66
|
+
list|push|pull|fingerprint) shift ;;
|
|
67
|
+
-h|--help|help) usage ;;
|
|
68
|
+
*) die "unknown env subcommand: $SUB (--help)" ;;
|
|
69
|
+
esac
|
|
70
|
+
|
|
71
|
+
FILE=""
|
|
72
|
+
OUT=""
|
|
73
|
+
while [[ $# -gt 0 ]]; do
|
|
74
|
+
case "$1" in
|
|
75
|
+
--host) ESPELLAR_HOST="$2"; shift 2 ;;
|
|
76
|
+
--ssh-key) SSH_KEY="$2"; shift 2 ;;
|
|
77
|
+
--file) FILE="$2"; shift 2 ;;
|
|
78
|
+
--out) OUT="$2"; shift 2 ;;
|
|
79
|
+
-h|--help) usage ;;
|
|
80
|
+
*) die "unknown argument: $1 (--help)" ;;
|
|
81
|
+
esac
|
|
82
|
+
done
|
|
83
|
+
|
|
84
|
+
SSH_ARGS=(-o ConnectTimeout=10 -o BatchMode=yes)
|
|
85
|
+
[[ -n "$SSH_KEY" ]] && SSH_ARGS+=(-i "$SSH_KEY")
|
|
86
|
+
# $1 is a composed remote command — client-side expansion is the design.
|
|
87
|
+
# shellcheck disable=SC2029
|
|
88
|
+
vm() { ssh "${SSH_ARGS[@]}" "$ESPELLAR_HOST" "$1"; }
|
|
89
|
+
|
|
90
|
+
# --- macOS ssh-agent quirk (first run from a fresh machine) -------------------
|
|
91
|
+
if [[ "$(uname -s)" == "Darwin" ]]; then
|
|
92
|
+
SSH_AUTH_SOCK="$(launchctl getenv SSH_AUTH_SOCK 2>/dev/null || true)"
|
|
93
|
+
export SSH_AUTH_SOCK
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
require_host() {
|
|
97
|
+
[[ -n "$ESPELLAR_HOST" ]] || die "target required: --host root@<ip> or ESPELLAR_HOST=root@<ip> (resolution matches deploy.sh)"
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# env_fp_lines — env content on stdin -> fingerprint table on stdout.
|
|
101
|
+
# THE masking contract lives here: KEY + value length + first 2 chars.
|
|
102
|
+
# $value is never printed; only ${value:0:2} and its length are.
|
|
103
|
+
env_fp_lines() {
|
|
104
|
+
local line key value len
|
|
105
|
+
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
106
|
+
case "$line" in ''|'#'*) continue ;; esac
|
|
107
|
+
[[ "$line" == *=* ]] || continue
|
|
108
|
+
key="${line%%=*}"
|
|
109
|
+
[[ "$key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue
|
|
110
|
+
value="${line#*=}"
|
|
111
|
+
# one pair of matching surrounding quotes is formatting, not content
|
|
112
|
+
# (podman --env-file does not unquote — the deploy pipeline strips them)
|
|
113
|
+
if [[ ${#value} -ge 2 ]]; then
|
|
114
|
+
case "$value" in
|
|
115
|
+
\"*\") value="${value#\"}"; value="${value%\"}" ;;
|
|
116
|
+
\'*\') value="${value#\'}"; value="${value%\'}" ;;
|
|
117
|
+
esac
|
|
118
|
+
fi
|
|
119
|
+
len="${#value}"
|
|
120
|
+
if [[ "$len" -eq 0 ]]; then
|
|
121
|
+
printf '%-32s len=0 (empty)\n' "$key"
|
|
122
|
+
else
|
|
123
|
+
printf '%-32s len=%-7s %s…\n' "$key" "$len" "${value:0:2}"
|
|
124
|
+
fi
|
|
125
|
+
done
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
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)"
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
case "$SUB" in
|
|
133
|
+
list)
|
|
134
|
+
require_host
|
|
135
|
+
raw="$(fetch_remote_env)"
|
|
136
|
+
env_fp_lines <<<"$raw"
|
|
137
|
+
;;
|
|
138
|
+
|
|
139
|
+
push)
|
|
140
|
+
[[ -n "$FILE" ]] || die "push requires --file <local-env-file> (--help)"
|
|
141
|
+
[[ -f "$FILE" ]] || die "no such file: $FILE"
|
|
142
|
+
require_host
|
|
143
|
+
count="$(grep -cE '^[A-Za-z_][A-Za-z0-9_]*=' "$FILE" || true)"
|
|
144
|
+
[[ "${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)"
|
|
146
|
+
env_fp_lines < "$FILE"
|
|
147
|
+
tmp_remote="$ENV_TMP_BASE/env.tmp.$$"
|
|
148
|
+
# 0600 FROM CREATION: umask 077 + ssh stdin pipe — the temp is never
|
|
149
|
+
# world-readable, not even for an instant; `mv` within the same
|
|
150
|
+
# directory is atomic (busybox-safe: umask/cat/mv/chmod only).
|
|
151
|
+
vm "umask 077; cat > $tmp_remote" < "$FILE" || die "upload failed"
|
|
152
|
+
vm "chmod 600 $tmp_remote && mv -f $tmp_remote $ENV_FILE_REMOTE" \
|
|
153
|
+
|| 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"
|
|
156
|
+
;;
|
|
157
|
+
|
|
158
|
+
pull)
|
|
159
|
+
require_host
|
|
160
|
+
raw="$(fetch_remote_env)"
|
|
161
|
+
if [[ -n "$OUT" ]]; then
|
|
162
|
+
( umask 077; printf '%s\n' "$raw" > "$OUT" ) || die "cannot write $OUT"
|
|
163
|
+
chmod 600 "$OUT"
|
|
164
|
+
say "wrote $OUT (0600) — fingerprint summary below (payload went to the file, stdout is free):"
|
|
165
|
+
env_fp_lines <<<"$raw"
|
|
166
|
+
else
|
|
167
|
+
# stdout IS the payload (pipe into whatever needs the values);
|
|
168
|
+
# the human-readable summary goes to stderr, masked.
|
|
169
|
+
printf '%s\n' "$raw"
|
|
170
|
+
say "fingerprint summary for $ENV_FILE_REMOTE on $ESPELLAR_HOST (payload above on stdout):" >&2
|
|
171
|
+
env_fp_lines <<<"$raw" >&2
|
|
172
|
+
fi
|
|
173
|
+
;;
|
|
174
|
+
|
|
175
|
+
fingerprint)
|
|
176
|
+
if [[ -n "$FILE" ]]; then
|
|
177
|
+
[[ -f "$FILE" ]] || die "no such file: $FILE"
|
|
178
|
+
env_fp_lines < "$FILE"
|
|
179
|
+
else
|
|
180
|
+
env_fp_lines
|
|
181
|
+
fi
|
|
182
|
+
;;
|
|
183
|
+
esac
|
package/scripts/image-import.sh
CHANGED
|
@@ -39,7 +39,17 @@ KEEP_OBJECT=0
|
|
|
39
39
|
|
|
40
40
|
say() { printf '[import] %s\n' "$*"; }
|
|
41
41
|
die() { printf '[import][FAIL] %s\n' "$*" >&2; exit 1; }
|
|
42
|
-
usage() {
|
|
42
|
+
usage() {
|
|
43
|
+
cat <<'EOF'
|
|
44
|
+
Usage:
|
|
45
|
+
kampodine image-import [--image <qcow2>] [--bucket <name>] [--name-prefix <p>]
|
|
46
|
+
[--compartment <name>] [--from-pass] [--keep-object]
|
|
47
|
+
|
|
48
|
+
Examples:
|
|
49
|
+
EOF
|
|
50
|
+
grep '^# kampodine image-import' "$0" | sed 's/^# //'
|
|
51
|
+
exit 0
|
|
52
|
+
}
|
|
43
53
|
|
|
44
54
|
while [[ $# -gt 0 ]]; do
|
|
45
55
|
case "$1" in
|
package/scripts/migrate.sh
CHANGED
|
@@ -19,13 +19,29 @@ SERVICE="${SERVICE:-esellar-api}"
|
|
|
19
19
|
|
|
20
20
|
log() { printf '[migrate-all] %s\n' "$*"; }
|
|
21
21
|
fail() { printf '[migrate-all][FAIL] %s\n' "$*" >&2; exit 1; }
|
|
22
|
+
usage() {
|
|
23
|
+
cat <<'EOF'
|
|
24
|
+
Usage:
|
|
25
|
+
kampodine migrate [--allow-running]
|
|
26
|
+
|
|
27
|
+
Examples:
|
|
28
|
+
kampodine migrate # stop the API first (rc-service esellar-api stop)
|
|
29
|
+
kampodine migrate --allow-running # deliberate: migrate while the API serves (SQLITE_BUSY risk)
|
|
30
|
+
|
|
31
|
+
Runs drizzle migrations over the libSQL dbs under TENANT_DIR (default
|
|
32
|
+
/data/tenants): root.db first (auth/org plane), then tenant_*.db sorted,
|
|
33
|
+
bounded-parallel (MIGRATE_JOBS, default 4). Per-file failures are collected;
|
|
34
|
+
exits 1 if any failed.
|
|
35
|
+
EOF
|
|
36
|
+
exit 0
|
|
37
|
+
}
|
|
22
38
|
|
|
23
39
|
ALLOW_RUNNING=0
|
|
24
40
|
for arg in "$@"; do
|
|
25
41
|
case "$arg" in
|
|
26
42
|
--allow-running) ALLOW_RUNNING=1 ;;
|
|
27
|
-
-h|--help)
|
|
28
|
-
*) fail "unknown argument: $arg" ;;
|
|
43
|
+
-h|--help) usage ;;
|
|
44
|
+
*) fail "unknown argument: $arg (--help)" ;;
|
|
29
45
|
esac
|
|
30
46
|
done
|
|
31
47
|
|
package/scripts/status.sh
CHANGED
|
@@ -2,12 +2,35 @@
|
|
|
2
2
|
# status.sh — one command: what is LIVE (through the proxy) + the blue/green
|
|
3
3
|
# pair view (instances, reserved IP, health).
|
|
4
4
|
#
|
|
5
|
+
# Usage:
|
|
6
|
+
# kampodine status
|
|
7
|
+
#
|
|
5
8
|
# Env: APP_HOST_HEADER (default: app.example.com), OCI_PROFILE, OCI_COMPARTMENT.
|
|
6
9
|
set -euo pipefail
|
|
7
10
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
8
11
|
HOST="${APP_HOST_HEADER:-app.example.com}"
|
|
9
12
|
|
|
10
13
|
say() { printf '%s\n' "$*"; }
|
|
14
|
+
die() { printf '[status] FAIL: %s\n' "$*" >&2; exit 1; }
|
|
15
|
+
usage() {
|
|
16
|
+
cat <<'EOF'
|
|
17
|
+
Usage:
|
|
18
|
+
kampodine status
|
|
19
|
+
|
|
20
|
+
Examples:
|
|
21
|
+
kampodine status # live health (proxy host) + the blue/green pair view
|
|
22
|
+
|
|
23
|
+
Env: APP_HOST_HEADER (default app.example.com), OCI_PROFILE, OCI_COMPARTMENT.
|
|
24
|
+
EOF
|
|
25
|
+
exit 0
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
while [[ $# -gt 0 ]]; do
|
|
29
|
+
case "$1" in
|
|
30
|
+
-h|--help) usage ;;
|
|
31
|
+
*) die "unknown argument: $1 (--help)" ;;
|
|
32
|
+
esac
|
|
33
|
+
done
|
|
11
34
|
|
|
12
35
|
say "== live (through the proxy: https://$HOST) =="
|
|
13
36
|
if body="$(curl -sf -m 8 "https://$HOST/api/auth/ok" 2>/dev/null)"; then
|
package/scripts/vm-prepare.sh
CHANGED
|
@@ -45,7 +45,21 @@ SSH_KEY="${ESPELLAR_SSH_KEY:-}"
|
|
|
45
45
|
|
|
46
46
|
say() { printf '\033[1;32m[vm-prepare]\033[0m %s\n' "$*"; }
|
|
47
47
|
die() { printf '\033[1;31m[vm-prepare] FAIL:\033[0m %s\n' "$*" >&2; exit 1; }
|
|
48
|
-
usage() {
|
|
48
|
+
usage() {
|
|
49
|
+
cat <<'EOF'
|
|
50
|
+
Usage:
|
|
51
|
+
kampodine vm-prepare --host root@<new-ip> [--pull-images] [--ssh-key <path>]
|
|
52
|
+
|
|
53
|
+
Examples:
|
|
54
|
+
EOF
|
|
55
|
+
grep '^# kampodine vm-prepare' "$0" | sed 's/^# //'
|
|
56
|
+
cat <<'EOF'
|
|
57
|
+
|
|
58
|
+
Host/key resolution: --host | (no env default — explicit flag);
|
|
59
|
+
--ssh-key | ESPELLAR_SSH_KEY | ssh-agent / ~/.ssh/config.
|
|
60
|
+
EOF
|
|
61
|
+
exit 0
|
|
62
|
+
}
|
|
49
63
|
|
|
50
64
|
while [[ $# -gt 0 ]]; do
|
|
51
65
|
case "$1" in
|