diff --git a/.github/workflows/verify-keycloak-cohort-restores.yml b/.github/workflows/verify-keycloak-cohort-restores.yml index 128f5f4..121023e 100644 --- a/.github/workflows/verify-keycloak-cohort-restores.yml +++ b/.github/workflows/verify-keycloak-cohort-restores.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: inputs: keycloak_release_sha: - description: Exact protected Keycloak main SHA whose pinned 26.7.3 runtime must start all five restored databases + description: Exact protected Keycloak main SHA whose reviewed pinned runtime must start all five restored databases required: true type: string @@ -66,9 +66,7 @@ jobs: set -euo pipefail [[ "$(git rev-parse HEAD)" == "${GITHUB_SHA}" ]] [[ "$(git -C _keycloak rev-parse HEAD)" == "${KEYCLOAK_RELEASE_SHA}" ]] - grep -Fxq 'KEYCLOAK_UPSTREAM_VERSION=26.7.3' _keycloak/envs/production/.env.keycloak - grep -Fxq 'KEYCLOAK_IMAGE=dhi.io/keycloak:26-debian13@sha256:fab1484b1762fd1269e63a40f068ec73ea75b498eaaa5d02f62f022a5d00ff0f' \ - _keycloak/envs/production/.env.keycloak + python3 scripts/read-keycloak-runtime.py _keycloak/envs/production/.env.keycloak --field github-env >> "$GITHUB_ENV" - name: Ensure interrupted cohort material expires on the release host shell: bash @@ -140,7 +138,7 @@ jobs: run: | set -euo pipefail export DOCKER_CONFIG="/tmp/postgres-keycloak-cohort-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}/docker" - base='dhi.io/keycloak:26-debian13@sha256:fab1484b1762fd1269e63a40f068ec73ea75b498eaaa5d02f62f022a5d00ff0f' + base="${COHORT_KEYCLOAK_IMAGE}" tag="makepad/keycloak-catwlk-cohort:${KEYCLOAK_RELEASE_SHA}" manifest_runtime=$(python3 - <<'PY' import json @@ -167,7 +165,7 @@ jobs: set -euo pipefail job_root="/tmp/postgres-keycloak-cohort-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" export DOCKER_CONFIG="${job_root}/docker" - docker pull 'dhi.io/keycloak:26-debian13@sha256:fab1484b1762fd1269e63a40f068ec73ea75b498eaaa5d02f62f022a5d00ff0f' >/dev/null + docker pull "${COHORT_KEYCLOAK_IMAGE}" >/dev/null docker pull 'postgres:16-alpine@sha256:57c72fd2a128e416c7fcc499958864df5301e940bca0a56f58fddf30ffc07777' >/dev/null catwlk_image=$(<"${job_root}/catwlk-image-tag") scripts/restore-keycloak-cohort-backups.sh "${job_root}/backups" "${job_root}/results" _keycloak "${catwlk_image}" @@ -182,7 +180,7 @@ jobs: evidence_dir="${job_root}/evidence" install -d -m 0700 "${evidence_dir}" python3 - "${job_root}/results/instances.json" "${evidence_dir}/keycloak-cohort-restore-evidence.json" \ - "${GITHUB_RUN_ID}" "${GITHUB_RUN_ATTEMPT}" "${GITHUB_SHA}" "${KEYCLOAK_RELEASE_SHA}" <<'PY' + "${GITHUB_RUN_ID}" "${GITHUB_RUN_ATTEMPT}" "${GITHUB_SHA}" "${KEYCLOAK_RELEASE_SHA}" "${COHORT_KEYCLOAK_IMAGE}" "${COHORT_KEYCLOAK_VERSION}" <<'PY' import json import pathlib import sys @@ -197,10 +195,10 @@ jobs: "postgres_head_sha": sys.argv[5], "postgres_ref": "refs/heads/main", "keycloak_release_sha": sys.argv[6], - "keycloak_base_image": "dhi.io/keycloak:26-debian13@sha256:fab1484b1762fd1269e63a40f068ec73ea75b498eaaa5d02f62f022a5d00ff0f", + "keycloak_base_image": sys.argv[7], "catwlk_runtime_image_id": results["catwlk_runtime_image_id"], "fingerprint_schema": "makepad.keycloak-config-fingerprint.v2", - "keycloak_upstream_version": "26.7.3", + "keycloak_upstream_version": sys.argv[8], "result": "restored-databases-compatible", "instances": results["instances"], } diff --git a/README.md b/README.md index c6eb85e..424cfc9 100644 --- a/README.md +++ b/README.md @@ -364,7 +364,7 @@ Pull-request and protected-main checks run through the repository's native `pull_request_target`, never grants write permissions, and never exposes a deployment environment to pull-request code. -## Keycloak 26.7.3 cohort restore evidence +## Keycloak cohort restore evidence Before the five-realm Keycloak release, dispatch protected workflow `Verify Keycloak Cohort Restore Compatibility` with the exact lowercase current @@ -372,7 +372,10 @@ Keycloak protected-main SHA. There is no mutable rollout repository variable. The workflow resolves `Makepad-fr/keycloak` main independently, checks out that exact release, and verifies its pinned `dhi.io/keycloak:26-debian13@sha256:fab1484b1762fd1269e63a40f068ec73ea75b498eaaa5d02f62f022a5d00ff0f` -runtime and upstream version `26.7.3`. +runtime and upstream version `26.7.3`, or the reviewed `26.8.1` pair +`dhi.io/keycloak:26.8.1-debian13@sha256:e32c433741f6c253fd410ad209f548cb962746b76f7bc19a2fe34466cbd8cc12`. +`scripts/read-keycloak-runtime.py` rejects unknown, duplicate, missing, or +mismatched pins before capture. Build, restore and receipt use that same pair. The protected release runner captures fresh custom-format, no-owner, no-privilege dumps of exactly `keycloak_catwlk`, `keycloak_makepad`, diff --git a/scripts/read-keycloak-runtime.py b/scripts/read-keycloak-runtime.py new file mode 100644 index 0000000..b9898de --- /dev/null +++ b/scripts/read-keycloak-runtime.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +"""Resolve only reviewed immutable runtime pins from an exact Keycloak checkout.""" +import argparse +import importlib.util +from pathlib import Path + +spec = importlib.util.spec_from_file_location("cohort_evidence", Path(__file__).with_name("verify-keycloak-cohort-evidence.py")) +contract = importlib.util.module_from_spec(spec) +spec.loader.exec_module(contract) + + +def read_runtime(path): + fields = {} + for line in Path(path).read_text().splitlines(): + key, sep, value = line.partition("=") + if key in ("KEYCLOAK_IMAGE", "KEYCLOAK_UPSTREAM_VERSION"): + if key in fields or not sep: + raise ValueError("Runtime pin is duplicated or malformed") + fields[key] = value + image = fields.get("KEYCLOAK_IMAGE") + version = fields.get("KEYCLOAK_UPSTREAM_VERSION") + if image not in contract.REVIEWED_RUNTIMES or contract.REVIEWED_RUNTIMES[image] != version: + raise ValueError("Runtime image/version pair has not been reviewed") + return image, version + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("config", type=Path) + parser.add_argument("--field", choices=("image", "version", "github-env"), required=True) + args = parser.parse_args() + try: + image, version = read_runtime(args.config) + except (OSError, ValueError) as error: + parser.exit(1, str(error) + "\n") + if args.field == "github-env": + print("COHORT_KEYCLOAK_IMAGE=" + image) + print("COHORT_KEYCLOAK_VERSION=" + version) + else: + print(image if args.field == "image" else version) diff --git a/scripts/restore-keycloak-cohort-backups.sh b/scripts/restore-keycloak-cohort-backups.sh index 322158e..154ead8 100755 --- a/scripts/restore-keycloak-cohort-backups.sh +++ b/scripts/restore-keycloak-cohort-backups.sh @@ -11,7 +11,7 @@ backup_dir=$1 result_dir=$2 keycloak_source=$3 catwlk_image=$4 -keycloak_image='dhi.io/keycloak:26-debian13@sha256:fab1484b1762fd1269e63a40f068ec73ea75b498eaaa5d02f62f022a5d00ff0f' +keycloak_image=$(python3 "$(dirname "$0")/read-keycloak-runtime.py" "${keycloak_source}/envs/production/.env.keycloak" --field image) postgres_image='postgres:16-alpine@sha256:57c72fd2a128e416c7fcc499958864df5301e940bca0a56f58fddf30ffc07777' run_id=${GITHUB_RUN_ID:-1} run_attempt=${GITHUB_RUN_ATTEMPT:-1} @@ -197,7 +197,7 @@ PY [[ $(docker inspect "${active_keycloak_container}" --format '{{.State.Running}}') == true ]] || break sleep 1 done - [[ "${ready}" == 1 ]] || { docker logs "${active_keycloak_container}" >&2; echo "Keycloak 26.7.3 did not become ready for restored ${slug}." >&2; exit 1; } + [[ "${ready}" == 1 ]] || { docker logs "${active_keycloak_container}" >&2; echo "Reviewed Keycloak runtime did not become ready for restored ${slug}." >&2; exit 1; } fingerprint_configuration after fingerprints=() for category in $(printf '%s\n' "${!category_tables[@]}" | sort); do diff --git a/scripts/test-keycloak-cohort-evidence.sh b/scripts/test-keycloak-cohort-evidence.sh index 30b6ac2..e8d1e8e 100755 --- a/scripts/test-keycloak-cohort-evidence.sh +++ b/scripts/test-keycloak-cohort-evidence.sh @@ -142,4 +142,6 @@ if python3 "${validator}" "${oversized}" >/dev/null 2>&1; then exit 1 fi +python3 "$(dirname "$0")/test-read-keycloak-runtime.py" + echo "Keycloak cohort evidence contract tests passed." diff --git a/scripts/test-read-keycloak-runtime.py b/scripts/test-read-keycloak-runtime.py new file mode 100644 index 0000000..5a11ec2 --- /dev/null +++ b/scripts/test-read-keycloak-runtime.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +"""Runtime selection must reject arbitrary images before restore work starts.""" +import importlib.util +import tempfile +import unittest +from pathlib import Path + +spec = importlib.util.spec_from_file_location('runtime', Path(__file__).with_name('read-keycloak-runtime.py')) +runtime = importlib.util.module_from_spec(spec) +spec.loader.exec_module(runtime) + + +class RuntimeTests(unittest.TestCase): + def read(self, contents): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / 'runtime.env' + path.write_text(contents) + return runtime.read_runtime(path) + + def test_reviewed_pairs(self): + for image, version in runtime.contract.REVIEWED_RUNTIMES.items(): + with self.subTest(version=version): + self.assertEqual((image, version), self.read(f'IGNORED=value\nKEYCLOAK_IMAGE={image}\nKEYCLOAK_UPSTREAM_VERSION={version}\n')) + + def test_reject_unreviewed_or_ambiguous_config(self): + image, version = next(iter(runtime.contract.REVIEWED_RUNTIMES.items())) + valid = f'KEYCLOAK_IMAGE={image}\nKEYCLOAK_UPSTREAM_VERSION={version}\n' + for contents in ('', f'KEYCLOAK_IMAGE={image}\n', valid.replace(version, '99.0.0'), + valid.replace(image, 'dhi.io/keycloak:latest'), + valid + f'KEYCLOAK_IMAGE={image}\n', + valid + f'KEYCLOAK_UPSTREAM_VERSION={version}\n', + valid.replace(image, image + ' ')): + with self.subTest(contents=contents), self.assertRaises(ValueError): + self.read(contents) + + +if __name__ == '__main__': + unittest.main() diff --git a/scripts/validate-postgres-config.sh b/scripts/validate-postgres-config.sh index a323124..cd49c1f 100755 --- a/scripts/validate-postgres-config.sh +++ b/scripts/validate-postgres-config.sh @@ -868,7 +868,9 @@ for required in ( "makepad.keycloak-cohort-restore-evidence.v2", "restored-databases-compatible", "dhi.io/keycloak:26-debian13@sha256:fab1484b1762fd1269e63a40f068ec73ea75b498eaaa5d02f62f022a5d00ff0f", - "KEYCLOAK_UPSTREAM_VERSION=26.7.3", + "read-keycloak-runtime.py", + "COHORT_KEYCLOAK_IMAGE", + "COHORT_KEYCLOAK_VERSION", "restore-keycloak-cohort-backups.sh", "verify-keycloak-cohort-evidence.py", ):