#!/bin/bash
# =============================================================================
# Pull the seed repository.
#
#   muli-db-seed-pull [--check]
#
# Clones on first run, fetches thereafter, into $MULI_SEED_ROOT. Read-only by
# construction: the deploy key this runs under has read access to the Gitea
# repository and nothing more, so a compromised contract container cannot
# publish data to any other container.
#
# Configuration lives in /etc/muli/seed.conf:
#
#   SEED_REMOTE=gitea@gitea.mercuran.com.au:data_controls/muli_data_seeding.git
#   SEED_BRANCH=master
#   SEED_SSH_KEY=/usr/local/muli/etc/seed_deploy_key
#
# All three are required. The key is per-contract and attached to the
# contract's own read-only Gitea user; this script never creates or repairs it,
# and fails with an explanation if it is missing, unreadable, wrongly
# permissioned or rejected - followed by the walkthrough for authorising this
# container, with its own contract, host and paths filled in.
#
# Pulling and applying are separate on purpose. A fetch that brings down a bad
# commit changes nothing until muli-db-seed runs, and `--check` reports what a
# pull would bring without touching the working tree.
# =============================================================================
set -euo pipefail

CONF="${MULI_SEED_CONF:-/etc/muli/seed.conf}"
SEED_ROOT="${MULI_SEED_ROOT:-/usr/local/muli/dbmaster}"
SEED_BRANCH="${SEED_BRANCH:-master}"
CHECK_ONLY=false
[ "${1:-}" = "--check" ] && CHECK_ONLY=true

log() { echo "[muli-db-seed-pull] $*"; }
die() { echo "[muli-db-seed-pull] ERROR: $*" >&2; exit 1; }

# shellcheck source=/dev/null
[ -f "$CONF" ] && . "$CONF"

# The deploy key is required, not optional. Every failure below is reported
# here rather than being left to git, because git's own message for all of them
# is "Permission denied (publickey)" - which says nothing about which of the
# four possible causes it was.
#
# The walkthrough is a template shipped beside the migrations, shared with
# muli-db-converge so the two cannot give different instructions. The contract
# comes from the site's common database name, cdb<contract>.
GUIDE="${MULI_DB_ROOT:-/usr/local/muli/database}/seed/authorise-feed.txt"
provision_hint() {
    local contract host remote
    remote="${SEED_REMOTE:-$(sed -n 's/^SEED_REMOTE=//p' "$(dirname "$GUIDE")/seed.conf.example" 2>/dev/null | head -1 || true)}"
    contract=$(sed -n 's/^cdb//p' /usr/local/mulidata/site/common_database 2>/dev/null | head -1 || true)
    contract="${contract:-<contract>}"
    host=$(hostname)    # the short name, as muli-db-converge prints it
    echo >&2
    echo "    This container has no working deploy key for the seed repository." >&2
    echo "    Until it has, master data cannot be pulled. Data already pulled is" >&2
    echo "    untouched, and muli-db-seed still applies it." >&2
    echo >&2
    if [ -f "$GUIDE" ]; then
        sed -e "s|{{CONTRACT}}|${contract}|g" -e "s|{{GITEA_USER}}|c${contract}|g" \
            -e "s|{{HOST}}|${host}|g" -e "s|{{REMOTE}}|${remote:-<feed>}|g" \
            -e "s|{{KEY}}|${SEED_SSH_KEY:-/usr/local/muli/etc/seed_deploy_key}|g" \
            -e "s|{{CONF}}|${CONF}|g" "$GUIDE" >&2
    else
        echo "    See Master Data Seeding, section 6, for how to authorise it." >&2
    fi
}

# Error first, then the hint - the reason is what a reader needs before the
# remedy, and a hint printed above the error reads as though it were the error.
die_unprovisioned() {
    echo "[muli-db-seed-pull] ERROR: $*" >&2
    provision_hint
    exit 1
}

[ -n "${SEED_REMOTE:-}" ] || die_unprovisioned "SEED_REMOTE is not set (looked in ${CONF})"

if [ -z "${SEED_SSH_KEY:-}" ]; then
    die_unprovisioned "SEED_SSH_KEY is not set (looked in ${CONF})"
fi

if [ ! -f "$SEED_SSH_KEY" ]; then
    die_unprovisioned "deploy key not found at ${SEED_SSH_KEY}"
fi

if [ ! -r "$SEED_SSH_KEY" ]; then
    die "deploy key ${SEED_SSH_KEY} is not readable by $(id -un); it should be owned by the user this runs as, mode 0600"
