#!/bin/sh # Install signed OCI updates for system services (Podman/Docker) or rootless Podman user services. # Rootless: install-updater.sh agent --user --volume V --helper-image I --image-repo R --unit U # System: sudo install-updater.sh agent --engine podman|docker --volume V --helper-image I --image-repo R --unit U set -eu COMPONENT="${1:-}"; shift || true case "$COMPONENT" in agent) ;; *) echo "usage: install-updater.sh [--user] [--engine podman|docker] --data-dir D --image-repo R --unit U [--policy auto|manual] [--channel dev|release] [--health-url URL] [--authfile F] [--no-enable]" >&2; exit 2 ;; esac DATA_DIR=""; IMAGE_REPO=""; UNIT=""; POLICY=manual; AUTHFILE=""; ENGINE=podman; SERVICE_SCOPE=system; ENABLE=true; DATA_VOLUME=""; STATUS_VOLUME=""; HELPER_IMAGE="" CHANNEL=""; HEALTH_URL="" while [ $# -gt 0 ]; do case "$1" in --user) SERVICE_SCOPE=user; shift ;; --engine) ENGINE="$2"; shift 2 ;; --volume) DATA_VOLUME="$2"; shift 2 ;; --status-volume) STATUS_VOLUME="$2"; shift 2 ;; --helper-image) HELPER_IMAGE="$2"; shift 2 ;; --data-dir) DATA_DIR="$2"; shift 2 ;; --image-repo) IMAGE_REPO="$2"; shift 2 ;; --unit) UNIT="$2"; shift 2 ;; --policy) POLICY="$2"; shift 2 ;; --channel) CHANNEL="$2"; shift 2 ;; --health-url) HEALTH_URL="$2"; shift 2 ;; --authfile) AUTHFILE="$2"; shift 2 ;; --no-enable) ENABLE=false; shift ;; *) echo "unknown option: $1" >&2; exit 2 ;; esac done if [ -z "$IMAGE_REPO" ] || [ -z "$UNIT" ] || { [ -z "$DATA_DIR" ] && [ -z "$DATA_VOLUME" ]; }; then echo "--image-repo, --unit and --volume (or legacy --data-dir) are required" >&2; exit 2 fi case "$POLICY" in auto|manual) ;; *) echo "--policy must be auto or manual" >&2; exit 2 ;; esac case "$ENGINE" in podman|docker) ;; *) echo "--engine must be podman or docker" >&2; exit 2 ;; esac if [ -n "$DATA_VOLUME" ]; then [ -z "$DATA_DIR" ] || { echo "use --volume or --data-dir, not both" >&2; exit 2; } [ -n "$HELPER_IMAGE" ] || { echo "--volume needs --helper-image (an already pulled MoShiro image)" >&2; exit 2; } STATUS_VOLUME="${STATUS_VOLUME:-moshiro-$COMPONENT-status}" "$ENGINE" image inspect "$HELPER_IMAGE" >/dev/null "$ENGINE" volume inspect "$DATA_VOLUME" >/dev/null 2>&1 || "$ENGINE" volume create "$DATA_VOLUME" >/dev/null "$ENGINE" volume inspect "$STATUS_VOLUME" >/dev/null 2>&1 || "$ENGINE" volume create "$STATUS_VOLUME" >/dev/null else case "$DATA_DIR" in /*) ;; *) echo "--data-dir must be absolute" >&2; exit 2 ;; esac fi if [ "$SERVICE_SCOPE" = user ]; then [ "$(id -u)" -ne 0 ] || { echo "--user must run as the rootless Podman user, without sudo" >&2; exit 1; } [ "$ENGINE" = podman ] || { echo "--user currently supports rootless Podman; Docker uses system services" >&2; exit 2; } CONF_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/moshiro" STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/moshiro-update" UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user" BIN_DIR="$HOME/.local/bin" TARGET=default.target else [ "$(id -u)" -eq 0 ] || { echo "system installation needs root; use --user for rootless Podman" >&2; exit 1; } CONF_DIR=/etc/moshiro-update STATE_DIR=/var/lib/moshiro-update UNIT_DIR=/etc/systemd/system BIN_DIR=/usr/local/sbin TARGET=multi-user.target fi service_ctl() { if [ "$SERVICE_SCOPE" = user ]; then systemctl --user "$@"; else systemctl "$@"; fi } # Config is sourced by the updater; quote arguments as shell data. shell_quote() { printf "'"; printf '%s' "$1" | sed "s/'/'\"'\"'/g"; printf "'"; } unit_escape() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'; } HERE="$(cd "$(dirname "$0")" && pwd)" install -d -m 755 "$CONF_DIR" "$STATE_DIR" "$UNIT_DIR" "$BIN_DIR" BIN="$BIN_DIR/moshiro-update" install -m 755 "$HERE/moshiro-update.sh" "$BIN" install -m 644 "$HERE/release-key.pub.pem" "$CONF_DIR/release-key.pub.pem" if [ -n "$DATA_VOLUME" ]; then : # Named volumes are initialized by the image; no host directory or chown needed. elif [ "$SERVICE_SCOPE" = user ]; then install -d -m 700 "$DATA_DIR/update" else install -d -m 755 -o 1654 -g 1654 "$DATA_DIR/update" fi if [ -z "$DATA_VOLUME" ]; then install -d -m 755 "$DATA_DIR/update-status"; fi # Rootless service templates mount update-status separately read-only. ENV_FILE="$CONF_DIR/$COMPONENT.image" PUBKEY="$CONF_DIR/release-key.pub.pem" { for name in DATA_DIR IMAGE_REPO UNIT PUBKEY ENV_FILE STATE_DIR POLICY AUTHFILE ENGINE SERVICE_SCOPE DATA_VOLUME STATUS_VOLUME HELPER_IMAGE CHANNEL HEALTH_URL; do case "$name" in DATA_DIR) value="$DATA_DIR" ;; IMAGE_REPO) value="$IMAGE_REPO" ;; UNIT) value="$UNIT" ;; PUBKEY) value="$PUBKEY" ;; ENV_FILE) value="$ENV_FILE" ;; STATE_DIR) value="$STATE_DIR" ;; DATA_VOLUME) value="$DATA_VOLUME" ;; STATUS_VOLUME) value="$STATUS_VOLUME" ;; HELPER_IMAGE) value="$HELPER_IMAGE" ;; POLICY) value="$POLICY" ;; AUTHFILE) value="$AUTHFILE" ;; ENGINE) value="$ENGINE" ;; SERVICE_SCOPE) value="$SERVICE_SCOPE" ;; CHANNEL) value="$CHANNEL" ;; HEALTH_URL) value="$HEALTH_URL" ;; esac printf '%s=' "$name"; shell_quote "$value"; printf '\n' done } > "$CONF_DIR/$COMPONENT.conf" chmod 600 "$CONF_DIR/$COMPONENT.conf" if [ -n "$DATA_VOLUME" ]; then cat > "$UNIT_DIR/moshiro-update-$COMPONENT.service" < "$UNIT_DIR/moshiro-update-$COMPONENT.timer" < "$UNIT_DIR/moshiro-update-$action-$COMPONENT.service" < "$UNIT_DIR/moshiro-update-$action-$COMPONENT.path" </dev/null 2>&1 || true service_ctl enable --now "moshiro-update-$COMPONENT.timer" else service_ctl disable --now "moshiro-update-$COMPONENT.timer" >/dev/null 2>&1 || true service_ctl enable --now "moshiro-update-check-$COMPONENT.path" "moshiro-update-apply-$COMPONENT.path" fi fi echo "updater installed for $COMPONENT (engine: $ENGINE, scope: $SERVICE_SCOPE, policy: $POLICY)" echo "container service must read IMAGE from $ENV_FILE"