#!/bin/sh
# Torwarden agent installer. POSIX sh for the same reason as the server one.
set -eu

BASE="${TORWARDEN_RELEASE_BASE:-https://releases.torwarden.com}"
CHANNEL="${TORWARDEN_CHANNEL:-latest}"
URL=""
KEY=""
# Read from the environment by default: a shared secret in this script's own
# arguments lands in shell history and stays readable by every local account,
# through /proc, for as long as the installer runs.
#
# The environment is better but not perfect — `sudo VAR=value ...` puts the
# value in sudo's argument list, so the safest form is to export it first and
# let sudo pass it through, or to feed it from a secret store. The token never
# reaches curl's arguments either way; see the registration call below.
TOKEN="${TORWARDEN_ENROLMENT_TOKEN:-}"
HOSTNAME_ARG=""
INTERVAL="30s"
RESTART=1
CONF_DIR="/etc/torwarden"
BIN="/usr/local/bin/torwarden-agent"

die() { echo "install-agent: $*" >&2; exit 1; }

usage() {
    cat <<EOF
Usage: install-agent.sh --url URL --key KEY [options]
       install-agent.sh --url URL --token TOKEN [options]

  --url URL        the Torwarden server this agent reports to
  --key KEY        the API key the dashboard gave you when you added the host
  --token TOKEN    the enrolment token, if this server has auto-registration
                   on. The host registers itself and no one has to add it in
                   the dashboard first, so the same command works on every
                   machine. Prefer TORWARDEN_ENROLMENT_TOKEN in the
                   environment: an argument here is readable by every local
                   account for as long as this script runs.
  --hostname NAME  how this host appears (default: this machine's hostname)
  --interval DUR   collection interval, with a unit (default 30s)
  --version REL    install a specific release (e.g. v1.4.0) instead of latest
  --no-restart     replace the binary but leave the running agent alone; the new
                   binary does not take effect until you restart it yourself

Run it with no --url/--key on a host that is already enrolled to upgrade the
binary in place, keeping the existing agent.conf.
  --help           this text

Set TORWARDEN_RELEASE_BASE to install from an internal mirror instead of
$BASE — required for hosts with no internet access.
EOF
}

while [ $# -gt 0 ]; do
    case "$1" in
        --url)      URL="${2:-}"; shift 2 ;;
        --key)      KEY="${2:-}"; shift 2 ;;
        --token)    TOKEN="${2:-}"; shift 2 ;;
        --hostname) HOSTNAME_ARG="${2:-}"; shift 2 ;;
        --interval) INTERVAL="${2:-}"; shift 2 ;;
        --no-restart) RESTART=0; shift ;;
        --version)   CHANNEL="${2:-}"; [ -n "$CHANNEL" ] || die "--version needs a release, e.g. v1.4.0"; shift 2 ;;
        --help|-h)  usage; exit 0 ;;
        *)          die "unknown option $1 (try --help)" ;;
    esac
done

[ "$(id -u)" = "0" ] || die "must run as root"

# On an upgrade the host is already enrolled and its config already holds the
# API key. Demanding --key again would mean looking up a per-host credential
# for every machine just to install a new binary, which on a real estate means
# nobody upgrades. Keep what is there unless asked to change it.
KEEP_CONF=0
if [ -f "$CONF_DIR/agent.conf" ] && [ -z "$URL" ] && [ -z "$KEY" ] && [ -z "$TOKEN" ]; then
    KEEP_CONF=1
fi
if [ "$KEEP_CONF" = "0" ]; then
    [ -n "$URL" ] || { usage >&2; die "--url is required (or omit --url, --key and --token to keep the existing config)"; }
    if [ -n "$KEY" ] && [ -n "$TOKEN" ]; then
        die "--key and --token are two ways to do the same thing; pass one"
    fi
    [ -n "$KEY" ] || [ -n "$TOKEN" ] || { usage >&2; die "--key or --token is required (or omit both to keep the existing config)"; }
fi
# Only needed when a config is being written. On the upgrade path the hostname
# is already in agent.conf, and computing one we will not use is how a re-run
# died with 127 on a minimal image: `hostname` is a separate package on RHEL and
# is absent from UBI and hardened builds, so the agent silently stayed on the old
# version. uname is in coreutils and is always there.
if [ "$KEEP_CONF" = "0" ] && [ -z "$HOSTNAME_ARG" ]; then
    HOSTNAME_ARG="$(hostname -f 2>/dev/null || hostname 2>/dev/null || uname -n)"
fi

# The interval is a Go duration and needs its unit. Catching it here beats a
# service that fails to start with 'missing unit in duration "30"'.
if [ "$KEEP_CONF" = "0" ]; then
    case "$INTERVAL" in
        *[0-9]s|*[0-9]m|*[0-9]h|*[0-9]ms) ;;
        *) die "--interval needs a unit, e.g. 30s or 1m (got \"$INTERVAL\")" ;;
    esac
