Skip to main content

max / alloy

7.1 KB · 163 lines History Blame Raw
1 #!/usr/bin/env bash
2 #
3 # dev-push.sh — hand a freshly built image to a machine already running Alloy,
4 # without building an ISO.
5 #
6 # The ISO is how a machine is installed. It is the wrong artifact for "did my
7 # sway config land", and using it that way is what makes the edit-to-look-at-it
8 # loop four minutes long: measured on fw13, a config-only change is 15 seconds
9 # of `podman build` followed by an ISO assembly that costs sixteen times that
10 # and produces installer media nobody is going to install from.
11 #
12 # An installed machine adopts a locally built image in place. That is not a
13 # workaround: it is the same A/B staged switch with the same rollback that a
14 # registry-fed update uses (docs/IMAGE.md, "Updates"), with the image coming
15 # off a registry on this box instead of one on the internet.
16 #
17 # Why a registry and not a file. bootc reads several transports, and the
18 # obvious ones move the whole image every time: `containers-storage` is not
19 # reachable from another machine at all, and an archive is 3 GB down the wire
20 # per rebuild. A registry stores layers, so the target pulls only the ones it
21 # does not have, and a config-only rebuild changes the tail of the image.
22 #
23 # The push itself is NOT incremental, and measuring it is the only way anyone
24 # would know. skopeo re-copies all 125 blobs on every run — 22s, twice in a
25 # row, with no "Skipping fetch of repeat blob" — because a containers-storage
26 # source carries no compression metadata, so each layer has to be recompressed
27 # before its digest is even known. 22s is the fixed price of a push here. What
28 # the registry buys is the wire to the target, not this end.
29 #
30 # The registry is a dev tool that runs on this box and is torn down with
31 # `--stop`; nothing about it is the distribution story, which stays "no image
32 # is published anywhere" (docs/IMAGE.md, "Registry").
33 #
34 # Usage:
35 # build/dev-push.sh # push localhost/alloy:local, print the
36 # # line to run on the target
37 # build/dev-push.sh --address HOST # print that line for a given address
38 # build/dev-push.sh --port 5000 # a different port
39 # build/dev-push.sh --stop # stop the registry and forget its blobs
40 #
41 # On the target, the first time:
42 #
43 # printf '[[registry]]\nlocation = "ADDR"\ninsecure = true\n' \
44 # | sudo tee /etc/containers/registries.conf.d/99-alloy-dev.conf
45 # sudo bootc switch --transport registry ADDR/alloy:local
46 # sudo systemctl reboot
47 #
48 # and after that, for every later push, `sudo bootc upgrade && sudo systemctl
49 # reboot`: the machine remembers where it was switched to.
50 #
51 # The insecure line is what plain HTTP costs. It is scoped to one address in a
52 # drop-in file, it is a development machine talking to a registry on the same
53 # desk, and `build/vmtest` is the intended target. Do not carry it onto a
54 # machine that matters.
55 #
56 # The registry itself binds every interface, because a VM reaches it at
57 # 10.0.2.2 and a real target reaches it by tailnet name, and it takes no
58 # authentication: while it is up, anyone who can reach this box can read the
59 # image and push one. That is the same trust boundary as the tailnet and it is
60 # still a reason to run it only while a push is in flight. `--stop` is one
61 # command and the next push starts it again.
62
63 set -euo pipefail
64
65 REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
66
67 # priv / privc. run0 where it exists, sudo where it does not; see the header
68 # of build/privilege.sh for which of the two a call site wants.
69 # shellcheck source=build/privilege.sh
70 . "$REPO_ROOT/build/privilege.sh"
71
72 IMAGE="localhost/alloy:local"
73 REGISTRY_IMAGE="docker.io/library/registry:2"
74 CONTAINER="alloy-dev-registry"
75 # A named volume rather than a tmpfs, so the blobs survive a restart of the
76 # registry. What that protects is the target's next pull: with the layers still
77 # here, a machine that already fetched them fetches only what changed.
78 VOLUME="alloy-dev-registry"
79 PORT=5000
80 ADDRESS=""
81 STOP=0
82
83 die() { printf 'error: %s\n' "$*" >&2; exit 1; }
84 say() { printf '==> %s\n' "$*"; }
85
86 while [ $# -gt 0 ]; do
87 case "$1" in
88 --address) ADDRESS="${2:?--address needs a host}"; shift 2 ;;
89 --port) PORT="${2:?--port needs a number}"; shift 2 ;;
90 --stop) STOP=1; shift ;;
91 # The header block is the help text, so it stops where the comments stop.
92 -h|--help) awk 'NR==1 {next} !/^#/ {exit} {sub(/^# ?/, ""); print}' "${BASH_SOURCE[0]}"; exit 0 ;;
93 *) die "unknown argument: $1 (see --help)" ;;
94 esac
95 done
96
97 command -v podman >/dev/null || die "podman not found"
98
99 if [ "$STOP" -eq 1 ]; then
100 say "stopping $CONTAINER"
101 privc podman rm -f "$CONTAINER" >/dev/null 2>&1 || true
102 privc podman volume rm "$VOLUME" >/dev/null 2>&1 || true
103 say "stopped. The next target to switch to it pulls the whole image again."
104 exit 0
105 fi
106
107 privc podman image exists "$IMAGE" \
108 || die "$IMAGE is not in the root store; build it first with build/build-iso.sh --skip-source or podman build"
109
110 # The registry, started if it is not already up. Rootful, because the image it
111 # serves lives in the root store and this script pushes from there.
112 if [ "$(privc podman inspect -f '{{.State.Running}}' "$CONTAINER" 2>/dev/null || echo false)" != "true" ]; then
113 privc podman rm -f "$CONTAINER" >/dev/null 2>&1 || true
114 say "starting $CONTAINER on :$PORT"
115 priv podman run -d --name "$CONTAINER" \
116 -p "$PORT:5000" \
117 -v "$VOLUME:/var/lib/registry" \
118 "$REGISTRY_IMAGE" >/dev/null
119 else
120 say "$CONTAINER is up"
121 fi
122
123 # skopeo out of the Alloy image itself. The base carries it, so this needs no
124 # skopeo on the host — fw13 is Pop!_OS and has none — and no second image to
125 # keep current. --network host so 127.0.0.1 means this box rather than the
126 # skopeo container's own loopback, which is the failure that looks like the
127 # registry being down.
128 say "pushing $IMAGE (about 20s; skopeo recompresses every layer, see the header)"
129 priv podman run --rm --privileged --network host \
130 --security-opt label=type:unconfined_t \
131 -v /var/lib/containers/storage:/var/lib/containers/storage \
132 --entrypoint skopeo \
133 "$IMAGE" \
134 copy --dest-tls-verify=false \
135 "containers-storage:$IMAGE" "docker://127.0.0.1:$PORT/alloy:local"
136
137 # What to type on the target. Printed rather than run over ssh: the target is a
138 # machine that is about to be told to boot something else, and the script that
139 # builds an image should not also be the thing that reboots your laptop.
140 if [ -z "$ADDRESS" ]; then
141 echo
142 echo " Reachable as, depending on what the target is:"
143 echo " 10.0.2.2:$PORT a qemu guest on user-mode networking (build/vmtest)"
144 echo " <this host>:$PORT anything else, by tailnet name or LAN address"
145 ADDRESS="<host>:$PORT"
146 else
147 ADDRESS="$ADDRESS:$PORT"
148 fi
149
150 cat <<EOF
151
152 On the target, once:
153 printf '[[registry]]\\nlocation = "$ADDRESS"\\ninsecure = true\\n' \\
154 | sudo tee /etc/containers/registries.conf.d/99-alloy-dev.conf
155 sudo bootc switch --transport registry $ADDRESS/alloy:local
156 sudo systemctl reboot
157
158 And for every push after that:
159 sudo bootc upgrade && sudo systemctl reboot
160
161 Rolling back is bootc's own: sudo bootc rollback && sudo systemctl reboot
162 EOF
163