# `shoalcheck gate` on Bitbucket Pipelines -- the job that reads the cluster and fails on what changed.
#
# THE CONTRACT. Exit 0 the policy is satisfied, 2 it fired, 1 the run or the comparison could not be made, 4 the
# licence does not cover the gate -- and a comparison the gates refuse is never a pass. The findings land in the
# step's Tests tab (JUnit, read from test-results/) and its artifacts (the report, the reading, a Markdown summary).
#
# WHAT IT NEEDS (Repository settings > Repository variables): KUBECONFIG_B64, a SECURED variable holding a kubeconfig
# with the read-only access `shoalcheck audit --show-permissions` prints, base64-encoded on one line
# (`base64 < kubeconfig | tr -d '\n'`, because a variable is one line), whose context is the one named below;
# SHOALCHECK_LICENCE, a SECURED variable holding a Team licence key (the gate is a Team command); SHOALCHECK_VERSION,
# the release to pin without the leading v (e.g. 0.1.0-rc.12) -- a pipeline should not change under you, so this
# never asks for "latest".
#
# TWO RUNS FROM ONE FILE. The custom pipeline `shoalcheck-drift` reads the cluster against the committed baseline;
# give it a schedule (Pipelines > Schedules, weekday mornings) and it reads drift with nobody having opened anything.
# A pull request reads the cluster as it is, against the baseline on the branch it merges into, and judges the
# request's rendered manifests laid over that reading in memory (--with) -- before anything is applied. Nothing is
# applied by the gate.
#
# A RUN WITHOUT THE SECRETS DOES NOT GATE, AND SAYS SO. Bitbucket gives a pull request from a fork no secured
# variables. Such a run reads nothing, compares nothing, prints "shoalcheck did not gate this run" and ends without
# failing -- not exit 4, and not a pass.
#
# THE BASELINE IS A READING OF THIS SAME CLUSTER. shoalcheck refuses to compare readings of two different clusters --
# it knows a cluster by its kube-system namespace, not by a context name a rebuilt cluster can reuse -- and a refused
# comparison fails the job with exit 1. Record the baseline once from the cluster this job reads, with
# --update-baseline, and commit it. A cluster made new for every run records its baseline in the same run: apply the
# manifests the change starts from, run the gate with --update-baseline, apply the change, run the gate again.

# Pinned by digest: the tag is for the reader, the digest is what is pulled.
image: alpine:3.24@sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6

