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 / Resource quantities (pull_request) Successful in 14s
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 / Build template-chart (pull_request) Successful in 7s
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

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
This commit is contained in:
jklein committed 2026-10-12 15:16:26 +00:00
1 parent b5b5d9a310
commit 50f683776e
18 files changed
+1225 -37

No files matched your search

+58 -8
View File
@@ -22,25 +22,72 @@ Note for the copier: a NOTES.txt renders no Kubernetes object, so adding one
is purely additive - a MINOR bump, and safe to roll out at any time.
*/}}
{{- $replicas := include "xxx.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.xxx.replicas) -}}
{{- $httpRoute := .Values.httpRoute | default dict -}}
{{- $hostnames := $httpRoute.hostnames | default list -}}
{{- if not $hostnames -}}
{{- $hostnames = list .Values.ingress.domain -}}
{{- end -}}
xxx {{ .Chart.AppVersion }} — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}.
{{- if .Values.ingress.enabled }}
ACCESS
ACCESS — INGRESS MODE (ingress.enabled: true, httpRoute.enabled: false)
https://{{ .Values.ingress.domain }}
The certificate is requested by cert-manager through the "{{ .Values.ingress.clusterIssuer }}"
cluster issuer. Until it has been issued the browser shows a warning; follow it with
kubectl -n {{ .Release.Namespace }} get certificate tls-{{ .Release.Name }}-{{ .Chart.Name }}-ingress
{{- else if $httpRoute.enabled }}
ACCESS — GATEWAY MODE (httpRoute.enabled: true, ingress.enabled: false)
{{- range $hostnames }}
https://{{ . }}
{{- end }}
This release renders an HTTPRoute ({{ .Release.Name }}-{{ .Chart.Name }}-httproute) and nothing else of
the routing path — no Gateway, no class, no Certificate. Three things
therefore belong to the cluster rather than to this chart, and the hostname
above does not answer while any of them is missing:
1. The Gateway-API CRDs — present, or this install would have failed.
2. The Gateway named in httpRoute.parentRefs
{{- range $httpRoute.parentRefs }}
{{ .name }}{{ if .namespace }} in namespace {{ .namespace }}{{ end }}{{ if .sectionName }}, listener {{ .sectionName }}{{ end }}
{{- end }}
must exist AND allow routes from namespace {{ .Release.Namespace }} in its
listener's allowedRoutes. A refused attachment shows up only in the
route's own status:
kubectl -n {{ .Release.Namespace }} get httproute {{ .Release.Name }}-{{ .Chart.Name }}-httproute -o yaml
"Accepted: True" and "ResolvedRefs: True" under status.parents[] is
what a working attachment looks like; an empty status means no
controller has looked at the route at all.
3. The certificate for the hostname, on the Gateway's listener.
ingress.clusterIssuer is NOT used in this mode and is not required —
this chart renders no Certificate here.
ingress.annotations is inert in this mode as well: the Gateway API has no
annotation channel. When you copy this chart, say here what that costs YOUR
app — an upload limit set through proxy-body-size has no Gateway-API
equivalent at all, so silence on this point means the operator gets the
Gateway's default and finds out from a proxy error message.
ingress.domain STAYS REQUIRED in this mode, and not only as the hostname
fallback: the env block in values.yaml reads it for NORMAL_ENV, which is why
that read carries a required() of its own. Check that per chart (README,
guideline 12, point 1) — in a chart where nothing outside the routing
templates reads it, httpRoute.hostnames can replace it entirely.
{{- else }}
ACCESS — NO INGRESS
ingress.enabled is false, so this release has no Ingress and no certificate,
and ingress.clusterIssuer is not needed. ingress.domain STAYS REQUIRED even
so: the env block in values.yaml reads it for NORMAL_ENV, which is why that
read carries a required() of its own. Without it the workload would start
with an empty address and nothing would point at the mistake.
ACCESS — NEITHER INGRESS NOR HTTPROUTE
ingress.enabled and httpRoute.enabled are both false, so this release has no
Ingress, no HTTPRoute and no certificate, and ingress.clusterIssuer is not
needed. ingress.domain STAYS REQUIRED even so: the env block in values.yaml
reads it for NORMAL_ENV, which is why that read carries a required() of its
own. Without it the workload would start with an empty address and nothing
would point at the mistake.
Reach the release inside the cluster with
@@ -52,7 +99,10 @@ ACCESS — NO INGRESS
SCALED TO ZERO
{{ if .Values.scaleDown }}scaleDown: true{{ else }}xxx.replicas: 0{{ end }} — the workload runs with replicas: 0, so the endpoint
above does not answer until you set it back and upgrade again. PVC, Secret{{ if .Values.ingress.enabled }},
Service and Ingress stay in place, so the certificate is not re-issued.{{ else }} and
Service and Ingress stay in place, so the certificate is not re-issued.{{ else if $httpRoute.enabled }},
Service and HTTPRoute stay in place, so the route keeps pointing at a Service
with no endpoints — the Gateway then answers with an error of its own rather
than with nothing.{{ else }} and
Service stay in place.{{ end }}
{{ end }}
THE 1PASSWORD ITEM MUST CARRY THE FIELDS THE CHART READS