77b978d669253886ea3bcc0d4edf8b566b4e65e6
83
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
77b978d669
|
feat(outline)!: NetworkPolicy, on by default
Major, as every chart in this rollout is. Inbound 3000 only; outbound DNS plus 5432, 6379 and 443. Those three egress ports are the STANDARD ones and not the configured ones, which is this chart's real limitation and is written into values.yaml, the template header and NOTES.txt rather than left to be discovered: Outline's database, Redis and OIDC provider all arrive as connection URLs inside 1Password items that the chart never parses, so it cannot know where they listen and cannot validate a guess. A dependency on another port is blocked, and because all three probes hit /_health - which checks Postgres and Redis - the symptom is a pod that never becomes Ready rather than an error anyone reads. networkPolicy.egress.extraPorts is the escape hatch. That same property is what makes this chart the right place for the enforcement experiment in the next commit. Also reverts the cyberchef fixture from the first experiment: with the probe port restricted to an address that is not the node's, the install-test went GREEN, so the claim that it catches a blocked probe path does not hold as written. The next commit establishes whether policies are enforced in CI at all. Part of #5 |
||
|
|
d3ef3729f8
|
test(ci): prove the install-test enforces NetworkPolicies [EXPERIMENT, reverted next]
Validate Helm Charts / Detect changed charts (pull_request) Successful in 3s
Validate Helm Charts / Resource quantities (pull_request) Successful in 12s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 11s
Validate Helm Charts / Bump level (pull_request) Successful in 4s
Validate Helm Charts / Trivy config cyberchef (pull_request) Successful in 18s
Validate Helm Charts / Trivy config template-chart (pull_request) Successful in 17s
Install-test / Install-test cyberchef (pull_request) Successful in 1m4s
Validate Helm Charts / Install-test (pull_request) Successful in 1m4s
Validate Helm Charts / Selector match (pull_request) Successful in 12s
Validate Helm Charts / YAML scalars (pull_request) Successful in 11s
Validate Helm Charts / NetworkPolicy (pull_request) Failing after 11s
Validate Helm Charts / Build cyberchef (pull_request) Successful in 7s
Validate Helm Charts / Build template-chart (pull_request) Successful in 7s
The scope of Gitea #5 - 29 charts at once, default on - rests on the claim that the install-test enforces policies, so a policy that blocks the probe path goes red in CI rather than silently in production. k3d is created without --disable-network-policy, but a flag read off a workflow file is not evidence. This commit restricts cyberchef's probe port 8080 to 192.0.2.11/32, an RFC 5737 documentation address that cannot be the k3d node's IP. The kubelet probes from the node's own address, so they are dropped and the pod must never become Ready. Expected: the cyberchef install-test goes RED on rollout status. If it goes green, policies are not enforced in CI and the scope has to be renegotiated. Reverted in the next commit either way. Part of #5 |
||
|
|
36bcfd3b11
|
feat(ci): assert every rendered NetworkPolicy against its own render
The part that carries the rollout of Gitea #5. 29 charts get a policy that is on by default, and every way one of them can be wrong is SILENT: the API server accepts a policy that filters nothing just as readily as one that filters everything, and neither is a render error, so neither the build nor a rollout sees it. ci/run-networkpolicy-test.sh holds every rendered NetworkPolicy against the pod specs, probes and Services of the SAME render: 1. the podSelector really matches a pod of that render - otherwise the object sits in the namespace, is listed by kubectl, reads as a boundary and filters nothing, usually because a label was renamed in another file; 2. every INGRESS port is a containerPort of the selected pods, which catches the NodePort mistake mechanically. Egress ports are deliberately excluded: an egress port belongs to the DESTINATION, and comparing it would fail every chart that reaches a database; 3. no ingress rule allows every source by accident. "from: []" always fails - it allows everything while reading as a restriction. A MISSING "from" is the documented open form and is accepted only where the render justifies it: every port of the rule is addressed by a network probe, or published by a NodePort Service. That is stricter than the plan's wording, which banned both forms, and it had to be, because the precedents use the second one on purpose; 4. the probe path. For every port a NETWORK probe addresses, the policy either leaves it unrestricted or the chart offers networkPolicy.probeSources AND says in NOTES.txt that a list missing the node's IP leaves the pod permanently un-Ready. exec probes are excluded by design - they run inside the container and put no packet on the network, which is exactly why valkey and minecraft-server can restrict their port by source with no probeSources at all. Two guards beyond the four: a chart that renders NO policy fails, because that is how this issue decayed in the first place - garage and keycloak were both created after #5 and both arrived without one. And a "#" comment inside a rendered policy fails, because it is part of the object and is shipped to the cluster. Helm is the parser through ci/yaml2json, as in the other render tests: the runners have neither pyyaml nor yq, and a hand-written parser could read nothing, find nothing and leave this green over a chart it never looked at. Every failure path was run, not just the green ones, and one of them earned its keep immediately: the probeSources check was a substring test, so a chart whose key had been deleted while the comment above it still discussed the value passed. It now looks for the key. Part of #5 |
||
|
|
39844fc92c
|
ci: hold every scalar that must be a string, and the required podAffinity
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 15s
Validate Helm Charts / Resource quantities (pull_request) Successful in 15s
Validate Helm Charts / Selector match (pull_request) Successful in 14s
Validate Helm Charts / Bump level (pull_request) Successful in 7s
Validate Helm Charts / Trivy config paperless-ngx (pull_request) Successful in 29s
Validate Helm Charts / Install-test (pull_request) Successful in 5m11s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 16s
Publish Paperless-NGX Helm Chart / Publish Paperless-NGX Helm Chart (push) Successful in 16s
SonarQube Analysis / SonarQube Analysis (push) Successful in 47s
Validate Helm Charts / Detect changed charts (pull_request) Successful in 4s
Validate Helm Charts / Build paperless-ngx (pull_request) Successful in 9s
Validate Helm Charts / YAML scalars (pull_request) Successful in 15s
Install-test / Install-test paperless-ngx (pull_request) Successful in 5m11s
The test Gitea #11 asked for ("whoever fixes this ships a test case that would have caught it"), plus the guard for the fix #101 introduces. ci/run-yaml-scalar-render-test.sh renders every chart with the fixtures the install-test already uses and asserts three properties Kubernetes itself requires, so no per-chart expectations are needed: a CronJob schedule is a non-empty string, every env[].value of every container and initContainer is a string, and every label and annotation value is a string. The first catches the loud failure - the render aborting with a YAML error that names the template and not the value - and the other two catch the quiet one, where the render succeeds and the API server rejects the object at apply time. WHY A NEW JOB AND NOT THE INSTALL-TEST. The object that carried the defect is rendered by no fixture at all: paperless-ngx' consume CronJob sits behind smbConnection.enabled, false in the chart defaults and false in its install-test fixture, since the CI cluster has no SMB CSI driver. Nothing in this repository ever produced it. So the overlays under ci/yaml-scalar/ are the test, not the scaffolding: they switch such objects on AND feed them hostile values, and the script fails when an overlay stops reaching its object or when the run asserted nothing at all. The fail path was exercised in all three shapes against a throwaway copy of the chart as it stands on main, not reasoned about: the bare schedule aborted with "yaml: line 6: did not find expected alphabetic or numeric character"; with only the schedule quoted, the star-valued env entry aborted on the StatefulSet instead; and with only the bool-looking value left - a render that succeeds - the type assertion named the variable, the container and the API server's message. Green repo-wide otherwise: 30 charts, 408 scalars, 0 failed. ci/run-selector-match-test.sh additionally holds every requiredDuringSchedulingIgnoredDuringExecution podAffinity term against all pod templates of the same render (Gitea #101). That case is not the gap the script's header describes: a preference that cannot be met is dropped and an anti-affinity over zero pods is satisfied, but a REQUIRED term matching no pod can never be satisfied and the pod stays Pending for good. paperless-ngx' mover selects a label written in another template, which is exactly the copy this test holds - renaming it would leave both templates rendering perfectly while the mover silently stopped moving. Exercised by doing that rename in a throwaway copy: one FAIL naming the selector, the pod templates it missed and the labels actually present. Six charts still render their env block bare (matrix-synapse, minecraft-router, minecraft-server, rallly, satisfactory, teamspeak-server). They are green only because no fixture of theirs sets a hostile value; README now says so instead of repeating template-chart's claim that every chart quotes. Part of #11 Part of #101 |
||
|
|
59d2614384
|
feat(ci): the chart bump follows the app bump
Validate Helm Charts / Detect changed charts (pull_request) Successful in 5s
Validate Helm Charts / Build ${{ matrix.chart }} (pull_request) Skipped
Validate Helm Charts / Install-test (pull_request) Skipped
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 14s
Validate Helm Charts / Bump level (pull_request) Successful in 7s
Validate Helm Charts / Trivy config cyberchef (pull_request) Successful in 20s
Validate Helm Charts / Trivy config matrix-synapse (pull_request) Successful in 22s
SonarQube Analysis / SonarQube Analysis (push) Successful in 48s
Validate Helm Charts / Resource quantities (pull_request) Successful in 18s
Validate Helm Charts / Selector match (pull_request) Successful in 14s
Validate Helm Charts / Trivy config samba (pull_request) Successful in 21s
The version-bump hook classified every app update that was not a major as a chart patch, so an app minor arrived as a patch - the mechanism behind the sonarr incident, where a security default was switched off inside a bump the conventions told the reviewer not to read (Gitea #128). ci/renovate-bump-chart.sh now takes the level from Renovate's update type, passed in as a second argument: major -> major, minor -> MINOR, patch/digest/pin -> patch. The level comes from Renovate rather than from comparing appVersion because an appVersion comparison is blind to the case that prompted the rule - freesailarr keeps a hand-maintained appVersion for ten upstreams, and prowlarr's 2.5.2 -> 2.6.5 moved an image tag in values.yaml while appVersion stood still. Three properties come with it: - Maximum, not replacement. The chart jump is the greater of the chart's own classification and the app jump, so a value that becomes required stays a major under an app patch. Mechanically it is also what makes a grouped branch deterministic: the hook runs once per update from the same base commit, so without it the last call would win. - Non-semver app versions stay at patch for a minor (seven charts). In CalVer and date tags the second position is a month; in a four-part or suffixed version the positions are not semver positions. An app major is not subject to it. - A missing or unknown update type falls back to the pre-#128 behaviour. postUpgradeTasks only runs where the command is allow-listed on the shared instance, which is not visible from here and fails silently, so the worst case is the old classification rather than every chart PR red on the version check. ci/run-bump-level-test.sh holds 49 cases against the version string the script writes, wired up as the bump-level job: a wrong level is a valid version string, so the PR would go green and the only symptom would be a number that understates what it publishes. The convention texts follow in CLAUDE.md and README, including the limit the decision deliberately keeps: the reading duty was rejected, so the incident stays repeatable and the honest number is the whole of the remedy. Fixes #128 |
||
|
|
b4c9823e01
|
feat(stalwart): MTA-STS routes as a maintainable list, in both routing modes
The policy route for mta-sts.<domain> belonged to another repository
(jk-cluster-config) while pointing at this chart's own Service. It moves
here as mtaSts.hosts, a list, so several domains - subdomains included -
are maintainable in one place and in whichever routing mode is active.
THE LIST IS THE SWITCH. No mtaSts.enabled: "enabled with an empty list"
would be a state with no meaning that the chart would then have to catch.
The schema rejects the key outright, so the rule is enforced rather than
documented. Default [] renders nothing in either mode - the defaults and
the unchanged CI fixture both produce a byte-identical helm template
against origin/main.
Entry form {host, tlsSecretName, clusterIssuer?}. The two TLS fields are
read ONLY in Ingress mode; in Gateway mode TLS sits on the listener, so
requiring them there would demand a value that does nothing.
THE PATH IS HARDWIRED. RFC 8461 fixes /.well-known/mta-sts.txt, rendered
with pathType Exact (type: Exact in the route). A values key for a
protocol-mandated path can only ever be set wrong, and wrong is invisible
from this side. Exact also matters in itself: a prefix on these hostnames
would publish the mail server's whole management surface under them.
ONE INGRESS PER CLUSTER ISSUER, NOT ONE OBJECT - and the plan's own three
requirements forced that. cert-manager reads the two fields at different
scopes: cluster-issuer is an ANNOTATION (object-scoped, and absent means
nothing is issued for that object at all), while secretName is
tls-entry-scoped and yields one Certificate per entry. So an object cannot
both request a certificate and leave another host's secret alone, which is
what an empty per-entry issuer has to mean. Measured in jk-cluster:
ingress-shim names the Certificate AFTER THE SECRET and owns it from the
Ingress, so a shared secret must sit in exactly one object (hence grouping
the tls list by secret, reusing rss-bridge.tlsGroups' shape) and a secret
may not span two issuer groups (hence the abort). Objects are named by
issuer, not by index: an index is only free where the object holds no
certificate, which is the precondition homeassistant's route naming states
for itself.
Gateway mode renders ONE HTTPRoute with every hostname - nothing differs
per host there - attached to the same Gateway(s) as the WebUI route.
WHAT THE CHART CANNOT PROMISE, AND THEREFORE NAMES: it registers a route,
it does not create a domain. The domains live in Stalwart's data store, so
an entry whose domain is missing there answers 404 at a correctly routed
endpoint with every object green - and MTA-STS failures are silent on the
SENDER's side, which delivers in the clear and logs nothing here. Same for
the _mta-sts.<domain> TXT record and the policy's content. The subdomain
trap is documented too: mta-sts.sub.<domain> is one label below
*.<domain>, so a parent wildcard certificate does not cover it.
Not folded into certificate.extraDnsNames deliberately - that would hang
the MAIL certificate on this list, letting a bad policy entry take STARTTLS
and IMAPS with it.
Minor, additive, empty default: 1.2.0+upv0.16.25 -> 1.3.0+upv0.16.25.
CI: ci/httproute/stalwart.yaml enters all three shapes of the entry form
(with issuer, without, and a subdomain), taking the HTTPRoute render test
from 56 assertions over 21 charts to 58 over 21 - stalwart from 2 to 4.
The Ingress half cannot be covered there, since an overlay's whole job is
to switch the mode off, so ci/stalwart/test-values.yaml carries two
entries and the install-test has a real API server accept them.
Fixes #123
|
||
|
|
649417b428
|
Merge pull request 'feat(keycloak): one hostname per realm, in both routing modes' (#125) from feat/keycloak-realm-hosts into main | ||
|
|
5cdb2a6227
|
Merge origin/main into feat/keycloak-realm-hosts, and settle README line 184
Validate Helm Charts / Resource quantities (pull_request) Successful in 17s
Validate Helm Charts / Selector match (pull_request) Successful in 21s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 14s
Validate Helm Charts / Trivy config keycloak (pull_request) Successful in 26s
Validate Helm Charts / Install-test (pull_request) Successful in 3m43s
Validate Helm Charts / Detect changed charts (pull_request) Successful in 4s
Validate Helm Charts / Build keycloak (pull_request) Successful in 8s
Install-test / Install-test keycloak (pull_request) Successful in 3m43s
The rss-bridge Gateway-API round landed while this branch was open, and the two rounds shared exactly one line: the sentence in README.md that carries the whole history of the httproute-match assertion count. Merged rather than rebased on purpose: the branch is already pushed and its history is not rewritten here, so nothing of the other round's text can be lost to a force push. Their half is kept verbatim, including the delta rule they added - that 58/22 and 60/21 were measured on branches which did not contain each other, so the delta is what either number can be trusted for, and the running total belongs to the first branch that holds both. This branch IS that first branch, so the open question is now answered with a measurement instead of an addition: 62 assertions over 22 charts, 0 failed, read off ci/run-httproute-match-test.sh on this merge. keycloak contributes 8 of them, plus the two "not checked" lines for its realm routes' backend-less redirect rules. |
||
|
|
f530f58b1d
|
docs(rss-bridge): #51 was closed, and the cause was never identified (2.1.1)
Validate Helm Charts / Resource quantities (pull_request) Successful in 15s
Validate Helm Charts / Selector match (pull_request) Successful in 14s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 14s
Validate Helm Charts / Build rss-bridge (pull_request) Successful in 8s
Install-test / Install-test rss-bridge (pull_request) Successful in 1m6s
Validate Helm Charts / Install-test (pull_request) Successful in 1m6s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 12s
Publish RSS-Bridge Helm Chart / Publish RSS-Bridge Helm Chart (push) Successful in 12s
SonarQube Analysis / SonarQube Analysis (push) Successful in 45s
Validate Helm Charts / Detect changed charts (pull_request) Successful in 4s
Validate Helm Charts / Trivy config rss-bridge (pull_request) Successful in 23s
PR #124 merged at 12:25 and the maintainer closed #51 one second later, on the strength of that round's green install-test. Three statements that merged with #124 are stale as a result, and they are stale in the direction that matters - they describe a failing test and an open issue: - rss-bridge/templates/rss-bridge.httproute.yaml and values.yaml said the install-test "fails on the Gitea runner (#51)", in the present tense, and values.yaml added "and that test is still red". - ci/httproute/rss-bridge.yaml said "#51 is not closed on the strength of a colour", which was true when written and is now the opposite of what happened. Replaced with what is actually known, and the emphasis moved to the part that outlives the colour: the test went green on a render byte-identical to main, which rules the chart out and leaves the runner - and THE CAUSE WAS NEVER IDENTIFIED. The diagnostic programme in #51 (docker info for userns and the dind configuration, the same image in a local k3d without remapping) was not executed. So a recurrence is a fresh finding rather than a known quirk, and the overlay comment now says so where whoever sees the red will be reading. Also carries the wording change that was already on this branch when #124 merged: the reproduced green is phrased as "every run of that branch since" rather than pinned to a count, because each commit recording the count invalidated it - a third green arrived with the push after "two". Patch bump, per the rule #40 was argued on: a comment in a chart directory is still a change to that chart, so it is a patch plus a publish. No template output changes - the render for the fixture values is byte-identical to the pre-#124 main, which the tests re-confirm. |
||
|
|
a411ec3c90
|
docs(rss-bridge): phrase the reproduced green without a run count
The count was pinned at "two consecutive runs" and a third green arrived with the next push, which is a regress: every commit that records the number invalidates it. Phrased as "every run of that branch since" instead, which is stable, still establishes that it is not a fluke, and cannot go stale in main. The caution it carries is unchanged and is the one that matters: it is a colour, not a cause. Nobody has identified what changed on the runner, and #51's diagnostic programme is still unexecuted. |
||
|
|
23ac537e24
|
docs(rss-bridge): the green install-test reproduced, so say "twice" rather than "one run"
Validate Helm Charts / Detect changed charts (pull_request) Successful in 7s
Validate Helm Charts / Resource quantities (pull_request) Successful in 19s
Validate Helm Charts / Selector match (pull_request) Successful in 16s
Validate Helm Charts / Install-test (pull_request) Successful in 1m8s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 17s
Validate Helm Charts / Build rss-bridge (pull_request) Successful in 9s
Validate Helm Charts / Trivy config rss-bridge (pull_request) Successful in 20s
Install-test / Install-test rss-bridge (pull_request) Successful in 1m8s
The previous commit recorded the install-test going green and hedged it with "it is one run". Pushing that commit produced a second run, on a second commit, and it was green too - run 792 job 2623 (1m10s) and run 794 job 2638 (1m14s), both with the chart genuinely in the matrix. So the hedge was understated in one direction and the real caution lies elsewhere. Corrected to say what is actually true: it reproduced over two consecutive runs, so it is not a fluke - and nobody has identified what changed on the runner, which is the reason #51 stays open. A green test does not explain itself, and the diagnostic programme in that issue's body (docker info on the runner for userns and the dind configuration, the same image in a local k3d without remapping) remains unexecuted. "Close an issue on a cause, not on a colour" is unaffected by the count. |
||
|
|
7a2eb29e9e
|
feat(keycloak): one hostname per realm, in both routing modes
Validate Helm Charts / Detect changed charts (pull_request) Successful in 4s
Validate Helm Charts / Resource quantities (pull_request) Successful in 14s
Validate Helm Charts / Selector match (pull_request) Successful in 14s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 14s
Validate Helm Charts / Trivy config keycloak (pull_request) Successful in 24s
Validate Helm Charts / Install-test (pull_request) Successful in 3m29s
Install-test / Install-test keycloak (pull_request) Successful in 3m29s
Validate Helm Charts / Build keycloak (pull_request) Successful in 8s
keycloak.realmHosts gives a realm a hostname of its own: this release's Service published under a further name, with that name's bare / sent to /realms/<realm>/account/. It renders in BOTH modes - one Ingress per entry carrying nginx.ingress.kubernetes.io/app-root, or one HTTPRoute per entry whose Exact match on / carries a RequestRedirect filter - because a routing value in this repository is mode-independent by design - ingress.domain is read in Gateway mode too - so a feature rendering in one mode only would silently disappear when an installation switched. Default []: nothing renders, and the render is byte-identical to 1.2.0 for the fixture's values in both modes. Minor, additive, no value becomes required. ONE OBJECT PER ENTRY, not one with several hostnames. spec.hostnames applies to the whole object and a rule's matches are path-based, so inside a shared route every entry would match Exact: / and the realm could not be told apart. Names therefore carry the LIST INDEX, as homeassistant's per-host routes do and for the same reason - a realm name is not a dependable object-name fragment. The consequence is written down rather than left to be discovered: reordering the list renames and recreates the objects. THE WORK IS IN THE INGRESS HALF, NOT THE ROUTE. Two convention rules do not survive these objects, and both deviations are required rather than tolerated: tlsSecretName is a per-entry value with NO default, because these hostnames already have certificates and several entries typically share one, so a derived name would ask cert-manager for a second certificate for a name a wildcard already covers; and the cert-manager annotation is OPTIONAL per entry, because cert-manager keys a Certificate by the secret it writes and only one object per secret may request it - the same deliberate omission the qBittorrent object in freesailarr makes. Annotations are per entry too: of the four hostnames this was written for, three carry the proxy-buffer pair and one does not, which a chart-global block could not express. statusCode: 302 is written out at the field's own default. 301 and 302 are both Support: Core, so the portability trap of #119 - an implementation declining an Extended code sets Accepted: False on the WHOLE route - does not apply here; ingress-nginx emits 302 for app-root, and a value that is only accidentally right documents nothing. Five render aborts, each against a failure that reports nothing: an entry without a host (ingress-nginx reads an empty host as a catch-all for the whole controller), without a realm (/realms//account/, a 404 behind a healthy hostname and certificate), without a tlsSecretName in Ingress mode (the controller's default certificate, a browser-only failure), a repeated host or one the release already publishes (webhook rejection in Ingress mode, two merged routes decided by object age in Gateway mode), and a filled list with both modes off (no object at all, and nothing in the release that reveals the list was ignored). The entry-shape checks sit in a shared helper so they exist in both modes, and the Ingress template skips them while httpRoute.enabled is also true so that the specific message cannot hide the mutual-exclusion one. CI: the two entries live in ci/keycloak/test-values.yaml, which ci/httproute/keycloak.yaml is applied on top of, so one definition covers both modes - two Ingress objects in the install-test, two HTTPRoutes in the render test. They cover both Ingress forms, with and without a clusterIssuer, and the render test holds their inertness in Gateway mode. The httproute match test goes from 56 assertions over 21 charts to 60, keycloak's share from 4 to 8 plus two "not checked" lines for the backend-less redirect rules. The #118 prefix/redirect pairing correctly does not fire: it is scoped to rules that strip a prefix, and a realm route strips nothing. NOT IN THIS CHANGE: the migration. Nothing in jk-cluster-config is touched. With the default [] no running system changes, so the order a takeover has to run in is documented instead - remove the objects in the owning repository first, then install with the list filled, because the reverse or both at once is rejected by ingress-nginx's admission webhook and leaves a Fleet bundle in ErrApplied until forceSyncGeneration is raised. Whether anything there depends on those objects BY NAME is unchecked and named as unchecked. Fixes #122 |
||
|
|
7d365c912c
|
docs(rss-bridge): the install-test went green, and the chart is ruled out as the cause
Validate Helm Charts / Resource quantities (pull_request) Successful in 15s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 16s
Validate Helm Charts / Build rss-bridge (pull_request) Successful in 8s
Install-test / Install-test rss-bridge (pull_request) Successful in 1m14s
Validate Helm Charts / Install-test (pull_request) Successful in 1m14s
Validate Helm Charts / Detect changed charts (pull_request) Successful in 4s
Validate Helm Charts / Selector match (pull_request) Successful in 15s
Validate Helm Charts / Trivy config rss-bridge (pull_request) Successful in 23s
This chart's install-test had failed on the Gitea runner since the move (#51): php-fpm as uid 33 unable to read /app/index.php, the startup probe 403ing through its whole 150s budget, the pod never Ready. The chart was deliberately kept OUT of ci/excluded-charts.txt so that a pass would be noticed rather than silenced. It passed, in this PR. Measured from the run: k3d cluster up, helm install in Ingress mode, pod 1/1 Ready in 22s with 0 restarts, no "Permission denied" anywhere in the job, and all eight check contexts green. The chart is ruled out as the cause by a diff rather than by an argument: the rendered output for the fixture values is BYTE-IDENTICAL to origin/main - proven in the previous commit to justify the Minor bump - so the pod spec that succeeded is, line for line, the pod spec that had been failing. The implicated layer is the runner, which is where #51 had already placed the burden of proof. The control case arrived for free, from evidence collected for an unrelated reason. What this does NOT establish, and the docs say so: that it is repaired. It is one run, nobody has identified what changed on the runner, and an intermittent effect needs several runs per side. #51 therefore stays open and is to be closed on a cause, not on a colour - which was the standing instruction there before the green arrived. Corrected accordingly, because shipping documentation that says the test fails while it demonstrably passed is the half-finished state this repo tries not to leave behind: - README: the httproute-test paragraph, guideline 4's closing passage on the last chart of the sweep, and the chart table row. - rss-bridge/templates/NOTES.txt: the Gateway branch no longer claims the chart has no successful install-test. What remains true, and is what it now says, is that the install-test installs the INGRESS mode, so the Gateway mode is render-only - the same position as all 21 other charts of the sweep rather than a worse one. - ci/httproute/rss-bridge.yaml: same correction, plus why the overlay stays load-bearing either way. The wiki carries the same correction in the same round (rss-bridge, CI-CD) plus a Lessons-Learned entry on the decision that produced the signal: an excluded test is not a quiet test but a blind one, because you also lose the moment the fault stops existing. |
||
|
|
45b2d2ea4e
|
feat(rss-bridge): Gateway-API as an alternative to the Ingress (last chart)
Validate Helm Charts / Detect changed charts (pull_request) Successful in 4s
Validate Helm Charts / Resource quantities (pull_request) Successful in 17s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 15s
Validate Helm Charts / Build rss-bridge (pull_request) Successful in 8s
Validate Helm Charts / Trivy config rss-bridge (pull_request) Successful in 24s
Install-test / Install-test rss-bridge (pull_request) Successful in 1m10s
Validate Helm Charts / Install-test (pull_request) Successful in 1m10s
Validate Helm Charts / Selector match (pull_request) Successful in 14s
rss-bridge was the twenty-second and last chart of the Gateway-API sweep: 22 charts carried an Ingress template, 21 an HTTPRoute template, and the difference was exactly this one. It had been held back as frozen by #51 - a runner defect, not a property of the chart - and that freeze is lifted. One HTTPRoute carrying ALL THREE hostnames, because nothing differs per host: every name serves the same Service at "/", measured on the live Ingress. spec.hostnames is a list, so one object is the whole answer and a second would repeat one rule. (Unlike homeassistant, whose paths differ per host, or matrix-synapse, whose two routes want different backends.) httpRoute.hostnames is its own MANDATORY value, which is a third shape for this repository and follows from the values rather than from taste. Most charts fall back to ingress.domain; freesailarr and homeassistant reject a hostnames key outright, because a chart-global list could not say which app or host entry it belonged to. This chart has no ingress.domain either, but its names are unambiguous - and the ingress.hosts list it would otherwise reuse carries per-host tlsSecretName values that address nothing once TLS sits on the Gateway's listener. Reusing it would drag a structure whose other half is meaningless into the new mode. rss-bridge.tlsGroups is therefore INERT in Gateway mode, and that is named as a simplification rather than quietly translated: the helper exists only to group hosts by TLS secret name, and there is nothing left to group. The required() on ingress.hosts needed no change to become conditional - it already sits inside the Ingress template behind ingress.enabled - so what is new is that values.yaml and the schema now say so. The ingress.enabled-derived-value screen that caught sure, freesailarr and ytdlp-web-player was run here and came back CLEAN: outside the Ingress template the only readers of an ingress value are NOTES.txt, which knows its own mode, and tlsGroups, called only from behind the switch. The app takes no base URL in its environment, so no "published" helper is needed. First chart of the sweep whose Gateway branch has NO loss to explain, and it was measured rather than assumed (read-only kubectl against the live object): the running Ingress carries the cert-manager annotation plus Helm and Rancher/Fleet bookkeeping, and not one nginx.ingress.kubernetes.io annotation. The group-3 screen came back empty, so NOTES.txt says so positively instead of copying a loss paragraph from another chart. Also corrects the last occurrence in the repository of the outdated sentence "charts in this repo deliberately have no dependencies" (rss-bridge/templates/_helpers.tpl) - dependencies are admissible but expensive since #214, and ytdlp-web-player, stalwart and paperless-ngx use subcharts. The reason the helper is duplicated is unchanged, only its content: sharing it would mean a library chart, a second artefact to publish, version and pull for about one change a year. Minor, not major: additive, defaults unchanged, and relaxing a required is not a tightening. Proven with an EMPTY helm template diff against origin/main for the defaults (229 lines, byte-identical); the Gateway mode with its overlay renders the HTTPRoute in place of the Ingress and nothing else changes. appVersion untouched - 2026-09-30 is the newest date tag the registry has, everything newer is sha-* and latest. ci/run-httproute-match-test.sh goes from 56 assertions over 21 charts to 58 over 22. The overlay at ci/httproute/rss-bridge.yaml has to SUPPLY hostnames, the second in the repository that does (homeassistant's was the first, for a different reason), because this chart has no fallback. NOT PROVEN, and stated in the PR as well: that traffic flows. No Gateway controller runs in CI and no HTTPRoute reaches "Accepted: True" there. For this chart there is additionally NO successful install-test at all - it fails on the Gitea runner in either mode (#51, reproduced on two images while the same chart and image run healthy in the cluster). The chart is deliberately NOT added to ci/excluded-charts.txt: an exclusion would silence the signal, and the day the runner is fixed the test goes green by itself. So the only red contexts expected here are "Install-test / Install-test rss-bridge" and the collective "Validate Helm Charts / Install-test". Fixes #114 Fixes #40 |
||
|
|
e4b41577ae
|
feat(ytdlp-web-player): Gateway-API as an alternative to the Ingress
Group 3 of the #114 sweep, last chart - and the one that carried the group-2 pattern this sweep was told to look for. It reads ingress.enabled OUTSIDE its Ingress template, in ytdlp-web-player.validate, and in Gateway mode ingress.enabled is false by construction, so that guard would have gone silent. Fixed with ytdlp-web-player.published = ingress.enabled OR httpRoute.enabled - the freesailarr.published / sure.publishedOverTls shape, identical to ingress.enabled while httpRoute.enabled is false, which is why it changes no existing render. AND THE GUARD IS NEEDED MORE IN GATEWAY MODE, NOT LESS. It refuses to publish the application and the oauth2-proxy on the same host: - In INGRESS mode the consequence of ignoring it is loud: ingress-nginx's admission webhook rejects the second Ingress and the Fleet bundle goes to ErrApplied. - In GATEWAY mode THE WEBHOOK NEVER FIRES. It compares Ingress objects with Ingress objects, and an HTTPRoute is a different kind - so nothing rejects anything, both objects stay valid, and one hostname is served over two data paths that are NOT equivalent: the proxy's Ingress goes through AUTHENTICATION and this chart's HTTPRoute goes straight to the application. Which one a client reaches depends on what DNS resolves to. That is an authentication bypass that comes and goes with a DNS record and that no Kubernetes object reports. So the Gateway-mode abort has its own message saying exactly that. Both messages exercised, plus the collision reached through httpRoute.hostnames rather than through the ingress.domain fallback, and the guard still firing when only the Deployment is rendered. THE SUBCHART HAS NO GATEWAY SUPPORT, and this chart cannot give it one - upstream's chart is consumed as it comes (README, "Chart dependencies"). It renders an Ingress or nothing. So in the RECOMMENDED configuration, where the proxy sits in the data path and owns the host, the publishing object is the SUBCHART'S INGRESS in both of this chart's modes, and turning httpRoute on buys nothing. The route is for the proxy-disabled case, or for the application on a host of its own - which is the documented unauthenticated bypass. Said in the template header, in values.yaml, in the schema and in the proxy branch of NOTES.txt, because someone migrating hosts will otherwise set the value and wonder why nothing moved. The 16k proxy-buffer-size this chart recommends is NOT lost by this switch, and that needed stating rather than assuming: it belongs on the PROXY's Ingress (oauth2-proxy.ingress.annotations) as the second half of the 4kB session-cookie trap, and the subchart keeps rendering an Ingress. This chart's own ingress.annotations default is {} and it sets nothing but the cert-manager annotation, so there is no chart-side annotation loss to name. CORRECTED CLAIM: values.yaml and the schema both said that everything the Ingress needs is read only inside its template, so switching it off freed BOTH ingress.domain and ingress.clusterIssuer. clusterIssuer is still freed; domain is now read by the HTTPRoute, so it is freed only with both modes off. The reason the claim was made still holds and is kept - the application has no base-URL setting and builds no absolute links. NOTHING WAS MEASURED ON A LIVE INGRESS OF THIS CHART, because there is none. Measured 2026-10-12: the release runs in namespace jlab-player with both Deployments Ready, and NOT ONE of the cluster's 23 Ingress objects is in that namespace - neither this chart's nor the subchart's. What publishes it instead could not be determined: this workspace's identity may not list httproutes or secrets, so neither a hand-written HTTPRoute nor the release's Helm values are readable. Recorded as the open question it is, in values.yaml and in the overlay, rather than guessed at. Minor: additive, defaults unchanged. Empty helm template diff against origin/main with ci/ytdlp-web-player/test-values.yaml. Part of #114 |
||
|
|
b10800da2a
|
feat(homeassistant): Gateway-API as an alternative to the Ingress
Group 3 of the #114 sweep, and the one chart where the convention had to bend rather than the chart. homeassistant is the DOCUMENTED INGRESS EXCEPTION (README): upstream's flexible values, a list of hosts each with its own list of paths, instead of the repo's required domain/clusterIssuer pair. The exception stays - nothing here unifies it. NO httpRoute.hostnames KEY, which is the freesailarr shape from group 2 and for the same reason: there is no ingress.domain to fall back to, and a chart-global hostname list could not say which host entry - and therefore which set of paths - it belonged to. So httpRoute carries only enabled and parentRefs, the hostnames come from ingress.hosts, and the chart renders ONE HTTPRoute PER ENTRY with one rule per path of that entry. One object with several hostnames would be wrong, not merely different: an HTTPRoute's rules apply to every hostname of the object, and per-host paths are this chart's whole point. The schema rejects httpRoute.hostnames outright, so the absence is enforced rather than merely intended. ingress.hosts is therefore read in BOTH modes and is not an Ingress-only value. ingress.className, ingress.tls and ingress.annotations are not read in Gateway mode: class and certificate belong to the Gateway's listener, and there is no annotation channel. pathType IS THE SHARP EDGE, AND THE CHART'S DEFAULT IS THE AWKWARD VALUE. Ingress has Exact/Prefix/ImplementationSpecific; the Gateway API has Exact/PathPrefix/RegularExpression. Exact and Prefix map cleanly. ImplementationSpecific - the default in values.yaml - means "ask the controller" and has NO counterpart, so: - on "/" it renders as PathPrefix. Every controller treats a root ImplementationSpecific path as a prefix match, so the chart's own default works in this mode unchanged (verified: the untouched default renders a valid route). - on ANY OTHER path the render ABORTS, naming the value and asking for Prefix or Exact. In ingress-nginx - this chart's default class - an ImplementationSpecific sub-path is a REGEX, and PathPrefix is not one. Mapping it silently would change how the path matches while the render stayed green, which is exactly the class of defect #118 was. RegularExpression is deliberately not offered as an escape hatch: its support level is Implementation-specific, which decision 1 of #114 forbids. NOTHING WAS MEASURED ON THE LIVE CLUSTER FOR THIS CHART, because there is nothing to measure: homeassistant is NOT DEPLOYED in jk-cluster (checked 2026-10-12 - 23 Ingress objects cluster-wide, none this chart's, and no workload of it in any namespace). So it is the one chart of the six with no operator-supplied annotation to lose. values.yaml and NOTES.txt say that as an absence of evidence about this installation rather than as a property of the chart. Route NAMES carry the entry's INDEX, not its hostname - a hostname is not a dependable object-name fragment. Consequence named in values.yaml and in the template: reordering ingress.hosts renames routes and therefore recreates rather than edits them. Harmless, since a route holds no state and no certificate. ci/httproute/homeassistant.yaml has to SUPPLY HOSTS, which no other overlay in the sweep does, and it adds coverage the install-test never had: this chart's ingress.enabled defaults to FALSE and the fixture does not switch it on, so no routing object was exercised at all before. Two entries on purpose - a plain one, and one with two paths including an Exact - because that second entry is what proves the chart renders one object per host rather than one object with several hostnames. 5 assertions over 2 routes and 3 rules. NOTES.txt asks $published where it asked ingress.enabled, and the trusted_proxies section now warns that moving from an Ingress to a Gateway CHANGES the proxy's pod network: a list carried over unchanged turns every request into a 400 if the CIDR differs, or silently keeps attributing every client to one address if it does not. Minor: additive, defaults unchanged. Empty helm template diff against origin/main, both with the fixture as it stands and with ingress.enabled=true. Part of #114 |
||
|
|
3e969c5659
|
feat(stalwart): Gateway-API as an alternative to the Ingress
Group 3 of the #114 sweep. ONE route - not one per node group. #114 describes this chart as rendering "per node group"; its SERVICES and StatefulSets are, the routing object is not and must not be. The route targets exactly ingress.nodeGroup's own http Service and keeps both of the Ingress's guards, because the WebUI's setup and management screens are not safe to spread over a group that may not run the management tasks. ingress.nodeGroup is therefore shared by both modes: it names where the management surface is published from, which is not a property of the Ingress. THE MAIL CERTIFICATE STAYS, and this is the only chart in the sweep where anything of the TLS path does. stalwart.certificate.yaml covers STARTTLS on 25 and implicit TLS on 465/993; those connections pass through neither an ingress controller nor a Gateway. So in Gateway mode ingress.domain is still required (the certificate's dnsNames read it), ingress.clusterIssuer is still required unless certificate.clusterIssuer is set, and the Certificate must NOT be removed as part of a migration - deleting it drops SMTP and IMAP back to Stalwart's self-signed certificate, with clients seeing a name mismatch while every Kubernetes object stays green. Verified in the render: Gateway mode emits Certificate + HTTPRoute and no Ingress. WHAT THIS MODE DROPS - AND HERE IT BREAKS A SECOND CHART OF THE SAME GROUP. Measured on 2026-10-12, the live Ingress on mail.bekemeyer-klein.de carried, all of it operator-supplied through ingress.annotations (this chart sets none of it): enable-cors: true cors-allow-origin: https://webmail.bekemeyer-klein.de cors-allow-methods: GET, POST, OPTIONS cors-allow-headers: Authorization, Content-Type That origin is BULWARK, the webmail chart in this same group. Without the CORS configuration the browser-side webmail cannot call this server's JMAP endpoint at all, and the failure is entirely client-side: a console error, nothing in the server's logs. AND "no counterpart" WOULD HAVE BEEN WRONG. Measured by grepping the CRDs this repo pins (gateway-api v1.6.3 standard channel, ci/gateway-api-crds.sh) rather than inferred: HTTPRoute's filters carry a full 'cors' field (GEP-1767) with allowOrigins/allowMethods/allowHeaders/allowCredentials/maxAge, and its documented level - verbatim from that CRD - is "Support: Extended". responseHeaderModifier is in the same CRD and is also Extended, not Core. The chart still does not render it, for three reasons in order: 1. The chart has NO CORS values. The live configuration is the operator's, and the chart sets no CORS annotations in Ingress mode either - so rendering a filter means inventing a values surface that would exist in Gateway mode only. That asymmetry is its own defect and its own work. 2. Extended support is not support. A rejected route is rejected WHOLE, which would take the WebUI and JMAP down together (the reasoning #119 reached for statusCode); and the observed failure is quieter - Cilium accepts filters.type: CORS and forwards the preflight upstream anyway (cilium/cilium#45272), i.e. Accepted: True with the behaviour absent. 3. Half a CORS is worse than none, and the translatable half is the useless one HERE. A responseHeaderModifier can add the Access-Control-Allow-* headers; the PREFLIGHT has to be answered BY THE GATEWAY, which the CORS filter's own contract states and a bare header filter cannot do. Because the allowed headers include Authorization, EVERY webmail request is preflighted - so the expressible half is exactly the half that does not help. Named, not rebuilt (decision 2 of #114), in values.yaml, in the schema and in the Gateway branch of NOTES.txt, with the operational instruction: configure CORS at the Gateway with that implementation's own support BEFORE moving this host, and test it from the webmail in a browser - curl sends no preflight, so it passes while the webmail fails. NOTES.txt now asks $published (ingress.enabled OR httpRoute.enabled) where it asked ingress.enabled, and two paragraphs that named the ingress controller specifically now name the Gateway in that mode: the auto-ban step (switching API does not avoid or reset it - one proxy address instead of another) and the NetworkPolicy paragraph, where the namespace to allow is the Gateway's data plane rather than an ingress controller's. Minor: additive, defaults unchanged. Empty helm template diff against origin/main with ci/stalwart/test-values.yaml. Part of #114 |
||
|
|
e54cfe5fdd
|
feat(matrix-synapse): Gateway-API as an alternative to the Ingress
Group 3 of the #114 sweep. TWO HTTPRoutes - and the shape is not the one the issue described. #114 says this chart renders its Ingresses in a LOOP OVER SEVERAL APPS. It does not; that is freesailarr. It renders ONE Ingress object with TWO HOST rules: the homeserver on ingress.domain at "/", and the delegation nginx on matrix.delegation.serverUrl at "/.well-known/matrix". An HTTPRoute's spec.hostnames applies to the WHOLE object, so two hostnames that want different paths and different backends have to be two objects. That is the only faithful translation, not a stylistic choice. httpRoute.delegation.hostnames deliberately does NOT fall back to httpRoute.hostnames. The server name is what REMOTE homeservers resolve and is a different thing from the homeserver's own hostname; the migration order - homeserver host first, server name last - depends on telling them apart, and a chart-global hostname list covering both would erase the distinction. httpRoute.delegation.parentRefs falls back to httpRoute.parentRefs, which matters because the server name is usually an apex domain behind a different Gateway. ONE THING THIS MODE IMPROVES, and it is the awkward part of Matrix delegation. Under ingress-nginx two Ingress objects cannot both claim one hostname - the admission webhook rejects the second - which is why the delegation rule has to live inside this chart's single Ingress object even though the apex domain usually belongs to a website. HTTPRoutes attached to the same listener for the same hostname are merged by the implementation, longest match winning, so the /.well-known/matrix route can sit beside whatever owns that apex. WHAT THIS MODE DROPS, MEASURED ON THE LIVE OBJECT ON 2026-10-12 - and ONE OF THE TWO IS NOT WHAT IT LOOKS LIKE. The live Ingress carried, all of it operator-supplied through ingress.annotations (this chart sets none of it): nginx.ingress.kubernetes.io/proxy-body-size: 50m nginx.org/client-max-body-size: 50m nginx.ingress.kubernetes.io/configuration-snippet: | more_set_headers "X-Forwarded-For $remote_addr"; more_set_headers "X-Forwarded-Proto $scheme"; more_set_headers "Host $host:$server_port"; 1. THE BODY SIZE IS THE REAL LOSS. Synapse's media repository takes uploads; its own ceiling is max_upload_size in homeserver.yaml, which this chart cannot see because the whole config arrives from 1Password. The effective limit is the smaller of the two, and in this mode the proxy half becomes the GATEWAY'S default - neither number anyone can point at is necessarily the one that decides. No counterpart in the Gateway API, so per decision 2 of #114 it is documented, not rebuilt. nginx.org/client-max-body-size is the same limit spelled for NGINX Inc's controller, a hedge across both implementations, and it goes for the same reason. (#114's own measured table did not list matrix-synapse under proxy-body-size at all; the live object has it.) 2. THE SNIPPET IS NOT LOAD-BEARING, which took checking rather than assuming. It looks like what Synapse's x_forwarded setting depends on - client IP for rate limiting, and federation. It is not: more_set_headers is the headers-more module's OUTPUT-header directive ("Replaces (if any) or adds (if not any) the specified output headers"), the request-side directive being more_set_input_headers. So those three headers are added to the RESPONSE TO THE CLIENT; Synapse never sees them, and what it reads with x_forwarded: true is the REQUEST's X-Forwarded-For, which ingress-nginx supplies itself. The Gateway API's ResponseHeaderModifier could not rebuild it anyway (it knows no nginx variables), and should not: NOTES.txt says in as many words not to re-create it and not to read its absence as the cause of a rate-limiting or federation problem. Stated as what it is - a reading of the directive's documented semantics. Nothing here observed a Synapse instance with and without the snippet. NOTES.txt now asks $published (ingress.enabled OR httpRoute.enabled) wherever it used to ask ingress.enabled - the server-name/serverUrl mismatch warning, the "clients work fine" aside and the scaled-to-zero paragraph were all really asking whether anything publishes the release. Minor: additive, defaults unchanged. Empty helm template diff against origin/main with ci/matrix-synapse/test-values.yaml. Part of #114 |
||
|
|
9625d8b182
|
feat(bulwark): Gateway-API as an alternative to the Ingress
Group 3 of the #114 sweep. One route, one rule, at the root - and THREE EXPECTATIONS ABOUT THIS CHART DID NOT SURVIVE CONTACT WITH IT: - #114 says bulwark has TWO Ingress objects. It has ONE. keycloak has two. - #114's measured table lists bulwark under rewrite-target. It has no rewrite-target annotation and CANNOT have one: NEXT_PUBLIC_BASE_PATH is compiled into the bundle, so the application cannot be served under a sub-path, and stripping a prefix at the proxy would yield a page that loads and then fetches everything from the wrong place. Every occurrence of the string "rewrite-target" in this chart - values.yaml, the schema, the Ingress template - is an explanation of exactly that. A grep for the string finds the refutations and counts them as occurrences. - ci/run-httproute-match-test.sh said in its header that bulwark was the second rewrite-target chart and would meet the #118 prefix/redirect assurance when group 3 landed. Group 3 landed and it does not: with no URLRewrite on a prefix other than "/", the assurance's precondition is never met here. The header is corrected rather than left standing, and it now says freesailarr is the only chart the pairing applies to. So no redirect rule is added, and none is needed. WHAT THIS MODE DROPS, MEASURED ON THE LIVE OBJECT rather than inferred. On 2026-10-12 the live Ingress on webmail.bekemeyer-klein.de carried nginx.ingress.kubernetes.io/proxy-body-size: 25m supplied by the OPERATOR through ingress.annotations - values.yaml recommends exactly that value and explains why the chart cannot pick it, so the chart sets nothing and a reader of this repository alone would not find it. It is the single annotation this installation depends on, and the Gateway API has no body-size counterpart: implementation policy, not routing. Per decisions 1 and 2 of #114 it is NAMED, not rebuilt - in values.yaml, in the schema and in the Gateway branch of NOTES.txt, in this application's own terms: an attachment above the Gateway's default is rejected before Bulwark sees a byte, the 413 comes from the proxy, and it arrives after the user has written the mail and attached the file. CORRECTED CLAIM, the same one garage and calibre-web-automated needed: the Ingress template, the bulwark.publicUrl helper and the schema all stated that nothing outside the Ingress template FUNCTIONALLY reads ingress.domain. The HTTPRoute reads it, so the claim held only while both modes were off. All three now say so, and the route carries a required() of its own. bulwark.publicUrl follows the ACTIVE mode instead of always printing ingress.domain. The two differ exactly when httpRoute.hostnames is set, which is how a migration off the Ingress runs - and printing the old host there would hand the operator an OIDC redirect URI for the hostname being migrated away from, a failure that surfaces as a rejected login at the identity provider and says nothing about this chart. Minor: additive, defaults unchanged. Empty helm template diff against origin/main with ci/bulwark/test-values.yaml. Part of #114 |
||
|
|
d0f0d49a93
|
feat(keycloak): Gateway-API as an alternative to the two Ingresses
Group 3 of the #114 sweep, first chart. keycloak has TWO Ingress objects - the public hostname and the admin hostname - so it gets TWO HTTPRoutes, with httpRoute.admin.parentRefs falling back to httpRoute.parentRefs exactly as ingress.admin.className falls back to ingress.className: "which data path" is a property of the Gateway in this API, not of the route, so an internal Gateway here is what an internal ingress class is in the other mode. The admin route stays gated on ingress.admin.enabled, because that value is not an Ingress switch - it also decides whether KC_HOSTNAME_ADMIN is set on the Deployment, i.e. whether the admin hostname exists at all. Switching to Gateway mode therefore must not require turning it off. FIXED ALONG THE WAY: the admin Ingress did not hang off ingress.enabled. Measured on origin/main, `helm template -f ci/keycloak/test-values.yaml --set ingress.enabled=false` emitted kc-keycloak-admin-ingress - so "ingress.enabled: false" did not mean "no Ingress", and ingress.clusterIssuer stayed required for an object nobody asked for. It also made Gateway mode impossible: the admin Ingress would have gone on claiming the admin host next to the admin HTTPRoute, one host over two data paths, which is the exact conflict the mutual exclusion exists to prevent. The default render is unchanged; what changed is a combination that could not be coherent. WHAT THIS MODE DROPS, MEASURED ON THE LIVE CLUSTER rather than inferred. On 2026-10-12 BOTH of this chart's Ingress objects in jk-cluster carried nginx.ingress.kubernetes.io/proxy-buffer-size: 16k nginx.ingress.kubernetes.io/proxy-buffers-number: 4 set by the OPERATOR through ingress.annotations and ingress.admin.annotations - the chart sets neither, which is why reading the chart alone does not reveal them. The Gateway API has no annotation channel and no counterpart for proxy buffer sizing; it is implementation policy, not routing. Per decision 1 of #114 the chart renders no policy object, so this is NAMED and not rebuilt, in values.yaml, in the schema and in the Gateway branch of NOTES.txt, with the consequence spelled out in Keycloak's own terms: large headers (JWTs, SAML assertions, long Set-Cookie chains on the login flow) make a too-small buffer answer 502 at the PROXY while Keycloak logs the request as successful. Named and deliberately not touched: four further Ingress objects in the same namespace (keycloak-realm-*-ingress, with app-root) belong to the Helm release jk-cluster-config-fleet-cd-keycloak-realm-ingress, i.e. to another repository. They point at this release's Service under four further hostnames. This chart renders exactly the two routes its own two Ingresses cover. Minor: additive, defaults unchanged. Empty helm template diff against origin/main with ci/keycloak/test-values.yaml. Part of #114 |
||
|
|
f202fdc3fc
|
fix(freesailarr): redirect the bare /qbittorrent prefix in Gateway mode
Validate Helm Charts / Detect changed charts (pull_request) Successful in 5s
Validate Helm Charts / Resource quantities (pull_request) Successful in 15s
Validate Helm Charts / Selector match (pull_request) Successful in 14s
Install-test / Install-test freesailarr (pull_request) Successful in 4m19s
Validate Helm Charts / Install-test (pull_request) Successful in 4m19s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 12s
Publish Freesailarr Helm Chart / Publish Freesailarr Helm Chart (push) Successful in 12s
SonarQube Analysis / SonarQube Analysis (push) Successful in 48s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 14s
Validate Helm Charts / Build freesailarr (pull_request) Successful in 8s
Validate Helm Charts / Trivy config freesailarr (pull_request) Successful in 22s
qBittorrent has no base-path setting, so the chart serves it under a prefix and strips that prefix before the request reaches it. For the slashed form that is correct. For the BARE form - /qbittorrent, no trailing slash - the request is rewritten to "/" and qBittorrent answers 200 with its login page, while the browser's document URL keeps the prefixless form. Every relative link in that page therefore resolves against the host root, and on the shared host the root is seerr: the login form comes up unstyled and does nothing while a different application answers its assets and XHRs. Nothing reports an error at any level. The shared-domain HTTPRoute now carries a second rule for qBittorrent: an Exact match on qbittorrent.ingress.path with a RequestRedirect filter that replaces the full path with the slashed form, statusCode 301. The Gateway API ranks an Exact path match above every prefix match, so it wins over both qBittorrent's own PathPrefix rule and seerr's "/" wherever it is emitted - the rules' length-descending order stays a rendering convention. 301 rather than 308 because only 301 and 302 are Core in that API; an Extended value an implementation declines makes it set Accepted=False on the WHOLE route, which would take the four neighbouring apps' routing down with it, and that would also contradict decision 1 of Gitea #114. statusCode is written out because it defaults to 302. The rule is guarded on the app being qbittorrent and its path not being "/", and it reads the path from the value, never a literal. The INGRESS BRANCH IS UNCHANGED, deliberately. The remedy there is an nginx configuration-snippet annotation, and the chart cannot ship it: snippets are ignored unless the controller runs with allow-snippet-annotations=true, off by default since ingress-nginx 1.9 and only switched on by accepting annotation risk level "Critical". A shipped annotation would be a silent no-op on every cluster keeping the default. The working snippet, that price, and why a third Ingress object with pathType: Exact is not a substitute - buildLocation in ingress-nginx controller-v1.14.5 returns a regex location in its enforceRegex branch before reaching the PathTypeExact branch, and enforceRegex is computed once per nginx server over every location of the hostname, one of which carries rewrite-target - are all written out in values.yaml instead. ci/run-httproute-match-test.sh gains a third assertion, per HTTPRoute object: a rule stripping a prefix with ReplacePrefixMatch requires a rule in the same route matching that path Exactly and carrying a RequestRedirect filter. This is what stops the gap reappearing; bulwark is the second rewrite-target chart and meets the same check when group 3 of Gitea #114 lands. 37 assertions over 15 charts, up from 36. Both fail paths were exercised: the rule deleted, and the redirect pointed at a path other than the one stripped. Minor and not major: with the Ingress branch untouched and httpRoute.enabled defaulting to false, no existing release's render changes by a byte and no value becomes required. Shown by an empty helm template diff against origin/main in both Ingress shapes, shared-domain and per-app hosts. Fixes #118 |
||
|
|
bd1fdc09a0
|
feat: Gateway-API as an alternative to the Ingress in five annotation charts
Validate Helm Charts / Detect changed charts (pull_request) Successful in 4s
Validate Helm Charts / Resource quantities (pull_request) Successful in 14s
Validate Helm Charts / Selector match (pull_request) Successful in 14s
Validate Helm Charts / Build freesailarr (pull_request) Successful in 7s
Validate Helm Charts / Build paperless-ngx (pull_request) Successful in 8s
Validate Helm Charts / Trivy config freesailarr (pull_request) Successful in 24s
Validate Helm Charts / Trivy config outline (pull_request) Successful in 17s
Validate Helm Charts / Trivy config zipline (pull_request) Successful in 19s
Install-test / Install-test calibre-web-automated (pull_request) Successful in 2m11s
Install-test / Install-test freesailarr (pull_request) Successful in 3m55s
Install-test / Install-test outline (pull_request) Successful in 2m44s
Install-test / Install-test paperless-ngx (pull_request) Successful in 3m52s
Install-test / Install-test zipline (pull_request) Successful in 2m6s
Validate Helm Charts / Install-test (pull_request) Successful in 14m50s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 14s
Validate Helm Charts / Build outline (pull_request) Successful in 7s
Validate Helm Charts / Build zipline (pull_request) Successful in 8s
Validate Helm Charts / Build calibre-web-automated (pull_request) Successful in 7s
Validate Helm Charts / Trivy config calibre-web-automated (pull_request) Successful in 19s
Validate Helm Charts / Trivy config paperless-ngx (pull_request) Successful in 19s
Group 2 of the #114 sweep: the charts whose behaviour hangs off an nginx annotation - calibre-web-automated, outline, paperless-ngx, zipline (proxy-body-size) and freesailarr (rewrite-target). Each gets the form the pilot (cyberchef, PR #115) fixed and group 1 (PR #116) repeated: an httpRoute block off by default, a template rendering ONLY an HTTPRoute with parentRefs - no Gateway, no gatewayClassName, no Certificate, no implementation-specific policy object - render aborts on both modes at once (naming both value paths), on an empty parentRefs and on a missing hostname, empty hostnames meaning [ingress.domain], a schema entry closed against unknown keys with an open parentRef definition, and a NOTES.txt that names the active mode and what the mode costs. Minor, additive, defaults unchanged: `helm template` with each chart's CI values is byte-identical to origin/main for all five. The upload limit is DOCUMENTED, not rebuilt (decision 2 of the issue). The Gateway API knows no body size; a policy object would bind the chart to one implementation. So the Gateway branch of NOTES.txt says, per chart and in the chart's own terms, that the value is NOT ENFORCED and names where to set it: an e-book library (calibre), an upload service (zipline), the web-UI half of the document intake while the consume directory keeps no limit at all (paperless), and the open #12 pair (outline). Four findings that were not in the plan, each argued in the file it changes: freesailarr gated freesailarr.sharedIngressApps on ingress.enabled, and that list is read by the <APP>__SERVER__URLBASE variables as well as by the routing objects. In Gateway mode it would have come out EMPTY while the route served radarr, sonarr and prowlarr under their prefixes: UrlBase unset, assets and API calls linked at the host root, broken UI behind a Ready pod. The helper's two guards (seerr/mediathekarr only at "/", no duplicate paths) would have gone unenforced there too. Fixed with freesailarr.published (ingress.enabled OR httpRoute.enabled) - the same shape as sure.publishedOverTls in group 1, and identical to ingress.enabled while httpRoute.enabled is false. Measured: on origin/main with ingress.enabled: false the render emits ZERO URLBASE variables; with this change in Gateway mode it emits three. paperless-ngx was the last chart in the repository rendering its Ingress UNCONDITIONALLY. Without an ingress.enabled switch there is no second value path for the mutual-exclusion abort to name, and the Ingress plus its unconditional required() on clusterIssuer would have kept rendering in Gateway mode - a value demanded for nothing, which is issue #114's "Folge 1" and README guideline 12. The switch was added, defaulting to true, so the render is unchanged. paperless-ngx PAPERLESS_URL had no required() of its own: it relied on the Ingress template's, which stops firing the moment the Ingress is switchable. With httpRoute.hostnames set and ingress.domain empty it would have rendered "https://" and refused every login POST as a CSRF failure. It now carries its own required(), naming what the value is for. calibre-web-automated claimed, in values.yaml and in its schema, that ingress.domain is read nowhere but in the Ingress template. The HTTPRoute reads it, so the claim now holds only while both modes are off. Corrected rather than left standing - the same correction garage needed in group 1. freesailarr is where the Gateway API is LESS machinery than what it replaces, not a translation of it. rewrite-target applies per Ingress OBJECT, so qBittorrent (no base path, prefix must be stripped) needed a second Ingress on the shared host, which then had to repeat the first object's TLS secret name, omit the cert-manager annotation conditionally so only one Certificate is requested, and match an ImplementationSpecific regex path for the rewrite's capture group. A URLRewrite filter applies per RULE: one route, one rule per app, the filter on qBittorrent's rule, a plain PathPrefix. All four crutches gone. It is also the only chart without httpRoute.hostnames - it has no ingress.domain either, and a chart-global hostname list could not say which of the ten apps it belonged to, so the hostnames stay in ingress.host and <app>.ingress.host. CI: one render overlay per chart under ci/httproute/, so the assertion is not vacuum-green - ci/run-httproute-match-test.sh goes from 20 held assertions over 10 charts to 36 over 15. freesailarr's overlay moves bazarr onto a host of its own, so the shared-host route AND the per-app route are both rendered; without that the per-app branch would be exercised by nothing. The install-test fixtures stay in Ingress mode: the install runs one configuration per chart, cyberchef already pays that price against the real Gateway-API CRDs, and for paperless-ngx that fixture is the only install of the valkey-subchart path anywhere. README also carries a stale figure fixed here: the HTTPRoute test was still documented at the pilot's 4 assertions over 2 charts, although group 1 had taken it to 20 over 10. Not proven, by construction: that traffic flows. No Gateway controller runs in CI. Not run locally either: k3d, Docker, Trivy. The Gateway branch of NOTES.txt is EXECUTED by the render test (a template error in it fails the job, verified) but its text is printed by no pipeline stage - helm template does not emit notes, and the install-test installs in Ingress mode. Part of #114 |
||
|
|
33d7cedbab
|
feat: Gateway-API as an alternative to the Ingress in eight charts
Validate Helm Charts / Selector match (pull_request) Successful in 14s
Validate Helm Charts / Build bentopdf (pull_request) Successful in 9s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 17s
Validate Helm Charts / Trivy config bentopdf (pull_request) Successful in 19s
Validate Helm Charts / Build garage (pull_request) Successful in 8s
Validate Helm Charts / Build rallly (pull_request) Successful in 7s
Validate Helm Charts / Build searxng (pull_request) Successful in 6s
Validate Helm Charts / Build sure (pull_request) Successful in 7s
Validate Helm Charts / Build urlaubsverwaltung (pull_request) Successful in 6s
Validate Helm Charts / Trivy config garage (pull_request) Successful in 18s
Validate Helm Charts / Trivy config patchmon (pull_request) Successful in 16s
Validate Helm Charts / Trivy config rallly (pull_request) Successful in 18s
Install-test / Install-test bentopdf (pull_request) Successful in 1m10s
Validate Helm Charts / Trivy config sure (pull_request) Successful in 18s
Validate Helm Charts / Trivy config voucher-vault (pull_request) Successful in 19s
Install-test / Install-test rallly (pull_request) Successful in 1m39s
Install-test / Install-test sure (pull_request) Successful in 2m39s
Install-test / Install-test voucher-vault (pull_request) Successful in 2m9s
Validate Helm Charts / Install-test (pull_request) Successful in 13m15s
Install-test / Install-test urlaubsverwaltung (pull_request) Successful in 1m50s
Publish BentoPDF Helm Chart / Publish BentoPDF Helm Chart (push) Successful in 10s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 9s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 9s
Publish Rallly Helm Chart / Publish Rallly Helm Chart (push) Successful in 10s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 10s
Publish SearXNG Helm Chart / Publish SearXNG Helm Chart (push) Successful in 10s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 9s
Publish Sure Helm Chart / Publish Sure Helm Chart (push) Successful in 10s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 10s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 10s
Publish Patchmon Helm Chart / Publish Patchmon Helm Chart (push) Successful in 9s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 10s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 10s
SonarQube Analysis / SonarQube Analysis (push) Successful in 43s
Validate Helm Charts / Detect changed charts (pull_request) Successful in 5s
Validate Helm Charts / Resource quantities (pull_request) Successful in 15s
Validate Helm Charts / Build patchmon (pull_request) Successful in 7s
Validate Helm Charts / Build voucher-vault (pull_request) Successful in 7s
Validate Helm Charts / Trivy config searxng (pull_request) Successful in 22s
Validate Helm Charts / Trivy config urlaubsverwaltung (pull_request) Successful in 17s
Install-test / Install-test garage (pull_request) Successful in 1m17s
Install-test / Install-test patchmon (pull_request) Successful in 1m24s
Install-test / Install-test searxng (pull_request) Successful in 1m7s
Publish Garage Helm Chart / Publish Garage Helm Chart (push) Successful in 9s
Publish Urlaubsverwaltung Helm Chart / Publish Urlaubsverwaltung Helm Chart (push) Successful in 10s
Publish VoucherVault Helm Chart / Publish VoucherVault Helm Chart (push) Successful in 10s
Group 1 of the #114 sweep: the charts that render exactly one Ingress object and set no behaviour-bearing nginx annotation - bentopdf, garage, patchmon, rallly, searxng, sure, urlaubsverwaltung, voucher-vault. Each gets the form the pilot (cyberchef, PR #115) fixed and template-chart carries: an httpRoute block (enabled/parentRefs/hostnames, off by default), a template rendering ONLY an HTTPRoute with parentRefs - no Gateway, no gatewayClassName, no Certificate, no implementation-specific policy object - render aborts on both modes at once (naming both value paths), on an empty parentRefs and on a missing hostname, empty hostnames meaning [ingress.domain], a schema entry closed against unknown keys with an open parentRef definition, and a NOTES.txt that names the active mode and what the mode costs. Minor, additive, defaults unchanged: `helm template` with each chart's CI values is byte-identical to origin/main for seven of the eight, and for sure identical in every rendered value (the diff is two YAML comment lines). Three findings that were not in the plan, each argued in the file it changes: sure derived RAILS_ASSUME_SSL/RAILS_FORCE_SSL from ingress.enabled alone. Gateway mode sets that false by construction while TLS is still terminated at the Gateway, so both flags would have gone false: insecure cookies, no HSTS, http:// links, with a Ready pod and a working UI as the only symptom. They now read sure.publishedOverTls (ingress.enabled OR httpRoute.enabled), which is ingress.enabled while httpRoute.enabled is false - hence still Minor. garage claimed, in its Ingress template and its schema, that nothing outside that template reads ingress.domain. The HTTPRoute reads it, so the claim now holds only while both modes are off. Corrected rather than left standing. garage also depends on an upload limit it does not set: its annotations comment tells the operator to set proxy-body-size: "0" because ingress-nginx truncates S3 uploads at 1 MB. proxy-body-size has no Gateway-API counterpart, so its Gateway-mode NOTES prints that the limit is the Gateway's. README's list of affected charts named five and now names garage as a sixth. clusterIssuer needed no change in any of the eight: its only required() sits inside the Ingress template behind ingress.enabled, the same no-op the pilot found. Annotation guidance that stops applying in Gateway mode moved into the Gateway branch of NOTES.txt for bentopdf (X-Robots-Tag) and searxng (forward-auth). CI: one render overlay per chart under ci/httproute/, so the assertion is not vacuum-green - ci/run-httproute-match-test.sh goes from 4 held assertions over 2 charts to 20 over 10. The install-test fixtures stay in Ingress mode: the install runs one configuration per chart, cyberchef already pays that price against the real Gateway-API CRDs, and for garage the Ingress is the only place its Service port is referenced by NAME. Not proven, by construction: that traffic flows. No Gateway controller runs in CI. Not run locally either: k3d, Docker, Trivy. Part of #114 |
||
|
|
50f683776e
|
feat(cyberchef): Gateway-API as an alternative to the Ingress (pilot)
Validate Helm Charts / Detect changed charts (pull_request) Successful in 4s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 13s
Validate Helm Charts / Build cyberchef (pull_request) Successful in 6s
Validate Helm Charts / Trivy config cyberchef (pull_request) Successful in 18s
Validate Helm Charts / Trivy config template-chart (pull_request) Successful in 17s
Install-test / Install-test cyberchef (pull_request) Successful in 1m0s
Validate Helm Charts / Install-test (pull_request) Successful in 1m0s
Validate Helm Charts / Selector match (pull_request) Successful in 12s
Validate Helm Charts / Resource quantities (pull_request) Successful in 14s
Validate Helm Charts / Build template-chart (pull_request) Successful in 7s
The Gateway-API pilot of Gitea #114: one chart, the convention, the CI that holds it. The remaining 20 charts follow in their own PRs. The form, additive and off by default: httpRoute: enabled: false # counterpart to ingress.enabled parentRefs: [] # the Gateway - a platform object hostnames: [] # empty = [ingress.domain] Only HTTPRoute with parentRefs is rendered: no Gateway, no gatewayClassName, no Certificate, and no implementation-specific policy object. That subset is identical on nginx-gateway-fabric, Cilium, Envoy Gateway and Istio, which is what lets the implementation be chosen later without touching the charts again. Class and certificate move to the platform. Four guards, all against silent failures: - mutually exclusive with ingress.enabled, with a render abort naming BOTH value paths. A migration runs over a second hostname, not in parallel. - parentRefs mandatory in Gateway mode, render abort on the empty list: a route without a parent is created, is schema-valid, and routes nothing. - hostnames empty means [ingress.domain], so the hostname has one source. - the mode is a VALUE, not auto-detection: .Capabilities.APIVersions is unreliable at template time and a missing CRD fails at install time. ingress.clusterIssuer was already only read inside the Ingress template, so the "conditionally required" half of #114 needed no change in this chart - checked rather than assumed, and it has to be re-checked per chart in the sweep. CI, both halves, because neither covers the other: - the install-test installs cyberchef in GATEWAY mode against the REAL Gateway-API CRDs (standard channel v1.6.3, pinned and Renovate-tracked, applied --server-side). A schema-less stub would accept a malformed route and prove only that something was created. The Ingress path of this one chart is no longer installed; it is a built-in type that never routed in k3d anyway, and helm lint renders this chart's defaults - Ingress mode - on every PR. - ci/run-httproute-match-test.sh, over every chart on every PR: every backendRefs entry must name a Service, on a port, that the SAME render creates, and parentRefs must not be empty. The CRDs accept both of those mistakes, so the schema cannot catch them, and the only report would be a route status no controller writes in CI. What stays unprovable, and is written down instead of implied: that traffic flows. No Gateway controller runs in k3d. Minor: additive with unchanged defaults, proven by an empty helm template diff against origin/main for the default values. Part of #114 |
||
|
|
6ade74db80
|
feat(stalwart): spread each node group over nodes and give each one a PDB
A two-replica node group was redundancy on paper: nothing kept both pods off one node, and no PodDisruptionBudget kept a drain from taking both at once. Both objects are now rendered per node group, gated on that group's COMPUTED replica count being >= 2. The distribution is a promise, not an effort: maxSkew 1 on kubernetes.io/hostname with whenUnsatisfiable DoNotSchedule, selecting only this group's own pods, so an "mx" and a "full" pod may share a node. The selector comes from the chart (stalwart.groupLabels), never from the operator - a spread constraint whose selector matches no pod is trivially satisfied, distributes nothing and reports nothing. nodeAffinityPolicy: Ignore is set explicitly, and that is the load-bearing detail. Its API default is Honor, under which a narrowing nodeSelector shrinks the set of counted domains until both replicas fit into one: skew 0, both pods co-located, no Pending, no event, and a selector that is correct - so no check can see it. With Ignore the same narrowing gives skew 2 and the second pod is Pending. A render abort was the alternative and would also have forbidden a nodeSelector spanning five nodes, which is legal. A user entry colliding with the chart's own on (topologyKey, whenUnsatisfiable) aborts the render naming both values, because the API rejects the duplicate with a message about a list index and an apply-based path merges the two over patchMergeKey: topologyKey silently, dropping one selector. Minor rather than major, proved with a diff rather than argued: with the shipped defaults (full: 1, mx: 0) and with ci/stalwart/test-values.yaml, helm template renders byte-for-byte what 1.0.1+upv0.16.24 rendered - both diffs empty. New in CI, over every chart on every PR: ci/run-selector-match-test.sh holds every rendered PodDisruptionBudget selector and every topologySpreadConstraints labelSelector against the labels of the pod templates in the same render. Helm itself is the YAML parser (ci/yaml2json), because the runners have neither pyyaml nor yq and a hand-written parser could read nothing and report nothing. podAffinity/podAntiAffinity are out of scope; matchExpressions-only selectors are reported as not checked rather than counted as passing. Two anti-vacuum guards: a chart with an overlay under ci/selector-match/ that renders no selector fails, and a run that checks no selector at all fails. Fixes #96 |
||
|
|
6e196c26a5
|
test(ci): restore the theme mount - the A/B shows the restart is pre-existing
Validate Helm Charts / Detect changed charts (pull_request) Successful in 3s
Validate Helm Charts / Resource quantities (pull_request) Successful in 14s
Validate Helm Charts / Trivy config keycloak (pull_request) Successful in 12s
Install-test / Install-test keycloak (pull_request) Successful in 3m10s
Validate Helm Charts / Install-test (pull_request) Successful in 3m10s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 7s
Publish Keycloak Helm Chart / Publish Keycloak Helm Chart (push) Successful in 7s
Validate Helm Charts / Build keycloak (pull_request) Successful in 4s
SonarQube Analysis / SonarQube Analysis (push) Successful in 28s
|
||
|
|
8cd0c14e3f
|
test(ci): control run 598 recorded (81s, 0 restarts)
Validate Helm Charts / Detect changed charts (pull_request) Successful in 3s
Validate Helm Charts / Build keycloak (pull_request) Successful in 9s
Validate Helm Charts / Trivy config keycloak (pull_request) Successful in 16s
Install-test / Install-test keycloak (pull_request) Successful in 9m24s
Validate Helm Charts / Resource quantities (pull_request) Successful in 13s
Validate Helm Charts / Install-test (pull_request) Successful in 9m24s
|
||
|
|
71cc545277
|
test(ci): A/B control run - keycloak install-test without the theme mount (temporary)
Validate Helm Charts / Detect changed charts (pull_request) Successful in 3s
Validate Helm Charts / Resource quantities (pull_request) Successful in 8s
Validate Helm Charts / Trivy config keycloak (pull_request) Successful in 10s
Install-test / Install-test keycloak (pull_request) Successful in 2m14s
Validate Helm Charts / Install-test (pull_request) Successful in 2m14s
Validate Helm Charts / Build keycloak (pull_request) Successful in 4s
|
||
|
|
d7067abfab
|
docs(ci): the whole restart series, intermittent on an unchanged spec
Validate Helm Charts / Detect changed charts (pull_request) Successful in 4s
Validate Helm Charts / Resource quantities (pull_request) Successful in 9s
Validate Helm Charts / Build keycloak (pull_request) Successful in 4s
Validate Helm Charts / Install-test (pull_request) Successful in 2m45s
Validate Helm Charts / Trivy config keycloak (pull_request) Successful in 10s
Install-test / Install-test keycloak (pull_request) Successful in 2m45s
|
||
|
|
5e47191252
|
docs(ci): three runs, and the load explanation does not hold
Validate Helm Charts / Detect changed charts (pull_request) Successful in 4s
Validate Helm Charts / Resource quantities (pull_request) Successful in 9s
Validate Helm Charts / Build keycloak (pull_request) Successful in 4s
Validate Helm Charts / Trivy config keycloak (pull_request) Successful in 13s
Install-test / Install-test keycloak (pull_request) Successful in 3m59s
Validate Helm Charts / Install-test (pull_request) Successful in 3m59s
|
||
|
|
34c600f7a8
|
docs(ci): the second run settles it - the keycloak restart was the runner, not the pod spec
Validate Helm Charts / Resource quantities (pull_request) Successful in 10s
Validate Helm Charts / Build keycloak (pull_request) Successful in 4s
Validate Helm Charts / Trivy config keycloak (pull_request) Successful in 10s
Validate Helm Charts / Install-test (pull_request) Successful in 4m2s
Validate Helm Charts / Detect changed charts (pull_request) Successful in 3s
Install-test / Install-test keycloak (pull_request) Successful in 4m2s
|
||
|
|
1cf59f5448
|
docs(ci): record the keycloak install-test run that was slower and restarted once
Validate Helm Charts / Build keycloak (pull_request) Successful in 4s
Validate Helm Charts / Trivy config keycloak (pull_request) Successful in 11s
Validate Helm Charts / Detect changed charts (pull_request) Successful in 3s
Install-test / Install-test keycloak (pull_request) Successful in 2m18s
Validate Helm Charts / Resource quantities (pull_request) Successful in 9s
Validate Helm Charts / Install-test (pull_request) Successful in 2m18s
|
||
|
|
d8b392bf64
|
feat(keycloak): extraVolumes/extraVolumeMounts for custom themes and providers (1.1.0+up26.8.0)
Validate Helm Charts / Resource quantities (pull_request) Successful in 15s
Validate Helm Charts / Build keycloak (pull_request) Successful in 4s
Validate Helm Charts / Trivy config keycloak (pull_request) Successful in 17s
Install-test / Install-test keycloak (pull_request) Successful in 3m9s
Validate Helm Charts / Install-test (pull_request) Successful in 3m9s
Validate Helm Charts / Detect changed charts (pull_request) Successful in 11s
|
||
|
|
b39f36ff5c
|
feat(bulwark): new chart for Bulwark Webmail (1.0.0+up1.12.0)
A JMAP webmail client for the stalwart chart (Gitea #65): StatefulSet, one PVC at /app/data, Service plus headless Service, Ingress at a host root, two OnePasswordItems, three probes, computed limits, replicas plus scaleDown, strict values.schema.json. Everything below was measured against the source at tag 1.12.0 rather than read out of the documentation, which was wrong about three of them. THE DEFINING PROPERTY: THE VOLUME BEATS THE CHART, FOR EVERY VALUE, SILENTLY. configManager.get resolves "admin override > env var > default", the admin dashboard may write any key of CONFIG_ENV_MAP, and nothing reports that it just disconnected the chart - so a helm upgrade can succeed and change nothing. Upstream documents this for the server URL only; it is true of everything. Exactly three values escape it (SESSION_SECRET(_FILE), ADMIN_PASSWORD, BULWARK_TELEMETRY). The value that closes it, bulwark.adminConfigReadOnly, defaults to false because its price is threefold: it freezes the settings policy (and with it the only way to move Web Push off the vendor relay), it freezes an override that ALREADY exists instead of removing it, and on a fresh install it stops admin.json from being written at all - so the admin dashboard never comes up and its login answers HTTP 500 while the pod stays Ready. OIDC-only sign-in, with the real way back out of a lockout: not helm upgrade with OAUTH_ONLY=false (oauthOnly goes through the same precedence chain) but removing the override in the admin dashboard. That is why ADMIN_PASSWORD is set - the dashboard is the door. It generates no random password and logs nothing, so the #65 analogy does not apply. SESSION_SECRET_FILE and OAUTH_CLIENT_SECRET_FILE are both used - the better path garage (#81) and keycloak (#83) did not have - in one projected volume mounted WITHOUT subPath, because both are re-read on every use and a subPath mount is never refreshed, and at mode 0440 because secret files are owned by root and fsGroup only supplies the group. Two backup statements, both at the value: without the session secret the synchronized settings are unreadable out of an intact volume, and if the dashboard's OAuth setup was ever used the client secret sits in config.json on that volume in cleartext. Probes: all three a plain GET on /api/health, with the limit written at the readiness value - it carries process, HTTP acceptance, a free event loop and Next.js routing, and nothing about the reported state or about Stalwart. No ConfigMap, so the checksum/config rule (#82) has nothing to attach to, and values.yaml says so explicitly; the op:// paths are covered by checksum/secret-ref. Renovate: no versioningTemplate, measured rather than assumed. 315 tags in eight shapes (releases, a second v-prefixed family of the same images, an -always flavour, floating minors and names, commit SHAs, betas, attestations) and plain docker versioning excludes all six unwanted ones. Dry runs at 1.12.0, 1.11.0 and 1.10.0 are recorded in the block. Part of #89 - deliberately not "Fixes", because one binding decision of the issue was changed out of necessity and wants ratification rather than an automatic close: the plan specifies HEAD /api/health for all three probes, and a Kubernetes probe cannot send HEAD at all (HTTPGetAction has no method field; the kubelet always issues GET). The alternative, an exec probe, would mean starting a Node process every few seconds in an image that carries no curl and no wget. GET is used instead and is marginally more capable than HEAD, so every limit the issue argues is unchanged - but the decision is the maintainer's to confirm. |
||
|
|
320257c742 |
fix(keycloak): readiness could not fail on the database, and the measured budget
Two corrections found while verifying the first CI run, not from the plan. THE DEFAULT THAT MADE THE READINESS PROBE TOOTHLESS. Keycloak's DATABASE health check is the one row of upstream's "Available Checks" table whose "Requires Metrics" column says yes; Cluster, Graceful shutdown and Keycloak Initialized say no. So with metrics-enabled=false - this chart's default until now - /health/ready answers 200 while the database is gone, and the pod keeps taking logins it cannot serve. The probe path was right and the aggregate behind it contained nothing external, which is the urlaubsverwaltung defect (#218) in a new hiding place: not a health group this time, but an option named for observability, where nobody looks when reviewing a probe. keycloak.metrics.enabled therefore defaults to TRUE, and its comment argues about readiness rather than about scraping. The price is stated at the value: /metrics then answers on the management port, unauthenticated, carrying realm names, client ids and login counters - on a port that is ClusterIP-only and in no Ingress. Switching it off is now documented as a decision about whether readiness can fail at all. The CI fixture keeps it on and says why, so a green run really does touch the database. THE STARTUP BUDGET NOW CARRIES ITS MEASUREMENT. From the install-test of PR #87 on the Gitea docker runner (k3s v1.35.5, two replicas, empty database, image pulled during the run): 71.8 seconds from `helm install` to BOTH pods Ready - first at ~70.8s, second one second later, 0 restarts. The 600s budget stays, with the reason for the headroom written down: everything in that run was the cheap case, and a first install against a real database serializes the second replica behind the first's Liquibase migration lock instead of letting it arrive a second later. AND AN OVERCLAIM OF MY OWN, REMOVED. ci/keycloak/test-values.yaml said a green run proves jdbc-ping found the second replica. It does not: readiness reports one node's own health, so a node that never found its peer is Ready, healthy and alone - and two such nodes are two islands that each believe they are the cluster. What the test does rule out is the louder half, a replica that cannot start or cannot share the database. The Infinispan view has to be read out of the log, which is what step 2 of NOTES.txt tells the operator to do, now with the note that no probe and no install-test replaces that step. Measurement point 4 of the plan therefore stays open. Part of #83 |
||
|
|
4c1cff1744 |
feat(keycloak): new chart for Keycloak (1.0.0+up26.8.0)
Two replicas with cache=ispn, clustered through Keycloak's default jdbc-ping stack: discovery runs over the PostgreSQL that is there anyway, so no headless Service and no JGroups wiring. Deployment rather than StatefulSet - Keycloak 26 keeps everything in the database, user sessions included. The chart's defining property is that it has TWO INIT CONTAINERS and needs both. db, health-enabled and metrics-enabled are Keycloak BUILD options (verified in the 26.8.0 option table), the published image is augmented with their defaults, and supplying different ones at runtime makes Keycloak rewrite /opt/keycloak/lib/quarkus at start - which readOnlyRootFilesystem: true forbids and which upstream describes as a start that "creates an image that is mutable". Not setting db is no way out: the probes alone force it, because health-enabled is a build option too. An empty emptyDir over that path fails as well, because QuarkusEntryPoint loads quarkus/quarkus-application.dat before it dispatches any command, `build` included. So one init container seeds the directory into a volume, the next runs `kc.sh build` into it, and the serving container runs `start --optimized` - every container keeping the full hardening baseline, with no documented deviation to carry. Build options therefore exist in exactly one place, and the asymmetry is enforced rather than trusted: the serving container passes no build option at all, and a build option's variable name in keycloak.env fails the render with a message naming it. Hostname handling is the security-relevant part. KC_HOSTNAME is derived from ingress.domain as a full URL (which upstream requires alongside hostname-admin), hostname-strict stays at its default true, and proxy-headers is set rather than optional. The admin hostname is NOT presented as an access control: upstream states that hostname-admin "does not prevent accessing the Administration REST API endpoints via the frontend URL specified by the hostname option", so the chart renders no /admin deny rule and values.yaml, NOTES.txt and the wiki all say in the same words that restricting that API is reverse-proxy work. The management port 9000 is a containerPort and a ClusterIP Service for in-cluster scraping, and is in no Ingress: it serves /health/* and optionally /metrics without authentication. Liveness uses /health/live and deliberately not /health or /health/ready - those include the database, and a liveness probe on them would restart the whole cluster for as long as the database is away. Also in this change: - First PodDisruptionBudget in this repository, which makes it a convention (README section 11): minAvailable as a value, and rendered only while the computed replica count is >= 2, because minAvailable: 1 with one replica blocks every node drain forever with a message that names the PDB rather than the replica count. - First quay.io image, so quay.io joins the trusted registries in ci/trivy/data/ksv0125.yaml - configured rather than suppressed, so AVD-KSV-0125 keeps being able to say something. - Renovate block with NO version constraint, which is the counter-case to garage and stalwart and is measured rather than assumed: docker versioning already excludes the -N rebuild tags, the floating minors and latest/nightly. Three dry-run fixtures are recorded at the block, and against the committed config 26.7.4 offers exactly 26.7.5 (patch) and 26.8.0 (minor). - No automerge exception, by decision (Gitea #83): the global automerge applies, and the minor-upgrade procedure plus `kc.sh update-compatibility` is documented in NOTES.txt instead. - Two upstream defaults deviated from, each with its reason at the value: db-pool-max-size 20 instead of 100 (two replicas at 100 ask a default PostgreSQL for twice its max_connections), and uid 1000 / gid 0 instead of this repo's 20000/1000, because the image declares keycloak:x:1000:0 and chowns its install tree to that pair. Part of #83 |
||
|
|
79b7e44fdc
|
feat(garage): new chart for the Garage object store
Validate Helm Charts / Build garage (pull_request) Successful in 6s
Validate Helm Charts / Trivy config garage (pull_request) Successful in 16s
Install-test / Install-test garage (pull_request) Successful in 1m15s
Validate Helm Charts / Install-test (pull_request) Successful in 1m15s
Validate Helm Charts / Detect changed charts (pull_request) Successful in 3s
Validate Helm Charts / Resource quantities (pull_request) Successful in 11s
One Garage node with replication_factor = 1, built from template-chart to the style guide rather than copied from upstream's in-tree chart (Gitea #81). The single-node trade is deliberate and its price is named at the value: the factor applies to the metadata as well, where upstream calls a corrupted database "generally not recoverable" and a replicated volume replicates the corruption faithfully. The three countermeasures that work at factor 1 are therefore mandatory, not optional - metadata_fsync = true (off in Garage's defaults), the automatic metadata snapshots, and those snapshots on a second PVC. Storage: PVC 1 carries metadata_dir AND data_dir as two subdirectories, because volume snapshots are atomic per volume and that is what keeps index and blocks consistent with each other; PVC 2 carries metadata_snapshots_dir, because a recovery point must not die with the volume whose corruption it should repair. Garage creates all three directories itself, which matters because the image is FROM scratch and has no mkdir. Two corrections to the approved plan, both verified against the source at tag v2.4.1 and decided on the issue before implementation: - The RPC secret arrives as GARAGE_RPC_SECRET via secretKeyRef, not through rpc_secret_file. The file form cannot work in a non-root pod: read_secret_file rejects any mode with a group or other bit set (src/garage/secrets.rs:144, mask 0o077) while the kubelet OR-s the group-read bit onto every file of a secret volume once fsGroup is set (pkg/volume/volume_linux.go), and Kubernetes can set the group of such a file but never its owner. The mechanism is documented at the value so the next round does not "fix" it back. - The first start is done by the server, not by hand: garage server --single-node creates the layout in-process before any listener opens, is idempotent on the layout version and has no --version argument that could be reused, and --default-access-key/--default-bucket import key and bucket FROM the vault item. Both premises of the original "manual first start" decision fell with that. Probes follow from the same source reading: readiness is GET /health on the admin port, which answers 503 while no layout exists; liveness and startup are a TCP connect, because a probe that can only fail would restart a node waiting for its layout forever (same lesson as zipline, README 12.3). The S3 listener binds independently of the layout, so a TCP probe on 3900 would have been the lie the plan warned about. CI: fixtures under ci/garage/ rather than an exclusion - with --single-node the pod reaches Ready on its own, which makes the install-test an assertion about the layout, the one-PVC subdirectory layout and readOnlyRootFilesystem instead of a formality. The Renovate block carries a regex versioning anchored to ^v<major>.<minor>.<patch>$, because the registry receives 40-character commit-SHA tags almost daily (measured 2026-10-03: the newest 22 tags are all of that kind) and the four v1.99.x-internal builds sort above the v1.3.x line. Part of #81 |
||
|
|
a9ca5302de
|
feat(paperless-ngx): optional valkey subchart, password from the redis item
Validate Helm Charts / Detect changed charts (pull_request) Successful in 4s
Validate Helm Charts / Resource quantities (pull_request) Successful in 12s
Validate Helm Charts / Build paperless-ngx (pull_request) Successful in 6s
Validate Helm Charts / Trivy config paperless-ngx (pull_request) Successful in 22s
Install-test / Install-test paperless-ngx (pull_request) Successful in 3m31s
Validate Helm Charts / Install-test (pull_request) Successful in 3m31s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 10s
Publish Paperless-NGX Helm Chart / Publish Paperless-NGX Helm Chart (push) Successful in 10s
SonarQube Analysis / SonarQube Analysis (push) Successful in 33s
paperless-ngx cannot run without a Redis - PAPERLESS_REDIS is the Celery broker - so every installation needed a second Helm release whose address lived in a 1Password item and in neither chart. This adds our own published valkey chart as an optional dependency, off by default and gated on valkeySubchart.enabled (not valkey.enabled, which the subchart's strict schema rejects and which Helm would only have complained about for whoever first switched it on). Additive, default behaviour unchanged: minor, 12.1.3+up3.2.1 -> 12.2.0+up3.2.1. Configured as a QUEUE (PVC + noeviction, the subchart's defaults), unlike stalwart's cache. Read off the 3.2.1 source rather than assumed: one PAPERLESS_REDIS url serves five consumers - the Celery broker, the result backend (Redis in 3.x, the database before), the channels pub/sub layer and two Django caches. allkeys-lru would not be a trade here but actively wrong, because LRU evicts typeless across the whole keyspace and a backlog of waiting consume tasks is exactly what it reads as cold; a dropped task is a document silently never processed. The price, named in values.yaml rather than smoothed over: two of the five consumers are caches, so a full instance refuses writes - new tasks included - and the answer is memory sizing, not a policy change. All three cache-like consumers are TTL-bounded, which is what keeps that price small. The password comes from the SAME item as the Redis connection, in a third field auth-conf holding the complete line "requirepass <password>". It cannot DEFAULT to secrets.redisSecretRef, and that is a property of Helm, not an omission: a values.yaml is never templated and a parent cannot compute a subchart's value. Measured - a template expression there lands verbatim in the subchart's itemPath and fails as a YAML parse error naming neither the value nor the cause. Forgetting it is therefore caught in values.schema.json, by a conditional rule rather than a fail() in a template. Also measured, and the reason: Helm renders a subchart's templates BEFORE the parent's, so a guard in this chart loses the race to the subchart's own required(), which reports "secrets.authSecretRef" and cannot know the path belongs to the redis item. A check that cannot fire is the mistake found while testing #77, so the first draft of that guard was removed rather than shipped. The schema runs before any rendering and names /valkey/secrets/authSecretRef. One trap found while testing and closed here: "valkeySubchart: null" DELETES the key, Helm then cannot resolve the condition path and treats the dependency as ENABLED - so erasing the switch turned the subchart on with no password set. Unlike every other block in this chart (issue #99), that one is rejected by the schema instead of defaulting. The install-test now runs with the subchart ON and the throwaway redis Deployment removed: the url in the fixtures names ci-valkey-service, so the bundled Valkey is the broker the release actually uses. #65 covered this path by a render only - deliberately, since an unused Valkey would have made that test look like it covered clustering. This is the first real installation of the valkey-subchart path. Documented, not glossed: the password now sits in that item twice and a rotation has to touch both fields; with the subchart on the vault url is a mandatory step, not a hint; and a OnePasswordItem materializes every field of its item, so both Secrets carry all three keys - untidy, harmless, and written down so nobody "tidies it up". Fixes #78 |
||
|
|
780cb8eafd |
feat(stalwart): new chart for the Stalwart mail server
Cluster-capable from the start, with roles rather than symmetry: one StatefulSet per entry of the "nodes" map, the key passed in STALWART_ROLE. Since v0.16 Stalwart keeps its whole configuration inside the data store and manages it over JMAP, so the chart's scope is deliberately narrow: it renders config.json (exactly the DataStore object), the workloads, the Services, the NetworkPolicies, the Ingress and a cert-manager Certificate. Listeners, blob store, in-memory store, TLS object and cluster roles are operator work in the WebUI, which is why NOTES.txt carries an eight-step first install. config.json stays secret-free: authSecret uses the EnvironmentVariable variant (verified against the serde definition of PostgreSqlStore at v0.16.24), so the password arrives through secretKeyRef and checksum/config stays meaningful. The username cannot follow it - authUsername is a plain String with no secret form - so it is a public value and says so. Services are per PORT, not per group: a NodePort is cluster-unique, so "full" and "mx" can only accept SMTP behind one Service, selected through a per-port pod label. The http port gets a per-group ClusterIP Service instead, so the Ingress reaches exactly ingress.nodeGroup. Four traps, documented rather than explained away: - The role lookup FAILS OPEN. A node whose ClusterRole is missing from the database enables every listener and every task while staying Running with a green probe, and the log line arrives after the listeners are bound. Hence mx at replicas 0, per-port Services, and a NetworkPolicy that is a ceiling independent of what was bound. - NET_BIND_SERVICE is a documented deviation needed to EXEC, not to bind. The image sets setcap cap_net_bind_service=+ep, which makes the kernel fail execve when the bounding set is empty. The values comment says both why it is there and that it is never used, because the second half is what stops the next hardening pass from removing it. - externalTrafficPolicy must be Cluster - no pod runs on a control-plane node while the HA proxy delivers to them, so Local would drop every packet - and the source address that costs is restored only by the HA proxy's PROXY v2 header. - /healthz is rate-limited to 100 requests/60s per source IP, which is a node IP for kubelet probes, so all pods on a node share one bucket. The probe intervals are calculated (6 req/min/pod, ~16 pods per node) and the sum is a comment in values.yaml. A first install needs bootstrap.enabled: true: the setup wizard appears only when config.json is ABSENT, and this chart always renders one, so recovery mode is upstream's documented path for a templated config.json. Second chart in the repo with a dependency - the Valkey project's own chart, off by default behind condition:, because Stalwart's cluster coordinator rides on the in-memory store connection. The render aborts when more than one pod runs with no in-memory store address anywhere. Also wired up: publish-stalwart.yml through the shared block in workflow-util/pipelines, the change-app-version.yml choice list, a Renovate customManagers block anchored on the v prefix (which excludes the moving latest / v0.16 / -alpine tags), CI fixtures, and the README and CLAUDE.md lines that claimed ytdlp-web-player was the only chart with a dependency. Verified locally: ci/helm-dependencies.sh, helm lint --strict (0 failed), helm package, helm template against chart defaults and against ci/stalwart/test-values.yaml, the rendered config.json parsed as JSON with the field types the serde definition requires, ci/run-quantity-render-test.sh (0 failed, both nodes.*.resources paths exercised), NOTES.txt rendered through a scratch ConfigMap, and the null-safety and fail paths (securityContext=null keeps NET_BIND_SERVICE; a cluster without an in-memory store, an unknown port name, a group without http and an unknown ingress.nodeGroup all abort with a named message; the schema rejects timeout, a string timeoutMs and a list-shaped nodes). NO runtime verification was possible - there is no k3d or Docker daemon in this workspace. Part of #65 |
||
|
|
6a3b05503e
|
Make the all-charts install-test repeatable, as one shared job
Validate Helm Charts / Detect changed charts (pull_request) Successful in 3s
Validate Helm Charts / Resource quantities (pull_request) Successful in 10s
Validate Helm Charts / Build ${{ fromJSON(needs.detect-changed-charts.outputs.charts) }} (pull_request) Skipped
Validate Helm Charts / Install-test (pull_request) Skipped
Validate Helm Charts / Trivy config ${{ fromJSON(needs.detect-changed-charts.outputs.scan-charts) }} (pull_request) Successful in 21s
The install-test matrix follows the charts a pull request changes, so a chart nobody has touched since the forge move carries a green history from a different runner - and that green says nothing about today. Gitea PR #48 showed what the answer is worth: correcting a comment in 21 charts made every one of them "changed" and turned the run into the first all-charts install-test after the move, 19 of 20 matrix charts Ready in 12m41s and exactly one not (rss-bridge, Gitea #51). By accident. The install-test job moves into the reusable .github/workflows/ install-test.yml and is called twice: by validate-charts.yml with the charts a pull request changed, and by the new install-test-all-charts.yml with all of them. Not copied for the second caller - a second copy is a second chance to test differently, unnoticed, which is the argument that gives ci/excluded-charts.txt exactly one parser. The parts that would drift are the ones that were paid for once already: the pinned kubectl with its skew check, envsubst, the containerised-job detection, the per-leg cluster name. Nothing about how a chart is installed changed. The moved job differs from its predecessor in the matrix source (inputs.charts instead of the detect job's output), an id on the install step, and a step that records the result; every other step is byte-identical. The all-charts run is workflow_dispatch only - a schedule would spend compute every day on a question that changes rarely - with an optional subset input, which is what lets a single chart be tested against the runner as Gitea #51 needs. One throwaway cluster per chart, no reuse: reuse saves time and changes what the test proves. An unknown or excluded chart name, and an empty matrix, are loud errors rather than a green run over nothing. Every leg writes its own outcome into the job summary next to the four facts that decide what the outcome means - date, runner label, K3S_IMAGE, kubectl version - and a final job records the verdict over the matrix. A green run nobody notes is unknown again next time. The excluded charts appear with their justification, from the one parser, as in the pull request pipeline. 20 charts in 12m41s is written down as the expectation, so a later deviation is visible as one. Fixes #52 |
||
|
|
813c796a7b
|
feat(valkey): one Valkey instance per application (1.0.0+up9.1.2.alpine)
Validate Helm Charts / Build ${{ fromJSON(needs.detect-changed-charts.outputs.charts) }} (pull_request) Successful in 4s
Validate Helm Charts / Detect changed charts (pull_request) Successful in 3s
Validate Helm Charts / Trivy config ${{ fromJSON(needs.detect-changed-charts.outputs.scan-charts) }} (pull_request) Successful in 11s
Validate Helm Charts / Install-test ${{ fromJSON(needs.detect-changed-charts.outputs.install-charts) }} (pull_request) Successful in 1m22s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 7s
Publish Valkey Helm Chart / Publish Valkey Helm Chart (push) Successful in 7s
Validate Helm Charts / Resource quantities (pull_request) Successful in 8s
A new chart for a single Valkey instance per application, replacing the
shared cluster-redis whose separation rests on one ACL file in one
1Password field - a construction in which every change touches six
applications and which cost a month of Paperless standstill.
Shape: StatefulSet with one replica, ClusterIP Service, no cluster mode,
no Ingress and no value to add one.
The password arrives as a finished configuration line ("requirepass
<secret>") in the 1Password field auth-conf, is mounted as a file and
included by the main configuration - never in argv, never in the
environment. The include is rendered last, so nothing in extraConfig can
override it. Because a misspelled or empty field would leave an open
server that looks perfectly healthy, an init container greps the mounted
file for the line and refuses to start the pod without it; it never
prints the file.
maxmemory is derived from the computed memory limit (maxmemoryFactor,
default 0.75) and a configured value above that limit aborts the render:
otherwise the kubelet's OOM kill decides what the eviction policy was
configured to decide. Persistence and eviction are documented as pairs -
queue (PVC + noeviction, the default) and cache (emptyDir +
allkeys-lru) - and the combination "no persistence + noeviction" carries
a warning in values.yaml and in NOTES.txt.
Probes: valkey-cli ping as an exec probe, authenticated through
VALKEYCLI_AUTH read out of the mounted file, with "-e" and an assertion
on the answer PONG. Without -e valkey-cli exits 0 on an error reply
(valkey 9.1.2, src/valkey-cli.c: the config.set_errcode branch in
cliReadReply), which would make the probe unable to fail.
Hardening: uid 999 / gid 1000 - the image's own valkey user - with
fsGroup matching, runAsNonRoot, readOnlyRootFilesystem, all capabilities
dropped and seccompProfile RuntimeDefault in the pod context so the init
container is covered too. /data is the only writable path.
NetworkPolicy on by default (first chart in this repo with one), with a
switch and a list of additional namespaces; values.yaml names that it is
only a boundary where the CNI enforces policies.
Wiring: publish-valkey.yml, the chart-name choice list, ci/valkey
fixtures for the persistent variant, a plain docker-versioning Renovate
block, README chart row.
|
||
|
|
d9267b2180 |
sure: fix the readiness probe, the ephemeral-storage budget and HOME
Second install-test run, three findings, all measured: - Readiness on "/" failed with "http: server gave HTTP response to HTTPS client" on a pod that was answering correctly. The kubelet's HTTP prober follows redirects to the same host, and with RAILS_ASSUME_SSL=true the app builds its redirect as https://<pod-ip>:3000/registration/new, which Puma does not speak. The probe now runs curl without -L and accepts 2xx-3xx itself. The same log also proves what the probe is for: against an empty database the redirect target was /registration/new, the branch User.count decides, so the path really does reach Postgres. - The pod was evicted within two minutes of every start: "ephemeral local storage usage exceeds the total limit of containers 60Mi". emptyDirs and the stdout log both count there, and the image rebuilds its Bootsnap cache under /rails/tmp on every start because the emptyDir hides the one baked into the image. The request is now 256Mi (limit 768Mi) per app container - headroom, not a measurement. - /home/rails is read-only, so Bundler created a throwaway home under TMPDIR on every invocation, worker probe included. HOME now points at a directory the init container creates. The CI postgres fixture gets -d sure on its readiness probe, so the server stops logging a missing database that nothing asked for. |
||
|
|
93fdb3eaea |
sure: new chart for the we-promise Sure app (1.0.0+up0.7.4)
One StatefulSet with two containers - the Puma web process and a Sidekiq worker - sharing one ReadWriteOnce PVC at /rails/storage. That shared volume is the reason they live in one pod: splitting them would need an RWX volume. Only the web container migrates, because the image entrypoint runs db:prepare solely for the './bin/rails server' command; the worker is started through args: so the entrypoint (and its jemalloc preload) stays in place and the two can never race. Probes are chosen for what they prove: liveness and startup use Rails' /up, which is a boot check and touches no database, while readiness uses '/', which runs User.count through authenticate_user! in self-hosted mode and therefore fails while Postgres is unreachable. The worker speaks no HTTP, so all three of its probes ask sidekiqmon whether this pod still holds a Sidekiq heartbeat in Redis. Three OnePasswordItem CRs (app, database, redis), ingress.domain kept required outside the Ingress because APP_DOMAIN and the WebAuthn relying party read it, strict values.schema.json, CI fixtures with throwaway Postgres 16 and Redis, and a Renovate manager on semver so the -alpha/-rc/-hotfix channel and the moving tags are never selected. |
||
|
|
ae621746a1
|
Merge main into the MediathekArr branch
Validate Helm Charts / Build ${{ fromJSON(needs.detect-changed-charts.outputs.charts) }} (pull_request) Successful in 4s
Validate Helm Charts / Install-test ${{ fromJSON(needs.detect-changed-charts.outputs.install-charts) }} (pull_request) Successful in 3m45s
Validate Helm Charts / Resource quantities (pull_request) Successful in 7s
Validate Helm Charts / Detect changed charts (pull_request) Successful in 3s
Validate Helm Charts / Trivy config ${{ fromJSON(needs.detect-changed-charts.outputs.scan-charts) }} (pull_request) Successful in 11s
The branch predates the Gitea move (#253) by 37 commits and both sides touched the same two files. Merged rather than rebased, so the signed commit of the original work stays intact, and resolved by combining rather than choosing - both statements are true at once: the chart has ten images now, and the repo lives on Gitea. README.md, the one real conflict: this branch rewrote the freesailarr image bullet from nine upstreams to ten, while main had grown a minecraft-server bullet next to it that this branch never had. Kept both - our "ten" wording and main's new bullet. renovate.json5 merged cleanly, but "cleanly" is not "correctly", so the result was read rather than trusted: the freesailarr exception now says ten and gained the pointer to the mediathekarr digest block, while the two things main added right beside it survived unchanged - the note that the post-upgrade hook is allow-listed on the shared Gitea instance, and the forge-neutral wording of the automerge rule. Nothing from before the move brought "GitHub" back into a sentence that had been made neutral. |
||
|
|
78d7020dab
|
freesailarr: MediathekArr as the tenth container, off by default (3.1.0+up1.0.0)
MediathekArr fakes a Usenet indexer towards Prowlarr and a SABnzbd downloader towards Sonarr/Radarr and fetches from the ARD and ZDF media libraries. Both consumers already share gluetun's network namespace, so they reach it over 127.0.0.1 - no service, no DNS, no ingress detour. Purely additive: the flag defaults to false and a default render is byte-identical to 3.0.5 (verified against the previous chart for both the default values and the CI values). The four things that fail silently without them: - A START GATE on gluetun. MediathekArr fetches its ruleset once at start and never retries; started before the tunnel it answers every Newznab search with 0 results while the container is up, Ready and quiet. Kubernetes cannot order two regular containers and an initContainer would deadlock, so the wait sits in front of the image entrypoint: poll gluetun's health server on 127.0.0.1:9999 and exec the entrypoint afterwards; on timeout the container exits rather than starting blind, because a crash loop is visible and an empty indexer is not. The startup probe budget (300s) deliberately outlasts the gate (180s). - incomplete/ AND complete/ ON THE SHARED DOWNLOADS CLAIM, not in an emptyDir. An emptyDir counts against the container's ephemeral-storage limit, and one large download would have the kubelet evict the whole pod with all ten containers - the trap ytdlp-web-player documents. - THE COMPLETE PATH SONARR SEES. The claim is mounted at /downloads exactly as sonarr/radarr mount it, and the chart sets DOWNLOAD_COMPLETE_PATH itself, so the SABnzbd complete_dir it reports is the same absolute path on both sides. - TWO PORTS: 5007 (SABnzbd role and UI, primary) and 5008 (Newznab role for Prowlarr), both declared to gluetun's firewall and both on one Service. The SABnzbd API is at /download/api - /api on 5007 answers with a misleading Newznab validation error. Further details: - Image pinned by digest (tag + digest): "latest" is a 2024 build, "beta" moves. Single-arch linux/amd64. The built-in helm-values manager cannot follow that pair, so renovate.json5 gets the chart's first customManagers block; verified by dry-run with two fixtures, since a matching and a broken rule look identical while the pin is current. - Hardened as measured: non-root, uid/gid from puid/pgid, readOnlyRootFilesystem: true, all capabilities dropped, no PUID/PGID. - Probes on measured 200 endpoints (there is no health endpoint): readiness and startup on /download/, liveness as an exec covering BOTH ports, because one container runs two processes and a wedged Newznab half is exactly what makes searches come back empty. - No ingress by default: the UI links its assets with root-absolute paths, so it can only be served at the root of a host; on the shared domain that root belongs to seerr. A sub-path fails the render instead of producing a broken UI. - values.yaml and NOTES.txt document the geo MARKER PROCEDURE (not a table of series), the escape route of a German exit and its price for the whole stack, the fail_message: null trap, and the HLS question as an open question. - CI: mediathekarr enabled in ci/freesailarr/test-values.yaml, install timeout raised to 1200s for the extra image and the gate. Fixes #245 |
||
|
|
622e4c3ff8
|
ci: drop excluded charts from the install-test matrix
A chart in ci/excluded-charts.txt got an install-test job that ran the script, printed SKIP and exited 0. GitHub knows only the exit code, so the check list showed "Install-test satisfactory OK" for a chart whose image was never pulled - indistinguishable from one that really came up in the cluster (#231). detect-changed-charts now removes those charts before the matrix is built, so no job exists for them and nothing claims they passed. An absence says nothing by itself, so the detect job writes every excluded chart of the PR with its documented justification into the job summary. A "Skipped" would only say "not run"; the reason is the question a review actually asks. The list is not parsed in YAML. The workflow calls ci/run-install-test.sh --list-excluded, the one parser, and compares whole strings with jq, so a chart name is never read as a pattern. The script's own exclusion branch now goes through the same excluded_reason() helper instead of a grep that used the chart name as a regex; it stays for local runs, where the CI filter does not apply. Not viable, and recorded at the matrix as a comment: deciding per chart in an "if" on the install-test job, which would give a visible Skipped entry instead of an absent one. GitHub rejects it - jobs.<id>.if is evaluated before the matrix is expanded, so the matrix context does not exist there ("context \"matrix\" is not allowed here. available contexts are \"github\", \"inputs\", \"needs\", \"vars\"."). Run 33650724109 on the closed PR #233 ended in failure with zero jobs created. |
||
|
|
7e168da492
|
ci: parse the exclusion list in exactly one place
Two consumers read ci/excluded-charts.txt: this script and the install-test matrix in validate-charts.yml, which has to drop excluded charts before the matrix is built (#231). Two parsers would be two chances to select different charts, and the disagreement would be silent in either direction. So the script grows a --list-excluded mode and the workflow will call it instead of re-implementing the parsing. A line carrying only a chart name and no justification is deliberately not an exclusion: the README requires the reason, and a bare name would drop a chart out of CI with nothing to review. The reason lookup matches the name exactly instead of as a regex, so a chart name is never read as a pattern. The workflow side is not part of this commit. |
||
|
|
bfa72181eb
|
minecraft-server: move to the java25 line (4.1.0+up2026.8.2.java25)
Minecraft 26.2 requires Java 25, so the chart's default minecraft.version can only be raised on a java25 image. appVersion becomes 2026.8.2-java25 - the same dated upstream release the java21 line ships, so the only variables that change are the JVM and the Minecraft version. The chart version stays at 4.1.0: the Java lines are siblings of one chart generation, not successors, and are told apart by the +up suffix. The Java boundaries in the values.yaml / NOTES.txt comments are read off Mojang's version manifest (javaVersion.majorVersion per release), not estimated: 1.21.11 is the last release asking for Java 21, every release from 26.1 on asks for Java 25. |
||
|
|
6b0d8c9232
|
ytdlp-web-player: new chart with an optional oauth2-proxy subchart (1.0.0+upv1.2.0)
Deploys matszwe02/ytdlp_web_player, a Flask app that fetches videos with
yt-dlp, transcodes them with ffmpeg and serves them as HLS. It ships no
authentication of any kind, so the chart carries a preconfigured oauth2-proxy
as an optional subchart, in the data path in front of the app.
This is the first chart in the repo with a dependency, so the rule "charts
have no dependencies" falls here. It is replaced in CLAUDE.md and README.md by
a bar: a dependency has to be optional and off by default, upstream's chart is
consumed as it comes (documented where it falls short of our conventions,
never patched), and the price is written down - above all that a subchart is
invisible to everything downstream watching Helm release versions, so a new
proxy release only reaches a cluster when this chart is republished.
CI learns the dependency in five places, not the three the plan expected:
helm lint, package, template and install all exit 1 while charts/ is unfilled,
so ci/helm-dependencies.sh runs before lint and package in validate-charts.yml,
before package in publish-helm-chart.yml, and inside run-install-test.sh,
run-trivy-config.sh and run-quantity-render-test.sh - the last of which renders
every chart on every PR and would have gone red first.
Notable chart decisions, each measured rather than assumed:
- The image has no USER and runs as root by default, but not by necessity: it
runs under an arbitrary uid with readOnlyRootFilesystem and the full
hardening baseline. Only the pinned uid is a deviation, and it is documented.
- Two emptyDirs, each justified separately. /data holds the video cache
(DATA_PATH). /tmp is NOT mounted - measured across metadata extraction, HLS
transcoding, sprite generation and audio extraction, the image never writes
there. What it does write is $HOME/.cache, so /cache is mounted with HOME and
XDG_CACHE_HOME pointed at it; without it yt-dlp re-downloads its JavaScript
challenge-solver from github.com on every extraction.
- The proxy is wired through config.configFile, the only value the subchart
passes through tpl - config.upstreams is emitted with toJson and would carry
a literal {{ .Release.Name }} into the manifest.
- Rendering aborts when this chart's Ingress and the proxy's claim the same
host: ingress-nginx rejects the second one and a Fleet bundle then sits in
ErrApplied until forceSyncGeneration is bumped.
Fixes #214
|
||
|
|
148e15a1f6
|
ci: guard resource quantities against exponential notation
Adds ci/run-quantity-render-test.sh and a validate-charts job that runs it over every chart on every PR. The bug behind #157 has a loud and a silent half. Loud: a bare integer request of 1048576 is reformatted by printf "%v" to "1.048576e+06", the helper rejects the suffix it produced itself and the render aborts. Silent: with a fractional limitFactor the *product* is reformatted after the suffix check, the render succeeds, and the manifest carries memory: "1.4999985e+06" - rejected by nothing until the API server sees it at apply time. So the test renders each chart twice and, for the silent case, searches the rendered text rather than the exit code. It runs over all charts, not the changed ones: the resources helper is copied into every chart's _helpers.tpl, so the regression arrives by copy, in a PR whose diff shows a new chart and not the bug inside it. The resources paths come from values.schema.json (JSON, so python3 reads it without pyyaml), which also means a new chart is enrolled automatically and there is no list to maintain. Charts that pin every computable limit in their fallback defaults never reach the helper (gotenberg, rss-bridge). They are reported as "not exercised" instead of counted as passing. |