#!/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."