#!/bin/bash
# Move a Stork agent's saved registration from the 2.4 names to the 2.6 names.
#
# Stork 2.6.0 reads these files directly from /var/lib/stork-agent/:
#   root-cert.pem, server-cert.pem, server-key.pem, token.txt, server-cert.sha256
# The 2.4 packages stored them under certs/ and tokens/ with different names.
# The 2.6 package does not move the old files, so the agent treats the store
# as empty and exits. Copying the existing files is enough. Do not re-register.
#
# Safe to run before or after the 2.6.0 package upgrade. The 2.4 agent ignores
# the new names, and a later upgrade restart will find them. The old files are
# left in place. A new file that already exists and differs is not replaced
# unless --force is given.
#
# The agent listens on port 8080. On a host that also runs isc-stork-server,
# that port belongs to the server. This script still copies the files, then
# the agent start fails with "address already in use". That is expected there.
#
# https://gitlab.isc.org/isc-projects/stork/-/wikis/Known-issues

set -euo pipefail

base=/var/lib/stork-agent
owner=stork-agent
group=stork-agent
unit=isc-stork-agent

force=0
enable_unit=0
dry_run=0

usage() {
    cat <<'EOF'
Usage: sudo fix-stork-agent-certs.sh [--dry-run] [--force] [--enable]

Copy the pre-2.6.0 Stork agent certificates and token to the filenames
2.6.0 expects, then start isc-stork-agent.

  --dry-run   Show the copies and the service action without changing anything
  --force     Replace a destination whose contents differ from the source
  --enable    Also enable isc-stork-agent so it starts on boot

The original files under certs/ and tokens/ are not removed.
EOF
}

# Accept only the flags above. Anything else is a typo, not a path.
while [[ $# -gt 0 ]]; do
    case "$1" in
        --dry-run) dry_run=1 ;;
        --force) force=1 ;;
        --enable) enable_unit=1 ;;
        -h|--help) usage; exit 0 ;;
        *) echo "Unknown argument: $1" >&2; usage >&2; exit 2 ;;
    esac
    shift
done

# install -o and systemctl need root. The certificate directories are mode 700.
if [[ "$(id -u)" -ne 0 ]]; then
    echo "Run this as root: sudo $0" >&2
    exit 1
fi

# The package creates this account. Refuse early if the package is not installed.
if ! id "$owner" >/dev/null 2>&1; then
    echo "User $owner does not exist." >&2
    exit 1
fi
if ! getent group "$group" >/dev/null 2>&1; then
    echo "Group $group does not exist." >&2
    exit 1
fi

# Old path, relative to $base | new path, relative to $base | how to recognize it.
# The names come from newCertPaths in backend/agent/certstore.go.
files=(
    "certs/ca.pem|root-cert.pem|pem"
    "certs/cert.pem|server-cert.pem|pem"
    "certs/key.pem|server-key.pem|pem"
    "tokens/agent-token.txt|token.txt|hex"
    "tokens/server-cert.sha256|server-cert.sha256|hex"
)

die() {
    echo "error: $*" >&2
    exit 1
}

# Reject a file that is the wrong kind before it is copied over a good one.
# PEM files are the CA, the agent certificate, and the private key.
# The token and the server-certificate fingerprint are 64 hex digits.
check_contents() {
    local path=$1
    local kind=$2
    case "$kind" in
        pem)
            grep -q -- "-----BEGIN " "$path"
            ;;
        hex)
            grep -Eq '^[[:xdigit:]]{64}[[:space:]]*$' "$path"
            ;;
        *)
            return 1
            ;;
    esac
}

# First pass only looks. A problem in any file skips every copy and the restart.
problems=0
declare -a actions=()

