# Cilium Gateway (Phase-8a bug #14 follow-up to #484).
# Moved out of bootstrap-kit/01-cilium.yaml because gateway.networking.k8s.io/v1
# CRDs are installed by the Cilium HelmRelease itself; Flux dry-runs the
# whole Kustomization before applying any HR, so Gateway dry-run fails on
# a fresh cluster. The sovereign-tls Kustomization dependsOn bootstrap-kit
# Ready, so by the time Gateway is applied here, Cilium has installed.
#
# Multi-zone listeners (issue #831, parent epic #827)
# ---------------------------------------------------
# Before this change the Gateway declared a single listener pair (HTTPS +
# HTTP) on `*.${SOVEREIGN_FQDN}`. That worked for legacy single-zone
# Sovereigns but BROKE every tenant URL under a non-primary parent zone:
#   - Primary zone: omani.works   → console.omani.works  ✅ TLS terminates
#   - SME pool:    omani.homes    → wp-foo.omani.homes  ❌ TLS handshake
#                                                       mismatch (cert
#                                                       exists per chart's
#                                                       sovereign-wildcard-
#                                                       certs.yaml but no
#                                                       Gateway listener
#                                                       claims the
#                                                       hostname).
# Symptom: cilium-envoy serves the default fallback cert, browser shows
# NET::ERR_CERT_COMMON_NAME_INVALID, marketplace WordPress tenants on
# omani.homes are unreachable.
#
# Fix: render one listener pair per parent zone. The listener block is
# materialised at Terraform plan time (infra/hetzner/main.tf
# locals.parent_domains_listeners_yaml — jsonencode of the listener
# objects), threaded through Flux postBuild.substitute as
# ${PARENT_DOMAINS_LISTENERS_YAML}, and consumed BELOW as a YAML inline-
# flow array value on `spec.listeners`. Each pair's certificateRefs
# target the per-zone Secret rendered by products/catalyst/chart/
# templates/sovereign-wildcard-certs.yaml (PR #827) so the Gateway
# listener and the cert resource are always in lockstep.
#
# Why a scalar placeholder, not a multi-line block:
#   - kustomize-build PARSES the YAML before Flux runs envsubst. A
#     placeholder on its own line at column 0 is rejected by the YAML
#     parser ("could not find expected ':'"), and kustomize fails. A
#     scalar like `listeners: ${VAR}` parses cleanly — kustomize sees
#     the value as the literal string `${VAR}` and emits it unchanged.
#     Flux's envsubst step then swaps it for the JSON-flow array string
#     `[{name: https-omani-works, ...}, ...]`, which the apiserver
#     parses as the real listener list.
#
# Why not a Helm template here: the Cilium Gateway resource lives in the
# Kustomize-managed sovereign-tls path (not the chart) because its
# Kustomization dependsOn bootstrap-kit Ready — i.e. it lands BEFORE
# bp-catalyst-platform reconciles. Moving it into the chart would invert
# the ordering and produce a transient "no Gateway → no envoy listener →
# console unreachable" gap during every Helm upgrade. envsubst-driven
# pre-rendered YAML is the canonical pattern for this slot.
#
# Listener naming convention (t20 critical fix #3):
#   - SINGLE parent zone (the common case) → bare names `https` /
#     `http`. Every platform chart's HTTPRoute (harbor, keycloak,
#     grafana, gitea, openbao, powerdns, stalwart-tenant) hardcodes
#     `parentRefs[0].sectionName: https`. If we rename the listener to
#     `https-<sanitised-zone>` for a single-zone Sovereign, every
#     HTTPRoute reports `Accepted=False NoMatchingListener` and the
#     Sovereign Console / Harbor / Keycloak etc. are unreachable at
#     the Gateway. Keeping bare names for the single-zone case is the
#     safer rollback. (Was broken between PR #1640 and the t20 fix.)
#   - MULTIPLE parent zones → unique names `https-<sanitised-zone>` /
#     `http-<sanitised-zone>` where sanitised-zone = zone name with
#     '.' → '-' (e.g. omani-works, omani-homes). Distinct names per
#     listener so the Gateway controller programs them all (duplicate
#     `name: https` produces a Conflicting status condition and skips
#     all but the first). For multi-zone Sovereigns whose HTTPRoutes
#     must attach under a non-primary zone, override `sectionName` via
#     values.yaml at the chart level.
#   - The certificateRefs.name is ALWAYS the per-zone
#     `sovereign-wildcard-tls-<sanitised-zone>` (see
#     products/catalyst/chart/templates/sovereign-wildcard-certs.yaml)
#     — independent of the listener-name choice above.
#
# TBD-A32 (#1886) — Per-prov 2-label wildcard listener
# ----------------------------------------------------
# The parent-zone listener above declares `hostname: *.<zone>` (e.g.
# `*.omani.works`). Per Gateway-API spec wildcard semantics, that
# pattern matches EXACTLY one label depth: `foo.omani.works` ✅, but
# NOT `console.t28.omani.works` (2-label depth). On every shared
# parent-zone topology, the operator-facing FQDN is per-prov
# (`t28.omani.works`) and every operator endpoint (console.<fqdn>,
# api.<fqdn>, marketplace.<fqdn>, …) is 2-label-deep — UNREACHABLE
# through the parent-zone listener. Caught on t28 (A110 scorecard,
# 2026-05-19): `curl -skI https://console.t28.omani.works/` reset at
# TLS handshake even though `sovereign-wildcard-tls-t28-omani-works`
# already contained all 13 per-prov SANs.
#
# Fix: locals.per_prov_listeners (infra/hetzner/main.tf) emits an
# ADDITIONAL listener pair hostnamed `*.<sovereign_fqdn>` (e.g.
# `*.t28.omani.works`) bound to the per-prov cert
# `sovereign-wildcard-tls-<fqdn-dashed>` rendered by
# cilium-gateway-cert.yaml in this same Kustomization. The pair
# uses unique names `https-<fqdn-dashed>` / `http-<fqdn-dashed>`.
# Skipped when sovereign_fqdn == one of the parent-zone names (legacy
# single-zone-on-apex case) so no duplicate listener-name condition
# is raised. Safe because every catalyst-system HTTPRoute now OMITS
# sectionName (PR #1888 closing #1884) — Cilium attaches by hostname
# match.
#
# The listener block is rendered by infra/hetzner/main.tf locals.
# parent_domains_listeners_yaml using local.parent_domains_single_zone
# to switch between the two naming schemes (and appending per-prov
# listeners via local.per_prov_listeners).

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: cilium-gateway
  namespace: kube-system
  labels:
    catalyst.openova.io/sovereign: ${SOVEREIGN_FQDN}
    catalyst.openova.io/component: cilium-gateway
