@mriekki/lean 0.1.0 → 0.2.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/README.md +71 -67
- package/assets/components.json +33 -33
- package/assets/core/distribution.json +52 -51
- package/assets/deployment/distribution.json +34 -34
- package/assets/generated/ASSEMBLY.json +38 -33
- package/assets/generated/core/core/skills/core-sync/SKILL.md +78 -120
- package/assets/generated/core/core/skills/deploy/SKILL.md +74 -91
- package/assets/generated/core/core/skills/lean-update/SKILL.md +189 -0
- package/assets/generated/core/core/skills/upstream-fix/SKILL.md +91 -76
- package/assets/generated/deployment/distribution/lean-poc-deploy.py +8 -8
- package/assets/generated/deployment/src/lean_poc_deployment/__init__.py +3 -3
- package/assets/generated/deployment/src/lean_poc_deployment/__main__.py +3 -3
- package/assets/generated/deployment/src/lean_poc_deployment/app_cli.py +140 -140
- package/assets/generated/deployment/src/lean_poc_deployment/app_runtime.py +455 -455
- package/assets/generated/deployment/src/lean_poc_deployment/app_runtime_remote.py +415 -415
- package/assets/generated/deployment/src/lean_poc_deployment/box_platform.py +208 -208
- package/assets/generated/deployment/src/lean_poc_deployment/box_platform_assets.py +100 -100
- package/assets/generated/deployment/src/lean_poc_deployment/box_platform_remote.py +271 -271
- package/assets/generated/deployment/src/lean_poc_deployment/cli.py +437 -437
- package/assets/generated/deployment/src/lean_poc_deployment/doctor.py +121 -121
- package/assets/generated/deployment/src/lean_poc_deployment/entrypoint.py +25 -25
- package/assets/generated/deployment/src/lean_poc_deployment/errors.py +5 -5
- package/assets/generated/deployment/src/lean_poc_deployment/keys.py +171 -171
- package/assets/generated/deployment/src/lean_poc_deployment/profiles.py +204 -204
- package/assets/generated/deployment/src/lean_poc_deployment/route_registry.py +367 -367
- package/assets/generated/deployment/src/lean_poc_deployment/route_registry_remote.py +443 -443
- package/assets/generated/deployment/src/lean_poc_deployment/security.py +290 -290
- package/assets/generated/deployment/src/lean_poc_deployment/sqlite_adapter.py +334 -334
- package/assets/generated/deployment/src/lean_poc_deployment/sqlite_adapter_remote.py +339 -339
- package/assets/generated/deployment/src/lean_poc_deployment/sqlite_cli.py +85 -85
- package/assets/generated/deployment/src/lean_poc_deployment/sqlite_import_recovery.py +437 -437
- package/assets/generated/deployment/src/lean_poc_deployment/ssh.py +118 -118
- package/assets/generated/deployment/src/lean_poc_deployment/teardown.py +286 -286
- package/assets/generated/deployment/src/lean_poc_deployment/teardown_cli.py +120 -120
- package/assets/generated/deployment/src/lean_poc_deployment/teardown_remote.py +100 -100
- package/assets/generated/deployment/src/lean_poc_deployment/trust.py +278 -278
- package/bin/lean.mjs +9 -9
- package/package.json +33 -33
- package/src/cli.mjs +131 -131
- package/src/engine.mjs +243 -243
- package/src/package-data.mjs +18 -18
|
@@ -1,340 +1,340 @@
|
|
|
1
|
-
"""Remote Bash template for the SQLite persistence adapter."""
|
|
2
|
-
|
|
3
|
-
REMOTE_SCRIPT = r'''#!/usr/bin/env bash
|
|
4
|
-
set -Eeuo pipefail
|
|
5
|
-
|
|
6
|
-
ACTION="$1"
|
|
7
|
-
EXPECTED_RESERVATION_SHA="${2:-}"
|
|
8
|
-
DEPLOY_USER="${3:-}"
|
|
9
|
-
APP_ROOT="__APP_ROOT__"
|
|
10
|
-
SQLITE_SCHEMA_VERSION="__SQLITE_SCHEMA_VERSION__"
|
|
11
|
-
OWNER_B64="__OWNER_B64__"
|
|
12
|
-
ENV_B64="__ENV_B64__"
|
|
13
|
-
APP_B64="__APP_B64__"
|
|
14
|
-
IMAGE_B64="__IMAGE_B64__"
|
|
15
|
-
RUN_AS_B64="__RUN_AS_B64__"
|
|
16
|
-
DB_FILE_B64="__DB_FILE_B64__"
|
|
17
|
-
SOURCE_DB_B64="__SOURCE_DB_B64__"
|
|
18
|
-
FIRST_INSTALL="__FIRST_INSTALL__"
|
|
19
|
-
RESERVATION_SHA="__RESERVATION_SHA__"
|
|
20
|
-
ENV_KEYS=(__ENV_KEYS__)
|
|
21
|
-
ENV_VALUES=(__ENV_VALUES__)
|
|
22
|
-
|
|
23
|
-
b64d() { printf '%s' "$1" | base64 --decode; }
|
|
24
|
-
meta_value() {
|
|
25
|
-
local path="$1" key="$2"
|
|
26
|
-
[[ -f "$path" ]] || return 1
|
|
27
|
-
sed -n "s/^${key}=//p" "$path" | head -n1
|
|
28
|
-
}
|
|
29
|
-
valid_slug() { [[ "$1" =~ ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$ ]]; }
|
|
30
|
-
valid_repo() { [[ "$1" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; }
|
|
31
|
-
valid_image() { [[ "$1" =~ ^[A-Za-z0-9][A-Za-z0-9._/:+-]*@sha256:[0-9a-f]{64}$ ]]; }
|
|
32
|
-
valid_run_as() { [[ "$1" =~ ^[0-9]+(:[0-9]+)?$ ]]; }
|
|
33
|
-
valid_db_file() { [[ "$1" =~ ^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$ && "$1" != "." && "$1" != ".." ]]; }
|
|
34
|
-
valid_env_key() { [[ "$1" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; }
|
|
35
|
-
|
|
36
|
-
configure_registry_auth() {
|
|
37
|
-
[[ -n "$DEPLOY_USER" && "$DEPLOY_USER" =~ ^[A-Za-z_][A-Za-z0-9_.-]{0,31}$ ]] || {
|
|
38
|
-
echo "Deployment account is invalid." >&2; exit 42;
|
|
39
|
-
}
|
|
40
|
-
local home config dir_mode file_mode deploy_uid
|
|
41
|
-
home="$(getent passwd "$DEPLOY_USER" | cut -d: -f6)"
|
|
42
|
-
[[ -n "$home" && "$home" == /* ]] || { echo "Could not resolve deployment account home." >&2; exit 42; }
|
|
43
|
-
config="$home/.docker"
|
|
44
|
-
if [[ ! -e "$config/config.json" ]]; then return 0; fi
|
|
45
|
-
[[ -d "$config" && ! -L "$config" && -f "$config/config.json" && ! -L "$config/config.json" ]] || {
|
|
46
|
-
echo "Docker credential path is not a regular protected directory/file." >&2; exit 42;
|
|
47
|
-
}
|
|
48
|
-
dir_mode="$(stat -c '%a' "$config")"
|
|
49
|
-
file_mode="$(stat -c '%a' "$config/config.json")"
|
|
50
|
-
deploy_uid="$(id -u "$DEPLOY_USER")"
|
|
51
|
-
if (( (8#$dir_mode & 077) != 0 || (8#$file_mode & 077) != 0 )) ||
|
|
52
|
-
[[ "$(stat -c '%u' "$config")" != "$deploy_uid" || "$(stat -c '%u' "$config/config.json")" != "$deploy_uid" ]]; then
|
|
53
|
-
echo "Docker credentials must be owned by the deployment account and deny group/other access." >&2
|
|
54
|
-
exit 42
|
|
55
|
-
fi
|
|
56
|
-
export DOCKER_CONFIG="$config"
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
managed_runtime_info() {
|
|
60
|
-
local root state current file
|
|
61
|
-
root="$1"
|
|
62
|
-
state="$root/deployment.env"
|
|
63
|
-
MANAGED_CONTAINER=""
|
|
64
|
-
MANAGED_IMAGE=""
|
|
65
|
-
MANAGED_RUNNING=false
|
|
66
|
-
[[ -f "$state" && ! -L "$state" ]] || return 0
|
|
67
|
-
current="$(meta_value "$state" CURRENT_RELEASE || true)"
|
|
68
|
-
[[ -n "$current" ]] || { echo "Managed application state is malformed." >&2; exit 43; }
|
|
69
|
-
file="$root/releases/$current/release.env"
|
|
70
|
-
[[ -f "$file" && ! -L "$file" ]] || { echo "Managed application release metadata is missing." >&2; exit 43; }
|
|
71
|
-
MANAGED_CONTAINER="$(meta_value "$file" CONTAINER_NAME || true)"
|
|
72
|
-
MANAGED_IMAGE="$(meta_value "$file" IMAGE || true)"
|
|
73
|
-
[[ -n "$MANAGED_CONTAINER" && -n "$MANAGED_IMAGE" ]] || { echo "Managed application release metadata is malformed." >&2; exit 43; }
|
|
74
|
-
[[ "$(docker inspect --format '{{.State.Status}}' "$MANAGED_CONTAINER" 2>/dev/null || true)" == "running" ]] && MANAGED_RUNNING=true || true
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
inventory() {
|
|
78
|
-
local marker root owner env app run_as db_b64 db file present bytes backups running image count=0 sha
|
|
79
|
-
if [[ -d "$APP_ROOT" ]]; then
|
|
80
|
-
while IFS= read -r marker; do
|
|
81
|
-
[[ -n "$marker" ]] || continue
|
|
82
|
-
[[ ! -L "$marker" ]] || { echo "SQLite reservation marker must not be a symbolic link: $marker" >&2; exit 44; }
|
|
83
|
-
[[ "$(meta_value "$marker" ADAPTER || true)" == "sqlite" ]] || continue
|
|
84
|
-
owner="$(meta_value "$marker" OWNER_REPO || true)"
|
|
85
|
-
env="$(meta_value "$marker" ENVIRONMENT || true)"
|
|
86
|
-
app="$(meta_value "$marker" APP_SLUG || true)"
|
|
87
|
-
run_as="$(meta_value "$marker" RUN_AS || true)"
|
|
88
|
-
db_b64="$(meta_value "$marker" DATABASE_FILE_B64 || true)"
|
|
89
|
-
db="$(b64d "$db_b64")"
|
|
90
|
-
sha="$(meta_value "$marker" RESERVATION_SHA256 || true)"
|
|
91
|
-
[[ -n "$owner" && -n "$env" && -n "$app" && -n "$run_as" && -n "$db" && "$sha" =~ ^[0-9a-f]{64}$ ]] || {
|
|
92
|
-
echo "SQLite reservation marker is malformed: $marker" >&2; exit 44;
|
|
93
|
-
}
|
|
94
|
-
root="$(dirname "$marker")"
|
|
95
|
-
file="$root/data/$db"
|
|
96
|
-
present=false; bytes=0
|
|
97
|
-
if [[ -e "$file" ]]; then
|
|
98
|
-
[[ -f "$file" && ! -L "$file" ]] || { echo "Managed SQLite database is not a regular file: $file" >&2; exit 44; }
|
|
99
|
-
present=true
|
|
100
|
-
bytes="$(stat -c '%s' "$file")"
|
|
101
|
-
fi
|
|
102
|
-
backups=0
|
|
103
|
-
if [[ -d "$root/backups" && ! -L "$root/backups" ]]; then
|
|
104
|
-
backups="$(find "$root/backups" -maxdepth 1 -type f -printf '.' | wc -c)"
|
|
105
|
-
fi
|
|
106
|
-
managed_runtime_info "$root"
|
|
107
|
-
running="$MANAGED_RUNNING"; image="$MANAGED_IMAGE"
|
|
108
|
-
printf 'SQLITE\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
|
|
109
|
-
"$owner" "$env" "$app" "$run_as" "$db" "$sha" "$present" "$bytes" "$backups" "$running" "$image"
|
|
110
|
-
count=$((count + 1))
|
|
111
|
-
done < <(find "$APP_ROOT" -mindepth 3 -maxdepth 3 -type f -name persistence.env -print 2>/dev/null | sort)
|
|
112
|
-
fi
|
|
113
|
-
printf 'SUMMARY\tsqliteCount\t%s\n' "$count"
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
decode_spec() {
|
|
117
|
-
OWNER="$(b64d "$OWNER_B64")"
|
|
118
|
-
ENVIRONMENT="$(b64d "$ENV_B64")"
|
|
119
|
-
APP_SLUG="$(b64d "$APP_B64")"
|
|
120
|
-
IMAGE="$(b64d "$IMAGE_B64")"
|
|
121
|
-
RUN_AS="$(b64d "$RUN_AS_B64")"
|
|
122
|
-
DATABASE_FILE="$(b64d "$DB_FILE_B64")"
|
|
123
|
-
SOURCE_DATABASE="$(b64d "$SOURCE_DB_B64")"
|
|
124
|
-
valid_repo "$OWNER" || { echo "Invalid SQLite owner repository." >&2; exit 45; }
|
|
125
|
-
valid_slug "$ENVIRONMENT" || { echo "Invalid SQLite environment." >&2; exit 45; }
|
|
126
|
-
valid_slug "$APP_SLUG" || { echo "Invalid SQLite app slug." >&2; exit 45; }
|
|
127
|
-
valid_image "$IMAGE" || { echo "SQLite maintenance image is not an immutable digest reference." >&2; exit 45; }
|
|
128
|
-
valid_run_as "$RUN_AS" || { echo "Invalid SQLite runAs." >&2; exit 45; }
|
|
129
|
-
valid_db_file "$DATABASE_FILE" || { echo "Invalid SQLite database file name." >&2; exit 45; }
|
|
130
|
-
[[ "$RESERVATION_SHA" =~ ^[0-9a-f]{64}$ ]] || { echo "Invalid SQLite reservation fingerprint." >&2; exit 45; }
|
|
131
|
-
[[ "${#ENV_KEYS[@]}" -eq "${#ENV_VALUES[@]}" && "${#ENV_KEYS[@]}" -gt 0 ]] || { echo "Invalid SQLite maintenance environment payload." >&2; exit 45; }
|
|
132
|
-
local index key
|
|
133
|
-
for index in "${!ENV_KEYS[@]}"; do
|
|
134
|
-
key="$(b64d "${ENV_KEYS[$index]}")"
|
|
135
|
-
valid_env_key "$key" || { echo "Invalid SQLite maintenance environment key." >&2; exit 45; }
|
|
136
|
-
done
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
assert_marker() {
|
|
140
|
-
local marker="$1" expected="$2"
|
|
141
|
-
[[ -f "$marker" && ! -L "$marker" ]] || { echo "SQLite reservation is missing." >&2; exit 46; }
|
|
142
|
-
[[ "$(meta_value "$marker" ADAPTER || true)" == "sqlite" &&
|
|
143
|
-
"$(meta_value "$marker" OWNER_REPO || true)" == "$OWNER" &&
|
|
144
|
-
"$(meta_value "$marker" ENVIRONMENT || true)" == "$ENVIRONMENT" &&
|
|
145
|
-
"$(meta_value "$marker" APP_SLUG || true)" == "$APP_SLUG" &&
|
|
146
|
-
"$(meta_value "$marker" RUN_AS || true)" == "$RUN_AS" &&
|
|
147
|
-
"$(b64d "$(meta_value "$marker" DATABASE_FILE_B64 || true)")" == "$DATABASE_FILE" &&
|
|
148
|
-
"$(meta_value "$marker" RESERVATION_SHA256 || true)" == "$RESERVATION_SHA" ]] || {
|
|
149
|
-
echo "SQLite reservation ownership/configuration mismatch." >&2; exit 46;
|
|
150
|
-
}
|
|
151
|
-
[[ "$expected" != "NONE" && "$expected" == "$RESERVATION_SHA" ]] || {
|
|
152
|
-
echo "SQLite reservation changed after planning." >&2; exit 47;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
write_marker() {
|
|
157
|
-
local marker tmp
|
|
158
|
-
marker="$1"
|
|
159
|
-
tmp="$marker.tmp.$$"
|
|
160
|
-
{
|
|
161
|
-
printf 'SCHEMA_VERSION=%s\n' "$SQLITE_SCHEMA_VERSION"
|
|
162
|
-
printf 'ADAPTER=sqlite\n'
|
|
163
|
-
printf 'OWNER_REPO=%s\n' "$OWNER"
|
|
164
|
-
printf 'ENVIRONMENT=%s\n' "$ENVIRONMENT"
|
|
165
|
-
printf 'APP_SLUG=%s\n' "$APP_SLUG"
|
|
166
|
-
printf 'RUN_AS=%s\n' "$RUN_AS"
|
|
167
|
-
printf 'DATABASE_FILE_B64=%s\n' "$DB_FILE_B64"
|
|
168
|
-
printf 'RESERVATION_SHA256=%s\n' "$RESERVATION_SHA"
|
|
169
|
-
} > "$tmp"
|
|
170
|
-
chmod 0600 "$tmp"
|
|
171
|
-
mv -f "$tmp" "$marker"
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
maintenance_args() {
|
|
175
|
-
local -n out_ref=$1
|
|
176
|
-
local index key value
|
|
177
|
-
out_ref=(docker run --rm --network none --user "$RUN_AS")
|
|
178
|
-
for index in "${!ENV_KEYS[@]}"; do
|
|
179
|
-
key="$(b64d "${ENV_KEYS[$index]}")"
|
|
180
|
-
value="$(b64d "${ENV_VALUES[$index]}")"
|
|
181
|
-
out_ref+=(--env "$key=$value")
|
|
182
|
-
done
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
run_backup() {
|
|
186
|
-
local source_dir="$1" backup_dir="$2" backup_name="$3"
|
|
187
|
-
local -a args
|
|
188
|
-
maintenance_args args
|
|
189
|
-
args+=(--mount "type=bind,src=$source_dir,dst=/data" --mount "type=bind,src=$backup_dir,dst=/backup" "$IMAGE" --backup --backup-path "/backup/$backup_name")
|
|
190
|
-
"${args[@]}"
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
run_check() {
|
|
194
|
-
local data_dir="$1"
|
|
195
|
-
local -a args
|
|
196
|
-
maintenance_args args
|
|
197
|
-
args+=(--mount "type=bind,src=$data_dir,dst=/data" "$IMAGE" --check-rollback-compatibility)
|
|
198
|
-
"${args[@]}"
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
run_migrate() {
|
|
202
|
-
local data_dir="$1"
|
|
203
|
-
local -a args
|
|
204
|
-
maintenance_args args
|
|
205
|
-
args+=(--mount "type=bind,src=$data_dir,dst=/data" "$IMAGE" --migrate)
|
|
206
|
-
[[ "$FIRST_INSTALL" == true ]] && args+=(--first-install)
|
|
207
|
-
"${args[@]}"
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
record_evidence() {
|
|
211
|
-
local root="$1" action="$2" outcome="$3" artifact="${4:-}" timestamp
|
|
212
|
-
timestamp="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
|
|
213
|
-
printf '{"timestamp":"%s","action":"%s","ownerRepo":"%s","environment":"%s","appSlug":"%s","image":"%s","databaseFile":"%s","reservationSha256":"%s","artifact":"%s","outcome":"%s"}\n' \
|
|
214
|
-
"$timestamp" "$action" "$OWNER" "$ENVIRONMENT" "$APP_SLUG" "$IMAGE" "$DATABASE_FILE" "$RESERVATION_SHA" "$artifact" "$outcome" >> "$root/sqlite-evidence.jsonl"
|
|
215
|
-
chmod 0600 "$root/sqlite-evidence.jsonl"
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
set_paths() {
|
|
219
|
-
ROOT="$APP_ROOT/$APP_SLUG/$ENVIRONMENT"
|
|
220
|
-
DATA="$ROOT/data"
|
|
221
|
-
BACKUPS="$ROOT/backups"
|
|
222
|
-
MARKER="$ROOT/persistence.env"
|
|
223
|
-
LOCK_FILE="/run/lock/lean-poc-app-$APP_SLUG--$ENVIRONMENT.lock"
|
|
224
|
-
for path in "$ROOT" "$DATA" "$BACKUPS" "$MARKER" "$ROOT/deployment.env" "$ROOT/sqlite-evidence.jsonl"; do
|
|
225
|
-
[[ ! -L "$path" ]] || { echo "SQLite managed path must not be a symbolic link: $path" >&2; exit 48; }
|
|
226
|
-
done
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
expected_ids() {
|
|
230
|
-
EXPECTED_UID="${RUN_AS%%:*}"
|
|
231
|
-
[[ "$RUN_AS" == *:* ]] && EXPECTED_GID="${RUN_AS#*:}" || EXPECTED_GID="$EXPECTED_UID"
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
create_reserved_dirs() {
|
|
235
|
-
expected_ids
|
|
236
|
-
install -d -m 0750 "$APP_ROOT" "$APP_ROOT/$APP_SLUG" "$ROOT"
|
|
237
|
-
if [[ ! -e "$DATA" ]]; then install -d -m 0750 -o "$EXPECTED_UID" -g "$EXPECTED_GID" "$DATA"; fi
|
|
238
|
-
if [[ ! -e "$BACKUPS" ]]; then install -d -m 0750 -o "$EXPECTED_UID" -g "$EXPECTED_GID" "$BACKUPS"; fi
|
|
239
|
-
verify_reserved_dirs
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
verify_reserved_dirs() {
|
|
243
|
-
expected_ids
|
|
244
|
-
[[ -d "$DATA" && ! -L "$DATA" && -d "$BACKUPS" && ! -L "$BACKUPS" ]] || {
|
|
245
|
-
echo "SQLite reserved data/backup directories are missing or invalid." >&2; exit 48;
|
|
246
|
-
}
|
|
247
|
-
[[ "$(stat -c '%u' "$DATA")" == "$EXPECTED_UID" && "$(stat -c '%g' "$DATA")" == "$EXPECTED_GID" &&
|
|
248
|
-
"$(stat -c '%u' "$BACKUPS")" == "$EXPECTED_UID" && "$(stat -c '%g' "$BACKUPS")" == "$EXPECTED_GID" ]] || {
|
|
249
|
-
echo "SQLite data/backup directory ownership does not match runAs." >&2; exit 48;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
operate() {
|
|
254
|
-
decode_spec
|
|
255
|
-
configure_registry_auth
|
|
256
|
-
set_paths
|
|
257
|
-
exec 9>>"$LOCK_FILE"
|
|
258
|
-
flock -n 9 || { echo "Another app/persistence operation holds the lock for $APP_SLUG--$ENVIRONMENT." >&2; exit 75; }
|
|
259
|
-
|
|
260
|
-
local db="$DATA/$DATABASE_FILE" stamp backup_name source_real source_parent
|
|
261
|
-
managed_runtime_info "$ROOT"
|
|
262
|
-
|
|
263
|
-
case "$ACTION" in
|
|
264
|
-
import)
|
|
265
|
-
[[ "$EXPECTED_RESERVATION_SHA" == "NONE" && ! -e "$MARKER" ]] || { echo "SQLite import destination was reserved after planning." >&2; exit 47; }
|
|
266
|
-
[[ ! -e "$ROOT/deployment.env" ]] || { echo "SQLite import must occur before the first managed application deployment." >&2; exit 49; }
|
|
267
|
-
[[ ! -e "$db" ]] || { echo "SQLite import refuses to overwrite an existing destination database." >&2; exit 49; }
|
|
268
|
-
[[ "$SOURCE_DATABASE" == /srv/* && "$SOURCE_DATABASE" != "$APP_ROOT"/* ]] || { echo "SQLite import source must be outside the Kit app root under /srv." >&2; exit 49; }
|
|
269
|
-
source_real="$(realpath -e "$SOURCE_DATABASE" 2>/dev/null || true)"
|
|
270
|
-
[[ "$source_real" == "$SOURCE_DATABASE" && -f "$SOURCE_DATABASE" && ! -L "$SOURCE_DATABASE" ]] || { echo "SQLite import source must be an existing non-symlink regular file with no symlinked path components." >&2; exit 49; }
|
|
271
|
-
[[ "$(basename "$SOURCE_DATABASE")" == "$DATABASE_FILE" ]] || { echo "SQLite import source file name mismatch." >&2; exit 49; }
|
|
272
|
-
create_reserved_dirs
|
|
273
|
-
docker pull "$IMAGE" >/dev/null
|
|
274
|
-
source_parent="$(dirname "$SOURCE_DATABASE")"
|
|
275
|
-
run_backup "$source_parent" "$DATA" "$DATABASE_FILE"
|
|
276
|
-
run_check "$DATA"
|
|
277
|
-
write_marker "$MARKER"
|
|
278
|
-
record_evidence "$ROOT" import imported "$SOURCE_DATABASE"
|
|
279
|
-
printf 'SQLite import completed and verified: %s\n' "$db"
|
|
280
|
-
;;
|
|
281
|
-
backup)
|
|
282
|
-
assert_marker "$MARKER" "$EXPECTED_RESERVATION_SHA"
|
|
283
|
-
verify_reserved_dirs
|
|
284
|
-
[[ -f "$db" && ! -L "$db" ]] || { echo "SQLite backup requires the reserved database file." >&2; exit 49; }
|
|
285
|
-
docker pull "$IMAGE" >/dev/null
|
|
286
|
-
stamp="$(date -u +'%Y%m%dT%H%M%SZ')"
|
|
287
|
-
backup_name="backup-$stamp-$DATABASE_FILE"
|
|
288
|
-
run_backup "$DATA" "$BACKUPS" "$backup_name"
|
|
289
|
-
record_evidence "$ROOT" backup backed-up "$backup_name"
|
|
290
|
-
printf 'SQLite verified backup created: %s\n' "$BACKUPS/$backup_name"
|
|
291
|
-
;;
|
|
292
|
-
migrate)
|
|
293
|
-
if [[ "$FIRST_INSTALL" == true ]]; then
|
|
294
|
-
[[ "$EXPECTED_RESERVATION_SHA" == "NONE" && ! -e "$MARKER" ]] || { echo "First-install SQLite destination was reserved after planning." >&2; exit 47; }
|
|
295
|
-
[[ ! -e "$ROOT/deployment.env" && ! -e "$db" ]] || { echo "First-install SQLite migration requires no runtime state or database." >&2; exit 49; }
|
|
296
|
-
create_reserved_dirs
|
|
297
|
-
docker pull "$IMAGE" >/dev/null
|
|
298
|
-
run_migrate "$DATA"
|
|
299
|
-
run_check "$DATA"
|
|
300
|
-
write_marker "$MARKER"
|
|
301
|
-
record_evidence "$ROOT" migrate migrated-first-install ""
|
|
302
|
-
printf 'SQLite first-install migration completed and verified.\n'
|
|
303
|
-
else
|
|
304
|
-
assert_marker "$MARKER" "$EXPECTED_RESERVATION_SHA"
|
|
305
|
-
verify_reserved_dirs
|
|
306
|
-
[[ -f "$db" && ! -L "$db" ]] || { echo "SQLite migration requires the reserved database file." >&2; exit 49; }
|
|
307
|
-
docker pull "$IMAGE" >/dev/null
|
|
308
|
-
stamp="$(date -u +'%Y%m%dT%H%M%SZ')"
|
|
309
|
-
backup_name="pre-migrate-$stamp-$DATABASE_FILE"
|
|
310
|
-
run_backup "$DATA" "$BACKUPS" "$backup_name"
|
|
311
|
-
if [[ "$MANAGED_RUNNING" == true ]]; then docker stop "$MANAGED_CONTAINER" >/dev/null; fi
|
|
312
|
-
if ! run_migrate "$DATA"; then
|
|
313
|
-
record_evidence "$ROOT" migrate failed "$backup_name"
|
|
314
|
-
echo "SQLite migration failed; verified pre-migration backup is retained and managed runtime remains stopped." >&2
|
|
315
|
-
exit 50
|
|
316
|
-
fi
|
|
317
|
-
run_check "$DATA"
|
|
318
|
-
record_evidence "$ROOT" migrate migrated "$backup_name"
|
|
319
|
-
printf 'SQLite migration completed; verified pre-migration backup: %s\n' "$BACKUPS/$backup_name"
|
|
320
|
-
fi
|
|
321
|
-
;;
|
|
322
|
-
rollback-check)
|
|
323
|
-
assert_marker "$MARKER" "$EXPECTED_RESERVATION_SHA"
|
|
324
|
-
verify_reserved_dirs
|
|
325
|
-
[[ -f "$db" && ! -L "$db" ]] || { echo "SQLite rollback check requires the reserved database file." >&2; exit 49; }
|
|
326
|
-
docker pull "$IMAGE" >/dev/null
|
|
327
|
-
run_check "$DATA"
|
|
328
|
-
record_evidence "$ROOT" rollback-check compatible ""
|
|
329
|
-
printf 'SQLite rollback compatibility check passed for image %s.\n' "$IMAGE"
|
|
330
|
-
;;
|
|
331
|
-
*) echo "Unsupported SQLite adapter action: $ACTION" >&2; exit 2 ;;
|
|
332
|
-
esac
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
case "$ACTION" in
|
|
336
|
-
inventory) inventory ;;
|
|
337
|
-
import|backup|migrate|rollback-check) operate ;;
|
|
338
|
-
*) echo "Unsupported SQLite adapter action: $ACTION" >&2; exit 2 ;;
|
|
339
|
-
esac
|
|
1
|
+
"""Remote Bash template for the SQLite persistence adapter."""
|
|
2
|
+
|
|
3
|
+
REMOTE_SCRIPT = r'''#!/usr/bin/env bash
|
|
4
|
+
set -Eeuo pipefail
|
|
5
|
+
|
|
6
|
+
ACTION="$1"
|
|
7
|
+
EXPECTED_RESERVATION_SHA="${2:-}"
|
|
8
|
+
DEPLOY_USER="${3:-}"
|
|
9
|
+
APP_ROOT="__APP_ROOT__"
|
|
10
|
+
SQLITE_SCHEMA_VERSION="__SQLITE_SCHEMA_VERSION__"
|
|
11
|
+
OWNER_B64="__OWNER_B64__"
|
|
12
|
+
ENV_B64="__ENV_B64__"
|
|
13
|
+
APP_B64="__APP_B64__"
|
|
14
|
+
IMAGE_B64="__IMAGE_B64__"
|
|
15
|
+
RUN_AS_B64="__RUN_AS_B64__"
|
|
16
|
+
DB_FILE_B64="__DB_FILE_B64__"
|
|
17
|
+
SOURCE_DB_B64="__SOURCE_DB_B64__"
|
|
18
|
+
FIRST_INSTALL="__FIRST_INSTALL__"
|
|
19
|
+
RESERVATION_SHA="__RESERVATION_SHA__"
|
|
20
|
+
ENV_KEYS=(__ENV_KEYS__)
|
|
21
|
+
ENV_VALUES=(__ENV_VALUES__)
|
|
22
|
+
|
|
23
|
+
b64d() { printf '%s' "$1" | base64 --decode; }
|
|
24
|
+
meta_value() {
|
|
25
|
+
local path="$1" key="$2"
|
|
26
|
+
[[ -f "$path" ]] || return 1
|
|
27
|
+
sed -n "s/^${key}=//p" "$path" | head -n1
|
|
28
|
+
}
|
|
29
|
+
valid_slug() { [[ "$1" =~ ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$ ]]; }
|
|
30
|
+
valid_repo() { [[ "$1" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; }
|
|
31
|
+
valid_image() { [[ "$1" =~ ^[A-Za-z0-9][A-Za-z0-9._/:+-]*@sha256:[0-9a-f]{64}$ ]]; }
|
|
32
|
+
valid_run_as() { [[ "$1" =~ ^[0-9]+(:[0-9]+)?$ ]]; }
|
|
33
|
+
valid_db_file() { [[ "$1" =~ ^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$ && "$1" != "." && "$1" != ".." ]]; }
|
|
34
|
+
valid_env_key() { [[ "$1" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; }
|
|
35
|
+
|
|
36
|
+
configure_registry_auth() {
|
|
37
|
+
[[ -n "$DEPLOY_USER" && "$DEPLOY_USER" =~ ^[A-Za-z_][A-Za-z0-9_.-]{0,31}$ ]] || {
|
|
38
|
+
echo "Deployment account is invalid." >&2; exit 42;
|
|
39
|
+
}
|
|
40
|
+
local home config dir_mode file_mode deploy_uid
|
|
41
|
+
home="$(getent passwd "$DEPLOY_USER" | cut -d: -f6)"
|
|
42
|
+
[[ -n "$home" && "$home" == /* ]] || { echo "Could not resolve deployment account home." >&2; exit 42; }
|
|
43
|
+
config="$home/.docker"
|
|
44
|
+
if [[ ! -e "$config/config.json" ]]; then return 0; fi
|
|
45
|
+
[[ -d "$config" && ! -L "$config" && -f "$config/config.json" && ! -L "$config/config.json" ]] || {
|
|
46
|
+
echo "Docker credential path is not a regular protected directory/file." >&2; exit 42;
|
|
47
|
+
}
|
|
48
|
+
dir_mode="$(stat -c '%a' "$config")"
|
|
49
|
+
file_mode="$(stat -c '%a' "$config/config.json")"
|
|
50
|
+
deploy_uid="$(id -u "$DEPLOY_USER")"
|
|
51
|
+
if (( (8#$dir_mode & 077) != 0 || (8#$file_mode & 077) != 0 )) ||
|
|
52
|
+
[[ "$(stat -c '%u' "$config")" != "$deploy_uid" || "$(stat -c '%u' "$config/config.json")" != "$deploy_uid" ]]; then
|
|
53
|
+
echo "Docker credentials must be owned by the deployment account and deny group/other access." >&2
|
|
54
|
+
exit 42
|
|
55
|
+
fi
|
|
56
|
+
export DOCKER_CONFIG="$config"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
managed_runtime_info() {
|
|
60
|
+
local root state current file
|
|
61
|
+
root="$1"
|
|
62
|
+
state="$root/deployment.env"
|
|
63
|
+
MANAGED_CONTAINER=""
|
|
64
|
+
MANAGED_IMAGE=""
|
|
65
|
+
MANAGED_RUNNING=false
|
|
66
|
+
[[ -f "$state" && ! -L "$state" ]] || return 0
|
|
67
|
+
current="$(meta_value "$state" CURRENT_RELEASE || true)"
|
|
68
|
+
[[ -n "$current" ]] || { echo "Managed application state is malformed." >&2; exit 43; }
|
|
69
|
+
file="$root/releases/$current/release.env"
|
|
70
|
+
[[ -f "$file" && ! -L "$file" ]] || { echo "Managed application release metadata is missing." >&2; exit 43; }
|
|
71
|
+
MANAGED_CONTAINER="$(meta_value "$file" CONTAINER_NAME || true)"
|
|
72
|
+
MANAGED_IMAGE="$(meta_value "$file" IMAGE || true)"
|
|
73
|
+
[[ -n "$MANAGED_CONTAINER" && -n "$MANAGED_IMAGE" ]] || { echo "Managed application release metadata is malformed." >&2; exit 43; }
|
|
74
|
+
[[ "$(docker inspect --format '{{.State.Status}}' "$MANAGED_CONTAINER" 2>/dev/null || true)" == "running" ]] && MANAGED_RUNNING=true || true
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
inventory() {
|
|
78
|
+
local marker root owner env app run_as db_b64 db file present bytes backups running image count=0 sha
|
|
79
|
+
if [[ -d "$APP_ROOT" ]]; then
|
|
80
|
+
while IFS= read -r marker; do
|
|
81
|
+
[[ -n "$marker" ]] || continue
|
|
82
|
+
[[ ! -L "$marker" ]] || { echo "SQLite reservation marker must not be a symbolic link: $marker" >&2; exit 44; }
|
|
83
|
+
[[ "$(meta_value "$marker" ADAPTER || true)" == "sqlite" ]] || continue
|
|
84
|
+
owner="$(meta_value "$marker" OWNER_REPO || true)"
|
|
85
|
+
env="$(meta_value "$marker" ENVIRONMENT || true)"
|
|
86
|
+
app="$(meta_value "$marker" APP_SLUG || true)"
|
|
87
|
+
run_as="$(meta_value "$marker" RUN_AS || true)"
|
|
88
|
+
db_b64="$(meta_value "$marker" DATABASE_FILE_B64 || true)"
|
|
89
|
+
db="$(b64d "$db_b64")"
|
|
90
|
+
sha="$(meta_value "$marker" RESERVATION_SHA256 || true)"
|
|
91
|
+
[[ -n "$owner" && -n "$env" && -n "$app" && -n "$run_as" && -n "$db" && "$sha" =~ ^[0-9a-f]{64}$ ]] || {
|
|
92
|
+
echo "SQLite reservation marker is malformed: $marker" >&2; exit 44;
|
|
93
|
+
}
|
|
94
|
+
root="$(dirname "$marker")"
|
|
95
|
+
file="$root/data/$db"
|
|
96
|
+
present=false; bytes=0
|
|
97
|
+
if [[ -e "$file" ]]; then
|
|
98
|
+
[[ -f "$file" && ! -L "$file" ]] || { echo "Managed SQLite database is not a regular file: $file" >&2; exit 44; }
|
|
99
|
+
present=true
|
|
100
|
+
bytes="$(stat -c '%s' "$file")"
|
|
101
|
+
fi
|
|
102
|
+
backups=0
|
|
103
|
+
if [[ -d "$root/backups" && ! -L "$root/backups" ]]; then
|
|
104
|
+
backups="$(find "$root/backups" -maxdepth 1 -type f -printf '.' | wc -c)"
|
|
105
|
+
fi
|
|
106
|
+
managed_runtime_info "$root"
|
|
107
|
+
running="$MANAGED_RUNNING"; image="$MANAGED_IMAGE"
|
|
108
|
+
printf 'SQLITE\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
|
|
109
|
+
"$owner" "$env" "$app" "$run_as" "$db" "$sha" "$present" "$bytes" "$backups" "$running" "$image"
|
|
110
|
+
count=$((count + 1))
|
|
111
|
+
done < <(find "$APP_ROOT" -mindepth 3 -maxdepth 3 -type f -name persistence.env -print 2>/dev/null | sort)
|
|
112
|
+
fi
|
|
113
|
+
printf 'SUMMARY\tsqliteCount\t%s\n' "$count"
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
decode_spec() {
|
|
117
|
+
OWNER="$(b64d "$OWNER_B64")"
|
|
118
|
+
ENVIRONMENT="$(b64d "$ENV_B64")"
|
|
119
|
+
APP_SLUG="$(b64d "$APP_B64")"
|
|
120
|
+
IMAGE="$(b64d "$IMAGE_B64")"
|
|
121
|
+
RUN_AS="$(b64d "$RUN_AS_B64")"
|
|
122
|
+
DATABASE_FILE="$(b64d "$DB_FILE_B64")"
|
|
123
|
+
SOURCE_DATABASE="$(b64d "$SOURCE_DB_B64")"
|
|
124
|
+
valid_repo "$OWNER" || { echo "Invalid SQLite owner repository." >&2; exit 45; }
|
|
125
|
+
valid_slug "$ENVIRONMENT" || { echo "Invalid SQLite environment." >&2; exit 45; }
|
|
126
|
+
valid_slug "$APP_SLUG" || { echo "Invalid SQLite app slug." >&2; exit 45; }
|
|
127
|
+
valid_image "$IMAGE" || { echo "SQLite maintenance image is not an immutable digest reference." >&2; exit 45; }
|
|
128
|
+
valid_run_as "$RUN_AS" || { echo "Invalid SQLite runAs." >&2; exit 45; }
|
|
129
|
+
valid_db_file "$DATABASE_FILE" || { echo "Invalid SQLite database file name." >&2; exit 45; }
|
|
130
|
+
[[ "$RESERVATION_SHA" =~ ^[0-9a-f]{64}$ ]] || { echo "Invalid SQLite reservation fingerprint." >&2; exit 45; }
|
|
131
|
+
[[ "${#ENV_KEYS[@]}" -eq "${#ENV_VALUES[@]}" && "${#ENV_KEYS[@]}" -gt 0 ]] || { echo "Invalid SQLite maintenance environment payload." >&2; exit 45; }
|
|
132
|
+
local index key
|
|
133
|
+
for index in "${!ENV_KEYS[@]}"; do
|
|
134
|
+
key="$(b64d "${ENV_KEYS[$index]}")"
|
|
135
|
+
valid_env_key "$key" || { echo "Invalid SQLite maintenance environment key." >&2; exit 45; }
|
|
136
|
+
done
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
assert_marker() {
|
|
140
|
+
local marker="$1" expected="$2"
|
|
141
|
+
[[ -f "$marker" && ! -L "$marker" ]] || { echo "SQLite reservation is missing." >&2; exit 46; }
|
|
142
|
+
[[ "$(meta_value "$marker" ADAPTER || true)" == "sqlite" &&
|
|
143
|
+
"$(meta_value "$marker" OWNER_REPO || true)" == "$OWNER" &&
|
|
144
|
+
"$(meta_value "$marker" ENVIRONMENT || true)" == "$ENVIRONMENT" &&
|
|
145
|
+
"$(meta_value "$marker" APP_SLUG || true)" == "$APP_SLUG" &&
|
|
146
|
+
"$(meta_value "$marker" RUN_AS || true)" == "$RUN_AS" &&
|
|
147
|
+
"$(b64d "$(meta_value "$marker" DATABASE_FILE_B64 || true)")" == "$DATABASE_FILE" &&
|
|
148
|
+
"$(meta_value "$marker" RESERVATION_SHA256 || true)" == "$RESERVATION_SHA" ]] || {
|
|
149
|
+
echo "SQLite reservation ownership/configuration mismatch." >&2; exit 46;
|
|
150
|
+
}
|
|
151
|
+
[[ "$expected" != "NONE" && "$expected" == "$RESERVATION_SHA" ]] || {
|
|
152
|
+
echo "SQLite reservation changed after planning." >&2; exit 47;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
write_marker() {
|
|
157
|
+
local marker tmp
|
|
158
|
+
marker="$1"
|
|
159
|
+
tmp="$marker.tmp.$$"
|
|
160
|
+
{
|
|
161
|
+
printf 'SCHEMA_VERSION=%s\n' "$SQLITE_SCHEMA_VERSION"
|
|
162
|
+
printf 'ADAPTER=sqlite\n'
|
|
163
|
+
printf 'OWNER_REPO=%s\n' "$OWNER"
|
|
164
|
+
printf 'ENVIRONMENT=%s\n' "$ENVIRONMENT"
|
|
165
|
+
printf 'APP_SLUG=%s\n' "$APP_SLUG"
|
|
166
|
+
printf 'RUN_AS=%s\n' "$RUN_AS"
|
|
167
|
+
printf 'DATABASE_FILE_B64=%s\n' "$DB_FILE_B64"
|
|
168
|
+
printf 'RESERVATION_SHA256=%s\n' "$RESERVATION_SHA"
|
|
169
|
+
} > "$tmp"
|
|
170
|
+
chmod 0600 "$tmp"
|
|
171
|
+
mv -f "$tmp" "$marker"
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
maintenance_args() {
|
|
175
|
+
local -n out_ref=$1
|
|
176
|
+
local index key value
|
|
177
|
+
out_ref=(docker run --rm --network none --user "$RUN_AS")
|
|
178
|
+
for index in "${!ENV_KEYS[@]}"; do
|
|
179
|
+
key="$(b64d "${ENV_KEYS[$index]}")"
|
|
180
|
+
value="$(b64d "${ENV_VALUES[$index]}")"
|
|
181
|
+
out_ref+=(--env "$key=$value")
|
|
182
|
+
done
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
run_backup() {
|
|
186
|
+
local source_dir="$1" backup_dir="$2" backup_name="$3"
|
|
187
|
+
local -a args
|
|
188
|
+
maintenance_args args
|
|
189
|
+
args+=(--mount "type=bind,src=$source_dir,dst=/data" --mount "type=bind,src=$backup_dir,dst=/backup" "$IMAGE" --backup --backup-path "/backup/$backup_name")
|
|
190
|
+
"${args[@]}"
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
run_check() {
|
|
194
|
+
local data_dir="$1"
|
|
195
|
+
local -a args
|
|
196
|
+
maintenance_args args
|
|
197
|
+
args+=(--mount "type=bind,src=$data_dir,dst=/data" "$IMAGE" --check-rollback-compatibility)
|
|
198
|
+
"${args[@]}"
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
run_migrate() {
|
|
202
|
+
local data_dir="$1"
|
|
203
|
+
local -a args
|
|
204
|
+
maintenance_args args
|
|
205
|
+
args+=(--mount "type=bind,src=$data_dir,dst=/data" "$IMAGE" --migrate)
|
|
206
|
+
[[ "$FIRST_INSTALL" == true ]] && args+=(--first-install)
|
|
207
|
+
"${args[@]}"
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
record_evidence() {
|
|
211
|
+
local root="$1" action="$2" outcome="$3" artifact="${4:-}" timestamp
|
|
212
|
+
timestamp="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
|
|
213
|
+
printf '{"timestamp":"%s","action":"%s","ownerRepo":"%s","environment":"%s","appSlug":"%s","image":"%s","databaseFile":"%s","reservationSha256":"%s","artifact":"%s","outcome":"%s"}\n' \
|
|
214
|
+
"$timestamp" "$action" "$OWNER" "$ENVIRONMENT" "$APP_SLUG" "$IMAGE" "$DATABASE_FILE" "$RESERVATION_SHA" "$artifact" "$outcome" >> "$root/sqlite-evidence.jsonl"
|
|
215
|
+
chmod 0600 "$root/sqlite-evidence.jsonl"
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
set_paths() {
|
|
219
|
+
ROOT="$APP_ROOT/$APP_SLUG/$ENVIRONMENT"
|
|
220
|
+
DATA="$ROOT/data"
|
|
221
|
+
BACKUPS="$ROOT/backups"
|
|
222
|
+
MARKER="$ROOT/persistence.env"
|
|
223
|
+
LOCK_FILE="/run/lock/lean-poc-app-$APP_SLUG--$ENVIRONMENT.lock"
|
|
224
|
+
for path in "$ROOT" "$DATA" "$BACKUPS" "$MARKER" "$ROOT/deployment.env" "$ROOT/sqlite-evidence.jsonl"; do
|
|
225
|
+
[[ ! -L "$path" ]] || { echo "SQLite managed path must not be a symbolic link: $path" >&2; exit 48; }
|
|
226
|
+
done
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
expected_ids() {
|
|
230
|
+
EXPECTED_UID="${RUN_AS%%:*}"
|
|
231
|
+
[[ "$RUN_AS" == *:* ]] && EXPECTED_GID="${RUN_AS#*:}" || EXPECTED_GID="$EXPECTED_UID"
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
create_reserved_dirs() {
|
|
235
|
+
expected_ids
|
|
236
|
+
install -d -m 0750 "$APP_ROOT" "$APP_ROOT/$APP_SLUG" "$ROOT"
|
|
237
|
+
if [[ ! -e "$DATA" ]]; then install -d -m 0750 -o "$EXPECTED_UID" -g "$EXPECTED_GID" "$DATA"; fi
|
|
238
|
+
if [[ ! -e "$BACKUPS" ]]; then install -d -m 0750 -o "$EXPECTED_UID" -g "$EXPECTED_GID" "$BACKUPS"; fi
|
|
239
|
+
verify_reserved_dirs
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
verify_reserved_dirs() {
|
|
243
|
+
expected_ids
|
|
244
|
+
[[ -d "$DATA" && ! -L "$DATA" && -d "$BACKUPS" && ! -L "$BACKUPS" ]] || {
|
|
245
|
+
echo "SQLite reserved data/backup directories are missing or invalid." >&2; exit 48;
|
|
246
|
+
}
|
|
247
|
+
[[ "$(stat -c '%u' "$DATA")" == "$EXPECTED_UID" && "$(stat -c '%g' "$DATA")" == "$EXPECTED_GID" &&
|
|
248
|
+
"$(stat -c '%u' "$BACKUPS")" == "$EXPECTED_UID" && "$(stat -c '%g' "$BACKUPS")" == "$EXPECTED_GID" ]] || {
|
|
249
|
+
echo "SQLite data/backup directory ownership does not match runAs." >&2; exit 48;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
operate() {
|
|
254
|
+
decode_spec
|
|
255
|
+
configure_registry_auth
|
|
256
|
+
set_paths
|
|
257
|
+
exec 9>>"$LOCK_FILE"
|
|
258
|
+
flock -n 9 || { echo "Another app/persistence operation holds the lock for $APP_SLUG--$ENVIRONMENT." >&2; exit 75; }
|
|
259
|
+
|
|
260
|
+
local db="$DATA/$DATABASE_FILE" stamp backup_name source_real source_parent
|
|
261
|
+
managed_runtime_info "$ROOT"
|
|
262
|
+
|
|
263
|
+
case "$ACTION" in
|
|
264
|
+
import)
|
|
265
|
+
[[ "$EXPECTED_RESERVATION_SHA" == "NONE" && ! -e "$MARKER" ]] || { echo "SQLite import destination was reserved after planning." >&2; exit 47; }
|
|
266
|
+
[[ ! -e "$ROOT/deployment.env" ]] || { echo "SQLite import must occur before the first managed application deployment." >&2; exit 49; }
|
|
267
|
+
[[ ! -e "$db" ]] || { echo "SQLite import refuses to overwrite an existing destination database." >&2; exit 49; }
|
|
268
|
+
[[ "$SOURCE_DATABASE" == /srv/* && "$SOURCE_DATABASE" != "$APP_ROOT"/* ]] || { echo "SQLite import source must be outside the Kit app root under /srv." >&2; exit 49; }
|
|
269
|
+
source_real="$(realpath -e "$SOURCE_DATABASE" 2>/dev/null || true)"
|
|
270
|
+
[[ "$source_real" == "$SOURCE_DATABASE" && -f "$SOURCE_DATABASE" && ! -L "$SOURCE_DATABASE" ]] || { echo "SQLite import source must be an existing non-symlink regular file with no symlinked path components." >&2; exit 49; }
|
|
271
|
+
[[ "$(basename "$SOURCE_DATABASE")" == "$DATABASE_FILE" ]] || { echo "SQLite import source file name mismatch." >&2; exit 49; }
|
|
272
|
+
create_reserved_dirs
|
|
273
|
+
docker pull "$IMAGE" >/dev/null
|
|
274
|
+
source_parent="$(dirname "$SOURCE_DATABASE")"
|
|
275
|
+
run_backup "$source_parent" "$DATA" "$DATABASE_FILE"
|
|
276
|
+
run_check "$DATA"
|
|
277
|
+
write_marker "$MARKER"
|
|
278
|
+
record_evidence "$ROOT" import imported "$SOURCE_DATABASE"
|
|
279
|
+
printf 'SQLite import completed and verified: %s\n' "$db"
|
|
280
|
+
;;
|
|
281
|
+
backup)
|
|
282
|
+
assert_marker "$MARKER" "$EXPECTED_RESERVATION_SHA"
|
|
283
|
+
verify_reserved_dirs
|
|
284
|
+
[[ -f "$db" && ! -L "$db" ]] || { echo "SQLite backup requires the reserved database file." >&2; exit 49; }
|
|
285
|
+
docker pull "$IMAGE" >/dev/null
|
|
286
|
+
stamp="$(date -u +'%Y%m%dT%H%M%SZ')"
|
|
287
|
+
backup_name="backup-$stamp-$DATABASE_FILE"
|
|
288
|
+
run_backup "$DATA" "$BACKUPS" "$backup_name"
|
|
289
|
+
record_evidence "$ROOT" backup backed-up "$backup_name"
|
|
290
|
+
printf 'SQLite verified backup created: %s\n' "$BACKUPS/$backup_name"
|
|
291
|
+
;;
|
|
292
|
+
migrate)
|
|
293
|
+
if [[ "$FIRST_INSTALL" == true ]]; then
|
|
294
|
+
[[ "$EXPECTED_RESERVATION_SHA" == "NONE" && ! -e "$MARKER" ]] || { echo "First-install SQLite destination was reserved after planning." >&2; exit 47; }
|
|
295
|
+
[[ ! -e "$ROOT/deployment.env" && ! -e "$db" ]] || { echo "First-install SQLite migration requires no runtime state or database." >&2; exit 49; }
|
|
296
|
+
create_reserved_dirs
|
|
297
|
+
docker pull "$IMAGE" >/dev/null
|
|
298
|
+
run_migrate "$DATA"
|
|
299
|
+
run_check "$DATA"
|
|
300
|
+
write_marker "$MARKER"
|
|
301
|
+
record_evidence "$ROOT" migrate migrated-first-install ""
|
|
302
|
+
printf 'SQLite first-install migration completed and verified.\n'
|
|
303
|
+
else
|
|
304
|
+
assert_marker "$MARKER" "$EXPECTED_RESERVATION_SHA"
|
|
305
|
+
verify_reserved_dirs
|
|
306
|
+
[[ -f "$db" && ! -L "$db" ]] || { echo "SQLite migration requires the reserved database file." >&2; exit 49; }
|
|
307
|
+
docker pull "$IMAGE" >/dev/null
|
|
308
|
+
stamp="$(date -u +'%Y%m%dT%H%M%SZ')"
|
|
309
|
+
backup_name="pre-migrate-$stamp-$DATABASE_FILE"
|
|
310
|
+
run_backup "$DATA" "$BACKUPS" "$backup_name"
|
|
311
|
+
if [[ "$MANAGED_RUNNING" == true ]]; then docker stop "$MANAGED_CONTAINER" >/dev/null; fi
|
|
312
|
+
if ! run_migrate "$DATA"; then
|
|
313
|
+
record_evidence "$ROOT" migrate failed "$backup_name"
|
|
314
|
+
echo "SQLite migration failed; verified pre-migration backup is retained and managed runtime remains stopped." >&2
|
|
315
|
+
exit 50
|
|
316
|
+
fi
|
|
317
|
+
run_check "$DATA"
|
|
318
|
+
record_evidence "$ROOT" migrate migrated "$backup_name"
|
|
319
|
+
printf 'SQLite migration completed; verified pre-migration backup: %s\n' "$BACKUPS/$backup_name"
|
|
320
|
+
fi
|
|
321
|
+
;;
|
|
322
|
+
rollback-check)
|
|
323
|
+
assert_marker "$MARKER" "$EXPECTED_RESERVATION_SHA"
|
|
324
|
+
verify_reserved_dirs
|
|
325
|
+
[[ -f "$db" && ! -L "$db" ]] || { echo "SQLite rollback check requires the reserved database file." >&2; exit 49; }
|
|
326
|
+
docker pull "$IMAGE" >/dev/null
|
|
327
|
+
run_check "$DATA"
|
|
328
|
+
record_evidence "$ROOT" rollback-check compatible ""
|
|
329
|
+
printf 'SQLite rollback compatibility check passed for image %s.\n' "$IMAGE"
|
|
330
|
+
;;
|
|
331
|
+
*) echo "Unsupported SQLite adapter action: $ACTION" >&2; exit 2 ;;
|
|
332
|
+
esac
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
case "$ACTION" in
|
|
336
|
+
inventory) inventory ;;
|
|
337
|
+
import|backup|migrate|rollback-check) operate ;;
|
|
338
|
+
*) echo "Unsupported SQLite adapter action: $ACTION" >&2; exit 2 ;;
|
|
339
|
+
esac
|
|
340
340
|
'''
|