for entry in "${files[@]}"; do
    src_rel=${entry%%|*}
    rest=${entry#*|}
    dest_rel=${rest%%|*}
    kind=${rest##*|}
    src="$base/$src_rel"
    dest="$base/$dest_rel"

    src_ok=0
    if [[ -f "$src" && -s "$src" ]]; then
        if check_contents "$src" "$kind"; then
            src_ok=1
        else
            echo "Source does not look like a Stork $kind file: $src" >&2
            problems=1
        fi
    fi

    # A directory or socket at the new name cannot be replaced by install.
    if [[ -e "$dest" && ! -f "$dest" ]]; then
        echo "Destination exists and is not a regular file: $dest" >&2
        problems=1
        continue
    fi

    if [[ -f "$dest" ]]; then
        if ! check_contents "$dest" "$kind"; then
            echo "Destination does not look like a Stork $kind file: $dest" >&2
            problems=1
            continue
        fi
        if [[ "$src_ok" -eq 1 ]]; then
            if cmp -s "$src" "$dest"; then
                # Same bytes. The later install only refreshes owner and mode.
                actions+=("perms $dest")
            elif [[ "$force" -eq 1 ]]; then
                actions+=("replace $src -> $dest")
            else
                # A different new file may be a newer registration. Keep it.
                echo "Destination differs from source: $dest" >&2
                echo "Leave it, or re-run with --force to replace it from $src." >&2
                problems=1
            fi
        else
            # The 2.6 file is already valid and the 2.4 file is gone.
            actions+=("keep $dest")
        fi
    elif [[ "$src_ok" -eq 1 ]]; then
        actions+=("copy $src -> $dest")
    else
        echo "Missing source for $dest ($src)." >&2
        problems=1
    fi
done

if [[ "$problems" -ne 0 ]]; then
    die "Refusing to change files or start $unit."
fi

echo "Stork agent certificate migration ($base):"
printf '  %s\n' "${actions[@]}"

if [[ "$dry_run" -eq 1 ]]; then
    echo "Would reset a failed $unit state, if it is failed."
    if [[ "$enable_unit" -eq 1 ]]; then
        echo "Would enable and start $unit."
    else
        echo "Would start $unit without enabling it."
    fi
    echo "Dry run only. No files were changed and $unit was not started."
    exit 0
fi

# Second pass does the copies. install sets owner stork-agent and mode 600,
# which is what the agent expects for the private key and the token.
for entry in "${files[@]}"; do
    src_rel=${entry%%|*}
    rest=${entry#*|}
    dest_rel=${rest%%|*}
    src="$base/$src_rel"
    dest="$base/$dest_rel"

    if [[ -f "$src" ]]; then
        # Stop if a destination changed after the check above decided to keep it.
        if [[ -f "$dest" ]] && ! cmp -s "$src" "$dest" && [[ "$force" -ne 1 ]]; then
            die "Destination changed during the run: $dest"
        fi
        install -o "$owner" -g "$group" -m 600 "$src" "$dest"
    elif [[ -f "$dest" ]]; then
        chown "$owner:$group" "$dest"
        chmod 600 "$dest"
    else
        die "Missing both $src and $dest."
    fi
done

echo "Installed files:"
ls -l "$base/root-cert.pem" "$base/server-cert.pem" "$base/server-key.pem" \
    "$base/token.txt" "$base/server-cert.sha256"

# reset-failed returns an error when the unit was never started, because
# systemd has not loaded it. That is not a failed start. Clear the failed
# state only when there is one, so a first start is not aborted.
if systemctl is-failed --quiet "$unit"; then
    systemctl reset-failed "$unit"
fi
if [[ "$enable_unit" -eq 1 ]]; then
    systemctl enable "$unit"
fi

# A start failure still needs the status and log below, so do not exit here.
start_rc=0
systemctl start "$unit" || start_rc=$?

# Type=simple returns from start as soon as the process is forked. The agent
# can still exit a moment later if the certificates are rejected or the
# listen port is taken. Wait briefly so the check below sees that exit.
for _ in 1 2 3 4 5; do
    state=$(systemctl is-active "$unit" || true)
    if [[ "$state" == "active" ]]; then
        break
    fi
    if [[ "$state" == "failed" ]]; then
        break
    fi
    sleep 1
done

echo
systemctl --no-pager --full status "$unit" || true
echo
journalctl -u "$unit" -n 30 --no-pager || true

state=$(systemctl is-active "$unit" || true)
if [[ "$start_rc" -ne 0 || "$state" != "active" ]]; then
    die "$unit is $state after the certificate copy."
fi

echo
echo "$unit is active."
if [[ "$(systemctl is-enabled "$unit" 2>/dev/null || true)" != "enabled" ]]; then
    echo "The unit is not enabled. It will not start on boot until you run:"
    echo "  sudo systemctl enable $unit"
    echo "Or re-run this script with --enable."
fi