fi

case "$(uname -m)" in
    x86_64|amd64)  ARCH=amd64 ;;
    aarch64|arm64) ARCH=arm64 ;;
    *) die "unsupported architecture $(uname -m); Torwarden ships amd64 and arm64" ;;
esac

command -v curl >/dev/null 2>&1 || die "curl is required"

TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT

echo "Downloading torwarden-agent ($ARCH) from $BASE/$CHANNEL ..."
curl -fsSL "$BASE/$CHANNEL/torwarden-agent-linux-$ARCH" -o "$TMP/torwarden-agent" \
    || die "download failed. If this host has no internet, set TORWARDEN_RELEASE_BASE to your mirror."

curl -fsSL "$BASE/$CHANNEL/SHA256SUMS" -o "$TMP/SHA256SUMS" \
    || die "could not fetch SHA256SUMS from $BASE/$CHANNEL"
WANT="$(grep " torwarden-agent-linux-$ARCH\$" "$TMP/SHA256SUMS" | awk '{print $1}')"
[ -n "$WANT" ] || die "SHA256SUMS has no entry for torwarden-agent-linux-$ARCH"
GOT="$(sha256sum "$TMP/torwarden-agent" | awk '{print $1}')"
[ "$WANT" = "$GOT" ] || die "checksum mismatch: expected $WANT, got $GOT"
echo "Checksum verified."

# Nothing to do if this is already the installed binary. Config
# management runs this on a schedule, and an installer that restarts the
# service every single run is one nobody is allowed to schedule.
#
# KEEP_CONF is part of the condition because the binary being current does not
# mean the install is complete: with --url and --key supplied the caller is
# asking for a config to be written, and a host whose agent.conf is missing or
# being replaced still needs one. Testing the binary alone exited 0 having
# written nothing and left the service stopped.
if [ "$KEEP_CONF" = "1" ] && [ -x "$BIN" ] && [ "$GOT" = "$(sha256sum "$BIN" | awk '{print $1}')" ]; then
    # The binary matching is not the same as the install being current. The
    # unit gained StateDirectory in 0.4.14, and a host can hold a new binary
    # with an old unit — the fetch below it can fail on its own, or the binary
    # can arrive by other means. That host has nowhere to write its spool, and
    # the agent tells its operator to re-run this script: exiting here made
    # that advice a loop with no way out of it.
    #
    # Compared with sha256sum rather than cmp: cmp lives in diffutils, which is
    # absent from a minimal Rocky or UBI image. A missing cmp exits 127, which
    # `! cmp` reads as "they differ" — so on exactly the hosts this product
    # targets, every re-run rewrote the unit and restarted the agent, and said
    # the definition had been out of date when it had not. sha256sum is already
    # what verifies the download, so it is present by definition.
    UNIT=/etc/systemd/system/torwarden-agent.service
    if curl -fsSL "$BASE/$CHANNEL/torwarden-agent.service" -o "$TMP/agent.service" 2>/dev/null &&
       [ "$(sha256sum < "$TMP/agent.service" | awk '{print $1}')" != \
         "$(sha256sum < "$UNIT" 2>/dev/null | awk '{print $1}')" ]; then
        cp "$TMP/agent.service" "$UNIT"
        systemctl daemon-reload
        [ "$RESTART" = "1" ] && systemctl restart torwarden-agent
        echo "The agent is already at this version. Its service definition was out of date and has been updated."
        exit 0
    fi
    echo "The agent is already at this version; nothing to do."
    exit 0
fi

