#!/usr/bin/env bash
# Checks that every node can run the OpsChain MintModel API with render isolation, before you
# install or upgrade. Runs the same check as the opschain-mintmodel-api-isolation-check job, with
# the same security context, once on each node instead of on one node the scheduler picks.
#
# Usage: check-mintmodel-api-nodes.sh -v <opschain version> [-n <namespace>] [-l <node label selector>]
#                                     [-s <image pull secret>] [-i <image>]
#
# Needs kubectl access to the cluster. Exits 0 when every node passes, 1 otherwise.
set -euo pipefail

namespace=opschain
version=
selector=
pull_secret=opschain-image-secret
image=
timeout_seconds=300

usage() {
  cat >&2 <<'EOF'
Usage: check-mintmodel-api-nodes.sh -v <opschain version> [-n <namespace>] [-l <node label selector>]
                                    [-s <image pull secret>] [-i <image>]
EOF
  exit 2
}

while getopts 'v:n:l:s:i:h' opt; do
  case "${opt}" in
    v) version=${OPTARG} ;;
    n) namespace=${OPTARG} ;;
    l) selector=${OPTARG} ;;
    s) pull_secret=${OPTARG} ;;
    i) image=${OPTARG} ;;
    *) usage ;;
  esac
done
[[ -n ${version} || -n ${image} ]] || usage
image=${image:-limepoint/opschain-mintmodel-api:${version}}

kubectl get namespace "${namespace}" >/dev/null || { echo "namespace ${namespace} does not exist; create it first, or pass -n" >&2; exit 2; }

pull_secrets=
if kubectl -n "${namespace}" get secret "${pull_secret}" >/dev/null 2>&1; then
  pull_secrets="imagePullSecrets: [{name: ${pull_secret}}]"
else
  echo "note: no ${pull_secret} secret in ${namespace}; pulling ${image} with the nodes' own credentials" >&2
fi

mapfile -t nodes < <(kubectl get nodes ${selector:+-l "${selector}"} -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}')
[[ ${#nodes[@]} -gt 0 ]] || { echo "no nodes match" >&2; exit 2; }

pod_manifest() {
  cat <<EOF
apiVersion: v1
kind: Pod
metadata:
  generateName: opschain-mintmodel-api-node-check-
  labels:
    app: opschain-mintmodel-api-node-check
spec:
  nodeName: $1
  restartPolicy: Never
  automountServiceAccountToken: false
  ${pull_secrets}
  securityContext:
    runAsUser: 10001
    runAsGroup: 10001
    runAsNonRoot: true
    fsGroup: 10001
  containers:
    - name: check-isolation
      image: ${image}
      command: [opschain-mintmodel-server, --check-isolation]
      securityContext:
        allowPrivilegeEscalation: true
        seccompProfile: {type: Unconfined}
        appArmorProfile: {type: Unconfined}
      env:
        - {name: OPSCHAIN_FORK_FS_ISOLATION, value: request}
        - {name: OPSCHAIN_FORK_FS_SCRATCH_STORAGE, value: /var/lib/opschain-fs-scratch}
      volumeMounts:
        - {name: fs-scratch, mountPath: /var/lib/opschain-fs-scratch}
  volumes:
    - {name: fs-scratch, emptyDir: {}}
EOF
}

check_node() {
  local node=$1 pod phase reason deadline
  if ! pod=$(pod_manifest "${node}" | kubectl -n "${namespace}" create -f - -o name 2>&1); then
    echo "FAIL  ${node}: the pod was rejected, most likely by an admission policy: ${pod}"
    return 1
  fi
  deadline=$((SECONDS + timeout_seconds))
  while :; do
    phase=$(kubectl -n "${namespace}" get "${pod}" -o jsonpath='{.status.phase}')
    reason=$(kubectl -n "${namespace}" get "${pod}" -o jsonpath='{.status.containerStatuses[0].state.waiting.reason}')
    [[ ${phase} == Succeeded || ${phase} == Failed ]] && break
    if [[ ${reason} == ErrImagePull || ${reason} == ImagePullBackOff || ${reason} == InvalidImageName ]]; then
      echo "FAIL  ${node}: could not pull ${image} (${reason}); pass -s with a pull secret or -i with an image the node can reach"
      kubectl -n "${namespace}" delete "${pod}" --wait=false >/dev/null
      return 1
    fi
    if ((SECONDS >= deadline)); then
      echo "FAIL  ${node}: the check did not finish within ${timeout_seconds}s (phase ${phase:-unknown}${reason:+, ${reason}})"
      kubectl -n "${namespace}" delete "${pod}" --wait=false >/dev/null
      return 1
    fi
    sleep 2
  done
  local log
  log=$(kubectl -n "${namespace}" logs "${pod}" 2>&1 | sed 's/^/      /')
  kubectl -n "${namespace}" delete "${pod}" --wait=false >/dev/null
  if [[ ${phase} == Succeeded ]]; then
    echo "PASS  ${node}"
    [[ -n ${log} ]] && echo "${log}"
    return 0
  fi
  echo "FAIL  ${node}:"
  echo "${log}"
  return 1
}

failed=0
for node in "${nodes[@]}"; do
  check_node "${node}" || failed=1
done

if ((failed)); then
  echo "One or more nodes cannot run the MintModel API with isolation. See https://docs.opschain.io/operations/mintmodel-api#what-the-nodes-must-support" >&2
fi
exit "${failed}"
