@mriekki/lean 0.0.0-stage → 0.1.1

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 (82) hide show
  1. package/README.md +66 -2
  2. package/assets/components.json +33 -0
  3. package/assets/core/distribution.json +51 -0
  4. package/assets/deployment/distribution.json +34 -0
  5. package/assets/generated/ASSEMBLY.json +382 -0
  6. package/assets/generated/core/core/agents/coach.agent.md +69 -0
  7. package/assets/generated/core/core/agents/reviewer.agent.md +85 -0
  8. package/assets/generated/core/core/instructions/lean-docs.instructions.md +14 -0
  9. package/assets/generated/core/core/instructions/lean-workflow.instructions.md +35 -0
  10. package/assets/generated/core/core/learning/CODING_DRILLS.md +35 -0
  11. package/assets/generated/core/core/learning/DIAGRAM_GUIDE.md +19 -0
  12. package/assets/generated/core/core/learning/ENGINEERING_LEARNING_WORKBOOK.md +82 -0
  13. package/assets/generated/core/core/learning/LEARNING_MAP.md +51 -0
  14. package/assets/generated/core/core/learning/LEARNING_PROGRESS.md +31 -0
  15. package/assets/generated/core/core/learning/RESOURCE_CATALOG.md +25 -0
  16. package/assets/generated/core/core/learning/SQL_DRILLS.md +27 -0
  17. package/assets/generated/core/core/learning/SYSTEM_DESIGN_DRILLS.md +21 -0
  18. package/assets/generated/core/core/process/COACHING_GUIDE.md +57 -0
  19. package/assets/generated/core/core/process/GUIDED_NEXT_STEPS.md +48 -0
  20. package/assets/generated/core/core/process/HUMAN_GATES.md +81 -0
  21. package/assets/generated/core/core/process/INTERACTIVE_SESSION_GUIDE.md +62 -0
  22. package/assets/generated/core/core/process/PRODUCTION_ENABLEMENT.md +59 -0
  23. package/assets/generated/core/core/process/PR_WORKFLOW.md +53 -0
  24. package/assets/generated/core/core/process/SETUP_ACCEPTANCE_CHECKLIST.md +72 -0
  25. package/assets/generated/core/core/process/WORK_ITEM_MODEL.md +56 -0
  26. package/assets/generated/core/core/skills/cleanup-branches/SKILL.md +190 -0
  27. package/assets/generated/core/core/skills/coach/SKILL.md +136 -0
  28. package/assets/generated/core/core/skills/code-walkthrough/SKILL.md +197 -0
  29. package/assets/generated/core/core/skills/core-sync/SKILL.md +220 -0
  30. package/assets/generated/core/core/skills/define-feature/SKILL.md +144 -0
  31. package/assets/generated/core/core/skills/deliver/SKILL.md +315 -0
  32. package/assets/generated/core/core/skills/deploy/SKILL.md +193 -0
  33. package/assets/generated/core/core/skills/grill-me/SKILL.md +77 -0
  34. package/assets/generated/core/core/skills/grill-with-docs/SKILL.md +90 -0
  35. package/assets/generated/core/core/skills/learning-review/SKILL.md +90 -0
  36. package/assets/generated/core/core/skills/next-step/SKILL.md +166 -0
  37. package/assets/generated/core/core/skills/production-readiness/SKILL.md +357 -0
  38. package/assets/generated/core/core/skills/pull-latest/SKILL.md +166 -0
  39. package/assets/generated/core/core/skills/refine-requirements/SKILL.md +207 -0
  40. package/assets/generated/core/core/skills/start-work/SKILL.md +178 -0
  41. package/assets/generated/core/core/skills/tear-down/SKILL.md +138 -0
  42. package/assets/generated/core/core/skills/upstream-fix/SKILL.md +206 -0
  43. package/assets/generated/core/core/skills/wait-what/SKILL.md +86 -0
  44. package/assets/generated/core/core/templates/FEATURE_BRIEF_TEMPLATE.md +27 -0
  45. package/assets/generated/core/core/templates/PRODUCT_BRIEF_TEMPLATE.md +27 -0
  46. package/assets/generated/core/core/templates/PULL_REQUEST_TEMPLATE.md +37 -0
  47. package/assets/generated/core/core/templates/REQUIREMENTS_TEMPLATE.md +17 -0
  48. package/assets/generated/core/profiles/greenfield/lean-workspace.json +12 -0
  49. package/assets/generated/core/tools/package/check_baseline.py +102 -0
  50. package/assets/generated/core/tools/package/check_upstream.py +171 -0
  51. package/assets/generated/deployment/distribution/lean-poc-deploy.py +8 -0
  52. package/assets/generated/deployment/src/lean_poc_deployment/__init__.py +3 -0
  53. package/assets/generated/deployment/src/lean_poc_deployment/__main__.py +3 -0
  54. package/assets/generated/deployment/src/lean_poc_deployment/app_cli.py +140 -0
  55. package/assets/generated/deployment/src/lean_poc_deployment/app_runtime.py +455 -0
  56. package/assets/generated/deployment/src/lean_poc_deployment/app_runtime_remote.py +416 -0
  57. package/assets/generated/deployment/src/lean_poc_deployment/box_platform.py +208 -0
  58. package/assets/generated/deployment/src/lean_poc_deployment/box_platform_assets.py +100 -0
  59. package/assets/generated/deployment/src/lean_poc_deployment/box_platform_remote.py +271 -0
  60. package/assets/generated/deployment/src/lean_poc_deployment/cli.py +437 -0
  61. package/assets/generated/deployment/src/lean_poc_deployment/doctor.py +121 -0
  62. package/assets/generated/deployment/src/lean_poc_deployment/entrypoint.py +25 -0
  63. package/assets/generated/deployment/src/lean_poc_deployment/errors.py +5 -0
  64. package/assets/generated/deployment/src/lean_poc_deployment/keys.py +171 -0
  65. package/assets/generated/deployment/src/lean_poc_deployment/profiles.py +204 -0
  66. package/assets/generated/deployment/src/lean_poc_deployment/route_registry.py +367 -0
  67. package/assets/generated/deployment/src/lean_poc_deployment/route_registry_remote.py +443 -0
  68. package/assets/generated/deployment/src/lean_poc_deployment/security.py +290 -0
  69. package/assets/generated/deployment/src/lean_poc_deployment/sqlite_adapter.py +335 -0
  70. package/assets/generated/deployment/src/lean_poc_deployment/sqlite_adapter_remote.py +340 -0
  71. package/assets/generated/deployment/src/lean_poc_deployment/sqlite_cli.py +86 -0
  72. package/assets/generated/deployment/src/lean_poc_deployment/sqlite_import_recovery.py +437 -0
  73. package/assets/generated/deployment/src/lean_poc_deployment/ssh.py +118 -0
  74. package/assets/generated/deployment/src/lean_poc_deployment/teardown.py +286 -0
  75. package/assets/generated/deployment/src/lean_poc_deployment/teardown_cli.py +120 -0
  76. package/assets/generated/deployment/src/lean_poc_deployment/teardown_remote.py +100 -0
  77. package/assets/generated/deployment/src/lean_poc_deployment/trust.py +278 -0
  78. package/bin/lean.mjs +9 -0
  79. package/package.json +31 -4
  80. package/src/cli.mjs +131 -0
  81. package/src/engine.mjs +243 -0
  82. package/src/package-data.mjs +18 -0
