@ouro.bot/cli 0.1.0-alpha.797 → 0.1.0-alpha.799
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.json +15 -0
- package/deploy/unraid/Dockerfile +2 -2
- package/deploy/unraid/README.txt +693 -215
- package/deploy/unraid/docker-man-template-transaction.mjs +526 -0
- package/deploy/unraid/docker-man-template-xml.cjs +105 -0
- package/deploy/unraid/migrate-sanctuary-bundle.mjs +56 -17
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.ouro/tool-profiles.json +2 -2
- package/deploy/unraid/sanctuary.xml +2 -2
- package/dist/heart/daemon/container-healthcheck.js +16 -0
- package/dist/heart/daemon/container-spec-auditor-main.js +23 -13
- package/dist/heart/daemon/container-spec-auditor.js +93 -33
- package/dist/heart/daemon/daemon-bootstrap-startup.js +38 -8
- package/dist/heart/daemon/daemon-entry.js +12 -1
- package/dist/heart/daemon/sanctuary-bundle-migration.js +411 -81
- package/dist/heart/daemon/sanctuary-package-management.js +103 -0
- package/dist/repertoire/tools-unraid.js +2 -1
- package/dist/senses/sanctuary-runtime.js +7 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/deploy/unraid/README.txt
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
Mendelow Cloud Butler operator runbook
|
|
2
2
|
|
|
3
|
-
The production container is ouro-butler. It runs as UID/GID 10001, publishes no
|
|
4
|
-
ports, uses host networking only so its loopback-only Unraid GraphQL client can
|
|
5
|
-
reach 127.0.0.1, mounts the runtime and sanctuary.ouro bundle read-write plus
|
|
6
|
-
the privileged event spool and SAB configuration read-only, and uses restart
|
|
7
|
-
policy unless-stopped.
|
|
3
|
+
The production container is ouro-butler. It runs as UID/GID 10001, publishes no ports, uses host networking only so its loopback-only Unraid GraphQL client can reach 127.0.0.1, mounts the runtime and sanctuary.ouro bundle read-write plus the privileged event spool read-only, and uses restart policy unless-stopped.
|
|
8
4
|
|
|
9
5
|
Start/stop:
|
|
10
6
|
docker start ouro-butler
|
|
@@ -26,12 +22,18 @@ Effective-spec audit helper:
|
|
|
26
22
|
AUDIT_EXPECTED_IMAGE=$2
|
|
27
23
|
AUDIT_RUNNER_IMAGE_ID=$3
|
|
28
24
|
AUDIT_MOUNT_CONTRACT=${4-canonical}
|
|
25
|
+
AUDIT_EXPECTED_IMAGE_REFERENCE=${5-}
|
|
26
|
+
AUDIT_EXPECTED_ICON=${6-}
|
|
29
27
|
validate_exact_image_id "$AUDIT_EXPECTED_IMAGE" || return $?
|
|
30
28
|
validate_exact_image_id "$AUDIT_RUNNER_IMAGE_ID" || return $?
|
|
31
29
|
test "$AUDIT_RUNNER_IMAGE_ID" != sha256:681449ad47a2621705cd339b481e6339236b31dc65e195b1cf5025d0f2191d7d || return $?
|
|
32
30
|
case "$AUDIT_MOUNT_CONTRACT" in
|
|
33
|
-
canonical)
|
|
34
|
-
|
|
31
|
+
canonical)
|
|
32
|
+
test -n "$AUDIT_EXPECTED_IMAGE_REFERENCE" || return 1
|
|
33
|
+
test "$AUDIT_EXPECTED_ICON" = https://raw.githubusercontent.com/ourostack/ouroboros/main/assets/ouroboros.png || return 1
|
|
34
|
+
set -- --expected-image-reference "$AUDIT_EXPECTED_IMAGE_REFERENCE" --expected-icon "$AUDIT_EXPECTED_ICON"
|
|
35
|
+
;;
|
|
36
|
+
legacy-alpha742|prepackage-alpha797) set -- --mount-contract "$AUDIT_MOUNT_CONTRACT" ;;
|
|
35
37
|
*) return 1 ;;
|
|
36
38
|
esac
|
|
37
39
|
INSPECT_DIR=$(mktemp -d /mnt/user/appdata/ouro-butler/staging/inspect.XXXXXX) || return $?
|
|
@@ -133,7 +135,7 @@ Effective-spec audit helper:
|
|
|
133
135
|
set_butler_autostart() {
|
|
134
136
|
(
|
|
135
137
|
AUTOSTART_PROFILE=$1
|
|
136
|
-
case "$AUTOSTART_PROFILE" in disabled|production) ;; *) return 1 ;; esac
|
|
138
|
+
case "$AUTOSTART_PROFILE" in disabled|production|staging) ;; *) return 1 ;; esac
|
|
137
139
|
test "$(id -u)" -eq 0 || return $?
|
|
138
140
|
AUTOSTART_FILE=/var/lib/docker/unraid-autostart
|
|
139
141
|
AUTOSTART_INCLUDE=/usr/local/emhttp/plugins/dynamix.docker.manager/include/UpdateConfig.php
|
|
@@ -145,9 +147,10 @@ Effective-spec audit helper:
|
|
|
145
147
|
set -- $AUTOSTART_COUNTS
|
|
146
148
|
test "$#" -eq 4 || return $?
|
|
147
149
|
for AUTOSTART_COUNT in "$@"; do case "$AUTOSTART_COUNT" in 0|1) ;; *) return 1 ;; esac; done
|
|
148
|
-
|
|
149
|
-
docker container inspect ouro-butler >/dev/null 2>&1 || return $?
|
|
150
|
-
|
|
150
|
+
case "$AUTOSTART_PROFILE" in
|
|
151
|
+
production) docker container inspect ouro-butler >/dev/null 2>&1 || return $? ;;
|
|
152
|
+
staging) docker container inspect ouro-butler-staging >/dev/null 2>&1 || return $? ;;
|
|
153
|
+
esac
|
|
151
154
|
AUTOSTART_BACKUP=$(mktemp /var/lib/docker/.unraid-autostart.ouro.XXXXXX) || return $?
|
|
152
155
|
AUTOSTART_NONBUTLER_BEFORE=$(mktemp /run/unraid-autostart.nonbutler-before.XXXXXX) || { rm -f -- "$AUTOSTART_BACKUP"; return 1; }
|
|
153
156
|
AUTOSTART_NONBUTLER_AFTER=$(mktemp /run/unraid-autostart.nonbutler-after.XXXXXX) || { rm -f -- "$AUTOSTART_BACKUP" "$AUTOSTART_NONBUTLER_BEFORE"; return 1; }
|
|
@@ -189,16 +192,21 @@ Effective-spec audit helper:
|
|
|
189
192
|
sync -f /var/lib/docker || return $?
|
|
190
193
|
AUTOSTART_BACKUP_READY=yes
|
|
191
194
|
snapshot_nonbutler_autostart >"$AUTOSTART_NONBUTLER_BEFORE" || return $?
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
mutate_butler_autostart
|
|
200
|
-
|
|
201
|
-
|
|
195
|
+
case "$AUTOSTART_PROFILE" in
|
|
196
|
+
production) AUTOSTART_TARGET=ouro-butler ;;
|
|
197
|
+
staging) AUTOSTART_TARGET=ouro-butler-staging ;;
|
|
198
|
+
disabled) AUTOSTART_TARGET=- ;;
|
|
199
|
+
esac
|
|
200
|
+
for AUTOSTART_CONTAINER in ouro-butler ouro-butler-staging ouro-butler-rollback ouro-butler-legacy-evidence; do
|
|
201
|
+
test "$AUTOSTART_CONTAINER" = "$AUTOSTART_TARGET" && continue
|
|
202
|
+
mutate_butler_autostart "$AUTOSTART_CONTAINER" false || return $?
|
|
203
|
+
done
|
|
204
|
+
test "$AUTOSTART_TARGET" = - || mutate_butler_autostart "$AUTOSTART_TARGET" true || return $?
|
|
205
|
+
case "$AUTOSTART_PROFILE" in
|
|
206
|
+
production) AUTOSTART_EXPECTED_COUNTS="1 0 0 0" ;;
|
|
207
|
+
staging) AUTOSTART_EXPECTED_COUNTS="0 1 0 0" ;;
|
|
208
|
+
disabled) AUTOSTART_EXPECTED_COUNTS="0 0 0 0" ;;
|
|
209
|
+
esac
|
|
202
210
|
verify_butler_autostart "$AUTOSTART_EXPECTED_COUNTS" || return $?
|
|
203
211
|
snapshot_nonbutler_autostart >"$AUTOSTART_NONBUTLER_AFTER" || return $?
|
|
204
212
|
cmp -s -- "$AUTOSTART_NONBUTLER_BEFORE" "$AUTOSTART_NONBUTLER_AFTER" || return 1
|
|
@@ -336,19 +344,50 @@ Effective-spec audit helper:
|
|
|
336
344
|
esac
|
|
337
345
|
assert_only_running_butler ouro-butler || return $?
|
|
338
346
|
}
|
|
347
|
+
assert_sanctuary_update_source_pin() {
|
|
348
|
+
(
|
|
349
|
+
PIN_SOURCE_CONTAINER=$1
|
|
350
|
+
PIN_SOURCE_IMAGE_ID=$2
|
|
351
|
+
validate_exact_image_id "$PIN_SOURCE_IMAGE_ID" || return $?
|
|
352
|
+
test "$(docker inspect --format '{{.Image}}' "$PIN_SOURCE_CONTAINER")" = "$PIN_SOURCE_IMAGE_ID" || return $?
|
|
353
|
+
case "$PIN_SOURCE_CONTAINER $PIN_SOURCE_IMAGE_ID" in
|
|
354
|
+
"ouro-butler sha256:681449ad47a2621705cd339b481e6339236b31dc65e195b1cf5025d0f2191d7d"|"ouro-butler sha256:e337dff04c92d116b269052f473b26a47eea933d017d1befc73af50dd37bb08d"|"ouro-butler-staging sha256:e337dff04c92d116b269052f473b26a47eea933d017d1befc73af50dd37bb08d") return 0 ;;
|
|
355
|
+
"ouro-butler "*) ;;
|
|
356
|
+
*) return 1 ;;
|
|
357
|
+
esac
|
|
358
|
+
PIN_SOURCE_IMAGE_REFERENCE=$(docker inspect --format '{{.Config.Image}}' "$PIN_SOURCE_CONTAINER") || return $?
|
|
359
|
+
printf '%s\n' "$PIN_SOURCE_IMAGE_REFERENCE" | grep -Eq '^ghcr\.io/ourostack/ouroboros-butler:[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$' || return $?
|
|
360
|
+
test "$(docker image inspect --format '{{.Id}}' "$PIN_SOURCE_IMAGE_REFERENCE")" = "$PIN_SOURCE_IMAGE_ID" || return $?
|
|
361
|
+
PIN_SOURCE_IMAGE_ORIGIN=$(docker image inspect --format '{{with .Config.Labels}}{{index . "org.opencontainers.image.source"}}{{end}}' "$PIN_SOURCE_IMAGE_ID") || return $?
|
|
362
|
+
test "$PIN_SOURCE_IMAGE_ORIGIN" = https://github.com/ourostack/ouroboros || return $?
|
|
363
|
+
test "$(docker inspect --format '{{with .Config.Labels}}{{index . "net.unraid.docker.managed"}}{{end}}' "$PIN_SOURCE_CONTAINER")" = dockerman || return $?
|
|
364
|
+
test "$(docker inspect --format '{{with .Config.Labels}}{{index . "net.unraid.docker.icon"}}{{end}}' "$PIN_SOURCE_CONTAINER")" = https://raw.githubusercontent.com/ourostack/ouroboros/main/assets/ouroboros.png
|
|
365
|
+
)
|
|
366
|
+
}
|
|
339
367
|
assert_legacy_alpha742_source() {
|
|
340
368
|
EXPECTED_SOURCE_IMAGE_ID=$1
|
|
341
369
|
AUDIT_RUNNER_IMAGE_ID=$2
|
|
342
|
-
|
|
370
|
+
assert_sanctuary_update_source_pin ouro-butler "$EXPECTED_SOURCE_IMAGE_ID" || return $?
|
|
343
371
|
audit_effective ouro-butler "$EXPECTED_SOURCE_IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" legacy-alpha742
|
|
344
372
|
}
|
|
373
|
+
assert_prepackage_alpha797_source() {
|
|
374
|
+
EXPECTED_SOURCE_IMAGE_ID=$1
|
|
375
|
+
AUDIT_RUNNER_IMAGE_ID=$2
|
|
376
|
+
SOURCE_CONTAINER=$3
|
|
377
|
+
assert_sanctuary_update_source_pin "$SOURCE_CONTAINER" "$EXPECTED_SOURCE_IMAGE_ID" || return $?
|
|
378
|
+
audit_effective "$SOURCE_CONTAINER" "$EXPECTED_SOURCE_IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" prepackage-alpha797
|
|
379
|
+
}
|
|
345
380
|
assert_update_source() {
|
|
346
381
|
EXPECTED_SOURCE_IMAGE_ID=$1
|
|
347
382
|
AUDIT_RUNNER_IMAGE_ID=$2
|
|
348
383
|
if test "$EXPECTED_SOURCE_IMAGE_ID" = sha256:681449ad47a2621705cd339b481e6339236b31dc65e195b1cf5025d0f2191d7d; then
|
|
349
384
|
assert_legacy_alpha742_source "$EXPECTED_SOURCE_IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID"
|
|
385
|
+
elif test "$EXPECTED_SOURCE_IMAGE_ID" = sha256:e337dff04c92d116b269052f473b26a47eea933d017d1befc73af50dd37bb08d; then
|
|
386
|
+
assert_prepackage_alpha797_source "$EXPECTED_SOURCE_IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" ouro-butler
|
|
350
387
|
else
|
|
351
|
-
|
|
388
|
+
assert_sanctuary_update_source_pin ouro-butler "$EXPECTED_SOURCE_IMAGE_ID" || return $?
|
|
389
|
+
EXPECTED_SOURCE_IMAGE_REFERENCE=$(docker inspect --format '{{.Config.Image}}' ouro-butler) || return $?
|
|
390
|
+
audit_effective ouro-butler "$EXPECTED_SOURCE_IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" canonical "$EXPECTED_SOURCE_IMAGE_REFERENCE" https://raw.githubusercontent.com/ourostack/ouroboros/main/assets/ouroboros.png
|
|
352
391
|
fi
|
|
353
392
|
}
|
|
354
393
|
validate_sanctuary_roots() {
|
|
@@ -576,6 +615,7 @@ Effective-spec audit helper:
|
|
|
576
615
|
test -n "${BACKUP_ROOT-}" || return 1
|
|
577
616
|
test -n "${IMAGE_ID-}" || return 1
|
|
578
617
|
test -n "${AUDIT_RUNNER_IMAGE_ID-}" || return 1
|
|
618
|
+
test -n "${RESTORE_VERSION_IMAGE-}" || return 1
|
|
579
619
|
case "$BACKUP_ROOT" in
|
|
580
620
|
/*) ;;
|
|
581
621
|
*) return 1 ;;
|
|
@@ -589,7 +629,12 @@ Effective-spec audit helper:
|
|
|
589
629
|
validate_sanctuary_roots "$BACKUP_ROOT/runtime/.ouro-cli" "$BACKUP_ROOT/agent/sanctuary.ouro" || return $?
|
|
590
630
|
validate_exact_image_id "$IMAGE_ID" || return $?
|
|
591
631
|
validate_exact_image_id "$AUDIT_RUNNER_IMAGE_ID" || return $?
|
|
632
|
+
test "$IMAGE_ID" != sha256:681449ad47a2621705cd339b481e6339236b31dc65e195b1cf5025d0f2191d7d || return $?
|
|
633
|
+
test "$IMAGE_ID" != sha256:e337dff04c92d116b269052f473b26a47eea933d017d1befc73af50dd37bb08d || return $?
|
|
592
634
|
test "$AUDIT_RUNNER_IMAGE_ID" != sha256:681449ad47a2621705cd339b481e6339236b31dc65e195b1cf5025d0f2191d7d || return $?
|
|
635
|
+
test "$AUDIT_RUNNER_IMAGE_ID" != sha256:e337dff04c92d116b269052f473b26a47eea933d017d1befc73af50dd37bb08d || return $?
|
|
636
|
+
printf '%s\n' "$RESTORE_VERSION_IMAGE" | grep -Eq '^ghcr\.io/ourostack/ouroboros-butler:[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$' || return $?
|
|
637
|
+
test "$(docker image inspect --format '{{.Id}}' "$RESTORE_VERSION_IMAGE")" = "$IMAGE_ID" || return $?
|
|
593
638
|
RESTORE_CONTAINER_NAMES=$(docker container ls -a --format '{{.Names}}') || return $?
|
|
594
639
|
RESTORE_NAME_COUNTS=$(printf '%s\n' "$RESTORE_CONTAINER_NAMES" | awk '
|
|
595
640
|
$0 == "ouro-butler" { production++ }
|
|
@@ -646,7 +691,7 @@ Effective-spec audit helper:
|
|
|
646
691
|
MIGRATE_OPERATION=$2
|
|
647
692
|
MIGRATE_ROLLBACK_IMAGE_ID=${3-}
|
|
648
693
|
validate_exact_image_id "$MIGRATE_IMAGE_ID" || return $?
|
|
649
|
-
case "$MIGRATE_OPERATION" in migrate|rollback|commit|status) ;; *) return 1 ;; esac
|
|
694
|
+
case "$MIGRATE_OPERATION" in migrate|rollback|finalize-rollback|commit|status|inspect) ;; *) return 1 ;; esac
|
|
650
695
|
if test "$MIGRATE_OPERATION" = migrate; then
|
|
651
696
|
validate_exact_image_id "$MIGRATE_ROLLBACK_IMAGE_ID" || return $?
|
|
652
697
|
test "$MIGRATE_IMAGE_ID" != "$MIGRATE_ROLLBACK_IMAGE_ID" || return 1
|
|
@@ -662,22 +707,226 @@ Effective-spec audit helper:
|
|
|
662
707
|
--agent-root /home/ouro/AgentBundles/sanctuary.ouro \
|
|
663
708
|
--operation "$MIGRATE_OPERATION" "$@" || return $?
|
|
664
709
|
}
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
710
|
+
read_sanctuary_bundle_transaction_status() {
|
|
711
|
+
READ_BUNDLE_IMAGE_ID=$1
|
|
712
|
+
READ_BUNDLE_AGENT_ROOT=/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro
|
|
713
|
+
READ_BUNDLE_EXPECTED_UID=10001
|
|
714
|
+
READ_BUNDLE_EXPECTED_GID=10001
|
|
715
|
+
READ_BUNDLE_ROLLBACK_RECORD=$READ_BUNDLE_AGENT_ROOT/.sanctuary-package-managed-rollback.json
|
|
716
|
+
READ_BUNDLE_COMMITTING_RECORD=$READ_BUNDLE_ROLLBACK_RECORD.committing
|
|
717
|
+
validate_exact_image_id "$READ_BUNDLE_IMAGE_ID" || return $?
|
|
718
|
+
READ_BUNDLE_ROOT_STATE=$(/usr/local/bin/node -e '
|
|
719
|
+
const fs = require("node:fs");
|
|
720
|
+
const path = require("node:path");
|
|
721
|
+
const [rootPath, expectedUid, expectedGid] = process.argv.slice(1);
|
|
722
|
+
if (!path.isAbsolute(rootPath) || path.normalize(rootPath) !== rootPath) process.exit(1);
|
|
723
|
+
let current = path.parse(rootPath).root;
|
|
724
|
+
let rootStat;
|
|
725
|
+
for (const segment of rootPath.slice(current.length).split(path.sep)) {
|
|
726
|
+
current = path.join(current, segment);
|
|
727
|
+
let stat;
|
|
728
|
+
try { stat = fs.lstatSync(current); } catch (error) {
|
|
729
|
+
if (error?.code === "ENOENT") { process.stdout.write("missing"); process.exit(0); }
|
|
730
|
+
process.exit(1);
|
|
731
|
+
}
|
|
732
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || fs.realpathSync(current) !== current) process.exit(1);
|
|
733
|
+
rootStat = stat;
|
|
734
|
+
}
|
|
735
|
+
if (!rootStat || rootStat.uid !== Number(expectedUid) || rootStat.gid !== Number(expectedGid) || (rootStat.mode & 0o777) !== 0o700) process.exit(1);
|
|
736
|
+
process.stdout.write("present");
|
|
737
|
+
' "$READ_BUNDLE_AGENT_ROOT" "$READ_BUNDLE_EXPECTED_UID" "$READ_BUNDLE_EXPECTED_GID") || return $?
|
|
738
|
+
if test "$READ_BUNDLE_ROOT_STATE" = missing; then
|
|
739
|
+
printf 'null\n'
|
|
740
|
+
return 0
|
|
741
|
+
fi
|
|
742
|
+
test "$READ_BUNDLE_ROOT_STATE" = present || return 1
|
|
743
|
+
READ_BUNDLE_STAGING_RESIDUE=$(find "$READ_BUNDLE_AGENT_ROOT" -mindepth 1 -maxdepth 1 \( \
|
|
744
|
+
-name '.sanctuary-package-managed-rollback.json.package-migration.*' -o \
|
|
745
|
+
-name '.sanctuary-package-managed-rollback.json.committing.package-migration.*' \
|
|
746
|
+
\) -print -quit) || return $?
|
|
747
|
+
test -z "$READ_BUNDLE_STAGING_RESIDUE" || return 1
|
|
748
|
+
READ_BUNDLE_RECORD_COUNT=0
|
|
749
|
+
for READ_BUNDLE_CANDIDATE in "$READ_BUNDLE_ROLLBACK_RECORD" "$READ_BUNDLE_COMMITTING_RECORD"; do
|
|
750
|
+
test ! -L "$READ_BUNDLE_CANDIDATE" || return 1
|
|
751
|
+
if test -e "$READ_BUNDLE_CANDIDATE"; then
|
|
752
|
+
test -f "$READ_BUNDLE_CANDIDATE" || return 1
|
|
753
|
+
READ_BUNDLE_RECORD_COUNT=$((READ_BUNDLE_RECORD_COUNT + 1))
|
|
754
|
+
fi
|
|
755
|
+
done
|
|
756
|
+
test "$READ_BUNDLE_RECORD_COUNT" -le 1 || return 1
|
|
757
|
+
if test "$READ_BUNDLE_RECORD_COUNT" -eq 0; then
|
|
758
|
+
printf 'null\n'
|
|
759
|
+
return 0
|
|
760
|
+
fi
|
|
761
|
+
migrate_sanctuary_package_managed_bundle "$READ_BUNDLE_IMAGE_ID" status || return $?
|
|
762
|
+
}
|
|
763
|
+
verify_dockerman_and_community_apps() {
|
|
764
|
+
VERIFY_VERSION_IMAGE=$1
|
|
765
|
+
VERIFY_INSTALL_PROOF_PATH=$2
|
|
766
|
+
VERIFY_TEMPLATE_PATH=/boot/config/plugins/dockerMan/templates-user/my-ouro-butler.xml
|
|
767
|
+
test "$(stat -c '%u:%g:%a' "$VERIFY_TEMPLATE_PATH")" = 0:0:600 || return $?
|
|
768
|
+
test ! -e "$VERIFY_INSTALL_PROOF_PATH" || return 1
|
|
769
|
+
timeout -s KILL 20 /usr/bin/php -r '
|
|
770
|
+
[$expectedImage, $outputPath] = array_slice($argv, 1);
|
|
771
|
+
$expectedPath = "/boot/config/plugins/dockerMan/templates-user/my-ouro-butler.xml";
|
|
772
|
+
$expectedUrl = "https://raw.githubusercontent.com/ourostack/ouroboros/main/deploy/unraid/sanctuary.xml";
|
|
773
|
+
$expectedIcon = "https://raw.githubusercontent.com/ourostack/ouroboros/main/assets/ouroboros.png";
|
|
774
|
+
$communityAppsEntryPath = "/usr/local/emhttp/plugins/community.applications/include/exec.php";
|
|
775
|
+
$communityAppsHelperPath = "/usr/local/emhttp/plugins/community.applications/include/previous_apps_helpers.php";
|
|
776
|
+
$communityAppsDependencies = ["/usr/local/emhttp/plugins/community.applications/include/paths.php", "/usr/local/emhttp/plugins/community.applications/include/plugin_identity.php", "/usr/local/emhttp/plugins/community.applications/include/helpers.php"];
|
|
777
|
+
if (!preg_match("#^ghcr\\.io/ourostack/ouroboros-butler:[0-9]+\\.[0-9]+\\.[0-9]+(?:-[0-9A-Za-z.-]+)?$#", $expectedImage)) exit(2);
|
|
778
|
+
$readActivePhp = static function($path) {
|
|
779
|
+
if (!is_file($path) || is_link($path) || fileowner($path) !== 0 || (fileperms($path) & 0022) !== 0 || realpath($path) !== $path) exit(3);
|
|
780
|
+
$lint = proc_open(["/usr/bin/php", "-l", $path], [0 => ["file", "/dev/null", "r"], 1 => ["file", "/dev/null", "w"], 2 => ["file", "/dev/null", "w"]], $pipes);
|
|
781
|
+
if (!is_resource($lint) || proc_close($lint) !== 0) exit(4);
|
|
782
|
+
$source = php_strip_whitespace($path);
|
|
783
|
+
if (!is_string($source) || $source === "") exit(4);
|
|
784
|
+
return $source;
|
|
785
|
+
};
|
|
786
|
+
$functionSlice = static function($source, $start, $next) {
|
|
787
|
+
if (substr_count($source, $start) !== 1 || substr_count($source, $next) !== 1) return false;
|
|
788
|
+
$startAt = strpos($source, $start);
|
|
789
|
+
$nextAt = strpos($source, $next, $startAt + strlen($start));
|
|
790
|
+
return $nextAt !== false && $nextAt > $startAt ? substr($source, $startAt, $nextAt - $startAt) : false;
|
|
791
|
+
};
|
|
792
|
+
$entrySource = $readActivePhp($communityAppsEntryPath);
|
|
793
|
+
$inlineBody = $functionSlice($entrySource, "function previous_apps(", "function remove_application(");
|
|
794
|
+
$delegatedBody = $functionSlice($entrySource, "function previous_apps(", "function installedAndPreviousCounts(");
|
|
795
|
+
$isInline = $inlineBody !== false && hash("sha256", $inlineBody) === "1914b9c6b8910b7bb116dad48fe76d4790a069becbf0d68b3822ee3df7f22b1c";
|
|
796
|
+
$isDelegated = $delegatedBody !== false && hash("sha256", $delegatedBody) === "16b52cd87cc3f74915bf172f8a6eb46f981529208766916d4aeedf4b3344a3a4";
|
|
797
|
+
if (($isInline ? 1 : 0) + ($isDelegated ? 1 : 0) !== 1) exit(4);
|
|
798
|
+
$_SERVER["DOCUMENT_ROOT"] = "/usr/local/emhttp";
|
|
799
|
+
$docroot = $_SERVER["DOCUMENT_ROOT"];
|
|
800
|
+
require_once "$docroot/plugins/dynamix/include/Wrappers.php";
|
|
801
|
+
require_once "$docroot/plugins/dynamix.docker.manager/include/DockerClient.php";
|
|
802
|
+
$driver = DockerUtil::driver();
|
|
803
|
+
$client = new DockerClient();
|
|
804
|
+
$templates = new DockerTemplates();
|
|
805
|
+
$resolved = $templates->getUserTemplate("ouro-butler");
|
|
806
|
+
if ($resolved !== $expectedPath) exit(5);
|
|
807
|
+
$xml = @simplexml_load_file($resolved);
|
|
808
|
+
if (!$xml || (string)$xml->Name !== "ouro-butler" || (string)$xml->Repository !== $expectedImage || (string)$xml->TemplateURL !== $expectedUrl || (string)$xml->Icon !== $expectedIcon || trim((string)$xml->WebUI) !== "") exit(6);
|
|
809
|
+
$containers = $client->getDockerContainers();
|
|
810
|
+
$matches = array_values(array_filter($containers, fn($container) => $container["Name"] === "ouro-butler"));
|
|
811
|
+
if (count($matches) !== 1) exit(7);
|
|
812
|
+
$container = $matches[0];
|
|
813
|
+
if ($container["Image"] !== $expectedImage || $container["Manager"] !== "dockerman" || $container["Icon"] !== $expectedIcon || !empty($container["Url"])) exit(8);
|
|
814
|
+
$communityAppsInstalled = $container["Name"] === (string)$xml->Name && (str_starts_with(str_replace("library/", "", $container["Image"]), (string)$xml->Repository) || str_starts_with($container["Image"], (string)$xml->Repository));
|
|
815
|
+
if (!$communityAppsInstalled) exit(9);
|
|
816
|
+
if ($isDelegated) {
|
|
817
|
+
$readActivePhp($communityAppsHelperPath);
|
|
818
|
+
foreach ($communityAppsDependencies as $dependencyPath) $readActivePhp($dependencyPath);
|
|
819
|
+
require_once "$docroot/plugins/community.applications/include/helpers.php";
|
|
820
|
+
require_once $communityAppsHelperPath;
|
|
821
|
+
if (!is_callable(["PreviousAppsHelpers", "collectDockerApplications"])) exit(4);
|
|
822
|
+
$helperUpdateCount = 0;
|
|
823
|
+
$helperRows = PreviousAppsHelpers::collectDockerApplications(true, "true", "docker", $containers, $helperUpdateCount, [], [], [], []);
|
|
824
|
+
if (!is_array($helperRows)) exit(9);
|
|
825
|
+
$helperMatches = array_values(array_filter($helperRows, fn($row) => is_array($row) && ($row["InstallPath"] ?? null) === $expectedPath && ($row["Name"] ?? null) === "ouro-butler" && ($row["Repository"] ?? null) === $expectedImage && ($row["TemplateURL"] ?? null) === $expectedUrl && ($row["Icon"] ?? null) === $expectedIcon));
|
|
826
|
+
if (count($helperMatches) !== 1) exit(9);
|
|
827
|
+
$communityAppsStateModel = "previous-apps-helper-v1";
|
|
828
|
+
$communityAppsImplementationPath = $communityAppsHelperPath;
|
|
829
|
+
$communityAppsImplementationSymbol = "PreviousAppsHelpers::collectDockerApplications";
|
|
830
|
+
} else {
|
|
831
|
+
$communityAppsStateModel = "previous-apps-inline-v1";
|
|
832
|
+
$communityAppsImplementationPath = $communityAppsEntryPath;
|
|
833
|
+
$communityAppsImplementationSymbol = "previous_apps";
|
|
834
|
+
}
|
|
835
|
+
$proof = [
|
|
836
|
+
"dockerMan" => ["templatePath" => $resolved, "name" => (string)$xml->Name, "repository" => (string)$xml->Repository, "templateUrl" => (string)$xml->TemplateURL, "icon" => (string)$xml->Icon],
|
|
837
|
+
"communityApps" => ["installed" => true, "name" => (string)$xml->Name, "repository" => (string)$xml->Repository, "templateUrl" => (string)$xml->TemplateURL, "stateModel" => $communityAppsStateModel, "entryPath" => $communityAppsEntryPath, "entryFunction" => "previous_apps", "implementationPath" => $communityAppsImplementationPath, "implementationSymbol" => $communityAppsImplementationSymbol],
|
|
838
|
+
];
|
|
839
|
+
$temporary = $outputPath . ".tmp";
|
|
840
|
+
if (file_exists($temporary) || is_link($temporary) || file_exists($outputPath) || is_link($outputPath)) exit(10);
|
|
841
|
+
$payload = json_encode($proof, JSON_UNESCAPED_SLASHES) . "\n";
|
|
842
|
+
$handle = @fopen($temporary, "x");
|
|
843
|
+
if ($handle === false || fwrite($handle, $payload) !== strlen($payload) || !fflush($handle) || !fsync($handle) || !fclose($handle) || !chmod($temporary, 0600) || !chown($temporary, 0) || !chgrp($temporary, 0) || !rename($temporary, $outputPath)) exit(11);
|
|
844
|
+
' "$VERIFY_VERSION_IMAGE" "$VERIFY_INSTALL_PROOF_PATH" || return $?
|
|
845
|
+
test "$(stat -c '%u:%g:%a' "$VERIFY_INSTALL_PROOF_PATH")" = 0:0:600 || return $?
|
|
846
|
+
}
|
|
847
|
+
audit_registered_dockerman_template() {
|
|
848
|
+
REGISTERED_TEMPLATE_RUNNER_IMAGE_ID=$1
|
|
849
|
+
REGISTERED_TEMPLATE_VERSION_IMAGE=$2
|
|
850
|
+
REGISTERED_TEMPLATE_PATH=/boot/config/plugins/dockerMan/templates-user/my-ouro-butler.xml
|
|
851
|
+
validate_exact_image_id "$REGISTERED_TEMPLATE_RUNNER_IMAGE_ID" || return $?
|
|
852
|
+
printf '%s\n' "$REGISTERED_TEMPLATE_VERSION_IMAGE" | grep -Eq '^ghcr\.io/ourostack/ouroboros-butler:[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$' || return $?
|
|
853
|
+
test -f "$REGISTERED_TEMPLATE_PATH" && test ! -L "$REGISTERED_TEMPLATE_PATH" || return 1
|
|
854
|
+
docker run --rm --pull=never --network=none --user 0:0 --read-only --cap-drop=ALL --security-opt=no-new-privileges --entrypoint /opt/ouro/deploy/unraid/audit-container-spec.sh --mount "type=bind,src=$REGISTERED_TEMPLATE_PATH,dst=/audit/sanctuary.xml,readonly" "$REGISTERED_TEMPLATE_RUNNER_IMAGE_ID" --persistent-template /audit/sanctuary.xml --runtime-policy /opt/ouro/deploy/unraid/container-runtime.json --expected-image-reference "$REGISTERED_TEMPLATE_VERSION_IMAGE" || return $?
|
|
855
|
+
}
|
|
856
|
+
verify_known_good_rollback_artifact() {
|
|
857
|
+
EXPECTED_KNOWN_GOOD_IMAGE_ID=$1
|
|
858
|
+
validate_exact_image_id "$EXPECTED_KNOWN_GOOD_IMAGE_ID" || return $?
|
|
859
|
+
MATCHING_KNOWN_GOOD_COUNT=0
|
|
860
|
+
for KNOWN_GOOD_NAME in ouro-butler-rollback ouro-butler-legacy-evidence; do
|
|
861
|
+
if docker container inspect "$KNOWN_GOOD_NAME" >/dev/null 2>&1; then
|
|
862
|
+
KNOWN_GOOD_IMAGE_ID=$(docker inspect --format '{{.Image}}' "$KNOWN_GOOD_NAME") || return $?
|
|
863
|
+
if test "$KNOWN_GOOD_IMAGE_ID" = "$EXPECTED_KNOWN_GOOD_IMAGE_ID"; then
|
|
864
|
+
test "$(docker inspect --format '{{.State.Running}}' "$KNOWN_GOOD_NAME")" = false || return 1
|
|
865
|
+
MATCHING_KNOWN_GOOD_COUNT=$((MATCHING_KNOWN_GOOD_COUNT + 1))
|
|
866
|
+
fi
|
|
676
867
|
fi
|
|
677
868
|
done
|
|
678
|
-
test "$
|
|
679
|
-
|
|
680
|
-
|
|
869
|
+
test "$MATCHING_KNOWN_GOOD_COUNT" -eq 1 || return 1
|
|
870
|
+
}
|
|
871
|
+
write_dockerman_final_proof() {
|
|
872
|
+
FINAL_PROOF_IMAGE_ID=$1
|
|
873
|
+
FINAL_PROOF_VERSION_IMAGE=$2
|
|
874
|
+
FINAL_PROOF_ICON=$3
|
|
875
|
+
FINAL_PROOF_ROOT=$4
|
|
876
|
+
validate_exact_image_id "$FINAL_PROOF_IMAGE_ID" || return $?
|
|
877
|
+
test "$FINAL_PROOF_VERSION_IMAGE" = "ghcr.io/ourostack/ouroboros-butler:$PACKAGE_VERSION" || return 1
|
|
878
|
+
install -d -m 0700 -o 0 -g 0 "$FINAL_PROOF_ROOT" || return $?
|
|
879
|
+
FINAL_CONTAINER_PATH=$FINAL_PROOF_ROOT/container.json
|
|
880
|
+
FINAL_BUNDLE_PATH=$FINAL_PROOF_ROOT/bundle.json
|
|
881
|
+
FINAL_INSTALL_PATH=$FINAL_PROOF_ROOT/install.json
|
|
882
|
+
FINAL_JELLYFIN_PATH=$FINAL_PROOF_ROOT/jellyfin.json
|
|
883
|
+
FINAL_PROOF_PATH=$FINAL_PROOF_ROOT/final-proof.json
|
|
884
|
+
test ! -e "$FINAL_CONTAINER_PATH" && test ! -e "$FINAL_BUNDLE_PATH" && test ! -e "$FINAL_INSTALL_PATH" && test ! -e "$FINAL_JELLYFIN_PATH" && test ! -e "$FINAL_PROOF_PATH" || return 1
|
|
885
|
+
docker inspect ouro-butler >"$FINAL_CONTAINER_PATH" || return $?
|
|
886
|
+
migrate_sanctuary_package_managed_bundle "$FINAL_PROOF_IMAGE_ID" inspect >"$FINAL_BUNDLE_PATH" || return $?
|
|
887
|
+
verify_dockerman_and_community_apps "$FINAL_PROOF_VERSION_IMAGE" "$FINAL_INSTALL_PATH" || return $?
|
|
888
|
+
/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" jellyfin-status >"$FINAL_JELLYFIN_PATH" || return $?
|
|
889
|
+
chown 0:0 "$FINAL_CONTAINER_PATH" "$FINAL_BUNDLE_PATH" "$FINAL_JELLYFIN_PATH" || return $?
|
|
890
|
+
chmod 0600 "$FINAL_CONTAINER_PATH" "$FINAL_BUNDLE_PATH" "$FINAL_JELLYFIN_PATH" || return $?
|
|
891
|
+
FINAL_AUTOSTART_COUNT=$(awk '$1 == "ouro-butler" { count++ } END { print count + 0 }' /var/lib/docker/unraid-autostart) || return $?
|
|
892
|
+
/usr/local/bin/node -e '
|
|
893
|
+
const fs = require("node:fs");
|
|
894
|
+
const [containerPath, bundlePath, installPath, jellyfinPath, outputPath, imageId, imageReference, icon, autostartCount] = process.argv.slice(1);
|
|
895
|
+
const containers = JSON.parse(fs.readFileSync(containerPath, "utf8"));
|
|
896
|
+
const bundle = JSON.parse(fs.readFileSync(bundlePath, "utf8"));
|
|
897
|
+
const install = JSON.parse(fs.readFileSync(installPath, "utf8"));
|
|
898
|
+
const jellyfin = JSON.parse(fs.readFileSync(jellyfinPath, "utf8"));
|
|
899
|
+
if (!Array.isArray(containers) || containers.length !== 1) process.exit(1);
|
|
900
|
+
const source = containers[0];
|
|
901
|
+
const labels = source.Config?.Labels;
|
|
902
|
+
const health = source.State?.Health?.Status;
|
|
903
|
+
if (source.Name !== "/ouro-butler" || source.Image !== imageId || source.Config?.Image !== imageReference || source.State?.Running !== true || health !== "healthy" || autostartCount !== "1" || labels?.["net.unraid.docker.managed"] !== "dockerman" || labels?.["net.unraid.docker.icon"] !== icon || Object.prototype.hasOwnProperty.call(labels ?? {}, "net.unraid.docker.webui")) process.exit(1);
|
|
904
|
+
if (bundle?.ok !== true || bundle?.data?.parity !== "exact" || bundle?.data?.journalState !== "absent" || bundle?.data?.ready !== true) process.exit(1);
|
|
905
|
+
const proof = { container: { name: source.Name, imageId: source.Image, imageReference: source.Config.Image, running: source.State.Running, healthy: health === "healthy", autostart: true, labels: { "net.unraid.docker.managed": labels["net.unraid.docker.managed"], "net.unraid.docker.icon": labels["net.unraid.docker.icon"] } }, bundle, ...install, jellyfin };
|
|
906
|
+
fs.writeFileSync(outputPath, `${JSON.stringify(proof)}\n`, { mode: 0o600, flag: "wx" });
|
|
907
|
+
' "$FINAL_CONTAINER_PATH" "$FINAL_BUNDLE_PATH" "$FINAL_INSTALL_PATH" "$FINAL_JELLYFIN_PATH" "$FINAL_PROOF_PATH" "$FINAL_PROOF_IMAGE_ID" "$FINAL_PROOF_VERSION_IMAGE" "$FINAL_PROOF_ICON" "$FINAL_AUTOSTART_COUNT" || return $?
|
|
908
|
+
chown 0:0 "$FINAL_PROOF_PATH" || return $?
|
|
909
|
+
chmod 0600 "$FINAL_PROOF_PATH" || return $?
|
|
910
|
+
printf '%s\n' "$FINAL_PROOF_PATH"
|
|
911
|
+
}
|
|
912
|
+
start_only_butler_for_recovery() {
|
|
913
|
+
RECOVERY_START_STATE=$(docker inspect --format '{{.State.Running}}' ouro-butler) || return $?
|
|
914
|
+
case "$RECOVERY_START_STATE" in
|
|
915
|
+
true)
|
|
916
|
+
assert_only_running_butler ouro-butler || return $?
|
|
917
|
+
;;
|
|
918
|
+
false)
|
|
919
|
+
assert_only_running_butler - || return $?
|
|
920
|
+
docker start ouro-butler || return $?
|
|
921
|
+
assert_only_running_butler ouro-butler || return $?
|
|
922
|
+
;;
|
|
923
|
+
*) return 1 ;;
|
|
924
|
+
esac
|
|
925
|
+
}
|
|
926
|
+
recover_pending_sanctuary_bundle_migration() {
|
|
927
|
+
RECOVERY_IMAGE_ID=$1
|
|
928
|
+
RECOVERY_STATUS=$(read_sanctuary_bundle_transaction_status "$RECOVERY_IMAGE_ID") || return $?
|
|
929
|
+
test "$RECOVERY_STATUS" != null || return 0
|
|
681
930
|
RECOVERY_IDENTITIES=$(printf '%s' "$RECOVERY_STATUS" | /usr/local/bin/node -e '
|
|
682
931
|
let input = "";
|
|
683
932
|
process.stdin.setEncoding("utf8");
|
|
@@ -702,15 +951,10 @@ Effective-spec audit helper:
|
|
|
702
951
|
! docker container inspect ouro-butler-staging >/dev/null 2>&1 || return 1
|
|
703
952
|
RECOVERY_PRODUCTION_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler) || return $?
|
|
704
953
|
test "$(docker inspect --format '{{.State.Running}}' ouro-butler)" = true || return 1
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
elif test "$RECOVERY_PRODUCTION_IMAGE_ID" = "$RECOVERY_ROLLBACK_IMAGE_ID"; then
|
|
710
|
-
! docker container inspect ouro-butler-rollback >/dev/null 2>&1 || return 1
|
|
711
|
-
else
|
|
712
|
-
return 1
|
|
713
|
-
fi
|
|
954
|
+
test "$RECOVERY_PRODUCTION_IMAGE_ID" = "$RECOVERY_TARGET_IMAGE_ID" || return 1
|
|
955
|
+
RECOVERY_CURRENT_ROLLBACK_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler-rollback) || return $?
|
|
956
|
+
test "$RECOVERY_CURRENT_ROLLBACK_IMAGE_ID" = "$RECOVERY_ROLLBACK_IMAGE_ID" || return 1
|
|
957
|
+
test "$(docker inspect --format '{{.State.Running}}' ouro-butler-rollback)" = false || return 1
|
|
714
958
|
assert_only_running_butler ouro-butler || return $?
|
|
715
959
|
assert_update_source "$RECOVERY_PRODUCTION_IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" || return $?
|
|
716
960
|
wait_butler_ready ouro-butler || return $?
|
|
@@ -741,17 +985,17 @@ Effective-spec audit helper:
|
|
|
741
985
|
RECOVERY_PRODUCTION_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler) || return $?
|
|
742
986
|
test "$RECOVERY_PRODUCTION_IMAGE_ID" = "$RECOVERY_ROLLBACK_IMAGE_ID" || return 1
|
|
743
987
|
! docker container inspect ouro-butler-rollback >/dev/null 2>&1 || return 1
|
|
988
|
+
migrate_sanctuary_package_managed_bundle "$RECOVERY_IMAGE_ID" rollback || return $?
|
|
744
989
|
else
|
|
745
990
|
return 1
|
|
746
991
|
fi
|
|
747
992
|
RECOVERY_PRODUCTION_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler) || return $?
|
|
748
993
|
test "$RECOVERY_PRODUCTION_IMAGE_ID" = "$RECOVERY_ROLLBACK_IMAGE_ID" || return 1
|
|
749
994
|
assert_update_source "$RECOVERY_ROLLBACK_IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" || return $?
|
|
750
|
-
|
|
751
|
-
assert_only_running_butler ouro-butler || return $?
|
|
995
|
+
start_only_butler_for_recovery || return $?
|
|
752
996
|
wait_butler_ready ouro-butler || return $?
|
|
753
997
|
enable_butler_autostart || return $?
|
|
754
|
-
migrate_sanctuary_package_managed_bundle "$RECOVERY_IMAGE_ID"
|
|
998
|
+
migrate_sanctuary_package_managed_bundle "$RECOVERY_IMAGE_ID" finalize-rollback || return $?
|
|
755
999
|
}
|
|
756
1000
|
rollback_sanctuary_bundle_if_pending() {
|
|
757
1001
|
OPTIONAL_ROLLBACK_IMAGE_ID=$1
|
|
@@ -765,6 +1009,219 @@ Effective-spec audit helper:
|
|
|
765
1009
|
migrate_sanctuary_package_managed_bundle "$OPTIONAL_ROLLBACK_IMAGE_ID" rollback || return $?
|
|
766
1010
|
fi
|
|
767
1011
|
}
|
|
1012
|
+
finalize_sanctuary_bundle_rollback_if_retained() {
|
|
1013
|
+
FINALIZE_ROLLBACK_IMAGE_ID=$1
|
|
1014
|
+
FINALIZE_ROLLBACK_STATUS=$(read_sanctuary_bundle_transaction_status "$FINALIZE_ROLLBACK_IMAGE_ID") || return $?
|
|
1015
|
+
test "$FINALIZE_ROLLBACK_STATUS" != null || return 0
|
|
1016
|
+
FINALIZE_ROLLBACK_STATE=$(printf '%s' "$FINALIZE_ROLLBACK_STATUS" | /usr/local/bin/node -e '
|
|
1017
|
+
let input = "";
|
|
1018
|
+
process.stdin.setEncoding("utf8");
|
|
1019
|
+
process.stdin.on("data", chunk => { input += chunk; });
|
|
1020
|
+
process.stdin.on("end", () => {
|
|
1021
|
+
const value = JSON.parse(input);
|
|
1022
|
+
if (!value || !["rollback", "committing"].includes(value.state)) process.exit(1);
|
|
1023
|
+
process.stdout.write(value.state);
|
|
1024
|
+
});
|
|
1025
|
+
') || return $?
|
|
1026
|
+
test "$FINALIZE_ROLLBACK_STATE" = rollback || return 1
|
|
1027
|
+
migrate_sanctuary_package_managed_bundle "$FINALIZE_ROLLBACK_IMAGE_ID" finalize-rollback || return $?
|
|
1028
|
+
}
|
|
1029
|
+
write_dockerman_recovery_evidence() {
|
|
1030
|
+
RECOVERY_EVIDENCE_BUNDLE_STATE=$1
|
|
1031
|
+
RECOVERY_EVIDENCE_PRODUCTION=$2
|
|
1032
|
+
RECOVERY_EVIDENCE_INSPECTION_PATH=$3
|
|
1033
|
+
RECOVERY_EVIDENCE_PATH=$4
|
|
1034
|
+
test ! -e "$RECOVERY_EVIDENCE_PATH" || return 1
|
|
1035
|
+
/usr/local/bin/node -e '
|
|
1036
|
+
const fs = require("node:fs");
|
|
1037
|
+
const [bundleJournalState, production, inspectionPath, outputPath] = process.argv.slice(1);
|
|
1038
|
+
const evidence = { bundleJournalState, production };
|
|
1039
|
+
if (inspectionPath !== "-") evidence.inspection = JSON.parse(fs.readFileSync(inspectionPath, "utf8"));
|
|
1040
|
+
fs.writeFileSync(outputPath, `${JSON.stringify(evidence)}\n`, { mode: 0o600, flag: "wx" });
|
|
1041
|
+
' "$RECOVERY_EVIDENCE_BUNDLE_STATE" "$RECOVERY_EVIDENCE_PRODUCTION" "$RECOVERY_EVIDENCE_INSPECTION_PATH" "$RECOVERY_EVIDENCE_PATH" || return $?
|
|
1042
|
+
chown 0:0 "$RECOVERY_EVIDENCE_PATH" || return $?
|
|
1043
|
+
chmod 0600 "$RECOVERY_EVIDENCE_PATH" || return $?
|
|
1044
|
+
}
|
|
1045
|
+
recover_dockerman_template_transaction() {
|
|
1046
|
+
TEMPLATE_RECOVERY_IDENTITY=$(/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" recovery-identity) || return $?
|
|
1047
|
+
if test "$TEMPLATE_RECOVERY_IDENTITY" = null; then
|
|
1048
|
+
test "$(/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" recover-status)" = null || return 1
|
|
1049
|
+
return 0
|
|
1050
|
+
fi
|
|
1051
|
+
TEMPLATE_RECOVERY_IDENTITIES=$(printf '%s' "$TEMPLATE_RECOVERY_IDENTITY" | /usr/local/bin/node -e '
|
|
1052
|
+
let input = "";
|
|
1053
|
+
process.stdin.setEncoding("utf8");
|
|
1054
|
+
process.stdin.on("data", chunk => { input += chunk; });
|
|
1055
|
+
process.stdin.on("end", () => {
|
|
1056
|
+
const value = JSON.parse(input);
|
|
1057
|
+
if (!value || !["rollback", "committing"].includes(value.state)) process.exit(1);
|
|
1058
|
+
process.stdout.write([value.state, value.rollbackImageId, value.targetImageId, value.canonicalVersionTag, value.reviewedManifestDigest].join(" "));
|
|
1059
|
+
});
|
|
1060
|
+
') || return $?
|
|
1061
|
+
set -- $TEMPLATE_RECOVERY_IDENTITIES
|
|
1062
|
+
test "$#" -eq 5 || return 1
|
|
1063
|
+
TEMPLATE_RECOVERY_STATE=$1
|
|
1064
|
+
TEMPLATE_RECOVERY_ROLLBACK_IMAGE_ID=$2
|
|
1065
|
+
TEMPLATE_RECOVERY_TARGET_IMAGE_ID=$3
|
|
1066
|
+
TEMPLATE_RECOVERY_VERSION_IMAGE=$4
|
|
1067
|
+
TEMPLATE_RECOVERY_MANIFEST_DIGEST=$5
|
|
1068
|
+
test "$TEMPLATE_RECOVERY_TARGET_IMAGE_ID" = "$IMAGE_ID" || return 1
|
|
1069
|
+
test "$TEMPLATE_RECOVERY_VERSION_IMAGE" = "$VERSION_IMAGE" || return 1
|
|
1070
|
+
test "$TEMPLATE_RECOVERY_MANIFEST_DIGEST" = "$MANIFEST_DIGEST" || return 1
|
|
1071
|
+
validate_exact_image_id "$TEMPLATE_RECOVERY_ROLLBACK_IMAGE_ID" || return $?
|
|
1072
|
+
TEMPLATE_RECOVERY_BUNDLE_STATUS=$(read_sanctuary_bundle_transaction_status "$IMAGE_ID") || return $?
|
|
1073
|
+
TEMPLATE_RECOVERY_BUNDLE_IDENTITIES=$(printf '%s' "$TEMPLATE_RECOVERY_BUNDLE_STATUS" | /usr/local/bin/node -e '
|
|
1074
|
+
let input = "";
|
|
1075
|
+
process.stdin.setEncoding("utf8");
|
|
1076
|
+
process.stdin.on("data", chunk => { input += chunk; });
|
|
1077
|
+
process.stdin.on("end", () => {
|
|
1078
|
+
const value = JSON.parse(input);
|
|
1079
|
+
if (value === null) process.stdout.write("absent - -");
|
|
1080
|
+
else if (value && ["rollback", "committing"].includes(value.state) && typeof value.rollbackImageId === "string" && typeof value.targetImageId === "string") process.stdout.write([value.state, value.rollbackImageId, value.targetImageId].join(" "));
|
|
1081
|
+
else process.exit(1);
|
|
1082
|
+
});
|
|
1083
|
+
') || return $?
|
|
1084
|
+
set -- $TEMPLATE_RECOVERY_BUNDLE_IDENTITIES
|
|
1085
|
+
test "$#" -eq 3 || return 1
|
|
1086
|
+
TEMPLATE_RECOVERY_BUNDLE_STATE=$1
|
|
1087
|
+
TEMPLATE_RECOVERY_BUNDLE_ROLLBACK_IMAGE_ID=$2
|
|
1088
|
+
TEMPLATE_RECOVERY_BUNDLE_TARGET_IMAGE_ID=$3
|
|
1089
|
+
if test "$TEMPLATE_RECOVERY_BUNDLE_STATE" = absent; then
|
|
1090
|
+
test "$TEMPLATE_RECOVERY_BUNDLE_ROLLBACK_IMAGE_ID" = - && test "$TEMPLATE_RECOVERY_BUNDLE_TARGET_IMAGE_ID" = - || return 1
|
|
1091
|
+
else
|
|
1092
|
+
validate_exact_image_id "$TEMPLATE_RECOVERY_BUNDLE_ROLLBACK_IMAGE_ID" || return $?
|
|
1093
|
+
validate_exact_image_id "$TEMPLATE_RECOVERY_BUNDLE_TARGET_IMAGE_ID" || return $?
|
|
1094
|
+
test "$TEMPLATE_RECOVERY_BUNDLE_ROLLBACK_IMAGE_ID" != "$TEMPLATE_RECOVERY_BUNDLE_TARGET_IMAGE_ID" || return 1
|
|
1095
|
+
test "$TEMPLATE_RECOVERY_BUNDLE_ROLLBACK_IMAGE_ID" = "$TEMPLATE_RECOVERY_ROLLBACK_IMAGE_ID" || return 1
|
|
1096
|
+
test "$TEMPLATE_RECOVERY_BUNDLE_TARGET_IMAGE_ID" = "$TEMPLATE_RECOVERY_TARGET_IMAGE_ID" || return 1
|
|
1097
|
+
fi
|
|
1098
|
+
TEMPLATE_RECOVERY_STATUS=$(/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" recover-status) || return $?
|
|
1099
|
+
test "$TEMPLATE_RECOVERY_STATUS" = "$TEMPLATE_RECOVERY_IDENTITY" || return 1
|
|
1100
|
+
TEMPLATE_RECOVERY_EVIDENCE=$EVENT_ASSET_STAGE/template-recovery-evidence.json
|
|
1101
|
+
TEMPLATE_RECOVERY_INSPECTION=$EVENT_ASSET_STAGE/template-recovery-inspection.json
|
|
1102
|
+
TEMPLATE_RECOVERY_FINAL_ROOT=$EVENT_ASSET_STAGE/template-recovery-final
|
|
1103
|
+
if test "$TEMPLATE_RECOVERY_BUNDLE_STATE" = rollback; then
|
|
1104
|
+
recover_pending_sanctuary_bundle_migration "$IMAGE_ID" || return $?
|
|
1105
|
+
TEMPLATE_RECOVERY_POST_BUNDLE_STATUS=$(read_sanctuary_bundle_transaction_status "$IMAGE_ID") || return $?
|
|
1106
|
+
test "$TEMPLATE_RECOVERY_POST_BUNDLE_STATUS" = null || return 1
|
|
1107
|
+
test "$(docker inspect --format '{{.Image}}' ouro-butler)" = "$TEMPLATE_RECOVERY_ROLLBACK_IMAGE_ID" || return 1
|
|
1108
|
+
write_dockerman_recovery_evidence absent rollback-exact - "$TEMPLATE_RECOVERY_EVIDENCE" || return $?
|
|
1109
|
+
test "$(/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" recovery-action --evidence "$TEMPLATE_RECOVERY_EVIDENCE")" = '{"action":"restore-prior-template"}' || return 1
|
|
1110
|
+
/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" rollback >/dev/null || return $?
|
|
1111
|
+
return 0
|
|
1112
|
+
fi
|
|
1113
|
+
TEMPLATE_RECOVERY_PRODUCTION_PRESENT=false
|
|
1114
|
+
TEMPLATE_RECOVERY_PRODUCTION_IMAGE_ID=
|
|
1115
|
+
if docker container inspect ouro-butler >/dev/null 2>&1; then
|
|
1116
|
+
TEMPLATE_RECOVERY_PRODUCTION_PRESENT=true
|
|
1117
|
+
TEMPLATE_RECOVERY_PRODUCTION_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler) || return $?
|
|
1118
|
+
fi
|
|
1119
|
+
if test "$TEMPLATE_RECOVERY_BUNDLE_STATE" = committing; then
|
|
1120
|
+
test "$TEMPLATE_RECOVERY_PRODUCTION_PRESENT" = true || return 1
|
|
1121
|
+
test "$TEMPLATE_RECOVERY_STATE" = committing || return 1
|
|
1122
|
+
test "$TEMPLATE_RECOVERY_PRODUCTION_IMAGE_ID" = "$IMAGE_ID" || return 1
|
|
1123
|
+
test "$(docker inspect --format '{{.Image}}' ouro-butler-rollback)" = "$TEMPLATE_RECOVERY_ROLLBACK_IMAGE_ID" || return 1
|
|
1124
|
+
test "$(docker inspect --format '{{.State.Running}}' ouro-butler-rollback)" = false || return 1
|
|
1125
|
+
assert_only_running_butler ouro-butler || return $?
|
|
1126
|
+
audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" canonical "$VERSION_IMAGE" "$TEMPLATE_ICON" || return $?
|
|
1127
|
+
migrate_sanctuary_package_managed_bundle "$IMAGE_ID" inspect >"$TEMPLATE_RECOVERY_INSPECTION" || return $?
|
|
1128
|
+
chown 0:0 "$TEMPLATE_RECOVERY_INSPECTION" && chmod 0600 "$TEMPLATE_RECOVERY_INSPECTION" || return $?
|
|
1129
|
+
write_dockerman_recovery_evidence committing target-exact-committing "$TEMPLATE_RECOVERY_INSPECTION" "$TEMPLATE_RECOVERY_EVIDENCE" || return $?
|
|
1130
|
+
test "$(/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" recovery-action --evidence "$TEMPLATE_RECOVERY_EVIDENCE")" = '{"action":"finish-bundle-commit"}' || return 1
|
|
1131
|
+
migrate_sanctuary_package_managed_bundle "$IMAGE_ID" commit || return $?
|
|
1132
|
+
wait_butler_ready ouro-butler || return $?
|
|
1133
|
+
test "$(docker inspect --format '{{.Image}}' ouro-butler)" = "$IMAGE_ID" || return 1
|
|
1134
|
+
verify_butler_autostart "1 0 0 0" || return $?
|
|
1135
|
+
elif test "$TEMPLATE_RECOVERY_BUNDLE_STATE" = absent && test "$TEMPLATE_RECOVERY_PRODUCTION_PRESENT" = false; then
|
|
1136
|
+
test "$TEMPLATE_RECOVERY_STATE" = rollback || return 1
|
|
1137
|
+
if docker container inspect ouro-butler-staging >/dev/null 2>&1; then
|
|
1138
|
+
! docker container inspect ouro-butler-rollback >/dev/null 2>&1 || return 1
|
|
1139
|
+
! docker container inspect ouro-butler-legacy-evidence >/dev/null 2>&1 || return 1
|
|
1140
|
+
TEMPLATE_RECOVERY_STAGING_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler-staging) || return $?
|
|
1141
|
+
test "$TEMPLATE_RECOVERY_STAGING_IMAGE_ID" = "$TEMPLATE_RECOVERY_ROLLBACK_IMAGE_ID" || return 1
|
|
1142
|
+
assert_prepackage_alpha797_source "$TEMPLATE_RECOVERY_ROLLBACK_IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" ouro-butler-staging || return $?
|
|
1143
|
+
TEMPLATE_RECOVERY_STAGING_RUNNING=$(docker inspect --format '{{.State.Running}}' ouro-butler-staging) || return $?
|
|
1144
|
+
case "$TEMPLATE_RECOVERY_STAGING_RUNNING" in
|
|
1145
|
+
true)
|
|
1146
|
+
assert_only_running_butler ouro-butler-staging || return $?
|
|
1147
|
+
;;
|
|
1148
|
+
false)
|
|
1149
|
+
assert_only_running_butler - || return $?
|
|
1150
|
+
docker start ouro-butler-staging || return $?
|
|
1151
|
+
assert_only_running_butler ouro-butler-staging || return $?
|
|
1152
|
+
;;
|
|
1153
|
+
*) return 1 ;;
|
|
1154
|
+
esac
|
|
1155
|
+
set_butler_autostart staging || return $?
|
|
1156
|
+
write_dockerman_recovery_evidence absent adoption-source-exact - "$TEMPLATE_RECOVERY_EVIDENCE" || return $?
|
|
1157
|
+
test "$(/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" recovery-action --evidence "$TEMPLATE_RECOVERY_EVIDENCE")" = '{"action":"restore-prior-template"}' || return 1
|
|
1158
|
+
/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" rollback >/dev/null || return $?
|
|
1159
|
+
return 0
|
|
1160
|
+
fi
|
|
1161
|
+
assert_only_running_butler - || return $?
|
|
1162
|
+
if docker container inspect ouro-butler-rollback >/dev/null 2>&1; then
|
|
1163
|
+
TEMPLATE_RECOVERY_CURRENT_ROLLBACK_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler-rollback) || return $?
|
|
1164
|
+
test "$TEMPLATE_RECOVERY_CURRENT_ROLLBACK_IMAGE_ID" = "$TEMPLATE_RECOVERY_ROLLBACK_IMAGE_ID" || return 1
|
|
1165
|
+
test "$(docker inspect --format '{{.State.Running}}' ouro-butler-rollback)" = false || return 1
|
|
1166
|
+
docker rename ouro-butler-rollback ouro-butler || return $?
|
|
1167
|
+
assert_update_source "$TEMPLATE_RECOVERY_ROLLBACK_IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" || return $?
|
|
1168
|
+
start_only_butler_for_recovery || return $?
|
|
1169
|
+
wait_butler_ready ouro-butler || return $?
|
|
1170
|
+
enable_butler_autostart || return $?
|
|
1171
|
+
write_dockerman_recovery_evidence absent rollback-exact - "$TEMPLATE_RECOVERY_EVIDENCE" || return $?
|
|
1172
|
+
test "$(/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" recovery-action --evidence "$TEMPLATE_RECOVERY_EVIDENCE")" = '{"action":"restore-prior-template"}' || return 1
|
|
1173
|
+
/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" rollback >/dev/null || return $?
|
|
1174
|
+
return 0
|
|
1175
|
+
fi
|
|
1176
|
+
TEMPLATE_RECOVERY_LEGACY_EVIDENCE_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler-legacy-evidence) || return $?
|
|
1177
|
+
test "$TEMPLATE_RECOVERY_LEGACY_EVIDENCE_IMAGE_ID" = "$TEMPLATE_RECOVERY_ROLLBACK_IMAGE_ID" || return 1
|
|
1178
|
+
test "$(docker inspect --format '{{.State.Running}}' ouro-butler-legacy-evidence)" = false || return 1
|
|
1179
|
+
write_dockerman_recovery_evidence absent adoption-evidence-exact-stopped - "$TEMPLATE_RECOVERY_EVIDENCE" || return $?
|
|
1180
|
+
test "$(/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" recovery-action --evidence "$TEMPLATE_RECOVERY_EVIDENCE")" = '{"action":"quarantine-adoption"}' || return 1
|
|
1181
|
+
/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" rollback >/dev/null || return $?
|
|
1182
|
+
printf '%s\n' 'Legacy adoption recovery requires a reviewed retry; exact legacy evidence remains quarantined and stopped.' >&2
|
|
1183
|
+
return 1
|
|
1184
|
+
elif test "$TEMPLATE_RECOVERY_BUNDLE_STATE" = absent && test "$TEMPLATE_RECOVERY_STATE" = rollback && test "$TEMPLATE_RECOVERY_PRODUCTION_IMAGE_ID" = "$TEMPLATE_RECOVERY_ROLLBACK_IMAGE_ID"; then
|
|
1185
|
+
! docker container inspect ouro-butler-staging >/dev/null 2>&1 || return 1
|
|
1186
|
+
! docker container inspect ouro-butler-rollback >/dev/null 2>&1 || return 1
|
|
1187
|
+
assert_update_source "$TEMPLATE_RECOVERY_ROLLBACK_IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" || return $?
|
|
1188
|
+
start_only_butler_for_recovery || return $?
|
|
1189
|
+
wait_butler_ready ouro-butler || return $?
|
|
1190
|
+
enable_butler_autostart || return $?
|
|
1191
|
+
write_dockerman_recovery_evidence absent rollback-exact - "$TEMPLATE_RECOVERY_EVIDENCE" || return $?
|
|
1192
|
+
test "$(/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" recovery-action --evidence "$TEMPLATE_RECOVERY_EVIDENCE")" = '{"action":"restore-prior-template"}' || return 1
|
|
1193
|
+
/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" rollback >/dev/null || return $?
|
|
1194
|
+
return 0
|
|
1195
|
+
elif test "$TEMPLATE_RECOVERY_BUNDLE_STATE" = absent && test "$TEMPLATE_RECOVERY_STATE" = rollback && test "$TEMPLATE_RECOVERY_PRODUCTION_IMAGE_ID" = "$IMAGE_ID"; then
|
|
1196
|
+
! docker container inspect ouro-butler-staging >/dev/null 2>&1 || return 1
|
|
1197
|
+
! docker container inspect ouro-butler-rollback >/dev/null 2>&1 || return 1
|
|
1198
|
+
TEMPLATE_RECOVERY_LEGACY_EVIDENCE_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler-legacy-evidence) || return $?
|
|
1199
|
+
test "$TEMPLATE_RECOVERY_LEGACY_EVIDENCE_IMAGE_ID" = "$TEMPLATE_RECOVERY_ROLLBACK_IMAGE_ID" || return 1
|
|
1200
|
+
test "$(docker inspect --format '{{.State.Running}}' ouro-butler-legacy-evidence)" = false || return 1
|
|
1201
|
+
audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" canonical "$VERSION_IMAGE" "$TEMPLATE_ICON" || return $?
|
|
1202
|
+
start_only_butler_for_recovery || return $?
|
|
1203
|
+
wait_butler_ready ouro-butler || return $?
|
|
1204
|
+
migrate_sanctuary_package_managed_bundle "$IMAGE_ID" inspect >"$TEMPLATE_RECOVERY_INSPECTION" || return $?
|
|
1205
|
+
chown 0:0 "$TEMPLATE_RECOVERY_INSPECTION" && chmod 0600 "$TEMPLATE_RECOVERY_INSPECTION" || return $?
|
|
1206
|
+
write_dockerman_recovery_evidence absent adoption-target-exact-ready "$TEMPLATE_RECOVERY_INSPECTION" "$TEMPLATE_RECOVERY_EVIDENCE" || return $?
|
|
1207
|
+
test "$(/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" recovery-action --evidence "$TEMPLATE_RECOVERY_EVIDENCE")" = '{"action":"roll-forward-adoption"}' || return 1
|
|
1208
|
+
enable_butler_autostart || return $?
|
|
1209
|
+
/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" mark-committing >/dev/null || return $?
|
|
1210
|
+
elif test "$TEMPLATE_RECOVERY_BUNDLE_STATE" = absent && test "$TEMPLATE_RECOVERY_STATE" = committing && test "$TEMPLATE_RECOVERY_PRODUCTION_IMAGE_ID" = "$IMAGE_ID"; then
|
|
1211
|
+
audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" canonical "$VERSION_IMAGE" "$TEMPLATE_ICON" || return $?
|
|
1212
|
+
wait_butler_ready ouro-butler || return $?
|
|
1213
|
+
verify_butler_autostart "1 0 0 0" || return $?
|
|
1214
|
+
migrate_sanctuary_package_managed_bundle "$IMAGE_ID" inspect >"$TEMPLATE_RECOVERY_INSPECTION" || return $?
|
|
1215
|
+
chown 0:0 "$TEMPLATE_RECOVERY_INSPECTION" && chmod 0600 "$TEMPLATE_RECOVERY_INSPECTION" || return $?
|
|
1216
|
+
write_dockerman_recovery_evidence absent target-exact-ready "$TEMPLATE_RECOVERY_INSPECTION" "$TEMPLATE_RECOVERY_EVIDENCE" || return $?
|
|
1217
|
+
test "$(/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" recovery-action --evidence "$TEMPLATE_RECOVERY_EVIDENCE")" = '{"action":"finish-template-commit"}' || return 1
|
|
1218
|
+
else
|
|
1219
|
+
return 1
|
|
1220
|
+
fi
|
|
1221
|
+
TEMPLATE_RECOVERY_FINAL_PROOF=$(write_dockerman_final_proof "$IMAGE_ID" "$VERSION_IMAGE" "$TEMPLATE_ICON" "$TEMPLATE_RECOVERY_FINAL_ROOT") || return $?
|
|
1222
|
+
verify_known_good_rollback_artifact "$TEMPLATE_RECOVERY_ROLLBACK_IMAGE_ID" || return $?
|
|
1223
|
+
/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" commit --proof "$TEMPLATE_RECOVERY_FINAL_PROOF" >/dev/null || return $?
|
|
1224
|
+
}
|
|
768
1225
|
# Package installation and migration never grant restart authority. The
|
|
769
1226
|
# migration is the pre-activation assertion: it fails before live mutation
|
|
770
1227
|
# if packaged steward.json carries any routine grant. It preserves an
|
|
@@ -833,12 +1290,33 @@ Effective-spec audit helper:
|
|
|
833
1290
|
test "$LEGACY_STAGING_IMAGE_ID" = "$EXPECTED_LEGACY_IMAGE_ID" || return 1
|
|
834
1291
|
fi
|
|
835
1292
|
}
|
|
1293
|
+
classify_sanctuary_update_source() {
|
|
1294
|
+
UPDATE_ENTRY_CONTAINER_NAMES=$(docker container ls -a --format '{{.Names}}') || return $?
|
|
1295
|
+
if printf '%s\n' "$UPDATE_ENTRY_CONTAINER_NAMES" | grep -Fxq ouro-butler-staging; then
|
|
1296
|
+
validate_sanctuary_legacy_staging || return $?
|
|
1297
|
+
assert_sanctuary_update_source_pin ouro-butler-staging "$LEGACY_STAGING_IMAGE_ID"
|
|
1298
|
+
else
|
|
1299
|
+
UPDATE_ENTRY_SOURCE_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler) || return $?
|
|
1300
|
+
assert_update_topology "$UPDATE_ENTRY_SOURCE_IMAGE_ID" || return $?
|
|
1301
|
+
assert_sanctuary_update_source_pin ouro-butler "$UPDATE_ENTRY_SOURCE_IMAGE_ID"
|
|
1302
|
+
fi
|
|
1303
|
+
}
|
|
1304
|
+
admit_sanctuary_update_entry() {
|
|
1305
|
+
UPDATE_ENTRY_RECOVERY_JOURNAL=/boot/config/custom/ouro-butler/docker-man-template-transaction.json
|
|
1306
|
+
if test -e "$UPDATE_ENTRY_RECOVERY_JOURNAL" || test -L "$UPDATE_ENTRY_RECOVERY_JOURNAL"; then
|
|
1307
|
+
test -f "$UPDATE_ENTRY_RECOVERY_JOURNAL" && test ! -L "$UPDATE_ENTRY_RECOVERY_JOURNAL" || return 1
|
|
1308
|
+
test "$(stat -c '%u:%g:%a' "$UPDATE_ENTRY_RECOVERY_JOURNAL")" = 0:0:600
|
|
1309
|
+
else
|
|
1310
|
+
classify_sanctuary_update_source
|
|
1311
|
+
fi
|
|
1312
|
+
}
|
|
836
1313
|
prepare_sanctuary_legacy_adoption() {
|
|
837
1314
|
IMAGE_ID=$1
|
|
838
1315
|
validate_exact_image_id "$IMAGE_ID" || return $?
|
|
839
1316
|
validate_sanctuary_legacy_staging || return $?
|
|
840
1317
|
PREPARED_LEGACY_CONTAINER_ID=$LEGACY_STAGING_CONTAINER_ID
|
|
841
1318
|
PREPARED_LEGACY_IMAGE_ID=$LEGACY_STAGING_IMAGE_ID
|
|
1319
|
+
assert_prepackage_alpha797_source "$PREPARED_LEGACY_IMAGE_ID" "$IMAGE_ID" ouro-butler-staging || return $?
|
|
842
1320
|
prepare_canonical_sanctuary_roots "$IMAGE_ID" || return $?
|
|
843
1321
|
bootstrap_sanctuary_vault "$IMAGE_ID" \
|
|
844
1322
|
/mnt/user/appdata/ouro-butler/runtime/container-credentials.json \
|
|
@@ -976,7 +1454,14 @@ Effective-spec audit helper:
|
|
|
976
1454
|
validate_sanctuary_legacy_staging "$ADOPTION_PREPARED_CONTAINER_ID" "$ADOPTION_PREPARED_IMAGE_ID" || return $?
|
|
977
1455
|
LEGACY_STAGING_CONTAINER_ID=$ADOPTION_PREPARED_CONTAINER_ID
|
|
978
1456
|
LEGACY_STAGING_IMAGE_ID=$ADOPTION_PREPARED_IMAGE_ID
|
|
979
|
-
|
|
1457
|
+
/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" prepare --source-template "$STAGED_TEMPLATE" --version-tag "$VERSION_IMAGE" --manifest-digest "$MANIFEST_DIGEST" --rollback-image-id "$LEGACY_STAGING_IMAGE_ID" --target-image-id "$IMAGE_ID" >/dev/null || return $?
|
|
1458
|
+
if disable_butler_autostart; then
|
|
1459
|
+
:
|
|
1460
|
+
else
|
|
1461
|
+
ADOPTION_AUTOSTART_STATUS=$?
|
|
1462
|
+
/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" rollback >/dev/null || return $?
|
|
1463
|
+
return "$ADOPTION_AUTOSTART_STATUS"
|
|
1464
|
+
fi
|
|
980
1465
|
if docker stop "$LEGACY_STAGING_CONTAINER_ID" \
|
|
981
1466
|
&& CURRENT_LEGACY_STAGING_IMAGE_ID=$(docker inspect --format '{{.Image}}' "$LEGACY_STAGING_CONTAINER_ID") \
|
|
982
1467
|
&& test "$CURRENT_LEGACY_STAGING_IMAGE_ID" = "$LEGACY_STAGING_IMAGE_ID" \
|
|
@@ -986,33 +1471,32 @@ Effective-spec audit helper:
|
|
|
986
1471
|
&& test "$CURRENT_LEGACY_EVIDENCE_IMAGE_ID" = "$LEGACY_STAGING_IMAGE_ID" \
|
|
987
1472
|
&& test "$(docker inspect --format '{{.State.Running}}' ouro-butler-legacy-evidence)" = false \
|
|
988
1473
|
&& assert_only_running_butler - \
|
|
989
|
-
&& docker
|
|
1474
|
+
&& test "$(docker buildx imagetools inspect "$VERSION_IMAGE" --format '{{.Manifest.Digest}}')" = "$MANIFEST_DIGEST" \
|
|
1475
|
+
&& test "$(docker image inspect --format '{{.Id}}' "$VERSION_IMAGE")" = "$IMAGE_ID" \
|
|
1476
|
+
&& docker create --pull=never --name ouro-butler --network host --restart unless-stopped --user 10001:10001 \
|
|
1477
|
+
--label net.unraid.docker.managed=dockerman \
|
|
1478
|
+
--label "net.unraid.docker.icon=$TEMPLATE_ICON" \
|
|
990
1479
|
--mount "type=bind,src=/mnt/user/appdata/ouro-butler/runtime/.ouro-cli,dst=/home/ouro/.ouro-cli" \
|
|
991
1480
|
--mount "type=bind,src=/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro,dst=/home/ouro/AgentBundles/sanctuary.ouro" \
|
|
992
1481
|
--mount "type=bind,src=/boot/config/custom/ouro-events/spool,dst=/run/ouro-events,readonly" \
|
|
993
|
-
"$
|
|
994
|
-
&& audit_effective ouro-butler
|
|
995
|
-
&& docker start ouro-butler-staging \
|
|
996
|
-
&& assert_only_running_butler ouro-butler-staging \
|
|
997
|
-
&& wait_butler_ready ouro-butler-staging \
|
|
998
|
-
&& docker stop ouro-butler-staging \
|
|
999
|
-
&& test "$(docker inspect --format '{{.Image}}' ouro-butler-staging)" = "$IMAGE_ID" \
|
|
1000
|
-
&& docker rm ouro-butler-staging \
|
|
1482
|
+
"$VERSION_IMAGE" \
|
|
1483
|
+
&& audit_effective ouro-butler "$IMAGE_ID" "$IMAGE_ID" canonical "$VERSION_IMAGE" "$TEMPLATE_ICON" \
|
|
1001
1484
|
&& assert_only_running_butler - \
|
|
1002
|
-
&& docker create --name ouro-butler --network host --restart unless-stopped --user 10001:10001 \
|
|
1003
|
-
--mount "type=bind,src=/mnt/user/appdata/ouro-butler/runtime/.ouro-cli,dst=/home/ouro/.ouro-cli" \
|
|
1004
|
-
--mount "type=bind,src=/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro,dst=/home/ouro/AgentBundles/sanctuary.ouro" \
|
|
1005
|
-
--mount "type=bind,src=/boot/config/custom/ouro-events/spool,dst=/run/ouro-events,readonly" \
|
|
1006
|
-
"$IMAGE_ID" \
|
|
1007
|
-
&& audit_effective ouro-butler "$IMAGE_ID" "$IMAGE_ID" \
|
|
1008
1485
|
&& docker start ouro-butler \
|
|
1009
1486
|
&& assert_only_running_butler ouro-butler \
|
|
1487
|
+
&& test "$(docker inspect --format '{{.Image}}' ouro-butler)" = "$IMAGE_ID" \
|
|
1010
1488
|
&& wait_butler_ready ouro-butler \
|
|
1011
|
-
&& enable_butler_autostart
|
|
1489
|
+
&& enable_butler_autostart \
|
|
1490
|
+
&& /usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" mark-committing >/dev/null; then
|
|
1491
|
+
ADOPTION_FINAL_ROOT="$EVENT_ASSET_STAGE/adoption-final-proof"
|
|
1492
|
+
ADOPTION_FINAL_PROOF=$(write_dockerman_final_proof "$IMAGE_ID" "$VERSION_IMAGE" "$TEMPLATE_ICON" "$ADOPTION_FINAL_ROOT") || return $?
|
|
1493
|
+
verify_known_good_rollback_artifact "$LEGACY_STAGING_IMAGE_ID" || return $?
|
|
1494
|
+
/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" commit --proof "$ADOPTION_FINAL_PROOF" >/dev/null || return $?
|
|
1012
1495
|
return 0
|
|
1013
1496
|
else
|
|
1014
1497
|
ADOPTION_STATUS=$?
|
|
1015
1498
|
fi
|
|
1499
|
+
/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" verify-jellyfin >/dev/null || return $?
|
|
1016
1500
|
ADOPTION_RECOVERY_NAMES=$(docker container ls -a --format '{{.Names}}') || return $?
|
|
1017
1501
|
case "
|
|
1018
1502
|
$ADOPTION_RECOVERY_NAMES
|
|
@@ -1055,6 +1539,7 @@ ouro-butler-staging
|
|
|
1055
1539
|
CURRENT_LEGACY_EVIDENCE_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler-legacy-evidence) || return $?
|
|
1056
1540
|
test "$CURRENT_LEGACY_EVIDENCE_IMAGE_ID" = "$LEGACY_STAGING_IMAGE_ID" || return $?
|
|
1057
1541
|
test "$(docker inspect --format '{{.State.Running}}' ouro-butler-legacy-evidence)" = false || return $?
|
|
1542
|
+
/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" rollback >/dev/null || return $?
|
|
1058
1543
|
return "$ADOPTION_STATUS"
|
|
1059
1544
|
}
|
|
1060
1545
|
validate_sanctuary_legacy_import_marker() {
|
|
@@ -1589,22 +2074,36 @@ NODE
|
|
|
1589
2074
|
return 1
|
|
1590
2075
|
}
|
|
1591
2076
|
Update:
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
the
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
2077
|
+
Before downloading or staging anything, classify the live source with the existing topology gates unless the fixed root-owned DockerMan transaction journal signals that an interrupted update needs the exact packaged recovery tools. Initial alpha.797 adoption must prove the exact staging name and image pin; normal updates must prove the existing production topology. A recovery journal admits only image acquisition and private staging until the staged transaction implementation validates the full journal and restores a classifiable source:
|
|
2078
|
+
admit_sanctuary_update_entry
|
|
2079
|
+
Set the released package version, its reviewed release-manifest digest, and its reviewed platform-local image ID. The canonical package-version tag is immutable release identity, while the digest and local ID independently prove what the registry and this host resolved:
|
|
2080
|
+
PACKAGE_VERSION=<released-version>
|
|
2081
|
+
MANIFEST_DIGEST=sha256:<reviewed-release-manifest-digest>
|
|
2082
|
+
IMAGE_ID=sha256:<reviewed-local-image-id>
|
|
2083
|
+
VERSION_IMAGE="ghcr.io/ourostack/ouroboros-butler:$PACKAGE_VERSION"
|
|
2084
|
+
printf '%s\n' "$PACKAGE_VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$'
|
|
2085
|
+
test "$(docker buildx imagetools inspect "$VERSION_IMAGE" --format '{{.Manifest.Digest}}')" = "$MANIFEST_DIGEST"
|
|
2086
|
+
docker pull "$VERSION_IMAGE"
|
|
2087
|
+
validate_exact_image_id "$IMAGE_ID"
|
|
2088
|
+
test "$(docker image inspect --format '{{.Id}}' "$VERSION_IMAGE")" = "$IMAGE_ID"
|
|
1599
2089
|
AUDIT_RUNNER_IMAGE_ID=$IMAGE_ID
|
|
1600
2090
|
validate_exact_image_id "$AUDIT_RUNNER_IMAGE_ID"
|
|
1601
|
-
|
|
2091
|
+
TEMPLATE_ICON=https://raw.githubusercontent.com/ourostack/ouroboros/main/assets/ouroboros.png
|
|
2092
|
+
DOCKERMAN_TEMPLATE_PATH=/boot/config/plugins/dockerMan/templates-user/my-ouro-butler.xml
|
|
2093
|
+
DOCKERMAN_TEMPLATE_JOURNAL=/boot/config/custom/ouro-butler/docker-man-template-transaction.json
|
|
2094
|
+
test "$DOCKERMAN_TEMPLATE_PATH" = /boot/config/plugins/dockerMan/templates-user/my-ouro-butler.xml
|
|
2095
|
+
test "$DOCKERMAN_TEMPLATE_JOURNAL" = /boot/config/custom/ouro-butler/docker-man-template-transaction.json
|
|
2096
|
+
Before stopping, renaming, or creating any Butler container, extract the packaged event, template, runtime-policy, and DockerMan transaction assets from that exact image ID. Do not copy these files from a checkout or another image.
|
|
2097
|
+
Stage the event assets, audit the original version-tagged template, create a separate temporary copy for exact local-image-ID auditing, and keep the private stage until the outer transaction commits:
|
|
1602
2098
|
EVENT_ASSET_STAGE=$(mktemp -d /mnt/user/appdata/ouro-butler/staging/ouro-events.XXXXXX)
|
|
1603
2099
|
chmod 0700 "$EVENT_ASSET_STAGE"
|
|
1604
2100
|
EVENT_SCRIPT_STAGE="$EVENT_ASSET_STAGE/ouro-events"
|
|
1605
2101
|
mkdir "$EVENT_SCRIPT_STAGE"
|
|
1606
2102
|
STAGED_TEMPLATE="$EVENT_ASSET_STAGE/sanctuary.xml"
|
|
2103
|
+
STAGED_EXACT_TEMPLATE="$EVENT_ASSET_STAGE/sanctuary.exact-image.xml"
|
|
1607
2104
|
STAGED_RUNTIME_POLICY="$EVENT_ASSET_STAGE/container-runtime.json"
|
|
2105
|
+
STAGED_DOCKERMAN_TRANSACTION="$EVENT_ASSET_STAGE/docker-man-template-transaction.mjs"
|
|
2106
|
+
STAGED_DOCKERMAN_XML_VALIDATOR="$EVENT_ASSET_STAGE/docker-man-template-xml.cjs"
|
|
1608
2107
|
EVENT_ASSET_CONTAINER=
|
|
1609
2108
|
cleanup_event_asset_stage() {
|
|
1610
2109
|
if test -n "$EVENT_ASSET_CONTAINER"; then
|
|
@@ -1618,20 +2117,24 @@ Update:
|
|
|
1618
2117
|
docker cp "$EVENT_ASSET_CONTAINER:/opt/ouro/deploy/unraid/ouro-events/." "$EVENT_SCRIPT_STAGE/"
|
|
1619
2118
|
docker cp "$EVENT_ASSET_CONTAINER:/opt/ouro/deploy/unraid/sanctuary.xml" "$STAGED_TEMPLATE"
|
|
1620
2119
|
docker cp "$EVENT_ASSET_CONTAINER:/opt/ouro/deploy/unraid/container-runtime.json" "$STAGED_RUNTIME_POLICY"
|
|
2120
|
+
docker cp "$EVENT_ASSET_CONTAINER:/opt/ouro/deploy/unraid/docker-man-template-transaction.mjs" "$STAGED_DOCKERMAN_TRANSACTION"
|
|
2121
|
+
docker cp "$EVENT_ASSET_CONTAINER:/opt/ouro/deploy/unraid/docker-man-template-xml.cjs" "$STAGED_DOCKERMAN_XML_VALIDATOR"
|
|
1621
2122
|
docker rm "$EVENT_ASSET_CONTAINER"
|
|
1622
2123
|
EVENT_ASSET_CONTAINER=
|
|
1623
|
-
EXPECTED_RELEASE_ASSETS=$(printf '%s\n' container-runtime.json ouro-events sanctuary.xml)
|
|
2124
|
+
EXPECTED_RELEASE_ASSETS=$(printf '%s\n' container-runtime.json docker-man-template-transaction.mjs docker-man-template-xml.cjs ouro-events sanctuary.xml)
|
|
1624
2125
|
ACTUAL_RELEASE_ASSETS=$(find "$EVENT_ASSET_STAGE" -mindepth 1 -maxdepth 1 -exec basename {} \; | LC_ALL=C sort)
|
|
1625
2126
|
test "$ACTUAL_RELEASE_ASSETS" = "$EXPECTED_RELEASE_ASSETS"
|
|
1626
2127
|
test -d "$EVENT_SCRIPT_STAGE" && test ! -L "$EVENT_SCRIPT_STAGE"
|
|
1627
2128
|
test -f "$STAGED_TEMPLATE" && test ! -L "$STAGED_TEMPLATE"
|
|
1628
2129
|
test -f "$STAGED_RUNTIME_POLICY" && test ! -L "$STAGED_RUNTIME_POLICY"
|
|
2130
|
+
test -f "$STAGED_DOCKERMAN_TRANSACTION" && test ! -L "$STAGED_DOCKERMAN_TRANSACTION"
|
|
2131
|
+
test -f "$STAGED_DOCKERMAN_XML_VALIDATOR" && test ! -L "$STAGED_DOCKERMAN_XML_VALIDATOR"
|
|
2132
|
+
chown 0:0 "$STAGED_TEMPLATE" "$STAGED_RUNTIME_POLICY" "$STAGED_DOCKERMAN_TRANSACTION" "$STAGED_DOCKERMAN_XML_VALIDATOR"
|
|
2133
|
+
chmod 0600 "$STAGED_TEMPLATE" "$STAGED_RUNTIME_POLICY" "$STAGED_DOCKERMAN_TRANSACTION" "$STAGED_DOCKERMAN_XML_VALIDATOR"
|
|
1629
2134
|
EXPECTED_EVENT_ASSETS=$(printf '%s\n' bootstrap-spool.sh emit-event.mjs emit-usenet-event.sh install-usenet-guard.sh usenet-health.sh)
|
|
1630
2135
|
ACTUAL_EVENT_ASSETS=$(find "$EVENT_SCRIPT_STAGE" -mindepth 1 -maxdepth 1 -type f -exec basename {} \; | LC_ALL=C sort)
|
|
1631
2136
|
test "$ACTUAL_EVENT_ASSETS" = "$EXPECTED_EVENT_ASSETS"
|
|
1632
2137
|
test -z "$(find "$EVENT_SCRIPT_STAGE" -mindepth 1 -maxdepth 1 \( -type l -o ! -type f \) -print -quit)"
|
|
1633
|
-
/bin/bash "$EVENT_SCRIPT_STAGE/install-usenet-guard.sh" --source-root "$EVENT_SCRIPT_STAGE"
|
|
1634
|
-
/bin/bash /boot/config/custom/ouro-events/install-usenet-guard.sh --boot --install-root /boot/config/custom
|
|
1635
2138
|
verify_installed_usenet_guard() {
|
|
1636
2139
|
test "$(findmnt -n -o FSTYPE --target /boot/config/custom)" = vfat
|
|
1637
2140
|
test "$(stat -c '%u:%g:%a' /boot/config/custom/usenet_health.sh)" = 0:0:600
|
|
@@ -1661,62 +2164,53 @@ Update:
|
|
|
1661
2164
|
INSTALLED_SPOOL_SIZE=$(findmnt -bn -o SIZE --target /boot/config/custom/ouro-events/spool)
|
|
1662
2165
|
test "$INSTALLED_SPOOL_SIZE" -le 4194304
|
|
1663
2166
|
}
|
|
1664
|
-
|
|
2167
|
+
docker run --rm --pull=never --network=none \
|
|
2168
|
+
--user 0:0 --read-only --cap-drop=ALL --security-opt=no-new-privileges \
|
|
2169
|
+
--entrypoint /opt/ouro/deploy/unraid/audit-container-spec.sh \
|
|
2170
|
+
--mount "type=bind,src=$STAGED_TEMPLATE,dst=/audit/sanctuary.xml,readonly" \
|
|
2171
|
+
--mount "type=bind,src=$STAGED_RUNTIME_POLICY,dst=/audit/container-runtime.json,readonly" \
|
|
2172
|
+
"$IMAGE_ID" --persistent-template /audit/sanctuary.xml --runtime-policy /audit/container-runtime.json --expected-image-reference "$VERSION_IMAGE"
|
|
1665
2173
|
/usr/local/bin/node -e '
|
|
1666
2174
|
const fs = require("node:fs");
|
|
1667
|
-
const [
|
|
2175
|
+
const [sourcePath, destinationPath, imageId] = process.argv.slice(1);
|
|
1668
2176
|
if (!/^sha256:[0-9a-f]{64}$/.test(imageId)) throw new Error("invalid exact image ID");
|
|
1669
|
-
const source = fs.readFileSync(
|
|
2177
|
+
const source = fs.readFileSync(sourcePath, "utf8");
|
|
1670
2178
|
const repositories = [...source.matchAll(/<Repository>[^<]*<\/Repository>/g)];
|
|
1671
2179
|
if (repositories.length !== 1) throw new Error("template must contain exactly one Repository element");
|
|
1672
2180
|
const staged = source.replace(repositories[0][0], `<Repository>${imageId}</Repository>`);
|
|
1673
2181
|
if ((staged.match(/<Repository>[^<]*<\/Repository>/g) ?? []).length !== 1
|
|
1674
2182
|
|| !staged.includes(`<Repository>${imageId}</Repository>`)) throw new Error("exact Repository staging failed");
|
|
1675
|
-
fs.writeFileSync(
|
|
1676
|
-
' "$STAGED_TEMPLATE" "$IMAGE_ID"
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
2183
|
+
fs.writeFileSync(destinationPath, staged, { mode: 0o600, flag: "wx" });
|
|
2184
|
+
' "$STAGED_TEMPLATE" "$STAGED_EXACT_TEMPLATE" "$IMAGE_ID"
|
|
2185
|
+
chown 0:0 "$STAGED_EXACT_TEMPLATE"
|
|
2186
|
+
chmod 0600 "$STAGED_EXACT_TEMPLATE"
|
|
2187
|
+
docker run --rm --pull=never --network=none \
|
|
2188
|
+
--user 0:0 --read-only --cap-drop=ALL --security-opt=no-new-privileges \
|
|
2189
|
+
--entrypoint /opt/ouro/deploy/unraid/audit-container-spec.sh \
|
|
2190
|
+
--mount "type=bind,src=$STAGED_EXACT_TEMPLATE,dst=/audit/sanctuary.exact-image.xml,readonly" \
|
|
2191
|
+
--mount "type=bind,src=$STAGED_RUNTIME_POLICY,dst=/audit/container-runtime.json,readonly" \
|
|
2192
|
+
"$IMAGE_ID" --template /audit/sanctuary.exact-image.xml --runtime-policy /audit/container-runtime.json --expected-image "$IMAGE_ID"
|
|
2193
|
+
recover_dockerman_template_transaction
|
|
2194
|
+
recover_pending_sanctuary_bundle_migration "$IMAGE_ID"
|
|
2195
|
+
classify_sanctuary_update_source
|
|
2196
|
+
/bin/bash "$EVENT_SCRIPT_STAGE/install-usenet-guard.sh" --source-root "$EVENT_SCRIPT_STAGE"
|
|
2197
|
+
/bin/bash /boot/config/custom/ouro-events/install-usenet-guard.sh --boot --install-root /boot/config/custom
|
|
2198
|
+
verify_installed_usenet_guard
|
|
2199
|
+
If extraction or transactional installation fails, abort before changing any live Butler container. The installer restores the previous assets, go file, and cron; production stays running.
|
|
2200
|
+
If boot activation or verification fails after that transaction commits, leave production untouched, repair or rerun this exact-image installation, and stop. Container and DockerMan rollback begin only after the later production preflight succeeds.
|
|
2201
|
+
Initial install/adoption is a separate terminal path for the verified older layout: no production or rollback, exactly one running (possibly unhealthy) ouro-butler-staging, and no legacy-evidence container. After the helpers above are loaded and IMAGE_ID is resolved, run this exact sequence.
|
|
2202
|
+
MiniMax credentials come from the byte-verified legacy bootstrap envelope. Never place credentials in arguments, shell variables, or history.
|
|
2203
|
+
These provider-readiness commands are adoption-only; do not use them as a normal-update precheck against an active canonical production daemon.
|
|
1687
2204
|
Sanctuary legacy adoption commands:
|
|
1688
2205
|
prepare_sanctuary_legacy_adoption "$IMAGE_ID"
|
|
1689
2206
|
verify_sanctuary_provider_readiness "$IMAGE_ID"
|
|
1690
2207
|
install_from_legacy_staging
|
|
1691
|
-
These commands are one terminal path; after install succeeds, stop and do not
|
|
1692
|
-
|
|
1693
|
-
it
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
Sanctuary skeleton, proves required files plus exact ownership/modes, and runs
|
|
1698
|
-
same-image vault bootstrap and provider readiness before durably snapshotting
|
|
1699
|
-
both exact container and image inspect records. Any preparation, vault, or
|
|
1700
|
-
readiness failure returns while legacy remains running with autostart
|
|
1701
|
-
untouched. It then stops and rechecks the legacy container and renames it to
|
|
1702
|
-
stopped ouro-butler-legacy-evidence. A fresh canonical staging container is
|
|
1703
|
-
created from exact IMAGE_ID and canonical binds, audited, started, proven to
|
|
1704
|
-
be the only running Butler, readiness-checked, then stopped and removed only
|
|
1705
|
-
after its image is rechecked. Only then is fresh canonical production created,
|
|
1706
|
-
audited, started, readiness-checked, and placed in bare production autostart.
|
|
1707
|
-
Failure removes only partial target-image staging/production containers,
|
|
1708
|
-
quarantines the exact legacy container without deletion or restart, leaves
|
|
1709
|
-
autostart disabled, and propagates the original status.
|
|
1710
|
-
For this cutover, preflight either the exact known alpha.742 two-mount source
|
|
1711
|
-
or an already-canonical source before any autostart or container mutation.
|
|
1712
|
-
Production must be the only running Butler poller;
|
|
1713
|
-
staging must be absent; rollback may be absent or one stopped container with
|
|
1714
|
-
the exact production image. A stopped legacy-evidence container is preserved.
|
|
1715
|
-
Disable every Butler name in Unraid's array-autostart file and verify that
|
|
1716
|
-
result before stopping production. First resolve and validate the exact image
|
|
1717
|
-
ID of the known-good production container while it is still running, so a
|
|
1718
|
-
lookup failure cannot strand a renamed container:
|
|
1719
|
-
recover_pending_sanctuary_bundle_migration "$IMAGE_ID"
|
|
2208
|
+
These commands are one terminal path; after install succeeds, stop and do not continue into the normal update below. The final install is noninteractive: it reruns resumable preparation plus fresh readiness, but never authentication.
|
|
2209
|
+
That function never applies the package-managed target contract to the older alpha.797 container or restarts it. It first proves the pinned alpha.797 image and exact allowed legacy shape, creates the missing canonical roots from the reviewed target image, checks required files and permissions, and finishes vault plus provider preparation. Any failure leaves the old Butler running with autostart untouched.
|
|
2210
|
+
After preparation succeeds, it records the exact old container and image, installs the original version-tagged template under the crash journal, disables Butler autostart, stops and rechecks the old container, and renames it to stopped ouro-butler-legacy-evidence. The journal also binds Jellyfin's exact container ID, image ID, state, and restart count under its digest. It then creates one canonical production container from the reviewed version tag with --pull=never, audits it before start, proves it is the only running healthy Butler, enables only production autostart, proves the stopped rollback plus DockerMan and Community Apps recognition, and commits the template journal only while Jellyfin remains unchanged.
|
|
2211
|
+
If activation fails, it removes only a partial target container, preserves the stopped legacy evidence, restores the prior template, leaves autostart disabled, and returns the original failure.
|
|
2212
|
+
For normal updates, preflight accepts only the pinned alpha.742 two-mount source, the pinned pre-package-managed alpha.797 source, or an already package-managed canonical source before any autostart or live-container change. The two pinned exceptions may validate an old source, but can never authorize creation of a new target.
|
|
2213
|
+
Production must be the only running Butler poller; staging must be absent; rollback may be absent or one stopped container with the exact production image. A stopped legacy-evidence container is preserved. Disable every Butler name in Unraid's array-autostart file and verify that result before stopping production. First resolve and validate the exact image ID of the known-good production container while it is still running, so a lookup failure cannot strand a renamed container:
|
|
1720
2214
|
/bin/bash /boot/config/custom/ouro-events/bootstrap-spool.sh --mount
|
|
1721
2215
|
test "$(findmnt -n -o FSTYPE --target /boot/config/custom/ouro-events/spool)" = tmpfs
|
|
1722
2216
|
test "$(stat -c '%u:%g:%a' /boot/config/custom/ouro-events/spool)" = 0:0:755
|
|
@@ -1729,32 +2223,22 @@ Update:
|
|
|
1729
2223
|
(exit "$UPDATE_PREFLIGHT_STATUS")
|
|
1730
2224
|
fi
|
|
1731
2225
|
assert_update_source "$ROLLBACK_IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID"
|
|
1732
|
-
|
|
1733
|
-
Repository element with $IMAGE_ID. After the topology and source gates pass,
|
|
1734
|
-
audit the staged template and runtime policy with the packaged auditor, then
|
|
1735
|
-
remove the private stage before any Butler mutation:
|
|
1736
|
-
docker run --rm --pull=never --network=none \
|
|
1737
|
-
--user 0:0 --read-only --cap-drop=ALL --security-opt=no-new-privileges \
|
|
1738
|
-
--entrypoint /opt/ouro/deploy/unraid/audit-container-spec.sh \
|
|
1739
|
-
--mount "type=bind,src=$STAGED_TEMPLATE,dst=/audit/sanctuary.xml,readonly" \
|
|
1740
|
-
--mount "type=bind,src=$STAGED_RUNTIME_POLICY,dst=/audit/container-runtime.json,readonly" \
|
|
1741
|
-
"$IMAGE_ID" --template /audit/sanctuary.xml --runtime-policy /audit/container-runtime.json --expected-image "$IMAGE_ID"
|
|
1742
|
-
cleanup_event_asset_stage
|
|
1743
|
-
trap - EXIT
|
|
2226
|
+
/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" prepare --source-template "$STAGED_TEMPLATE" --version-tag "$VERSION_IMAGE" --manifest-digest "$MANIFEST_DIGEST" --rollback-image-id "$ROLLBACK_IMAGE_ID" --target-image-id "$IMAGE_ID" >/dev/null
|
|
1744
2227
|
if provision_sanctuary_sab_credential "$IMAGE_ID" \
|
|
1745
2228
|
&& verify_sanctuary_sab_readiness "$IMAGE_ID" \
|
|
1746
2229
|
&& verify_sanctuary_telegram_readiness "$IMAGE_ID"; then
|
|
1747
2230
|
:
|
|
1748
2231
|
else
|
|
1749
2232
|
PRECUTOVER_READINESS_STATUS=$?
|
|
2233
|
+
/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" rollback >/dev/null
|
|
1750
2234
|
(exit "$PRECUTOVER_READINESS_STATUS")
|
|
1751
2235
|
fi
|
|
1752
|
-
Guard the atomic autostart disable separately. If it fails, production has not
|
|
1753
|
-
been touched and the captured status is propagated:
|
|
2236
|
+
Guard the atomic autostart disable separately. If it fails, production has not been touched and the captured status is propagated:
|
|
1754
2237
|
if disable_butler_autostart; then
|
|
1755
2238
|
:
|
|
1756
2239
|
else
|
|
1757
2240
|
AUTOSTART_DISABLE_STATUS=$?
|
|
2241
|
+
/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" rollback >/dev/null
|
|
1758
2242
|
(exit "$AUTOSTART_DISABLE_STATUS")
|
|
1759
2243
|
fi
|
|
1760
2244
|
Define the stale-rollback cleanup used by the preparation guard:
|
|
@@ -1775,33 +2259,12 @@ ouro-butler-rollback
|
|
|
1775
2259
|
;;
|
|
1776
2260
|
esac
|
|
1777
2261
|
}
|
|
1778
|
-
Stop production, remove only a stopped stale rollback, rename the known-good
|
|
1779
|
-
|
|
1780
|
-
package-managed bundle migration
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
fields. It never installs or mutates steward policy authority. It preserves
|
|
1785
|
-
agent.json, all steward policy and audit bytes, relationships, sessions, and
|
|
1786
|
-
every other state path. Repeating it is a no-op.
|
|
1787
|
-
Before its first managed write, the migrator atomically fsyncs one mode-0600
|
|
1788
|
-
rollback record at `.sanctuary-package-managed-rollback.json`. That transient
|
|
1789
|
-
record contains a verified SHA-256 digest over the exact prior bytes, modes,
|
|
1790
|
-
and parent existence for those
|
|
1791
|
-
files plus bundle-meta, bound to distinct exact rollback and target image IDs.
|
|
1792
|
-
It is the migration transaction record, not a second bundle authority. Normal
|
|
1793
|
-
managed-file failure restores it immediately; every
|
|
1794
|
-
later container rollback restores it before the old container restarts, while
|
|
1795
|
-
successful production readiness and autostart commit it. A killed updater is
|
|
1796
|
-
reconciled from the same record before update topology preflight: partial new
|
|
1797
|
-
containers are removed, interrupted atomic-write stages are discarded, the
|
|
1798
|
-
exact prior bundle is restored idempotently, and only the image ID recorded in
|
|
1799
|
-
the transaction may resume as production. A malformed record, wrong image,
|
|
1800
|
-
or restore failure leaves autostart disabled and fails closed. Once target
|
|
1801
|
-
readiness and autostart have passed, commit first durably renames the record;
|
|
1802
|
-
a kill then keeps target production in place and the next run validates its
|
|
1803
|
-
exact topology/readiness before finishing commit. Migration failure then enters the same exact container
|
|
1804
|
-
rollback arm before target production starts:
|
|
2262
|
+
Stop production, remove only a stopped stale rollback, rename the known-good container, verify it remains stopped, and apply the exact target image's package-managed bundle migration in one explicit preparation guard.
|
|
2263
|
+
Package-managed files are exactly `provider-readiness.json`, `tool-profiles.json`, `habits/sanctuary-health.md`, and the five canonical files under `psyche/`. The migration also merges the three bundle-meta version fields. It never installs or mutates steward policy authority. It preserves agent.json, all steward policy and audit bytes, relationships, sessions, and every other state path. Repeating it is a no-op.
|
|
2264
|
+
Before its first managed write, the migrator atomically fsyncs one mode-0600 rollback record at `.sanctuary-package-managed-rollback.json`. That record binds the verified prior bytes, modes, and parent existence for every managed file plus bundle-meta to distinct exact rollback and target image IDs. It is the migration receipt, not a second bundle authority.
|
|
2265
|
+
A managed-file failure restores the prior bytes immediately. Every later container rollback also restores those bytes before the old container restarts, while retaining the receipt; only after the old production container is audited, ready, and back on autostart does `finalize-rollback` remove it without marking the new release committed.
|
|
2266
|
+
After an interrupted update, the same receipt removes partial new containers, discards only validated interrupted-write stages, restores the prior bundle idempotently, and permits only the recorded rollback image to resume as production. A malformed receipt, wrong image, or restore failure leaves autostart disabled and stops safely.
|
|
2267
|
+
Once target readiness and autostart pass, `commit` durably marks the receipt as committing. If interruption happens then, the target stays in place and the next run rechecks its exact topology and readiness before finishing. Migration failure enters the same container rollback arm before target production starts:
|
|
1805
2268
|
if docker stop ouro-butler \
|
|
1806
2269
|
&& remove_stopped_rollback_if_present "$ROLLBACK_IMAGE_ID" \
|
|
1807
2270
|
&& docker rename ouro-butler ouro-butler-rollback \
|
|
@@ -1811,6 +2274,7 @@ ouro-butler-rollback
|
|
|
1811
2274
|
:
|
|
1812
2275
|
else
|
|
1813
2276
|
PRODUCTION_PREPARATION_STATUS=$?
|
|
2277
|
+
/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" verify-jellyfin >/dev/null
|
|
1814
2278
|
if docker container inspect ouro-butler >/dev/null 2>&1; then
|
|
1815
2279
|
if docker container inspect ouro-butler-rollback >/dev/null 2>&1; then
|
|
1816
2280
|
docker stop ouro-butler-rollback >/dev/null 2>&1 || true
|
|
@@ -1821,59 +2285,52 @@ ouro-butler-rollback
|
|
|
1821
2285
|
! docker container inspect ouro-butler-rollback >/dev/null 2>&1
|
|
1822
2286
|
rollback_sanctuary_bundle_if_pending "$IMAGE_ID"
|
|
1823
2287
|
assert_update_source "$ROLLBACK_IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID"
|
|
1824
|
-
|
|
1825
|
-
assert_only_running_butler ouro-butler
|
|
2288
|
+
start_only_butler_for_recovery
|
|
1826
2289
|
wait_butler_ready ouro-butler
|
|
1827
2290
|
enable_butler_autostart
|
|
1828
|
-
|
|
2291
|
+
finalize_sanctuary_bundle_rollback_if_retained "$IMAGE_ID"
|
|
1829
2292
|
elif docker container inspect ouro-butler-rollback >/dev/null 2>&1; then
|
|
1830
2293
|
docker stop ouro-butler-rollback >/dev/null 2>&1 || true
|
|
1831
2294
|
test "$(docker inspect --format '{{.Image}}' ouro-butler-rollback)" = "$ROLLBACK_IMAGE_ID"
|
|
1832
2295
|
rollback_sanctuary_bundle_if_pending "$IMAGE_ID"
|
|
1833
2296
|
docker rename ouro-butler-rollback ouro-butler
|
|
1834
2297
|
assert_update_source "$ROLLBACK_IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID"
|
|
1835
|
-
|
|
1836
|
-
assert_only_running_butler ouro-butler
|
|
2298
|
+
start_only_butler_for_recovery
|
|
1837
2299
|
wait_butler_ready ouro-butler
|
|
1838
2300
|
enable_butler_autostart
|
|
1839
|
-
|
|
2301
|
+
finalize_sanctuary_bundle_rollback_if_retained "$IMAGE_ID"
|
|
1840
2302
|
fi
|
|
2303
|
+
/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" rollback >/dev/null
|
|
1841
2304
|
(exit "$PRODUCTION_PREPARATION_STATUS")
|
|
1842
2305
|
fi
|
|
1843
|
-
Preparation failure therefore either restores the still-named exact production
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
from reconciling or claiming live external-event state before cutover.
|
|
1855
|
-
Create and activate production from the same exact image ID and exact authority
|
|
1856
|
-
in one explicit conditional so `set -eu` cannot exit before rollback. Only a
|
|
1857
|
-
successful create, effective audit, start, stopped rollback assertion, and
|
|
1858
|
-
bounded readiness wait may enable production autostart. Any failure captures
|
|
1859
|
-
the activation status, safely removes a partially created new production if it
|
|
1860
|
-
exists, restores the stopped rollback container, audits it against its exact
|
|
1861
|
-
old image ID, proves it ready, atomically enables only production autostart,
|
|
1862
|
-
and then propagates the original activation failure:
|
|
1863
|
-
if docker create --name ouro-butler --network host --restart unless-stopped --user 10001:10001 \
|
|
2306
|
+
Preparation failure therefore either restores the still-named exact production after removing any stale rollback, or renames the exact stopped rollback back. Both paths revalidate and start the old container, wait within the fixed bound, restore production-only autostart atomically, and return the original failure.
|
|
2307
|
+
If neither exact container can be found, the failure returns with Butler autostart disabled.
|
|
2308
|
+
Do not start a target-image daemon between the production rename and final production activation. The exact-image static audit, download-queue readiness, and vault-backed Telegram identity check have already passed before autostart or live-container changes.
|
|
2309
|
+
Provider and complete daemon readiness are exercised only by the transactional production activation below. Its failure arm restores and revalidates the exact prior production, so a disposable daemon cannot reconcile or claim live external-event state before cutover.
|
|
2310
|
+
Create and activate production from the same exact image ID and exact authority in one explicit conditional so `set -eu` cannot exit before rollback. Only a successful create, effective audit, start, stopped-rollback assertion, and bounded readiness wait may enable production autostart.
|
|
2311
|
+
On failure, capture the activation status, remove only a partially created new production container, restore and audit the stopped rollback against its exact old image ID, prove it ready, restore production-only autostart atomically, and return the original failure:
|
|
2312
|
+
if test "$(docker buildx imagetools inspect "$VERSION_IMAGE" --format '{{.Manifest.Digest}}')" = "$MANIFEST_DIGEST" \
|
|
2313
|
+
&& test "$(docker image inspect --format '{{.Id}}' "$VERSION_IMAGE")" = "$IMAGE_ID" \
|
|
2314
|
+
&& docker create --pull=never --name ouro-butler --network host --restart unless-stopped --user 10001:10001 \
|
|
2315
|
+
--label net.unraid.docker.managed=dockerman \
|
|
2316
|
+
--label "net.unraid.docker.icon=$TEMPLATE_ICON" \
|
|
1864
2317
|
--mount "type=bind,src=/mnt/user/appdata/ouro-butler/runtime/.ouro-cli,dst=/home/ouro/.ouro-cli" \
|
|
1865
2318
|
--mount "type=bind,src=/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro,dst=/home/ouro/AgentBundles/sanctuary.ouro" \
|
|
1866
2319
|
--mount "type=bind,src=/boot/config/custom/ouro-events/spool,dst=/run/ouro-events,readonly" \
|
|
1867
|
-
"$
|
|
1868
|
-
&& audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" \
|
|
2320
|
+
"$VERSION_IMAGE" \
|
|
2321
|
+
&& audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" canonical "$VERSION_IMAGE" "$TEMPLATE_ICON" \
|
|
2322
|
+
&& assert_only_running_butler - \
|
|
1869
2323
|
&& docker start ouro-butler \
|
|
1870
2324
|
&& assert_only_running_butler ouro-butler \
|
|
2325
|
+
&& test "$(docker inspect --format '{{.Image}}' ouro-butler)" = "$IMAGE_ID" \
|
|
1871
2326
|
&& test "$(docker inspect --format '{{.State.Running}}' ouro-butler-rollback)" = false \
|
|
1872
2327
|
&& wait_butler_ready ouro-butler \
|
|
1873
|
-
&& enable_butler_autostart
|
|
2328
|
+
&& enable_butler_autostart \
|
|
2329
|
+
&& /usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" mark-committing >/dev/null; then
|
|
1874
2330
|
:
|
|
1875
2331
|
else
|
|
1876
2332
|
PRODUCTION_ACTIVATION_STATUS=$?
|
|
2333
|
+
/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" verify-jellyfin >/dev/null
|
|
1877
2334
|
if docker container inspect ouro-butler >/dev/null 2>&1; then
|
|
1878
2335
|
docker stop ouro-butler >/dev/null 2>&1 || true
|
|
1879
2336
|
PARTIAL_PRODUCTION_IMAGE_ID=$(docker inspect --format '{{.Image}}' ouro-butler)
|
|
@@ -1886,17 +2343,23 @@ ouro-butler-rollback
|
|
|
1886
2343
|
migrate_sanctuary_package_managed_bundle "$IMAGE_ID" rollback
|
|
1887
2344
|
docker rename ouro-butler-rollback ouro-butler
|
|
1888
2345
|
assert_update_source "$ROLLBACK_IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID"
|
|
1889
|
-
|
|
1890
|
-
assert_only_running_butler ouro-butler
|
|
2346
|
+
start_only_butler_for_recovery
|
|
1891
2347
|
wait_butler_ready ouro-butler
|
|
1892
2348
|
enable_butler_autostart
|
|
1893
|
-
migrate_sanctuary_package_managed_bundle "$IMAGE_ID"
|
|
2349
|
+
migrate_sanctuary_package_managed_bundle "$IMAGE_ID" finalize-rollback
|
|
2350
|
+
/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" rollback >/dev/null
|
|
1894
2351
|
(exit "$PRODUCTION_ACTIVATION_STATUS")
|
|
1895
2352
|
fi
|
|
1896
2353
|
migrate_sanctuary_package_managed_bundle "$IMAGE_ID" commit
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
2354
|
+
FINAL_PROOF_ROOT="$EVENT_ASSET_STAGE/final-proof"
|
|
2355
|
+
FINAL_PROOF_PATH=$(write_dockerman_final_proof "$IMAGE_ID" "$VERSION_IMAGE" "$TEMPLATE_ICON" "$FINAL_PROOF_ROOT")
|
|
2356
|
+
verify_known_good_rollback_artifact "$ROLLBACK_IMAGE_ID"
|
|
2357
|
+
/usr/local/bin/node "$STAGED_DOCKERMAN_TRANSACTION" commit --proof "$FINAL_PROOF_PATH" >/dev/null
|
|
2358
|
+
cleanup_event_asset_stage
|
|
2359
|
+
trap - EXIT
|
|
2360
|
+
Keep ouro-butler-rollback stopped until the new production container is proven or the explicit rollback arm restores it. Never create production from a mutable tag, a bare tag, or a bare local image ID.
|
|
2361
|
+
Docker tab Update, Force Update, Edit/Apply, Update All, and CA Action Centre updates remain visible but are unsupported because stock recreation deletes reviewed rollback evidence. Visibility, start, stop, and autostart remain supported; use only this reviewed version-tag transaction for updates.
|
|
2362
|
+
Community Apps determines installed state from the DockerMan template plus the live container name and image. The helper proves that same relationship without calling the endpoint that refreshes Community Apps' UI cache; the later live UI smoke confirms what Ari sees.
|
|
1900
2363
|
|
|
1901
2364
|
Backup:
|
|
1902
2365
|
Set BACKUP_ROOT to a new absolute snapshot path on the destination filesystem.
|
|
@@ -2076,6 +2539,7 @@ Backup:
|
|
|
2076
2539
|
BACKUP_OPERATION_STATUS=$?
|
|
2077
2540
|
fi
|
|
2078
2541
|
if assert_update_source "$BACKUP_IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" \
|
|
2542
|
+
&& assert_only_running_butler - \
|
|
2079
2543
|
&& docker start ouro-butler \
|
|
2080
2544
|
&& assert_only_running_butler ouro-butler \
|
|
2081
2545
|
&& wait_butler_ready ouro-butler \
|
|
@@ -2106,13 +2570,20 @@ Restore:
|
|
|
2106
2570
|
legacy-aware auditor. The runner is independent from the restored image.
|
|
2107
2571
|
AUDIT_RUNNER_IMAGE_TAG=ouro-butler:<new-version>
|
|
2108
2572
|
AUDIT_RUNNER_IMAGE_ID=$(docker image inspect --format '{{.Id}}' "$AUDIT_RUNNER_IMAGE_TAG")
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2573
|
+
RESTORE_VERSION_IMAGE=$(/usr/local/bin/node -e '
|
|
2574
|
+
const fs = require("node:fs");
|
|
2575
|
+
const [containerPath, versionPath] = process.argv.slice(1);
|
|
2576
|
+
const containers = JSON.parse(fs.readFileSync(containerPath, "utf8"));
|
|
2577
|
+
const version = fs.readFileSync(versionPath, "utf8").trim();
|
|
2578
|
+
const expected = `ghcr.io/ourostack/ouroboros-butler:${version}`;
|
|
2579
|
+
if (!Array.isArray(containers) || containers.length !== 1 || containers[0]?.Config?.Image !== expected) process.exit(1);
|
|
2580
|
+
process.stdout.write(expected);
|
|
2581
|
+
' "$BACKUP_ROOT/provenance/container-inspect.json" "$BACKUP_ROOT/provenance/package-version")
|
|
2582
|
+
test "$(docker image inspect --format '{{.Id}}' "$RESTORE_VERSION_IMAGE")" = "$IMAGE_ID"
|
|
2583
|
+
audit_registered_dockerman_template "$AUDIT_RUNNER_IMAGE_ID" "$RESTORE_VERSION_IMAGE"
|
|
2584
|
+
Before changing autostart, durable roots, or live containers, run the nounset-safe input, backup-root, image, and topology preflight. It requires a nonempty canonical absolute BACKUP_ROOT other than /, both required directories, an exact local sha256 image ID, canonical production as the only running Butler, no staging or rollback, and at most one exact stopped legacy-evidence container. It also audits the live source container with the reviewed runner.
|
|
2585
|
+
Restore never rewrites DockerMan registration outside the reviewed template transaction. The preflight therefore stops unless the persistent template already names the exact snapshot version, and the post-start check proves DockerMan and Community Apps still recognize it.
|
|
2586
|
+
If the template names another version, first run the same reviewed version-tag update transaction for the snapshot version, then rerun Restore. A mismatch stops before autostart, durable-root, or production-container changes:
|
|
2116
2587
|
if assert_restore_preflight; then
|
|
2117
2588
|
:
|
|
2118
2589
|
else
|
|
@@ -2136,10 +2607,13 @@ Restore:
|
|
|
2136
2607
|
/bin/bash /boot/config/custom/ouro-events/bootstrap-spool.sh --mount
|
|
2137
2608
|
test "$(findmnt -n -o FSTYPE --target /boot/config/custom/ouro-events/spool)" = tmpfs
|
|
2138
2609
|
test "$(stat -c '%u:%g:%a' /boot/config/custom/ouro-events/spool)" = 0:0:755
|
|
2610
|
+
RESTORE_INSTALL_PROOF_ROOT=$(mktemp -d /mnt/user/appdata/ouro-butler/staging/restore-install-proof.XXXXXX)
|
|
2611
|
+
chmod 0700 "$RESTORE_INSTALL_PROOF_ROOT"
|
|
2139
2612
|
if disable_butler_autostart; then
|
|
2140
2613
|
:
|
|
2141
2614
|
else
|
|
2142
2615
|
RESTORE_AUTOSTART_DISABLE_STATUS=$?
|
|
2616
|
+
rmdir -- "$RESTORE_INSTALL_PROOF_ROOT"
|
|
2143
2617
|
(exit "$RESTORE_AUTOSTART_DISABLE_STATUS")
|
|
2144
2618
|
fi
|
|
2145
2619
|
Stop and remove only ouro-butler, restore both roots without stale files,
|
|
@@ -2163,15 +2637,22 @@ Restore:
|
|
|
2163
2637
|
&& /bin/bash "$HOST_RESTORE_INSTALLER" --restore-root "$BACKUP_ROOT/host" \
|
|
2164
2638
|
&& rm -f "$HOST_RESTORE_INSTALLER" \
|
|
2165
2639
|
&& docker image inspect "$IMAGE_ID" >/dev/null \
|
|
2166
|
-
&& docker create --name ouro-butler --network host --restart unless-stopped --user 10001:10001 \
|
|
2640
|
+
&& docker create --pull=never --name ouro-butler --network host --restart unless-stopped --user 10001:10001 \
|
|
2641
|
+
--label net.unraid.docker.managed=dockerman \
|
|
2642
|
+
--label "net.unraid.docker.icon=https://raw.githubusercontent.com/ourostack/ouroboros/main/assets/ouroboros.png" \
|
|
2167
2643
|
--mount "type=bind,src=/mnt/user/appdata/ouro-butler/runtime/.ouro-cli,dst=/home/ouro/.ouro-cli" \
|
|
2168
2644
|
--mount "type=bind,src=/mnt/user/appdata/ouro-butler/agent/sanctuary.ouro,dst=/home/ouro/AgentBundles/sanctuary.ouro" \
|
|
2169
2645
|
--mount "type=bind,src=/boot/config/custom/ouro-events/spool,dst=/run/ouro-events,readonly" \
|
|
2170
|
-
"$
|
|
2171
|
-
&& audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" \
|
|
2646
|
+
"$RESTORE_VERSION_IMAGE" \
|
|
2647
|
+
&& audit_effective ouro-butler "$IMAGE_ID" "$AUDIT_RUNNER_IMAGE_ID" canonical "$RESTORE_VERSION_IMAGE" https://raw.githubusercontent.com/ourostack/ouroboros/main/assets/ouroboros.png \
|
|
2648
|
+
&& assert_only_running_butler - \
|
|
2172
2649
|
&& docker start ouro-butler \
|
|
2173
2650
|
&& assert_only_running_butler ouro-butler \
|
|
2651
|
+
&& test "$(docker inspect --format '{{.Image}}' ouro-butler)" = "$IMAGE_ID" \
|
|
2174
2652
|
&& wait_butler_ready ouro-butler \
|
|
2653
|
+
&& verify_dockerman_and_community_apps "$RESTORE_VERSION_IMAGE" "$RESTORE_INSTALL_PROOF_ROOT/install.json" \
|
|
2654
|
+
&& rm -f -- "$RESTORE_INSTALL_PROOF_ROOT/install.json" \
|
|
2655
|
+
&& rmdir -- "$RESTORE_INSTALL_PROOF_ROOT" \
|
|
2175
2656
|
&& enable_butler_autostart; then
|
|
2176
2657
|
:
|
|
2177
2658
|
else
|
|
@@ -2184,6 +2665,8 @@ Restore:
|
|
|
2184
2665
|
fi
|
|
2185
2666
|
! docker container inspect ouro-butler >/dev/null 2>&1
|
|
2186
2667
|
rm -f "$HOST_RESTORE_INSTALLER"
|
|
2668
|
+
rm -f -- "$RESTORE_INSTALL_PROOF_ROOT/install.json"
|
|
2669
|
+
rmdir -- "$RESTORE_INSTALL_PROOF_ROOT"
|
|
2187
2670
|
(exit "$RESTORE_ACTIVATION_STATUS")
|
|
2188
2671
|
fi
|
|
2189
2672
|
A restore has no retained old container or pre-restore data root to roll back
|
|
@@ -2467,12 +2950,7 @@ Packaged Unit 16 acceptance execution:
|
|
|
2467
2950
|
inode used by the one-shot for marker correlation.
|
|
2468
2951
|
|
|
2469
2952
|
Audit and safety verification:
|
|
2470
|
-
Inspect AgentBundles/sanctuary.ouro/state/approvals for durable approval and
|
|
2471
|
-
restart-attempt receipts. Confirm no Docker socket/device/host-root mounts and
|
|
2472
|
-
no published ports with docker inspect. Confirm Config.Image equals the exact
|
|
2473
|
-
reviewed local image ID, not the build tag. The read key must reject Docker
|
|
2474
|
-
stop and restart mutations; only the separate write key may perform the one
|
|
2475
|
-
typed approved restart action.
|
|
2953
|
+
Inspect AgentBundles/sanctuary.ouro/state/approvals for durable approval and restart-attempt receipts. Confirm with docker inspect that the container has no Docker socket, device, host-root mount, or published port. Confirm `Config.Image` equals the canonical package-version tag and `.Image` equals the exact reviewed local image ID. The read key must reject Docker stop and restart mutations; only the separate write key may perform the one typed approved restart action.
|
|
2476
2954
|
The packaged `unraid-key-rotate` command is the only canonical key-rotation
|
|
2477
2955
|
authority. Its host launcher stops the exact production container and assumes
|
|
2478
2956
|
trap-backed recovery responsibility before key mutation. It inventories the
|