# Register, if that is how this host is getting its key.
#
# After the checksum and before anything on this machine is replaced: a
# registration that succeeds against a server we then fail to install for
# leaves a host row with no agent, which is visible in the dashboard as a host
# that never reported. The reverse order leaves a stopped service and a
# half-written config, which is not.
if [ -n "$TOKEN" ]; then
    echo "Registering $HOSTNAME_ARG with $URL ..."
    # The token goes to curl on stdin, not in its argument list. Arguments are
    # world-readable through /proc/<pid>/cmdline for as long as the process
    # lives, so `-H "X-Enrolment-Token: $TOKEN"` hands the fleet-wide secret to
    # every local account on the machine — and this is the one credential that
    # can re-key an existing host and take over its identity.
    #
    # A config file on stdin is read by curl before it starts, never appears in
    # argv, and needs no temporary file to clean up.
    CODE="$(printf 'header = "X-Enrolment-Token: %s"\n' "$TOKEN" \
        | curl -sS -K - -o "$TMP/register.json" -w '%{http_code}' \
            -X POST "${URL%/}/api/v1/agents/register" \
            -H "Content-Type: application/json" \
            -d "{\"hostname\":\"$HOSTNAME_ARG\"}")" \
        || die "could not reach $URL to register. Check --url, DNS, and that this host trusts the server's certificate."
    if [ "$CODE" != "200" ] && [ "$CODE" != "201" ]; then
        # Not "registration failed with $CODE". The server's refusal names the
        # actual reason — a licence with no free seat says which limit was hit
        # (SPEC §6.2.4) — and swallowing it is what sends someone to read
        # server logs for something they were already told.
        die "the server refused this registration ($CODE): $(sed -n 's/.*"error":"\([^"]*\)".*/\1/p' "$TMP/register.json" | head -1)"
    fi
    KEY="$(sed -n 's/.*"api_key":"\([^"]*\)".*/\1/p' "$TMP/register.json" | head -1)"
    [ -n "$KEY" ] || die "the server accepted the registration but returned no API key"
    if grep -q '"existing":true' "$TMP/register.json" 2>/dev/null; then
        echo "This hostname was already enrolled; it has been given a new key and kept its history."
    fi
    if grep -q '"monitoring_enabled":false' "$TMP/register.json" 2>/dev/null; then
        echo "Registered, but monitoring is off for this host: auto-approve is disabled on"
        echo "the server, so someone has to enable it before its data is kept."
    fi
fi

# Same trap as the server installer: "systemctl enable --now" does not restart
# a unit that is already running, so an upgrade would replace the file and
# leave the old agent shipping metrics.
UPGRADE=0
OLD_VERSION=""
WAS_ACTIVE=0
if [ -x "$BIN" ]; then
    UPGRADE=1
    OLD_VERSION="$("$BIN" --version 2>/dev/null | head -1 || echo unknown)"
    systemctl is-active --quiet torwarden-agent 2>/dev/null && WAS_ACTIVE=1
    cp -p "$BIN" "$BIN.prev" 2>/dev/null || true
fi
if [ "$UPGRADE" = "1" ] && [ "$WAS_ACTIVE" = "1" ] && [ "$RESTART" = "1" ]; then
    systemctl stop torwarden-agent
fi

install -o root -g root -m 0755 "$TMP/torwarden-agent" "$BIN"
mkdir -p "$CONF_DIR"

if [ "$KEEP_CONF" = "1" ]; then
    echo "Keeping existing $CONF_DIR/agent.conf"
    URL="$(sed -n 's/^collector_url *= *//p' "$CONF_DIR/agent.conf" | head -1)"
else
    cat > "$CONF_DIR/agent.conf" <<EOF
collector_url = $URL
api_key       = $KEY
hostname      = $HOSTNAME_ARG
interval      = $INTERVAL
EOF
    # The API key is a credential for this host's ingest. 0600, root-owned.
    chown root:root "$CONF_DIR/agent.conf"
    chmod 600 "$CONF_DIR/agent.conf"
fi

curl -fsSL "$BASE/$CHANNEL/torwarden-agent.service" -o /etc/systemd/system/torwarden-agent.service \
    || die "could not fetch the systemd unit"

command -v restorecon >/dev/null 2>&1 && restorecon -F "$BIN" 2>/dev/null || true

systemctl daemon-reload

if [ "$RESTART" = "0" ]; then
    systemctl enable torwarden-agent >/dev/null 2>&1 || true
    echo
    echo "Binary replaced. The running agent is still the old one — restart when ready:"
    echo "    sudo systemctl restart torwarden-agent"
    exit 0
fi

systemctl enable torwarden-agent >/dev/null 2>&1 || true
systemctl restart torwarden-agent

i=0
while [ "$i" -lt 15 ]; do
    if systemctl is-active --quiet torwarden-agent; then break; fi
    i=$((i + 1))
    sleep 1
done
if ! systemctl is-active --quiet torwarden-agent; then
    if [ "$UPGRADE" = "1" ] && [ -x "$BIN.prev" ]; then
        echo "The new agent did not start. Rolling back." >&2
        mv -f "$BIN.prev" "$BIN"
        command -v restorecon >/dev/null 2>&1 && restorecon -F "$BIN" 2>/dev/null || true
        systemctl start torwarden-agent || true
        die "upgrade failed and was rolled back; see: journalctl -u torwarden-agent -n 50"
    fi
    die "the agent did not start; see: journalctl -u torwarden-agent -n 50"
fi
rm -f "$BIN.prev"

echo
if [ "$UPGRADE" = "1" ]; then
    echo "Agent upgraded and restarted."
    echo "  was: $OLD_VERSION"
    echo "  now: $("$BIN" --version 2>/dev/null | head -1 || echo unknown)"
else
    echo "Torwarden agent installed, reporting to $URL as $HOSTNAME_ARG every $INTERVAL."
fi
echo "Confirm it is shipping:  journalctl -u torwarden-agent -f"
