@mriekki/project-forge 4.20.0 → 4.22.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/assets/components.json +2 -2
- package/assets/generated/ASSEMBLY.json +9 -9
- package/assets/generated/deployment/docs/CLOUDFLARE_INGRESS.md +36 -2
- package/assets/generated/deployment/src/project_forge_deployment/app_cli.py +18 -0
- package/assets/generated/deployment/src/project_forge_deployment/app_runtime.py +120 -8
- package/assets/generated/deployment/src/project_forge_deployment/app_runtime_remote.py +173 -3
- package/assets/generated/deployment/src/project_forge_deployment/cloudflare_ingress.py +594 -1
- package/assets/generated/deployment/src/project_forge_deployment/cloudflare_ingress_remote.py +347 -10
- package/assets/generated/deployment/src/project_forge_deployment/ingress_cli.py +51 -1
- package/package.json +1 -1
package/assets/generated/deployment/src/project_forge_deployment/cloudflare_ingress_remote.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
REMOTE_SCRIPT = r"""#!/usr/bin/env bash
|
|
4
4
|
set -Eeuo pipefail
|
|
5
|
+
export LC_ALL=C
|
|
5
6
|
|
|
6
7
|
ACTION="$1"
|
|
7
8
|
SCOPE="$2"
|
|
@@ -43,6 +44,9 @@ AUTH_ROOT="$PLATFORM_ROOT/proxy/data/authenticated/$SCOPE"
|
|
|
43
44
|
AUTH_ROUTES="$AUTH_ROOT/routes"
|
|
44
45
|
AUTH_CADDY="$PLATFORM_ROOT/proxy/conf.d/90-project-forge-auth-$SCOPE.caddy"
|
|
45
46
|
AUTH_SOCKET="$AUTH_ROOT/origin.sock"
|
|
47
|
+
MIGRATION_MARKER_BACKUP="$ROOT/.authenticated-origin-migration.marker"
|
|
48
|
+
MIGRATION_COMPOSE_BACKUP="$ROOT/.authenticated-origin-migration.compose"
|
|
49
|
+
MIGRATION_RECEIPT="$ROOT/.authenticated-origin-migration.receipt"
|
|
46
50
|
CONTAINER="project-forge-cloudflared-$SCOPE"
|
|
47
51
|
LOCK_FILE="/run/lock/project-forge-cloudflare-$SCOPE.lock"
|
|
48
52
|
|
|
@@ -204,6 +208,7 @@ http://:19078 {
|
|
|
204
208
|
EOF
|
|
205
209
|
chmod 0644 "$tmp"
|
|
206
210
|
mv -f "$tmp" "$AUTH_CADDY"
|
|
211
|
+
MIGRATION_CADDY_HASH="$(sha256sum "$AUTH_CADDY" | cut -d' ' -f1)"
|
|
207
212
|
docker exec "$CADDY_CONTAINER" caddy reload --config /etc/caddy/Caddyfile >/dev/null
|
|
208
213
|
[[ -S "$AUTH_SOCKET" ]] || {
|
|
209
214
|
echo "Authenticated private origin socket did not appear." >&2; exit 27;
|
|
@@ -245,7 +250,19 @@ auth_route_write() {
|
|
|
245
250
|
tmp="$file.tmp.$BASHPID"
|
|
246
251
|
backup="$file.backup.$BASHPID"
|
|
247
252
|
[[ ! -f "$file" ]] || cp -a "$file" "$backup"
|
|
248
|
-
|
|
253
|
+
auth_route_content > "$tmp"
|
|
254
|
+
chmod 0644 "$tmp"
|
|
255
|
+
mv -f "$tmp" "$file"
|
|
256
|
+
if ! docker exec "$CADDY_CONTAINER" caddy reload --config /etc/caddy/Caddyfile >/dev/null; then
|
|
257
|
+
if [[ -f "$backup" ]]; then mv -f "$backup" "$file"; else rm -f "$file"; fi
|
|
258
|
+
docker exec "$CADDY_CONTAINER" caddy reload --config /etc/caddy/Caddyfile >/dev/null 2>&1 || true
|
|
259
|
+
echo "Authenticated Caddy route failed validation; restored previous state." >&2
|
|
260
|
+
exit 35
|
|
261
|
+
fi
|
|
262
|
+
rm -f "$backup"
|
|
263
|
+
}
|
|
264
|
+
auth_route_content() {
|
|
265
|
+
cat <<EOF
|
|
249
266
|
@pf_$R_ROUTE host $R_PUBLIC
|
|
250
267
|
handle @pf_$R_ROUTE {
|
|
251
268
|
reverse_proxy $AUTH_UPSTREAM:$AUTH_PORT {
|
|
@@ -263,15 +280,6 @@ handle @pf_$R_ROUTE {
|
|
|
263
280
|
}
|
|
264
281
|
}
|
|
265
282
|
EOF
|
|
266
|
-
chmod 0644 "$tmp"
|
|
267
|
-
mv -f "$tmp" "$file"
|
|
268
|
-
if ! docker exec "$CADDY_CONTAINER" caddy reload --config /etc/caddy/Caddyfile >/dev/null; then
|
|
269
|
-
if [[ -f "$backup" ]]; then mv -f "$backup" "$file"; else rm -f "$file"; fi
|
|
270
|
-
docker exec "$CADDY_CONTAINER" caddy reload --config /etc/caddy/Caddyfile >/dev/null 2>&1 || true
|
|
271
|
-
echo "Authenticated Caddy route failed validation; restored previous state." >&2
|
|
272
|
-
exit 35
|
|
273
|
-
fi
|
|
274
|
-
rm -f "$backup"
|
|
275
283
|
}
|
|
276
284
|
auth_route_remove() {
|
|
277
285
|
[[ "$(marker_value AUTHENTICATED_ORIGIN)" == true ]] || return 0
|
|
@@ -364,6 +372,320 @@ route_inventory() {
|
|
|
364
372
|
return 0
|
|
365
373
|
}
|
|
366
374
|
|
|
375
|
+
route_inventory_digest() {
|
|
376
|
+
route_inventory | sha256sum | cut -d' ' -f1
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
auth_files_digest() {
|
|
380
|
+
if [[ ! -d "$AUTH_ROOT" ]]; then
|
|
381
|
+
printf ''
|
|
382
|
+
return
|
|
383
|
+
fi
|
|
384
|
+
(
|
|
385
|
+
cd "$AUTH_ROOT"
|
|
386
|
+
find . -type f -print0 | sort -z | xargs -0r sha256sum
|
|
387
|
+
) | sha256sum | cut -d' ' -f1
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
receipt_value() {
|
|
391
|
+
local key="$1"
|
|
392
|
+
[[ -f "$MIGRATION_RECEIPT" && ! -L "$MIGRATION_RECEIPT" ]] || return 1
|
|
393
|
+
sed -n "s/^${key}=//p" "$MIGRATION_RECEIPT" | head -n1
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
migration_pending_valid() {
|
|
397
|
+
[[ -f "$MIGRATION_MARKER_BACKUP" && ! -L "$MIGRATION_MARKER_BACKUP" &&
|
|
398
|
+
-f "$MIGRATION_COMPOSE_BACKUP" && ! -L "$MIGRATION_COMPOSE_BACKUP" &&
|
|
399
|
+
-f "$MIGRATION_RECEIPT" && ! -L "$MIGRATION_RECEIPT" &&
|
|
400
|
+
-f "$MARKER" && ! -L "$MARKER" &&
|
|
401
|
+
-f "$COMPOSE_FILE" && ! -L "$COMPOSE_FILE" &&
|
|
402
|
+
-f "$AUTH_CADDY" && ! -L "$AUTH_CADDY" &&
|
|
403
|
+
-d "$AUTH_ROOT" && ! -L "$AUTH_ROOT" &&
|
|
404
|
+
-d "$AUTH_ROUTES" && ! -L "$AUTH_ROUTES" ]] || return 1
|
|
405
|
+
[[ "$(marker_value AUTHENTICATED_ORIGIN 2>/dev/null || true)" == true ]] || return 1
|
|
406
|
+
[[ "$(route_inventory_digest)" == "$(receipt_value ROUTE_SNAPSHOT_SHA256)" ]] || return 1
|
|
407
|
+
[[ "$(sha256sum "$MARKER" | cut -d' ' -f1)" == "$(receipt_value MARKER_SHA256)" ]] || return 1
|
|
408
|
+
[[ "$(sha256sum "$COMPOSE_FILE" | cut -d' ' -f1)" == "$(receipt_value COMPOSE_SHA256)" ]] || return 1
|
|
409
|
+
[[ "$(sha256sum "$AUTH_CADDY" | cut -d' ' -f1)" == "$(receipt_value CADDY_SHA256)" ]] || return 1
|
|
410
|
+
[[ "$(auth_files_digest)" == "$(receipt_value AUTH_FILES_SHA256)" ]] || return 1
|
|
411
|
+
return 0
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
migration_inspect() {
|
|
415
|
+
local mode state
|
|
416
|
+
mode="$(marker_value AUTHENTICATED_ORIGIN 2>/dev/null || true)"
|
|
417
|
+
if [[ "$mode" == true ]]; then
|
|
418
|
+
if migration_pending_valid; then
|
|
419
|
+
state=pending
|
|
420
|
+
elif [[ ! -e "$MIGRATION_MARKER_BACKUP" && ! -e "$MIGRATION_COMPOSE_BACKUP" &&
|
|
421
|
+
! -e "$MIGRATION_RECEIPT" ]]; then
|
|
422
|
+
state=complete
|
|
423
|
+
else
|
|
424
|
+
echo "Authenticated-origin migration recovery evidence is incomplete or changed." >&2
|
|
425
|
+
exit 37
|
|
426
|
+
fi
|
|
427
|
+
elif [[ "$mode" == false && ! -e "$MIGRATION_MARKER_BACKUP" &&
|
|
428
|
+
! -e "$MIGRATION_COMPOSE_BACKUP" && ! -e "$MIGRATION_RECEIPT" &&
|
|
429
|
+
! -e "$AUTH_ROOT" && ! -e "$AUTH_CADDY" ]]; then
|
|
430
|
+
state=clean
|
|
431
|
+
else
|
|
432
|
+
echo "Authenticated-origin migration paths conflict with unowned or incomplete state." >&2
|
|
433
|
+
exit 37
|
|
434
|
+
fi
|
|
435
|
+
printf 'migrationState=%s\n' "$state"
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
migration_rollback_on_error() {
|
|
439
|
+
local original_status="$1" rollback_failed=false path
|
|
440
|
+
trap - ERR EXIT
|
|
441
|
+
set +e
|
|
442
|
+
if [[ -f "$MIGRATION_MARKER_BACKUP" && ! -L "$MIGRATION_MARKER_BACKUP" ]]; then
|
|
443
|
+
cp -p "$MIGRATION_MARKER_BACKUP" "$MARKER.rollback.$$" &&
|
|
444
|
+
mv -f "$MARKER.rollback.$$" "$MARKER" || rollback_failed=true
|
|
445
|
+
fi
|
|
446
|
+
if [[ -f "$MIGRATION_COMPOSE_BACKUP" && ! -L "$MIGRATION_COMPOSE_BACKUP" ]]; then
|
|
447
|
+
cp -p "$MIGRATION_COMPOSE_BACKUP" "$COMPOSE_FILE.rollback.$$" &&
|
|
448
|
+
mv -f "$COMPOSE_FILE.rollback.$$" "$COMPOSE_FILE" || rollback_failed=true
|
|
449
|
+
docker compose -f "$COMPOSE_FILE" up -d --force-recreate >/dev/null 2>&1 || rollback_failed=true
|
|
450
|
+
fi
|
|
451
|
+
if [[ -e "$AUTH_CADDY" ]]; then
|
|
452
|
+
if [[ -f "$AUTH_CADDY" && ! -L "$AUTH_CADDY" &&
|
|
453
|
+
"$(sha256sum "$AUTH_CADDY" | cut -d' ' -f1)" == "${MIGRATION_CADDY_HASH:-}" ]]; then
|
|
454
|
+
rm -f "$AUTH_CADDY"
|
|
455
|
+
docker exec "$CADDY_CONTAINER" caddy reload --config /etc/caddy/Caddyfile >/dev/null 2>&1 ||
|
|
456
|
+
rollback_failed=true
|
|
457
|
+
else
|
|
458
|
+
rollback_failed=true
|
|
459
|
+
fi
|
|
460
|
+
fi
|
|
461
|
+
local index
|
|
462
|
+
for index in "${!MIGRATION_CREATED_ROUTES[@]}"; do
|
|
463
|
+
path="${MIGRATION_CREATED_ROUTES[$index]}"
|
|
464
|
+
if [[ "$path" == "$AUTH_ROUTES/"*.caddy && -f "$path" && ! -L "$path" &&
|
|
465
|
+
"$(sha256sum "$path" | cut -d' ' -f1)" == "${MIGRATION_CREATED_ROUTE_HASHES[$index]}" ]]; then
|
|
466
|
+
rm -f "$path"
|
|
467
|
+
else
|
|
468
|
+
rollback_failed=true
|
|
469
|
+
fi
|
|
470
|
+
done
|
|
471
|
+
if [[ -e "$AUTH_SOCKET" ]]; then
|
|
472
|
+
if [[ -S "$AUTH_SOCKET" && "$(stat -c '%u:%g:%a' "$AUTH_SOCKET")" == "65532:65532:222" ]]; then
|
|
473
|
+
rm -f "$AUTH_SOCKET"
|
|
474
|
+
else
|
|
475
|
+
rollback_failed=true
|
|
476
|
+
fi
|
|
477
|
+
fi
|
|
478
|
+
if [[ -d "$AUTH_ROUTES" ]] && ! rmdir "$AUTH_ROUTES" 2>/dev/null; then rollback_failed=true; fi
|
|
479
|
+
if [[ -d "$AUTH_ROOT" ]] && ! rmdir "$AUTH_ROOT" 2>/dev/null; then rollback_failed=true; fi
|
|
480
|
+
if [[ "$rollback_failed" == false ]]; then
|
|
481
|
+
rm -f "$MIGRATION_MARKER_BACKUP" "$MIGRATION_COMPOSE_BACKUP" "$MIGRATION_RECEIPT"
|
|
482
|
+
echo "Authenticated-origin host preparation failed and was rolled back." >&2
|
|
483
|
+
else
|
|
484
|
+
echo "Authenticated-origin host preparation failed; rollback is incomplete and recovery evidence was retained." >&2
|
|
485
|
+
fi
|
|
486
|
+
exit "$original_status"
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
migrate_authenticated_origin() {
|
|
490
|
+
local expected_route_digest="$EXPECTED" state path route_spec
|
|
491
|
+
[[ "$AUTHENTICATED_ORIGIN" == true ]] || { echo "Authenticated-origin migration must opt in explicitly." >&2; exit 37; }
|
|
492
|
+
[[ "$(status | sed -n 's/^state=//p' | head -n1)" == ready &&
|
|
493
|
+
"$(marker_value AUTHENTICATED_ORIGIN 2>/dev/null || true)" == false ]] || {
|
|
494
|
+
echo "Authenticated-origin migration requires a ready legacy scope." >&2; exit 37;
|
|
495
|
+
}
|
|
496
|
+
[[ "$(route_inventory_digest)" == "$expected_route_digest" ]] || {
|
|
497
|
+
echo "Managed public-route inventory changed after planning." >&2; exit 37;
|
|
498
|
+
}
|
|
499
|
+
state="$(migration_inspect | sed -n 's/^migrationState=//p')"
|
|
500
|
+
[[ "$state" == clean ]] || { echo "Authenticated-origin migration is not in a clean host state." >&2; exit 37; }
|
|
501
|
+
parse_meta
|
|
502
|
+
[[ "$(marker_value ACCOUNT_ID)" == "$M_ACCOUNT" &&
|
|
503
|
+
"$(marker_value ZONE_ID)" == "$M_ZONE_ID" &&
|
|
504
|
+
"$(marker_value ZONE_NAME)" == "$M_ZONE_NAME" &&
|
|
505
|
+
"$(marker_value TUNNEL_ID)" == "$M_TUNNEL_ID" &&
|
|
506
|
+
"$(marker_value TUNNEL_NAME)" == "$M_TUNNEL_NAME" ]] || {
|
|
507
|
+
echo "Existing host tunnel binding changed after planning." >&2; exit 37;
|
|
508
|
+
}
|
|
509
|
+
[[ -f "$MARKER" && ! -L "$MARKER" && -f "$COMPOSE_FILE" && ! -L "$COMPOSE_FILE" &&
|
|
510
|
+
-f "$TOKEN_FILE" && ! -L "$TOKEN_FILE" && -d "$ROUTE_ROOT" && ! -L "$ROUTE_ROOT" ]] || {
|
|
511
|
+
echo "Existing ingress host files are incomplete or unsafe." >&2; exit 37;
|
|
512
|
+
}
|
|
513
|
+
local route_count=0
|
|
514
|
+
MIGRATION_CREATED_ROUTES=()
|
|
515
|
+
MIGRATION_CREATED_ROUTE_HASHES=()
|
|
516
|
+
trap 'migration_rollback_on_error "$?"' EXIT
|
|
517
|
+
cp -p "$MARKER" "$MIGRATION_MARKER_BACKUP"
|
|
518
|
+
cp -p "$COMPOSE_FILE" "$MIGRATION_COMPOSE_BACKUP"
|
|
519
|
+
chmod 0600 "$MIGRATION_MARKER_BACKUP" "$MIGRATION_COMPOSE_BACKUP"
|
|
520
|
+
setup_authenticated_origin
|
|
521
|
+
AUTHENTICATED_ORIGIN=true
|
|
522
|
+
write_marker
|
|
523
|
+
write_compose
|
|
524
|
+
for path in "$ROUTE_ROOT"/*.route; do
|
|
525
|
+
[[ -f "$path" ]] || continue
|
|
526
|
+
route_spec="$(printf '%s\t%s\t%s\t%s\t%s\t%s\t%s' \
|
|
527
|
+
"$(route_value "$path" SCHEMA_VERSION)" "$(route_value "$path" OWNER_REPO)" \
|
|
528
|
+
"$(route_value "$path" ENVIRONMENT)" "$(route_value "$path" APP_SLUG)" \
|
|
529
|
+
"$(route_value "$path" ROUTE_ID)" "$(route_value "$path" PUBLIC_HOSTNAME)" \
|
|
530
|
+
"$(route_value "$path" ORIGIN_HOSTNAME)")"
|
|
531
|
+
parse_spec_value "$route_spec"
|
|
532
|
+
[[ "$(basename "$path")" == "$R_ROUTE.route" ]] || {
|
|
533
|
+
echo "Managed public-route identity is malformed." >&2; exit 37;
|
|
534
|
+
}
|
|
535
|
+
SPEC="$route_spec"
|
|
536
|
+
auth_route_write
|
|
537
|
+
MIGRATION_CREATED_ROUTES+=("$(auth_route_path "$R_ROUTE")")
|
|
538
|
+
MIGRATION_CREATED_ROUTE_HASHES+=("$(sha256sum "$(auth_route_path "$R_ROUTE")" | cut -d' ' -f1)")
|
|
539
|
+
route_count=$((route_count + 1))
|
|
540
|
+
done
|
|
541
|
+
(( route_count > 0 )) || { echo "No managed public routes are available to migrate." >&2; exit 37; }
|
|
542
|
+
docker compose -f "$COMPOSE_FILE" up -d --force-recreate >/dev/null
|
|
543
|
+
[[ "$(status | sed -n 's/^state=//p' | head -n1)" == ready &&
|
|
544
|
+
"$(marker_value AUTHENTICATED_ORIGIN)" == true ]] || {
|
|
545
|
+
echo "Authenticated-origin connector did not become ready." >&2; exit 37;
|
|
546
|
+
}
|
|
547
|
+
cat > "$MIGRATION_RECEIPT.tmp.$$" <<EOF
|
|
548
|
+
ROUTE_SNAPSHOT_SHA256=$expected_route_digest
|
|
549
|
+
MARKER_SHA256=$(sha256sum "$MARKER" | cut -d' ' -f1)
|
|
550
|
+
COMPOSE_SHA256=$(sha256sum "$COMPOSE_FILE" | cut -d' ' -f1)
|
|
551
|
+
CADDY_SHA256=$(sha256sum "$AUTH_CADDY" | cut -d' ' -f1)
|
|
552
|
+
AUTH_FILES_SHA256=$(auth_files_digest)
|
|
553
|
+
EOF
|
|
554
|
+
chmod 0600 "$MIGRATION_RECEIPT.tmp.$$"
|
|
555
|
+
mv -f "$MIGRATION_RECEIPT.tmp.$$" "$MIGRATION_RECEIPT"
|
|
556
|
+
migration_pending_valid || { echo "Authenticated-origin recovery receipt validation failed." >&2; exit 37; }
|
|
557
|
+
trap - ERR EXIT
|
|
558
|
+
printf 'migrationState=pending\n'
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
rollback_authenticated_origin() {
|
|
562
|
+
migration_pending_valid || { echo "Authenticated-origin migration rollback evidence is stale or unsafe." >&2; exit 38; }
|
|
563
|
+
[[ "$(status | sed -n 's/^state=//p' | head -n1)" == ready ]] || {
|
|
564
|
+
echo "Authenticated-origin host is not ready for rollback." >&2; exit 38;
|
|
565
|
+
}
|
|
566
|
+
cp -p "$MIGRATION_MARKER_BACKUP" "$MARKER.rollback.$$"
|
|
567
|
+
mv -f "$MARKER.rollback.$$" "$MARKER"
|
|
568
|
+
cp -p "$MIGRATION_COMPOSE_BACKUP" "$COMPOSE_FILE.rollback.$$"
|
|
569
|
+
mv -f "$COMPOSE_FILE.rollback.$$" "$COMPOSE_FILE"
|
|
570
|
+
docker compose -f "$COMPOSE_FILE" up -d --force-recreate >/dev/null
|
|
571
|
+
[[ "$(status | sed -n 's/^state=//p' | head -n1)" == ready &&
|
|
572
|
+
"$(marker_value AUTHENTICATED_ORIGIN 2>/dev/null || true)" == false ]] || {
|
|
573
|
+
echo "Legacy connector did not become ready after authenticated-origin rollback." >&2; exit 38;
|
|
574
|
+
}
|
|
575
|
+
[[ -f "$AUTH_CADDY" && ! -L "$AUTH_CADDY" ]] || {
|
|
576
|
+
echo "Authenticated Caddy configuration changed before rollback." >&2; exit 38;
|
|
577
|
+
}
|
|
578
|
+
rm -f "$AUTH_CADDY"
|
|
579
|
+
docker exec "$CADDY_CONTAINER" caddy reload --config /etc/caddy/Caddyfile >/dev/null
|
|
580
|
+
for path in "$AUTH_ROUTES"/*.caddy; do [[ -f "$path" ]] && rm -f "$path"; done
|
|
581
|
+
if [[ -e "$AUTH_SOCKET" ]]; then
|
|
582
|
+
[[ -S "$AUTH_SOCKET" && "$(stat -c '%u:%g:%a' "$AUTH_SOCKET")" == "65532:65532:222" ]] || {
|
|
583
|
+
echo "Authenticated origin socket changed before rollback." >&2; exit 38;
|
|
584
|
+
}
|
|
585
|
+
rm -f "$AUTH_SOCKET"
|
|
586
|
+
fi
|
|
587
|
+
rmdir "$AUTH_ROUTES" "$AUTH_ROOT"
|
|
588
|
+
rm -f "$MIGRATION_MARKER_BACKUP" "$MIGRATION_COMPOSE_BACKUP" "$MIGRATION_RECEIPT"
|
|
589
|
+
printf 'migrationState=rolled-back\n'
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
finalize_authenticated_origin() {
|
|
593
|
+
migration_pending_valid || { echo "Authenticated-origin migration receipt is stale or unsafe." >&2; exit 39; }
|
|
594
|
+
[[ "$(status | sed -n 's/^state=//p' | head -n1)" == ready ]] || {
|
|
595
|
+
echo "Authenticated-origin host is not ready for finalization." >&2; exit 39;
|
|
596
|
+
}
|
|
597
|
+
rm -f "$MIGRATION_MARKER_BACKUP" "$MIGRATION_COMPOSE_BACKUP" "$MIGRATION_RECEIPT"
|
|
598
|
+
printf 'migrationState=complete\n'
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
verify_authenticated_origin() {
|
|
602
|
+
local expected_route_digest="$EXPECTED" path route_spec
|
|
603
|
+
[[ "$(status | sed -n 's/^state=//p' | head -n1)" == ready &&
|
|
604
|
+
"$(marker_value AUTHENTICATED_ORIGIN 2>/dev/null || true)" == true &&
|
|
605
|
+
-S "$AUTH_SOCKET" && -d "$AUTH_ROOT" && ! -L "$AUTH_ROOT" &&
|
|
606
|
+
"$(stat -c '%u:%g:%a' "$AUTH_ROOT")" == "65532:65532:700" &&
|
|
607
|
+
"$(stat -c '%u:%g:%a' "$AUTH_SOCKET")" == "65532:65532:222" ]] || {
|
|
608
|
+
echo "Authenticated-origin socket or scope is not ready." >&2; exit 40;
|
|
609
|
+
}
|
|
610
|
+
[[ "$(route_inventory_digest)" == "$expected_route_digest" ]] || {
|
|
611
|
+
echo "Managed public-route inventory changed during migration." >&2; exit 40;
|
|
612
|
+
}
|
|
613
|
+
grep -Fqx " - $AUTH_ROOT:/run/project-forge-origin:ro" "$COMPOSE_FILE" || {
|
|
614
|
+
echo "Connector Compose file does not grant the exact read-only origin mount." >&2; exit 40;
|
|
615
|
+
}
|
|
616
|
+
path_overlap() {
|
|
617
|
+
local left="${1%/}" right="${2%/}"
|
|
618
|
+
[[ "$left" == "$right" || "$left" == "$right/"* || "$right" == "$left/"* ]]
|
|
619
|
+
}
|
|
620
|
+
local connector_id caddy_id mount_count other_id mounts source destination writable source_mount
|
|
621
|
+
connector_id="$(docker inspect --format '{{.Id}}' "$CONTAINER")"
|
|
622
|
+
caddy_id="$(docker inspect --format '{{.Id}}' "$CADDY_CONTAINER")"
|
|
623
|
+
mapfile -t mounts < <(docker inspect --format '{{range .Mounts}}{{printf "%s|%s|%t\n" .Source .Destination .RW}}{{end}}' "$CONTAINER")
|
|
624
|
+
mount_count=0
|
|
625
|
+
for source_mount in "${mounts[@]}"; do
|
|
626
|
+
IFS='|' read -r source destination writable <<< "$source_mount"
|
|
627
|
+
if path_overlap "$source" "$AUTH_ROOT" || path_overlap "$destination" "/run/project-forge-origin"; then
|
|
628
|
+
[[ "$source" == "$AUTH_ROOT" && "$destination" == "/run/project-forge-origin" &&
|
|
629
|
+
"$writable" == false ]] || {
|
|
630
|
+
echo "Connector has an unexpected mount overlapping the authenticated-origin socket." >&2; exit 40;
|
|
631
|
+
}
|
|
632
|
+
mount_count=$((mount_count + 1))
|
|
633
|
+
fi
|
|
634
|
+
done
|
|
635
|
+
[[ "$mount_count" == 1 ]] || {
|
|
636
|
+
echo "Running connector does not have exactly the expected read-only origin mount." >&2; exit 40;
|
|
637
|
+
}
|
|
638
|
+
while IFS= read -r other_id; do
|
|
639
|
+
[[ -n "$other_id" && "$other_id" != "$connector_id" && "$other_id" != "$caddy_id" ]] || continue
|
|
640
|
+
mapfile -t mounts < <(docker inspect --format '{{range .Mounts}}{{printf "%s|%s\n" .Source .Destination}}{{end}}' "$other_id")
|
|
641
|
+
for source_mount in "${mounts[@]}"; do
|
|
642
|
+
IFS='|' read -r source destination <<< "$source_mount"
|
|
643
|
+
if path_overlap "$source" "$AUTH_ROOT" ||
|
|
644
|
+
path_overlap "$destination" "/run/project-forge-origin"; then
|
|
645
|
+
echo "Authenticated-origin mount is present on a non-connector container." >&2
|
|
646
|
+
exit 40
|
|
647
|
+
fi
|
|
648
|
+
done
|
|
649
|
+
done < <(docker ps -aq)
|
|
650
|
+
[[ -f "$AUTH_CADDY" && ! -L "$AUTH_CADDY" ]] &&
|
|
651
|
+
grep -Fqx " bind unix//data/authenticated/$SCOPE/origin.sock|0222" "$AUTH_CADDY" &&
|
|
652
|
+
grep -Fqx " import /data/authenticated/$SCOPE/routes/*.caddy" "$AUTH_CADDY" || {
|
|
653
|
+
echo "Authenticated Caddy Unix listener configuration is missing or changed." >&2; exit 40;
|
|
654
|
+
}
|
|
655
|
+
docker exec "$CADDY_CONTAINER" caddy validate --config /etc/caddy/Caddyfile >/dev/null
|
|
656
|
+
local route_count=0
|
|
657
|
+
for path in "$ROUTE_ROOT"/*.route; do
|
|
658
|
+
[[ -f "$path" ]] || continue
|
|
659
|
+
route_spec="$(printf '%s\t%s\t%s\t%s\t%s\t%s\t%s' \
|
|
660
|
+
"$(route_value "$path" SCHEMA_VERSION)" "$(route_value "$path" OWNER_REPO)" \
|
|
661
|
+
"$(route_value "$path" ENVIRONMENT)" "$(route_value "$path" APP_SLUG)" \
|
|
662
|
+
"$(route_value "$path" ROUTE_ID)" "$(route_value "$path" PUBLIC_HOSTNAME)" \
|
|
663
|
+
"$(route_value "$path" ORIGIN_HOSTNAME)")"
|
|
664
|
+
parse_spec_value "$route_spec"
|
|
665
|
+
auth_route_verify_lan
|
|
666
|
+
local expected_file tmp
|
|
667
|
+
expected_file="$(auth_route_path "$R_ROUTE")"
|
|
668
|
+
[[ -f "$expected_file" && ! -L "$expected_file" ]] || {
|
|
669
|
+
echo "Authenticated Caddy route file is missing or unsafe." >&2; exit 40;
|
|
670
|
+
}
|
|
671
|
+
tmp="$AUTH_ROUTES/.verify-$R_ROUTE.$$"
|
|
672
|
+
auth_route_content > "$tmp"
|
|
673
|
+
if ! cmp -s "$tmp" "$expected_file"; then
|
|
674
|
+
rm -f "$tmp"
|
|
675
|
+
echo "Authenticated Caddy route differs from the sanitized reviewed contract." >&2
|
|
676
|
+
exit 40
|
|
677
|
+
fi
|
|
678
|
+
rm -f "$tmp"
|
|
679
|
+
route_count=$((route_count + 1))
|
|
680
|
+
done
|
|
681
|
+
printf 'evidenceLevel=structural-only\n'
|
|
682
|
+
printf 'connectorOriginMount=read-only\n'
|
|
683
|
+
printf 'socketMetadataVerified=true\n'
|
|
684
|
+
printf 'caddyConfigValidated=true\n'
|
|
685
|
+
printf 'sanitizedRouteConfigVerified=true\n'
|
|
686
|
+
printf 'managedRouteCount=%s\n' "$route_count"
|
|
687
|
+
}
|
|
688
|
+
|
|
367
689
|
metadata_matches() {
|
|
368
690
|
local path="$1" encoded="$2"
|
|
369
691
|
parse_spec_value "$encoded"
|
|
@@ -449,6 +771,21 @@ case "$ACTION" in
|
|
|
449
771
|
route-inventory)
|
|
450
772
|
route_inventory
|
|
451
773
|
;;
|
|
774
|
+
migration-inspect)
|
|
775
|
+
migration_inspect
|
|
776
|
+
;;
|
|
777
|
+
migrate-authenticated)
|
|
778
|
+
migrate_authenticated_origin
|
|
779
|
+
;;
|
|
780
|
+
rollback-authenticated)
|
|
781
|
+
rollback_authenticated_origin
|
|
782
|
+
;;
|
|
783
|
+
finalize-authenticated)
|
|
784
|
+
finalize_authenticated_origin
|
|
785
|
+
;;
|
|
786
|
+
verify-authenticated)
|
|
787
|
+
verify_authenticated_origin
|
|
788
|
+
;;
|
|
452
789
|
route-register|route-remove)
|
|
453
790
|
exec 9>>"$LOCK_FILE"
|
|
454
791
|
flock -n 9 || { echo "Another Cloudflare ingress operation holds the lock." >&2; exit 75; }
|
|
@@ -9,14 +9,18 @@ import sys
|
|
|
9
9
|
from typing import Any
|
|
10
10
|
|
|
11
11
|
from .cloudflare_ingress import (
|
|
12
|
+
AUTHENTICATED_ORIGIN_CHANGE_FREEZE_PHRASE,
|
|
13
|
+
AUTHENTICATED_ORIGIN_MIGRATION_PHRASE,
|
|
12
14
|
CLOUDFLARE_API_TOKEN_ENV,
|
|
13
15
|
HOST_PUBLIC_INGRESS_WRITE_PHRASE,
|
|
14
16
|
PUBLIC_INGRESS_ROUTE_REMOVE_PHRASE,
|
|
15
17
|
PUBLIC_INGRESS_ROUTE_WRITE_PHRASE,
|
|
18
|
+
apply_authenticated_origin_migration,
|
|
16
19
|
apply_cloudflare_ingress,
|
|
17
20
|
cloudflare_ingress_doctor,
|
|
18
21
|
cloudflare_ingress_status,
|
|
19
22
|
cloudflare_route_inventory,
|
|
23
|
+
plan_authenticated_origin_migration,
|
|
20
24
|
plan_cloudflare_ingress,
|
|
21
25
|
plan_public_route,
|
|
22
26
|
plan_public_route_removal,
|
|
@@ -50,7 +54,7 @@ def _token() -> str:
|
|
|
50
54
|
|
|
51
55
|
|
|
52
56
|
def _confirm(args: argparse.Namespace, phrase: str, action: str) -> None:
|
|
53
|
-
if args
|
|
57
|
+
if getattr(args, "yes", False):
|
|
54
58
|
return
|
|
55
59
|
entered = input(f"Type {phrase} to {action}: ").strip()
|
|
56
60
|
if entered != phrase:
|
|
@@ -81,6 +85,15 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
81
85
|
configure.add_argument("--json", action="store_true")
|
|
82
86
|
configure.add_argument("--yes", action="store_true")
|
|
83
87
|
|
|
88
|
+
migrate = commands.add_parser(
|
|
89
|
+
"migrate-authenticated-origin",
|
|
90
|
+
help="Migrate an existing managed scope to connector-exclusive Unix-socket origins",
|
|
91
|
+
)
|
|
92
|
+
migrate.add_argument("name")
|
|
93
|
+
migrate.add_argument("--scope", required=True)
|
|
94
|
+
migrate.add_argument("--zone", required=True, help="Existing bound Cloudflare zone")
|
|
95
|
+
migrate.add_argument("--json", action="store_true")
|
|
96
|
+
|
|
84
97
|
route = commands.add_parser("route", help="Project-owned public hostname/DNS routes")
|
|
85
98
|
route_commands = route.add_subparsers(dest="route_command", required=True)
|
|
86
99
|
|
|
@@ -143,6 +156,43 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
143
156
|
)
|
|
144
157
|
_print(apply_cloudflare_ingress(plan, api_token=token), as_json=args.json)
|
|
145
158
|
return 0
|
|
159
|
+
if args.command == "migrate-authenticated-origin":
|
|
160
|
+
token = _token()
|
|
161
|
+
preplan_freeze_acknowledgement = (
|
|
162
|
+
f"{AUTHENTICATED_ORIGIN_CHANGE_FREEZE_PHRASE} ACTIVE scope={args.scope}"
|
|
163
|
+
)
|
|
164
|
+
_confirm(
|
|
165
|
+
args,
|
|
166
|
+
preplan_freeze_acknowledgement,
|
|
167
|
+
"confirm that all external Cloudflare tunnel-configuration writers are frozen for this scope before planning",
|
|
168
|
+
)
|
|
169
|
+
plan = plan_authenticated_origin_migration(
|
|
170
|
+
args.name,
|
|
171
|
+
scope=args.scope,
|
|
172
|
+
zone_name=args.zone,
|
|
173
|
+
api_token=token,
|
|
174
|
+
change_freeze_active_acknowledgement=preplan_freeze_acknowledgement,
|
|
175
|
+
)
|
|
176
|
+
_print(plan, as_json=False)
|
|
177
|
+
_confirm(
|
|
178
|
+
args,
|
|
179
|
+
AUTHENTICATED_ORIGIN_MIGRATION_PHRASE,
|
|
180
|
+
f"migrate existing Cloudflare ingress scope {args.scope} on target {args.name}",
|
|
181
|
+
)
|
|
182
|
+
_confirm(
|
|
183
|
+
args,
|
|
184
|
+
plan["requiredChangeFreezeAcknowledgement"],
|
|
185
|
+
"acknowledge the operator-controlled Cloudflare change freeze for this migration plan",
|
|
186
|
+
)
|
|
187
|
+
_print(
|
|
188
|
+
apply_authenticated_origin_migration(
|
|
189
|
+
plan,
|
|
190
|
+
api_token=token,
|
|
191
|
+
change_freeze_acknowledgement=plan["requiredChangeFreezeAcknowledgement"],
|
|
192
|
+
),
|
|
193
|
+
as_json=args.json,
|
|
194
|
+
)
|
|
195
|
+
return 0
|
|
146
196
|
if args.command == "route" and args.route_command == "inventory":
|
|
147
197
|
_print(cloudflare_route_inventory(args.name, args.scope), as_json=args.json)
|
|
148
198
|
return 0
|