Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74ba90c0b3 | ||
|
|
fb45d7441b | ||
|
|
8ed4e30201 | ||
|
|
24db269ea8 | ||
|
|
df96be98bf | ||
|
|
b4ff2879be | ||
|
|
d9571fc6d8 | ||
|
|
593d0fad15 |
No files matched your search
@@ -75,7 +75,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: keycloak
|
||||
image: quay.io/keycloak/keycloak
|
||||
image: quay.io/keycloak/keycloak:26.0.6
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
volumeMounts:
|
||||
|
||||
@@ -5,8 +5,15 @@
|
||||
# Validates against schemas/servicedefinition.schema.json.
|
||||
name: keycloak
|
||||
kind: container
|
||||
# DOCUMENTATION-ONLY IMAGE. The running server is NOT chosen here: the Keycloak operator
|
||||
# (platform/keycloak/kustomization.yaml, three pinned release URLs) picks it via its
|
||||
# RELATED_IMAGE_KEYCLOAK, and the Keycloak CR has no spec.image. This SD's render,
|
||||
# apps/wave-1/keycloak.yaml, is not listed in apps/wave-1/kustomization.yaml and deploys nothing
|
||||
# (ci/render-golden.sh RENDER_GOLDEN_NOT_ONBOARDED). The tag records the operator's version for
|
||||
# readers; renovate.json5 disables Renovate for this image so it cannot raise PRs against a file
|
||||
# nothing deploys. The Keycloak version moves with the operator — update this tag by hand then.
|
||||
source:
|
||||
image: quay.io/keycloak/keycloak
|
||||
image: quay.io/keycloak/keycloak:26.0.6
|
||||
exposure: public # login/redirect flows for internet-exposed services; hardened (docs/15; R-DNS/R-NET)
|
||||
egress: internet # normal WAN egress (docs/03; R-NET-05)
|
||||
storage:
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
date: 2026-10-30
|
||||
branch: fix/renovate-hold-auth-and-self
|
||||
title: Operator decision — the oauth2-proxy sidecar and Renovate's own version never auto-merge
|
||||
---
|
||||
|
||||
## What changed
|
||||
|
||||
`renovate.json5` gains two packageRules. Both set `automerge: false` and `addLabels: ["needs-review"]`.
|
||||
They sit after the auto-merge rule, so they override it at every update type. Each rule's
|
||||
`description` carries the operator's reason.
|
||||
|
||||
1. **`quay.io/oauth2-proxy/oauth2-proxy`.** This is the single depName emitted for:
|
||||
- `catalog/render.py` `OAUTH2_PROXY_IMAGE` (custom regex manager);
|
||||
- the five rendered `apps/wave-*` sidecars;
|
||||
- `platform/crafty/deployment.yaml` (kubernetes manager).
|
||||
|
||||
**Reason:** since v7.13.0, oauth2-proxy validates refreshed OIDC sessions against the access
|
||||
token, not the id_token. Every sidecar sets `--cookie-refresh`, and that path is untested against
|
||||
this Keycloak: the platform oauth2-proxy runs v7.15.5 without `--cookie-refresh`.
|
||||
`render-golden` and `--selftest` pass on the bump, but both check rendered text, not the binary.
|
||||
2. **`ghcr.io/renovatebot/renovate`.** This is the job `container.image` in
|
||||
`.gitea/workflows/renovate.yaml`. The rule also lists `renovatebot/github-action`, so a
|
||||
reintroduction would be held too. It is not used anywhere today; `ci/lint.sh` rejects it.
|
||||
|
||||
**Reason:** nothing re-validates `renovate.json5` against the new version before it takes
|
||||
effect. The 39.x `CONFIG_VALIDATION` failure is the precedent.
|
||||
|
||||
## What I verified
|
||||
|
||||
- **The config validates on the pinned 44.167.0.** `renovate-config-validator` printed
|
||||
`Config validated successfully`. The migration diff is only the pre-existing top-level `docker`
|
||||
block.
|
||||
- **The flag change is exactly the intended one.** I used Renovate 44.167.0's own
|
||||
`resolveConfigPresets` and `applyPackageRules` to evaluate b4ff287's config and this branch's
|
||||
config against the same updates (the local dry run of the #86 config). Exactly **2 of 29**
|
||||
branch rows differ:
|
||||
- `ghcr.io-renovatebot-renovate-44.x`: `automerge=true labels=` → `automerge=false labels=needs-review`;
|
||||
- `quay.io-oauth2-proxy-oauth2-proxy-7.x`: the same change, and every upgrade in the branch
|
||||
resolves `false`, the `catalog/render.py` one included.
|
||||
|
||||
The other 27 rows are byte-identical.
|
||||
|
||||
## What I did NOT verify
|
||||
|
||||
- **The three GitHub-hosted branches** in run 357 (`pin-dependencies`, `kubevirt-cdi`,
|
||||
`intel-intel-device-plugins-for-kubernetes-0.x`) are not in the harness table: my local run has
|
||||
no github.com token. Neither rule's package names can match them.
|
||||
- **A live Renovate run with this config.** The next debug-level dispatch should show
|
||||
`needs-review` on both branches.
|
||||
- **The oauth2-proxy refresh path itself** remains untested. Exercising it on one app is the
|
||||
review the label asks for.
|
||||
@@ -0,0 +1,128 @@
|
||||
---
|
||||
date: 2026-10-30
|
||||
branch: fix/renovate-run-hygiene
|
||||
title: Renovate's first completed dry runs, audited; it would have opened red PRs for every app and missed the databases. Config fixed
|
||||
---
|
||||
|
||||
## What changed
|
||||
|
||||
**`renovate.json5` only.** Each change carries a comment quoting the measurement behind it.
|
||||
|
||||
1. **Three new regex managers, so updates edit the SOURCE.**
|
||||
- `catalog/*.yaml` `source.image`.
|
||||
- `catalog/render.py` `OAUTH2_PROXY_IMAGE`.
|
||||
- CNPG `imageName:` under `platform/` and `apps/`.
|
||||
|
||||
The first two match the rendered line's depName, so the catalog edit and the generated
|
||||
`apps/wave-*` edit land in **one** branch.
|
||||
2. **`allowedVersions` mirroring each ServiceDefinition's `update.range`:**
|
||||
- gitea/gitea `<2.0.0`
|
||||
- nextcloud `<32.0.0`
|
||||
- qbittorrent `<6.0.0`
|
||||
- radarr `<6.0.0`
|
||||
- sonarr `<5.0.0`
|
||||
- semaphore `<3.0.0`
|
||||
|
||||
Plus CNPG postgresql `<17.0.0`, because a Postgres major is a migration, not a bump.
|
||||
3. **The blast-radius never-automerge list gains** `gitea/gitea`, `gitea/act_runner` and
|
||||
`ghcr.io/cloudnative-pg/postgresql`.
|
||||
4. **HelmRelease range widening** (`matchManagers: flux`, ranged currentValue) is never
|
||||
auto-merged and is labelled `range-widen`.
|
||||
5. **Majors need a Dependency Dashboard tick** (`dependencyDashboardApproval`) before a PR opens.
|
||||
6. **`flux` manager docker deps are disabled.** These are duplicates of `kubernetes` manager
|
||||
extractions.
|
||||
7. **`velero/velero-plugin-for-aws` uses `semver` versioning**, so its RC pin can reach a stable
|
||||
release.
|
||||
8. **`python` minors are not auto-merged.**
|
||||
9. **Two false comments are corrected.**
|
||||
- Ranges "cap Renovate at patches": false, Renovate widens them.
|
||||
- `nextcloud:31-apache` and `docker:27-dind` "cannot be maintained": false, they were.
|
||||
|
||||
PR #85 is merged, so nothing here overlaps an open branch.
|
||||
|
||||
### The audit: dispatch runs 353 (07:12 UTC) and 354 (07:23 UTC), on 593d0fa
|
||||
|
||||
Both runs ended `success`, Renovate `"result": "done"`, `exitCode 0`. Both were at `info` level, so
|
||||
I reproduced the per-dependency detail with a debug-level local dry run of the same commit (see
|
||||
below).
|
||||
|
||||
The runs were inside the 03:00–06:00 America/New_York window, so branches were processed:
|
||||
- run 353: 37 "Would commit" branches;
|
||||
- run 354: 40, because the operator had added `RENOVATE_GITHUB_COM_TOKEN` 9 s before it.
|
||||
|
||||
Gitea's router log for the act-runner pod during both runs shows 317 2xx responses. The only
|
||||
non-2xx were all expected:
|
||||
- `/v2/` 401: the registry challenge;
|
||||
- `/admin/orgs` 403: the #85 scope probe, by design;
|
||||
- 6× `HEAD …/manifests/sha256:000…` 404: the placeholder digests.
|
||||
|
||||
| # | Class | Count | Cause | Severity | Fix |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | App-image PRs edit only generated `apps/wave-*` | 9 branches | A ServiceDefinition has no `apiVersion`, so the `kubernetes` manager extracts 0 deps from `catalog/`. The oauth2-proxy sidecar is a Python constant (`catalog/render.py:132`) | **Real problem.** Every such PR fails `ci/render-golden.sh` (reproduced: rc 1 "GOLDEN DRIFT"), stays red and holds a PR slot | git (1) |
|
||||
| 2 | CNPG `imageName` untracked | 3 Clusters | The `kubernetes` manager reads `image:` only. The one tracked postgres line was the grant Job (`nextcloud-db.yaml:303`, "keep in step with spec.imageName") | **Real problem.** A real run bumps the Job and leaves the databases behind | git (1) |
|
||||
| 3 | Bogus or out-of-policy majors | 5 branches: gitea 28.2.0, qbittorrent 20.04.1, radarr 6.x, nextcloud 35, postgresql 18.6 | Odd legacy tags, plus `update.range` in `catalog/` not being read by Renovate | Risk if run for real: unmergeable PRs, which hold slots | git (2) |
|
||||
| 4 | Auto-merge of the merge gate's own host | gitea 1.24.3→1.27.3, act_runner 0.2.11→0.6.1, both "minor" | Not in the blast-radius list. A 0.x minor is breaking by semver | **Risk if run for real** | git (3) |
|
||||
| 5 | Range widening auto-merged | external-dns, headlamp (minor); longhorn (blast radius); kube-prometheus-stack (major) | Renovate widens `>=a <b`. The config's comment claimed the opposite | **Risk if run for real:** silently lets Flux adopt new minors | git (4, 9) |
|
||||
| 6 | Majors hold PR slots | 10 majors in run 354 against `prConcurrentLimit: 5` | Majors never auto-merge, so they stay open | Risk: starves auto-mergeable PRs | git (5) |
|
||||
| 7 | `Cannot find replaceString … alpine:3.22` (INFO) | 1 per run | The same HelmRelease `image:` line is extracted by `flux` and `kubernetes` (also the velero plugin) | Cosmetic. The branch is still right | git (6) |
|
||||
| 8 | velero-plugin-for-aws stuck on `v1.14.3-rc.2` | 1 dep, 0 updates | Docker versioning treats `-rc.2` as a suffix, so stable `v1.14.3` and `v1.14.4` never qualify | **Real problem** (silent) | git (7) |
|
||||
| 9 | python 3.13→3.15 as auto-mergeable "minor" | 2 files | Language runtime minor | Risk if run for real | git (8) |
|
||||
| 10 | `WARN GitHub token is required` | run 353: 5 packages, 17 deps | No github.com token | Real (they were unmaintained) | **operator, done:** run 354 has no WARN and adds `pin-dependencies`, `kubevirt-cdi`, `actions-checkout-7.x`, `major-github-artifact-actions` |
|
||||
| 11 | Floating or untagged images (`invalid-value`) | 4: jackett, plex `latest`; pihole, keycloak no tag | No version to compare | Real, already documented | none in git. Pinning by hand is an operator/PM decision |
|
||||
| 12 | `runs-on: ubuntu-latest` `invalid-version` | 11 | The github-actions manager extracts runner labels | Cosmetic, debug-only | none |
|
||||
| 13 | `Docker Manifest is unknown` (debug) | 3 | HEAD on the all-zero placeholder digests | Expected. Clears when the own-image digest PRs merge | none |
|
||||
| 14 | `Docker: error fetching data from DockerHub` 403 (debug) | 5 | Hub refuses `page=11`. Renovate already has the tags | Cosmetic. Updates were still found | none |
|
||||
| 15 | `Config migration necessary` | 1 | Top-level `"docker": {"enabled": true}` | Cosmetic, pre-existing | none (as before) |
|
||||
|
||||
**Checked and fine:**
|
||||
- The 3 own-image digest updates were found in both runs. All 4 `# renovate-digest:` markers parse.
|
||||
- No marker failed to parse.
|
||||
- No package file is under `conformance/runs` or `tests`, nor is `DECISIONS-LOG.md`, `decisions/` or
|
||||
any `*.sops.yaml` (77 package files checked).
|
||||
- `kubevirt-cdi` grouping works.
|
||||
- The schedule window behaved as designed.
|
||||
- Vulnerability alerts: none in either log.
|
||||
|
||||
## What I verified
|
||||
|
||||
- I read both job logs in full (132 and 129 lines), and the router log for 10.0.0.38 during both
|
||||
runs.
|
||||
- **`renovate-config-validator` on 44.167.0**: `Config validated successfully`. The migration diff
|
||||
is only the pre-existing `docker` block.
|
||||
- **Local dry runs.** Pinned image, `--platform=local`, debug, origin/main snapshot. "Before" is
|
||||
593d0fa and "after" is this config; the GitHub-hosted deps were skipped in both.
|
||||
- **Extraction:** `regex` went from 6 files / 7 deps to **19 files / 20 deps**. Every other
|
||||
manager is unchanged: flux 15, github-actions 24, kubernetes 131, kustomize 2.
|
||||
- **Branches:** 33 before, **29 after**. Removed: gitea-28.x, qbittorrent-20.x, radarr-6.x,
|
||||
nextcloud-35.x and postgresql-18.x. Added: velero `v1.14.3-rc.2 → v1.14.4` (patch).
|
||||
- **Gitea, qbittorrent, radarr, sonarr, semaphore and seerr branches** now each carry
|
||||
`catalog/<svc>.yaml` + `apps/wave-*/<svc>.yaml`.
|
||||
- **The oauth2-proxy branch** carries `catalog/render.py` plus the 5 rendered apps and
|
||||
`platform/crafty`.
|
||||
- **The postgresql-16.x branch** carries all three Clusters.
|
||||
- The 2 flux docker duplicates report `disabled`.
|
||||
- **render==golden holds for the new branch shape.**
|
||||
- In the toolbox, I applied the gitea branch edit (catalog + apps) and the oauth2-proxy branch
|
||||
edit (`render.py` constant + 5 apps) textually. `./ci/render-golden.sh` reported
|
||||
`all 10 onboarded service golden(s) reproduce`, rc 0.
|
||||
- Negative control, the old shape with only `apps/wave-0/gitea.yaml` bumped: rc 1,
|
||||
`GOLDEN DRIFT`.
|
||||
- Both edits were reverted afterwards.
|
||||
|
||||
## What I did NOT verify
|
||||
|
||||
- **The effect of the automerge, label and dashboard-approval rules (3, 4, 5, 8). UNVERIFIED in a
|
||||
run.** `--platform=local` stops before branch processing, so I saw only which updates exist, not
|
||||
their merged flags. The matchers are standard (`matchPackageNames`, `matchManagers`,
|
||||
`matchCurrentValue`, `matchUpdateTypes`). The next dispatch's debug log shows them per branch.
|
||||
- **`allowedVersions` under docker versioning** used Renovate's "Falling back to npm semver
|
||||
syntax" path (16 debug lines). That it filters is shown by the five removed branches.
|
||||
- **render==golden for the other app branches** (qbittorrent, radarr, sonarr, semaphore, seerr). I
|
||||
proved it only for gitea and oauth2-proxy. They have the same shape: one `source.image` line ↔
|
||||
one rendered `image:` line.
|
||||
- **The `allowedVersions` rules duplicate `catalog/*/update.range`** and can drift from it. Nothing
|
||||
checks them against each other. A lint comparing the two is the obvious follow-up.
|
||||
- **Any real (non-dry) run.** None has happened. PR creation, labels, auto-merge through Gitea's
|
||||
API and the Dependency Dashboard issue are all unobserved.
|
||||
- **Sensitive path?** `renovate.json5` is not in `infra/gitea/approval-policy.json`, as far as I
|
||||
know. CI's `approval-policy` result on the PR is the measurement.
|
||||
@@ -0,0 +1,113 @@
|
||||
---
|
||||
date: 2026-10-30
|
||||
branch: feat/renovate-track-keycloak-operator
|
||||
title: Renovate tracks the Keycloak operator (the real Keycloak version), never auto-merged; the catalog's phantom image is pinned as documentation and disabled
|
||||
---
|
||||
|
||||
## What changed
|
||||
|
||||
- **`platform/keycloak/kustomization.yaml`**: each of the three `keycloak-k8s-resources/26.0.6`
|
||||
release URLs (two CRDs and `kubernetes.yml`) now has a
|
||||
`# renovate: datasource=github-tags depName=keycloak/keycloak-k8s-resources` marker on the line
|
||||
above it. That is the format the existing `custom.regex` marker manager parses (the kubevirt
|
||||
pattern). A comment explains that these URLs are the Keycloak version. The `Keycloak` CR has no
|
||||
`spec.image`, so the operator's `RELATED_IMAGE_KEYCLOAK` picks the server image.
|
||||
- **`renovate.json5`** has two new packageRules:
|
||||
- **`keycloak/keycloak-k8s-resources`**: `groupName: keycloak-operator`, `automerge: false`,
|
||||
`addLabels: [needs-review, blast-radius]`. A comment explains why. The operator, the CRDs and
|
||||
the server are released in lockstep. The IdP gates SSO for every app and for kubectl. And
|
||||
`passkey-reconciler.yaml` encodes Keycloak 26.0.6 Admin API shapes that the static gate never
|
||||
exercises. Majors still go through the existing dashboard-approval rule.
|
||||
- **`quay.io/keycloak/keycloak`**: `enabled: false`. Its only occurrences are in files that
|
||||
deploy nothing.
|
||||
|
||||
The "STILL CANNOT FIX" comment also gains a one-line correction for keycloak.
|
||||
- **`catalog/keycloak.yaml`**: `source.image` changed from `quay.io/keycloak/keycloak` (no tag) to
|
||||
`quay.io/keycloak/keycloak:26.0.6`. A comment says the image is DOCUMENTATION-ONLY and that the
|
||||
operator chooses the real one. `apps/wave-1/keycloak.yaml` was re-rendered, which changes one
|
||||
line.
|
||||
- **`platform/keycloak/README.md`**:
|
||||
- Corrects the false claim "Renovate maintains the version". Until now Renovate could not see
|
||||
the version.
|
||||
- Adds a new section, **"Upgrading Keycloak: the operator IS the version"**. It lists the 26.0.6
|
||||
assumptions in the passkey reconciler to diff against a new tag, and the post-merge checks.
|
||||
|
||||
**The phantom: pin and disable, not delete.** `apps/wave-1/keycloak.yaml` is not listed in
|
||||
`apps/wave-1/kustomization.yaml`, and render-golden excludes it (`RENDER_GOLDEN_NOT_ONBOARDED`).
|
||||
Deleting it would still not be a no-op:
|
||||
|
||||
- It is the fixture for the keycloak cases in `apps/wave-1/tests/app-slis.test.yaml` (the
|
||||
StatefulSet-based `keycloak-down` rule). Those cases are discovered by the
|
||||
every-PrometheusRule-in-apps/wave-1 convention.
|
||||
- Removing it means moving that rule and its tests to `platform/keycloak`. That is a separate
|
||||
observability change, not a Renovate one.
|
||||
|
||||
Pinning plus `enabled: false` changes one line in a file nothing applies. It cannot affect the
|
||||
cluster, and render-golden stays green.
|
||||
|
||||
## What I verified
|
||||
|
||||
- **Live state, prod, read-only kubectl, 2026-10-30.**
|
||||
- Keycloak CR `keycloak` has an empty `spec.image`.
|
||||
- The operator Deployment runs `quay.io/keycloak/keycloak-operator:26.0.6`, imageID
|
||||
`sha256:8e3a1a56346f…`, with `RELATED_IMAGE_KEYCLOAK=quay.io/keycloak/keycloak:26.0.6`.
|
||||
- `keycloak-0` and `keycloak-1` both run `quay.io/keycloak/keycloak:26.0.6`, imageID
|
||||
`sha256:a93d22e13b86…`.
|
||||
- There is no PrometheusRule containing "keycloak" in any namespace. This confirms the phantom
|
||||
deploys nothing, including its `keycloak-down` rule (see the next section).
|
||||
- **`renovate-config-validator`** on the pinned `ghcr.io/renovatebot/renovate:44.167.0`:
|
||||
`Config validated successfully`. The only migration note is the pre-existing `docker` block.
|
||||
- **Local dry run** (`RENOVATE_PLATFORM=local`, `RENOVATE_DRY_RUN=full`, debug, the same pinned
|
||||
image, this worktree, with a GitHub token passed by environment variable; the token string
|
||||
occurs 0 times in the log):
|
||||
- The regex manager went to 21 files / 24 deps.
|
||||
- `keycloak/keycloak-k8s-resources` was extracted three times from
|
||||
`platform/keycloak/kustomization.yaml`, each at `currentValue 26.0.6`, datasource
|
||||
`github-tags`, versioning `semver`.
|
||||
- Each extraction has one update, `26.8.1`, updateType `minor`, and all three share **one**
|
||||
`branchName: renovate/keycloak-operator`.
|
||||
- `quay.io/keycloak/keycloak` was extracted from `catalog/keycloak.yaml` (regex) and from
|
||||
`apps/wave-1/keycloak.yaml` (kubernetes) at 26.0.6, with `skipReason: disabled` and no
|
||||
updates.
|
||||
- **Rule effects.** `--platform=local` does not print each branch's merged flags. So I ran
|
||||
Renovate's own `applyPackageRules` (from `dist/util/package-rules` in the same 44.167.0 image)
|
||||
over this `renovate.json5` for each case:
|
||||
|
||||
| Update | Result |
|
||||
|---|---|
|
||||
| operator minor 26.0.6 → 26.8.1 | `automerge:false`, `groupName:keycloak-operator`, labels `dependencies, automated-update, needs-review, blast-radius` |
|
||||
| operator patch | same as the minor |
|
||||
| operator major | `automerge:false`, `dependencyDashboardApproval:true`, labels add `breaking` |
|
||||
| `quay.io/keycloak/keycloak`, catalog and rendered | `enabled:false` |
|
||||
| control: sonarr patch | `automerge:true`, so the rule did not disable auto-merge globally |
|
||||
- **render-golden, host:** `render.py --selftest` passed, and `all 10 onboarded service golden(s)
|
||||
reproduce`. Before the pin, the committed phantom was byte-identical to the canonical render. I
|
||||
re-rendered it after the pin.
|
||||
- **Toolbox gate** (commit 24db269, worktree with the ENGINE shim mounting `.git` and the spec
|
||||
repo, `DEV_KUBECONFIG=/dev/null`): **rc 3**.
|
||||
- The only stage that did not run is **`gitea-settings`**.
|
||||
- spec-immutability, decisions-log, lint, l0, render-golden, l1 and approval-policy all passed.
|
||||
- L2/L3 `conformance` scaffold-skipped, as expected.
|
||||
- **Not green.**
|
||||
- **Sensitive paths:** none touched (checked against `infra/gitea/approval-policy.json`), so no
|
||||
`/approve-override` is needed.
|
||||
|
||||
## What I did NOT verify
|
||||
|
||||
- **A real Renovate run.** No PR was opened by Renovate. The labels and `automerge:false` are
|
||||
proven by Renovate's own matcher, not by an opened PR.
|
||||
- **That 26.8.1 is safe to adopt.** I did not diff 26.0.6 against 26.8.1. That includes the
|
||||
reconciler's assumptions: `recurseExecutions` displayName, `updateExecutions`, the
|
||||
`RpEntityName` guard, `requiredActions` REPLACE, and config aliases. The README section lists
|
||||
them for whoever reviews the first bump PR. It also says to take or confirm a fresh CNPG backup
|
||||
first, because a server minor may migrate the schema.
|
||||
- **FINDING, not fixed: Keycloak has no deployed "IdP down" alert.** The `keycloak-down` SLI rule
|
||||
exists only inside the undeployed `apps/wave-1/keycloak.yaml`. promtool tests it, but it is not
|
||||
on the cluster: there is no keycloak PrometheusRule live. Separately, because that file exists,
|
||||
`render.py --emit-waves` would treat keycloak as onboarded. It would then add a Deployment named
|
||||
`keycloak` beside the operator's StatefulSet (the known `--emit-waves` hazard). The proper fix is
|
||||
a separate PR: move the rule and its tests into `platform/keycloak`, then delete the phantom.
|
||||
- **The catalog tag can drift.** Nothing ties `catalog/keycloak.yaml`'s documentation tag to the
|
||||
operator URLs. It must be updated by hand after an operator bump, and the README says so.
|
||||
- **Concurrent branches** `fix/renovate-hold-auth-and-self` and `feat/pin-pihole-jackett-plex`
|
||||
also edit `renovate.json5`. Neither had merged when I checked. A rebase may be needed.
|
||||
@@ -19,7 +19,9 @@ Docker Hub images were purged (DECISIONS-LOG **F-KY1**) → that path ImagePullB
|
||||
3-replica **`longhorn`** StorageClass (DR-critical, R-IDP-05), with declarative backup hooks.
|
||||
|
||||
quay.io + ghcr.io are unaffected by the Bitnami purge. Operator manifests are pinned upstream release
|
||||
files pulled as remote kustomize bases (the kubevirt-stub pattern); Renovate maintains the version.
|
||||
files pulled as remote kustomize bases (the kubevirt-stub pattern). Renovate tracks the version via
|
||||
`# renovate:` markers on those URLs (since 2026-10-30 — before that it could not see them) and never
|
||||
auto-merges it; see [Upgrading Keycloak](#upgrading-keycloak-the-operator-is-the-version).
|
||||
|
||||
## Layout (controllers / configs split — F-MON1)
|
||||
|
||||
@@ -360,6 +362,38 @@ it on the BOUND flow, after go-live, in order:
|
||||
A failed passkey ceremony is usually silent in the server log — the browser refuses before Keycloak
|
||||
is involved — so step 2 failing with no log entry is the expected signature of a wrong RP ID.
|
||||
|
||||
## Upgrading Keycloak: the operator IS the version
|
||||
|
||||
The `Keycloak` CR has no `spec.image`, so the server image is whatever the operator's
|
||||
`RELATED_IMAGE_KEYCLOAK` names — the same tag as the operator (prod, 2026-10-30: operator
|
||||
`26.0.6`, `keycloak-0/1` on `quay.io/keycloak/keycloak:26.0.6`). Operator, CRDs and server are
|
||||
released in lockstep from `keycloak/keycloak-k8s-resources`, and the three URLs in
|
||||
`kustomization.yaml` are the one knob. Renovate raises them as ONE PR (`keycloak-operator` group,
|
||||
labels `needs-review` + `blast-radius`), **never auto-merged**; majors wait for a dashboard tick.
|
||||
`catalog/keycloak.yaml`'s image tag is documentation only (its render deploys nothing) and
|
||||
Renovate is disabled for it — update it by hand to match after an operator bump merges.
|
||||
|
||||
**An operator bump requires re-checking the passkey reconciler's assumptions.**
|
||||
`passkey-reconciler.yaml` (and `ci/render-keycloak-passkey-policy.py`, which generates its desired
|
||||
state) is written against **Keycloak 26.0.6 Admin API behaviour read from that tag's source** —
|
||||
grep it for `26.0.6`. The gate cannot catch a break: it is static and never runs the reconciler
|
||||
against a server. Before merging a bump, diff the new tag against 26.0.6 for at least:
|
||||
|
||||
- `AuthenticationManagementResource.recurseExecutions` — does a sub-flow row still carry its name
|
||||
in `displayName` (not `alias`)? The reconciler's flow matching depends on it.
|
||||
- `updateExecutions` / execution-requirement updates — same request shape and id resolution?
|
||||
- the WebAuthn Passwordless policy — is it still gated all-or-nothing on a non-empty
|
||||
`webAuthnPolicyPasswordlessRpEntityName`, and does a realm `PUT` still REPLACE rather than merge
|
||||
`requiredActions`? (The strict read-modify-write exists because of both.)
|
||||
- authenticator-config alias handling for copied flows.
|
||||
|
||||
Then, after the merge reconciles (operator actions): watch the `Keycloak` CR reach Ready and both
|
||||
pods roll; run the reconciler once **with `--audit`** (see "Passkeys" above for the one-off Job)
|
||||
and confirm it reports convergence with no write; confirm the client-secret and client-URL
|
||||
reconcilers' next runs succeed; log in to one SSO app and run one `kubectl --context k3s-prod`
|
||||
OIDC command. Roll back by reverting the PR — but a server minor may migrate the database
|
||||
schema on start, so take/confirm a fresh CNPG backup before merging.
|
||||
|
||||
## Exceptions that keep native auth (do not force SSO) — ADR-0014
|
||||
|
||||
Plex/Seerr (Plex auth), Minecraft (Mojang/Microsoft), UniFi (native).
|
||||
@@ -21,8 +21,17 @@ namespace: keycloak
|
||||
resources:
|
||||
- namespace.yaml
|
||||
# --- Keycloak Operator v26.0.6 (pinned upstream release manifests) ---
|
||||
# The operator, its CRDs and the SERVER IMAGE move in lockstep: the Keycloak CR has no
|
||||
# spec.image, so the operator's RELATED_IMAGE_KEYCLOAK (quay.io/keycloak/keycloak:<same tag>)
|
||||
# is what runs. These three URLs are therefore THE Keycloak version. Each carries a Renovate
|
||||
# marker (custom.regex manager, renovate.json5) under ONE depName, so all three bump in ONE
|
||||
# branch, never auto-merged (packageRule "keycloak-operator"). Before merging a bump, re-check
|
||||
# passkey-reconciler.yaml's 26.0.6 Admin API assumptions — see README.md "Upgrading Keycloak".
|
||||
# renovate: datasource=github-tags depName=keycloak/keycloak-k8s-resources
|
||||
- https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.0.6/kubernetes/keycloaks.k8s.keycloak.org-v1.yml
|
||||
# renovate: datasource=github-tags depName=keycloak/keycloak-k8s-resources
|
||||
- https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.0.6/kubernetes/keycloakrealmimports.k8s.keycloak.org-v1.yml
|
||||
# renovate: datasource=github-tags depName=keycloak/keycloak-k8s-resources
|
||||
- https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.0.6/kubernetes/kubernetes.yml
|
||||
# --- Admin (break-glass) credential — SOPS-encrypted, referenced by name (ADR-0007, R-IDP-04) ---
|
||||
- keycloak-admin-credentials.sops.yaml
|
||||
|
||||
+149
-5
@@ -149,6 +149,50 @@
|
||||
],
|
||||
"datasourceTemplate": "docker",
|
||||
"versioningTemplate": "docker"
|
||||
},
|
||||
// ADDED 2026-10-30 — THE CATALOG IS THE SOURCE; apps/wave-*/*.yaml IS GENERATED FROM IT.
|
||||
// First real dry run (dispatch runs 353/354): EVERY app-image update edited only the rendered
|
||||
// apps/wave-*/*.yaml and NONE edited catalog/. The `kubernetes` manager lists catalog/ above
|
||||
// but extracts nothing there: a ServiceDefinition has no `apiVersion`, so it is not a
|
||||
// Kubernetes manifest to that manager (measured: 0 deps from catalog/). Every such PR would
|
||||
// fail ci/render-golden.sh (the canonical render reverts the bump), sit red and hold a
|
||||
// prConcurrentLimit slot. These two managers read the SOURCES, under the SAME depName as the
|
||||
// rendered line, so both land in ONE branch and render==golden holds: `source.image` in each
|
||||
// ServiceDefinition, and render.py's OAUTH2_PROXY_IMAGE (the six auth sidecars).
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "ServiceDefinition `source.image: <repo>:<tag>` (catalog/*.yaml) — the source of the rendered apps/wave-* image.",
|
||||
"managerFilePatterns": ["/(^|/)catalog/[^/]+\\.ya?ml$/"],
|
||||
"matchStrings": [
|
||||
"\\n\\s+image:\\s*(?<depName>[^\\s:@#\"']+):(?<currentValue>[^\\s@#\"']+)"
|
||||
],
|
||||
"datasourceTemplate": "docker"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "catalog/render.py OAUTH2_PROXY_IMAGE — the per-app oauth2-proxy sidecar every forward-auth app is rendered with.",
|
||||
"managerFilePatterns": ["/(^|/)catalog/render\\.py$/"],
|
||||
"matchStrings": [
|
||||
"\\nOAUTH2_PROXY_IMAGE\\s*=\\s*\"(?<depName>[^\":@]+):(?<currentValue>[^\"@]+)\""
|
||||
],
|
||||
"datasourceTemplate": "docker"
|
||||
},
|
||||
// ADDED 2026-10-30 — CNPG `Cluster.spec.imageName`. The `kubernetes` manager reads `image:` only,
|
||||
// so the three database Clusters (platform/keycloak/postgres-cluster.yaml, apps/wave-0/
|
||||
// nextcloud-db.yaml, apps/wave-0/semaphore-db.yaml) were untracked, while the grant Job in
|
||||
// nextcloud-db.yaml — whose comment says "keep the tag in step with spec.imageName" — WAS. A real
|
||||
// run would have bumped the Job and left the database behind. Same depName, so one branch.
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "CloudNativePG Cluster `imageName: <repo>:<tag>`.",
|
||||
"managerFilePatterns": [
|
||||
"/(^|/)platform/.+\\.ya?ml$/",
|
||||
"/(^|/)apps/.+\\.ya?ml$/"
|
||||
],
|
||||
"matchStrings": [
|
||||
"\\n\\s+imageName:\\s*(?<depName>[^\\s:@#\"']+):(?<currentValue>[^\\s@#\"']+)"
|
||||
],
|
||||
"datasourceTemplate": "docker"
|
||||
}
|
||||
],
|
||||
"packageRules": [
|
||||
@@ -200,7 +244,15 @@
|
||||
"longhorn",
|
||||
"cilium",
|
||||
"cloudnative-pg",
|
||||
"kube-prometheus-stack"
|
||||
"kube-prometheus-stack",
|
||||
// ADDED 2026-10-30. gitea and act_runner ARE the merge gate: a bad auto-merged bump takes
|
||||
// down the very check that would have to approve its revert (the dry run proposed gitea
|
||||
// 1.24.3 -> 1.27.3 and act_runner 0.2.11 -> 0.6.1, both as auto-mergeable "minor"; a 0.x
|
||||
// minor is a breaking line by semver). The postgresql image restarts every CNPG database,
|
||||
// Keycloak's (the IdP) included, now that imageName is tracked.
|
||||
"gitea/gitea",
|
||||
"gitea/act_runner",
|
||||
"ghcr.io/cloudnative-pg/postgresql"
|
||||
],
|
||||
"automerge": false,
|
||||
"addLabels": [
|
||||
@@ -208,6 +260,35 @@
|
||||
"blast-radius"
|
||||
]
|
||||
},
|
||||
// ADDED 2026-10-30 — THE KEYCLOAK OPERATOR IS THE KEYCLOAK VERSION. The operator, its CRDs
|
||||
// and the server image are released in lockstep from keycloak/keycloak-k8s-resources, and the
|
||||
// Keycloak CR carries no spec.image, so the operator's RELATED_IMAGE_KEYCLOAK picks the server
|
||||
// (measured on prod 2026-10-30: operator 26.0.6 -> keycloak-0/1 run quay.io/keycloak/keycloak:
|
||||
// 26.0.6). The three release URLs in platform/keycloak/kustomization.yaml carry `# renovate:`
|
||||
// markers under this ONE depName, so they move together in one branch (groupName makes that
|
||||
// explicit). NEVER AUTO-MERGED, not even a patch: Keycloak is the IdP that gates SSO for every
|
||||
// app AND kubectl OIDC, and platform/keycloak/passkey-reconciler.yaml encodes Keycloak 26.0.6
|
||||
// Admin API shapes read from that tag's source (sub-flow rows named by displayName, the
|
||||
// RpEntityName all-or-nothing guard, requiredActions REPLACE-not-merge, ...). The gate is
|
||||
// static and does not run that reconciler against a server, so a green check says nothing
|
||||
// about whether a bump breaks it. Majors still wait on the dashboard (rule at the bottom).
|
||||
{
|
||||
"description": "Keycloak operator + CRDs + server (lockstep, one branch). IdP blast radius; passkey-reconciler.yaml encodes 26.0.6 Admin API shapes — re-check it against the new tag before merging. Never auto-merge.",
|
||||
"matchDepNames": ["keycloak/keycloak-k8s-resources"],
|
||||
"groupName": "keycloak-operator",
|
||||
"automerge": false,
|
||||
"addLabels": ["needs-review", "blast-radius"]
|
||||
},
|
||||
// ADDED 2026-10-30 — quay.io/keycloak/keycloak IS NOT A DEPENDENCY RENOVATE MAY MOVE. Its only
|
||||
// occurrences are catalog/keycloak.yaml `source.image` and the apps/wave-1/keycloak.yaml it
|
||||
// renders, which is NOT listed in apps/wave-1/kustomization.yaml and deploys nothing (the
|
||||
// operator above runs the server). A PR bumping it would change no running pod while looking
|
||||
// like a Keycloak upgrade, and could drift from the operator's real version.
|
||||
{
|
||||
"description": "quay.io/keycloak/keycloak appears only in the documentation-only catalog/keycloak.yaml and its undeployed render; the operator (keycloak/keycloak-k8s-resources) chooses the real server image.",
|
||||
"matchPackageNames": ["quay.io/keycloak/keycloak"],
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"description": "ADDED 2026-10-29. This repo's own images are followed ONLY through the `# renovate-digest:` markers above. The `kubernetes` manager also sees platform/pm-agent/deployment.yaml and infra/agent/worker-job-template.yaml, where the image has a digest and no tag; left on, it would look those up as `latest` (a tag CI never pushes) and, for the template, re-open the self-referential loop the custom manager deliberately avoids.",
|
||||
"matchManagers": ["kubernetes"],
|
||||
@@ -227,6 +308,65 @@
|
||||
"gitea.home.keithjackson.dev/home-network/conformance-runner"
|
||||
],
|
||||
"addLabels": ["own-image"]
|
||||
},
|
||||
{
|
||||
"description": "ADDED 2026-10-30. The `flux` manager also extracts `image:` lines inside HelmRelease values, which the `kubernetes` manager already owns. Measured: exactly two (alpine in platform/oauth2-proxy/helmrelease.yaml, velero-plugin-for-aws in platform/velero/helmrelease.yaml), each extracted twice; the second rewrite of the same line logged `Cannot find replaceString in current file content` in runs 353/354. Flux keeps its charts (datasource helm).",
|
||||
"matchManagers": ["flux"],
|
||||
"matchDatasources": ["docker"],
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"description": "ADDED 2026-10-30. MIRRORS catalog/<svc>.yaml `update.range` — the operator's declared per-service policy (schemas/servicedefinition.schema.json), which Renovate does not read. Without it the dry run proposed gitea 28.2.0, qbittorrent 20.04.1 (both stale/odd tags, not real successors), radarr 6.x and nextcloud 35 (four majors past 31; Nextcloud cannot skip majors). KEEP IN STEP with the catalog ranges.",
|
||||
"matchPackageNames": ["gitea/gitea"],
|
||||
"allowedVersions": "<2.0.0"
|
||||
},
|
||||
{ "matchPackageNames": ["nextcloud"], "allowedVersions": "<32.0.0" },
|
||||
{ "matchPackageNames": ["lscr.io/linuxserver/qbittorrent"], "allowedVersions": "<6.0.0" },
|
||||
{ "matchPackageNames": ["lscr.io/linuxserver/radarr"], "allowedVersions": "<6.0.0" },
|
||||
{ "matchPackageNames": ["lscr.io/linuxserver/sonarr"], "allowedVersions": "<5.0.0" },
|
||||
{ "matchPackageNames": ["semaphoreui/semaphore"], "allowedVersions": "<3.0.0" },
|
||||
{
|
||||
"description": "ADDED 2026-10-30. A CNPG Postgres MAJOR is a migration (new cluster + import), not an image bump; the dry run proposed 16.4 -> 18.6. Minors stay (and are blast-radius, above).",
|
||||
"matchPackageNames": ["ghcr.io/cloudnative-pg/postgresql"],
|
||||
"allowedVersions": "<17.0.0"
|
||||
},
|
||||
{
|
||||
"description": "ADDED 2026-10-30. A HelmRelease semver RANGE is a policy ceiling, and Renovate does not respect it — it WIDENS it (dry run: external-dns `<1.22.0` -> `<1.24.0`, headlamp `<0.42.0` -> `<0.47.0`), and the default rule would then auto-merge that as a 'minor', silently letting Flux adopt every new minor. Raising a ceiling is deliberate per-component work: propose it, never auto-merge it.",
|
||||
"matchManagers": ["flux"],
|
||||
"matchCurrentValue": "/^[<>=~^]|\\.x$/",
|
||||
"automerge": false,
|
||||
"addLabels": ["needs-review", "range-widen"]
|
||||
},
|
||||
{
|
||||
"description": "ADDED 2026-10-30. velero-plugin-for-aws is pinned to a RELEASE CANDIDATE (v1.14.3-rc.2) and Renovate proposed NOTHING for it, although v1.14.3 and v1.14.4 are stable on Docker Hub. Docker versioning treats everything after the first `-` as a compatibility SUFFIX, so only other `*-rc.2` tags qualify and the RC is a fossil forever. Semver versioning reads it as a prerelease, and a prerelease upgrades to the stable release.",
|
||||
"matchPackageNames": ["velero/velero-plugin-for-aws"],
|
||||
"versioning": "semver"
|
||||
},
|
||||
{
|
||||
"description": "ADDED 2026-10-30. A python MINOR is a language-runtime release (the dry run proposed 3.13 -> 3.15 for the Keycloak reconcilers), and the gate does not execute those scripts. Review it; patches still auto-merge.",
|
||||
"matchPackageNames": ["python"],
|
||||
"matchUpdateTypes": ["minor"],
|
||||
"automerge": false,
|
||||
"addLabels": ["needs-review"]
|
||||
},
|
||||
// OPERATOR DECISION 2026-10-30: these two never auto-merge, at ANY update type. Both sit after
|
||||
// the auto-merge rule (rule 1) so they override it; `needs-review` is added, not replaced.
|
||||
{
|
||||
"description": "ADDED 2026-10-30 (operator decision). The oauth2-proxy image, under the ONE depName every source of it emits: catalog/render.py OAUTH2_PROXY_IMAGE (custom regex manager), the rendered apps/wave-* sidecars and platform/crafty/deployment.yaml (kubernetes manager). WHY: since v7.13.0 oauth2-proxy validates REFRESHED OIDC sessions against the ACCESS token instead of the id_token (upstream CHANGELOG, v7.13.0), every sidecar sets --cookie-refresh, and that refresh path has never run against this Keycloak (the platform oauth2-proxy runs v7.15.5 WITHOUT --cookie-refresh). render-golden and render.py --selftest pass on the bump, but both check rendered text, not the binary, so a green gate is not evidence here.",
|
||||
"matchPackageNames": ["quay.io/oauth2-proxy/oauth2-proxy"],
|
||||
"automerge": false,
|
||||
"addLabels": ["needs-review"]
|
||||
},
|
||||
{
|
||||
"description": "ADDED 2026-10-30 (operator decision). Renovate's OWN version: the job `container.image` pin in .gitea/workflows/renovate.yaml (ghcr.io/renovatebot/renovate), and renovatebot/github-action should it ever be reintroduced (ci/lint.sh rejects it today). WHY: nothing re-validates renovate.json5 against the NEW version before it takes effect. The gate does not run renovate-config-validator, and this exact gap has bitten before: the action's default 39.x rejected this file (managerFilePatterns needs >= 40.2.0) and every run stopped at CONFIG_VALIDATION. Run the validator from the new tag, then merge.",
|
||||
"matchPackageNames": ["ghcr.io/renovatebot/renovate", "renovatebot/github-action"],
|
||||
"automerge": false,
|
||||
"addLabels": ["needs-review"]
|
||||
},
|
||||
{
|
||||
"description": "ADDED 2026-10-30. MAJORS WAIT FOR A TICK ON THE DEPENDENCY DASHBOARD instead of opening a PR. They are never auto-merged anyway, and with prConcurrentLimit 5 the dry run's 10 majors would hold every PR slot open indefinitely, starving the patch/minor/digest PRs that can merge on green.",
|
||||
"matchUpdateTypes": ["major"],
|
||||
"dependencyDashboardApproval": true
|
||||
}
|
||||
],
|
||||
// WHAT RENOVATE STILL CANNOT FIX, so nobody mistakes a working bot for full coverage. A
|
||||
@@ -235,10 +375,14 @@
|
||||
// lscr.io/linuxserver/jackett:latest, pihole/pihole (NO TAG AT ALL), quay.io/keycloak/keycloak
|
||||
// (no tag), nextcloud:31-apache and docker:27-dind. Those must be pinned BY HAND to a concrete
|
||||
// version before any automation can maintain them, and until then they drift on every pull with
|
||||
// nothing recording what is running. Separately, the HelmRelease minor-locking ranges (9 of 12,
|
||||
// e.g. `>=88.0.0 <89.0.0` on kube-prometheus-stack) cap Renovate at patches inside a line it
|
||||
// has been told not to leave; raising those ceilings is deliberate per-component work and is
|
||||
// NOT part of enabling the bot. Both lists are in decisions/2026-10-17-automated-upgrades.md.
|
||||
// nothing recording what is running. CORRECTED 2026-10-30: nextcloud:31-apache and docker:27-dind
|
||||
// ARE maintained (the first dry run proposed 35-apache and 29-dind; the suffix is kept).
|
||||
// CORRECTED 2026-10-30: quay.io/keycloak/keycloak was never a running reference — the operator
|
||||
// picks the server image — and the Keycloak version is now tracked via the operator URLs.
|
||||
// Separately, the HelmRelease minor-locking ranges (9 of 12, e.g. `>=88.0.0 <89.0.0` on
|
||||
// kube-prometheus-stack): THIS FILE USED TO SAY they cap Renovate at patches. They do not —
|
||||
// Renovate WIDENS the range (runs 353/354). The `range-widen` rule above keeps that a reviewed
|
||||
// PR rather than an auto-merge. Both lists are in decisions/2026-10-17-automated-upgrades.md.
|
||||
// platform/kyverno/tests/** ADDED 2026-10-17: those Kyverno policy fixtures pin `alpine:latest`
|
||||
// and a `conformance-runner:latest`, which are test inputs whose whole point is to be a fixed
|
||||
// shape, not software to keep current. `conformance/**` was already ignored for the same
|
||||
|
||||
Reference in new issue
Block a user