definitions:
  steps:
    - step: &gate
        name: shoalcheck gate
        script:
          # A RUN WITHOUT THE SECRETS DOES NOT GATE: it says so first and ends, before installing anything. One block,
          # because each item of a script is its own command and only the last one may decide the step's exit code.
          - |
            if [ -z "${SHOALCHECK_LICENCE:-}" ] || [ -z "${KUBECONFIG_B64:-}" ]; then
              echo "shoalcheck did not gate this run: it has no licence or no cluster credentials (a pull request from a fork is given neither). Nothing was read and nothing was compared, so this is not a pass."
              exit 0
            fi
            set -euo pipefail
            apk add --no-cache curl tar coreutils git

            # CHECKED THE WAY IT IS SIGNED. cosign itself is pinned and checked against the sha256 its release
            # publishes (cosign_checksums.txt, v2.5.2). Then the signature over checksums.txt with the release key,
            # then the archive against its checksum: a binary that is not the one that was signed has no business
            # reading a production cluster. The key is written here, not downloaded: a key fetched from the release it
            # vouches for proves nothing. It is the cosign.pub the releases repository publishes.
            : "${SHOALCHECK_VERSION:?set the repository variable SHOALCHECK_VERSION to the release to pin, without the leading v}"
            curl -fsSL -o /usr/local/bin/cosign https://github.com/sigstore/cosign/releases/download/v2.5.2/cosign-linux-amd64
            echo "bcfeae05557a9f313ee4392d2f335d0ff69ebbfd232019e3736fb04999fe1734  /usr/local/bin/cosign" | sha256sum -c -
            chmod +x /usr/local/bin/cosign
            base="https://github.com/Shoalcheck/shoalcheck/releases/download/v${SHOALCHECK_VERSION}"
            archive="shoalcheck_${SHOALCHECK_VERSION}_linux_amd64.tar.gz"
            for f in "$archive" checksums.txt checksums.txt.sigstore.json; do curl -fsSLO "$base/$f"; done
            printf '%s\n' '-----BEGIN PUBLIC KEY-----' 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE69u2ja9r2k/Vy8lSibowBdCnCD3P' 'OVzmcbjHVIvMgoOfkEzEwOY/b8kyjTVJcwrJ89+qC5bjW4XzLqz1S/AryQ==' '-----END PUBLIC KEY-----' > cosign.pub
            cosign verify-blob --key cosign.pub --bundle checksums.txt.sigstore.json checksums.txt
            grep " $archive\$" checksums.txt | sha256sum -c -
            tar xz -C /usr/local/bin -f "$archive" shoalcheck
            rm -f "$archive" checksums.txt checksums.txt.sigstore.json cosign.pub
            shoalcheck version

            # THE BASELINE COMES FROM THE BRANCH BEING MERGED INTO, never from the pull request: a pull request that
            # edits .shoalcheck/baseline.json could otherwise record its own finding as already known and pass its
            # own gate.
            mkdir -p .shoalcheck shoalcheck-out test-results
            if [ -n "${BITBUCKET_PR_DESTINATION_BRANCH:-}" ]; then
              git fetch --no-tags --depth=1 origin "$BITBUCKET_PR_DESTINATION_BRANCH"
              mkdir -p .shoalcheck
              if git show "FETCH_HEAD:.shoalcheck/baseline.json" > .shoalcheck/baseline.target 2>/dev/null; then
                mv .shoalcheck/baseline.target .shoalcheck/baseline.json
              else
                rm -f .shoalcheck/baseline.target .shoalcheck/baseline.json
                echo "the target branch has no .shoalcheck/baseline.json yet, so there is nothing to compare with"
              fi
            fi

            # The verdict in the step log, and the exit code the step ends on. --json keeps the reading the gate
            # judged; every output after it is rendered from that file, so the test results, the summary and the
            # red build cannot disagree.
            export SHOALCHECK_HOME="$BITBUCKET_CLONE_DIR/.shoalcheck-home"
            export KUBECONFIG="$BITBUCKET_CLONE_DIR/.kubeconfig"
            printf '%s' "$KUBECONFIG_B64" | base64 -d > "$KUBECONFIG"
            set +e
            # THE PULL REQUEST, BEFORE IT LANDS: its rendered manifests go to the gate with --with, which lays them
            # over the reading in memory and judges them -- nothing is applied. not-evaluated is in its policy, so an
            # object the gate could not judge (a kind it does not read, a Secret, a ConfigMap) fails the run rather
            # than passing unexamined; drop it if your changes carry ConfigMaps you accept as unjudged. A binding at
            # an API version the gate does not judge is refused (exit 1).
            if [ -n "${BITBUCKET_PR_ID:-}" ]; then
              # THE CHANGE, RENDERED: --with judges what would be applied, so it takes the rendered manifests, never the
              # sources. Swap in your renderer -- for a chart: helm template <release> charts/<chart> -f values.yaml.
              # The image's kubectl when it has one; otherwise a pinned release, checked against its published sha256.
              change="$(mktemp -d)"
              k="$(command -v kubectl || true)"
              if [ -z "$k" ]; then
                k="$(mktemp -d)/kubectl"
                curl -fsSLo "$k" https://dl.k8s.io/release/v1.31.4/bin/linux/amd64/kubectl
                echo "$(curl -fsSL https://dl.k8s.io/release/v1.31.4/bin/linux/amd64/kubectl.sha256)  $k" | sha256sum -c -
                chmod +x "$k"
              fi
              "$k" kustomize deploy/ > "$change/rendered.yaml"
              shoalcheck gate \
                --context staging \
                --baseline .shoalcheck/baseline.json \
                --with "$change" \
                --fail-on new:high,regressed,unreadable,not-evaluated \
                --output shoalcheck-out/report.html \
                --json shoalcheck-out/reading.json
            else
              shoalcheck gate \
                --context staging \
                --baseline .shoalcheck/baseline.json \
                --fail-on new:high,regressed,unreadable \
                --output shoalcheck-out/report.html \
                --json shoalcheck-out/reading.json
            fi
            code=$?
            set -e
            rm -f "$KUBECONFIG"
            if [ -s shoalcheck-out/reading.json ]; then
              shoalcheck findings --input shoalcheck-out/reading.json -o junit > test-results/shoalcheck.xml
              shoalcheck findings --input shoalcheck-out/reading.json -o md-summary --baseline .shoalcheck/baseline.json > shoalcheck-out/summary.md
              cat shoalcheck-out/summary.md
            fi
            exit $code
        # The report is the artifact a human opens when the gate fails: a single file with no external requests, so it
        # renders from the download exactly as it renders locally. Every finding is also a test in the Tests tab: a
        # failure at high or above, a skipped test below it (never a pass), and a skipped test per kind not read.
        artifacts:
          - shoalcheck-out/**
          - test-results/**

pipelines:
  pull-requests:
    '**':
      - step: *gate
  custom:
    shoalcheck-drift:
      - step: *gate

# Recording a baseline the first time (or after a deliberate change):
#   shoalcheck gate --context staging --baseline .shoalcheck/baseline.json --update-baseline
# It compares first and writes afterwards, so the run that records still reports what moved.
