#!/bin/bash -e
# converge the system state the instance description declares
#
# Position: after 09hostname, which sets the hostname and rewrites
# /etc/hosts with a sed over the old name, so an entry written before it
# would be edited or lost; and before the hooks at 15, 29 and 30, which
# regenerate the certificate and raise the init fence on the name this hook
# settles. The conf phase is 00declarative's, at position 00, and has
# already run by the time this hook does.
#
# Shipped by the keel package and not by inithooks: the package that owns
# the keel command owns the hook that runs it, so inithooks keeps no
# dependency on keel, and an image without keel has no hook here.
#
# Asks for the system phase alone (keel spec apply --system-only), with
# --defer-certificate: DNS rarely points at a machine that is still booting,
# so tls.acme is written and its certificate left to the next apply. Running
# the conf phase a second time in one boot would resolve the secrets again,
# and a secret declared generate: true would get a fresh value after
# 30rootpass had already set and shown the first one, leaving the appliance
# with a password nothing knows.
#
# And with --skip-network: 01ipconfig has already written the interfaces
# file from the same description, and a network change on a running machine
# waits for a confirmation nobody is there to give at first boot, so it would
# only revert (decision 0018).
#
# The output of apply is captured below and logged when it is over. A
# declared MariaDB server that is still starting is waited for, up to
# 300 s, and apply says so itself on the terminal (/dev/tty), outside
# this capture, so tty1 is not blank meanwhile (docs/apply.md).
#
# No failure here aborts the boot: an appliance that cannot converge a
# field must still finish booting, and say what it could not do.

INITHOOKS_DEFAULT="${INITHOOKS_DEFAULT:-/etc/default/inithooks}"
if [[ -e "$INITHOOKS_DEFAULT" ]]; then
    # shellcheck source=default/inithooks
    source "$INITHOOKS_DEFAULT"
fi
INITHOOKS_LOGFILE="${INITHOOKS_LOGFILE:-/var/log/inithooks.log}"

# Where the description is looked for, in order, when INITHOOKS_DECL does
# not name one outright: the same two paths, in the same order, that
# inithooks 00declarative searches. This hook cannot ask that hook, because
# keel does not depend on inithooks, so the list is repeated here and
# tested here; a colon separated override keeps it out of the way of an
# image that keeps its description elsewhere.
KEEL_SPEC_PATHS="${KEEL_SPEC_PATHS:-/etc/keel/instance.yaml:/etc/inithooks.yaml}"
KEEL="${KEEL:-keel}"

_hook=$(basename "$0")

log() {
    local level=$1
    shift
    logger -t inithooks -p "$level" "[$_hook] $*" || true
    if [[ -f "$INITHOOKS_LOGFILE" ]]; then
        echo "${level^^}: [$_hook] $*" >> "$INITHOOKS_LOGFILE"
    fi
}

info() { log info "$*"; }
warn() { log warning "$*"; }

# turnkey-init is an explicit interactive run: the operator is answering
# the dialogs, and a description must not overrule the answers
if [[ -n "$_TURNKEY_INIT" ]]; then
    exit 0
fi

spec=
if [[ -n "$INITHOOKS_DECL" ]]; then
    spec="$INITHOOKS_DECL"
else
    IFS=: read -r -a candidates <<< "$KEEL_SPEC_PATHS"
    for candidate in "${candidates[@]}"; do
        if [[ -e "$candidate" ]]; then
            spec="$candidate"
            break
        fi
    done
fi

if [[ -z "$spec" ]] || [[ ! -e "$spec" ]]; then
    info "no instance description (searched ${INITHOOKS_DECL:-$KEEL_SPEC_PATHS}), nothing to converge"
    exit 0
fi

if ! command -v "$KEEL" >/dev/null; then
    warn "$KEEL is not installed, $spec was not converged"
    exit 0
fi

status=0
output=$("$KEEL" spec apply --system-only --defer-certificate --skip-network --spec "$spec" 2>&1) || status=$?

while IFS= read -r line; do
    [[ -n "$line" ]] || continue
    if [[ "$status" -eq 0 ]]; then
        info "$line"
    else
        warn "$line"
    fi
done <<< "$output"

# 3 is keel's SPEC_INVALID: the spec itself was refused. Keep that where keel
# diff can show it, with one alert, and a later apply clears it (keel#268).
if [[ "$status" -eq 3 ]]; then
    printf '%s\n' "$output" | "$KEEL" spec record-refusal --spec "$spec" \
        || warn "could not record the refusal of $spec"
fi

if [[ "$status" -ne 0 ]]; then
    warn "keel spec apply --system-only exited $status:" \
         "$spec is not fully converged, the boot carries on"
fi

exit 0
