From 055e05cf34ea6c20665a0cb08f4a5e372365058e Mon Sep 17 00:00:00 2001 From: mikkeldamsgaard Date: Tue, 21 Apr 2026 23:33:00 +0200 Subject: [PATCH 1/4] feat(netbird): add Gateway API support and reject ingressGrpc without TLS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds HTTPRoute/GRPCRoute/TCPRoute as mutually-exclusive alternatives to each existing Ingress block (server.httpRoute, server.grpcRoute, server.relayHttpRoute, server.relayTcpRoute, dashboard.httpRoute). The chart renders routes only and attaches them via parentRefs to a user-managed Gateway. Omitted backendRefs auto-fill to the netbird Service on port 80. Fail-fast helm-template validation covers: * Ingress + Gateway route both enabled for the same traffic class * relayHttpRoute and relayTcpRoute both enabled * Route enabled with empty parentRefs * ingressGrpc enabled with empty tls — gRPC over nginx-ingress cannot negotiate h2c, and the default ssl-redirect annotation redirects plaintext gRPC to HTTPS, so this silently failed before Fixes #74 by giving users a plaintext-h2c path (grpcRoute) for gRPC and rejecting the silent-failure Ingress configuration. Includes 36 new helm-unittest cases and an Envoy-Gateway-based e2e test harness (ci/scripts/netbird/e2e-gateway.sh + CI job) that verifies routes reach Accepted=True on the gateway and that backendRefs auto-fill. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yaml | 38 ++++ CHANGELOG.md | 26 +++ Makefile | 6 +- charts/netbird/README.md | 208 +++++++++++++++--- charts/netbird/ci/e2e-values-gateway.yaml | 92 ++++++++ charts/netbird/templates/_helpers.tpl | 34 +++ .../templates/dashboard-httproute.yaml | 36 +++ .../netbird/templates/server-configmap.yaml | 1 + .../netbird/templates/server-grpcroute.yaml | 36 +++ .../netbird/templates/server-httproute.yaml | 36 +++ .../templates/server-relay-httproute.yaml | 36 +++ .../templates/server-relay-tcproute.yaml | 33 +++ .../tests/dashboard-httproute_test.yaml | 50 +++++ .../netbird/tests/server-configmap_test.yaml | 87 ++++++++ .../netbird/tests/server-grpcroute_test.yaml | 88 ++++++++ .../netbird/tests/server-httproute_test.yaml | 117 ++++++++++ .../tests/server-relay-httproute_test.yaml | 49 +++++ .../tests/server-relay-tcproute_test.yaml | 61 +++++ charts/netbird/values.yaml | 88 ++++++++ ci/scripts/netbird/e2e-gateway.sh | 175 +++++++++++++++ 20 files changed, 1260 insertions(+), 37 deletions(-) create mode 100644 charts/netbird/ci/e2e-values-gateway.yaml create mode 100644 charts/netbird/templates/dashboard-httproute.yaml create mode 100644 charts/netbird/templates/server-grpcroute.yaml create mode 100644 charts/netbird/templates/server-httproute.yaml create mode 100644 charts/netbird/templates/server-relay-httproute.yaml create mode 100644 charts/netbird/templates/server-relay-tcproute.yaml create mode 100644 charts/netbird/tests/dashboard-httproute_test.yaml create mode 100644 charts/netbird/tests/server-grpcroute_test.yaml create mode 100644 charts/netbird/tests/server-httproute_test.yaml create mode 100644 charts/netbird/tests/server-relay-httproute_test.yaml create mode 100644 charts/netbird/tests/server-relay-tcproute_test.yaml create mode 100755 ci/scripts/netbird/e2e-gateway.sh diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0e99523..c957fe0 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -210,6 +210,44 @@ jobs: echo "=== Events ===" kubectl -n netbird-e2e get events --sort-by='.lastTimestamp' || true + e2e-gateway: + name: "E2E — NetBird: Gateway API (Envoy Gateway)" + runs-on: ubuntu-latest + needs: [detect-changes, lint-and-unit-test] + if: needs.detect-changes.outputs.netbird == 'true' || needs.detect-changes.outputs.ci == 'true' + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Helm + uses: azure/setup-helm@v4 + with: + version: v4.0.2 + + - name: Create kind cluster + uses: helm/kind-action@v1 + with: + cluster_name: helms-e2e + + - name: Run e2e test (gateway) + run: ci/scripts/netbird/e2e-gateway.sh + + - name: Show debug info on failure + if: failure() + run: | + echo "=== Pod status ===" + kubectl -n netbird-gateway-e2e get pods -o wide || true + echo "=== Gateway status ===" + kubectl -n netbird-gateway-e2e get gateway netbird-gateway -o yaml || true + echo "=== Route statuses ===" + kubectl -n netbird-gateway-e2e get httproute,grpcroute -o yaml || true + echo "=== Envoy Gateway logs ===" + kubectl -n envoy-gateway-system logs deployment/envoy-gateway --tail=100 || true + echo "=== Server logs ===" + kubectl -n netbird-gateway-e2e logs deployment/netbird-gateway-e2e-server --all-containers --tail=100 || true + echo "=== Events ===" + kubectl -n netbird-gateway-e2e get events --sort-by='.lastTimestamp' || true + e2e-oidc-embedded: name: "E2E — NetBird: OIDC (Embedded IdP)" runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index b024423..2766105 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,12 +14,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/). `https://netbird.example.com`). NetBird clients require the port — without it the daemon fails with `missing port in address`. Use `https://netbird.example.com:443` instead. Fixes #75. +- **netbird**: Gateway API support as a mutually-exclusive alternative to + Kubernetes Ingress for every traffic class. New values: + `server.httpRoute` (`HTTPRoute`), `server.grpcRoute` (`GRPCRoute`), + `server.relayHttpRoute` (`HTTPRoute`), `server.relayTcpRoute` + (`TCPRoute`, v1alpha2), and `dashboard.httpRoute` (`HTTPRoute`). The + chart renders routes only; users provide `parentRefs` to a Gateway they + already manage. Omitted `backendRefs` auto-fill to the netbird + server / dashboard Service on port 80. Fixes #74 — controllers that + support plaintext h2c (Envoy Gateway, Traefik Gateway, …) can now expose + gRPC without TLS via `GRPCRoute`, sidestepping the nginx-ingress h2c + limitation that made `server.ingressGrpc` fail silently without a cert. +- **netbird**: Fail-fast validation that rejects enabling both an Ingress + and its Gateway-API counterpart for the same traffic class (and between + `server.relayHttpRoute` / `server.relayTcpRoute`), or enabling a route + with an empty `parentRefs` list. +- **netbird**: Fail-fast validation that rejects + `server.ingressGrpc.enabled=true` with an empty `server.ingressGrpc.tls` + list. gRPC over Kubernetes Ingress requires TLS (nginx-ingress cannot + negotiate plaintext h2c, and the default `ssl-redirect: "true"` + annotation redirects plaintext gRPC to HTTPS) — previously this + misconfiguration failed silently. Fixes #74. Users who want plaintext + gRPC should use `server.grpcRoute` with a Gateway API controller that + supports h2c. ### Changed - **netbird**: README and `values.yaml` examples now show `exposedAddress` with an explicit `:443` port and document that the port is required even when it matches the scheme default. +- **netbird**: README gains a "Gateway API as an alternative to Ingress" + section with copy-pasteable examples, parameter tables for the new + route blocks, and an updated architecture diagram. ## [0.4.1] — 2026-04-14 diff --git a/Makefile b/Makefile index b4e229c..5b78830 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: lint unittest e2e e2e-netbird e2e-sqlite e2e-postgres e2e-mysql e2e-oidc-keycloak e2e-oidc-zitadel e2e-keycloak e2e-keycloak-dev e2e-keycloak-postgres e2e-keycloak-replicas e2e-setup e2e-teardown test compat-matrix +.PHONY: lint unittest e2e e2e-netbird e2e-sqlite e2e-postgres e2e-mysql e2e-gateway e2e-oidc-keycloak e2e-oidc-zitadel e2e-keycloak e2e-keycloak-dev e2e-keycloak-postgres e2e-keycloak-replicas e2e-setup e2e-teardown test compat-matrix CHARTS := $(wildcard charts/*) @@ -36,6 +36,9 @@ e2e-postgres: e2e-setup e2e-mysql: e2e-setup ci/scripts/netbird/e2e.sh mysql +e2e-gateway: e2e-setup + ci/scripts/netbird/e2e-gateway.sh + e2e-oidc-keycloak: e2e-setup ci/scripts/netbird/e2e-oidc.sh keycloak @@ -46,6 +49,7 @@ e2e-netbird: e2e-setup ci/scripts/netbird/e2e.sh sqlite ci/scripts/netbird/e2e.sh postgres ci/scripts/netbird/e2e.sh mysql + ci/scripts/netbird/e2e-gateway.sh ci/scripts/netbird/e2e-oidc.sh keycloak ci/scripts/netbird/e2e-oidc.sh zitadel diff --git a/charts/netbird/README.md b/charts/netbird/README.md index 018011c..5ab1fa0 100644 --- a/charts/netbird/README.md +++ b/charts/netbird/README.md @@ -162,6 +162,13 @@ server: - secretName: netbird-tls hosts: - netbird.example.com + # ⚠ ingressGrpc requires TLS. Standard nginx-ingress cannot negotiate + # HTTP/2 cleartext (h2c), and the chart sets + # nginx.ingress.kubernetes.io/ssl-redirect: "true" by default, so + # plaintext gRPC is redirected to HTTPS and fails without a cert. + # Enabling this block with an empty `tls:` is rejected at template time. + # For plaintext h2c, use server.grpcRoute (Gateway API) instead — see the + # "Gateway API as an alternative to Ingress" section below. ingressGrpc: enabled: true hosts: @@ -209,6 +216,90 @@ dashboard: - netbird.example.com ``` +## Gateway API as an alternative to Ingress + +Each of the Ingress blocks above has a mutually-exclusive Gateway API +counterpart. Use Gateway API when: + +- You already terminate TLS at a cluster-wide Gateway and don't want per-app + `Secret` references. +- You need **plaintext h2c for gRPC**. Standard nginx-ingress cannot + negotiate HTTP/2 cleartext, so `server.ingressGrpc` requires TLS; + `server.grpcRoute` does not. +- You prefer Gateway API's richer matching (header/method matches, filters, + traffic splitting). + +The chart renders **routes only** — `HTTPRoute`, `GRPCRoute`, `TCPRoute` — +and attaches them via `parentRefs` to a `Gateway` you already manage. TLS +is configured on that Gateway's listeners, not in these values. Enabling +an Ingress block and its Gateway API counterpart for the same traffic +class fails template rendering with a clear error. + +```yaml +server: + httpRoute: + enabled: true + parentRefs: + - name: my-gateway + namespace: gateway-system + sectionName: https + hostnames: + - netbird.example.com + rules: + - matches: + - path: { type: PathPrefix, value: /api } + - path: { type: PathPrefix, value: /oauth2 } + grpcRoute: + enabled: true + parentRefs: + - name: my-gateway + namespace: gateway-system + sectionName: https + hostnames: + - netbird.example.com + rules: + - matches: + - method: { service: signalexchange.SignalExchange } + - matches: + - method: { service: management.ManagementService } + relayHttpRoute: + enabled: true + parentRefs: + - name: my-gateway + namespace: gateway-system + sectionName: https + hostnames: + - netbird.example.com + rules: + - matches: + - path: { type: PathPrefix, value: /relay } + - path: { type: PathPrefix, value: /ws-proxy } + +dashboard: + httpRoute: + enabled: true + parentRefs: + - name: my-gateway + namespace: gateway-system + sectionName: https + hostnames: + - netbird.example.com + rules: + - matches: + - path: { type: PathPrefix, value: / } +``` + +Rules that omit `backendRefs` get the netbird server / dashboard `Service` +auto-filled on port 80. Specify `backendRefs` explicitly for traffic +splitting or non-default ports. + +For deployments that expose relay as a raw TCP listener (no HTTP path +matching), use `server.relayTcpRoute` — apiVersion +`gateway.networking.k8s.io/v1alpha2`. TCPRoute ships in the Gateway API +**experimental channel**; make sure its CRDs are installed +(`experimental-install.yaml` from the Gateway API release) before +enabling it. + ## STUN Networking NetBird's embedded STUN server uses **UDP port 3478**, which standard HTTP @@ -650,23 +741,55 @@ ADFS) can be tested manually: #### Server Ingress -| Key | Type | Default | Description | -| --------------------------------- | ------ | --------------- | ----------------------------------------- | -| `server.ingress.enabled` | bool | `false` | Create HTTP ingress (API + OAuth2) | -| `server.ingress.className` | string | `"nginx"` | Ingress class | -| `server.ingress.annotations` | object | `{}` | Ingress annotations | -| `server.ingress.hosts` | list | `[]` | Ingress host rules | -| `server.ingress.tls` | list | `[]` | TLS configuration | -| `server.ingressGrpc.enabled` | bool | `false` | Create gRPC ingress (Signal + Management) | -| `server.ingressGrpc.className` | string | `"nginx"` | Ingress class | -| `server.ingressGrpc.annotations` | object | see values.yaml | GRPC backend annotations | -| `server.ingressGrpc.hosts` | list | `[]` | Ingress host rules | -| `server.ingressGrpc.tls` | list | `[]` | TLS configuration | -| `server.ingressRelay.enabled` | bool | `false` | Create relay/WebSocket ingress | -| `server.ingressRelay.className` | string | `"nginx"` | Ingress class | -| `server.ingressRelay.annotations` | object | `{}` | Ingress annotations | -| `server.ingressRelay.hosts` | list | `[]` | Ingress host rules | -| `server.ingressRelay.tls` | list | `[]` | TLS configuration | +| Key | Type | Default | Description | +| --------------------------------- | ------ | --------------- | ------------------------------------------------------ | +| `server.ingress.enabled` | bool | `false` | Create HTTP ingress (API + OAuth2). Mutually exclusive with `server.httpRoute`. | +| `server.ingress.className` | string | `"nginx"` | Ingress class | +| `server.ingress.annotations` | object | `{}` | Ingress annotations | +| `server.ingress.hosts` | list | `[]` | Ingress host rules | +| `server.ingress.tls` | list | `[]` | TLS configuration | +| `server.ingressGrpc.enabled` | bool | `false` | Create gRPC ingress (Signal + Management). Mutually exclusive with `server.grpcRoute`. | +| `server.ingressGrpc.className` | string | `"nginx"` | Ingress class | +| `server.ingressGrpc.annotations` | object | see values.yaml | GRPC backend annotations | +| `server.ingressGrpc.hosts` | list | `[]` | Ingress host rules | +| `server.ingressGrpc.tls` | list | `[]` | TLS configuration | +| `server.ingressRelay.enabled` | bool | `false` | Create relay/WebSocket ingress. Mutually exclusive with `server.relayHttpRoute` and `server.relayTcpRoute`. | +| `server.ingressRelay.className` | string | `"nginx"` | Ingress class | +| `server.ingressRelay.annotations` | object | `{}` | Ingress annotations | +| `server.ingressRelay.hosts` | list | `[]` | Ingress host rules | +| `server.ingressRelay.tls` | list | `[]` | TLS configuration | + +#### Server Gateway API routes + +Gateway API alternatives to the Ingress blocks above. Enabling both an +Ingress and its matching route block is a template-time error. TLS is +terminated at the referenced Gateway's listeners, not in these values. + +| Key | Type | Default | Description | +| ------------------------------------ | ------ | ------- | --------------------------------------------------------------------- | +| `server.httpRoute.enabled` | bool | `false` | Create `HTTPRoute` for HTTP (API + OAuth2). Requires `parentRefs`. | +| `server.httpRoute.parentRefs` | list | `[]` | Gateways to attach to (`name`, `namespace`, optional `sectionName`). | +| `server.httpRoute.hostnames` | list | `[]` | HTTPRoute hostnames | +| `server.httpRoute.rules` | list | `[]` | `HTTPRoute.spec.rules`. Omitted `backendRefs` default to server Service on port 80. | +| `server.httpRoute.annotations` | object | `{}` | Route annotations | +| `server.httpRoute.labels` | object | `{}` | Extra labels | +| `server.grpcRoute.enabled` | bool | `false` | Create `GRPCRoute` for Signal + Management. Works with plaintext h2c. | +| `server.grpcRoute.parentRefs` | list | `[]` | Gateway parent refs | +| `server.grpcRoute.hostnames` | list | `[]` | GRPCRoute hostnames | +| `server.grpcRoute.rules` | list | `[]` | `GRPCRoute.spec.rules` (method or header matches) | +| `server.grpcRoute.annotations` | object | `{}` | Route annotations | +| `server.grpcRoute.labels` | object | `{}` | Extra labels | +| `server.relayHttpRoute.enabled` | bool | `false` | Create `HTTPRoute` for relay + WebSocket (default Gateway API path). | +| `server.relayHttpRoute.parentRefs` | list | `[]` | Gateway parent refs | +| `server.relayHttpRoute.hostnames` | list | `[]` | HTTPRoute hostnames | +| `server.relayHttpRoute.rules` | list | `[]` | `HTTPRoute.spec.rules` | +| `server.relayHttpRoute.annotations` | object | `{}` | Route annotations | +| `server.relayHttpRoute.labels` | object | `{}` | Extra labels | +| `server.relayTcpRoute.enabled` | bool | `false` | Create `TCPRoute` (`v1alpha2`) for raw-TCP relay listeners. | +| `server.relayTcpRoute.parentRefs` | list | `[]` | Gateway parent refs | +| `server.relayTcpRoute.rules` | list | `[]` | `TCPRoute.spec.rules`. Defaults to a single rule targeting server Service on port 80. | +| `server.relayTcpRoute.annotations` | object | `{}` | Route annotations | +| `server.relayTcpRoute.labels` | object | `{}` | Extra labels | #### Server Pod @@ -725,15 +848,21 @@ ADFS) can be tested manually: #### Dashboard Networking -| Key | Type | Default | Description | -| ------------------------------- | ------ | ------------- | ------------------------ | -| `dashboard.service.type` | string | `"ClusterIP"` | Dashboard service type | -| `dashboard.service.port` | int | `80` | Dashboard service port | -| `dashboard.ingress.enabled` | bool | `false` | Create dashboard ingress | -| `dashboard.ingress.className` | string | `"nginx"` | Ingress class | -| `dashboard.ingress.annotations` | object | `{}` | Ingress annotations | -| `dashboard.ingress.hosts` | list | `[]` | Ingress host rules | -| `dashboard.ingress.tls` | list | `[]` | TLS configuration | +| Key | Type | Default | Description | +| --------------------------------- | ------ | ------------- | -------------------------------------------------------------------- | +| `dashboard.service.type` | string | `"ClusterIP"` | Dashboard service type | +| `dashboard.service.port` | int | `80` | Dashboard service port | +| `dashboard.ingress.enabled` | bool | `false` | Create dashboard ingress. Mutually exclusive with `dashboard.httpRoute`. | +| `dashboard.ingress.className` | string | `"nginx"` | Ingress class | +| `dashboard.ingress.annotations` | object | `{}` | Ingress annotations | +| `dashboard.ingress.hosts` | list | `[]` | Ingress host rules | +| `dashboard.ingress.tls` | list | `[]` | TLS configuration | +| `dashboard.httpRoute.enabled` | bool | `false` | Create Gateway API `HTTPRoute` for the dashboard. Requires `parentRefs`. | +| `dashboard.httpRoute.parentRefs` | list | `[]` | Gateways to attach to | +| `dashboard.httpRoute.hostnames` | list | `[]` | HTTPRoute hostnames | +| `dashboard.httpRoute.rules` | list | `[]` | `HTTPRoute.spec.rules`. Omitted `backendRefs` default to dashboard Service on port 80. | +| `dashboard.httpRoute.annotations` | object | `{}` | Route annotations | +| `dashboard.httpRoute.labels` | object | `{}` | Extra labels | #### Dashboard Pod @@ -753,21 +882,28 @@ ADFS) can be tested manually: ## Architecture ``` -┌──────────────────────────────────────────────────────────┐ -│ Ingress Controller │ -│ │ -│ /api, /oauth2 ──────────┐ │ -│ /signalexchange/*, /management/* ──► Server Pod :80 │ -│ /relay, /ws-proxy ──────┘ (Management + Signal │ -│ + Relay combined) │ -│ / ──────────────────────────────► Dashboard Pod :80 │ -└──────────────────────────────────────────────────────────┘ +┌─────────────────────────────────────────────────────────────┐ +│ Ingress Controller ─or─ Gateway API Gateway │ +│ │ +│ /api, /oauth2 ─────┐ HTTPRoute ──► Server Pod :80 │ +│ /signalexchange/*, /management/* GRPCRoute │ +│ ├─────────────► Server Pod :80 │ +│ /relay, /ws-proxy ─┘ HTTPRoute / TCPRoute │ +│ │ +│ / ──────────────────── HTTPRoute ──► Dashboard Pod :80 │ +└─────────────────────────────────────────────────────────────┘ │ STUN Service :3478/UDP (LoadBalancer — separate IP, - cannot use HTTP Ingress) + cannot use HTTP Ingress/Gateway) ``` +Each traffic class picks **either** an Ingress **or** a Gateway API route, +independently, via `server.ingress{,Grpc,Relay}` / `server.httpRoute` / +`server.grpcRoute` / `server.relayHttpRoute` / `server.relayTcpRoute` and +`dashboard.ingress` / `dashboard.httpRoute`. Enabling both for the same +class is rejected at template time. + ## Upstream Source This chart is based on the [NetBird](https://github.com/netbirdio/netbird) project. See the `sources` field in `Chart.yaml` for details. diff --git a/charts/netbird/ci/e2e-values-gateway.yaml b/charts/netbird/ci/e2e-values-gateway.yaml new file mode 100644 index 0000000..ab425a8 --- /dev/null +++ b/charts/netbird/ci/e2e-values-gateway.yaml @@ -0,0 +1,92 @@ +# E2E values for exercising Gateway API support on a kind cluster with +# Envoy Gateway. Mirrors the sqlite-backend minimal config but swaps the +# four Ingress blocks for their Gateway API counterparts. + +database: + type: sqlite + +server: + persistentVolume: + enabled: false + + stunService: + type: ClusterIP + + config: + exposedAddress: "http://netbird-gateway-e2e-server.netbird-gateway-e2e.svc.cluster.local:80" + auth: + issuer: "https://auth.localhost" + dashboardRedirectURIs: + - "https://netbird.localhost/nb-auth" + - "https://netbird.localhost/nb-silent-auth" + + livenessProbe: + failureThreshold: 10 + initialDelaySeconds: 10 + periodSeconds: 5 + timeoutSeconds: 3 + tcpSocket: + port: http + readinessProbe: + failureThreshold: 10 + initialDelaySeconds: 10 + periodSeconds: 5 + timeoutSeconds: 3 + tcpSocket: + port: http + + httpRoute: + enabled: true + parentRefs: + - name: netbird-gateway + sectionName: http + hostnames: + - netbird.localhost + rules: + - matches: + - path: { type: PathPrefix, value: /api } + - path: { type: PathPrefix, value: /oauth2 } + + grpcRoute: + enabled: true + parentRefs: + - name: netbird-gateway + sectionName: http + hostnames: + - netbird.localhost + rules: + - matches: + - method: { service: signalexchange.SignalExchange } + - matches: + - method: { service: management.ManagementService } + + relayHttpRoute: + enabled: true + parentRefs: + - name: netbird-gateway + sectionName: http + hostnames: + - netbird.localhost + rules: + - matches: + - path: { type: PathPrefix, value: /relay } + - path: { type: PathPrefix, value: /ws-proxy } + +dashboard: + config: + mgmtApiEndpoint: "https://netbird.localhost" + mgmtGrpcApiEndpoint: "https://netbird.localhost" + authAuthority: "https://auth.localhost" + authClientId: "test-client" + authAudience: "test-audience" + + httpRoute: + enabled: true + parentRefs: + - name: netbird-gateway + sectionName: http + hostnames: + - netbird.localhost + rules: + - matches: + - path: { type: PathPrefix, value: / } diff --git a/charts/netbird/templates/_helpers.tpl b/charts/netbird/templates/_helpers.tpl index f0e21c8..804405f 100644 --- a/charts/netbird/templates/_helpers.tpl +++ b/charts/netbird/templates/_helpers.tpl @@ -127,6 +127,40 @@ here to keep `helm template` usable for partial inspection). {{- end -}} {{- end }} +{{/* +netbird.validate.routeExclusion — enforce mutual exclusion between the +Kubernetes Ingress and the Gateway API route for each traffic class. Both +resources would otherwise claim the same paths/hostnames and silently +create duplicate or racing rules. +*/}} +{{- define "netbird.validate.routeExclusion" -}} +{{- if and .Values.server.ingress.enabled .Values.server.httpRoute.enabled -}} + {{- fail "server.ingress.enabled and server.httpRoute.enabled are mutually exclusive — pick Kubernetes Ingress or Gateway API HTTPRoute for server HTTP traffic." -}} +{{- end -}} +{{- if and .Values.server.ingressGrpc.enabled .Values.server.grpcRoute.enabled -}} + {{- fail "server.ingressGrpc.enabled and server.grpcRoute.enabled are mutually exclusive — pick Kubernetes Ingress or Gateway API GRPCRoute for server gRPC traffic." -}} +{{- end -}} +{{- if and .Values.server.ingressRelay.enabled (or .Values.server.relayHttpRoute.enabled .Values.server.relayTcpRoute.enabled) -}} + {{- fail "server.ingressRelay.enabled conflicts with server.relayHttpRoute/relayTcpRoute — pick exactly one route type for relay/WebSocket traffic." -}} +{{- end -}} +{{- if and .Values.server.relayHttpRoute.enabled .Values.server.relayTcpRoute.enabled -}} + {{- fail "server.relayHttpRoute.enabled and server.relayTcpRoute.enabled are mutually exclusive — pick HTTPRoute or TCPRoute, not both." -}} +{{- end -}} +{{- if and .Values.dashboard.ingress.enabled .Values.dashboard.httpRoute.enabled -}} + {{- fail "dashboard.ingress.enabled and dashboard.httpRoute.enabled are mutually exclusive — pick Kubernetes Ingress or Gateway API HTTPRoute for the dashboard." -}} +{{- end -}} +{{- range $path := list "server.httpRoute" "server.grpcRoute" "server.relayHttpRoute" "server.relayTcpRoute" "dashboard.httpRoute" -}} + {{- $parts := splitList "." $path -}} + {{- $block := index $.Values (index $parts 0) (index $parts 1) -}} + {{- if and $block.enabled (not $block.parentRefs) -}} + {{- fail (printf "%s.enabled is true but %s.parentRefs is empty — Gateway API routes must reference at least one Gateway." $path $path) -}} + {{- end -}} +{{- end -}} +{{- if and .Values.server.ingressGrpc.enabled (not .Values.server.ingressGrpc.tls) -}} + {{- fail "server.ingressGrpc.enabled is true but server.ingressGrpc.tls is empty. gRPC over Kubernetes Ingress requires TLS: standard nginx-ingress cannot negotiate HTTP/2 cleartext (h2c) and the default `nginx.ingress.kubernetes.io/ssl-redirect: \"true\"` annotation redirects plaintext gRPC to HTTPS — without a cert, requests fail silently. Either configure server.ingressGrpc.tls, or disable server.ingressGrpc and expose gRPC via server.grpcRoute (Gateway API) with a controller that supports plaintext h2c." -}} +{{- end -}} +{{- end }} + {{/* netbird.server.generatedSecretName — name of the auto-generated Secret. */}} diff --git a/charts/netbird/templates/dashboard-httproute.yaml b/charts/netbird/templates/dashboard-httproute.yaml new file mode 100644 index 0000000..4b198f9 --- /dev/null +++ b/charts/netbird/templates/dashboard-httproute.yaml @@ -0,0 +1,36 @@ +{{- if .Values.dashboard.httpRoute.enabled }} +{{- $serviceName := include "netbird.dashboard.fullname" . }} +{{- $rules := list }} +{{- range .Values.dashboard.httpRoute.rules }} + {{- $rule := deepCopy . }} + {{- if not (hasKey $rule "backendRefs") }} + {{- $_ := set $rule "backendRefs" (list (dict "name" $serviceName "port" 80)) }} + {{- end }} + {{- $rules = append $rules $rule }} +{{- end }} +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: {{ $serviceName }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "netbird.dashboard.labels" . | nindent 4 }} + {{- with .Values.dashboard.httpRoute.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.dashboard.httpRoute.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + parentRefs: + {{- toYaml .Values.dashboard.httpRoute.parentRefs | nindent 4 }} + {{- with .Values.dashboard.httpRoute.hostnames }} + hostnames: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with $rules }} + rules: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/netbird/templates/server-configmap.yaml b/charts/netbird/templates/server-configmap.yaml index 907dfc1..5eb6704 100644 --- a/charts/netbird/templates/server-configmap.yaml +++ b/charts/netbird/templates/server-configmap.yaml @@ -1,4 +1,5 @@ {{- include "netbird.validate.exposedAddress" . -}} +{{- include "netbird.validate.routeExclusion" . -}} apiVersion: v1 kind: ConfigMap metadata: diff --git a/charts/netbird/templates/server-grpcroute.yaml b/charts/netbird/templates/server-grpcroute.yaml new file mode 100644 index 0000000..d85b795 --- /dev/null +++ b/charts/netbird/templates/server-grpcroute.yaml @@ -0,0 +1,36 @@ +{{- if .Values.server.grpcRoute.enabled }} +{{- $serviceName := include "netbird.server.fullname" . }} +{{- $rules := list }} +{{- range .Values.server.grpcRoute.rules }} + {{- $rule := deepCopy . }} + {{- if not (hasKey $rule "backendRefs") }} + {{- $_ := set $rule "backendRefs" (list (dict "name" $serviceName "port" 80)) }} + {{- end }} + {{- $rules = append $rules $rule }} +{{- end }} +apiVersion: gateway.networking.k8s.io/v1 +kind: GRPCRoute +metadata: + name: {{ $serviceName }}-grpc + namespace: {{ .Release.Namespace }} + labels: + {{- include "netbird.server.labels" . | nindent 4 }} + {{- with .Values.server.grpcRoute.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.server.grpcRoute.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + parentRefs: + {{- toYaml .Values.server.grpcRoute.parentRefs | nindent 4 }} + {{- with .Values.server.grpcRoute.hostnames }} + hostnames: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with $rules }} + rules: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/netbird/templates/server-httproute.yaml b/charts/netbird/templates/server-httproute.yaml new file mode 100644 index 0000000..945968d --- /dev/null +++ b/charts/netbird/templates/server-httproute.yaml @@ -0,0 +1,36 @@ +{{- if .Values.server.httpRoute.enabled }} +{{- $serviceName := include "netbird.server.fullname" . }} +{{- $rules := list }} +{{- range .Values.server.httpRoute.rules }} + {{- $rule := deepCopy . }} + {{- if not (hasKey $rule "backendRefs") }} + {{- $_ := set $rule "backendRefs" (list (dict "name" $serviceName "port" 80)) }} + {{- end }} + {{- $rules = append $rules $rule }} +{{- end }} +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: {{ $serviceName }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "netbird.server.labels" . | nindent 4 }} + {{- with .Values.server.httpRoute.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.server.httpRoute.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + parentRefs: + {{- toYaml .Values.server.httpRoute.parentRefs | nindent 4 }} + {{- with .Values.server.httpRoute.hostnames }} + hostnames: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with $rules }} + rules: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/netbird/templates/server-relay-httproute.yaml b/charts/netbird/templates/server-relay-httproute.yaml new file mode 100644 index 0000000..415df79 --- /dev/null +++ b/charts/netbird/templates/server-relay-httproute.yaml @@ -0,0 +1,36 @@ +{{- if .Values.server.relayHttpRoute.enabled }} +{{- $serviceName := include "netbird.server.fullname" . }} +{{- $rules := list }} +{{- range .Values.server.relayHttpRoute.rules }} + {{- $rule := deepCopy . }} + {{- if not (hasKey $rule "backendRefs") }} + {{- $_ := set $rule "backendRefs" (list (dict "name" $serviceName "port" 80)) }} + {{- end }} + {{- $rules = append $rules $rule }} +{{- end }} +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: {{ $serviceName }}-relay + namespace: {{ .Release.Namespace }} + labels: + {{- include "netbird.server.labels" . | nindent 4 }} + {{- with .Values.server.relayHttpRoute.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.server.relayHttpRoute.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + parentRefs: + {{- toYaml .Values.server.relayHttpRoute.parentRefs | nindent 4 }} + {{- with .Values.server.relayHttpRoute.hostnames }} + hostnames: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with $rules }} + rules: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/netbird/templates/server-relay-tcproute.yaml b/charts/netbird/templates/server-relay-tcproute.yaml new file mode 100644 index 0000000..8bff12f --- /dev/null +++ b/charts/netbird/templates/server-relay-tcproute.yaml @@ -0,0 +1,33 @@ +{{- if .Values.server.relayTcpRoute.enabled }} +{{- $serviceName := include "netbird.server.fullname" . }} +{{- $rules := list }} +{{- range .Values.server.relayTcpRoute.rules }} + {{- $rule := deepCopy . }} + {{- if not (hasKey $rule "backendRefs") }} + {{- $_ := set $rule "backendRefs" (list (dict "name" $serviceName "port" 80)) }} + {{- end }} + {{- $rules = append $rules $rule }} +{{- end }} +{{- if not $rules }} + {{- $rules = list (dict "backendRefs" (list (dict "name" $serviceName "port" 80))) }} +{{- end }} +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: TCPRoute +metadata: + name: {{ $serviceName }}-relay-tcp + namespace: {{ .Release.Namespace }} + labels: + {{- include "netbird.server.labels" . | nindent 4 }} + {{- with .Values.server.relayTcpRoute.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.server.relayTcpRoute.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + parentRefs: + {{- toYaml .Values.server.relayTcpRoute.parentRefs | nindent 4 }} + rules: + {{- toYaml $rules | nindent 4 }} +{{- end }} diff --git a/charts/netbird/tests/dashboard-httproute_test.yaml b/charts/netbird/tests/dashboard-httproute_test.yaml new file mode 100644 index 0000000..ccd28ad --- /dev/null +++ b/charts/netbird/tests/dashboard-httproute_test.yaml @@ -0,0 +1,50 @@ +suite: dashboard HTTPRoute tests +templates: + - templates/dashboard-httproute.yaml +tests: + - it: should not render when disabled + asserts: + - hasDocuments: + count: 0 + + - it: should render HTTPRoute when enabled + set: + dashboard.httpRoute.enabled: true + dashboard.httpRoute.parentRefs: + - name: gw + dashboard.httpRoute.rules: + - matches: + - path: { type: PathPrefix, value: / } + asserts: + - hasDocuments: + count: 1 + - isKind: + of: HTTPRoute + - isAPIVersion: + of: gateway.networking.k8s.io/v1 + + - it: should carry dashboard component label + set: + dashboard.httpRoute.enabled: true + dashboard.httpRoute.parentRefs: + - name: gw + asserts: + - equal: + path: metadata.labels["app.kubernetes.io/component"] + value: dashboard + + - it: should auto-fill backendRefs to dashboard service when omitted + set: + dashboard.httpRoute.enabled: true + dashboard.httpRoute.parentRefs: + - name: gw + dashboard.httpRoute.rules: + - matches: + - path: { type: PathPrefix, value: / } + asserts: + - equal: + path: spec.rules[0].backendRefs[0].name + value: RELEASE-NAME-netbird-dashboard + - equal: + path: spec.rules[0].backendRefs[0].port + value: 80 diff --git a/charts/netbird/tests/server-configmap_test.yaml b/charts/netbird/tests/server-configmap_test.yaml index c972234..aaf1d1b 100644 --- a/charts/netbird/tests/server-configmap_test.yaml +++ b/charts/netbird/tests/server-configmap_test.yaml @@ -368,3 +368,90 @@ tests: - matchRegex: path: data["config.yaml.tpl"] pattern: 'authAudience: "my\$\{DOLLAR\}api"' + + # ── Route mutual-exclusion validation ───────────────────────────────── + + - it: should fail when server ingress and httpRoute are both enabled + set: + server.ingress.enabled: true + server.httpRoute.enabled: true + server.httpRoute.parentRefs: + - name: gw + asserts: + - failedTemplate: + errorPattern: "server.ingress.enabled and server.httpRoute.enabled are mutually exclusive" + + - it: should fail when server ingressGrpc and grpcRoute are both enabled + set: + server.ingressGrpc.enabled: true + server.grpcRoute.enabled: true + server.grpcRoute.parentRefs: + - name: gw + asserts: + - failedTemplate: + errorPattern: "server.ingressGrpc.enabled and server.grpcRoute.enabled are mutually exclusive" + + - it: should fail when server ingressRelay and relayHttpRoute are both enabled + set: + server.ingressRelay.enabled: true + server.relayHttpRoute.enabled: true + server.relayHttpRoute.parentRefs: + - name: gw + asserts: + - failedTemplate: + errorPattern: "conflicts with server.relayHttpRoute/relayTcpRoute" + + - it: should fail when server relayHttpRoute and relayTcpRoute are both enabled + set: + server.relayHttpRoute.enabled: true + server.relayHttpRoute.parentRefs: + - name: gw + server.relayTcpRoute.enabled: true + server.relayTcpRoute.parentRefs: + - name: gw + asserts: + - failedTemplate: + errorPattern: "server.relayHttpRoute.enabled and server.relayTcpRoute.enabled are mutually exclusive" + + - it: should fail when dashboard ingress and httpRoute are both enabled + set: + dashboard.ingress.enabled: true + dashboard.httpRoute.enabled: true + dashboard.httpRoute.parentRefs: + - name: gw + asserts: + - failedTemplate: + errorPattern: "dashboard.ingress.enabled and dashboard.httpRoute.enabled are mutually exclusive" + + - it: should fail when httpRoute enabled without parentRefs + set: + server.httpRoute.enabled: true + asserts: + - failedTemplate: + errorPattern: "server.httpRoute.parentRefs is empty" + + - it: should fail when ingressGrpc is enabled without TLS + set: + server.ingressGrpc.enabled: true + server.ingressGrpc.hosts: + - host: netbird.example.com + paths: + - path: /signalexchange.SignalExchange + asserts: + - failedTemplate: + errorPattern: "server.ingressGrpc.tls is empty" + + - it: should accept ingressGrpc when TLS is configured + set: + server.ingressGrpc.enabled: true + server.ingressGrpc.hosts: + - host: netbird.example.com + paths: + - path: /signalexchange.SignalExchange + server.ingressGrpc.tls: + - secretName: netbird-tls + hosts: + - netbird.example.com + asserts: + - hasDocuments: + count: 1 diff --git a/charts/netbird/tests/server-grpcroute_test.yaml b/charts/netbird/tests/server-grpcroute_test.yaml new file mode 100644 index 0000000..6fcae81 --- /dev/null +++ b/charts/netbird/tests/server-grpcroute_test.yaml @@ -0,0 +1,88 @@ +suite: server GRPCRoute tests +templates: + - templates/server-grpcroute.yaml +tests: + - it: should not render when disabled + asserts: + - hasDocuments: + count: 0 + + - it: should render GRPCRoute when enabled + set: + server.grpcRoute.enabled: true + server.grpcRoute.parentRefs: + - name: my-gateway + server.grpcRoute.rules: + - matches: + - method: { service: signalexchange.SignalExchange } + asserts: + - hasDocuments: + count: 1 + - isKind: + of: GRPCRoute + - isAPIVersion: + of: gateway.networking.k8s.io/v1 + + - it: should have -grpc suffix in name + set: + server.grpcRoute.enabled: true + server.grpcRoute.parentRefs: + - name: gw + asserts: + - matchRegex: + path: metadata.name + pattern: "-grpc$" + + - it: should pass method matches through + set: + server.grpcRoute.enabled: true + server.grpcRoute.parentRefs: + - name: gw + server.grpcRoute.rules: + - matches: + - method: { service: signalexchange.SignalExchange } + - matches: + - method: { service: management.ManagementService } + asserts: + - equal: + path: spec.rules[0].matches[0].method.service + value: signalexchange.SignalExchange + - equal: + path: spec.rules[1].matches[0].method.service + value: management.ManagementService + + - it: should auto-fill backendRefs when omitted + set: + server.grpcRoute.enabled: true + server.grpcRoute.parentRefs: + - name: gw + server.grpcRoute.rules: + - matches: + - method: { service: signalexchange.SignalExchange } + asserts: + - equal: + path: spec.rules[0].backendRefs[0].name + value: RELEASE-NAME-netbird-server + - equal: + path: spec.rules[0].backendRefs[0].port + value: 80 + + - it: should support header-based matches (traefik-style) + set: + server.grpcRoute.enabled: true + server.grpcRoute.parentRefs: + - name: traefik-gateway + namespace: kube-system + sectionName: web + server.grpcRoute.hostnames: + - vpn.example.com + server.grpcRoute.rules: + - matches: + - headers: + - type: RegularExpression + name: Content-Type + value: "application/grpc.*" + asserts: + - equal: + path: spec.rules[0].matches[0].headers[0].name + value: Content-Type diff --git a/charts/netbird/tests/server-httproute_test.yaml b/charts/netbird/tests/server-httproute_test.yaml new file mode 100644 index 0000000..a0e66c0 --- /dev/null +++ b/charts/netbird/tests/server-httproute_test.yaml @@ -0,0 +1,117 @@ +suite: server HTTPRoute tests +templates: + - templates/server-httproute.yaml +tests: + - it: should not render when disabled + asserts: + - hasDocuments: + count: 0 + + - it: should render HTTPRoute when enabled + set: + server.httpRoute.enabled: true + server.httpRoute.parentRefs: + - name: my-gateway + namespace: gateway-system + server.httpRoute.hostnames: + - netbird.example.com + server.httpRoute.rules: + - matches: + - path: { type: PathPrefix, value: /api } + asserts: + - hasDocuments: + count: 1 + - isKind: + of: HTTPRoute + - isAPIVersion: + of: gateway.networking.k8s.io/v1 + + - it: should pass parentRefs through + set: + server.httpRoute.enabled: true + server.httpRoute.parentRefs: + - name: my-gateway + namespace: gateway-system + sectionName: https + asserts: + - equal: + path: spec.parentRefs[0].name + value: my-gateway + - equal: + path: spec.parentRefs[0].namespace + value: gateway-system + - equal: + path: spec.parentRefs[0].sectionName + value: https + + - it: should pass hostnames through + set: + server.httpRoute.enabled: true + server.httpRoute.parentRefs: + - name: gw + server.httpRoute.hostnames: + - netbird.example.com + - netbird.example.org + asserts: + - contains: + path: spec.hostnames + content: netbird.example.com + - contains: + path: spec.hostnames + content: netbird.example.org + + - it: should auto-fill backendRefs when omitted + set: + server.httpRoute.enabled: true + server.httpRoute.parentRefs: + - name: gw + server.httpRoute.rules: + - matches: + - path: { type: PathPrefix, value: /api } + asserts: + - equal: + path: spec.rules[0].backendRefs[0].name + value: RELEASE-NAME-netbird-server + - equal: + path: spec.rules[0].backendRefs[0].port + value: 80 + + - it: should pass user backendRefs through unchanged + set: + server.httpRoute.enabled: true + server.httpRoute.parentRefs: + - name: gw + server.httpRoute.rules: + - matches: + - path: { type: PathPrefix, value: /api } + backendRefs: + - name: custom-svc + port: 8080 + weight: 100 + asserts: + - equal: + path: spec.rules[0].backendRefs[0].name + value: custom-svc + - equal: + path: spec.rules[0].backendRefs[0].port + value: 8080 + - equal: + path: spec.rules[0].backendRefs[0].weight + value: 100 + + - it: should render annotations and labels + set: + server.httpRoute.enabled: true + server.httpRoute.parentRefs: + - name: gw + server.httpRoute.annotations: + foo: bar + server.httpRoute.labels: + team: netbird + asserts: + - equal: + path: metadata.annotations.foo + value: bar + - equal: + path: metadata.labels.team + value: netbird diff --git a/charts/netbird/tests/server-relay-httproute_test.yaml b/charts/netbird/tests/server-relay-httproute_test.yaml new file mode 100644 index 0000000..a241cdd --- /dev/null +++ b/charts/netbird/tests/server-relay-httproute_test.yaml @@ -0,0 +1,49 @@ +suite: server relay HTTPRoute tests +templates: + - templates/server-relay-httproute.yaml +tests: + - it: should not render when disabled + asserts: + - hasDocuments: + count: 0 + + - it: should render HTTPRoute when enabled + set: + server.relayHttpRoute.enabled: true + server.relayHttpRoute.parentRefs: + - name: gw + server.relayHttpRoute.rules: + - matches: + - path: { type: PathPrefix, value: /relay } + asserts: + - hasDocuments: + count: 1 + - isKind: + of: HTTPRoute + + - it: should have -relay suffix in name + set: + server.relayHttpRoute.enabled: true + server.relayHttpRoute.parentRefs: + - name: gw + asserts: + - matchRegex: + path: metadata.name + pattern: "-relay$" + + - it: should pass path matches through + set: + server.relayHttpRoute.enabled: true + server.relayHttpRoute.parentRefs: + - name: gw + server.relayHttpRoute.rules: + - matches: + - path: { type: PathPrefix, value: /relay } + - path: { type: PathPrefix, value: /ws-proxy } + asserts: + - equal: + path: spec.rules[0].matches[0].path.value + value: /relay + - equal: + path: spec.rules[0].matches[1].path.value + value: /ws-proxy diff --git a/charts/netbird/tests/server-relay-tcproute_test.yaml b/charts/netbird/tests/server-relay-tcproute_test.yaml new file mode 100644 index 0000000..cb7e49e --- /dev/null +++ b/charts/netbird/tests/server-relay-tcproute_test.yaml @@ -0,0 +1,61 @@ +suite: server relay TCPRoute tests +templates: + - templates/server-relay-tcproute.yaml +tests: + - it: should not render when disabled + asserts: + - hasDocuments: + count: 0 + + - it: should render TCPRoute when enabled + set: + server.relayTcpRoute.enabled: true + server.relayTcpRoute.parentRefs: + - name: gw + asserts: + - hasDocuments: + count: 1 + - isKind: + of: TCPRoute + - isAPIVersion: + of: gateway.networking.k8s.io/v1alpha2 + + - it: should have -relay-tcp suffix in name + set: + server.relayTcpRoute.enabled: true + server.relayTcpRoute.parentRefs: + - name: gw + asserts: + - matchRegex: + path: metadata.name + pattern: "-relay-tcp$" + + - it: should default to a single backend rule when rules omitted + set: + server.relayTcpRoute.enabled: true + server.relayTcpRoute.parentRefs: + - name: gw + asserts: + - equal: + path: spec.rules[0].backendRefs[0].name + value: RELEASE-NAME-netbird-server + - equal: + path: spec.rules[0].backendRefs[0].port + value: 80 + + - it: should pass user backendRefs through + set: + server.relayTcpRoute.enabled: true + server.relayTcpRoute.parentRefs: + - name: gw + server.relayTcpRoute.rules: + - backendRefs: + - name: custom + port: 9000 + asserts: + - equal: + path: spec.rules[0].backendRefs[0].name + value: custom + - equal: + path: spec.rules[0].backendRefs[0].port + value: 9000 diff --git a/charts/netbird/values.yaml b/charts/netbird/values.yaml index 24cdd14..ca72749 100644 --- a/charts/netbird/values.yaml +++ b/charts/netbird/values.yaml @@ -424,6 +424,7 @@ server: annotations: {} # -- Ingress for HTTP routes (API + OAuth2). + # Mutually exclusive with server.httpRoute. ingress: enabled: false className: "nginx" @@ -431,7 +432,32 @@ server: hosts: [] tls: [] + # -- Gateway API HTTPRoute for HTTP routes (API + OAuth2). + # Mutually exclusive with server.ingress. TLS is terminated at the Gateway + # listener, so no tls block here. parentRefs and hostnames are required + # when enabled. rules pass through verbatim to HTTPRoute.spec.rules; + # backendRefs are auto-filled from the netbird server Service when a rule + # omits them. + httpRoute: + enabled: false + parentRefs: [] + # - name: my-gateway + # namespace: gateway-system + # sectionName: https + hostnames: [] + rules: [] + # - matches: + # - path: { type: PathPrefix, value: /api } + # - path: { type: PathPrefix, value: /oauth2 } + annotations: {} + labels: {} + # -- Ingress for gRPC routes (signal + management gRPC). + # Mutually exclusive with server.grpcRoute. Requires TLS: standard + # nginx-ingress cannot negotiate HTTP/2 cleartext (h2c) and + # `ssl-redirect: "true"` (set by default in this block) will redirect + # plaintext gRPC to HTTPS. For plaintext h2c, use server.grpcRoute with a + # Gateway API controller that supports it (Envoy Gateway, Traefik, …). ingressGrpc: enabled: false className: "nginx" @@ -443,7 +469,24 @@ server: hosts: [] tls: [] + # -- Gateway API GRPCRoute for gRPC routes (signal + management). + # Mutually exclusive with server.ingressGrpc. Gateway controllers forward + # gRPC over HTTP/2 — including plaintext h2c — so this works without TLS + # when the Gateway listener is HTTP. rules pass through to + # GRPCRoute.spec.rules; backendRefs are auto-filled when omitted. + grpcRoute: + enabled: false + parentRefs: [] + hostnames: [] + rules: [] + # - matches: + # - method: { service: signalexchange.SignalExchange } + # - method: { service: management.ManagementService } + annotations: {} + labels: {} + # -- Ingress for relay and WebSocket routes. + # Mutually exclusive with server.relayHttpRoute and server.relayTcpRoute. ingressRelay: enabled: false className: "nginx" @@ -451,6 +494,35 @@ server: hosts: [] tls: [] + # -- Gateway API HTTPRoute for relay + WebSocket. + # Default choice for Gateway API — WebSocket uses HTTP/1.1 Upgrade which + # HTTPRoute handles natively. Mutually exclusive with server.ingressRelay + # and server.relayTcpRoute. + relayHttpRoute: + enabled: false + parentRefs: [] + hostnames: [] + rules: [] + # - matches: + # - path: { type: PathPrefix, value: /relay } + # - path: { type: PathPrefix, value: /ws-proxy } + annotations: {} + labels: {} + + # -- Gateway API TCPRoute for relay exposed as raw TCP. + # Alternative to relayHttpRoute for deployments that front relay with a + # dedicated TCP listener (no HTTP path matching). TCPRoute is v1alpha2. + # Mutually exclusive with server.ingressRelay and server.relayHttpRoute. + relayTcpRoute: + enabled: false + parentRefs: [] + rules: [] + # - backendRefs: + # - name: + # port: 80 + annotations: {} + labels: {} + # -- CPU/memory resource requests and limits. resources: {} nodeSelector: {} @@ -513,6 +585,8 @@ dashboard: type: ClusterIP port: 80 + # -- Ingress for the dashboard web UI. + # Mutually exclusive with dashboard.httpRoute. ingress: enabled: false className: "nginx" @@ -520,6 +594,20 @@ dashboard: hosts: [] tls: [] + # -- Gateway API HTTPRoute for the dashboard web UI. + # Mutually exclusive with dashboard.ingress. TLS is terminated at the + # Gateway listener. rules pass through; backendRefs auto-filled when + # omitted. + httpRoute: + enabled: false + parentRefs: [] + hostnames: [] + rules: [] + # - matches: + # - path: { type: PathPrefix, value: / } + annotations: {} + labels: {} + resources: {} nodeSelector: {} tolerations: [] diff --git a/ci/scripts/netbird/e2e-gateway.sh b/ci/scripts/netbird/e2e-gateway.sh new file mode 100755 index 0000000..86b0a5e --- /dev/null +++ b/ci/scripts/netbird/e2e-gateway.sh @@ -0,0 +1,175 @@ +#!/usr/bin/env bash +# +# E2E test for the netbird chart's Gateway API support. +# +# Installs Gateway API CRDs + Envoy Gateway on the kind cluster, provisions +# a Gateway, and verifies the chart's HTTPRoute/GRPCRoute resources attach +# successfully (Accepted=True) with the correct backend references. +# +# Scope: route integration only. The full peer-registration/PAT flow is +# already covered by ci/scripts/netbird/e2e.sh — this test focuses on the +# Gateway API surface added in #74. +# +set -euo pipefail + +RELEASE="netbird-gateway-e2e" +NAMESPACE="netbird-gateway-e2e" +CHART="charts/netbird" +VALUES_FILE="$CHART/ci/e2e-values-gateway.yaml" +TIMEOUT="10m" + +# Pinned versions so CI is reproducible. +GATEWAY_API_VERSION="v1.2.1" +ENVOY_GATEWAY_VERSION="v1.5.3" +ENVOY_NS="envoy-gateway-system" + +log() { echo "==> $*"; } +fail() { echo "FAIL: $*" >&2; exit 1; } + +cleanup() { + log "Cleaning up..." + helm uninstall "$RELEASE" -n "$NAMESPACE" --ignore-not-found 2>/dev/null || true + kubectl delete namespace "$NAMESPACE" --ignore-not-found 2>/dev/null || true + # Leave Envoy Gateway + CRDs installed to speed up re-runs on the same + # kind cluster; the kind cluster itself is torn down by CI. +} +trap cleanup EXIT + +# ── Install Gateway API CRDs ────────────────────────────────────────── +log "Installing Gateway API CRDs ($GATEWAY_API_VERSION)..." +kubectl apply -f \ + "https://github.com/kubernetes-sigs/gateway-api/releases/download/$GATEWAY_API_VERSION/standard-install.yaml" +# TCPRoute lives in the experimental channel (still v1alpha2). +kubectl apply -f \ + "https://github.com/kubernetes-sigs/gateway-api/releases/download/$GATEWAY_API_VERSION/experimental-install.yaml" + +# ── Install Envoy Gateway ───────────────────────────────────────────── +log "Installing Envoy Gateway ($ENVOY_GATEWAY_VERSION)..." +helm upgrade --install eg oci://docker.io/envoyproxy/gateway-helm \ + --version "$ENVOY_GATEWAY_VERSION" \ + -n "$ENVOY_NS" \ + --create-namespace \ + --wait --timeout 5m + +# ── Create netbird namespace ────────────────────────────────────────── +log "Creating namespace $NAMESPACE..." +kubectl create namespace "$NAMESPACE" --dry-run=client -o yaml | kubectl apply -f - + +# ── Provision a Gateway for the routes to attach to ─────────────────── +log "Applying GatewayClass + Gateway..." +kubectl apply -f - <0: r.append(BASE62[n%62]); n//=62 + return ''.join(reversed(r)).rjust(l,'0') +s='TestSecretValue1234567890ABCDE' +print('nbp_'+s+b62(binascii.crc32(s.encode())&0xFFFFFFFF)) +") + kubectl -n "$NAMESPACE" create secret generic netbird-pat \ + --from-literal=token="$PAT_TOKEN" +} +generate_pat_secret + +# ── Install netbird chart ───────────────────────────────────────────── +log "Installing netbird chart with Gateway API routes enabled..." +if ! helm install "$RELEASE" "$CHART" \ + -n "$NAMESPACE" \ + -f "$VALUES_FILE" \ + --set pat.enabled=true \ + --set pat.secret.secretName=netbird-pat \ + --set server.persistentVolume.enabled=true \ + --timeout "$TIMEOUT"; then + log "Helm install failed — dumping logs..." + kubectl -n "$NAMESPACE" logs deployment/"$RELEASE"-server -c pat-seed 2>/dev/null || true + fail "Helm install failed" +fi + +log "Verifying deployments..." +kubectl -n "$NAMESPACE" rollout status deployment/"$RELEASE"-server --timeout=300s +kubectl -n "$NAMESPACE" rollout status deployment/"$RELEASE"-dashboard --timeout=120s + +# ── Verify the routes attached to the Gateway ───────────────────────── +wait_route_accepted() { + local kind="$1" name="$2" + log "Waiting for $kind/$name to report Accepted=True..." + for _ in $(seq 1 30); do + # Route status uses parents[].conditions[type=Accepted]. + local accepted + accepted=$(kubectl -n "$NAMESPACE" get "$kind" "$name" \ + -o jsonpath='{.status.parents[0].conditions[?(@.type=="Accepted")].status}' 2>/dev/null || true) + if [ "$accepted" = "True" ]; then + log " $kind/$name Accepted=True" + return 0 + fi + sleep 3 + done + log "$kind/$name did NOT reach Accepted=True. Full status:" + kubectl -n "$NAMESPACE" get "$kind" "$name" -o yaml | sed -n '/^status:/,$p' || true + fail "$kind/$name was not accepted by Gateway" +} + +wait_route_accepted httproute "$RELEASE-server" +wait_route_accepted httproute "$RELEASE-server-relay" +wait_route_accepted httproute "$RELEASE-dashboard" +wait_route_accepted grpcroute "$RELEASE-server-grpc" + +# ── Confirm backendRefs auto-filled correctly ──────────────────────── +assert_backend_ref() { + local kind="$1" name="$2" expected_svc="$3" + local svc + svc=$(kubectl -n "$NAMESPACE" get "$kind" "$name" \ + -o jsonpath='{.spec.rules[0].backendRefs[0].name}') + if [ "$svc" != "$expected_svc" ]; then + fail "$kind/$name backendRefs[0].name = '$svc' (expected '$expected_svc')" + fi + log " $kind/$name backendRefs[0].name = $svc ✓" +} + +assert_backend_ref httproute "$RELEASE-server" "$RELEASE-server" +assert_backend_ref httproute "$RELEASE-server-relay" "$RELEASE-server" +assert_backend_ref httproute "$RELEASE-dashboard" "$RELEASE-dashboard" +assert_backend_ref grpcroute "$RELEASE-server-grpc" "$RELEASE-server" + +# ── Confirm mutual-exclusion validation trips ──────────────────────── +log "Verifying template validation: enabling Ingress + HTTPRoute should fail..." +if helm template "$RELEASE" "$CHART" \ + -f "$VALUES_FILE" \ + --set server.ingress.enabled=true > /dev/null 2>&1; then + fail "Template rendered with both Ingress and HTTPRoute enabled — mutual-exclusion check missing" +fi +log " mutual-exclusion validation correctly rejects the combination ✓" + +log "E2E Gateway API test PASSED — routes accepted, backendRefs auto-filled, exclusion validated." From 24b40c5711f0712975ba43d75cafe10d4ead8adc Mon Sep 17 00:00:00 2001 From: mikkeldamsgaard Date: Tue, 21 Apr 2026 23:35:06 +0200 Subject: [PATCH 2/4] style(netbird): dprint-format README and values.yaml Co-Authored-By: Claude Opus 4.7 (1M context) --- charts/netbird/README.md | 110 ++++++++++++++++++------------------- charts/netbird/values.yaml | 34 ++++++------ 2 files changed, 72 insertions(+), 72 deletions(-) diff --git a/charts/netbird/README.md b/charts/netbird/README.md index 5ab1fa0..4343ba9 100644 --- a/charts/netbird/README.md +++ b/charts/netbird/README.md @@ -741,23 +741,23 @@ ADFS) can be tested manually: #### Server Ingress -| Key | Type | Default | Description | -| --------------------------------- | ------ | --------------- | ------------------------------------------------------ | -| `server.ingress.enabled` | bool | `false` | Create HTTP ingress (API + OAuth2). Mutually exclusive with `server.httpRoute`. | -| `server.ingress.className` | string | `"nginx"` | Ingress class | -| `server.ingress.annotations` | object | `{}` | Ingress annotations | -| `server.ingress.hosts` | list | `[]` | Ingress host rules | -| `server.ingress.tls` | list | `[]` | TLS configuration | -| `server.ingressGrpc.enabled` | bool | `false` | Create gRPC ingress (Signal + Management). Mutually exclusive with `server.grpcRoute`. | -| `server.ingressGrpc.className` | string | `"nginx"` | Ingress class | -| `server.ingressGrpc.annotations` | object | see values.yaml | GRPC backend annotations | -| `server.ingressGrpc.hosts` | list | `[]` | Ingress host rules | -| `server.ingressGrpc.tls` | list | `[]` | TLS configuration | +| Key | Type | Default | Description | +| --------------------------------- | ------ | --------------- | ----------------------------------------------------------------------------------------------------------- | +| `server.ingress.enabled` | bool | `false` | Create HTTP ingress (API + OAuth2). Mutually exclusive with `server.httpRoute`. | +| `server.ingress.className` | string | `"nginx"` | Ingress class | +| `server.ingress.annotations` | object | `{}` | Ingress annotations | +| `server.ingress.hosts` | list | `[]` | Ingress host rules | +| `server.ingress.tls` | list | `[]` | TLS configuration | +| `server.ingressGrpc.enabled` | bool | `false` | Create gRPC ingress (Signal + Management). Mutually exclusive with `server.grpcRoute`. | +| `server.ingressGrpc.className` | string | `"nginx"` | Ingress class | +| `server.ingressGrpc.annotations` | object | see values.yaml | GRPC backend annotations | +| `server.ingressGrpc.hosts` | list | `[]` | Ingress host rules | +| `server.ingressGrpc.tls` | list | `[]` | TLS configuration | | `server.ingressRelay.enabled` | bool | `false` | Create relay/WebSocket ingress. Mutually exclusive with `server.relayHttpRoute` and `server.relayTcpRoute`. | -| `server.ingressRelay.className` | string | `"nginx"` | Ingress class | -| `server.ingressRelay.annotations` | object | `{}` | Ingress annotations | -| `server.ingressRelay.hosts` | list | `[]` | Ingress host rules | -| `server.ingressRelay.tls` | list | `[]` | TLS configuration | +| `server.ingressRelay.className` | string | `"nginx"` | Ingress class | +| `server.ingressRelay.annotations` | object | `{}` | Ingress annotations | +| `server.ingressRelay.hosts` | list | `[]` | Ingress host rules | +| `server.ingressRelay.tls` | list | `[]` | TLS configuration | #### Server Gateway API routes @@ -765,31 +765,31 @@ Gateway API alternatives to the Ingress blocks above. Enabling both an Ingress and its matching route block is a template-time error. TLS is terminated at the referenced Gateway's listeners, not in these values. -| Key | Type | Default | Description | -| ------------------------------------ | ------ | ------- | --------------------------------------------------------------------- | -| `server.httpRoute.enabled` | bool | `false` | Create `HTTPRoute` for HTTP (API + OAuth2). Requires `parentRefs`. | -| `server.httpRoute.parentRefs` | list | `[]` | Gateways to attach to (`name`, `namespace`, optional `sectionName`). | -| `server.httpRoute.hostnames` | list | `[]` | HTTPRoute hostnames | -| `server.httpRoute.rules` | list | `[]` | `HTTPRoute.spec.rules`. Omitted `backendRefs` default to server Service on port 80. | -| `server.httpRoute.annotations` | object | `{}` | Route annotations | -| `server.httpRoute.labels` | object | `{}` | Extra labels | -| `server.grpcRoute.enabled` | bool | `false` | Create `GRPCRoute` for Signal + Management. Works with plaintext h2c. | -| `server.grpcRoute.parentRefs` | list | `[]` | Gateway parent refs | -| `server.grpcRoute.hostnames` | list | `[]` | GRPCRoute hostnames | -| `server.grpcRoute.rules` | list | `[]` | `GRPCRoute.spec.rules` (method or header matches) | -| `server.grpcRoute.annotations` | object | `{}` | Route annotations | -| `server.grpcRoute.labels` | object | `{}` | Extra labels | -| `server.relayHttpRoute.enabled` | bool | `false` | Create `HTTPRoute` for relay + WebSocket (default Gateway API path). | -| `server.relayHttpRoute.parentRefs` | list | `[]` | Gateway parent refs | -| `server.relayHttpRoute.hostnames` | list | `[]` | HTTPRoute hostnames | -| `server.relayHttpRoute.rules` | list | `[]` | `HTTPRoute.spec.rules` | -| `server.relayHttpRoute.annotations` | object | `{}` | Route annotations | -| `server.relayHttpRoute.labels` | object | `{}` | Extra labels | -| `server.relayTcpRoute.enabled` | bool | `false` | Create `TCPRoute` (`v1alpha2`) for raw-TCP relay listeners. | -| `server.relayTcpRoute.parentRefs` | list | `[]` | Gateway parent refs | -| `server.relayTcpRoute.rules` | list | `[]` | `TCPRoute.spec.rules`. Defaults to a single rule targeting server Service on port 80. | -| `server.relayTcpRoute.annotations` | object | `{}` | Route annotations | -| `server.relayTcpRoute.labels` | object | `{}` | Extra labels | +| Key | Type | Default | Description | +| ----------------------------------- | ------ | ------- | ------------------------------------------------------------------------------------- | +| `server.httpRoute.enabled` | bool | `false` | Create `HTTPRoute` for HTTP (API + OAuth2). Requires `parentRefs`. | +| `server.httpRoute.parentRefs` | list | `[]` | Gateways to attach to (`name`, `namespace`, optional `sectionName`). | +| `server.httpRoute.hostnames` | list | `[]` | HTTPRoute hostnames | +| `server.httpRoute.rules` | list | `[]` | `HTTPRoute.spec.rules`. Omitted `backendRefs` default to server Service on port 80. | +| `server.httpRoute.annotations` | object | `{}` | Route annotations | +| `server.httpRoute.labels` | object | `{}` | Extra labels | +| `server.grpcRoute.enabled` | bool | `false` | Create `GRPCRoute` for Signal + Management. Works with plaintext h2c. | +| `server.grpcRoute.parentRefs` | list | `[]` | Gateway parent refs | +| `server.grpcRoute.hostnames` | list | `[]` | GRPCRoute hostnames | +| `server.grpcRoute.rules` | list | `[]` | `GRPCRoute.spec.rules` (method or header matches) | +| `server.grpcRoute.annotations` | object | `{}` | Route annotations | +| `server.grpcRoute.labels` | object | `{}` | Extra labels | +| `server.relayHttpRoute.enabled` | bool | `false` | Create `HTTPRoute` for relay + WebSocket (default Gateway API path). | +| `server.relayHttpRoute.parentRefs` | list | `[]` | Gateway parent refs | +| `server.relayHttpRoute.hostnames` | list | `[]` | HTTPRoute hostnames | +| `server.relayHttpRoute.rules` | list | `[]` | `HTTPRoute.spec.rules` | +| `server.relayHttpRoute.annotations` | object | `{}` | Route annotations | +| `server.relayHttpRoute.labels` | object | `{}` | Extra labels | +| `server.relayTcpRoute.enabled` | bool | `false` | Create `TCPRoute` (`v1alpha2`) for raw-TCP relay listeners. | +| `server.relayTcpRoute.parentRefs` | list | `[]` | Gateway parent refs | +| `server.relayTcpRoute.rules` | list | `[]` | `TCPRoute.spec.rules`. Defaults to a single rule targeting server Service on port 80. | +| `server.relayTcpRoute.annotations` | object | `{}` | Route annotations | +| `server.relayTcpRoute.labels` | object | `{}` | Extra labels | #### Server Pod @@ -848,21 +848,21 @@ terminated at the referenced Gateway's listeners, not in these values. #### Dashboard Networking -| Key | Type | Default | Description | -| --------------------------------- | ------ | ------------- | -------------------------------------------------------------------- | -| `dashboard.service.type` | string | `"ClusterIP"` | Dashboard service type | -| `dashboard.service.port` | int | `80` | Dashboard service port | -| `dashboard.ingress.enabled` | bool | `false` | Create dashboard ingress. Mutually exclusive with `dashboard.httpRoute`. | -| `dashboard.ingress.className` | string | `"nginx"` | Ingress class | -| `dashboard.ingress.annotations` | object | `{}` | Ingress annotations | -| `dashboard.ingress.hosts` | list | `[]` | Ingress host rules | -| `dashboard.ingress.tls` | list | `[]` | TLS configuration | -| `dashboard.httpRoute.enabled` | bool | `false` | Create Gateway API `HTTPRoute` for the dashboard. Requires `parentRefs`. | -| `dashboard.httpRoute.parentRefs` | list | `[]` | Gateways to attach to | -| `dashboard.httpRoute.hostnames` | list | `[]` | HTTPRoute hostnames | +| Key | Type | Default | Description | +| --------------------------------- | ------ | ------------- | -------------------------------------------------------------------------------------- | +| `dashboard.service.type` | string | `"ClusterIP"` | Dashboard service type | +| `dashboard.service.port` | int | `80` | Dashboard service port | +| `dashboard.ingress.enabled` | bool | `false` | Create dashboard ingress. Mutually exclusive with `dashboard.httpRoute`. | +| `dashboard.ingress.className` | string | `"nginx"` | Ingress class | +| `dashboard.ingress.annotations` | object | `{}` | Ingress annotations | +| `dashboard.ingress.hosts` | list | `[]` | Ingress host rules | +| `dashboard.ingress.tls` | list | `[]` | TLS configuration | +| `dashboard.httpRoute.enabled` | bool | `false` | Create Gateway API `HTTPRoute` for the dashboard. Requires `parentRefs`. | +| `dashboard.httpRoute.parentRefs` | list | `[]` | Gateways to attach to | +| `dashboard.httpRoute.hostnames` | list | `[]` | HTTPRoute hostnames | | `dashboard.httpRoute.rules` | list | `[]` | `HTTPRoute.spec.rules`. Omitted `backendRefs` default to dashboard Service on port 80. | -| `dashboard.httpRoute.annotations` | object | `{}` | Route annotations | -| `dashboard.httpRoute.labels` | object | `{}` | Extra labels | +| `dashboard.httpRoute.annotations` | object | `{}` | Route annotations | +| `dashboard.httpRoute.labels` | object | `{}` | Extra labels | #### Dashboard Pod diff --git a/charts/netbird/values.yaml b/charts/netbird/values.yaml index ca72749..e22da3b 100644 --- a/charts/netbird/values.yaml +++ b/charts/netbird/values.yaml @@ -441,14 +441,14 @@ server: httpRoute: enabled: false parentRefs: [] - # - name: my-gateway - # namespace: gateway-system - # sectionName: https + # - name: my-gateway + # namespace: gateway-system + # sectionName: https hostnames: [] rules: [] - # - matches: - # - path: { type: PathPrefix, value: /api } - # - path: { type: PathPrefix, value: /oauth2 } + # - matches: + # - path: { type: PathPrefix, value: /api } + # - path: { type: PathPrefix, value: /oauth2 } annotations: {} labels: {} @@ -479,9 +479,9 @@ server: parentRefs: [] hostnames: [] rules: [] - # - matches: - # - method: { service: signalexchange.SignalExchange } - # - method: { service: management.ManagementService } + # - matches: + # - method: { service: signalexchange.SignalExchange } + # - method: { service: management.ManagementService } annotations: {} labels: {} @@ -503,9 +503,9 @@ server: parentRefs: [] hostnames: [] rules: [] - # - matches: - # - path: { type: PathPrefix, value: /relay } - # - path: { type: PathPrefix, value: /ws-proxy } + # - matches: + # - path: { type: PathPrefix, value: /relay } + # - path: { type: PathPrefix, value: /ws-proxy } annotations: {} labels: {} @@ -517,9 +517,9 @@ server: enabled: false parentRefs: [] rules: [] - # - backendRefs: - # - name: - # port: 80 + # - backendRefs: + # - name: + # port: 80 annotations: {} labels: {} @@ -603,8 +603,8 @@ dashboard: parentRefs: [] hostnames: [] rules: [] - # - matches: - # - path: { type: PathPrefix, value: / } + # - matches: + # - path: { type: PathPrefix, value: / } annotations: {} labels: {} From 4c59c044059033e9e68374de2a2654a3222b9315 Mon Sep 17 00:00:00 2001 From: mikkeldamsgaard Date: Tue, 21 Apr 2026 23:38:23 +0200 Subject: [PATCH 3/4] ci(netbird): let Envoy Gateway install Gateway API CRDs Installing standard-install.yaml with kubectl then running the envoy-gateway Helm chart caused server-side-apply conflicts: EG owns the same CRDs via its packaged manifests but our prior kubectl-apply set "kubectl-client-side-apply" as the field manager. The EG chart bundles both standard and experimental channel CRDs (including TCPRoute), so a single install is enough. Co-Authored-By: Claude Opus 4.7 (1M context) --- ci/scripts/netbird/e2e-gateway.sh | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/ci/scripts/netbird/e2e-gateway.sh b/ci/scripts/netbird/e2e-gateway.sh index 86b0a5e..1268085 100755 --- a/ci/scripts/netbird/e2e-gateway.sh +++ b/ci/scripts/netbird/e2e-gateway.sh @@ -18,8 +18,10 @@ CHART="charts/netbird" VALUES_FILE="$CHART/ci/e2e-values-gateway.yaml" TIMEOUT="10m" -# Pinned versions so CI is reproducible. -GATEWAY_API_VERSION="v1.2.1" +# Pinned versions so CI is reproducible. Envoy Gateway bundles the Gateway +# API CRDs (standard + experimental channels, including TCPRoute), so we +# don't install them separately — doing both causes field-manager conflicts +# during EG's server-side apply. ENVOY_GATEWAY_VERSION="v1.5.3" ENVOY_NS="envoy-gateway-system" @@ -35,15 +37,7 @@ cleanup() { } trap cleanup EXIT -# ── Install Gateway API CRDs ────────────────────────────────────────── -log "Installing Gateway API CRDs ($GATEWAY_API_VERSION)..." -kubectl apply -f \ - "https://github.com/kubernetes-sigs/gateway-api/releases/download/$GATEWAY_API_VERSION/standard-install.yaml" -# TCPRoute lives in the experimental channel (still v1alpha2). -kubectl apply -f \ - "https://github.com/kubernetes-sigs/gateway-api/releases/download/$GATEWAY_API_VERSION/experimental-install.yaml" - -# ── Install Envoy Gateway ───────────────────────────────────────────── +# ── Install Envoy Gateway (also installs Gateway API CRDs) ──────────── log "Installing Envoy Gateway ($ENVOY_GATEWAY_VERSION)..." helm upgrade --install eg oci://docker.io/envoyproxy/gateway-helm \ --version "$ENVOY_GATEWAY_VERSION" \ From 1f09ef861fd3a77a2d95114b81923eff774d5627 Mon Sep 17 00:00:00 2001 From: mikkeldamsgaard Date: Wed, 22 Apr 2026 00:23:48 +0200 Subject: [PATCH 4/4] ci(netbird): wait for Gateway Accepted, preserve state for debug on fail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gateway Programmed condition requires Envoy's data-plane pod to be Ready, which is too slow on kind within the 3m timeout. Route attachment only needs the Gateway to be Accepted (admission by the controller), so wait on that instead — routes progress independently of proxy readiness. Also skip the cleanup trap on non-zero exit so CI's "Show debug info on failure" step can actually see pod status / events / logs — previously the namespace was deleted before debug ran. Co-Authored-By: Claude Opus 4.7 (1M context) --- ci/scripts/netbird/e2e-gateway.sh | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/ci/scripts/netbird/e2e-gateway.sh b/ci/scripts/netbird/e2e-gateway.sh index 1268085..52151d4 100755 --- a/ci/scripts/netbird/e2e-gateway.sh +++ b/ci/scripts/netbird/e2e-gateway.sh @@ -29,6 +29,11 @@ log() { echo "==> $*"; } fail() { echo "FAIL: $*" >&2; exit 1; } cleanup() { + local rc=$? + if [ $rc -ne 0 ]; then + log "Exit $rc — skipping cleanup so CI can collect debug info." + return + fi log "Cleaning up..." helm uninstall "$RELEASE" -n "$NAMESPACE" --ignore-not-found 2>/dev/null || true kubectl delete namespace "$NAMESPACE" --ignore-not-found 2>/dev/null || true @@ -76,8 +81,12 @@ spec: from: Same EOF -log "Waiting for Gateway to be Programmed..." -kubectl -n "$NAMESPACE" wait --for=condition=Programmed gateway/netbird-gateway --timeout=3m +# Wait for Accepted (admission by the controller) rather than Programmed +# (data-plane pod ready). Route attachment only requires Accepted, and +# waiting on Programmed means waiting for Envoy proxy image pull + pod +# readiness, which is slow on resource-constrained kind nodes. +log "Waiting for Gateway to be Accepted..." +kubectl -n "$NAMESPACE" wait --for=condition=Accepted gateway/netbird-gateway --timeout=2m # ── Generate PAT secret (chart requires pat.secret.secretName) ──────── generate_pat_secret() {