spec:
  gatewayClassName: cilium
  # ── TBD-A31 (#1885): Hetzner LB annotations for the gateway Service ──
  #
  # The Gateway-API spec (`spec.infrastructure.annotations`) is the canonical
  # mechanism for declaring annotations that the controller MUST propagate
  # to any infrastructure resources it creates in response to this Gateway —
  # in Cilium's case, the auto-generated `cilium-gateway-cilium-gateway`
  # Service in kube-system. Cilium 1.16+ honours this block and forwards
  # the annotations onto the Service `metadata.annotations`, where
  # hcloud-cloud-controller-manager (bp-hcloud-ccm slot 55) picks them up
  # at Service reconcile time and provisions a Hetzner LB.
  #
  # Why this matters operationally:
  #   - A98+A107 evidence on t28 (76fdffb42532e6cc): the gateway Service
  #     showed `type=ClusterIP` with no Hetzner LB attached → public TLS
  #     to console.t28.omani.works:443 reset at the handshake. Even with
  #     the tofu-provisioned `hcloud_load_balancer.main` (infra/hetzner/
  #     main.tf:955) carrying 443→30443 service-port, operators inspecting
  #     `kubectl get svc -n kube-system cilium-gateway-cilium-gateway`
  #     saw a non-LoadBalancer Service and concluded the LB chain was
  #     broken. Without these annotations, hcloud-CCM has no signal to
  #     materialise a parallel Service-level LB (the tofu LB at the
  #     infra layer is invisible to the cluster-side CCM).
  #   - For multi-region Sovereigns the per-region cilium-gateway in each
  #     secondary cluster ALSO needs a public LB so external clients can
  #     reach region-local listeners directly (the omani.homes / omani.rest
  #     SME-pool subdomains attach to the secondary region's gateway).
  #     `${SOVEREIGN_REGION_KEY:=primary}` segments the LB name per region
  #     (mirrors the clustermesh-apiserver LB naming in
  #     clusters/_template/bootstrap-kit/01-cilium.yaml:237).
  #
  # use-private-ip: "false" — per docs/SOVEREIGN-MULTI-REGION-DOD.md A2
  # (inter-region link = PUBLIC IPs ALWAYS) AND the empirical lesson from
  # PR #1538: the Hetzner per-region LB has no private-network attachment
  # by default so CCM rejects `use private ip: missing network id`. The
  # firewall already opens 30000-32767/tcp (infra/hetzner/main.tf:233) so
  # the public-IP LB health checks pass against node:30443.
  #
  # health-check pinned to TCP:30443 — without this annotation, hcloud-CCM
  # defaults the health check to the Service's nodePort (which Cilium
  # allocates randomly when hostNetwork=true). Pinning to 30443 (the
  # actual host-bound cilium-envoy HTTPS listener) ensures the CCM LB
  # marks targets healthy AS SOON AS envoy is listening — without this,
  # the LB stayed `unhealthy` indefinitely on prov #76 (2026-05-14).
  #
  # TBD-A36 (#1896) — Gateway-API CRD annotations cap = 8 entries
  # -------------------------------------------------------------
  # `gateways.gateway.networking.k8s.io` (CRD published by the Cilium
  # Gateway-API support) declares `spec.infrastructure.annotations` as a
  # map with `maxProperties: 8`. The 10-annotation list that landed in
  # #1889 (TBD-A31) tripped the CRD validator at Flux SSA time:
  #   spec.infrastructure.annotations: Too many: 10: must have at most 8 items
  # → Gateway never reconciled → cilium-gateway-cilium-gateway Service
  # never reached `type=LoadBalancer` → no Hetzner LB at the Service
  # layer → public TLS at console.<fqdn>:443 reset at the handshake.
  # Blocked t28/t29/t30 since 2026-05-19 00:50:35Z.
  #
  # Resolution (Option A per A130): drop the two health-check timing
  # annotations (`health-check-interval`, `health-check-timeout`). hcloud-
  # CCM defaults are reasonable (15s interval, 10s timeout) and identical
  # to the values we were declaring, so the runtime behaviour of the
  # health check is unchanged. The remaining 8 annotations (name,
  # location, type, use-private-ip, disable-private-ingress,
  # health-check-protocol, health-check-port, health-check-retries) are
  # the minimum set required to materialise a public-IP TCP-health-checked
  # Hetzner LB on the correct location/type with the correct backend port.
  #
  # Validated with `kubectl apply --dry-run=server` against a live cluster
  # before merge (Principle #15 — IaC evaluator over text grep). DO NOT
  # add a 9th annotation here without first checking the CRD limit and
  # re-running the server-side dry-run.
  infrastructure:
    annotations:
      load-balancer.hetzner.cloud/name: "${SOVEREIGN_FQDN_SLUG:=catalyst}-${SOVEREIGN_REGION_KEY:=primary}-gateway"
      load-balancer.hetzner.cloud/location: "${HCLOUD_LB_LOCATION}"
      load-balancer.hetzner.cloud/type: "lb11"
      load-balancer.hetzner.cloud/use-private-ip: "false"
      load-balancer.hetzner.cloud/disable-private-ingress: "true"
      load-balancer.hetzner.cloud/health-check-protocol: "tcp"
      load-balancer.hetzner.cloud/health-check-port: "30443"
      load-balancer.hetzner.cloud/health-check-retries: "3"
  # NOTE: ports 30080/30443 (not 80/443) — even with hostNetwork=true,
  # cilium-envoy refuses to bind privileged ports because cilium-agent
  # gates that bind through its `envoy-keep-cap-netbindservice` flag and
  # the resulting bind() syscall is intercepted by the agent's BPF
  # socket-LB program. Setting privileged: true on the cilium-envoy
  # DaemonSet + adding NET_BIND_SERVICE + flipping the configmap flag
  # all failed to lift the bind() rejection (verified live on otech45,
  # otech46, otech47).
  #
  # High-port (>1024) bind succeeds without NET_BIND_SERVICE. The
  # Hetzner LB does the public-facing port translation: HCLB listens on
  # 80→forwards to CP node:30080; HCLB listens on 443→forwards to CP
  # node:30443. Browsers hit the canonical URL (`https://console.<fqdn>/`)
  # so port 30443 is never visible externally.
  #
  # See infra/hetzner/main.tf hcloud_load_balancer_service.{http,https}
  # destination_port settings — they MUST match these listener ports.
  listeners: ${PARENT_DOMAINS_LISTENERS_YAML}
