#!/usr/bin/env bash
#
# dev-push.sh — hand a freshly built image to a machine already running Alloy,
# without building an ISO.
#
# The ISO is how a machine is installed. It is the wrong artifact for "did my
# sway config land", and using it that way is what makes the edit-to-look-at-it
# loop four minutes long: measured on fw13, a config-only change is 15 seconds
# of `podman build` followed by an ISO assembly that costs sixteen times that
# and produces installer media nobody is going to install from.
#
# An installed machine adopts a locally built image in place. That is not a
# workaround: it is the same A/B staged switch with the same rollback that a
# registry-fed update uses (docs/IMAGE.md, "Updates"), with the image coming
# off a registry on this box instead of one on the internet.
#
# Why a registry and not a file. bootc reads several transports, and the
# obvious ones move the whole image every time: `containers-storage` is not
# reachable from another machine at all, and an archive is 3 GB down the wire
# per rebuild. A registry stores layers, so the target pulls only the ones it
# does not have, and a config-only rebuild changes the tail of the image.
#
# The push itself is NOT incremental, and measuring it is the only way anyone
# would know. skopeo re-copies all 125 blobs on every run — 22s, twice in a
# row, with no "Skipping fetch of repeat blob" — because a containers-storage
# source carries no compression metadata, so each layer has to be recompressed
# before its digest is even known. 22s is the fixed price of a push here. What
# the registry buys is the wire to the target, not this end.
#
# The registry is a dev tool that runs on this box and is torn down with
# `--stop`; nothing about it is the distribution story, which stays "no image
# is published anywhere" (docs/IMAGE.md, "Registry").
#
# Usage:
#   build/dev-push.sh                  # push localhost/alloy:local, print the
#                                      # line to run on the target
#   build/dev-push.sh --address HOST   # print that line for a given address
#   build/dev-push.sh --port 5000      # a different port
#   build/dev-push.sh --stop           # stop the registry and forget its blobs
#
# On the target, the first time:
#
#   printf '[[registry]]\nlocation = "ADDR"\ninsecure = true\n' \
#     | sudo tee /etc/containers/registries.conf.d/99-alloy-dev.conf
#   sudo bootc switch --transport registry ADDR/alloy:local
#   sudo systemctl reboot
#
# and after that, for every later push, `sudo bootc upgrade && sudo systemctl
# reboot`: the machine remembers where it was switched to.
#
# The insecure line is what plain HTTP costs. It is scoped to one address in a
# drop-in file, it is a development machine talking to a registry on the same
# desk, and `build/vmtest` is the intended target. Do not carry it onto a
# machine that matters.
#
# The registry itself binds every interface, because a VM reaches it at
# 10.0.2.2 and a real target reaches it by tailnet name, and it takes no
# authentication: while it is up, anyone who can reach this box can read the
# image and push one. That is the same trust boundary as the tailnet and it is
# still a reason to run it only while a push is in flight. `--stop` is one
# command and the next push starts it again.

set -euo pipefail

REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"

# priv / privc. run0 where it exists, sudo where it does not; see the header
# of build/privilege.sh for which of the two a call site wants.
# shellcheck source=build/privilege.sh
. "$REPO_ROOT/build/privilege.sh"

IMAGE="localhost/alloy:local"
REGISTRY_IMAGE="docker.io/library/registry:2"
CONTAINER="alloy-dev-registry"
# A named volume rather than a tmpfs, so the blobs survive a restart of the
# registry. What that protects is the target's next pull: with the layers still
# here, a machine that already fetched them fetches only what changed.
VOLUME="alloy-dev-registry"
PORT=5000
ADDRESS=""
STOP=0

die() { printf 'error: %s\n' "$*" >&2; exit 1; }
say() { printf '==> %s\n' "$*"; }

while [ $# -gt 0 ]; do
  case "$1" in
    --address) ADDRESS="${2:?--address needs a host}"; shift 2 ;;
    --port)    PORT="${2:?--port needs a number}"; shift 2 ;;
    --stop)    STOP=1; shift ;;
    # The header block is the help text, so it stops where the comments stop.
    -h|--help) awk 'NR==1 {next} !/^#/ {exit} {sub(/^# ?/, ""); print}' "${BASH_SOURCE[0]}"; exit 0 ;;
    *)         die "unknown argument: $1 (see --help)" ;;
  esac
done

command -v podman >/dev/null || die "podman not found"

if [ "$STOP" -eq 1 ]; then
  say "stopping $CONTAINER"
  privc podman rm -f "$CONTAINER" >/dev/null 2>&1 || true
  privc podman volume rm "$VOLUME" >/dev/null 2>&1 || true
  say "stopped. The next target to switch to it pulls the whole image again."
  exit 0
fi

privc podman image exists "$IMAGE" \
  || die "$IMAGE is not in the root store; build it first with build/build-iso.sh --skip-source or podman build"

# The registry, started if it is not already up. Rootful, because the image it
# serves lives in the root store and this script pushes from there.
if [ "$(privc podman inspect -f '{{.State.Running}}' "$CONTAINER" 2>/dev/null || echo false)" != "true" ]; then
  privc podman rm -f "$CONTAINER" >/dev/null 2>&1 || true
  say "starting $CONTAINER on :$PORT"
  priv podman run -d --name "$CONTAINER" \
    -p "$PORT:5000" \
    -v "$VOLUME:/var/lib/registry" \
    "$REGISTRY_IMAGE" >/dev/null
else
  say "$CONTAINER is up"
fi

# skopeo out of the Alloy image itself. The base carries it, so this needs no
# skopeo on the host — fw13 is Pop!_OS and has none — and no second image to
# keep current. --network host so 127.0.0.1 means this box rather than the
# skopeo container's own loopback, which is the failure that looks like the
# registry being down.
say "pushing $IMAGE (about 20s; skopeo recompresses every layer, see the header)"
priv podman run --rm --privileged --network host \
  --security-opt label=type:unconfined_t \
  -v /var/lib/containers/storage:/var/lib/containers/storage \
  --entrypoint skopeo \
  "$IMAGE" \
  copy --dest-tls-verify=false \
  "containers-storage:$IMAGE" "docker://127.0.0.1:$PORT/alloy:local"

# What to type on the target. Printed rather than run over ssh: the target is a
# machine that is about to be told to boot something else, and the script that
# builds an image should not also be the thing that reboots your laptop.
if [ -z "$ADDRESS" ]; then
  echo
  echo "    Reachable as, depending on what the target is:"
  echo "      10.0.2.2:$PORT       a qemu guest on user-mode networking (build/vmtest)"
  echo "      <this host>:$PORT    anything else, by tailnet name or LAN address"
  ADDRESS="<host>:$PORT"
else
  ADDRESS="$ADDRESS:$PORT"
fi

cat <<EOF

    On the target, once:
      printf '[[registry]]\\nlocation = "$ADDRESS"\\ninsecure = true\\n' \\
        | sudo tee /etc/containers/registries.conf.d/99-alloy-dev.conf
      sudo bootc switch --transport registry $ADDRESS/alloy:local
      sudo systemctl reboot

    And for every push after that:
      sudo bootc upgrade && sudo systemctl reboot

    Rolling back is bootc's own: sudo bootc rollback && sudo systemctl reboot
EOF
