Files
helm-charts/ci/gateway-api-crds.sh
jklein 50f683776e
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
feat(cyberchef): Gateway-API as an alternative to the Ingress (pilot)
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
2026-10-12 15:16:26 +00:00

100 lines
5.3 KiB
Bash
Executable File

#!/usr/bin/env bash
# Install the REAL Gateway-API CRDs (standard channel) into the cluster of the
# current kubectl context, for the install-test of a chart that renders an
# HTTPRoute (Gitea #114).
#
# Usage: ci/gateway-api-crds.sh (called by ci/run-install-test.sh)
#
# WHY NOT A STUB CRD, WHICH IS WHAT ci/common/ DOES FOR OnePasswordItem
#
# Because a stub would make the install-test prove nothing. ci/common's
# OnePasswordItem CRD is schema-less on purpose (x-kubernetes-preserve-unknown-
# fields): nobody is testing the 1Password operator's API, only that the chart's
# CR applies. For an HTTPRoute the situation is the opposite - the SCHEMA is the
# entire value of the exercise. With a schema-less stub the API server accepts a
# malformed parentRefs, a backendRef with a port written as a string, a
# path type that does not exist; the install goes green and the only thing
# demonstrated is "something was created". With the real CRDs the same apply is
# validated field by field, and a malformed route fails the install-test loudly.
#
# This is the half that CANNOT be moved into a render test: a render cannot know
# the Gateway API's schema. And the half the CRDs cannot cover is held by
# ci/run-httproute-match-test.sh - schema validation accepts an empty
# parentRefs list and a backendRef naming a Service that does not exist.
#
# What neither proves, and nothing here can: that traffic flows. No Gateway
# controller runs in k3d, so no route is ever reconciled and no status is ever
# written. Provable: the object is schema-valid and the pod becomes Ready.
#
# WHY --server-side, AND THE MEASUREMENT BEHIND IT
#
# Client-side `kubectl apply` stores the whole applied object in the
# kubectl.kubernetes.io/last-applied-configuration annotation, and an object's
# annotations may total at most 262144 bytes. The HTTPRoute CRD of the standard
# channel is 429367 bytes of YAML, which comes to 244792 bytes as JSON
# (measured on v1.6.3, re-read through ci/yaml2json) - i.e. 93% of the limit,
# with every release adding fields. So client-side apply happens to fit today
# and is one upstream release away from failing with "metadata.annotations: Too
# long", on a Renovate PR that has nothing to do with it. Server-side apply
# writes no such annotation and has no such limit.
#
# This is also why the CRDs are NOT in ci/common/, which ci/run-install-test.sh
# applies client-side for EVERY chart: putting them there would hand that
# landmine to all 20-odd charts and spend the download on charts that render no
# route at all.
#
# WHY DOWNLOADED AND NOT VENDORED
#
# 1.1 MB of generated YAML in the repository would go stale invisibly and would
# bury every diff that touches ci/. Downloading a PINNED artifact is what this
# repo already does for every other piece of CI tooling - Trivy's tarball, k3d's
# install script, kubectl - each with a `# renovate:` comment keeping the pin
# current. The cost is named rather than hidden: one more network fetch, and if
# the release asset ever disappears this goes red on a pull request that changed
# nothing related. It fails loudly, which is the acceptable half.
set -euo pipefail
# The pinned standard channel. Renovate keeps this current through the
# customManager for ci/*.sh in renovate.json5; prereleases are excluded there,
# so the -rc.N tags upstream publishes are never offered.
# renovate: datasource=github-releases depName=kubernetes-sigs/gateway-api
GATEWAY_API_VERSION=v1.6.3
# The standard channel, as published - the whole set and not a hand-picked
# subset. The charts render only HTTPRoute today, so HTTPRoute's CRD is the only
# one strictly needed; taking the published set instead means the sweep of #114
# needs no change here, and a fixture that wants a real Gateway object later
# finds its CRD already installed.
URL="https://github.com/kubernetes-sigs/gateway-api/releases/download/${GATEWAY_API_VERSION}/standard-install.yaml"
MANIFEST="$(mktemp)"
trap 'rm -f "$MANIFEST"' EXIT
echo "==> Gateway API ${GATEWAY_API_VERSION} (standard channel): $URL"
curl -sSfL -o "$MANIFEST" "$URL"
# A truncated or redirected download would otherwise be applied as whatever it
# happens to contain. Asserting the one object the charts actually need is
# cheaper than a checksum nobody can regenerate, and it names what is wrong.
if ! grep -q '^ name: httproutes.gateway.networking.k8s.io$' "$MANIFEST"; then
echo "ERROR: the downloaded manifest does not contain the httproutes CRD." >&2
echo "That is not a chart failure: either the release asset changed shape or the" >&2
echo "download was truncated. Got $(wc -c <"$MANIFEST") bytes from $URL" >&2
exit 1
fi
kubectl apply --server-side -f "$MANIFEST"
# Established, not just created. `kubectl apply` returns as soon as the CRD
# object exists, while the API server needs a moment to serve the new endpoint -
# and a `helm install` in that window fails with "no matches for kind
# HTTPRoute", which reads exactly like a missing CRD rather than like a race.
# Same lesson as the Ready-node loop in install-test.yml: wait for the
# condition that means usable, not for the one that means present.
for CRD in $(kubectl get crd -o name | grep '\.gateway\.networking\.k8s\.io$'); do
kubectl wait --for=condition=Established "$CRD" --timeout=60s
done
kubectl get crd -o name | grep '\.gateway\.networking\.k8s\.io$' | sed 's/^/ /'
echo "==> Gateway API CRDs established."