diff --git a/docs/provisioning.md b/docs/provisioning.md index 75408b8..6e0cecf 100644 --- a/docs/provisioning.md +++ b/docs/provisioning.md @@ -70,3 +70,43 @@ boot. A correctly configured queue is a quick no-op. The boot provisioning command never upgrades or reconfigures anything already installed, and it never touches a repository it did not add. Classroom software updates are the OTA channel's job, not this. + +## Permanent download addresses + +``` +https://updates.riverofglorymn.org/stable/latest.deb +https://updates.riverofglorymn.org/canary/latest.deb +``` + +One address per channel that never changes. Each is a symlink beside that +channel's manifest, pointing at the versioned package the manifest names, +so there is still exactly one record of which version is current — the +manifest. Nothing here is a second version database. + +The versioned files under `/packages/` are never rewritten. A release +uploads its package, then the manifest, then swaps the pointer by rename. +Renaming is atomic, so nothing downloads a half-written pointer, and +going last means `latest.deb` can lag the manifest by a moment but can +never lead it: it will never offer a package the channel has not +published. + +`latest.deb` and `manifest.json` are served `no-store, no-cache, +must-revalidate`. That header matters more than it looks: Cloudflare sits +in front of this server and caches `.deb` by extension, so without it a +room could be handed the previous release from an edge for hours. The +versioned packages are deliberately left cacheable — they are immutable. + +**Installing from a channel URL does not move a machine onto that +channel.** `CHANNEL` in device.conf is the only thing that decides which +manifest the updater follows, and the package's postinst only ever +appends settings that are missing. Downloading the canary package to fix +a machine by hand leaves it on stable, and the next automatic update +brings it back to stable. + +Verify a download against the channel it came from: + +``` +curl -fsSL -o c.deb https://updates.riverofglorymn.org/canary/latest.deb +sha256sum c.deb +curl -fsS https://updates.riverofglorymn.org/canary/manifest.json +``` diff --git a/tests/test-deployment-support.sh b/tests/test-deployment-support.sh index 5612f30..c0fa6dc 100755 --- a/tests/test-deployment-support.sh +++ b/tests/test-deployment-support.sh @@ -445,6 +445,37 @@ check("the same Trixie gate runs first", check("and the manifest goes up last", code.rindex("manifest.json") > code.index("packages/")) +print() +print("7c-iii. the permanent download addresses") +# .../stable/latest.deb and .../canary/latest.deb. One address per channel +# that never changes, and still no second record of which version is +# current: the pointer is a symlink to the package the manifest names. +for channel, script in (("canary", "update_canary.sh"), + ("stable", "update_stable.sh")): + text = (root / script).read_text() + code = "\n".join(line for line in text.splitlines() + if not line.lstrip().startswith("#")) + check(f"{script} moves the {channel} pointer", + f"{channel}/latest.deb" in text or "latest.deb" in code) + check(f"{script} points it at the versioned package", + "ln -sfn ../packages/$package_name" in code) + # Atomic, so nothing downloads a half-written pointer, and after the + # manifest, so the pointer can lag it but never lead it — it can never + # offer a package the channel has not published. + check(f"{script} swaps it by rename rather than in place", + "mv -Tf latest.deb.new latest.deb" in code) + # Against the rsync that actually uploads the manifest, not the echo + # at the end that prints its URL. + upload = f"classroom-updates/{channel}/manifest.json" + check(f"{script} moves it after the manifest goes up", + code.index("latest.deb") > code.index(upload)) + check(f"{script} refuses to point at a package that is not there", + "test -f ../packages/$package_name" in code) + +check("and neither script writes a version anywhere but the manifest", + "latest.json" not in (root / "update_canary.sh").read_text() + and "latest.json" not in (root / "update_stable.sh").read_text()) + print() print("7d. the broker every room shares") mqtt_unit = (root / "systemd/mqtt/ekklesia-classroom-mqtt.service").read_text() diff --git a/update_canary.sh b/update_canary.sh index 3bd6b94..272c7df 100755 --- a/update_canary.sh +++ b/update_canary.sh @@ -70,6 +70,30 @@ rsync -av \ "$ROOT/repo/canary/manifest.json" \ classroom-control:/srv/ekklesia/classroom-updates/canary/manifest.json + +# THE PERMANENT URL. +# +# .../canary/latest.deb is a symlink beside the manifest pointing at the +# versioned package the manifest names, so there is one download address +# that never changes and still no second place recording which version is +# current. The manifest remains the only answer to that question. +# +# It is swapped AFTER the manifest, and by rename, for two reasons. A +# rename is atomic, so nothing ever downloads a half-written pointer; and +# going last means latest.deb can only ever lag the manifest by a moment, +# never lead it — it can never offer a package the channel has not +# published. The versioned file itself is never touched again once +# uploaded. +ssh classroom-control " + set -e + cd /srv/ekklesia/classroom-updates/canary + test -f ../packages/$package_name + ln -sfn ../packages/$package_name latest.deb.new + mv -Tf latest.deb.new latest.deb +" + +echo "https://updates.riverofglorymn.org/canary/latest.deb -> $package_name" + echo echo "Published Ekklesia Classroom $version to CANARY" echo "https://updates.riverofglorymn.org/canary/manifest.json" diff --git a/update_stable.sh b/update_stable.sh index f4ed14b..79be798 100755 --- a/update_stable.sh +++ b/update_stable.sh @@ -48,6 +48,32 @@ rsync -av \ "$ROOT/repo/stable/manifest.json" \ classroom-control:/srv/ekklesia/classroom-updates/stable/manifest.json + +package_name="ekklesia-classroom_${version}_amd64.deb" + +# THE PERMANENT URL. +# +# .../stable/latest.deb is a symlink beside the manifest pointing at the +# versioned package the manifest names, so there is one download address +# that never changes and still no second place recording which version is +# current. The manifest remains the only answer to that question. +# +# It is swapped AFTER the manifest, and by rename, for two reasons. A +# rename is atomic, so nothing ever downloads a half-written pointer; and +# going last means latest.deb can only ever lag the manifest by a moment, +# never lead it — it can never offer a package the channel has not +# published. The versioned file itself is never touched again once +# uploaded. +ssh classroom-control " + set -e + cd /srv/ekklesia/classroom-updates/stable + test -f ../packages/$package_name + ln -sfn ../packages/$package_name latest.deb.new + mv -Tf latest.deb.new latest.deb +" + +echo "https://updates.riverofglorymn.org/stable/latest.deb -> $package_name" + echo echo "Ekklesia Classroom $version is now STABLE" echo "https://updates.riverofglorymn.org/stable/manifest.json"