#!/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, issued by Gitea and installed
# by provision_seed_access.py; this script never creates or repairs it, and
# fails with an explanation if it is missing, unreadable, wrongly permissioned
# or rejected.
#
# 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"
[ -n "${SEED_REMOTE:-}" ] || die "SEED_REMOTE is not set (looked in ${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.
provision_hint() {
    cat >&2 <<HINT

    This container has no working deploy key for the seed repository.
    Provision one from an operator machine:

        cd CommandAndControl/gitea_management/muli_contract_users
        python3 provision_seed_access.py <contract> --host <container>

    That creates the Gitea user, generates its keypair, installs the private key
    here, and writes the configuration at:
        ${CONF}

    Until then master data cannot be pulled. Data already pulled is untouched,
    and muli-db-seed still applies it.
HINT
}

# 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
}

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
    [ "$CHECK_ONLY" = true ] && { log "not cloned yet; a pull would clone ${SEED_REMOTE}"; exit 0; }
    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"
