kampodine 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +118 -0
- package/README.md +72 -9
- package/cli.js +21 -1
- package/kampodine.md +13 -3
- package/package.json +1 -1
- package/scripts/backup.sh +290 -0
- package/scripts/bluegreen.sh +21 -1
- package/scripts/common.sh +644 -0
- package/scripts/config.sh +247 -0
- package/scripts/deploy-lifecycle.sh +266 -0
- package/scripts/deploy.sh +136 -18
- package/scripts/dns.sh +17 -0
- package/scripts/env.sh +23 -9
- package/scripts/image-import.sh +9 -0
- package/scripts/metrics.sh +127 -0
- package/scripts/migrate.sh +39 -5
- package/scripts/status.sh +132 -12
- package/scripts/vm-prepare.sh +20 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,123 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.0 — 2026-10-08
|
|
4
|
+
|
|
5
|
+
Three surfaces (per-instance profiles, VM metrics, OCI backups) plus
|
|
6
|
+
host init auto-detection groundwork. Tests grew from 171 to 277.
|
|
7
|
+
|
|
8
|
+
- **Profiles (per-instance config)** — `kampodine config
|
|
9
|
+
init|list|show|set-default|remove` manages `~/.kampodine/config.json`
|
|
10
|
+
(dir 0700, file 0600). FLAT by owner decision: each profile carries
|
|
11
|
+
`host` / `sshKey` (PATH only — the config never holds secrets) /
|
|
12
|
+
`proxyHost` / `group`; `group` is a cosmetic `config list --group`
|
|
13
|
+
filter, and the schema only LEAVES ROOM for a future `defaults` block —
|
|
14
|
+
no inheritance logic exists. The first profile auto-becomes the default;
|
|
15
|
+
removing the LAST profile requires `--force`; removing the default
|
|
16
|
+
promotes the alphabetically-first remaining profile. Resolution
|
|
17
|
+
EVERYWHERE (deploy, status, bluegreen, migrate, vm-prepare,
|
|
18
|
+
image-import, env, dns, every deploy-lifecycle sub, metrics):
|
|
19
|
+
`--profile` flag > `KAMPODINE_PROFILE` env > config defaultProfile >
|
|
20
|
+
the legacy env behavior. A resolved profile's host/sshKey/proxyHost feed
|
|
21
|
+
the SAME ladders those scripts already use (explicit per-invocation
|
|
22
|
+
flags always win; profile.proxyHost replaces the `PROXY_HOST` env in
|
|
23
|
+
deploy and the health header in bluegreen/status). The deploy ledger
|
|
24
|
+
already records host, so `deploy list`/ledger filtering work
|
|
25
|
+
per-profile automatically. Reads/rewrites require jq; machines without
|
|
26
|
+
a config are unaffected
|
|
27
|
+
- **`kampodine metrics`** — one-shot VM snapshot over ONE ssh
|
|
28
|
+
round-trip: load + CPU count, memory (used/available from
|
|
29
|
+
/proc/meminfo), disk WITH an images/data/other breakdown (`df` for /
|
|
30
|
+
plus `du -sm` for /var/lib/containers and /data), per-container CPU/MEM
|
|
31
|
+
via `podman stats --no-stream` (busybox-safe remote commands; all
|
|
32
|
+
parsing is client-side), uptime, top-5 processes by RSS.
|
|
33
|
+
`--warn-disk <pct>` (default 90) exits 1 when root disk usage is at or
|
|
34
|
+
above the threshold — the SAME verdict logic as deploy's fail-closed
|
|
35
|
+
gate (reuses the common.sh disk helpers). `--watch N [--count M]`
|
|
36
|
+
re-snapshots every N seconds. `status --verbose` folds the SAME
|
|
37
|
+
snapshot in. NO continuous monitoring — Beszel owns that; this is the
|
|
38
|
+
CLI triage view
|
|
39
|
+
- **`kampodine backup list|download|verify|restore-plan`** — OCI Object
|
|
40
|
+
Storage family (bucket `--bucket`, default esellar-libsql-backups;
|
|
41
|
+
auth via the oci CLI's config file (`--profile`, dns convention) or
|
|
42
|
+
`--instance-principal` — NEVER stored credentials). `list` renders
|
|
43
|
+
objects + sizes + timestamps and, when LIST is denied by policy, prints
|
|
44
|
+
the exact policy shape that is missing (real case: an instance
|
|
45
|
+
principal without inspect/read on list). `download` works even when
|
|
46
|
+
LIST is denied, verifies sha256 (opc-meta-sha256/content-sha256) or
|
|
47
|
+
MD5 (opc-content-md5) metadata when present, writes the output 0600
|
|
48
|
+
and prints fingerprint summaries — never contents. `verify` is
|
|
49
|
+
READ-ONLY: the local .db image travels over ssh stdin to the VM's
|
|
50
|
+
`/usr/local/bin/restore-verify` (exit codes propagate); .tgz archives
|
|
51
|
+
are extracted to a scratch dir and every .db member piped the same
|
|
52
|
+
way — it NEVER writes into /data/tenants. `restore-plan` prints the
|
|
53
|
+
documented stop/swap/start sequence bound to the object and NEVER
|
|
54
|
+
executes a mutating step (proven hermetically: ssh/oci stubs that exit
|
|
55
|
+
99 stay untouched)
|
|
56
|
+
- **Host init auto-detection — groundwork for non-Alpine targets.** On
|
|
57
|
+
first connect `init_detect` probes `rc-service` (OpenRC) vs
|
|
58
|
+
`systemctl` (systemd) vs neither, and caches the verdict in the active
|
|
59
|
+
profile entry (`"init"` field; per-process without a profile). One
|
|
60
|
+
helper, `svc_action <name> <start|stop|restart>`, emits the
|
|
61
|
+
init-specific command — migrated call sites: deploy's restart path +
|
|
62
|
+
its failure diagnostics, `deploy-lifecycle restart`, and the status
|
|
63
|
+
roll call. Behavior on Alpine is byte-identical (existing tests pin
|
|
64
|
+
the exact `rc-service` strings). A host with NEITHER init refuses
|
|
65
|
+
service actions with guidance instead of guessing. NO per-OS
|
|
66
|
+
vm-prepare / distro bootstrap — detection + action resolution only
|
|
67
|
+
- **Health-probe compat** — the busybox-wget health probe stays the
|
|
68
|
+
default (byte-identical), with a curl fallback for GNU-only hosts;
|
|
69
|
+
detection piggybacks the same first-connect probe
|
|
70
|
+
(`REMOTE_HTTP_CLIENT`)
|
|
71
|
+
- Help everywhere held: every new command and subcommand (config init/
|
|
72
|
+
list/show/set-default/remove, metrics, backup list/download/verify/
|
|
73
|
+
restore-plan) answers `--help`/`-h` with usage + examples; the
|
|
74
|
+
top-level index gained CONFIG / METRICS / BACKUP groups; help
|
|
75
|
+
coverage, cli-dispatch, and script-gates gates extended for every new
|
|
76
|
+
script
|
|
77
|
+
|
|
78
|
+
## 0.5.0 — 2026-10-08
|
|
79
|
+
|
|
80
|
+
Deployment lifecycle management is now native — born from a real outage:
|
|
81
|
+
the VM disk hit 100% because old sha-tagged deploy images accumulated
|
|
82
|
+
silently (~1GB each), prod sqlite writes failed with disk I/O errors, and
|
|
83
|
+
nothing surfaced it. Plus the owner's ask: total deployments, visible
|
|
84
|
+
always.
|
|
85
|
+
|
|
86
|
+
- **Deployment ledger** — every `deploy` and `deploy --rollback` appends
|
|
87
|
+
`{ts, host, sha, tag, result, duration_ms, subject}` to the local
|
|
88
|
+
append-only `~/.kampodine/deployments.jsonl` (dir 0700, file 0600);
|
|
89
|
+
shas/tags/hosts/subjects only — NEVER secrets. Failed runs are recorded
|
|
90
|
+
too (EXIT trap). Every deploy run ends with a
|
|
91
|
+
`Total deployments: N · current tag: X` footer; `status` prints the count
|
|
92
|
+
- **`deploy list`** — deployment history merging VM truth (podman
|
|
93
|
+
sha-tagged images + created timestamps, the RUNNING one marked) with the
|
|
94
|
+
local ledger (last result + recorded git subject per tag); pruned tags
|
|
95
|
+
stay listed from the ledger; `Total deployments: N` footer. VM
|
|
96
|
+
unreachable degrades to a ledger-only view
|
|
97
|
+
- **Disk guard** — deploy reads the VM's `df` for `/var/lib/containers`
|
|
98
|
+
before the build and after cleanup: above 90% it warns LOUD with the
|
|
99
|
+
prune hint; `--require-disk <pct>` fails closed before anything ships;
|
|
100
|
+
`status --host` shows the same reading
|
|
101
|
+
- **`deploy prune [--keep N] [--dry-run]`** — removes old sha-tagged deploy
|
|
102
|
+
images on the VM, ALWAYS keeping the currently-running image, `ts-rollback`,
|
|
103
|
+
and the newest N sha-tags (default 2); refuses anything a running
|
|
104
|
+
container uses; `--dry-run` prints the exact `podman rmi` commands and
|
|
105
|
+
removes nothing
|
|
106
|
+
- **Ops conveniences** (all `--host`/`--ssh-key` aware, same resolution as
|
|
107
|
+
deploy): `deploy logs [--lines N]` (podman logs --tail), `deploy restart`
|
|
108
|
+
(rc-service — deploy's own restart path, status printed after), and
|
|
109
|
+
`deploy shell` (interactive sh in the api container; `shell exec -- <cmd>`
|
|
110
|
+
for one-shot)
|
|
111
|
+
- **`status` grew a VM section**: disk usage, sha-tagged image count with a
|
|
112
|
+
reclaimable-by-prune estimate, and a service roll call (kampodine-api,
|
|
113
|
+
kamal-proxy, walshipper if present)
|
|
114
|
+
- Help everywhere held: every new subcommand answers `--help`/`-h` with
|
|
115
|
+
usage + examples; the top-level index gained a DEPLOY LIFECYCLE section.
|
|
116
|
+
Tests grew from 114 to 171: ledger append/read (incl. rollback entries and
|
|
117
|
+
exact key-set pins), list table + total footer, prune keep-set fixtures
|
|
118
|
+
(running/ts-rollback/newest-N never removed), disk threshold matrix +
|
|
119
|
+
fail-closed, logs/restart/shell command construction, help coverage
|
|
120
|
+
|
|
3
121
|
## 0.4.0 — 2026-10-07
|
|
4
122
|
|
|
5
123
|
**Breaking: project-generic naming.** Every origin-coupled default name is
|
package/README.md
CHANGED
|
@@ -45,9 +45,53 @@ kampodine deploy:
|
|
|
45
45
|
public smoke: health + build-id endpoints
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
+
- **`kampodine config`** — per-instance profiles in
|
|
49
|
+
`~/.kampodine/config.json` (dir 0700, file 0600): `init --name prod
|
|
50
|
+
--host root@<ip> --ssh-key <path> [--proxy-host <h>] [--group main]
|
|
51
|
+
[--set-default]`, `list` (+ `--group` cosmetic filter, default marker,
|
|
52
|
+
sshKey shown as PATH only), `show`, `set-default`, `remove` (the last
|
|
53
|
+
profile needs `--force`). FLAT — no inheritance between profiles (the
|
|
54
|
+
schema leaves room for a future `defaults` block; nothing reads one).
|
|
55
|
+
Resolution everywhere: `--profile` flag | `KAMPODINE_PROFILE` |
|
|
56
|
+
defaultProfile | the legacy env vars — a profile's host/sshKey/proxyHost
|
|
57
|
+
feed the same ladders every subcommand already uses
|
|
58
|
+
- **`kampodine metrics`** — one-shot VM snapshot over one SSH round-trip:
|
|
59
|
+
load + CPUs, memory, disk with an images/data/other breakdown,
|
|
60
|
+
per-container CPU/MEM, uptime, top-5 by RSS; `--warn-disk <pct>`
|
|
61
|
+
(default 90) exits 1 at the threshold (the deploy gate's verdict
|
|
62
|
+
logic); `--watch N` to loop. `status --verbose` folds the same
|
|
63
|
+
snapshot in. One-shot CLI view only — continuous monitoring belongs to
|
|
64
|
+
Beszel
|
|
65
|
+
- **`kampodine backup`** — OCI Object Storage backup family
|
|
66
|
+
(`--bucket`, default `esellar-libsql-backups`; oci config-file
|
|
67
|
+
`--profile` or `--instance-principal` auth ONLY — never stored
|
|
68
|
+
credentials): `list` (sizes + timestamps; LIST-denied prints the
|
|
69
|
+
missing-policy guidance), `download` (GET works without LIST;
|
|
70
|
+
sha256/MD5 verification against object metadata when present; output
|
|
71
|
+
0600; fingerprint summaries, never contents), `verify` (read-only:
|
|
72
|
+
pipes the local .db — or every .db member of a .tgz, extracted to a
|
|
73
|
+
scratch dir — over ssh stdin into the VM's
|
|
74
|
+
`/usr/local/bin/restore-verify`; NEVER writes into /data/tenants), and
|
|
75
|
+
`restore-plan` (prints the documented stop/swap/start sequence; never
|
|
76
|
+
executes anything)
|
|
48
77
|
- **`kampodine deploy`** — stream deploy; `--version <sha>` restreams an
|
|
49
78
|
existing build; `--rollback [sha]` is an instant image-tag rollback (the
|
|
50
|
-
previous image stays on the VM for exactly this)
|
|
79
|
+
previous image stays on the VM for exactly this). Every run appends to the
|
|
80
|
+
local deployment ledger and ends with a total-deployments footer; a disk
|
|
81
|
+
guard reads the VM's `df` before the build (LOUD warning above 90%,
|
|
82
|
+
`--require-disk <pct>` fails closed)
|
|
83
|
+
- **`kampodine deploy <lifecycle>`** — the deployment lifecycle as native
|
|
84
|
+
subcommands (same `--host`/`--ssh-key` resolution as deploy):
|
|
85
|
+
`list` merges the VM's sha-tagged images (running one marked) with the
|
|
86
|
+
local ledger (`~/.kampodine/deployments.jsonl` — append-only history of
|
|
87
|
+
every deploy/rollback: ts, host, sha, tag, result, duration, git subject;
|
|
88
|
+
never secrets) into a table with a `Total deployments: N` footer;
|
|
89
|
+
`prune [--keep N] [--dry-run]` reclaims VM disk by removing old
|
|
90
|
+
sha-tagged images while ALWAYS keeping the running image, `ts-rollback`,
|
|
91
|
+
and the newest N (the 2026-10-08 disk-full outage is the reason this
|
|
92
|
+
exists); `logs [--lines N]`, `restart` (rc-service, deploy's own restart
|
|
93
|
+
path), and `shell` (interactive sh, or `exec -- <cmd>`) end the
|
|
94
|
+
hand-rolled-ssh era
|
|
51
95
|
- **`kampodine env`** — manage the remote app env file (`/etc/kampodine/env`,
|
|
52
96
|
0600 root) without ever printing a secret: `list` shows KEY + fingerprints
|
|
53
97
|
only (value length + first 2 chars), `push --file <env>` uploads over ssh
|
|
@@ -70,6 +114,11 @@ kampodine deploy:
|
|
|
70
114
|
rollback. A guest **anchor watcher** installed by `vm-prepare` holds the
|
|
71
115
|
flip's IP half — add-only and inert until a flip writes its anchor config.
|
|
72
116
|
Every sub-step has its own `--help` with usage + examples
|
|
117
|
+
- **`kampodine status`** — deployment count (from the ledger), live health
|
|
118
|
+
through the proxy, and — with `--host` — VM disk usage (loud warning above
|
|
119
|
+
90%), the sha-tagged image count with a reclaimable-by-prune estimate, a
|
|
120
|
+
service roll call (app, kamal-proxy, walshipper if present), plus the
|
|
121
|
+
blue/green pair view; `--verbose` adds the full metrics snapshot
|
|
73
122
|
- **`kampodine vm-prepare`** — first-run bootstrap of a bare Alpine host:
|
|
74
123
|
sshd hardening (fresh VMs pass unattended), busybox-wget health probes (the
|
|
75
124
|
golden image ships no curl), the blue-green anchor watcher, the podman
|
|
@@ -80,9 +129,18 @@ kampodine deploy:
|
|
|
80
129
|
- **`kampodine migrate`** — sqlite migrations over SSH
|
|
81
130
|
|
|
82
131
|
**Help everywhere:** `kampodine --help` (or bare `kampodine`) prints a
|
|
83
|
-
grouped command index (DEPLOY / INFRA / DNS / ENV
|
|
84
|
-
|
|
85
|
-
|
|
132
|
+
grouped command index (DEPLOY / DEPLOY LIFECYCLE / INFRA / DNS / ENV /
|
|
133
|
+
CONFIG / METRICS / BACKUP, vercel-style); every command — and every
|
|
134
|
+
deploy-lifecycle/bluegreen/env/dns/config/backup sub-step — answers
|
|
135
|
+
`--help` with usage + examples. No subcommand silently does nothing on
|
|
136
|
+
`--help`.
|
|
137
|
+
|
|
138
|
+
**Host init auto-detection:** on first connect kampodine probes
|
|
139
|
+
`rc-service` (Alpine/OpenRC) vs `systemctl` (systemd) and caches the
|
|
140
|
+
verdict in the active profile entry; service restarts and the status roll
|
|
141
|
+
call emit the right commands for the detected init (`svc_action`).
|
|
142
|
+
Alpine behavior is byte-identical to always. Groundwork for non-Alpine
|
|
143
|
+
targets — no per-distro bootstrap.
|
|
86
144
|
|
|
87
145
|
## Kamal parity
|
|
88
146
|
|
|
@@ -92,8 +150,12 @@ usage + examples. No subcommand silently does nothing on `--help`.
|
|
|
92
150
|
| `kamal rollback` | `kampodine deploy --rollback [sha]` |
|
|
93
151
|
| `kamal details` | `kampodine bluegreen status` |
|
|
94
152
|
| `kamal proxy …` | kamal-proxy container, unchanged |
|
|
95
|
-
| `kamal app
|
|
96
|
-
| `kamal
|
|
153
|
+
| `kamal app logs` | `kampodine deploy logs` |
|
|
154
|
+
| `kamal app exec` | `kampodine deploy shell` (or `deploy shell exec -- <cmd>`) |
|
|
155
|
+
| (no equivalent) | `kampodine deploy list` / `deploy prune` / `deploy restart` / `status` |
|
|
156
|
+
| `kamal reboot / app boot` | `rc-service <app> restart` over SSH (init-aware: systemctl on systemd hosts) |
|
|
157
|
+
| kamal's per-env config files | `kampodine config` profiles (`--profile` everywhere) |
|
|
158
|
+
| (no equivalent) | `kampodine metrics` / `backup list|download|verify|restore-plan` |
|
|
97
159
|
|
|
98
160
|
## Building the expected image
|
|
99
161
|
|
|
@@ -156,9 +218,10 @@ automation is on the roadmap; today the stream is the automated path.
|
|
|
156
218
|
## Prerequisites
|
|
157
219
|
|
|
158
220
|
Deploy machine: node ≥ 20, podman, ssh key access to the target, `oci` CLI
|
|
159
|
-
(for bluegreen / image-import / dns), `jq` (for dns record surgery
|
|
160
|
-
whatever secret-resolution your env-file step
|
|
161
|
-
the reference setup uses
|
|
221
|
+
(for bluegreen / image-import / dns / backup), `jq` (for dns record surgery
|
|
222
|
+
+ profile config reads), and whatever secret-resolution your env-file step
|
|
223
|
+
uses (kampodine is agnostic; the reference setup uses
|
|
224
|
+
[varlock](https://varlock.dev) + pass).
|
|
162
225
|
|
|
163
226
|
Target: a converged Alpine + Podman + OpenRC host (see above), reachable over
|
|
164
227
|
ssh as root, with kamal-proxy running.
|
package/cli.js
CHANGED
|
@@ -16,6 +16,9 @@ const commands = {
|
|
|
16
16
|
migrate: "migrate.sh",
|
|
17
17
|
env: "env.sh",
|
|
18
18
|
dns: "dns.sh",
|
|
19
|
+
config: "config.sh",
|
|
20
|
+
metrics: "metrics.sh",
|
|
21
|
+
backup: "backup.sh",
|
|
19
22
|
};
|
|
20
23
|
|
|
21
24
|
// Command index, grouped vercel-style. Every command (and every sub-step of
|
|
@@ -30,10 +33,17 @@ DEPLOY
|
|
|
30
33
|
bluegreen reserved-IP blue/green pair: status | init | provision | flip | rollback (each sub-step has --help)
|
|
31
34
|
migrate tenant db migrations over SSH
|
|
32
35
|
|
|
36
|
+
DEPLOY LIFECYCLE
|
|
37
|
+
deploy list deployment history: VM sha-tagged images (running one marked) merged with the local ledger + total deployments count
|
|
38
|
+
deploy prune reclaim VM disk: remove old sha-tagged images (keeps running + ts-rollback + newest N); --dry-run prints exact commands
|
|
39
|
+
deploy logs tail the running api container's logs (--lines N)
|
|
40
|
+
deploy restart restart the api service (init-aware: rc-service on Alpine, systemctl on systemd hosts)
|
|
41
|
+
deploy shell interactive sh in the api container (exec -- <cmd> for one-shot)
|
|
42
|
+
|
|
33
43
|
INFRA
|
|
34
44
|
vm-prepare first-run bootstrap of a bare Alpine host (OpenRC + podman stack)
|
|
35
45
|
image-import golden qcow2 -> OCI custom image
|
|
36
|
-
status live health
|
|
46
|
+
status live health + deployment count + VM disk/image/service state + metrics (--verbose) + the blue/green pair view
|
|
37
47
|
|
|
38
48
|
DNS
|
|
39
49
|
dns OCI DNS records (oci config-file / instance-principal auth ONLY): records | add | rm
|
|
@@ -41,6 +51,16 @@ DNS
|
|
|
41
51
|
ENV
|
|
42
52
|
env remote app env file (/etc/kampodine/env, 0600): list | push | pull — values NEVER printed, fingerprints only
|
|
43
53
|
|
|
54
|
+
CONFIG
|
|
55
|
+
config per-instance profiles (~/.kampodine/config.json, 0700/0600): init | list | show | set-default | remove
|
|
56
|
+
resolution everywhere: --profile flag > KAMPODINE_PROFILE env > defaultProfile > legacy env
|
|
57
|
+
|
|
58
|
+
METRICS
|
|
59
|
+
metrics one-shot VM snapshot over SSH: load, memory, disk (images/data/other), containers, top procs; --watch N; --warn-disk exits 1
|
|
60
|
+
|
|
61
|
+
BACKUP
|
|
62
|
+
backup OCI Object Storage backups (config-file / instance-principal auth ONLY): list | download | verify | restore-plan
|
|
63
|
+
|
|
44
64
|
Every command supports --help with usage + examples. All further args pass through to the underlying script.
|
|
45
65
|
`;
|
|
46
66
|
|
package/kampodine.md
CHANGED
|
@@ -74,17 +74,27 @@ no registry, no tunnel, no docker, no systemd — SSH + podman only.
|
|
|
74
74
|
npm, with package tests (script contract gates: `bash -n` + shellcheck
|
|
75
75
|
over every script; cli dispatch contract: help/version/exit codes/bin
|
|
76
76
|
integrity)
|
|
77
|
+
- [x] per-instance profiles (`config`) — flat, multi-instance-ready, one
|
|
78
|
+
resolution ladder everywhere (v0.6.0)
|
|
79
|
+
- [x] one-shot VM metrics snapshot (`metrics`, `status --verbose`) —
|
|
80
|
+
continuous monitoring stays external (Beszel) (v0.6.0)
|
|
81
|
+
- [x] OCI Object Storage backup family (`backup list/download/verify/
|
|
82
|
+
restore-plan`) with policy-denied LIST guidance and read-only
|
|
83
|
+
restore-verify over stdin (v0.6.0)
|
|
84
|
+
- [x] host init auto-detection (openrc vs systemd) — action-resolution
|
|
85
|
+
groundwork for non-Alpine targets, Alpine byte-identical (v0.6.0)
|
|
77
86
|
|
|
78
87
|
**In progress:**
|
|
79
88
|
- [ ] reference sweep (docs → kampodine invocations)
|
|
80
89
|
|
|
81
90
|
## Roadmap
|
|
82
91
|
|
|
83
|
-
- `kampodine status` — live served sha + pair view in one command
|
|
84
92
|
- registry-path automation (mirror mode: push to any OCI registry,
|
|
85
93
|
VM-side pull, same health-gate/cutover tail)
|
|
86
|
-
-
|
|
87
|
-
|
|
94
|
+
- non-Alpine targets on top of the init auto-detection groundwork
|
|
95
|
+
(per-distro bootstrap is explicitly OUT of scope for now)
|
|
96
|
+
- optional `defaults` block in config.json feeding unset profile fields
|
|
97
|
+
(the schema already leaves room; NO inheritance exists today)
|
|
88
98
|
|
|
89
99
|
## Non-goals
|
|
90
100
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# backup.sh — OCI Object Storage backup family (libSQL db backups).
|
|
3
|
+
#
|
|
4
|
+
# AUTH RULE (hard, same as dns.sh): the `oci` CLI's own auth ONLY — its
|
|
5
|
+
# config file (--profile = OCI CONFIG profile) or instance principal (when
|
|
6
|
+
# run ON a VM). kampodine NEVER accepts, stores, or logs credential material:
|
|
7
|
+
# no tokens, no key material, nothing secret-shaped in args or output.
|
|
8
|
+
#
|
|
9
|
+
# The default bucket is esellar-libsql-backups (--bucket overrides). NOTE:
|
|
10
|
+
# --profile here is the OCI config profile (dns convention) — kampodine
|
|
11
|
+
# INSTANCE profiles do not apply to bucket ops; `backup verify` (the only
|
|
12
|
+
# ssh leg) resolves its VM target through the standard ladder:
|
|
13
|
+
# --host flag | KAMPODINE_PROFILE | config defaultProfile | KAMPODINE_HOST.
|
|
14
|
+
#
|
|
15
|
+
# LIST vs GET: object LIST can be denied by policy while GET still works —
|
|
16
|
+
# `list` prints policy guidance when denied; `download` needs only the name.
|
|
17
|
+
#
|
|
18
|
+
# Usage:
|
|
19
|
+
# kampodine backup list [--prefix <prefix>] [--bucket <name>] [--profile <oci>] [--instance-principal]
|
|
20
|
+
# kampodine backup download <object> [--out <file>] [--bucket <name>] [--profile <oci>] [--instance-principal]
|
|
21
|
+
# kampodine backup verify <local.db|.tgz> [--host <user@ip>] [--ssh-key <path>]
|
|
22
|
+
# kampodine backup restore-plan <object> [--bucket <name>]
|
|
23
|
+
#
|
|
24
|
+
# verify is READ-ONLY: the local db image travels over ssh stdin to the VM's
|
|
25
|
+
# /usr/local/bin/restore-verify; .tgz archives are extracted to a scratch
|
|
26
|
+
# dir and each db member piped the same way. It NEVER writes into
|
|
27
|
+
# /data/tenants. restore-plan prints the documented stop/swap/start sequence
|
|
28
|
+
# and NEVER executes a mutating step.
|
|
29
|
+
set -euo pipefail
|
|
30
|
+
|
|
31
|
+
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
32
|
+
# shellcheck source=common.sh
|
|
33
|
+
source "$HERE/common.sh"
|
|
34
|
+
|
|
35
|
+
BUCKET="${KAMPODINE_BACKUP_BUCKET:-esellar-libsql-backups}"
|
|
36
|
+
OCI_PROFILE_NAME="${OCI_PROFILE:-default}"
|
|
37
|
+
AUTH_ARGS=()
|
|
38
|
+
|
|
39
|
+
say() { printf '\033[1;34m[backup]\033[0m %s\n' "$*"; }
|
|
40
|
+
die() { printf '\033[1;31m[backup] FAIL:\033[0m %s\n' "$*" >&2; exit 1; }
|
|
41
|
+
usage() {
|
|
42
|
+
local code="${1:-0}"
|
|
43
|
+
printf 'Usage:\n'
|
|
44
|
+
grep '^# kampodine backup' "$0" | sed 's/^# //'
|
|
45
|
+
cat <<'EOF'
|
|
46
|
+
|
|
47
|
+
Auth is the oci CLI's own — its config file (--profile) or instance
|
|
48
|
+
principal. kampodine never accepts, stores, or logs credential material.
|
|
49
|
+
Bucket: --bucket (default esellar-libsql-backups). LIST-denied by policy is
|
|
50
|
+
not fatal for download/verify — GET works with just the object name; `list`
|
|
51
|
+
prints the exact policy shape when denied. verify is read-only over ssh
|
|
52
|
+
stdin into /usr/local/bin/restore-verify; scratch dirs only, NEVER writes
|
|
53
|
+
into /data/tenants. restore-plan prints the documented stop/swap/start
|
|
54
|
+
sequence and NEVER executes anything.
|
|
55
|
+
|
|
56
|
+
Examples:
|
|
57
|
+
kampodine backup list --prefix tenants/
|
|
58
|
+
kampodine backup list --bucket my-backups --profile my-oci-profile
|
|
59
|
+
kampodine backup download tenants/acme/20261008T050000Z.db --out /tmp/restore.db
|
|
60
|
+
kampodine backup verify /tmp/restore.db --host root@203.0.113.10
|
|
61
|
+
kampodine backup verify ./bundle.tgz # every .db member, read-only
|
|
62
|
+
kampodine backup restore-plan tenants/acme/20261008T050000Z.db
|
|
63
|
+
EOF
|
|
64
|
+
exit "$code"
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# --- shared helpers -------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
need_oci() {
|
|
70
|
+
command -v oci >/dev/null 2>&1 \
|
|
71
|
+
|| 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"
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# backup_expected_digest "<oci os object head json>" -> "sha256:<hex>" |
|
|
75
|
+
# "md5:<base64>" | "" (verification skipped). Prefers sha256 metadata
|
|
76
|
+
# (opc-meta-sha256 / content-sha256), falls back to opc-content-md5.
|
|
77
|
+
backup_expected_digest() {
|
|
78
|
+
local j="$1" v=""
|
|
79
|
+
command -v jq >/dev/null 2>&1 || { printf ''; return 0; }
|
|
80
|
+
v="$(jq -r '.data["opc-meta-sha256"] // .data["content-sha256"] // empty' <<<"$j" 2>/dev/null || true)"
|
|
81
|
+
if [[ -n "$v" ]]; then
|
|
82
|
+
printf 'sha256:%s' "$v"
|
|
83
|
+
return 0
|
|
84
|
+
fi
|
|
85
|
+
v="$(jq -r '.data["opc-content-md5"] // empty' <<<"$j" 2>/dev/null || true)"
|
|
86
|
+
if [[ -n "$v" ]]; then
|
|
87
|
+
printf 'md5:%s' "$v"
|
|
88
|
+
fi
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# hash_file <file> <sha256|md5> -> prints the digest (sha256: hex, md5:
|
|
92
|
+
# base64 — the same encoding OCI reports). Portable across macOS/Linux.
|
|
93
|
+
hash_file() {
|
|
94
|
+
local f="$1" alg="$2"
|
|
95
|
+
if [[ "$alg" == "sha256" ]]; then
|
|
96
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
97
|
+
sha256sum "$f" | awk '{print $1}'
|
|
98
|
+
elif command -v shasum >/dev/null 2>&1; then
|
|
99
|
+
shasum -a 256 "$f" | awk '{print $1}'
|
|
100
|
+
else
|
|
101
|
+
return 1
|
|
102
|
+
fi
|
|
103
|
+
else
|
|
104
|
+
if command -v openssl >/dev/null 2>&1; then
|
|
105
|
+
openssl dgst -md5 -binary "$f" | base64
|
|
106
|
+
else
|
|
107
|
+
return 1
|
|
108
|
+
fi
|
|
109
|
+
fi
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# --- dispatch -------------------------------------------------------------------
|
|
113
|
+
SUB="${1:-}"
|
|
114
|
+
case "$SUB" in
|
|
115
|
+
list|download|verify|restore-plan) shift ;;
|
|
116
|
+
-h|--help|help) usage 0 ;;
|
|
117
|
+
"") usage 2 ;;
|
|
118
|
+
*)
|
|
119
|
+
printf 'kampodine backup: unknown subcommand: %s\n\n' "$SUB" >&2
|
|
120
|
+
usage 2
|
|
121
|
+
;;
|
|
122
|
+
esac
|
|
123
|
+
|
|
124
|
+
BUCKET_FLAG="" PREFIX="" OUT="" OBJ=""
|
|
125
|
+
OCI_PROFILE_FLAG="" INSTANCE_PRINCIPAL=0
|
|
126
|
+
HOST_FLAG="" KEY_FLAG=""
|
|
127
|
+
KAMPODINE_HOST="${KAMPODINE_HOST:-}"
|
|
128
|
+
SSH_KEY="${KAMPODINE_SSH_KEY:-}"
|
|
129
|
+
PROFILE_FLAG=""
|
|
130
|
+
POSITIONAL=()
|
|
131
|
+
while [[ $# -gt 0 ]]; do
|
|
132
|
+
case "$1" in
|
|
133
|
+
--bucket) BUCKET_FLAG="$2"; shift 2 ;;
|
|
134
|
+
--prefix) PREFIX="$2"; shift 2 ;;
|
|
135
|
+
--out) OUT="$2"; shift 2 ;;
|
|
136
|
+
--profile) OCI_PROFILE_FLAG="$2"; shift 2 ;;
|
|
137
|
+
--instance-principal) INSTANCE_PRINCIPAL=1; shift ;;
|
|
138
|
+
--host) HOST_FLAG="$2"; KAMPODINE_HOST="$2"; shift 2 ;;
|
|
139
|
+
--ssh-key) KEY_FLAG="$2"; SSH_KEY="$2"; shift 2 ;;
|
|
140
|
+
--kampodine-profile) PROFILE_FLAG="$2"; shift 2 ;;
|
|
141
|
+
-h|--help) usage 0 ;;
|
|
142
|
+
--) shift; POSITIONAL+=("$@"); break ;;
|
|
143
|
+
*) POSITIONAL+=("$1"); shift ;;
|
|
144
|
+
esac
|
|
145
|
+
done
|
|
146
|
+
[[ -n "$BUCKET_FLAG" ]] && BUCKET="$BUCKET_FLAG"
|
|
147
|
+
[[ -n "$OCI_PROFILE_FLAG" ]] && OCI_PROFILE_NAME="$OCI_PROFILE_FLAG"
|
|
148
|
+
(( INSTANCE_PRINCIPAL == 1 )) && AUTH_ARGS=(--auth instance_principal)
|
|
149
|
+
OBJ="${POSITIONAL[0]:-}"
|
|
150
|
+
|
|
151
|
+
case "$SUB" in
|
|
152
|
+
list)
|
|
153
|
+
need_oci
|
|
154
|
+
PREFIX_ARGS=()
|
|
155
|
+
[[ -n "$PREFIX" ]] && PREFIX_ARGS=(--prefix "$PREFIX")
|
|
156
|
+
say "objects in bucket $BUCKET${PREFIX:+ (prefix: $PREFIX)} (profile: $OCI_PROFILE_NAME${INSTANCE_PRINCIPAL:+, instance-principal}):"
|
|
157
|
+
# NOTE: --profile stays ON the first line (script-gates static scan reads it there)
|
|
158
|
+
if ! out="$(oci os object list --all --bucket-name "$BUCKET" "${PREFIX_ARGS[@]+"${PREFIX_ARGS[@]}"}" --profile "$OCI_PROFILE_NAME" ${AUTH_ARGS[@]+"${AUTH_ARGS[@]}"} 2>&1)"; then
|
|
159
|
+
case "$out" in
|
|
160
|
+
*NotAuthorizedOrNotFound*|*NotAuthorized*|*NotAuthenticated*|*"Authorization failed"*)
|
|
161
|
+
printf '\033[1;31m[backup] FAIL:\033[0m LIST denied — this identity (profile %s / instance principal) lacks INSPECT+READ on bucket %s.\n' "$OCI_PROFILE_NAME" "$BUCKET" >&2
|
|
162
|
+
printf 'The bucket compartment needs a policy like:\n' >&2
|
|
163
|
+
printf ' Allow dynamic-group <your-dg> to read buckets in compartment <compartment>\n' >&2
|
|
164
|
+
printf ' Allow dynamic-group <your-dg> to read objects in compartment <compartment>\n' >&2
|
|
165
|
+
printf 'GET works without LIST once the object name is known:\n' >&2
|
|
166
|
+
printf ' kampodine backup download <object> --bucket %s\n' "$BUCKET" >&2
|
|
167
|
+
exit 1
|
|
168
|
+
;;
|
|
169
|
+
*)
|
|
170
|
+
die "object list failed: $out"
|
|
171
|
+
;;
|
|
172
|
+
esac
|
|
173
|
+
fi
|
|
174
|
+
rows="$(jq -r '.data.objects[]? | [.name, (.size | tostring), (.timeCreated // "-")] | @tsv' <<<"$out")"
|
|
175
|
+
if [[ -z "$rows" ]]; then
|
|
176
|
+
say "(no objects${PREFIX:+ with prefix $PREFIX})"
|
|
177
|
+
exit 0
|
|
178
|
+
fi
|
|
179
|
+
printf '%-56s %-12s %s\n' "NAME" "SIZE" "UPDATED"
|
|
180
|
+
while IFS=$'\t' read -r n s t; do
|
|
181
|
+
printf '%-56s %-12s %s\n' "$n" "$s" "$t"
|
|
182
|
+
done <<<"$rows"
|
|
183
|
+
;;
|
|
184
|
+
|
|
185
|
+
download)
|
|
186
|
+
need_oci
|
|
187
|
+
[[ -n "$OBJ" ]] || die "usage: kampodine backup download <object> [--out <file>] (--help)"
|
|
188
|
+
OUT="${OUT:-$(basename "$OBJ")}"
|
|
189
|
+
head_json="$(oci os object head --bucket-name "$BUCKET" --name "$OBJ" --profile "$OCI_PROFILE_NAME" ${AUTH_ARGS[@]+"${AUTH_ARGS[@]}"} 2>/dev/null || true)"
|
|
190
|
+
expected="$(backup_expected_digest "$head_json")"
|
|
191
|
+
# 0600 FROM CREATION: umask 077 around the write; chmod after.
|
|
192
|
+
( umask 077; oci os object get --bucket-name "$BUCKET" --name "$OBJ" --file "$OUT" --profile "$OCI_PROFILE_NAME" ${AUTH_ARGS[@]+"${AUTH_ARGS[@]}"} >/dev/null ) \
|
|
193
|
+
|| die "object get failed for $OBJ (bucket $BUCKET)"
|
|
194
|
+
chmod 600 "$OUT"
|
|
195
|
+
size="$(wc -c < "$OUT" | tr -d '[:space:]')"
|
|
196
|
+
fp="$(hash_file "$OUT" sha256 || true)"
|
|
197
|
+
say "downloaded $OBJ -> $OUT (0600, $size bytes)"
|
|
198
|
+
[[ -n "$fp" ]] && say "sha256: ${fp:0:16}…"
|
|
199
|
+
if [[ -z "$expected" ]]; then
|
|
200
|
+
say "integrity: skipped (no digest metadata on the object)"
|
|
201
|
+
else
|
|
202
|
+
alg="${expected%%:*}"
|
|
203
|
+
want="${expected#*:}"
|
|
204
|
+
got="$(hash_file "$OUT" "$alg" 2>/dev/null || true)"
|
|
205
|
+
if [[ -z "$got" || "$got" != "$want" ]]; then
|
|
206
|
+
printf '\033[1;31m[backup] FAIL:\033[0m digest MISMATCH (%s)\n' "$alg" >&2
|
|
207
|
+
printf ' expected: %s\n' "$want" >&2
|
|
208
|
+
printf ' got : %s\n' "$got" >&2
|
|
209
|
+
exit 1
|
|
210
|
+
fi
|
|
211
|
+
say "integrity: verified ($alg)"
|
|
212
|
+
fi
|
|
213
|
+
;;
|
|
214
|
+
|
|
215
|
+
verify)
|
|
216
|
+
[[ -n "$OBJ" ]] || die "usage: kampodine backup verify <local.db|.tgz> (--help)"
|
|
217
|
+
[[ -f "$OBJ" ]] || die "no such file: $OBJ"
|
|
218
|
+
case "$OBJ" in
|
|
219
|
+
*.db|*.sqlite|*.sqlite3) VERIFY_MODE="db" ;;
|
|
220
|
+
*.tgz|*.tar.gz) VERIFY_MODE="tgz" ;;
|
|
221
|
+
*) die "backup verify takes a .db (or .sqlite/.sqlite3) or .tgz file — got: $OBJ" ;;
|
|
222
|
+
esac
|
|
223
|
+
# ssh leg resolves through the kampodine ladder: --host flag | profile
|
|
224
|
+
profile_resolve "$PROFILE_FLAG" "$HOST_FLAG" "$KEY_FLAG"
|
|
225
|
+
SSH_KEY="${KEY_FLAG:-${KAMPODINE_SSH_KEY:-}}"
|
|
226
|
+
[[ -n "$KAMPODINE_HOST" ]] || die "verify needs a VM target: --host root@<ip>, --kampodine-profile <name>, KAMPODINE_PROFILE, config defaultProfile, or KAMPODINE_HOST"
|
|
227
|
+
SSH_ARGS=(-o ConnectTimeout=10 -o BatchMode=yes)
|
|
228
|
+
[[ -n "$SSH_KEY" ]] && SSH_ARGS+=(-i "$SSH_KEY")
|
|
229
|
+
# $1 is a composed remote command — client-side expansion is the design.
|
|
230
|
+
# shellcheck disable=SC2029
|
|
231
|
+
vm() { ssh "${SSH_ARGS[@]}" "$KAMPODINE_HOST" "$1"; }
|
|
232
|
+
if [[ "$(uname -s)" == "Darwin" ]]; then
|
|
233
|
+
SSH_AUTH_SOCK="$(launchctl getenv SSH_AUTH_SOCK 2>/dev/null || true)"
|
|
234
|
+
export SSH_AUTH_SOCK
|
|
235
|
+
fi
|
|
236
|
+
|
|
237
|
+
if [[ "$VERIFY_MODE" == "db" ]]; then
|
|
238
|
+
say "piping $OBJ over ssh stdin to /usr/local/bin/restore-verify on $KAMPODINE_HOST (read-only; scratch only — NEVER writes into /data/tenants)"
|
|
239
|
+
cat "$OBJ" | vm "/usr/local/bin/restore-verify /dev/stdin"
|
|
240
|
+
say "OK — restore-verify accepted the db image"
|
|
241
|
+
else
|
|
242
|
+
scratch="$(mktemp -d "${TMPDIR:-/tmp}/kampodine-verify.XXXXXX")"
|
|
243
|
+
trap 'rm -rf "$scratch"' EXIT
|
|
244
|
+
tar -xzf "$OBJ" -C "$scratch"
|
|
245
|
+
members="$(find "$scratch" -type f \( -name '*.db' -o -name '*.sqlite' -o -name '*.sqlite3' \) | LC_ALL=C sort)"
|
|
246
|
+
[[ -n "$members" ]] || die "no .db/.sqlite members in $OBJ — nothing to verify"
|
|
247
|
+
say "piping every .db member of $OBJ over ssh stdin (read-only; scratch dir $scratch)"
|
|
248
|
+
while IFS= read -r m; do
|
|
249
|
+
member_name="${m#"$scratch"/}"
|
|
250
|
+
say "verify member: $member_name"
|
|
251
|
+
cat "$m" | vm "/usr/local/bin/restore-verify /dev/stdin" \
|
|
252
|
+
|| die "restore-verify FAILED for $member_name"
|
|
253
|
+
done <<<"$members"
|
|
254
|
+
say "OK — every db member passed restore-verify"
|
|
255
|
+
fi
|
|
256
|
+
;;
|
|
257
|
+
|
|
258
|
+
restore-plan)
|
|
259
|
+
[[ -n "$OBJ" ]] || usage 2
|
|
260
|
+
# PRINT-ONLY by design: no oci, no ssh, no mutation — the plan is
|
|
261
|
+
# documentation bound to this object, never a runner.
|
|
262
|
+
cat <<EOF
|
|
263
|
+
== restore plan (print-only — kampodine NEVER executes these steps) ==
|
|
264
|
+
object : $OBJ
|
|
265
|
+
bucket : $BUCKET
|
|
266
|
+
|
|
267
|
+
1. stage the backup locally (download works even when LIST is denied):
|
|
268
|
+
kampodine backup download '$OBJ' --out /tmp/restore.db
|
|
269
|
+
2. copy to the VM scratch dir (never directly into /data/tenants):
|
|
270
|
+
scp /tmp/restore.db root@<vm>:/tmp/restore.db
|
|
271
|
+
3. verify BEFORE touching anything (read-only, over ssh stdin):
|
|
272
|
+
kampodine backup verify /tmp/restore.db --host root@<vm>
|
|
273
|
+
4. STOP the api so writes drain:
|
|
274
|
+
ssh root@<vm> 'rc-service kampodine-api stop'
|
|
275
|
+
5. snapshot the current state (your rollback point):
|
|
276
|
+
ssh root@<vm> 'cp -a /data/tenants/<tenant-dir> /data/tenants/<tenant-dir>.pre-restore'
|
|
277
|
+
6. SWAP the restored file in (permissions matter):
|
|
278
|
+
ssh root@<vm> 'install -m 640 -o root -g root /tmp/restore.db /data/tenants/<tenant-dir>/db.sqlite'
|
|
279
|
+
7. START and smoke:
|
|
280
|
+
ssh root@<vm> 'rc-service kampodine-api start'
|
|
281
|
+
kampodine status
|
|
282
|
+
|
|
283
|
+
Notes:
|
|
284
|
+
- step 3 is this family's restore-verify contract: the db image travels
|
|
285
|
+
over ssh stdin; /usr/local/bin/restore-verify checks it read-only.
|
|
286
|
+
- kampodine NEVER executes any step above — this plan documents the
|
|
287
|
+
infra runbook sequence for this object; a human runs each step.
|
|
288
|
+
EOF
|
|
289
|
+
;;
|
|
290
|
+
esac
|
package/scripts/bluegreen.sh
CHANGED
|
@@ -71,9 +71,29 @@
|
|
|
71
71
|
# loopback; the reserved IP is checked over :80 with the prod Host header).
|
|
72
72
|
set -euo pipefail
|
|
73
73
|
|
|
74
|
+
HERE="${BASH_SOURCE[0]%/*}"
|
|
75
|
+
# shellcheck source=common.sh
|
|
76
|
+
source "$HERE/common.sh"
|
|
77
|
+
|
|
78
|
+
# --profile pre-scan (kampodine INSTANCE profile): every sub-step accepts it
|
|
79
|
+
# before its own flags; flip/provision have their own flag loops, so the flag
|
|
80
|
+
# is lifted here once, validated, and its proxyHost feeds the status health
|
|
81
|
+
# header (APP_HOST_HEADER) when the env did not pin one. The flip's own
|
|
82
|
+
# ACME hostname stays derived from the reserved IP by design.
|
|
83
|
+
PROFILE_FLAG=""
|
|
84
|
+
args=()
|
|
85
|
+
while [[ $# -gt 0 ]]; do
|
|
86
|
+
case "$1" in
|
|
87
|
+
--profile) PROFILE_FLAG="$2"; shift 2 ;;
|
|
88
|
+
*) args+=("$1"); shift ;;
|
|
89
|
+
esac
|
|
90
|
+
done
|
|
91
|
+
set -- ${args[@]+"${args[@]}"}
|
|
92
|
+
profile_resolve "$PROFILE_FLAG" "" ""
|
|
93
|
+
|
|
74
94
|
PROFILE="${OCI_PROFILE:-default}"
|
|
75
95
|
COMPARTMENT_NAME="${OCI_COMPARTMENT:-kampodine}"
|
|
76
|
-
APP_HOST_HEADER="${APP_HOST_HEADER:-app.example.com}"
|
|
96
|
+
APP_HOST_HEADER="${PROFILE_PROXY_HOST:-${APP_HOST_HEADER:-app.example.com}}"
|
|
77
97
|
REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)}"
|
|
78
98
|
SSH_OPTS=(-o ConnectTimeout=6 -o BatchMode=yes -o StrictHostKeyChecking=accept-new)
|
|
79
99
|
|