summaryrefslogtreecommitdiff
path: root/infra/guix/test-recovery.sh
diff options
context:
space:
mode:
authorDoan Luu Duc Tai <doan@luuduct.ai>2026-11-01 07:13:45 -0600
committerDoan Luu Duc Tai <doan@luuduct.ai>2026-11-01 07:13:45 -0600
commit9349a2de648e241da8e00ba7ae25cb2a877ff3e9 (patch)
tree2fea913ee0aaff4370877f4a91c911db2fe37592 /infra/guix/test-recovery.sh
parent5b89c370d5974f057306a52089f14c337565fd1d (diff)
feat(guix): carry prebuilt runtime and recover preserved state
Diffstat (limited to 'infra/guix/test-recovery.sh')
-rw-r--r--infra/guix/test-recovery.sh133
1 files changed, 133 insertions, 0 deletions
diff --git a/infra/guix/test-recovery.sh b/infra/guix/test-recovery.sh
new file mode 100644
index 0000000..c03ed2f
--- /dev/null
+++ b/infra/guix/test-recovery.sh
@@ -0,0 +1,133 @@
+#!/bin/sh
+# Copyright (C) 2026 Doan Luu Duc Tai <doan@luuduct.ai>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+set -eu
+umask 077
+ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
+command -v bwrap >/dev/null || { echo 'Recovery tests require bubblewrap.' >&2; exit 1; }
+STAGE=$(mktemp -d)
+trap 'chmod -R u+w "$STAGE"; rm -rf -- "$STAGE"' EXIT HUP INT TERM
+mkdir -p "$STAGE/capsule" "$STAGE/runtime/bin" "$STAGE/test-bin"
+cp "$ROOT/infra/guix/restore.sh" "$STAGE/capsule/restore"
+cp "$ROOT/infra/guix/run.sh" "$STAGE/capsule/run"
+cp "$ROOT/infra/guix/containers.conf" "$STAGE/capsule/containers.conf"
+printf '%s\n' 'opaque encrypted backup fixture' > "$STAGE/capsule/backup.tar.gz.gpg"
+printf '%s\n' 'opaque release fixture' > "$STAGE/capsule/release.tar.gz"
+printf '%s\n' 80 > "$STAGE/lowport"
+printf '%s\n' '#!/bin/sh' 'exit 0' > "$STAGE/runtime/bin/gpg"
+cat > "$STAGE/runtime/bin/podman" <<'PODMAN'
+#!/bin/sh
+set -eu
+[ "$CONTAINERS_CONF" = /capsule/containers.conf ]
+printf '%s\n' "$*" >> /tmp/podman.calls
+PODMAN
+cat > "$STAGE/test-bin/id" <<'ID'
+#!/bin/sh
+case "$1" in -u) echo 1000 ;; -un) echo service ;; *) exit 1 ;; esac
+ID
+for HELPER in newuidmap newgidmap; do
+ printf '%s\n' '#!/bin/sh' 'exit 0' > "$STAGE/test-bin/$HELPER"
+done
+cat > "$STAGE/capsule/stack" <<'STACK'
+#!/bin/sh
+set -eu
+[ "$CONTAINERS_CONF" = /capsule/containers.conf ]
+case "$(command -v podman)" in /tmp/*/runtime/bin/podman) ;; *) exit 97 ;; esac
+case "$1" in
+ restore)
+ [ "$#" = 3 ] && [ "$2" = /capsule/backup.tar.gz.gpg ]
+ mkdir -p "$3/var/data" "$3/bin"
+ printf '%s\n' private-environment > "$3/.env"
+ printf '%s\n' encrypted-history > "$3/var/data/history"
+ printf '%s\n' images > "$3/var/images.tar"
+ printf '%s\n' image-lock > "$3/var/image-lock.json"
+ printf '%s\n' '#!/bin/sh' 'exit 99' > "$3/bin/stack"
+ chmod 700 "$3/bin/stack"
+ printf '%s\n' old-source > "$3/source"
+ echo restored >> /tmp/restorations
+ ;;
+ --root)
+ [ "$#" = 4 ] && [ "$3" = activate ] && [ "$4" = /capsule/release.tar.gz ]
+ [ ! -e /proc/self/fd/9 ]
+ [ "$(cat "$4")" = 'opaque release fixture' ]
+ echo activated >> /tmp/activations
+ [ ! -f /tmp/fail-activation ] || exit 42
+ mkdir -p "$2/infra/guix"
+ printf '%s\n' '#!/bin/sh' 'echo new-management' > "$2/bin/stack"
+ chmod 700 "$2/bin/stack"
+ printf '%s\n' new-source > "$2/source"
+ cp /capsule/run "$2/infra/guix/run.sh"
+ cp /capsule/run "$2/bin/run"
+ cp /capsule/containers.conf "$2/infra/guix/containers.conf"
+ ;;
+ *) exit 98 ;;
+esac
+STACK
+chmod 700 "$STAGE/capsule/stack" "$STAGE/runtime/bin/"* "$STAGE/test-bin/"*
+tar -czf "$STAGE/capsule/runtime.tar.gz" -C "$STAGE/runtime" .
+(cd "$STAGE/capsule" && sha256sum backup.tar.gz.gpg release.tar.gz runtime.tar.gz restore run stack containers.conf > SHA256SUMS)
+bwrap --unshare-user --uid 0 --gid 0 --unshare-pid --cap-add CAP_DAC_OVERRIDE \
+ --tmpfs / --ro-bind /usr /usr \
+ --symlink usr/bin /bin --symlink usr/lib /lib --symlink usr/lib /lib64 \
+ --ro-bind "$STAGE/capsule" /fixture --ro-bind "$STAGE/test-bin" /test-bin \
+ --proc /proc --dev /dev --tmpfs /tmp \
+ --ro-bind "$STAGE/lowport" /proc/sys/net/ipv4/ip_unprivileged_port_start \
+ --setenv PATH /test-bin:/usr/bin --setenv XDG_RUNTIME_DIR /tmp/xdg \
+ --chdir / /bin/sh -eu <<'TEST'
+fail() { printf '%s\n' "$1" >&2; exit 1; }
+reject() { if sh /capsule/restore "$1"; then fail "$2"; fi; }
+cp -a /fixture /capsule
+mkdir -p /etc /gnu/.stack-runtimes /tmp/xdg
+printf '%s\n' service:100000:65536 > /etc/subuid
+cp /etc/subuid /etc/subgid
+DIGEST=$(sha256sum /capsule/runtime.tar.gz)
+DIGEST=${DIGEST%% *}
+printf '%s\n' "$DIGEST" > "/gnu/.stack-runtimes/$DIGEST"
+touch /tmp/fail-activation
+STATUS=0
+sh /capsule/restore /recovered || STATUS=$?
+[ "$STATUS" = 42 ] || fail 'Did not reach the paired release activation.'
+[ -f /recovered/var/guix-recovery ]
+[ "$(cat /recovered/.env)" = private-environment ]
+[ "$(cat /recovered/var/data/history)" = encrypted-history ]
+[ "$(cat /recovered/source)" = old-source ]
+cp /capsule/backup.tar.gz.gpg /tmp/original-backup
+echo changed >> /capsule/backup.tar.gz.gpg
+(cd /capsule && sha256sum backup.tar.gz.gpg release.tar.gz runtime.tar.gz restore run stack containers.conf > SHA256SUMS)
+reject /recovered 'Accepted a different backup for pending recovery.'
+[ "$(wc -l < /tmp/restorations)" = 1 ] && [ "$(wc -l < /tmp/activations)" = 1 ]
+cp /tmp/original-backup /capsule/backup.tar.gz.gpg
+(cd /capsule && sha256sum backup.tar.gz.gpg release.tar.gz runtime.tar.gz restore run stack containers.conf > SHA256SUMS)
+exec 9>/recovered/var/guix-recovery.lock
+flock 9
+reject /recovered 'Accepted concurrent recovery.' 9>&-
+exec 9>&-
+[ "$(wc -l < /tmp/activations)" = 1 ]
+rm /tmp/fail-activation
+sh /capsule/restore /recovered
+[ "$(wc -l < /tmp/restorations)" = 1 ] && [ "$(wc -l < /tmp/activations)" = 2 ]
+[ ! -e /recovered/var/guix-recovery ]
+[ "$(/recovered/bin/stack)" = new-management ] && [ "$(cat /recovered/source)" = new-source ]
+cmp /capsule/run /recovered/infra/guix/run.sh
+cmp /capsule/containers.conf /recovered/infra/guix/containers.conf
+[ "$(cat /recovered/.env)" = private-environment ]
+[ "$(cat /recovered/var/data/history)" = encrypted-history ]
+mkdir /unrelated
+reject /unrelated 'Accepted unrelated existing destination.'
+reject /recovered 'Repeated a completed recovery.'
+[ "$(wc -l < /tmp/restorations)" = 1 ] && [ "$(wc -l < /tmp/activations)" = 2 ]
+printf '%s\n' 'Synthetic recovery orchestration, resume, intent, locks and preservation passed; no real encryption or container lifecycle proved.'
+TEST