@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.
Files changed (41) hide show
  1. package/README.md +71 -67
  2. package/assets/components.json +33 -33
  3. package/assets/core/distribution.json +52 -51
  4. package/assets/deployment/distribution.json +34 -34
  5. package/assets/generated/ASSEMBLY.json +38 -33
  6. package/assets/generated/core/core/skills/core-sync/SKILL.md +78 -120
  7. package/assets/generated/core/core/skills/deploy/SKILL.md +74 -91
  8. package/assets/generated/core/core/skills/lean-update/SKILL.md +189 -0
  9. package/assets/generated/core/core/skills/upstream-fix/SKILL.md +91 -76
  10. package/assets/generated/deployment/distribution/lean-poc-deploy.py +8 -8
  11. package/assets/generated/deployment/src/lean_poc_deployment/__init__.py +3 -3
  12. package/assets/generated/deployment/src/lean_poc_deployment/__main__.py +3 -3
  13. package/assets/generated/deployment/src/lean_poc_deployment/app_cli.py +140 -140
  14. package/assets/generated/deployment/src/lean_poc_deployment/app_runtime.py +455 -455
  15. package/assets/generated/deployment/src/lean_poc_deployment/app_runtime_remote.py +415 -415
  16. package/assets/generated/deployment/src/lean_poc_deployment/box_platform.py +208 -208
  17. package/assets/generated/deployment/src/lean_poc_deployment/box_platform_assets.py +100 -100
  18. package/assets/generated/deployment/src/lean_poc_deployment/box_platform_remote.py +271 -271
  19. package/assets/generated/deployment/src/lean_poc_deployment/cli.py +437 -437
  20. package/assets/generated/deployment/src/lean_poc_deployment/doctor.py +121 -121
  21. package/assets/generated/deployment/src/lean_poc_deployment/entrypoint.py +25 -25
  22. package/assets/generated/deployment/src/lean_poc_deployment/errors.py +5 -5
  23. package/assets/generated/deployment/src/lean_poc_deployment/keys.py +171 -171
  24. package/assets/generated/deployment/src/lean_poc_deployment/profiles.py +204 -204
  25. package/assets/generated/deployment/src/lean_poc_deployment/route_registry.py +367 -367
  26. package/assets/generated/deployment/src/lean_poc_deployment/route_registry_remote.py +443 -443
  27. package/assets/generated/deployment/src/lean_poc_deployment/security.py +290 -290
  28. package/assets/generated/deployment/src/lean_poc_deployment/sqlite_adapter.py +334 -334
  29. package/assets/generated/deployment/src/lean_poc_deployment/sqlite_adapter_remote.py +339 -339
  30. package/assets/generated/deployment/src/lean_poc_deployment/sqlite_cli.py +85 -85
  31. package/assets/generated/deployment/src/lean_poc_deployment/sqlite_import_recovery.py +437 -437
  32. package/assets/generated/deployment/src/lean_poc_deployment/ssh.py +118 -118
  33. package/assets/generated/deployment/src/lean_poc_deployment/teardown.py +286 -286
  34. package/assets/generated/deployment/src/lean_poc_deployment/teardown_cli.py +120 -120
  35. package/assets/generated/deployment/src/lean_poc_deployment/teardown_remote.py +100 -100
  36. package/assets/generated/deployment/src/lean_poc_deployment/trust.py +278 -278
  37. package/bin/lean.mjs +9 -9
  38. package/package.json +33 -33
  39. package/src/cli.mjs +131 -131
  40. package/src/engine.mjs +243 -243
  41. 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
  '''