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 +47 -0
- package/README.md +24 -3
- package/cli.js +18 -4
- package/package.json +1 -1
- package/scripts/bluegreen.sh +157 -55
- package/scripts/deploy.sh +35 -21
- package/scripts/dns.sh +266 -0
- package/scripts/env.sh +183 -0
- package/scripts/image-import.sh +31 -20
- package/scripts/migrate.sh +21 -5
- package/scripts/status.sh +23 -0
- package/scripts/vm-prepare.sh +98 -84
package/scripts/image-import.sh
CHANGED
|
@@ -14,32 +14,42 @@
|
|
|
14
14
|
# Auth: ~/.oci/config (brew install oci-cli; oci setup config) by default, or
|
|
15
15
|
# --from-pass to source API credentials from the local pass store — the key
|
|
16
16
|
# NEVER enters this repo, the image, or any log (values only go into env vars).
|
|
17
|
-
# Every oci call pins --profile $PROFILE (OCI_PROFILE, default
|
|
18
|
-
# the CLI's DEFAULT profile is NOT the
|
|
17
|
+
# Every oci call pins --profile $PROFILE (OCI_PROFILE, default "default") —
|
|
18
|
+
# the CLI's DEFAULT profile is NOT the kampodine tenancy.
|
|
19
19
|
# Placeholder pass paths (create yours to match):
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
20
|
+
# kampodine/oci/user user OCID
|
|
21
|
+
# kampodine/oci/tenancy tenancy OCID
|
|
22
|
+
# kampodine/oci/fingerprint API key fingerprint
|
|
23
|
+
# kampodine/oci/api-key PEM private key
|
|
24
24
|
#
|
|
25
25
|
# Usage:
|
|
26
26
|
# kampodine image-import # defaults below
|
|
27
27
|
# kampodine image-import --from-pass
|
|
28
|
-
# kampodine image-import --image build/
|
|
28
|
+
# kampodine image-import --image build/kampodine-alpine-3.22.6-aarch64.qcow2
|
|
29
29
|
set -euo pipefail
|
|
30
30
|
|
|
31
31
|
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
|
32
32
|
IMAGE=""
|
|
33
|
-
BUCKET="${OCI_IMPORT_BUCKET:-
|
|
34
|
-
NAME_PREFIX="${OCI_IMPORT_NAME:-
|
|
35
|
-
COMPARTMENT_NAME="${OCI_COMPARTMENT:-
|
|
36
|
-
PROFILE="${OCI_PROFILE:-
|
|
33
|
+
BUCKET="${OCI_IMPORT_BUCKET:-kampodine-image-import}"
|
|
34
|
+
NAME_PREFIX="${OCI_IMPORT_NAME:-kampodine-alpine}"
|
|
35
|
+
COMPARTMENT_NAME="${OCI_COMPARTMENT:-kampodine}"
|
|
36
|
+
PROFILE="${OCI_PROFILE:-default}"
|
|
37
37
|
FROM_PASS=0
|
|
38
38
|
KEEP_OBJECT=0
|
|
39
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
|
|
@@ -55,18 +65,19 @@ while [[ $# -gt 0 ]]; do
|
|
|
55
65
|
done
|
|
56
66
|
|
|
57
67
|
command -v oci >/dev/null 2>&1 || die "oci CLI not found — brew install oci-cli, then: oci setup config (infra/oci/README.md § CLI setup)"
|
|
68
|
+
[[ -n "${OCI_COMPARTMENT:-}" ]] || die "set OCI_COMPARTMENT=<compartment name or ocid> — no default: compartments are account-specific"
|
|
58
69
|
command -v pass >/dev/null 2>&1 || true
|
|
59
70
|
|
|
60
71
|
if [[ $FROM_PASS -eq 1 ]]; then
|
|
61
72
|
command -v pass >/dev/null 2>&1 || die "pass not installed (brew install pass)"
|
|
62
73
|
say "sourcing OCI API credentials from pass (values never printed/logged)…"
|
|
63
74
|
for entry in user tenancy fingerprint api-key; do
|
|
64
|
-
pass show "
|
|
75
|
+
pass show "kampodine/oci/$entry" >/dev/null 2>&1 || die "missing pass entry kampodine/oci/$entry"
|
|
65
76
|
done
|
|
66
|
-
OCI_CLI_USER="$(pass show
|
|
67
|
-
OCI_CLI_TENANCY="$(pass show
|
|
68
|
-
OCI_CLI_FINGERPRINT="$(pass show
|
|
69
|
-
OCI_CLI_KEY_CONTENT="$(pass show
|
|
77
|
+
OCI_CLI_USER="$(pass show kampodine/oci/user)"
|
|
78
|
+
OCI_CLI_TENANCY="$(pass show kampodine/oci/tenancy)"
|
|
79
|
+
OCI_CLI_FINGERPRINT="$(pass show kampodine/oci/fingerprint)"
|
|
80
|
+
OCI_CLI_KEY_CONTENT="$(pass show kampodine/oci/api-key)"
|
|
70
81
|
export OCI_CLI_USER OCI_CLI_TENANCY OCI_CLI_FINGERPRINT OCI_CLI_KEY_CONTENT
|
|
71
82
|
fi
|
|
72
83
|
|
|
@@ -74,7 +85,7 @@ fi
|
|
|
74
85
|
if [[ -z "$IMAGE" ]]; then
|
|
75
86
|
# Constrained glob (our own build output names) — ls is fine here.
|
|
76
87
|
# shellcheck disable=SC2012
|
|
77
|
-
IMAGE="$(ls -t "$REPO_ROOT/infra/alpine-host/build"/
|
|
88
|
+
IMAGE="$(ls -t "$REPO_ROOT/infra/alpine-host/build"/kampodine-alpine-*.qcow2 2>/dev/null | head -1 || true)"
|
|
78
89
|
[[ -n "$IMAGE" ]] || die "no qcow2 under infra/alpine-host/build/ — run: (cd infra/alpine-host && packer build .)"
|
|
79
90
|
fi
|
|
80
91
|
[[ -f "$IMAGE" ]] || die "image not found: $IMAGE"
|
|
@@ -86,7 +97,7 @@ NAMESPACE="$(oci os ns get --profile "$PROFILE" --query data --raw-output)"
|
|
|
86
97
|
|
|
87
98
|
STAMP="$(date +%Y%m%d-%H%M%S)"
|
|
88
99
|
OBJECT_NAME="${NAME_PREFIX}-${STAMP}.qcow2"
|
|
89
|
-
IMAGE_NAME="${NAME_PREFIX}-$(basename "$IMAGE" .qcow2 | sed 's/^
|
|
100
|
+
IMAGE_NAME="${NAME_PREFIX}-$(basename "$IMAGE" .qcow2 | sed 's/^kampodine-alpine-//')-${STAMP}"
|
|
90
101
|
|
|
91
102
|
say "uploading $(basename "$IMAGE") -> os://$BUCKET/$OBJECT_NAME"
|
|
92
103
|
oci os object put -bn "$BUCKET" --profile "$PROFILE" --file "$IMAGE" --name "$OBJECT_NAME" --force \
|
|
@@ -152,7 +163,7 @@ say " display : $IMAGE_NAME"
|
|
|
152
163
|
say " firmware : $IMPORT_FIRMWARE"
|
|
153
164
|
say "next:"
|
|
154
165
|
if [[ "$IMPORT_FIRMWARE" == "UEFI_64" ]]; then
|
|
155
|
-
say " A1-ready. kampodine bluegreen provision <blue|green> picks this image (newest
|
|
166
|
+
say " A1-ready. kampodine bluegreen provision <blue|green> picks this image (newest kampodine-alpine*)."
|
|
156
167
|
else
|
|
157
168
|
say " A1 launch will REJECT this image (firmware $IMPORT_FIRMWARE) — see the WARN above."
|
|
158
169
|
fi
|
package/scripts/migrate.sh
CHANGED
|
@@ -15,17 +15,33 @@ set -euo pipefail
|
|
|
15
15
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
16
16
|
REPO_ROOT="${REPO_ROOT:-$(cd "${SCRIPT_DIR}/../../.." && pwd)}"
|
|
17
17
|
TENANT_DIR="${TENANT_DIR:-/data/tenants}"
|
|
18
|
-
SERVICE="${SERVICE:-
|
|
18
|
+
SERVICE="${SERVICE:-kampodine-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 kampodine-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
|
|
|
@@ -35,11 +51,11 @@ command -v pnpm >/dev/null 2>&1 || fail "pnpm not found in PATH"
|
|
|
35
51
|
[[ -d "$TENANT_DIR" ]] || fail "tenant dir ${TENANT_DIR} does not exist"
|
|
36
52
|
|
|
37
53
|
# Writing schema while the API serves traffic risks SQLITE_BUSY on a single-writer
|
|
38
|
-
# engine — the service must be stopped first (rc-service
|
|
54
|
+
# engine — the service must be stopped first (rc-service kampodine-api stop). Override exists
|
|
39
55
|
# for deliberate local use.
|
|
40
56
|
if systemctl is-active --quiet "$SERVICE" 2>/dev/null; then
|
|
41
57
|
if [[ $ALLOW_RUNNING -ne 1 ]]; then
|
|
42
|
-
fail "${SERVICE} is running — stop it first (rc-service
|
|
58
|
+
fail "${SERVICE} is running — stop it first (rc-service kampodine-api stop) or pass --allow-running"
|
|
43
59
|
fi
|
|
44
60
|
log "WARNING: migrating while ${SERVICE} is running (--allow-running)"
|
|
45
61
|
fi
|
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
|
@@ -12,21 +12,21 @@
|
|
|
12
12
|
# 2. apk repositories: ensure the v3.22 community repo (podman lives there)
|
|
13
13
|
# 3. podman stack: podman podman-docker crun catatonit netavark
|
|
14
14
|
# aardvark-dns fuse-overlayfs
|
|
15
|
-
# 4. /etc/
|
|
15
|
+
# 4. /etc/kampodine/ (kampodine deploy writes /etc/kampodine/env here, 0600 root)
|
|
16
16
|
# 5. /etc/containers/registries.conf (insecure 127.0.0.1:5000; search docker.io)
|
|
17
17
|
# 6. sysctl net.ipv4.ip_unprivileged_port_start=80 (persisted + applied live)
|
|
18
|
-
# 7. OpenRC services /etc/init.d/
|
|
18
|
+
# 7. OpenRC services /etc/init.d/kampodine-api + /etc/init.d/kamal-proxy
|
|
19
19
|
# (supervise-daemon around plain `podman run`), rc-update'd into the
|
|
20
20
|
# default runlevel — that IS boot survival (no quadlets, no systemctl)
|
|
21
|
-
# 8.
|
|
22
|
-
# busybox watcher (no container) that polls /etc/
|
|
21
|
+
# 8. kampodine-anchor: the blue-green reserved-ip flip's GUEST half — a
|
|
22
|
+
# busybox watcher (no container) that polls /etc/kampodine/anchor.conf and
|
|
23
23
|
# `ip addr add`s the anchor address at flip time. Started+enabled on every
|
|
24
24
|
# VM, inert without the conf (flip tooling writes it over ssh).
|
|
25
25
|
# 9. kamal-proxy image pulled + service UP — the first deploy execs into it
|
|
26
26
|
# to issue the fresh ACME certificate
|
|
27
27
|
#
|
|
28
|
-
# The
|
|
29
|
-
# the first deploy over the registry tunnel) nor /etc/
|
|
28
|
+
# The kampodine-api container is NOT started here: neither its image (pulled by
|
|
29
|
+
# the first deploy over the registry tunnel) nor /etc/kampodine/env (written by
|
|
30
30
|
# kampodine deploy) exists yet on a fresh VM.
|
|
31
31
|
#
|
|
32
32
|
# Idempotent: safe to re-run. apk add is a no-op when satisfied; managed files
|
|
@@ -36,16 +36,30 @@
|
|
|
36
36
|
#
|
|
37
37
|
# kampodine vm-prepare --host root@<new-ip>
|
|
38
38
|
# ... --pull-images # + pre-pull the app image over the registry tunnel
|
|
39
|
-
# ... --ssh-key <path> # ssh identity (default: agent /
|
|
39
|
+
# ... --ssh-key <path> # ssh identity (default: agent / KAMPODINE_SSH_KEY)
|
|
40
40
|
set -euo pipefail
|
|
41
41
|
|
|
42
42
|
HOST=""
|
|
43
43
|
DO_PULL=0
|
|
44
|
-
SSH_KEY="${
|
|
44
|
+
SSH_KEY="${KAMPODINE_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 | KAMPODINE_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
|
|
@@ -73,7 +87,7 @@ if [[ "$(uname -s)" == "Darwin" ]]; then
|
|
|
73
87
|
fi
|
|
74
88
|
|
|
75
89
|
# --- managed file payloads (built Mac-side, scp'd) ----------------------------
|
|
76
|
-
TMPDIR_LOCAL="$(mktemp -d /tmp/
|
|
90
|
+
TMPDIR_LOCAL="$(mktemp -d /tmp/kampodine-vm-prepare.XXXXXX)"
|
|
77
91
|
trap 'rm -rf "$TMPDIR_LOCAL"' EXIT
|
|
78
92
|
|
|
79
93
|
cat > "$TMPDIR_LOCAL/registries.conf" <<'REGISTRIES'
|
|
@@ -88,17 +102,17 @@ location = "127.0.0.1:5000"
|
|
|
88
102
|
insecure = true
|
|
89
103
|
REGISTRIES
|
|
90
104
|
|
|
91
|
-
cat > "$TMPDIR_LOCAL/60-
|
|
105
|
+
cat > "$TMPDIR_LOCAL/60-kampodine.conf" <<'SYSCTL'
|
|
92
106
|
# Managed by packages/kampodine/scripts/vm-prepare.sh — do not hand-edit.
|
|
93
107
|
# OpenRC contract: let unprivileged/container paths bind port 80
|
|
94
108
|
# (kamal-proxy publishes 80+443; belt-and-braces for a future rootless move).
|
|
95
109
|
net.ipv4.ip_unprivileged_port_start=80
|
|
96
110
|
SYSCTL
|
|
97
111
|
|
|
98
|
-
cat > "$TMPDIR_LOCAL/
|
|
112
|
+
cat > "$TMPDIR_LOCAL/kampodine-api" <<'INITD_API'
|
|
99
113
|
#!/sbin/openrc-run
|
|
100
114
|
# Managed by packages/kampodine/scripts/vm-prepare.sh — do not hand-edit.
|
|
101
|
-
# KEEP IN SYNC with
|
|
115
|
+
# KEEP IN SYNC with the golden image's container-service init template
|
|
102
116
|
# (rendered with role defaults): vm-prepare bootstraps a fresh VM BEFORE the
|
|
103
117
|
# first ansible converge; ansible owns the file afterwards (drift repair).
|
|
104
118
|
#
|
|
@@ -108,7 +122,7 @@ cat > "$TMPDIR_LOCAL/esellar-api" <<'INITD_API'
|
|
|
108
122
|
# dies, the WHOLE run restarts against the local :latest (no pull inside the
|
|
109
123
|
# run — restarts are offline-safe; the registry tunnel is down between deploys).
|
|
110
124
|
#
|
|
111
|
-
# Env: /etc/
|
|
125
|
+
# Env: /etc/kampodine/env (0600 root, written by kampodine deploy per deploy) is
|
|
112
126
|
# the single SECRETS source (podman --env-file reads KEY=VALUE lines directly).
|
|
113
127
|
# The five CLEAR vars (NODE_ENV, PORT, LIBSQL_TENANT_DIR, LIBSQL_API_MOUNT,
|
|
114
128
|
# STATIC_SPA_MOUNT) are owned HERE via -e — deploy filters them out of the env
|
|
@@ -116,16 +130,16 @@ cat > "$TMPDIR_LOCAL/esellar-api" <<'INITD_API'
|
|
|
116
130
|
# (tenant sqlite files) — without the bind mount, tenant data would be
|
|
117
131
|
# container-ephemeral and lost on every restart.
|
|
118
132
|
|
|
119
|
-
name="
|
|
120
|
-
description="
|
|
133
|
+
name="kampodine-api"
|
|
134
|
+
description="kampodine API + SPA container (127.0.0.1:5000/kampodine-api:latest on 8080)"
|
|
121
135
|
|
|
122
136
|
supervisor=supervise-daemon
|
|
123
137
|
command="/usr/bin/podman"
|
|
124
|
-
command_args="run --rm --name
|
|
138
|
+
command_args="run --rm --name kampodine-api --network kamal -p 8080:8080"
|
|
125
139
|
command_args="$command_args -v /data/tenants:/data/tenants"
|
|
126
140
|
command_args="$command_args -e NODE_ENV=production -e PORT=8080"
|
|
127
141
|
command_args="$command_args -e LIBSQL_TENANT_DIR=/data/tenants -e LIBSQL_API_MOUNT=1 -e STATIC_SPA_MOUNT=1"
|
|
128
|
-
command_args="$command_args --env-file /etc/
|
|
142
|
+
command_args="$command_args --env-file /etc/kampodine/env 127.0.0.1:5000/kampodine-api:latest"
|
|
129
143
|
|
|
130
144
|
# Respawn forever (blue's restart=always precedent): a crash loop self-heals at
|
|
131
145
|
# the next deploy's restart; the 10s delay bounds log noise.
|
|
@@ -133,26 +147,26 @@ respawn_delay=10
|
|
|
133
147
|
respawn_max=0
|
|
134
148
|
|
|
135
149
|
# podman run's OWN stderr (missing image, name conflicts, netavark failures);
|
|
136
|
-
# container stdout/stderr go to `podman logs
|
|
137
|
-
supervise_daemon_args="--stderr /var/log/
|
|
150
|
+
# container stdout/stderr go to `podman logs kampodine-api`.
|
|
151
|
+
supervise_daemon_args="--stderr /var/log/kampodine-service.log"
|
|
138
152
|
|
|
139
153
|
depend() {
|
|
140
154
|
need net
|
|
141
|
-
after
|
|
155
|
+
after kampodine-datamount
|
|
142
156
|
}
|
|
143
157
|
|
|
144
158
|
start_pre() {
|
|
145
159
|
# SIGKILLed runs leave the container name holding port 8080 (--rm only
|
|
146
160
|
# cleans CLEAN exits) — sweep any leftover before the supervised run.
|
|
147
|
-
podman container exists
|
|
161
|
+
podman container exists kampodine-api 2>/dev/null && podman rm -f kampodine-api >/dev/null 2>&1
|
|
148
162
|
return 0
|
|
149
163
|
}
|
|
150
164
|
|
|
151
165
|
stop_post() {
|
|
152
166
|
# supervise-daemon killed the podman client; make sure the container goes
|
|
153
167
|
# down too (TERM -> graceful shutdown, then a tolerant sweep).
|
|
154
|
-
podman stop --time 10
|
|
155
|
-
podman rm -f --time 0
|
|
168
|
+
podman stop --time 10 kampodine-api >/dev/null 2>&1 || true
|
|
169
|
+
podman rm -f --time 0 kampodine-api >/dev/null 2>&1 || true
|
|
156
170
|
}
|
|
157
171
|
INITD_API
|
|
158
172
|
|
|
@@ -160,7 +174,7 @@ cat > "$TMPDIR_LOCAL/kamal-proxy" <<'INITD_PROXY'
|
|
|
160
174
|
#!/sbin/openrc-run
|
|
161
175
|
# Managed by packages/kampodine/scripts/vm-prepare.sh — do not hand-edit.
|
|
162
176
|
# KEEP IN SYNC with ansible/roles/container-service/templates/kamal-proxy.initd.j2
|
|
163
|
-
# (see the
|
|
177
|
+
# (see the kampodine-api header for the vm-prepare/ansible split).
|
|
164
178
|
#
|
|
165
179
|
# TLS edge (kamal-proxy, Let's Encrypt HTTP-01 on :80). Publishes 80+443; the
|
|
166
180
|
# LE certs + host->target registrations persist in the kamal-proxy-config named
|
|
@@ -168,7 +182,7 @@ cat > "$TMPDIR_LOCAL/kamal-proxy" <<'INITD_PROXY'
|
|
|
168
182
|
# the first deploy survives container recreation AND reboots. Start-fresh:
|
|
169
183
|
# nothing is carried from the retired blue VM. Target registration happens at
|
|
170
184
|
# deploy time (kampodine deploy: podman exec kamal-proxy kamal-proxy deploy
|
|
171
|
-
#
|
|
185
|
+
# kampodine-api --host=$PROXY_HOST --target=kampodine-api:8080 --tls
|
|
172
186
|
# --health-check-path=/api/auth/ok).
|
|
173
187
|
|
|
174
188
|
name="kamal-proxy"
|
|
@@ -198,19 +212,19 @@ stop_post() {
|
|
|
198
212
|
}
|
|
199
213
|
INITD_PROXY
|
|
200
214
|
|
|
201
|
-
cat > "$TMPDIR_LOCAL/
|
|
215
|
+
cat > "$TMPDIR_LOCAL/kampodine-anchor.sh" <<'ANCHOR_WATCHER'
|
|
202
216
|
#!/bin/sh
|
|
203
|
-
# KEEP IN SYNC with ansible/roles/container-service/files/
|
|
217
|
+
# KEEP IN SYNC with ansible/roles/container-service/files/kampodine-anchor.sh
|
|
204
218
|
# (vm-prepare rendered-content convention: vm-prepare bootstraps a fresh VM
|
|
205
219
|
# BEFORE the first ansible converge; ansible owns the file afterwards).
|
|
206
220
|
#
|
|
207
|
-
#
|
|
221
|
+
# kampodine-anchor.sh — guest half of the blue-green reserved-ip flip.
|
|
208
222
|
#
|
|
209
223
|
# OCI assigns the reserved PUBLIC ip to a SECONDARY private ip ("the anchor")
|
|
210
224
|
# on the instance VNIC, but this image has NO oracle-cloud-agent: nothing
|
|
211
225
|
# configures that private ip inside the guest, so packets to the reserved ip
|
|
212
226
|
# die until the address exists on the interface. `kampodine bluegreen flip` writes
|
|
213
|
-
# /etc/
|
|
227
|
+
# /etc/kampodine/anchor.conf over ssh at flip time; this watcher polls it and
|
|
214
228
|
# runs `ip addr add` within one interval. The unit is enabled+started on every
|
|
215
229
|
# VM by default and is INERT without the conf — a VM that never flips never
|
|
216
230
|
# touches its addresses.
|
|
@@ -222,13 +236,13 @@ cat > "$TMPDIR_LOCAL/esellar-anchor.sh" <<'ANCHOR_WATCHER'
|
|
|
222
236
|
# ADD-ONLY BY DESIGN: this script NEVER removes or flushes addresses (rollback
|
|
223
237
|
# cleanup is the flip tool's explicit ssh job). Idempotent: a present address
|
|
224
238
|
# is a no-op. Only state TRANSITIONS are logged (bounded log noise), to
|
|
225
|
-
# stdout — supervise-daemon tees it to /var/log/
|
|
239
|
+
# stdout — supervise-daemon tees it to /var/log/kampodine-anchor.log.
|
|
226
240
|
|
|
227
|
-
CONF="/etc/
|
|
228
|
-
INTERVAL="${
|
|
241
|
+
CONF="/etc/kampodine/anchor.conf"
|
|
242
|
+
INTERVAL="${KAMPODINE_ANCHOR_INTERVAL:-5}"
|
|
229
243
|
STATE="boot" # last logged transition (boot | idle | added | error)
|
|
230
244
|
|
|
231
|
-
log() { printf '%s
|
|
245
|
+
log() { printf '%s kampodine-anchor: %s\n' "$(date '+%Y-%m-%dT%H:%M:%S%z')" "$*"; }
|
|
232
246
|
|
|
233
247
|
# supervise-daemon SIGTERMs on stop — die cleanly with the current tick.
|
|
234
248
|
trap 'exit 0' TERM INT
|
|
@@ -269,33 +283,33 @@ while :; do
|
|
|
269
283
|
done
|
|
270
284
|
ANCHOR_WATCHER
|
|
271
285
|
|
|
272
|
-
cat > "$TMPDIR_LOCAL/
|
|
286
|
+
cat > "$TMPDIR_LOCAL/kampodine-anchor" <<'INITD_ANCHOR'
|
|
273
287
|
#!/sbin/openrc-run
|
|
274
288
|
# Managed by packages/kampodine/scripts/vm-prepare.sh — do not hand-edit.
|
|
275
|
-
# KEEP IN SYNC with ansible/roles/container-service/templates/
|
|
276
|
-
# (rendered with role defaults — see the
|
|
289
|
+
# KEEP IN SYNC with ansible/roles/container-service/templates/kampodine-anchor.initd.j2
|
|
290
|
+
# (rendered with role defaults — see the kampodine-api header for the
|
|
277
291
|
# vm-prepare/ansible split).
|
|
278
292
|
#
|
|
279
293
|
# Guest half of the blue-green reserved-ip flip: supervise-daemon runs the
|
|
280
|
-
#
|
|
294
|
+
# kampodine-anchor.sh watcher, which polls /etc/kampodine/anchor.conf (written by
|
|
281
295
|
# `kampodine bluegreen flip` over ssh at flip time) and `ip addr add`s the
|
|
282
296
|
# anchor address when it appears. Started+enabled on EVERY vm by default and
|
|
283
297
|
# INERT without the conf: a VM that never flips never touches its addresses —
|
|
284
|
-
# unlike the container units, there is no
|
|
285
|
-
# Transitions land in /var/log/
|
|
298
|
+
# unlike the container units, there is no kampodine_start_containers gate.
|
|
299
|
+
# Transitions land in /var/log/kampodine-anchor.log.
|
|
286
300
|
|
|
287
|
-
name="
|
|
301
|
+
name="kampodine-anchor"
|
|
288
302
|
description="Blue-green reserved-ip anchor address watcher (guest half of the flip)"
|
|
289
303
|
|
|
290
304
|
supervisor=supervise-daemon
|
|
291
|
-
command="/usr/local/sbin/
|
|
305
|
+
command="/usr/local/sbin/kampodine-anchor.sh"
|
|
292
306
|
|
|
293
307
|
# Respawn forever: the watcher itself never exits (trap TERM/INT -> exit 0 on
|
|
294
308
|
# stop), so a respawn means the script died abnormally — retry gently.
|
|
295
309
|
respawn_delay=5
|
|
296
310
|
respawn_max=0
|
|
297
311
|
|
|
298
|
-
supervise_daemon_args="--stdout /var/log/
|
|
312
|
+
supervise_daemon_args="--stdout /var/log/kampodine-anchor.log --stderr /var/log/kampodine-anchor.log"
|
|
299
313
|
|
|
300
314
|
depend() {
|
|
301
315
|
need net
|
|
@@ -352,7 +366,7 @@ say "sshd hardening: ensure drop-in + Include + restart (golden image predates t
|
|
|
352
366
|
vm 'grep -q "^Include /etc/ssh/sshd_config.d/\*.conf" /etc/ssh/sshd_config || sed -i "1i Include /etc/ssh/sshd_config.d/*.conf" /etc/ssh/sshd_config' \
|
|
353
367
|
|| die "could not ensure the sshd_config Include line"
|
|
354
368
|
vm 'mkdir -p /etc/ssh/sshd_config.d && chmod 700 /etc/ssh/sshd_config.d'
|
|
355
|
-
scp -q "${SSH_ARGS[@]}" "$TMPDIR_LOCAL/sshd-hardening.conf" "$HOST:/etc/ssh/sshd_config.d/99-
|
|
369
|
+
scp -q "${SSH_ARGS[@]}" "$TMPDIR_LOCAL/sshd-hardening.conf" "$HOST:/etc/ssh/sshd_config.d/99-kampodine-hardening.conf"
|
|
356
370
|
vm 'rc-service sshd restart' || die "sshd restart failed after hardening ensure"
|
|
357
371
|
|
|
358
372
|
say "gate: sshd hardening effective"
|
|
@@ -390,50 +404,50 @@ PODSTACK_GATE
|
|
|
390
404
|
vm 'rc-update show boot | grep -q cgroups || rc-update add cgroups boot' || die "rc-update cgroups failed"
|
|
391
405
|
vm 'rc-service cgroups status >/dev/null 2>&1 || rc-service cgroups start' || die "cgroups start failed"
|
|
392
406
|
|
|
393
|
-
# --- 3. /etc/
|
|
394
|
-
say "creating /etc/
|
|
395
|
-
vm 'mkdir -p /etc/
|
|
407
|
+
# --- 3. /etc/kampodine + managed config files ------------------------------------
|
|
408
|
+
say "creating /etc/kampodine (env file lands here via kampodine deploy, 0600 root)…"
|
|
409
|
+
vm 'mkdir -p /etc/kampodine && chmod 700 /etc/kampodine'
|
|
396
410
|
|
|
397
411
|
say "writing /etc/containers/registries.conf (insecure 127.0.0.1:5000; search docker.io)…"
|
|
398
412
|
vm 'mkdir -p /etc/containers'
|
|
399
413
|
scp -q "${SSH_ARGS[@]}" "$TMPDIR_LOCAL/registries.conf" "$HOST:/etc/containers/registries.conf"
|
|
400
414
|
|
|
401
|
-
say "writing /etc/sysctl.d/60-
|
|
402
|
-
scp -q "${SSH_ARGS[@]}" "$TMPDIR_LOCAL/60-
|
|
415
|
+
say "writing /etc/sysctl.d/60-kampodine.conf + applying live…"
|
|
416
|
+
scp -q "${SSH_ARGS[@]}" "$TMPDIR_LOCAL/60-kampodine.conf" "$HOST:/etc/sysctl.d/60-kampodine.conf"
|
|
403
417
|
vm_sh <<'SYSCTL_APPLY' || die "sysctl ensure/apply failed"
|
|
404
418
|
rc-update show boot | grep -Eq '^[[:space:]]*sysctl[[:space:]]*\|' || rc-update add sysctl boot
|
|
405
419
|
sysctl -w net.ipv4.ip_unprivileged_port_start=80 > /dev/null
|
|
406
420
|
SYSCTL_APPLY
|
|
407
421
|
|
|
408
422
|
# --- 4. OpenRC services (canonical: ansible container-service templates) --------
|
|
409
|
-
say "ensuring the 'kamal' podman network (deploy's proxy re-point resolves
|
|
423
|
+
say "ensuring the 'kamal' podman network (deploy's proxy re-point resolves kampodine-api:8080 by network DNS)…"
|
|
410
424
|
# shellcheck disable=SC2016
|
|
411
425
|
vm 'podman network exists kamal 2>/dev/null || podman network create kamal' \
|
|
412
426
|
|| die "podman network create kamal failed"
|
|
413
|
-
say "installing OpenRC services
|
|
414
|
-
scp -q "${SSH_ARGS[@]}" "$TMPDIR_LOCAL/
|
|
427
|
+
say "installing OpenRC services kampodine-api + kamal-proxy (supervise-daemon around podman run)…"
|
|
428
|
+
scp -q "${SSH_ARGS[@]}" "$TMPDIR_LOCAL/kampodine-api" "$HOST:/etc/init.d/kampodine-api"
|
|
415
429
|
scp -q "${SSH_ARGS[@]}" "$TMPDIR_LOCAL/kamal-proxy" "$HOST:/etc/init.d/kamal-proxy"
|
|
416
|
-
vm 'chmod 755 /etc/init.d/
|
|
430
|
+
vm 'chmod 755 /etc/init.d/kampodine-api /etc/init.d/kamal-proxy'
|
|
417
431
|
|
|
418
|
-
# --- 4b.
|
|
432
|
+
# --- 4b. kampodine-anchor (blue-green flip guest half — watcher, no container) ----
|
|
419
433
|
# Ships on EVERY vm: started + enabled now, INERT until a flip writes
|
|
420
|
-
# /etc/
|
|
434
|
+
# /etc/kampodine/anchor.conf over ssh (ansible container-service owns both files
|
|
421
435
|
# afterwards — drift repair keeps them in sync).
|
|
422
|
-
say "installing the
|
|
436
|
+
say "installing the kampodine-anchor watcher service (blue-green flip guest half)…"
|
|
423
437
|
# /usr/local/sbin does NOT exist on the golden image (fresh Alpine ships no
|
|
424
438
|
# /usr/local hierarchy; the alpine-base role creates it — vm-prepare runs
|
|
425
439
|
# BEFORE any ansible)
|
|
426
440
|
vm 'mkdir -p /usr/local/sbin' || die "mkdir /usr/local/sbin failed"
|
|
427
|
-
scp -q "${SSH_ARGS[@]}" "$TMPDIR_LOCAL/
|
|
428
|
-
scp -q "${SSH_ARGS[@]}" "$TMPDIR_LOCAL/
|
|
429
|
-
vm 'chmod 755 /usr/local/sbin/
|
|
430
|
-
vm 'sh -n /usr/local/sbin/
|
|
431
|
-
vm 'sh -n /etc/init.d/
|
|
432
|
-
vm 'rc-update show default | grep -qE "^[[:space:]]*
|
|
433
|
-
|| die "rc-update add
|
|
434
|
-
vm 'rc-service
|
|
435
|
-
|
|
436
|
-
for SVC in
|
|
441
|
+
scp -q "${SSH_ARGS[@]}" "$TMPDIR_LOCAL/kampodine-anchor.sh" "$HOST:/usr/local/sbin/kampodine-anchor.sh"
|
|
442
|
+
scp -q "${SSH_ARGS[@]}" "$TMPDIR_LOCAL/kampodine-anchor" "$HOST:/etc/init.d/kampodine-anchor"
|
|
443
|
+
vm 'chmod 755 /usr/local/sbin/kampodine-anchor.sh /etc/init.d/kampodine-anchor' || die "chmod kampodine-anchor files failed"
|
|
444
|
+
vm 'sh -n /usr/local/sbin/kampodine-anchor.sh' || die "kampodine-anchor.sh does not parse (busybox sh)"
|
|
445
|
+
vm 'sh -n /etc/init.d/kampodine-anchor' || die "/etc/init.d/kampodine-anchor does not parse"
|
|
446
|
+
vm 'rc-update show default | grep -qE "^[[:space:]]*kampodine-anchor[[:space:]]*\\|" || rc-update add kampodine-anchor default' \
|
|
447
|
+
|| die "rc-update add kampodine-anchor default failed"
|
|
448
|
+
vm 'rc-service kampodine-anchor start' || die "rc-service kampodine-anchor start failed"
|
|
449
|
+
|
|
450
|
+
for SVC in kampodine-api kamal-proxy; do
|
|
437
451
|
vm "rc-update show default | grep -qE '^[[:space:]]*${SVC}[[:space:]]*\\|' || rc-update add ${SVC} default" \
|
|
438
452
|
|| die "rc-update add $SVC default failed"
|
|
439
453
|
done
|
|
@@ -442,7 +456,7 @@ done
|
|
|
442
456
|
# re-runs after a deploy must not bounce production.
|
|
443
457
|
# shellcheck disable=SC2016
|
|
444
458
|
vm_sh <<'UNIT_DRIFT' || true
|
|
445
|
-
for SVC in
|
|
459
|
+
for SVC in kampodine-api kamal-proxy kampodine-anchor; do
|
|
446
460
|
if rc-service "$SVC" status > /dev/null 2>&1; then
|
|
447
461
|
echo "RUNNING: $SVC (unit file was overwritten — rc-service $SVC restart to apply, on your call)"
|
|
448
462
|
fi
|
|
@@ -469,26 +483,26 @@ if [[ $DO_PULL -eq 1 ]]; then
|
|
|
469
483
|
# installs it later; vm-prepare runs BEFORE any ansible)
|
|
470
484
|
vm 'busybox wget -q -O /dev/null http://127.0.0.1:5000/v2/' || {
|
|
471
485
|
pkill -f "ssh.*-R 5000" 2>/dev/null || true; sleep 1
|
|
472
|
-
nohup ssh -R 5000:127.0.0.1:5000 -N -o ServerAliveInterval=30 -o ExitOnForwardFailure=yes "$HOST" >/tmp/
|
|
486
|
+
nohup ssh -R 5000:127.0.0.1:5000 -N -o ServerAliveInterval=30 -o ExitOnForwardFailure=yes "$HOST" >/tmp/kampodine-tunnel.log 2>&1 &
|
|
473
487
|
sleep 3
|
|
474
488
|
}
|
|
475
|
-
vm 'busybox wget -q -O /dev/null http://127.0.0.1:5000/v2/' || die "registry tunnel did not come up (/tmp/
|
|
476
|
-
vm 'podman pull --tls-verify=false 127.0.0.1:5000/
|
|
489
|
+
vm 'busybox wget -q -O /dev/null http://127.0.0.1:5000/v2/' || die "registry tunnel did not come up (/tmp/kampodine-tunnel.log)"
|
|
490
|
+
vm 'podman pull --tls-verify=false 127.0.0.1:5000/kampodine-api:latest' || die "app image pull failed"
|
|
477
491
|
else
|
|
478
492
|
say "skipping app-image pre-pull (pass --pull-images, or let the first deploy pull)"
|
|
479
493
|
fi
|
|
480
494
|
|
|
481
|
-
# --- 7. converge
|
|
495
|
+
# --- 7. converge kampodine-api ONLY if a previous deploy left image + env ---------
|
|
482
496
|
# On a truly fresh VM neither exists — the FIRST DEPLOY provides both and
|
|
483
497
|
# starts the service. Re-runs of this script after a deploy heal drift.
|
|
484
|
-
say "
|
|
498
|
+
say "kampodine-api start check (needs image + /etc/kampodine/env — first deploy provides both)…"
|
|
485
499
|
# shellcheck disable=SC2016
|
|
486
|
-
vm_sh <<'API_CONVERGE' || die "
|
|
487
|
-
if [ -f /etc/
|
|
488
|
-
rc-service
|
|
489
|
-
echo "
|
|
500
|
+
vm_sh <<'API_CONVERGE' || die "kampodine-api start failed (image + env present — investigate: podman logs kampodine-api)"
|
|
501
|
+
if [ -f /etc/kampodine/env ] && podman image exists 127.0.0.1:5000/kampodine-api:latest; then
|
|
502
|
+
rc-service kampodine-api start
|
|
503
|
+
echo "kampodine-api started (image + env present)"
|
|
490
504
|
else
|
|
491
|
-
echo "
|
|
505
|
+
echo "kampodine-api deferred: no image and/or /etc/kampodine/env yet (normal on a fresh VM — first deploy handles it)"
|
|
492
506
|
fi
|
|
493
507
|
API_CONVERGE
|
|
494
508
|
|
|
@@ -508,16 +522,16 @@ vm 'grep -q "127.0.0.1:5000" /etc/containers/registries.conf && grep -q "insecur
|
|
|
508
522
|
|| die "registries.conf incomplete (127.0.0.1:5000 insecure + docker.io search)"
|
|
509
523
|
|
|
510
524
|
say "gate: services enabled in the default runlevel"
|
|
511
|
-
vm 'rc-update show default | grep -qE "^[[:space:]]*
|
|
525
|
+
vm 'rc-update show default | grep -qE "^[[:space:]]*kampodine-api[[:space:]]*\\|"' || die "kampodine-api not in the default runlevel"
|
|
512
526
|
vm 'rc-update show default | grep -qE "^[[:space:]]*kamal-proxy[[:space:]]*\\|"' || die "kamal-proxy not in the default runlevel"
|
|
513
527
|
vm 'rc-service kamal-proxy status >/dev/null' || die "kamal-proxy service not started"
|
|
514
528
|
|
|
515
|
-
say "gate:
|
|
516
|
-
vm 'rc-update show default | grep -qE "^[[:space:]]*
|
|
517
|
-
vm 'rc-service
|
|
518
|
-
vm '! test -e /etc/
|
|
519
|
-
vm 'grep -q "no anchor conf" /var/log/
|
|
520
|
-
|| die "
|
|
529
|
+
say "gate: kampodine-anchor watcher running + INERT (no anchor conf on a fresh VM)"
|
|
530
|
+
vm 'rc-update show default | grep -qE "^[[:space:]]*kampodine-anchor[[:space:]]*\\|"' || die "kampodine-anchor not in the default runlevel"
|
|
531
|
+
vm 'rc-service kampodine-anchor status >/dev/null' || die "kampodine-anchor service not started"
|
|
532
|
+
vm '! test -e /etc/kampodine/anchor.conf' || die "/etc/kampodine/anchor.conf already exists on a fresh VM (wrong machine?)"
|
|
533
|
+
vm 'grep -q "no anchor conf" /var/log/kampodine-anchor.log' \
|
|
534
|
+
|| die "kampodine-anchor is started but never logged its idle transition (watcher loop not running?)"
|
|
521
535
|
|
|
522
536
|
# --- summary ---------------------------------------------------------------------
|
|
523
537
|
IP="${HOST#*@}"
|