@@ -0,0 +1,443 @@
1
+ """Remote script for owned route registration on a Kit-managed Ubuntu box."""
2
+
3
+ REMOTE_SCRIPT = r'''#!/usr/bin/env bash
4
+ set -Eeuo pipefail
5
+
6
+ ACTION="$1"
7
+ SPEC="${2:-}"
8
+ EXPECTED_SPEC="${3:-}"
9
+ PLATFORM_ROOT="__PLATFORM_ROOT__"
10
+ PROXY_ROOT="$PLATFORM_ROOT/proxy"
11
+ ROUTE_ROOT="$PLATFORM_ROOT/routes"
12
+ MARKER="$PLATFORM_ROOT/platform.env"
13
+ LOCK_FILE="/run/lock/lean-poc-platform.lock"
14
+ NETWORK_NAME="__NETWORK_NAME__"
15
+ CADDY_CONTAINER="__CADDY_CONTAINER__"
16
+ CADDY_IMAGE="__CADDY_IMAGE__"
17
+ PLATFORM_SCHEMA_VERSION="__PLATFORM_SCHEMA_VERSION__"
18
+ PLATFORM_VERSION="__PLATFORM_VERSION__"
19
+ PLATFORM_DISTRIBUTION="__PLATFORM_DISTRIBUTION__"
20
+ ROUTE_SCHEMA_VERSION="__ROUTE_SCHEMA_VERSION__"
21
+ ALIAS_ROOT="/etc/lean-poc-platform/aliases"
22
+ ALIAS_SERVICE="lean-poc-avahi-alias@"
23
+
24
+ platform_value() {
25
+ local key="$1"
26
+ [[ -f "$MARKER" ]] || return 1
27
+ sed -n "s/^${key}=//p" "$MARKER" | head -n1
28
+ }
29
+ platform_state() {
30
+ if [[ ! -f "$MARKER" ]]; then
31
+ printf 'missing-platform'
32
+ return
33
+ fi
34
+ if [[ "$(platform_value SCHEMA_VERSION || true)" != "$PLATFORM_SCHEMA_VERSION" || \
35
+ "$(platform_value DISTRIBUTION || true)" != "$PLATFORM_DISTRIBUTION" ]]; then
36
+ printf 'incompatible'
37
+ return
38
+ fi
39
+ if [[ "$(platform_value PLATFORM_VERSION || true)" != "$PLATFORM_VERSION" ]]; then
40
+ printf 'upgrade-required'
41
+ return
42
+ fi
43
+ if [[ "$(docker inspect --format '{{.State.Status}}' "$CADDY_CONTAINER" 2>/dev/null || true)" != "running" ]]; then
44
+ printf 'drifted'
45
+ return
46
+ fi
47
+ if ! docker network inspect "$NETWORK_NAME" >/dev/null 2>&1; then
48
+ printf 'drifted'
49
+ return
50
+ fi
51
+ if [[ ! -f "$PROXY_ROOT/Caddyfile" || ! -d "$PROXY_ROOT/conf.d" || ! -d "$ROUTE_ROOT" ]]; then
52
+ printf 'drifted'
53
+ return
54
+ fi
55
+ printf 'ready'
56
+ }
57
+
58
+ valid_slug() { [[ "$1" =~ ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$ ]]; }
59
+ valid_repo() { [[ "$1" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; }
60
+ valid_container() { [[ "$1" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$ ]]; }
61
+ valid_port() { [[ "$1" =~ ^[0-9]+$ ]] && (( 1 <= 10#$1 && 10#$1 <= 65535 )); }
62
+ valid_hostname() {
63
+ local host="$1" label
64
+ [[ "$host" == *.local ]] || return 1
65
+ [[ ${#host} -le 253 ]] || return 1
66
+ [[ "$host" != *:* && "$host" != *" "* && "$host" != */* && "$host" != *,* ]] || return 1
67
+ IFS='.' read -r -a LABELS <<< "$host"
68
+ (( ${#LABELS[@]} >= 2 )) || return 1
69
+ for label in "${LABELS[@]}"; do
70
+ [[ "$label" =~ ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$ ]] || return 1
71
+ done
72
+ }
73
+
74
+ parse_spec() {
75
+ IFS=$'\t' read -r S_SCHEMA S_OWNER S_ENV S_APP S_ROUTE S_UPSTREAM S_PORT S_HOSTS extra <<< "$SPEC"
76
+ [[ -z "${extra:-}" ]] || { echo "Route spec has too many fields." >&2; exit 20; }
77
+ [[ "$S_SCHEMA" == "$ROUTE_SCHEMA_VERSION" ]] || { echo "Unsupported route schema." >&2; exit 20; }
78
+ valid_repo "$S_OWNER" || { echo "Invalid owner repo." >&2; exit 20; }
79
+ valid_slug "$S_ENV" || { echo "Invalid environment slug." >&2; exit 20; }
80
+ valid_slug "$S_APP" || { echo "Invalid app slug." >&2; exit 20; }
81
+ [[ "$S_ROUTE" == "$S_APP--$S_ENV" ]] || { echo "Route ID mismatch." >&2; exit 20; }
82
+ valid_container "$S_UPSTREAM" || { echo "Invalid upstream container." >&2; exit 20; }
83
+ valid_port "$S_PORT" || { echo "Invalid upstream port." >&2; exit 20; }
84
+ [[ -n "$S_HOSTS" ]] || { echo "No route hostnames supplied." >&2; exit 20; }
85
+ IFS=',' read -r -a HOST_ARRAY <<< "$S_HOSTS"
86
+ local host seen="|"
87
+ for host in "${HOST_ARRAY[@]}"; do
88
+ valid_hostname "$host" || { echo "Invalid route hostname: $host" >&2; exit 20; }
89
+ [[ "$seen" != *"|$host|"* ]] || { echo "Duplicate route hostname: $host" >&2; exit 20; }
90
+ seen="$seen$host|"
91
+ done
92
+ }
93
+
94
+ metadata_path() { printf '%s/%s.route' "$ROUTE_ROOT" "$1"; }
95
+ route_path() { printf '%s/conf.d/20-%s.caddy' "$PROXY_ROOT" "$1"; }
96
+ alias_path() { printf '%s/%s.env' "$ALIAS_ROOT" "$1"; }
97
+
98
+ metadata_value() {
99
+ local path="$1" key="$2"
100
+ sed -n "s/^${key}=//p" "$path" | head -n1
101
+ }
102
+
103
+ emit_route() {
104
+ local path="$1" schema owner env app route upstream port hosts
105
+ schema="$(metadata_value "$path" SCHEMA_VERSION)"
106
+ owner="$(metadata_value "$path" OWNER_REPO)"
107
+ env="$(metadata_value "$path" ENVIRONMENT)"
108
+ app="$(metadata_value "$path" APP_SLUG)"
109
+ route="$(metadata_value "$path" ROUTE_ID)"
110
+ upstream="$(metadata_value "$path" UPSTREAM_CONTAINER)"
111
+ port="$(metadata_value "$path" UPSTREAM_PORT)"
112
+ hosts="$(metadata_value "$path" HOSTNAMES)"
113
+ printf 'ROUTE\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$schema" "$owner" "$env" "$app" "$route" "$upstream" "$port" "$hosts"
114
+ }
115
+
116
+ inventory() {
117
+ local state count=0 path
118
+ state="$(platform_state)"
119
+ if [[ -d "$ROUTE_ROOT" ]]; then
120
+ while IFS= read -r path; do
121
+ [[ -n "$path" ]] || continue
122
+ count=$((count + 1))
123
+ done < <(find "$ROUTE_ROOT" -maxdepth 1 -type f -name '*.route' -print 2>/dev/null | sort)
124
+ fi
125
+ printf 'SUMMARY\tplatformState\t%s\n' "$state"
126
+ printf 'SUMMARY\tmanagedRouteCount\t%s\n' "$count"
127
+ if [[ -d "$ROUTE_ROOT" ]]; then
128
+ while IFS= read -r path; do
129
+ [[ -n "$path" ]] || continue
130
+ emit_route "$path"
131
+ done < <(find "$ROUTE_ROOT" -maxdepth 1 -type f -name '*.route' -print 2>/dev/null | sort)
132
+ fi
133
+ }
134
+
135
+ require_ready() {
136
+ local state
137
+ state="$(platform_state)"
138
+ [[ "$state" == ready ]] || { echo "Kit-owned shared platform is not ready: $state" >&2; exit 21; }
139
+ }
140
+
141
+ metadata_matches_spec() {
142
+ local path="$1" encoded="$2"
143
+ local e_schema e_owner e_env e_app e_route e_upstream e_port e_hosts e_extra
144
+ IFS=$'\t' read -r e_schema e_owner e_env e_app e_route e_upstream e_port e_hosts e_extra <<< "$encoded"
145
+ [[ -z "${e_extra:-}" ]] || return 1
146
+ [[ "$(metadata_value "$path" SCHEMA_VERSION)" == "$e_schema" && \
147
+ "$(metadata_value "$path" OWNER_REPO)" == "$e_owner" && \
148
+ "$(metadata_value "$path" ENVIRONMENT)" == "$e_env" && \
149
+ "$(metadata_value "$path" APP_SLUG)" == "$e_app" && \
150
+ "$(metadata_value "$path" ROUTE_ID)" == "$e_route" && \
151
+ "$(metadata_value "$path" UPSTREAM_CONTAINER)" == "$e_upstream" && \
152
+ "$(metadata_value "$path" UPSTREAM_PORT)" == "$e_port" && \
153
+ "$(metadata_value "$path" HOSTNAMES)" == "$e_hosts" ]]
154
+ }
155
+
156
+ assert_expected_previous() {
157
+ local meta
158
+ meta="$(metadata_path "$S_ROUTE")"
159
+ if [[ "$EXPECTED_SPEC" == "NONE" ]]; then
160
+ [[ ! -e "$meta" ]] || { echo "Route appeared after planning; create a fresh plan." >&2; exit 28; }
161
+ return 0
162
+ fi
163
+ [[ -f "$meta" && ! -L "$meta" ]] || { echo "Expected previous route metadata is missing or unsafe." >&2; exit 28; }
164
+ metadata_matches_spec "$meta" "$EXPECTED_SPEC" || {
165
+ echo "Registered route changed after planning; create a fresh plan." >&2
166
+ exit 28
167
+ }
168
+ }
169
+
170
+ check_alias_collisions() {
171
+ local meta current_hosts="" host apath
172
+ meta="$(metadata_path "$S_ROUTE")"
173
+ if [[ -f "$meta" ]]; then current_hosts="$(metadata_value "$meta" HOSTNAMES)"; fi
174
+ for host in "${HOST_ARRAY[@]}"; do
175
+ apath="$(alias_path "$host")"
176
+ if [[ -L "$apath" ]]; then
177
+ echo "Alias registration path is a symbolic link: $apath" >&2
178
+ exit 29
179
+ fi
180
+ if [[ -e "$apath" && ",${current_hosts}," != *",${host},"* ]]; then
181
+ echo "Alias collision: $host already has unmanaged or differently owned alias state." >&2
182
+ exit 29
183
+ fi
184
+ if systemctl is-active --quiet "$ALIAS_SERVICE$host.service" 2>/dev/null && [[ ",${current_hosts}," != *",${host},"* ]]; then
185
+ echo "Alias collision: active service already publishes $host outside this route." >&2
186
+ exit 29
187
+ fi
188
+ done
189
+ }
190
+
191
+ check_collisions() {
192
+ local current="$1" path hosts host other_route
193
+ for path in "$ROUTE_ROOT"/*.route; do
194
+ [[ -f "$path" ]] || continue
195
+ other_route="$(metadata_value "$path" ROUTE_ID)"
196
+ [[ "$other_route" == "$current" ]] && continue
197
+ hosts="$(metadata_value "$path" HOSTNAMES)"
198
+ IFS=',' read -r -a EXISTING_HOSTS <<< "$hosts"
199
+ for host in "${HOST_ARRAY[@]}"; do
200
+ local existing
201
+ for existing in "${EXISTING_HOSTS[@]}"; do
202
+ if [[ "$host" == "$existing" ]]; then
203
+ echo "Hostname collision: $host is already owned by route $other_route" >&2
204
+ exit 22
205
+ fi
206
+ done
207
+ done
208
+ done
209
+ }
210
+
211
+ assert_same_owner_if_existing() {
212
+ local meta
213
+ meta="$(metadata_path "$S_ROUTE")"
214
+ [[ -f "$meta" ]] || return 0
215
+ [[ "$(metadata_value "$meta" OWNER_REPO)" == "$S_OWNER" && \
216
+ "$(metadata_value "$meta" ENVIRONMENT)" == "$S_ENV" && \
217
+ "$(metadata_value "$meta" APP_SLUG)" == "$S_APP" ]] || {
218
+ echo "Existing route ownership does not match requested owner/environment/app." >&2
219
+ exit 23
220
+ }
221
+ }
222
+
223
+ render_route() {
224
+ local dest="$1" host first=true
225
+ : > "$dest"
226
+ for host in "${HOST_ARRAY[@]}"; do
227
+ if [[ "$first" == true ]]; then
228
+ printf 'http://%s' "$host" >> "$dest"
229
+ first=false
230
+ else
231
+ printf ', http://%s' "$host" >> "$dest"
232
+ fi
233
+ done
234
+ cat >> "$dest" <<EOF
235
+ {
236
+ reverse_proxy $S_UPSTREAM:$S_PORT
237
+ }
238
+ EOF
239
+ }
240
+
241
+ write_metadata() {
242
+ local dest="$1"
243
+ cat > "$dest" <<EOF
244
+ SCHEMA_VERSION=$S_SCHEMA
245
+ OWNER_REPO=$S_OWNER
246
+ ENVIRONMENT=$S_ENV
247
+ APP_SLUG=$S_APP
248
+ ROUTE_ID=$S_ROUTE
249
+ UPSTREAM_CONTAINER=$S_UPSTREAM
250
+ UPSTREAM_PORT=$S_PORT
251
+ HOSTNAMES=$S_HOSTS
252
+ EOF
253
+ }
254
+
255
+ validate_candidate_conf() {
256
+ local candidate_conf="$1"
257
+ docker run --rm \
258
+ -v "$PROXY_ROOT/Caddyfile:/etc/caddy/Caddyfile:ro" \
259
+ -v "$candidate_conf:/etc/caddy/conf.d:ro" \
260
+ "$CADDY_IMAGE" caddy validate --config /etc/caddy/Caddyfile >/dev/null
261
+ }
262
+
263
+ copy_conf_for_candidate() {
264
+ local candidate="$1" target_route="$2"
265
+ install -d -m 0755 "$candidate"
266
+ local path
267
+ for path in "$PROXY_ROOT/conf.d"/*.caddy; do
268
+ [[ -f "$path" ]] || continue
269
+ [[ "$path" == "$target_route" ]] && continue
270
+ cp -a "$path" "$candidate/$(basename "$path")"
271
+ done
272
+ }
273
+
274
+ require_upstream_ready() {
275
+ local state
276
+ state="$(docker inspect --format '{{.State.Status}}' "$S_UPSTREAM" 2>/dev/null || true)"
277
+ [[ "$state" == running ]] || { echo "Upstream container is not running: $S_UPSTREAM" >&2; exit 24; }
278
+ if ! docker network inspect "$NETWORK_NAME" --format '{{range .Containers}}{{println .Name}}{{end}}' | grep -Fxq "$S_UPSTREAM"; then
279
+ echo "Upstream container is not attached to $NETWORK_NAME; application deployment must attach it before route registration." >&2
280
+ exit 27
281
+ fi
282
+ }
283
+
284
+ register_route() {
285
+ require_ready
286
+ parse_spec
287
+ install -d -m 0750 "$ROUTE_ROOT"
288
+ install -d -m 0755 "$ALIAS_ROOT"
289
+ assert_expected_previous
290
+ assert_same_owner_if_existing
291
+ check_alias_collisions
292
+ check_collisions "$S_ROUTE"
293
+
294
+ local route_file meta_file candidate_dir candidate_route temp_meta backup_dir old_hosts=""
295
+ route_file="$(route_path "$S_ROUTE")"
296
+ meta_file="$(metadata_path "$S_ROUTE")"
297
+ candidate_dir="$(mktemp -d)"
298
+ backup_dir="$(mktemp -d)"
299
+ candidate_route="$candidate_dir/$(basename "$route_file")"
300
+ copy_conf_for_candidate "$candidate_dir" "$route_file"
301
+ render_route "$candidate_route"
302
+ validate_candidate_conf "$candidate_dir"
303
+
304
+ if [[ -f "$meta_file" ]]; then old_hosts="$(metadata_value "$meta_file" HOSTNAMES)"; fi
305
+ [[ -f "$route_file" ]] && cp -a "$route_file" "$backup_dir/route.caddy"
306
+ [[ -f "$meta_file" ]] && cp -a "$meta_file" "$backup_dir/route.meta"
307
+ IFS=',' read -r -a OLD_HOST_ARRAY <<< "$old_hosts"
308
+ local host
309
+ for host in "${OLD_HOST_ARRAY[@]}" "${HOST_ARRAY[@]}"; do
310
+ [[ -n "$host" ]] || continue
311
+ local apath
312
+ apath="$(alias_path "$host")"
313
+ if [[ -f "$apath" && ! -f "$backup_dir/alias-$host.env" ]]; then cp -a "$apath" "$backup_dir/alias-$host.env"; fi
314
+ done
315
+
316
+ rollback() {
317
+ echo "Route registration failed; restoring previous route/alias state." >&2
318
+ if [[ -f "$backup_dir/route.caddy" ]]; then cp -f "$backup_dir/route.caddy" "$route_file"; else rm -f "$route_file"; fi
319
+ if [[ -f "$backup_dir/route.meta" ]]; then cp -f "$backup_dir/route.meta" "$meta_file"; else rm -f "$meta_file"; fi
320
+ local rollback_host
321
+ for rollback_host in "${OLD_HOST_ARRAY[@]}" "${HOST_ARRAY[@]}"; do
322
+ [[ -n "$rollback_host" ]] || continue
323
+ if [[ -f "$backup_dir/alias-$rollback_host.env" ]]; then
324
+ cp -f "$backup_dir/alias-$rollback_host.env" "$(alias_path "$rollback_host")"
325
+ systemctl enable --now "$ALIAS_SERVICE$rollback_host.service" >/dev/null 2>&1 || true
326
+ systemctl restart "$ALIAS_SERVICE$rollback_host.service" >/dev/null 2>&1 || true
327
+ else
328
+ systemctl disable --now "$ALIAS_SERVICE$rollback_host.service" >/dev/null 2>&1 || true
329
+ rm -f "$(alias_path "$rollback_host")"
330
+ fi
331
+ done
332
+ docker exec "$CADDY_CONTAINER" caddy reload --config /etc/caddy/Caddyfile >/dev/null 2>&1 || true
333
+ }
334
+
335
+ require_upstream_ready
336
+ local tmp_route="${route_file}.tmp.$$" tmp_meta="${meta_file}.tmp.$$"
337
+ cp -f "$candidate_route" "$tmp_route" && chmod 0644 "$tmp_route" && mv -f "$tmp_route" "$route_file"
338
+ write_metadata "$tmp_meta" && chmod 0644 "$tmp_meta" && mv -f "$tmp_meta" "$meta_file"
339
+
340
+ local rc=0
341
+ set +e
342
+ for host in "${HOST_ARRAY[@]}"; do
343
+ printf 'ALIAS_HOSTNAME=%s\n' "$host" > "$(alias_path "$host")"
344
+ chmod 0644 "$(alias_path "$host")"
345
+ systemctl enable --now "$ALIAS_SERVICE$host.service" >/dev/null
346
+ rc=$?; [[ $rc -eq 0 ]] || break
347
+ systemctl restart "$ALIAS_SERVICE$host.service" >/dev/null
348
+ rc=$?; [[ $rc -eq 0 ]] || break
349
+ done
350
+ [[ $rc -eq 0 ]] && docker exec "$CADDY_CONTAINER" caddy reload --config /etc/caddy/Caddyfile >/dev/null
351
+ rc=$(( rc == 0 ? $? : rc ))
352
+ set -e
353
+ if [[ $rc -ne 0 ]]; then
354
+ rollback
355
+ rm -rf "$candidate_dir" "$backup_dir"
356
+ exit "$rc"
357
+ fi
358
+
359
+ for host in "${OLD_HOST_ARRAY[@]}"; do
360
+ [[ -n "$host" ]] || continue
361
+ if [[ ",${S_HOSTS}," != *",${host},"* ]]; then
362
+ systemctl disable --now "$ALIAS_SERVICE$host.service" >/dev/null 2>&1 || true
363
+ rm -f "$(alias_path "$host")"
364
+ fi
365
+ done
366
+ rm -rf "$candidate_dir" "$backup_dir"
367
+ }
368
+
369
+ remove_route() {
370
+ require_ready
371
+ parse_spec
372
+ local route_file meta_file existing_hosts host candidate_dir
373
+ route_file="$(route_path "$S_ROUTE")"
374
+ meta_file="$(metadata_path "$S_ROUTE")"
375
+ [[ -f "$meta_file" ]] || { echo "Managed route is missing: $S_ROUTE" >&2; exit 25; }
376
+ assert_same_owner_if_existing
377
+ existing_hosts="$(metadata_value "$meta_file" HOSTNAMES)"
378
+ [[ "$existing_hosts" == "$S_HOSTS" && "$(metadata_value "$meta_file" UPSTREAM_CONTAINER)" == "$S_UPSTREAM" && "$(metadata_value "$meta_file" UPSTREAM_PORT)" == "$S_PORT" ]] || {
379
+ echo "Registered route changed since the authorized plan." >&2; exit 26;
380
+ }
381
+
382
+ candidate_dir="$(mktemp -d)"
383
+ copy_conf_for_candidate "$candidate_dir" "$route_file"
384
+ validate_candidate_conf "$candidate_dir"
385
+
386
+ local backup_dir
387
+ backup_dir="$(mktemp -d)"
388
+ cp -a "$route_file" "$backup_dir/route.caddy"
389
+ cp -a "$meta_file" "$backup_dir/route.meta"
390
+ IFS=',' read -r -a OLD_HOST_ARRAY <<< "$existing_hosts"
391
+ for host in "${OLD_HOST_ARRAY[@]}"; do
392
+ [[ -f "$(alias_path "$host")" ]] && cp -a "$(alias_path "$host")" "$backup_dir/alias-$host.env"
393
+ done
394
+
395
+ rollback() {
396
+ cp -f "$backup_dir/route.caddy" "$route_file"
397
+ cp -f "$backup_dir/route.meta" "$meta_file"
398
+ local rollback_host
399
+ for rollback_host in "${OLD_HOST_ARRAY[@]}"; do
400
+ [[ -f "$backup_dir/alias-$rollback_host.env" ]] || continue
401
+ cp -f "$backup_dir/alias-$rollback_host.env" "$(alias_path "$rollback_host")"
402
+ systemctl enable --now "$ALIAS_SERVICE$rollback_host.service" >/dev/null 2>&1 || true
403
+ systemctl restart "$ALIAS_SERVICE$rollback_host.service" >/dev/null 2>&1 || true
404
+ done
405
+ docker exec "$CADDY_CONTAINER" caddy reload --config /etc/caddy/Caddyfile >/dev/null 2>&1 || true
406
+ }
407
+
408
+ rm -f "$route_file" "$meta_file"
409
+ local rc=0
410
+ set +e
411
+ docker exec "$CADDY_CONTAINER" caddy reload --config /etc/caddy/Caddyfile >/dev/null
412
+ rc=$?
413
+ if [[ $rc -eq 0 ]]; then
414
+ for host in "${OLD_HOST_ARRAY[@]}"; do
415
+ systemctl disable --now "$ALIAS_SERVICE$host.service" >/dev/null 2>&1 || true
416
+ rm -f "$(alias_path "$host")"
417
+ done
418
+ fi
419
+ set -e
420
+ if [[ $rc -ne 0 ]]; then
421
+ rollback
422
+ rm -rf "$candidate_dir" "$backup_dir"
423
+ exit "$rc"
424
+ fi
425
+ rm -rf "$candidate_dir" "$backup_dir"
426
+ }
427
+
428
+ case "$ACTION" in
429
+ inventory)
430
+ inventory
431
+ ;;
432
+ register|remove)
433
+ exec 9>>"$LOCK_FILE"
434
+ if ! flock -n 9; then echo "Another platform/route operation holds the lock." >&2; exit 75; fi
435
+ if [[ "$ACTION" == register ]]; then register_route; else remove_route; fi
436
+ inventory
437
+ ;;
438
+ *)
439
+ echo "Unsupported route-registry action: $ACTION" >&2
440
+ exit 2
441
+ ;;
442
+ esac
443
+ '''