#!/bin/sh
# Run as root by cloudups-update.service when the agent drops
# /etc/cloudups/update-request (UPDATE_AGENT from the cloud). Upgrades the
# CloudUPS packages from the configured BuzNet repository and nothing else:
# the request's content is not read. The result is left for the agent that
# starts afterwards, which acks the command with it.
#
# The result lives under /var/lib/cloudups-update (root:root, created by the
# unit's StateDirectory=), NOT the agent's own log directory (owned by the
# unprivileged cloudups user), and its ownership is never changed after the
# fact. Writing into a directory the agent can write to, then re-assigning
# the result to that account, is a privilege-escalation hole: a compromised
# agent pre-creates the result path as a symlink to a root-owned file
# (/etc/shadow, /etc/sudoers.d/x, ...) and drops the request; root would
# follow the link, and re-assigning ownership afterwards would hand the
# target to the agent's account. StateDirectory= keeps root the only writer
# to the directory the result lands in, so nothing this script does writes
# through a path the agent controls.
set -u
REQ=/etc/cloudups/update-request
RESULT=/var/lib/cloudups-update/result
# Consume the request first: a failure below must not retrigger the path
# unit. `rm -f` on a symlink removes the link itself, never its target.
rm -f "$REQ"

installed() {
  if command -v dpkg-query >/dev/null 2>&1; then
    dpkg-query -W -f='${Status}' "$1" 2>/dev/null | grep -q "install ok installed"
  else
    rpm -q "$1" >/dev/null 2>&1
  fi
}
pkgs=""
for p in cloudups-agent cloudups-desktop; do installed "$p" && pkgs="$pkgs $p"; done

run() {
  if [ -f /etc/apt/sources.list.d/buznet.list ]; then
    # Refresh only the BuzNet list (Dir::Etc::sourcelist/-sourceparts), not
    # the customer's whole apt configuration: a third-party repo that is
    # broken or offline must not be able to fail OUR self-update.
    # APT::Get::List-Cleanup=0 stops that narrowed `update` from deleting the
    # cached index files of every OTHER source, which it would otherwise read
    # as "no longer in the sources it was given". DPkg::Lock::Timeout waits
    # out a lock held by something like unattended-upgrades instead of
    # failing immediately.
    apt-get update -o Dpkg::Use-Pty=0 \
        -o Dir::Etc::sourcelist=sources.list.d/buznet.list \
        -o Dir::Etc::sourceparts=- \
        -o APT::Get::List-Cleanup=0 &&
      DEBIAN_FRONTEND=noninteractive apt-get install --only-upgrade -y \
        -o Dpkg::Options::=--force-confold \
        -o DPkg::Lock::Timeout=300 $pkgs
  elif [ -f /etc/yum.repos.d/buznet.repo ]; then
    # dnf's metadata cache is valid for 48h by default, and a cache that
    # predates the release would report "Nothing to do" instead of installing
    # it. So the BuzNet metadata (repo id `buznet`, client/buznet.repo) is
    # refreshed first, and only those, as with the narrowed apt-get update
    # above. The upgrade itself then runs with every repository enabled: a
    # release that gains a dependency from the base repositories must still
    # resolve, which --disablerepo='*' would prevent. `--repo` and
    # `makecache --refresh` are spelled the same in dnf 4 (EL9) and dnf 5.
    # -y on the refresh too: buznet.repo has repo_gpgcheck=1, and when the
    # repository key has to be (re)imported dnf asks, which nobody answers
    # here. skip_if_unavailable on the upgrade: it reads every enabled
    # repository, and a third party's broken one must not fail our update
    # (BuzNet's metadata was refreshed just above, so ours is not skipped).
    dnf -y makecache --refresh --repo=buznet &&
      dnf -y --setopt=skip_if_unavailable=True upgrade $pkgs
  else
    echo "BuzNet repository not configured (no buznet.list / buznet.repo)"
    return 3
  fi
}
run > "$RESULT.log" 2>&1
rc=$?
{ echo "exit=$rc"; tail -n 20 "$RESULT.log"; } > "$RESULT"
exit 0