fi

# ssh refuses a private key whose permissions let anyone else read it, and the
# refusal looks identical to having no key at all. Catch it here where the
# cause can be named.
key_mode=$(stat -c '%a' "$SEED_SSH_KEY" 2>/dev/null || stat -f '%Lp' "$SEED_SSH_KEY" 2>/dev/null || echo "")
case "$key_mode" in
    600|400|"") ;;
    *) die "deploy key ${SEED_SSH_KEY} has mode ${key_mode}; ssh ignores keys readable by others. Run: chmod 600 ${SEED_SSH_KEY}" ;;
esac

# Offer the deploy key and nothing else. All three options are load-bearing, and
# the read-only guarantee this whole design rests on depends on them:
#
#   -F /dev/null        ignore the invoking user's ~/.ssh/config. IdentitiesOnly
#                       does NOT exclude IdentityFile entries found there - it
#                       only excludes the agent and the built-in defaults. Tested
#                       against the real server: with an unregistered deploy key
#                       and an operator's ~/.ssh/config present, ssh offered the
#                       operator's key and authenticated as them. That key has
#                       write access to the seed repository.
#   IdentitiesOnly=yes  do not add the default ~/.ssh/id_* keys.
#   IdentityAgent=none  do not offer agent identities.
#
# Verified after adding -F /dev/null: an unregistered key is the only one
# offered, and the server correctly denies it.
export GIT_SSH_COMMAND="ssh -F /dev/null -i ${SEED_SSH_KEY} -o IdentitiesOnly=yes -o IdentityAgent=none -o StrictHostKeyChecking=accept-new"

# The key can be present and well-formed and still be rejected - the Gitea user
# revoked, the key rotated, or the account removed from the read-only team. Only
# the server knows, so run git and explain a failure rather than pre-checking.
git_authenticated() {
    if ! "$@"; then
        echo "[muli-db-seed-pull] ERROR: git could not reach ${SEED_REMOTE} using ${SEED_SSH_KEY}." >&2
        echo "[muli-db-seed-pull] The key exists, so it was rejected rather than missing: it may have" >&2
        echo "[muli-db-seed-pull] been rotated, or the Gitea user removed from the read-only team." >&2
        provision_hint
        exit 1
    fi
}

if [ ! -d "${SEED_ROOT}/.git" ]; then
    # Ask the server even though there is nothing to compare against yet. A
    # --check that only reported "not cloned" passed an unregistered key on
    # every fresh container - verified on staging - which is exactly when
    # someone is checking whether authorising it worked.
    if [ "$CHECK_ONLY" = true ]; then
        git_authenticated git ls-remote --exit-code --heads "$SEED_REMOTE" "$SEED_BRANCH" >/dev/null
        log "not cloned yet; the key is accepted, and a pull would clone ${SEED_REMOTE}"
        exit 0
    fi
    log "cloning ${SEED_REMOTE} into ${SEED_ROOT}"
    mkdir -p "$(dirname "$SEED_ROOT")"
    git_authenticated git clone --depth 1 --branch "$SEED_BRANCH" "$SEED_REMOTE" "$SEED_ROOT"
else
    git_authenticated git -C "$SEED_ROOT" fetch --depth 1 origin "$SEED_BRANCH" --quiet
    local_rev=$(git -C "$SEED_ROOT" rev-parse HEAD)
    remote_rev=$(git -C "$SEED_ROOT" rev-parse FETCH_HEAD)
    if [ "$local_rev" = "$remote_rev" ]; then
        log "already at $(git -C "$SEED_ROOT" rev-parse --short HEAD)"
        exit 0
    fi
    if [ "$CHECK_ONLY" = true ]; then
        log "update available: $(echo "$local_rev" | cut -c1-7) -> $(echo "$remote_rev" | cut -c1-7)"
        git -C "$SEED_ROOT" log --oneline "${local_rev}..${remote_rev}" | sed 's/^/    /'
        exit 0
    fi
    log "updating $(echo "$local_rev" | cut -c1-7) -> $(echo "$remote_rev" | cut -c1-7)"
    # Hard reset rather than merge: this is a read-only mirror of the feed, and
    # local divergence would mean someone edited data that should be a commit.
    git -C "$SEED_ROOT" reset --hard FETCH_HEAD --quiet
fi

log "at $(git -C "$SEED_ROOT" rev-parse --short HEAD); run muli-db-seed to apply"
