Merge branch v3.7 into master

This commit is contained in:
kevinpollet committed 2026-09-14 12:02:08 +02:00
commit e75250a31a
147 files changed
+11671 -5193

No files matched your search

+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
persist-credentials: false
- name: Enable corepack
run: corepack enable
run: npm i -g corepack
- name: Setup node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
@@ -0,0 +1,47 @@
name: Test K8s Gateway API conformance merged
on:
pull_request:
branches:
- '*'
paths:
- '.github/workflows/test-gateway-api-conformance-merged.yaml'
- 'pkg/provider/kubernetes/gateway/**'
- 'integration/fixtures/gateway-api-conformance/**'
- 'integration/gateway_api_conformance_merged_test.go'
- 'integration/integration_test.go'
env:
CGO_ENABLED: 0
permissions:
contents: read
jobs:
test-gateway-api-conformance-merged:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Check out code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: '.go-version'
check-latest: true
- name: Avoid generating webui
run: |
touch webui/static/index.html
- name: Gateway API conformance merged test and report
run: |
make test-gateway-api-conformance-merged
# The date at which the report has been generated is expected to differ.
git diff --exit-code --ignore-matching-lines='^date:'
+1 -1
View File
@@ -48,7 +48,7 @@ jobs:
persist-credentials: false
- name: Enable corepack
run: corepack enable
run: npm i -g corepack
- name: Set up Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
+1 -1
View File
@@ -1,4 +1,4 @@
# syntax=docker/dockerfile:1.2
# syntax=docker/dockerfile:1
FROM alpine:3.24
RUN apk add --no-cache --no-progress ca-certificates tzdata
+11
View File
@@ -102,9 +102,20 @@ test-integration:
.PHONY: test-gateway-api-conformance
#? test-gateway-api-conformance: Run the Gateway API conformance tests
test-gateway-api-conformance: build-image-dirty
docker pull traefik/gateway-operator:latest
# The data plane image carries a fixed tag, and not latest, so that the pods
# the operator provisions keep the side loaded image instead of pulling the
# released Traefik over it.
docker tag traefik/traefik:latest traefik/traefik:conformance
# In case of a new Minor/Major version, the traefikVersion needs to be updated.
GOOS=$(GOOS) GOARCH=$(GOARCH) go test ./integration -v -tags gatewayAPIConformance -test.run GatewayAPIConformanceSuite -traefikVersion="v3.8" $(TESTFLAGS)
.PHONY: test-gateway-api-conformance-merged
#? test-gateway-api-conformance-merged: Run the Gateway API conformance tests against a single merged Traefik instance
test-gateway-api-conformance-merged: build-image-dirty
# In case of a new Minor/Major version, the traefikVersion needs to be updated.
GOOS=$(GOOS) GOARCH=$(GOARCH) go test ./integration -v -tags gatewayAPIConformanceMerged -test.run GatewayAPIConformanceMergedSuite -traefikVersion="v3.8" $(TESTFLAGS)
.PHONY: test-knative-conformance
#? test-knative-conformance: Run the Knative conformance tests
test-knative-conformance: build-image-dirty
+12 -5
View File
@@ -97,14 +97,21 @@ func runCmd(staticConfiguration *static.Configuration) error {
return fmt.Errorf("setting up logger: %w", err)
}
log.Warn().Msg("Traefik can reject some encoded characters in the request path." +
"When your backend is not fully compliant with [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986)," +
"it is recommended to set these options to `false` to avoid split-view situation." +
"Refer to the documentation for more details: https://doc.traefik.io/traefik/v3.7/migrate/v3/#encoded-characters-configuration-default-values")
http.DefaultTransport.(*http.Transport).Proxy = http.ProxyFromEnvironment
staticConfiguration.SetEffectiveConfiguration()
// Warn only when no entryPoint disallows an encoded character:
// a single denial means the operator already knows about these options.
// Keep this after SetEffectiveConfiguration: it creates the default entryPoint,
// without which a bare configuration would never warn.
if staticConfiguration.HasTCPEntryPoint() && !staticConfiguration.HasDeniedEncodedCharacters() {
log.Warn().Msg("Traefik can reject some encoded characters in the request path. " +
"When your backend is not fully compliant with [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986), " +
"it is recommended to set these options to `false` to avoid split-view situation. " +
"Refer to the documentation for more details: https://doc.traefik.io/traefik/v3.6/migrate/v3/#encoded-characters-configuration-default-values")
}
if err := staticConfiguration.ValidateConfiguration(); err != nil {
return err
}
+1
View File
@@ -1,3 +1,4 @@
# syntax=docker/dockerfile:1
FROM alpine:3.24
RUN apk --no-cache --no-progress add \
@@ -95,6 +95,7 @@ Your PR will not be reviewed until these are green on the CI.
* `make generate`
* `make generate-crd`
* `make test-gateway-api-conformance`
* `make test-gateway-api-conformance-merged`
* `make validate`
* `make pull-images`
* `make test`
@@ -2,7 +2,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.19.0
controller-gen.kubebuilder.io/version: v0.21.0
name: ingressroutes.traefik.io
spec:
group: traefik.io
@@ -491,7 +491,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.19.0
controller-gen.kubebuilder.io/version: v0.21.0
name: ingressroutetcps.traefik.io
spec:
group: traefik.io
@@ -754,7 +754,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.19.0
controller-gen.kubebuilder.io/version: v0.21.0
name: ingressrouteudps.traefik.io
spec:
group: traefik.io
@@ -870,7 +870,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.19.0
controller-gen.kubebuilder.io/version: v0.21.0
name: middlewares.traefik.io
spec:
group: traefik.io
@@ -2292,7 +2292,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.19.0
controller-gen.kubebuilder.io/version: v0.21.0
name: middlewaretcps.traefik.io
spec:
group: traefik.io
@@ -2381,7 +2381,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.19.0
controller-gen.kubebuilder.io/version: v0.21.0
name: serverstransports.traefik.io
spec:
group: traefik.io
@@ -2601,7 +2601,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.19.0
controller-gen.kubebuilder.io/version: v0.21.0
name: serverstransporttcps.traefik.io
spec:
group: traefik.io
@@ -2774,7 +2774,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.19.0
controller-gen.kubebuilder.io/version: v0.21.0
name: tlsoptions.traefik.io
spec:
group: traefik.io
@@ -2893,7 +2893,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.19.0
controller-gen.kubebuilder.io/version: v0.21.0
name: tlsstores.traefik.io
spec:
group: traefik.io
@@ -2990,7 +2990,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.19.0
controller-gen.kubebuilder.io/version: v0.21.0
name: traefikservices.traefik.io
spec:
group: traefik.io
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.19.0
controller-gen.kubebuilder.io/version: v0.21.0
name: ingressroutes.traefik.io
spec:
group: traefik.io
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.19.0
controller-gen.kubebuilder.io/version: v0.21.0
name: ingressroutetcps.traefik.io
spec:
group: traefik.io
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.19.0
controller-gen.kubebuilder.io/version: v0.21.0
name: ingressrouteudps.traefik.io
spec:
group: traefik.io
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.19.0
controller-gen.kubebuilder.io/version: v0.21.0
name: middlewares.traefik.io
spec:
group: traefik.io
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.19.0
controller-gen.kubebuilder.io/version: v0.21.0
name: middlewaretcps.traefik.io
spec:
group: traefik.io
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.19.0
controller-gen.kubebuilder.io/version: v0.21.0
name: serverstransports.traefik.io
spec:
group: traefik.io
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.19.0
controller-gen.kubebuilder.io/version: v0.21.0
name: serverstransporttcps.traefik.io
spec:
group: traefik.io
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.19.0
controller-gen.kubebuilder.io/version: v0.21.0
name: tlsoptions.traefik.io
spec:
group: traefik.io
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.19.0
controller-gen.kubebuilder.io/version: v0.21.0
name: tlsstores.traefik.io
spec:
group: traefik.io
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.19.0
controller-gen.kubebuilder.io/version: v0.21.0
name: traefikservices.traefik.io
spec:
group: traefik.io
@@ -375,6 +375,9 @@ THIS FILE MUST NOT BE EDITED BY HAND
| <a id="opt-providers-kubernetesgateway-crossprovidernamespaces" href="#opt-providers-kubernetesgateway-crossprovidernamespaces" title="#opt-providers-kubernetesgateway-crossprovidernamespaces">providers.kubernetesgateway.crossprovidernamespaces</a> | List of namespaces from which Gateway API routes are allowed to declare TraefikService backendRef references. | |
| <a id="opt-providers-kubernetesgateway-endpoint" href="#opt-providers-kubernetesgateway-endpoint" title="#opt-providers-kubernetesgateway-endpoint">providers.kubernetesgateway.endpoint</a> | Kubernetes server endpoint (required for external cluster client). | |
| <a id="opt-providers-kubernetesgateway-experimentalchannel" href="#opt-providers-kubernetesgateway-experimentalchannel" title="#opt-providers-kubernetesgateway-experimentalchannel">providers.kubernetesgateway.experimentalchannel</a> | Toggles Experimental Channel resources support. Requires the Experimental Channel CRDs. | false |
| <a id="opt-providers-kubernetesgateway-gateways" href="#opt-providers-kubernetesgateway-gateways" title="#opt-providers-kubernetesgateway-gateways">providers.kubernetesgateway.gateways</a> | Scopes the provider to specific Gateways. | |
| <a id="opt-providers-kubernetesgateway-gateways0-name" href="#opt-providers-kubernetesgateway-gateways0-name" title="#opt-providers-kubernetesgateway-gateways0-name">providers.kubernetesgateway.gateways[0].name</a> | Defines the resource name. | |
| <a id="opt-providers-kubernetesgateway-gateways0-namespace" href="#opt-providers-kubernetesgateway-gateways0-namespace" title="#opt-providers-kubernetesgateway-gateways0-namespace">providers.kubernetesgateway.gateways[0].namespace</a> | Defines the resource namespace. | |
| <a id="opt-providers-kubernetesgateway-labelselector" href="#opt-providers-kubernetesgateway-labelselector" title="#opt-providers-kubernetesgateway-labelselector">providers.kubernetesgateway.labelselector</a> | Kubernetes label selector to select specific GatewayClasses. | |
| <a id="opt-providers-kubernetesgateway-namespaces" href="#opt-providers-kubernetesgateway-namespaces" title="#opt-providers-kubernetesgateway-namespaces">providers.kubernetesgateway.namespaces</a> | Kubernetes namespaces. | |
| <a id="opt-providers-kubernetesgateway-nativelbbydefault" href="#opt-providers-kubernetesgateway-nativelbbydefault" title="#opt-providers-kubernetesgateway-nativelbbydefault">providers.kubernetesgateway.nativelbbydefault</a> | Defines whether to use Native Kubernetes load-balancing by default. | false |
@@ -397,10 +397,13 @@ Below the fields displayed with the generic CLF format:
| <a id="opt-TLSVersion" href="#opt-TLSVersion" title="#opt-TLSVersion">`TLSVersion`</a> | The TLS version used by the connection (e.g. `1.2`) (if connection is TLS). |
| <a id="opt-TLSCipher" href="#opt-TLSCipher" title="#opt-TLSCipher">`TLSCipher`</a> | The TLS cipher used by the connection (e.g. `TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA`) (if connection is TLS). |
| <a id="opt-TLSClientSubject" href="#opt-TLSClientSubject" title="#opt-TLSClientSubject">`TLSClientSubject`</a> | The string representation of the TLS client certificate's Subject (e.g. `CN=username,O=organization`). |
| <a id="opt-KubernetesIngressNamespace" href="#opt-KubernetesIngressNamespace" title="#opt-KubernetesIngressNamespace">`KubernetesIngressNamespace`</a> | The namespace of the Kubernetes Ingress resource the router handles. Only available with the Kubernetes Ingress and Kubernetes Ingress Nginx providers. |
| <a id="opt-KubernetesIngressName" href="#opt-KubernetesIngressName" title="#opt-KubernetesIngressName">`KubernetesIngressName`</a> | The name of the Kubernetes Ingress resource the router handles. Only available with the Kubernetes Ingress and Kubernetes Ingress Nginx providers. |
| <a id="opt-KubernetesServiceName" href="#opt-KubernetesServiceName" title="#opt-KubernetesServiceName">`KubernetesServiceName`</a> | The name of the Kubernetes Service associated with the Ingress the router handles. Only available with the Kubernetes Ingress and Kubernetes Ingress Nginx providers. |
| <a id="opt-KubernetesServicePort" href="#opt-KubernetesServicePort" title="#opt-KubernetesServicePort">`KubernetesServicePort`</a> | The port of the Kubernetes Service associated with the Ingress the router handles. Only available with the Kubernetes Ingress and Kubernetes Ingress Nginx providers. |
| <a id="opt-KubernetesIngressNamespace" href="#opt-KubernetesIngressNamespace" title="#opt-KubernetesIngressNamespace">`KubernetesIngressNamespace`</a> | The namespace of the Kubernetes Ingress resource the router handles. Emitted when the router originates from the Kubernetes Ingress or Kubernetes Ingress Nginx provider. |
| <a id="opt-KubernetesIngressName" href="#opt-KubernetesIngressName" title="#opt-KubernetesIngressName">`KubernetesIngressName`</a> | The name of the Kubernetes Ingress resource the router handles. Emitted when the router originates from the Kubernetes Ingress or Kubernetes Ingress Nginx provider. |
| <a id="opt-KubernetesIngressRouteNamespace" href="#opt-KubernetesIngressRouteNamespace" title="#opt-KubernetesIngressRouteNamespace">`KubernetesIngressRouteNamespace`</a> | The namespace of the Kubernetes IngressRoute (CRD) the router handles. Emitted when the router originates from the Kubernetes CRD provider. |
| <a id="opt-KubernetesIngressRouteName" href="#opt-KubernetesIngressRouteName" title="#opt-KubernetesIngressRouteName">`KubernetesIngressRouteName`</a> | The name of the Kubernetes IngressRoute (CRD) the router handles. Emitted when the router originates from the Kubernetes CRD provider. |
| <a id="opt-KubernetesServiceNamespace" href="#opt-KubernetesServiceNamespace" title="#opt-KubernetesServiceNamespace">`KubernetesServiceNamespace`</a> | The namespace of the Kubernetes Service that handled the request. Available with all Kubernetes providers. Stamped per request from the actual backend chosen by the load balancing strategy, so it reflects the real target even under multi-service routes, TraefikService weighted/mirroring, or cross-namespace references. |
| <a id="opt-KubernetesServiceName" href="#opt-KubernetesServiceName" title="#opt-KubernetesServiceName">`KubernetesServiceName`</a> | The name of the Kubernetes Service that handled the request. See `KubernetesServiceNamespace` for the per-request semantics. |
| <a id="opt-KubernetesServicePort" href="#opt-KubernetesServicePort" title="#opt-KubernetesServicePort">`KubernetesServicePort`</a> | The port of the Kubernetes Service that handled the request. See `KubernetesServiceNamespace` for the per-request semantics. |
### Log Rotation
@@ -79,11 +79,14 @@ providers:
|:----------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------|:---------|
| <a id="opt-providers-providersThrottleDuration" href="#opt-providers-providersThrottleDuration" title="#opt-providers-providersThrottleDuration">`providers.providersThrottleDuration`</a> | Minimum amount of time to wait for, after a configuration reload, before taking into account any new configuration refresh event.<br />If multiple events occur within this time, only the most recent one is taken into account, and all others are discarded.<br />**This option cannot be set per provider, but the throttling algorithm applies to each of them independently.** | 2s | No |
| <a id="opt-providers-kubernetesGateway-endpoint" href="#opt-providers-kubernetesGateway-endpoint" title="#opt-providers-kubernetesGateway-endpoint">`providers.kubernetesGateway.endpoint`</a> | Server endpoint URL.<br />More information [here](#endpoint). | "" | No |
| <a id="opt-providers-kubernetesGateway-experimentalChannel" href="#opt-providers-kubernetesGateway-experimentalChannel" title="#opt-providers-kubernetesGateway-experimentalChannel">`providers.kubernetesGateway.experimentalChannel`</a> | Toggles support for the Experimental Channel resources ([Gateway API release channels documentation](https://gateway-api.sigs.k8s.io/concepts/versioning/#release-channels)). | false | No |
| <a id="opt-providers-kubernetesGateway-experimentalChannel" href="#opt-providers-kubernetesGateway-experimentalChannel" title="#opt-providers-kubernetesGateway-experimentalChannel">`providers.kubernetesGateway.experimentalChannel`</a> | Toggles support for the Experimental Channel resources ([Gateway API release channels documentation](https://gateway-api.sigs.k8s.io/concepts/versioning/#release-channels)). | false | No |
| <a id="opt-providers-kubernetesGateway-token" href="#opt-providers-kubernetesGateway-token" title="#opt-providers-kubernetesGateway-token">`providers.kubernetesGateway.token`</a> | Bearer token used for the Kubernetes client configuration. Accepts either the token value directly or a path to a file containing the token. | "" | No |
| <a id="opt-providers-kubernetesGateway-certAuthFilePath" href="#opt-providers-kubernetesGateway-certAuthFilePath" title="#opt-providers-kubernetesGateway-certAuthFilePath">`providers.kubernetesGateway.certAuthFilePath`</a> | Path to the certificate authority file.<br />Used for the Kubernetes client configuration. | "" | No |
| <a id="opt-providers-kubernetesGateway-namespaces" href="#opt-providers-kubernetesGateway-namespaces" title="#opt-providers-kubernetesGateway-namespaces">`providers.kubernetesGateway.namespaces`</a> | Array of namespaces to watch.<br />If left empty, watch all namespaces. | [] | No |
| <a id="opt-providers-kubernetesGateway-labelSelector" href="#opt-providers-kubernetesGateway-labelSelector" title="#opt-providers-kubernetesGateway-labelSelector">`providers.kubernetesGateway.labelSelector`</a> | Allow filtering on `GatewayClass` only. If left empty, Traefik processes all GatewayClass objects.<br />See [label-selectors](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors) for details. | "" | No |
| <a id="opt-providers-kubernetesGateway-labelSelector" href="#opt-providers-kubernetesGateway-labelSelector" title="#opt-providers-kubernetesGateway-labelSelector">`providers.kubernetesGateway.labelSelector`</a> | Allow filtering on `GatewayClass` only. If left empty, Traefik processes all GatewayClass objects.<br />See [label-selectors](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors) for details. | "" | No |
| <a id="opt-providers-kubernetesGateway-gateways" href="#opt-providers-kubernetesGateway-gateways" title="#opt-providers-kubernetesGateway-gateways">`providers.kubernetesGateway.gateways`</a> | Scopes the provider to specific Gateways. | [] | No |
| <a id="opt-providers-kubernetesGateway-gateways0-name" href="#opt-providers-kubernetesGateway-gateways0-name" title="#opt-providers-kubernetesGateway-gateways0-name">`providers.kubernetesGateway.gateways[0].name`</a> | Gateway name. | "" | No |
| <a id="opt-providers-kubernetesGateway-gateways0-namespace" href="#opt-providers-kubernetesGateway-gateways0-namespace" title="#opt-providers-kubernetesGateway-gateways0-namespace">`providers.kubernetesGateway.gateways[0].namespace`</a> | Gateway namespace. | "" | No |
| <a id="opt-providers-kubernetesGateway-throttleDuration" href="#opt-providers-kubernetesGateway-throttleDuration" title="#opt-providers-kubernetesGateway-throttleDuration">`providers.kubernetesGateway.throttleDuration`</a> | Minimum amount of time to wait between two Kubernetes events before producing a new configuration.<br />This prevents a Kubernetes cluster that updates many times per second from continuously changing your Traefik configuration.<br />If empty, every event is caught. | 0s | No |
| <a id="opt-providers-kubernetesGateway-nativeLBByDefault" href="#opt-providers-kubernetesGateway-nativeLBByDefault" title="#opt-providers-kubernetesGateway-nativeLBByDefault">`providers.kubernetesGateway.nativeLBByDefault`</a> | Defines whether to use Native Kubernetes load-balancing mode by default. For more information, please check out the `traefik.io/service.nativelb` service annotation documentation. | false | No |
| <a id="opt-providers-kubernetesGateway-statusAddress-hostname" href="#opt-providers-kubernetesGateway-statusAddress-hostname" title="#opt-providers-kubernetesGateway-statusAddress-hostname">`providers.kubernetesGateway.`<br />`statusAddress.hostname`</a> | Hostname copied to the Gateway `status.addresses`. | "" | No |
@@ -91,8 +94,8 @@ providers:
| <a id="opt-providers-kubernetesGateway-statusAddress-service-namespace" href="#opt-providers-kubernetesGateway-statusAddress-service-namespace" title="#opt-providers-kubernetesGateway-statusAddress-service-namespace">`providers.kubernetesGateway.`<br />`statusAddress.service.namespace`</a> | The namespace of the Kubernetes service to copy status addresses from.<br />When using third parties tools like External-DNS, this option can be used to copy the service `loadbalancer.status` (containing the service's endpoints IPs) to the Gateway `status.addresses`. | "" | No |
| <a id="opt-providers-kubernetesGateway-statusAddress-service-name" href="#opt-providers-kubernetesGateway-statusAddress-service-name" title="#opt-providers-kubernetesGateway-statusAddress-service-name">`providers.kubernetesGateway.`<br />`statusAddress.service.name`</a> | The name of the Kubernetes service to copy status addresses from.<br />When using third parties tools like External-DNS, this option can be used to copy the service `loadbalancer.status` (containing the service's endpoints IPs) to the Gateway `status.addresses`. | "" | No |
| <a id="opt-providers-kubernetesGateway-crossProviderNamespaces" href="#opt-providers-kubernetesGateway-crossProviderNamespaces" title="#opt-providers-kubernetesGateway-crossProviderNamespaces">`providers.kubernetesGateway.crossProviderNamespaces`</a> | List of namespaces from which Gateway API routes (`HTTPRoute`, `TCPRoute`, `TLSRoute`) are allowed to declare a `backendRef` of kind `TraefikService`.<br />When unset, all namespaces are allowed. When set to `[]`, every such backendRef is rejected and the route is dropped. | [] | No |
| <a id="opt-providers-kubernetesGateway-qps" href="#opt-providers-kubernetesGateway-qps" title="#opt-providers-kubernetesGateway-qps">`providers.kubernetesGateway.qps`</a> | Defines the maximum QPS to the Kubernetes API server. Setting this to a negative value will disable client-side ratelimiting. | 50 | No |
| <a id="opt-providers-kubernetesGateway-burst" href="#opt-providers-kubernetesGateway-burst" title="#opt-providers-kubernetesGateway-burst">`providers.kubernetesGateway.burst`</a> | Defines the maximum burst of requests to the Kubernetes API server. | 100 | No |
| <a id="opt-providers-kubernetesGateway-qps" href="#opt-providers-kubernetesGateway-qps" title="#opt-providers-kubernetesGateway-qps">`providers.kubernetesGateway.qps`</a> | Defines the maximum QPS to the Kubernetes API server. Setting this to a negative value will disable client-side ratelimiting. | 50 | No |
| <a id="opt-providers-kubernetesGateway-burst" href="#opt-providers-kubernetesGateway-burst" title="#opt-providers-kubernetesGateway-burst">`providers.kubernetesGateway.burst`</a> | Defines the maximum burst of requests to the Kubernetes API server. | 100 | No |
<!-- markdownlint-enable MD013 -->
@@ -343,7 +343,7 @@ The following annotations are organized by category for easier navigation.
| <a id="opt-nginx-ingress-kubernetes-iocors-allow-credentials" href="#opt-nginx-ingress-kubernetes-iocors-allow-credentials" title="#opt-nginx-ingress-kubernetes-iocors-allow-credentials">`nginx.ingress.kubernetes.io/cors-allow-credentials`</a> | |
| <a id="opt-nginx-ingress-kubernetes-iocors-allow-headers" href="#opt-nginx-ingress-kubernetes-iocors-allow-headers" title="#opt-nginx-ingress-kubernetes-iocors-allow-headers">`nginx.ingress.kubernetes.io/cors-allow-headers`</a> | |
| <a id="opt-nginx-ingress-kubernetes-iocors-allow-methods" href="#opt-nginx-ingress-kubernetes-iocors-allow-methods" title="#opt-nginx-ingress-kubernetes-iocors-allow-methods">`nginx.ingress.kubernetes.io/cors-allow-methods`</a> | |
| <a id="opt-nginx-ingress-kubernetes-iocors-allow-origin" href="#opt-nginx-ingress-kubernetes-iocors-allow-origin" title="#opt-nginx-ingress-kubernetes-iocors-allow-origin">`nginx.ingress.kubernetes.io/cors-allow-origin`</a> | |
| <a id="opt-nginx-ingress-kubernetes-iocors-allow-origin" href="#opt-nginx-ingress-kubernetes-iocors-allow-origin" title="#opt-nginx-ingress-kubernetes-iocors-allow-origin">`nginx.ingress.kubernetes.io/cors-allow-origin`</a> | Single-level wildcard origins are supported in the `protocol://*.domain[:port]` form, where `*` matches exactly one DNS label. Wildcards in any other position are ignored, as with ingress-nginx. |
| <a id="opt-nginx-ingress-kubernetes-iocors-expose-headers" href="#opt-nginx-ingress-kubernetes-iocors-expose-headers" title="#opt-nginx-ingress-kubernetes-iocors-expose-headers">`nginx.ingress.kubernetes.io/cors-expose-headers`</a> | |
| <a id="opt-nginx-ingress-kubernetes-iocors-max-age" href="#opt-nginx-ingress-kubernetes-iocors-max-age" title="#opt-nginx-ingress-kubernetes-iocors-max-age">`nginx.ingress.kubernetes.io/cors-max-age`</a> | |
+1
View File
@@ -1,3 +1,4 @@
# syntax=docker/dockerfile:1
FROM alpine:3.24
ENV PATH="${PATH}:/venv/bin"
Generated
+4 -4
View File
@@ -31,17 +31,17 @@
},
"nixpkgs-kct": {
"locked": {
"lastModified": 1763934636,
"narHash": "sha256-9glbI7f1uU+yzQCq5LwLgdZqx6svOhZWkd4JRY265fc=",
"lastModified": 1787814960,
"narHash": "sha256-PYZq1qzCJXC2zGI0mH07vrZBsw6DRBAOX0jN1pPtqOQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "ee09932cedcef15aaf476f9343d1dea2cb77e261",
"rev": "c27cdad491a991b11ed731760aa2ef8db0cb0410",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "ee09932cedcef15aaf476f9343d1dea2cb77e261",
"rev": "c27cdad491a991b11ed731760aa2ef8db0cb0410",
"type": "github"
}
},
+1 -1
View File
@@ -7,7 +7,7 @@
# Pinned nixpkgs for kubernetes-controller-tools
# Search: https://www.nixhub.io/packages/kubernetes-controller-tools
nixpkgs-kct.url = "github:NixOS/nixpkgs/ee09932cedcef15aaf476f9343d1dea2cb77e261";
nixpkgs-kct.url = "github:NixOS/nixpkgs/c27cdad491a991b11ed731760aa2ef8db0cb0410";
# Pinned nixpkgs for golangci-lint
# Search: https://www.nixhub.io/packages/golangci-lint
+5 -7
View File
@@ -110,10 +110,10 @@ require (
google.golang.org/grpc v1.83.2
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.36.1
k8s.io/apiextensions-apiserver v0.36.1
k8s.io/apimachinery v0.36.1
k8s.io/client-go v0.36.1
k8s.io/api v0.36.3
k8s.io/apiextensions-apiserver v0.36.3
k8s.io/apimachinery v0.36.3
k8s.io/client-go v0.36.3
k8s.io/utils v0.0.0-20260319190234-28399d86e0b5 // No tag on the repo.
knative.dev/networking v0.0.0-20251217020127-11890a5dabea
knative.dev/pkg v0.0.0-20251216153728-9c8140b780d1
@@ -125,8 +125,6 @@ require (
sigs.k8s.io/yaml v1.6.0
)
require github.com/go-acme/lego/v4 v4.35.2
require (
cloud.google.com/go/auth v0.23.2 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
@@ -407,7 +405,7 @@ require (
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/klog/v2 v2.140.0 // indirect
k8s.io/kube-openapi v0.0.0-20260501160325-927ab1f70cd6 // indirect
k8s.io/streaming v0.36.1 // indirect
k8s.io/streaming v0.36.3 // indirect
nhooyr.io/websocket v1.8.7 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
+10 -13
View File
@@ -607,7 +607,6 @@ github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8af
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 h1:Dy3M9aegiI7d7PF1LUdjbVigJReo+QOceYsMyFh9qoE=
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0/go.mod h1:ZakZtbCXxCz82NJvq7MoREtiQesnDfrtF6RFUGzQfLo=
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.1 h1:zvXfGJCWvywnCA814d8ZiVyt+fm9nnTE8xSb99zRyfo=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.1/go.mod h1:iptorS+VYKFL2N6PnebpS91dubG35eAOEERnT4PJbQU=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.1 h1:u93s+zU2JD62im61Bm5CZIc1ZrOJaIAWEg0WOrMVkEo=
@@ -995,8 +994,6 @@ github.com/go-acme/esa-20240910/v3 v3.13.0 h1:6TMbBOzcBhZslc4u7OqRvbhpHYXQ7A4Ky0
github.com/go-acme/esa-20240910/v3 v3.13.0/go.mod h1:F/DUQNpbXj0Y4FPj5PoFtl1rG+q3yq7Zd4IEWcSJQDg=
github.com/go-acme/jdcloud-sdk-go v1.64.0 h1:AW9j5khk8tRYbpBJPxKmqdwIqgLs2Fz3HUK3hn2YXjs=
github.com/go-acme/jdcloud-sdk-go v1.64.0/go.mod h1:qc/m8HNX1Zgd7GAv2DSEinup8fwy3Ted3/VVx7LB5bU=
github.com/go-acme/lego/v4 v4.35.2 h1:uVQg+KC/yj9R2g7Q9W5wDqhvQvxV5SMu5eqFVoN5xZU=
github.com/go-acme/lego/v4 v4.35.2/go.mod h1:pX2jN5n8OphMGY1IaMjYm5DAEzguBaKRt8AvJAgJXpc=
github.com/go-acme/lego/v5 v5.4.1 h1:Jt6xAP9xQrJ3SLLGgxejfDDd3pzdrmuLCWe4oTqHHQc=
github.com/go-acme/lego/v5 v5.4.1/go.mod h1:OvMMHc6DVeqycOGhNRuMlNa1V304qEFRljWDHMpEcLc=
github.com/go-acme/tencentclouddnspod v1.3.131 h1:0Bd/OKGdf29c8w7CHwmZVmy+n/XEtOOv4AFhLXzm6Fw=
@@ -3039,20 +3036,20 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=
k8s.io/api v0.36.1 h1:XbL/EMj8K2aJpJtePmqUyQMsM0D4QI2pvl7YKJ20FTY=
k8s.io/api v0.36.1/go.mod h1:KOWo4ey3TINlXjeHVuwB3i+tXXnu+UcwFBHlI/9dvEo=
k8s.io/apiextensions-apiserver v0.36.1 h1:6JfYmPUsuUIHuN+3QxutXYWj492RqF5fBSx67GYK5Ks=
k8s.io/apiextensions-apiserver v0.36.1/go.mod h1:pLzZin90riwisdzKwv/GoTwENooytoIx5zWJb4Hkby8=
k8s.io/apimachinery v0.36.1 h1:G63Gjx2W+q0YD+72Vo8oY0nDnePVwnuzTmmy5ENrVSA=
k8s.io/apimachinery v0.36.1/go.mod h1:ibYOR00vW/I1kzvi5SF0dRuJ52BvKtfvRdOn35GPQ+8=
k8s.io/client-go v0.36.1 h1:FN/K8QIT2CEDt+2WB2HnWrUANZ50AP5GII43/SP2JR0=
k8s.io/client-go v0.36.1/go.mod h1:s6rAnCtTGYDQnpNjEhSaISV+2O8jwruZ6m3QOYBFbtU=
k8s.io/api v0.36.3 h1:NxB+05W2UGqXWFXcLO0RB5cnqnUPP5v5sVlaOH0Iz4w=
k8s.io/api v0.36.3/go.mod h1:JzLQKqRHC5+I8RVj/lS3lCg0mg6nWI9Fo/Sk3ElxHzg=
k8s.io/apiextensions-apiserver v0.36.3 h1:dPmOAPhwTtqb1bTxbFPsy18KHPhktQeO3WUPXunZIB0=
k8s.io/apiextensions-apiserver v0.36.3/go.mod h1:KTXFqgXiuw2pRoL+Wpmttqc+up9Xt/GohadPWeLLOa4=
k8s.io/apimachinery v0.36.3 h1:PkzMRBRG8joFD8EhCuQAtNPvJlxb82FwplP26HIzvAM=
k8s.io/apimachinery v0.36.3/go.mod h1:cTSjBWgPe/6CQyBKzY/hDIRWCQQQeK0mfLbml0UYFHE=
k8s.io/client-go v0.36.3 h1:M4JdVzXxYcZk4fGpfDdYnxSwhLKWCFoQsHW6t+z8Hfg=
k8s.io/client-go v0.36.3/go.mod h1:gcPwr0c87vjjG6HB6pWEqOeuYVoXSsREjzux2j6GF30=
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
k8s.io/kube-openapi v0.0.0-20260501160325-927ab1f70cd6 h1:ngxu1nL4SbFuXwu1EY7cSKcVqSjTQPVbYQT6WNjTXaU=
k8s.io/kube-openapi v0.0.0-20260501160325-927ab1f70cd6/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0=
k8s.io/streaming v0.36.1 h1:L+K68n4Gg940BGNNYtUBvL1WTLL0YnKT3s+P1MNAmR4=
k8s.io/streaming v0.36.1/go.mod h1:z6fV3D+NVkoeqRMtWwlUZK6U17SY/LqNzOxWL6GyR/s=
k8s.io/streaming v0.36.3 h1:9rAaqBk0C0Pc7+/fqGekj07NV+/Xrew58p647A0JT8w=
k8s.io/streaming v0.36.3/go.mod h1:z6fV3D+NVkoeqRMtWwlUZK6U17SY/LqNzOxWL6GyR/s=
k8s.io/utils v0.0.0-20260319190234-28399d86e0b5 h1:kBawHLSnx/mYHmRnNUf9d4CpjREbeZuxoSGOX/J+aYM=
k8s.io/utils v0.0.0-20260319190234-28399d86e0b5/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
knative.dev/networking v0.0.0-20251217020127-11890a5dabea h1:CsVi1M+NbPIfvBPWI9DQOwlzBG6+w+mAfhUDqw1jeXM=
@@ -0,0 +1,521 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.18.0
name: traefikproxies.gateway.traefik.io
spec:
group: gateway.traefik.io
names:
kind: TraefikProxy
listKind: TraefikProxyList
plural: traefikproxies
shortNames:
- tp
singular: traefikproxy
scope: Cluster
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
description: TraefikProxy is the data plane configuration referenced by a
GatewayClass.
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: |-
TraefikProxySpec defines the data plane provisioned for a Gateway.
It is referenced from GatewayClass.spec.parametersRef, mirroring the
EnvoyProxy model of Envoy Gateway.
properties:
autoscaling:
description: |-
Autoscaling configures a HorizontalPodAutoscaler for the data plane. When
set, the operator does not manage the Deployment replica count.
properties:
maxReplicas:
description: MaxReplicas is the upper bound for the number of
replicas.
format: int32
minimum: 1
type: integer
minReplicas:
description: MinReplicas is the lower bound for the number of
replicas.
format: int32
type: integer
targetCPUUtilizationPercentage:
description: TargetCPUUtilizationPercentage is the average CPU
target across pods.
format: int32
type: integer
required:
- maxReplicas
type: object
deployment:
description: Deployment configures the data plane Deployment.
properties:
image:
description: Image is the Traefik container image to run.
type: string
podAnnotations:
additionalProperties:
type: string
description: PodAnnotations are added to the data plane pods.
type: object
podLabels:
additionalProperties:
type: string
description: PodLabels are added to the data plane pods.
type: object
replicas:
description: Replicas is the number of data plane replicas.
format: int32
type: integer
resources:
description: Resources are the container resource requirements.
properties:
claims:
description: |-
Claims lists the names of resources, defined in spec.resourceClaims,
that are used by this container.
This field depends on the
DynamicResourceAllocation feature gate.
This field is immutable. It can only be set for containers.
items:
description: ResourceClaim references one entry in PodSpec.ResourceClaims.
properties:
name:
description: |-
Name must match the name of one entry in pod.spec.resourceClaims of
the Pod where this field is used. It makes that resource available
inside a container.
type: string
request:
description: |-
Request is the name chosen for a request in the referenced claim.
If empty, everything from the claim is made available, otherwise
only the result of this request.
type: string
required:
- name
type: object
type: array
x-kubernetes-list-map-keys:
- name
x-kubernetes-list-type: map
limits:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: |-
Limits describes the maximum amount of compute resources allowed.
More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
type: object
requests:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: |-
Requests describes the minimum amount of compute resources required.
If Requests is omitted for a container, it defaults to Limits if that is explicitly specified,
otherwise to an implementation-defined value. Requests cannot exceed Limits.
More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
type: object
type: object
type: object
logs:
description: Logs configures the data plane logging.
properties:
level:
description: Level is the log level (DEBUG, INFO, WARN, ERROR).
type: string
type: object
metrics:
description: Metrics configures the data plane metrics.
properties:
prometheus:
description: |-
Prometheus enables the Prometheus metrics endpoint on a dedicated
"metrics" entry point.
type: boolean
type: object
podDisruptionBudget:
description: PodDisruptionBudget configures a PodDisruptionBudget
for the data plane.
properties:
maxUnavailable:
anyOf:
- type: integer
- type: string
description: MaxUnavailable is the maximum number of unavailable
pods.
x-kubernetes-int-or-string: true
minAvailable:
anyOf:
- type: integer
- type: string
description: MinAvailable is the minimum number of available pods.
x-kubernetes-int-or-string: true
type: object
service:
description: Service configures the data plane Service.
properties:
annotations:
additionalProperties:
type: string
description: Annotations are added to the data plane Service.
type: object
loadBalancerClass:
description: |-
LoadBalancerClass selects the load balancer implementation when Type
is LoadBalancer.
type: string
type:
description: Type is the Service type used to expose the data
plane.
enum:
- LoadBalancer
- ClusterIP
- NodePort
type: string
type: object
tracing:
description: Tracing configures the data plane tracing.
properties:
otlpEndpoint:
description: OTLPEndpoint enables OTLP tracing to the given HTTP
endpoint.
type: string
type: object
type: object
status:
description: TraefikProxyStatus is the observed state of a TraefikProxy.
properties:
conditions:
description: Conditions describe the current state of the TraefikProxy.
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
properties:
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
with respect to the current state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: |-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
Producers of specific condition types may define expected values and meanings for this field,
and whether the values are considered a guaranteed API.
The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
type: object
type: object
served: true
storage: true
subresources:
status: {}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: manager-role
rules:
- apiGroups:
- ""
resources:
- events
verbs:
- create
- patch
- apiGroups:
- ""
resources:
- serviceaccounts
- services
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- apps
resources:
- deployments
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- autoscaling
resources:
- horizontalpodautoscalers
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- coordination.k8s.io
resources:
- leases
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- gateway.networking.k8s.io
resources:
- gatewayclasses
verbs:
- get
- list
- watch
- apiGroups:
- gateway.networking.k8s.io
resources:
- gateways
verbs:
- get
- list
- patch
- update
- watch
- apiGroups:
- gateway.networking.k8s.io
resources:
- gateways/finalizers
verbs:
- update
- apiGroups:
- gateway.networking.k8s.io
resources:
- gateways/status
verbs:
- get
- patch
- update
- apiGroups:
- gateway.traefik.io
resources:
- traefikproxies
verbs:
- get
- list
- watch
- apiGroups:
- policy
resources:
- poddisruptionbudgets
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- rbac.authorization.k8s.io
resources:
- clusterrolebindings
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- rbac.authorization.k8s.io
resources:
- clusterroles
verbs:
- bind
- get
- list
- watch
---
# Static ClusterRole bound by the operator to every provisioned data plane
# ServiceAccount. It carries the permissions the in-tree kubernetesGateway
# provider needs to translate routes and write route status. The operator
# creates one ClusterRoleBinding per data plane referencing this role.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: traefik-gateway-dataplane
rules:
- apiGroups: [""]
resources: [services, endpoints, secrets, configmaps, namespaces, nodes, pods]
verbs: [get, list, watch]
- apiGroups: [discovery.k8s.io]
resources: [endpointslices]
verbs: [get, list, watch]
- apiGroups: [gateway.networking.k8s.io]
resources: [gatewayclasses, gateways, httproutes, grpcroutes, tcproutes, tlsroutes, referencegrants, backendtlspolicies]
verbs: [get, list, watch]
- apiGroups: [gateway.networking.k8s.io]
resources: [gateways/status, gatewayclasses/status, httproutes/status, grpcroutes/status, tcproutes/status, tlsroutes/status, backendtlspolicies/status]
verbs: [update, patch]
---
apiVersion: v1
kind: Namespace
metadata:
name: traefik-gateway-operator-system
labels:
app.kubernetes.io/managed-by: traefik-gateway-operator
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: traefik-gateway-operator
namespace: traefik-gateway-operator-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: traefik-gateway-operator-manager
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: manager-role
subjects:
- kind: ServiceAccount
name: traefik-gateway-operator
namespace: traefik-gateway-operator-system
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: traefik-gateway-operator
namespace: traefik-gateway-operator-system
labels:
app.kubernetes.io/name: traefik-gateway-operator
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: traefik-gateway-operator
template:
metadata:
labels:
app.kubernetes.io/name: traefik-gateway-operator
spec:
serviceAccountName: traefik-gateway-operator
securityContext:
runAsNonRoot: true
containers:
- name: manager
image: traefik/gateway-operator:latest
imagePullPolicy: Never
args:
- --health-probe-bind-address=:8081
- --metrics-bind-address=:8080
ports:
- name: metrics
containerPort: 8080
livenessProbe:
httpGet:
path: /healthz
port: 8081
initialDelaySeconds: 15
periodSeconds: 20
readinessProbe:
httpGet:
path: /readyz
port: 8081
initialDelaySeconds: 5
periodSeconds: 10
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 500m
memory: 256Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
@@ -0,0 +1,29 @@
---
apiVersion: gateway.traefik.io/v1alpha1
kind: TraefikProxy
metadata:
name: conformance
spec:
deployment:
replicas: 1
# The tag is fixed, and not latest, so that the provisioned pods keep the
# image side loaded into the node: a latest tag defaults imagePullPolicy to
# Always, which pulls the released Traefik over the one built from the
# working tree.
image: traefik/traefik:conformance
service:
type: LoadBalancer
logs:
level: DEBUG
---
kind: GatewayClass
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: traefik
spec:
controllerName: traefik.io/gateway-controller
parametersRef:
group: gateway.traefik.io
kind: TraefikProxy
name: conformance
@@ -0,0 +1,16 @@
---
# The data plane is the single writer of the GatewayClass status, and the
# operator only provisions one once a Gateway exists. The conformance suite
# waits for the GatewayClass to be accepted before it creates any Gateway, so
# this one exists for the whole run to keep the status written.
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: traefik-bootstrap
namespace: default
spec:
gatewayClassName: traefik
listeners:
- name: http
protocol: HTTP
port: 80
+10 -10
View File
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.19.0
controller-gen.kubebuilder.io/version: v0.21.0
name: ingressroutes.traefik.io
spec:
group: traefik.io
@@ -492,7 +492,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.19.0
controller-gen.kubebuilder.io/version: v0.21.0
name: ingressroutetcps.traefik.io
spec:
group: traefik.io
@@ -755,7 +755,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.19.0
controller-gen.kubebuilder.io/version: v0.21.0
name: ingressrouteudps.traefik.io
spec:
group: traefik.io
@@ -871,7 +871,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.19.0
controller-gen.kubebuilder.io/version: v0.21.0
name: middlewares.traefik.io
spec:
group: traefik.io
@@ -2293,7 +2293,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.19.0
controller-gen.kubebuilder.io/version: v0.21.0
name: middlewaretcps.traefik.io
spec:
group: traefik.io
@@ -2382,7 +2382,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.19.0
controller-gen.kubebuilder.io/version: v0.21.0
name: serverstransports.traefik.io
spec:
group: traefik.io
@@ -2602,7 +2602,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.19.0
controller-gen.kubebuilder.io/version: v0.21.0
name: serverstransporttcps.traefik.io
spec:
group: traefik.io
@@ -2775,7 +2775,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.19.0
controller-gen.kubebuilder.io/version: v0.21.0
name: tlsoptions.traefik.io
spec:
group: traefik.io
@@ -2894,7 +2894,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.19.0
controller-gen.kubebuilder.io/version: v0.21.0
name: tlsstores.traefik.io
spec:
group: traefik.io
@@ -2991,7 +2991,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.19.0
controller-gen.kubebuilder.io/version: v0.21.0
name: traefikservices.traefik.io
spec:
group: traefik.io
@@ -1,5 +1,5 @@
apiVersion: gateway.networking.k8s.io/v1
date: "2026-09-09T12:00:32+02:00"
date: "2026-09-14T10:33:21+02:00"
gatewayAPIChannel: experimental
gatewayAPIVersion: v1.6.1
implementation:
@@ -18,28 +18,47 @@ profiles:
Failed: 0
Passed: 15
Skipped: 0
name: GATEWAY-GRPC
summary: Core tests succeeded.
- core:
result: partial
skippedTests:
- HTTPRouteMultipleGateways
statistics:
Failed: 0
Passed: 36
Skipped: 1
extended:
result: success
statistics:
Failed: 0
Passed: 30
Passed: 3
Skipped: 0
supportedFeatures:
- GatewayAddressEmpty
- GatewayHTTPListenerIsolation
- GatewayHTTPSListenerDetectMisdirectedRequests
- GatewayInfrastructurePropagation
- GatewayPort8080
- GatewayStaticAddresses
unsupportedFeatures:
- GatewayBackendClientCertificate
- GatewayFrontendClientCertificateValidation
- GatewayFrontendClientCertificateValidationInsecureFallback
- ListenerSet
name: GATEWAY-GRPC
summary: Core tests succeeded. Extended tests succeeded.
- core:
result: success
statistics:
Failed: 0
Passed: 37
Skipped: 0
extended:
result: success
statistics:
Failed: 0
Passed: 33
Skipped: 0
supportedFeatures:
- BackendTLSPolicy
- BackendTLSPolicySANValidation
- GatewayAddressEmpty
- GatewayHTTPListenerIsolation
- GatewayHTTPSListenerDetectMisdirectedRequests
- GatewayInfrastructurePropagation
- GatewayPort8080
- GatewayStaticAddresses
- HTTPRoute303RedirectStatusCode
- HTTPRoute307RedirectStatusCode
- HTTPRoute308RedirectStatusCode
@@ -59,12 +78,9 @@ profiles:
- HTTPRouteResponseHeaderModification
- HTTPRouteSchemeRedirect
unsupportedFeatures:
- GatewayAddressEmpty
- GatewayBackendClientCertificate
- GatewayFrontendClientCertificateValidation
- GatewayFrontendClientCertificateValidationInsecureFallback
- GatewayInfrastructurePropagation
- GatewayStaticAddresses
- HTTPRouteBackendTimeout
- HTTPRouteRequestMirror
- HTTPRouteRequestMultipleMirrors
@@ -75,15 +91,34 @@ profiles:
- HTTPRouteRetryConnectionError
- ListenerSet
name: GATEWAY-HTTP
summary: Core tests partially succeeded with 1 test skips. Extended tests succeeded.
summary: Core tests succeeded. Extended tests succeeded.
- core:
result: success
statistics:
Failed: 0
Passed: 19
Skipped: 0
extended:
result: success
statistics:
Failed: 0
Passed: 3
Skipped: 0
supportedFeatures:
- GatewayAddressEmpty
- GatewayHTTPListenerIsolation
- GatewayHTTPSListenerDetectMisdirectedRequests
- GatewayInfrastructurePropagation
- GatewayPort8080
- GatewayStaticAddresses
- TCPRoute
unsupportedFeatures:
- GatewayBackendClientCertificate
- GatewayFrontendClientCertificateValidation
- GatewayFrontendClientCertificateValidationInsecureFallback
- ListenerSet
name: GATEWAY-TCP
summary: Core tests succeeded.
summary: Core tests succeeded. Extended tests succeeded.
- core:
result: success
statistics:
@@ -94,26 +129,28 @@ profiles:
result: success
statistics:
Failed: 0
Passed: 4
Passed: 7
Skipped: 0
supportedFeatures:
- GatewayAddressEmpty
- GatewayHTTPListenerIsolation
- GatewayHTTPSListenerDetectMisdirectedRequests
- GatewayInfrastructurePropagation
- GatewayPort8080
- GatewayStaticAddresses
- TLSRouteModeMixed
- TLSRouteModeTerminate
unsupportedFeatures:
- GatewayAddressEmpty
- GatewayBackendClientCertificate
- GatewayFrontendClientCertificateValidation
- GatewayFrontendClientCertificateValidationInsecureFallback
- GatewayInfrastructurePropagation
- GatewayStaticAddresses
- ListenerSet
name: GATEWAY-TLS
summary: Core tests succeeded. Extended tests succeeded.
succeededProvisionalTests:
- GRPCRouteNamedRule
- GatewayInfrastructure
- GatewayOptionalAddressValue
- HTTPRoute303Redirect
- HTTPRoute307Redirect
- HTTPRoute308Redirect
@@ -0,0 +1,123 @@
apiVersion: gateway.networking.k8s.io/v1
date: "2026-09-14T10:44:42+02:00"
gatewayAPIChannel: experimental
gatewayAPIVersion: v1.6.1
implementation:
contact:
- '@traefik/maintainers'
organization: traefik
project: traefik
url: https://traefik.io/
version: v3.8
kind: ConformanceReport
mode: merged
profiles:
- core:
result: success
statistics:
Failed: 0
Passed: 15
Skipped: 0
name: GATEWAY-GRPC
summary: Core tests succeeded.
- core:
result: partial
skippedTests:
- HTTPRouteMultipleGateways
statistics:
Failed: 0
Passed: 36
Skipped: 1
extended:
result: success
statistics:
Failed: 0
Passed: 30
Skipped: 0
supportedFeatures:
- BackendTLSPolicy
- BackendTLSPolicySANValidation
- GatewayHTTPListenerIsolation
- GatewayHTTPSListenerDetectMisdirectedRequests
- GatewayPort8080
- HTTPRoute303RedirectStatusCode
- HTTPRoute307RedirectStatusCode
- HTTPRoute308RedirectStatusCode
- HTTPRouteBackendProtocolH2C
- HTTPRouteBackendProtocolWebSocket
- HTTPRouteBackendRequestHeaderModification
- HTTPRouteCORS
- HTTPRouteDestinationPortMatching
- HTTPRouteHostRewrite
- HTTPRouteMethodMatching
- HTTPRouteNamedRouteRule
- HTTPRouteParentRefPort
- HTTPRoutePathRedirect
- HTTPRoutePathRewrite
- HTTPRoutePortRedirect
- HTTPRouteQueryParamMatching
- HTTPRouteResponseHeaderModification
- HTTPRouteSchemeRedirect
unsupportedFeatures:
- GatewayAddressEmpty
- GatewayBackendClientCertificate
- GatewayFrontendClientCertificateValidation
- GatewayFrontendClientCertificateValidationInsecureFallback
- GatewayInfrastructurePropagation
- GatewayStaticAddresses
- HTTPRouteBackendTimeout
- HTTPRouteRequestMirror
- HTTPRouteRequestMultipleMirrors
- HTTPRouteRequestPercentageMirror
- HTTPRouteRequestTimeout
- HTTPRouteRetry
- HTTPRouteRetryBackendTimeout
- HTTPRouteRetryConnectionError
- ListenerSet
name: GATEWAY-HTTP
summary: Core tests partially succeeded with 1 test skips. Extended tests succeeded.
- core:
result: partial
skippedTests:
- TLSRouteHostnameIntersection
statistics:
Failed: 0
Passed: 19
Skipped: 1
extended:
result: success
statistics:
Failed: 0
Passed: 4
Skipped: 0
supportedFeatures:
- GatewayHTTPListenerIsolation
- GatewayHTTPSListenerDetectMisdirectedRequests
- GatewayPort8080
- TLSRouteModeMixed
- TLSRouteModeTerminate
unsupportedFeatures:
- GatewayAddressEmpty
- GatewayBackendClientCertificate
- GatewayFrontendClientCertificateValidation
- GatewayFrontendClientCertificateValidationInsecureFallback
- GatewayInfrastructurePropagation
- GatewayStaticAddresses
- ListenerSet
name: GATEWAY-TLS
summary: Core tests partially succeeded with 1 test skips. Extended tests succeeded.
succeededProvisionalTests:
- GRPCRouteNamedRule
- HTTPRoute303Redirect
- HTTPRoute307Redirect
- HTTPRoute308Redirect
- HTTPRouteNamedRule
- TCPRouteInvalidBackendRefNonexistent
- TCPRouteInvalidCrossNamespaceBackendRef
- TCPRouteInvalidNonTCPListener
- TCPRouteParentRefAttachAll
- TCPRouteParentRefPortAndSectionName
- TCPRouteReferenceGrant
- TCPRouteWeightedRouting
- TLSRouteMixedTerminationSameNamespace
- TLSRouteTerminateSimpleSameNamespace
@@ -0,0 +1,217 @@
//go:build gatewayAPIConformanceMerged
package integration
import (
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"slices"
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/modules/k3s"
"github.com/testcontainers/testcontainers-go/network"
"github.com/traefik/traefik/v3/integration/try"
"github.com/traefik/traefik/v3/pkg/provider/kubernetes/gateway"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
kclientset "k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"sigs.k8s.io/controller-runtime/pkg/client"
klog "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
gatev1 "sigs.k8s.io/gateway-api/apis/v1"
"sigs.k8s.io/gateway-api/conformance"
v1 "sigs.k8s.io/gateway-api/conformance/apis/v1"
"sigs.k8s.io/gateway-api/conformance/tests"
"sigs.k8s.io/gateway-api/conformance/utils/config"
ksuite "sigs.k8s.io/gateway-api/conformance/utils/suite"
"sigs.k8s.io/yaml"
)
// GatewayAPIConformanceMergedSuite runs the Gateway API conformance suite
// against a single, statically deployed Traefik instance serving every Gateway.
//
// It is the counterpart to the operator-provisioned per-Gateway data planes the
// GatewayAPIConformanceSuite exercises: here one Traefik merges all Gateways
// behind a single address, so the features a single instance cannot satisfy
// (per-Gateway addresses, GatewayStaticAddresses, GatewayInfrastructurePropagation,
// and the multiple-Gateways test) are left out.
type GatewayAPIConformanceMergedSuite struct {
BaseSuite
k3sContainer *k3s.K3sContainer
kubeClient client.Client
restConfig *rest.Config
clientSet *kclientset.Clientset
}
func TestGatewayAPIConformanceMergedSuite(t *testing.T) {
suite.Run(t, new(GatewayAPIConformanceMergedSuite))
}
func (s *GatewayAPIConformanceMergedSuite) SetupSuite() {
s.BaseSuite.SetupSuite()
// Avoid panic.
klog.SetLogger(zap.New())
ctx := s.T().Context()
provider, err := testcontainers.ProviderDocker.GetProvider()
require.NoError(s.T(), err)
// Ensure image is available locally.
images, err := provider.ListImages(ctx)
require.NoError(s.T(), err)
if !slices.ContainsFunc(images, func(img testcontainers.ImageInfo) bool {
return img.Name == traefikImage
}) {
s.T().Fatal("Traefik image is not present")
}
s.k3sContainer, err = k3s.Run(
ctx,
k3sImage,
k3s.WithManifest("./fixtures/gateway-api-conformance/00-experimental-v1.6.1.yml"),
k3s.WithManifest("./fixtures/gateway-api-conformance/merged/01-rbac.yml"),
k3s.WithManifest("./fixtures/gateway-api-conformance/merged/02-traefik.yml"),
network.WithNetwork(nil, s.network),
)
require.NoError(s.T(), err)
require.NoError(s.T(), s.k3sContainer.LoadImages(ctx, traefikImage))
exitCode, _, err := s.k3sContainer.Exec(ctx, []string{"kubectl", "wait", "-n", traefikNamespace, traefikDeployment, "--for=condition=Available", "--timeout=30s"})
if err != nil || exitCode > 0 {
s.T().Fatalf("Traefik pod is not ready: %v", err)
}
kubeConfigYaml, err := s.k3sContainer.GetKubeConfig(ctx)
require.NoError(s.T(), err)
s.restConfig, err = clientcmd.RESTConfigFromKubeConfig(kubeConfigYaml)
require.NoError(s.T(), err)
s.kubeClient, err = client.New(s.restConfig, client.Options{})
require.NoError(s.T(), err)
s.clientSet, err = kclientset.NewForConfig(s.restConfig)
require.NoError(s.T(), err)
require.NoError(s.T(), gatev1.Install(s.kubeClient.Scheme()))
require.NoError(s.T(), apiextensionsv1.AddToScheme(s.kubeClient.Scheme()))
}
func (s *GatewayAPIConformanceMergedSuite) TearDownSuite() {
ctx := s.T().Context()
if s.T().Failed() || *showLog {
k3sLogs, err := s.k3sContainer.Logs(ctx)
if err == nil {
if res, err := io.ReadAll(k3sLogs); err == nil {
s.T().Log(string(res))
}
}
exitCode, result, err := s.k3sContainer.Exec(ctx, []string{"kubectl", "logs", "-n", traefikNamespace, traefikDeployment})
if err == nil || exitCode == 0 {
if res, err := io.ReadAll(result); err == nil {
s.T().Log(string(res))
}
}
}
require.NoError(s.T(), s.k3sContainer.Terminate(ctx))
s.BaseSuite.TearDownSuite()
}
func (s *GatewayAPIConformanceMergedSuite) TestK8sGatewayAPIConformanceMerged() {
// Wait for traefik to start
k3sContainerIP, err := s.k3sContainer.ContainerIP(s.T().Context())
require.NoError(s.T(), err)
err = try.GetRequest("http://"+k3sContainerIP+":9000/api/entrypoints", 10*time.Second, try.BodyContains(`"name":"web"`))
require.NoError(s.T(), err)
// Traefik reconciles a resource in a couple of seconds or less.
// They are shortened for a status Traefik will never report to fail before
// the test binary timeout, which would discard the whole run and its report.
timeoutConfig := config.DefaultTimeoutConfig()
timeoutConfig.GatewayMustHaveAddress = 60 * time.Second
timeoutConfig.GatewayMustHaveCondition = 60 * time.Second
timeoutConfig.GWCMustBeAccepted = 60 * time.Second
timeoutConfig.ListenerSetMustHaveCondition = 60 * time.Second
timeoutConfig.NamespacesMustBeReady = 60 * time.Second
cSuite, err := ksuite.NewConformanceTestSuite(ksuite.ConformanceOptions{
Client: s.kubeClient,
Clientset: s.clientSet,
RestConfig: s.restConfig,
ManifestFS: []fs.FS{&conformance.Manifests},
ConfigurableOptions: ksuite.ConfigurableOptions{
// A single instance serves every Gateway, so the report is written
// under a dedicated mode to avoid clobbering the per-Gateway
// operator report.
Mode: "merged",
GatewayClassName: "traefik",
Debug: true,
CleanupBaseResources: true,
CleanupTestResources: true,
TimeoutConfig: timeoutConfig,
EnableAllSupportedFeatures: false,
RunTest: *gatewayAPIConformanceRunTest,
Implementation: v1.Implementation{
Organization: "traefik",
Project: "traefik",
URL: "https://traefik.io/",
Version: *traefikVersion,
Contact: []string{"@traefik/maintainers"},
},
ConformanceProfiles: []ksuite.ConformanceProfileName{
ksuite.GatewayHTTPConformanceProfileName,
ksuite.GatewayGRPCConformanceProfileName,
ksuite.GatewayTLSConformanceProfileName,
},
SupportedFeatures: gateway.SupportedFeatures(),
// The following tests are skipped because they require features that a single Traefik instance
// cannot satisfy in merged mode cause they enforce having an operator.
SkipTests: []string{
tests.HTTPRouteMultipleGateways.ShortName,
tests.TLSRouteHostnameIntersection.ShortName,
},
},
})
require.NoError(s.T(), err)
cSuite.Setup(s.T(), tests.ConformanceTests)
err = cSuite.Run(s.T(), tests.ConformanceTests)
require.NoError(s.T(), err)
report, err := cSuite.Report()
require.NoError(s.T(), err, "failed generating conformance report")
// Ordering profile reports for the serialized report to be comparable.
slices.SortFunc(report.ProfileReports, func(a, b v1.ProfileReport) int {
return strings.Compare(a.Name, b.Name)
})
rawReport, err := yaml.Marshal(report)
require.NoError(s.T(), err)
s.T().Logf("Conformance report:\n%s", string(rawReport))
require.NoError(s.T(), os.MkdirAll("./gateway-api-conformance-reports/"+report.GatewayAPIVersion, 0o755))
outFile := filepath.Join("gateway-api-conformance-reports/"+report.GatewayAPIVersion, fmt.Sprintf("%s-%s-%s-report.yaml", report.GatewayAPIChannel, report.Version, report.Mode))
require.NoError(s.T(), os.WriteFile(outFile, rawReport, 0o600))
s.T().Logf("Report written to: %s", outFile)
}
+425 -69
View File
@@ -3,27 +3,35 @@
package integration
import (
"context"
"encoding/binary"
"fmt"
"io"
"io/fs"
"maps"
"net/netip"
"os"
"path/filepath"
"slices"
"strings"
"sync"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/exec"
"github.com/testcontainers/testcontainers-go/modules/k3s"
"github.com/testcontainers/testcontainers-go/network"
"github.com/traefik/traefik/v3/integration/try"
"github.com/traefik/traefik/v3/pkg/provider/kubernetes/gateway"
corev1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
kclientset "k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/utils/ptr"
"sigs.k8s.io/controller-runtime/pkg/client"
klog "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
@@ -33,10 +41,29 @@ import (
"sigs.k8s.io/gateway-api/conformance/tests"
"sigs.k8s.io/gateway-api/conformance/utils/config"
ksuite "sigs.k8s.io/gateway-api/conformance/utils/suite"
"sigs.k8s.io/gateway-api/pkg/features"
"sigs.k8s.io/yaml"
)
// GatewayAPIConformanceSuite tests suite.
const (
// traefikConformanceImage tags the image built from the working tree for
// the data plane pods. The tag is fixed, and not latest, so that the pods
// keep the image side loaded into the node: a latest tag defaults
// imagePullPolicy to Always, which pulls the released Traefik instead.
traefikConformanceImage = "traefik/traefik:conformance"
operatorImage = "traefik/gateway-operator:latest"
operatorNamespace = "traefik-gateway-operator-system"
operatorDeployment = "deployments/traefik-gateway-operator"
)
// GatewayAPIConformanceSuite runs the Gateway API conformance suite against the
// Traefik data planes the Traefik Gateway API operator provisions per Gateway.
//
// The operator owns the infrastructure part of the specification, which a
// single statically deployed instance cannot satisfy: a per-Gateway
// status.addresses, GatewayStaticAddresses, GatewayInfrastructurePropagation,
// and the tests needing two Gateways reachable at two distinct addresses.
type GatewayAPIConformanceSuite struct {
BaseSuite
@@ -44,6 +71,9 @@ type GatewayAPIConformanceSuite struct {
kubeClient client.Client
restConfig *rest.Config
clientSet *kclientset.Clientset
loadBalancer *nodeLoadBalancer
cancelLoadBalancer context.CancelFunc
}
func TestGatewayAPIConformanceSuite(t *testing.T) {
@@ -56,78 +86,79 @@ func (s *GatewayAPIConformanceSuite) SetupSuite() {
// Avoid panic.
klog.SetLogger(zap.New())
provider, err := testcontainers.ProviderDocker.GetProvider()
if err != nil {
s.T().Fatal(err)
}
ctx := s.T().Context()
// Ensure image is available locally.
images, err := provider.ListImages(ctx)
if err != nil {
s.T().Fatal(err)
}
provider, err := testcontainers.ProviderDocker.GetProvider()
require.NoError(s.T(), err)
if !slices.ContainsFunc(images, func(img testcontainers.ImageInfo) bool {
return img.Name == traefikImage
}) {
s.T().Fatal("Traefik image is not present")
// Ensure images are available locally: make test-gateway-api-conformance
// builds the Traefik one and pulls the operator one.
images, err := provider.ListImages(ctx)
require.NoError(s.T(), err)
for _, image := range []string{traefikConformanceImage, operatorImage} {
if !slices.ContainsFunc(images, func(img testcontainers.ImageInfo) bool {
return img.Name == image
}) {
s.T().Fatalf("Image %s is not present", image)
}
}
s.k3sContainer, err = k3s.Run(
ctx,
k3sImage,
// The k3s service load balancer exposes Services through host ports,
// which a single node cannot do for the several port 80 Services the
// operator provisions. nodeLoadBalancer assigns their addresses.
testcontainers.WithCmdArgs("--disable=servicelb"),
k3s.WithManifest("./fixtures/gateway-api-conformance/00-experimental-v1.6.1.yml"),
k3s.WithManifest("./fixtures/gateway-api-conformance/01-rbac.yml"),
k3s.WithManifest("./fixtures/gateway-api-conformance/02-traefik.yml"),
k3s.WithManifest("./fixtures/gateway-api-conformance/operator/01-operator.yml"),
k3s.WithManifest("./fixtures/gateway-api-conformance/operator/02-gatewayclass.yml"),
k3s.WithManifest("./fixtures/gateway-api-conformance/operator/03-bootstrap-gateway.yml"),
network.WithNetwork(nil, s.network),
)
if err != nil {
s.T().Fatal(err)
}
require.NoError(s.T(), err)
if err = s.k3sContainer.LoadImages(ctx, traefikImage); err != nil {
s.T().Fatal(err)
}
require.NoError(s.T(), s.k3sContainer.LoadImages(ctx, traefikConformanceImage, operatorImage))
exitCode, _, err := s.k3sContainer.Exec(ctx, []string{"kubectl", "wait", "-n", traefikNamespace, traefikDeployment, "--for=condition=Available", "--timeout=30s"})
exitCode, _, err := s.k3sContainer.Exec(ctx, []string{"kubectl", "wait", "-n", operatorNamespace, operatorDeployment, "--for=condition=Available", "--timeout=120s"})
if err != nil || exitCode > 0 {
s.T().Fatalf("Traefik pod is not ready: %v", err)
s.T().Fatalf("Operator pod is not ready: %v", err)
}
kubeConfigYaml, err := s.k3sContainer.GetKubeConfig(ctx)
if err != nil {
s.T().Fatal(err)
}
require.NoError(s.T(), err)
s.restConfig, err = clientcmd.RESTConfigFromKubeConfig(kubeConfigYaml)
if err != nil {
s.T().Fatalf("Error loading Kubernetes config: %v", err)
}
require.NoError(s.T(), err)
s.kubeClient, err = client.New(s.restConfig, client.Options{})
if err != nil {
s.T().Fatalf("Error initializing Kubernetes client: %v", err)
}
require.NoError(s.T(), err)
s.clientSet, err = kclientset.NewForConfig(s.restConfig)
if err != nil {
s.T().Fatalf("Error initializing Kubernetes REST client: %v", err)
}
require.NoError(s.T(), err)
if err = gatev1.Install(s.kubeClient.Scheme()); err != nil {
s.T().Fatal(err)
}
require.NoError(s.T(), gatev1.Install(s.kubeClient.Scheme()))
require.NoError(s.T(), apiextensionsv1.AddToScheme(s.kubeClient.Scheme()))
if err = apiextensionsv1.AddToScheme(s.kubeClient.Scheme()); err != nil {
s.T().Fatal(err)
}
s.loadBalancer, err = newNodeLoadBalancer(ctx, s.k3sContainer, s.clientSet)
require.NoError(s.T(), err)
// The load balancer must keep reconciling across the whole suite, not just
// for the lifetime of whichever test happens to be running when a Service
// needs an address, so it gets a context of its own.
lbCtx, cancel := context.WithCancel(context.WithoutCancel(ctx))
s.cancelLoadBalancer = cancel
s.loadBalancer.Start(lbCtx)
}
func (s *GatewayAPIConformanceSuite) TearDownSuite() {
ctx := s.T().Context()
if s.cancelLoadBalancer != nil {
s.cancelLoadBalancer()
}
if s.T().Failed() || *showLog {
k3sLogs, err := s.k3sContainer.Logs(ctx)
if err == nil {
@@ -136,37 +167,31 @@ func (s *GatewayAPIConformanceSuite) TearDownSuite() {
}
}
exitCode, result, err := s.k3sContainer.Exec(ctx, []string{"kubectl", "logs", "-n", traefikNamespace, traefikDeployment})
if err == nil || exitCode == 0 {
if res, err := io.ReadAll(result); err == nil {
s.T().Log(string(res))
}
}
s.logCommand(ctx, "kubectl", "logs", "-n", operatorNamespace, operatorDeployment)
// The data planes are spread over the conformance namespaces, and are
// where a routing failure shows up.
s.logCommand(ctx, "kubectl", "get", "gateways,pods,services", "--all-namespaces")
s.logDataPlanes(ctx)
}
if err := s.k3sContainer.Terminate(ctx); err != nil {
s.T().Fatal(err)
}
require.NoError(s.T(), s.k3sContainer.Terminate(ctx))
s.BaseSuite.TearDownSuite()
}
func (s *GatewayAPIConformanceSuite) TestK8sGatewayAPIConformance() {
// Wait for traefik to start
k3sContainerIP, err := s.k3sContainer.ContainerIP(s.T().Context())
require.NoError(s.T(), err)
err = try.GetRequest("http://"+k3sContainerIP+":9000/api/entrypoints", 10*time.Second, try.BodyContains(`"name":"web"`))
require.NoError(s.T(), err)
// Traefik reconciles a resource in a couple of seconds or less.
// They are shortened for a status Traefik will never report to fail before
// the test binary timeout, which would discard the whole run and its report.
// Provisioning a data plane adds a Deployment rollout to every Gateway
// reconciliation, and the GatewayClass is only accepted once the first data
// plane runs, so the timeouts are longer than for a statically deployed
// instance. They stay shortened for a status Traefik will never report, to
// fail before the test binary timeout, which would discard the whole run
// and its report.
timeoutConfig := config.DefaultTimeoutConfig()
timeoutConfig.GatewayMustHaveAddress = 30 * time.Second
timeoutConfig.GatewayMustHaveCondition = 30 * time.Second
timeoutConfig.GWCMustBeAccepted = 30 * time.Second
timeoutConfig.ListenerSetMustHaveCondition = 30 * time.Second
timeoutConfig.GatewayMustHaveAddress = 60 * time.Second
timeoutConfig.GatewayMustHaveCondition = 60 * time.Second
timeoutConfig.GWCMustBeAccepted = 60 * time.Second
timeoutConfig.ListenerSetMustHaveCondition = 60 * time.Second
timeoutConfig.NamespacesMustBeReady = 60 * time.Second
cSuite, err := ksuite.NewConformanceTestSuite(ksuite.ConformanceOptions{
@@ -182,6 +207,14 @@ func (s *GatewayAPIConformanceSuite) TestK8sGatewayAPIConformance() {
TimeoutConfig: timeoutConfig,
EnableAllSupportedFeatures: false,
RunTest: *gatewayAPIConformanceRunTest,
UsableNetworkAddresses: []gatev1.GatewaySpecAddress{{
Type: ptr.To(gatev1.IPAddressType),
Value: s.loadBalancer.StaticAddress(),
}},
UnusableNetworkAddresses: []gatev1.GatewaySpecAddress{{
Type: ptr.To(gatev1.IPAddressType),
Value: nodeLBUnusableAddress,
}},
Implementation: v1.Implementation{
Organization: "traefik",
Project: "traefik",
@@ -195,8 +228,13 @@ func (s *GatewayAPIConformanceSuite) TestK8sGatewayAPIConformance() {
ksuite.GatewayTLSConformanceProfileName,
ksuite.GatewayTCPConformanceProfileName,
},
SupportedFeatures: gateway.SupportedFeatures(),
SkipTests: []string{tests.HTTPRouteMultipleGateways.ShortName},
// Here we are concatenating the features supported by the Traefik Gateway API implementation with the
// features supported by the Traefik Gateway API operator.
SupportedFeatures: slices.Concat(gateway.SupportedFeatures(), []features.FeatureName{
features.GatewayEmptyAddressFeature.Name,
features.GatewayInfrastructurePropagationFeature.Name,
features.GatewayStaticAddressesFeature.Name,
}),
},
})
require.NoError(s.T(), err)
@@ -223,3 +261,321 @@ func (s *GatewayAPIConformanceSuite) TestK8sGatewayAPIConformance() {
require.NoError(s.T(), os.WriteFile(outFile, rawReport, 0o600))
s.T().Logf("Report written to: %s", outFile)
}
// logDataPlanes logs the data plane pods one by one: kubectl logs cannot select
// pods across namespaces.
func (s *GatewayAPIConformanceSuite) logDataPlanes(ctx context.Context) {
if s.clientSet == nil {
return
}
pods, err := s.clientSet.CoreV1().Pods(metav1.NamespaceAll).List(ctx, metav1.ListOptions{
LabelSelector: "app.kubernetes.io/managed-by=traefik-gateway-operator",
})
if err != nil {
s.T().Logf("listing data plane pods: %v", err)
return
}
for _, pod := range pods.Items {
s.logCommand(ctx, "kubectl", "logs", "-n", pod.Namespace, pod.Name, "--tail=200")
}
}
// logCommand runs a command in the k3s container and logs its output.
func (s *GatewayAPIConformanceSuite) logCommand(ctx context.Context, command ...string) {
exitCode, result, err := s.k3sContainer.Exec(ctx, command, exec.Multiplexed())
if err != nil {
s.T().Logf("%v: %v", command, err)
return
}
output, err := io.ReadAll(result)
if err != nil {
s.T().Logf("%v: %v", command, err)
return
}
s.T().Logf("%v (exit code %d):\n%s", command, exitCode, output)
}
const (
// nodeLBPoolSize is the number of addresses reserved at the top of the node
// subnet. The Docker IPAM allocates container addresses from the bottom of
// the subnet, so the top stays free.
nodeLBPoolSize = 48
// nodeLBUnusableAddress is a TEST-NET-1 (RFC 5737) address: routable
// nowhere, and never part of the pool. The GatewayStaticAddresses
// conformance test needs an address the infrastructure cannot assign.
nodeLBUnusableAddress = "192.0.2.1"
nodeLBReconcileInterval = 500 * time.Millisecond
)
// nodeLoadBalancer assigns addresses to LoadBalancer Services on the single-node
// k3s cluster the operator conformance suite runs on.
//
// The k3s built-in ServiceLB is disabled for that suite: it exposes Services
// through host ports, and the operator provisions one Service per Gateway, most
// of them on port 80, which a single node cannot satisfy. Addresses are taken
// from the unallocated end of the node subnet, added to the node interface, and
// published in the Service status, which is all kube-proxy needs to route them.
// This is the role MetalLB plays in the reference conformance environment.
type nodeLoadBalancer struct {
container *k3s.K3sContainer
client kclientset.Interface
iface string
prefix netip.Prefix
// staticAddress is held out of the automatic rotation, so that it stays
// available to a Gateway requesting it through spec.addresses.
staticAddress string
pool []string
mu sync.Mutex
assigned map[string]string
bound map[string]struct{}
}
// newNodeLoadBalancer discovers the node interface and carves an address pool
// out of its subnet.
func newNodeLoadBalancer(ctx context.Context, container *k3s.K3sContainer, client kclientset.Interface) (*nodeLoadBalancer, error) {
nodeIP, err := container.ContainerIP(ctx)
if err != nil {
return nil, fmt.Errorf("getting node address: %w", err)
}
iface, prefix, err := nodeInterface(ctx, container, nodeIP)
if err != nil {
return nil, err
}
pool, err := addressPool(prefix, nodeLBPoolSize)
if err != nil {
return nil, err
}
return &nodeLoadBalancer{
container: container,
client: client,
iface: iface,
prefix: prefix,
staticAddress: pool[0],
pool: pool[1:],
assigned: map[string]string{},
bound: map[string]struct{}{},
}, nil
}
// Start reconciles the LoadBalancer Services until ctx is done.
func (lb *nodeLoadBalancer) Start(ctx context.Context) {
go func() {
tick := time.Tick(nodeLBReconcileInterval)
for {
select {
case <-ctx.Done():
return
case <-tick:
// Errors are transient (a Service deleted mid-reconciliation,
// a conflicting status update) and resolve on the next tick.
_ = lb.reconcile(ctx)
}
}
}()
}
// StaticAddress returns the address reserved for the Gateways requesting one
// through spec.addresses.
func (lb *nodeLoadBalancer) StaticAddress() string {
return lb.staticAddress
}
func (lb *nodeLoadBalancer) reconcile(ctx context.Context) error {
services, err := lb.client.CoreV1().Services(metav1.NamespaceAll).List(ctx, metav1.ListOptions{})
if err != nil {
return err
}
live := map[string]struct{}{}
for _, service := range services.Items {
if service.Spec.Type != corev1.ServiceTypeLoadBalancer {
continue
}
live[service.Namespace+"/"+service.Name] = struct{}{}
if err := lb.reconcileService(ctx, &service); err != nil {
return err
}
}
// The conformance suite creates and deletes Gateways throughout the run,
// and each of them holds an address while it exists. Reclaiming the
// addresses of the deleted ones keeps the pool from running out.
lb.mu.Lock()
maps.DeleteFunc(lb.assigned, func(key, _ string) bool {
_, ok := live[key]
return !ok
})
lb.mu.Unlock()
return nil
}
func (lb *nodeLoadBalancer) reconcileService(ctx context.Context, service *corev1.Service) error {
address, ok := lb.address(service)
if !ok {
// A requested address outside the pool cannot be assigned: the Service
// is left without an ingress address, as a real load balancer would.
if len(service.Status.LoadBalancer.Ingress) == 0 {
return nil
}
service.Status.LoadBalancer.Ingress = nil
_, err := lb.client.CoreV1().Services(service.Namespace).UpdateStatus(ctx, service, metav1.UpdateOptions{})
return err
}
if len(service.Status.LoadBalancer.Ingress) == 1 && service.Status.LoadBalancer.Ingress[0].IP == address {
return nil
}
if err := lb.bind(ctx, address); err != nil {
return err
}
service.Status.LoadBalancer.Ingress = []corev1.LoadBalancerIngress{{IP: address}}
_, err := lb.client.CoreV1().Services(service.Namespace).UpdateStatus(ctx, service, metav1.UpdateOptions{})
return err
}
// address returns the address to publish for a Service. A Service requesting a
// specific address only gets it when it belongs to the pool.
func (lb *nodeLoadBalancer) address(service *corev1.Service) (string, bool) {
lb.mu.Lock()
defer lb.mu.Unlock()
key := service.Namespace + "/" + service.Name
if requested := service.Spec.LoadBalancerIP; requested != "" {
if requested != lb.staticAddress {
return "", false
}
lb.assigned[key] = requested
return requested, true
}
if address, ok := lb.assigned[key]; ok {
return address, true
}
used := slices.Collect(maps.Values(lb.assigned))
for _, address := range lb.pool {
if slices.Contains(used, address) {
continue
}
lb.assigned[key] = address
return address, true
}
return "", false
}
// bind adds the address to the node interface, so that the node answers for it
// and kube-proxy sees the traffic.
func (lb *nodeLoadBalancer) bind(ctx context.Context, address string) error {
lb.mu.Lock()
if _, ok := lb.bound[address]; ok {
lb.mu.Unlock()
return nil
}
lb.mu.Unlock()
cidr := fmt.Sprintf("%s/%d", address, lb.prefix.Bits())
exitCode, reader, err := lb.container.Exec(ctx, []string{"ip", "addr", "add", cidr, "dev", lb.iface}, exec.Multiplexed())
if err != nil {
return fmt.Errorf("binding %s: %w", cidr, err)
}
if exitCode != 0 {
output, _ := io.ReadAll(reader)
return fmt.Errorf("binding %s: exit code %d: %s", cidr, exitCode, output)
}
lb.mu.Lock()
lb.bound[address] = struct{}{}
lb.mu.Unlock()
return nil
}
// nodeInterface returns the name and subnet of the interface holding nodeIP.
func nodeInterface(ctx context.Context, container *k3s.K3sContainer, nodeIP string) (string, netip.Prefix, error) {
exitCode, reader, err := container.Exec(ctx, []string{"ip", "-o", "-4", "addr", "show"}, exec.Multiplexed())
if err != nil {
return "", netip.Prefix{}, fmt.Errorf("listing node addresses: %w", err)
}
output, err := io.ReadAll(reader)
if err != nil {
return "", netip.Prefix{}, fmt.Errorf("reading node addresses: %w", err)
}
if exitCode != 0 {
return "", netip.Prefix{}, fmt.Errorf("listing node addresses: exit code %d: %s", exitCode, output)
}
// Lines look like: 2: eth0 inet 172.28.0.2/16 brd 172.28.255.255 scope global eth0
for line := range strings.Lines(string(output)) {
fields := strings.Fields(line)
if len(fields) < 4 || fields[2] != "inet" {
continue
}
prefix, err := netip.ParsePrefix(fields[3])
if err != nil || prefix.Addr().String() != nodeIP {
continue
}
return strings.TrimSuffix(fields[1], ":"), prefix, nil
}
return "", netip.Prefix{}, fmt.Errorf("no interface holding node address %s in:\n%s", nodeIP, output)
}
// addressPool returns the size highest usable addresses of an IPv4 prefix,
// lowest first.
func addressPool(prefix netip.Prefix, size int) ([]string, error) {
if !prefix.Addr().Is4() {
return nil, fmt.Errorf("not an IPv4 prefix: %s", prefix)
}
hostCount := uint64(1) << (32 - prefix.Bits())
if hostCount < uint64(size)+2 {
return nil, fmt.Errorf("prefix %s is too small for a pool of %d addresses", prefix, size)
}
network := prefix.Masked().Addr().As4()
// The last address of the subnet is the broadcast address.
last := binary.BigEndian.Uint32(network[:]) + uint32(hostCount) - 2
first := last - uint32(size) + 1
pool := make([]string, 0, size)
for i := range size {
var address [4]byte
binary.BigEndian.PutUint32(address[:], first+uint32(i))
pool = append(pool, netip.AddrFrom4(address).String())
}
return pool, nil
}
+1 -1
View File
@@ -83,7 +83,7 @@ type BaseSuite struct {
func (s *BaseSuite) SetupSuite() {
if isDockerDesktop(s.T()) {
_, err := os.Stat(tailscaleSecretFilePath)
require.NoError(s.T(), err, "Tailscale need to be configured when running integration tests with Docker Desktop: (https://doc.traefik.io/traefik/v2.11/contributing/building-testing/#testing)")
require.NoError(s.T(), err, "Tailscale need to be configured when running integration tests with Docker Desktop: (https://doc.traefik.io/traefik/contributing/building-testing/#testing)")
}
// configure default standard log.
+29 -6
View File
@@ -11,7 +11,14 @@
"accessLogs": true,
"metrics": true,
"tracing": true,
"traceVerbosity": "minimal"
"traceVerbosity": "minimal",
"metadata": {
"ingress": {
"namespace": "default",
"name": "api.route",
"kind": "IngressRoute"
}
}
},
"status": "enabled",
"using": [
@@ -32,7 +39,14 @@
"accessLogs": true,
"metrics": true,
"tracing": true,
"traceVerbosity": "minimal"
"traceVerbosity": "minimal",
"metadata": {
"ingress": {
"namespace": "default",
"name": "test.route",
"kind": "IngressRoute"
}
}
},
"status": "enabled",
"using": [
@@ -54,10 +68,10 @@
"loadBalancer": {
"servers": [
{
"url": "http://10.42.0.3:80"
"url": "http://10.42.0.8:80"
},
{
"url": "http://10.42.0.5:80"
"url": "http://10.42.0.4:80"
}
],
"strategy": "wrr",
@@ -66,13 +80,22 @@
"flushInterval": "100ms"
}
},
"observability": {
"metadata": {
"kubernetes": {
"namespace": "default",
"name": "whoami",
"port": "80"
}
}
},
"status": "enabled",
"usedBy": [
"default-test-route-6b204d94623b3df4370c@kubernetescrd"
],
"serverStatus": {
"http://10.42.0.3:80": "UP",
"http://10.42.0.5:80": "UP"
"http://10.42.0.4:80": "UP",
"http://10.42.0.8:80": "UP"
}
},
"noop@internal": {
+114 -29
View File
@@ -11,7 +11,14 @@
"accessLogs": true,
"metrics": true,
"tracing": true,
"traceVerbosity": "minimal"
"traceVerbosity": "minimal",
"metadata": {
"ingress": {
"namespace": "default",
"name": "api.route",
"kind": "IngressRoute"
}
}
},
"status": "enabled",
"using": [
@@ -32,7 +39,14 @@
"accessLogs": true,
"metrics": true,
"tracing": true,
"traceVerbosity": "minimal"
"traceVerbosity": "minimal",
"metadata": {
"ingress": {
"namespace": "default",
"name": "test.route",
"kind": "IngressRoute"
}
}
},
"status": "enabled",
"using": [
@@ -53,7 +67,14 @@
"accessLogs": true,
"metrics": true,
"tracing": true,
"traceVerbosity": "minimal"
"traceVerbosity": "minimal",
"metadata": {
"ingress": {
"namespace": "default",
"name": "test2.route",
"kind": "IngressRoute"
}
}
},
"status": "enabled",
"using": [
@@ -71,7 +92,14 @@
"accessLogs": true,
"metrics": true,
"tracing": true,
"traceVerbosity": "minimal"
"traceVerbosity": "minimal",
"metadata": {
"ingress": {
"namespace": "default",
"name": "test3.route",
"kind": "IngressRoute"
}
}
},
"status": "enabled",
"using": [
@@ -89,7 +117,14 @@
"accessLogs": true,
"metrics": true,
"tracing": true,
"traceVerbosity": "minimal"
"traceVerbosity": "minimal",
"metadata": {
"ingress": {
"namespace": "default",
"name": "test4.route",
"kind": "IngressRoute"
}
}
},
"status": "enabled",
"using": [
@@ -107,7 +142,14 @@
"accessLogs": true,
"metrics": true,
"tracing": true,
"traceVerbosity": "minimal"
"traceVerbosity": "minimal",
"metadata": {
"ingress": {
"namespace": "default",
"name": "testst.route",
"kind": "IngressRoute"
}
}
},
"status": "enabled",
"using": [
@@ -125,7 +167,14 @@
"accessLogs": true,
"metrics": true,
"tracing": true,
"traceVerbosity": "minimal"
"traceVerbosity": "minimal",
"metadata": {
"ingress": {
"namespace": "other-ns",
"name": "test6.route",
"kind": "IngressRoute"
}
}
},
"error": [
"the service \"other-ns-wrr3@kubernetescrd\" does not exist"
@@ -208,10 +257,10 @@
"loadBalancer": {
"servers": [
{
"url": "http://10.42.0.3:80"
"url": "http://10.42.0.8:80"
},
{
"url": "http://10.42.0.5:80"
"url": "http://10.42.0.4:80"
}
],
"strategy": "wrr",
@@ -220,23 +269,32 @@
"flushInterval": "100ms"
}
},
"observability": {
"metadata": {
"kubernetes": {
"namespace": "default",
"name": "whoami",
"port": "80"
}
}
},
"status": "enabled",
"usedBy": [
"default-test-route-6b204d94623b3df4370c@kubernetescrd"
],
"serverStatus": {
"http://10.42.0.3:80": "UP",
"http://10.42.0.5:80": "UP"
"http://10.42.0.4:80": "UP",
"http://10.42.0.8:80": "UP"
}
},
"default-test2-route-23c7f4c450289ee29016@kubernetescrd": {
"loadBalancer": {
"servers": [
{
"url": "http://10.42.0.3:80"
"url": "http://10.42.0.8:80"
},
{
"url": "http://10.42.0.5:80"
"url": "http://10.42.0.4:80"
}
],
"strategy": "wrr",
@@ -245,23 +303,32 @@
"flushInterval": "100ms"
}
},
"observability": {
"metadata": {
"kubernetes": {
"namespace": "default",
"name": "whoami",
"port": "80"
}
}
},
"status": "enabled",
"usedBy": [
"default-test2-route-23c7f4c450289ee29016@kubernetescrd"
],
"serverStatus": {
"http://10.42.0.3:80": "UP",
"http://10.42.0.5:80": "UP"
"http://10.42.0.4:80": "UP",
"http://10.42.0.8:80": "UP"
}
},
"default-testst-route-60ad45fcb5fc1f5f3629@kubernetescrd": {
"loadBalancer": {
"servers": [
{
"url": "http://10.42.0.3:80"
"url": "http://10.42.0.8:80"
},
{
"url": "http://10.42.0.5:80"
"url": "http://10.42.0.4:80"
}
],
"strategy": "wrr",
@@ -271,23 +338,32 @@
},
"serversTransport": "default-mytransport@kubernetescrd"
},
"observability": {
"metadata": {
"kubernetes": {
"namespace": "default",
"name": "whoami",
"port": "80"
}
}
},
"status": "enabled",
"usedBy": [
"default-testst-route-60ad45fcb5fc1f5f3629@kubernetescrd"
],
"serverStatus": {
"http://10.42.0.3:80": "UP",
"http://10.42.0.5:80": "UP"
"http://10.42.0.4:80": "UP",
"http://10.42.0.8:80": "UP"
}
},
"default-whoami-80@kubernetescrd": {
"loadBalancer": {
"servers": [
{
"url": "http://10.42.0.3:80"
"url": "http://10.42.0.8:80"
},
{
"url": "http://10.42.0.5:80"
"url": "http://10.42.0.4:80"
}
],
"strategy": "wrr",
@@ -296,10 +372,19 @@
"flushInterval": "100ms"
}
},
"observability": {
"metadata": {
"kubernetes": {
"namespace": "default",
"name": "whoami",
"port": "80"
}
}
},
"status": "enabled",
"serverStatus": {
"http://10.42.0.3:80": "UP",
"http://10.42.0.5:80": "UP"
"http://10.42.0.4:80": "UP",
"http://10.42.0.8:80": "UP"
}
},
"default-wrr1@kubernetescrd": {
@@ -371,17 +456,17 @@
"loadBalancer": {
"servers": [
{
"address": "10.42.0.2:8080"
"address": "10.42.0.7:8080"
},
{
"address": "10.42.0.6:8080"
"address": "10.42.0.9:8080"
}
]
},
"status": "enabled",
"serverStatus": {
"10.42.0.2:8080": "UP",
"10.42.0.6:8080": "UP"
"10.42.0.7:8080": "UP",
"10.42.0.9:8080": "UP"
}
},
"default-test3.route-673acf455cb2dab0b43a@kubernetescrd": {
@@ -430,10 +515,10 @@
"loadBalancer": {
"servers": [
{
"address": "10.42.0.4:8090"
"address": "10.42.0.2:8090"
},
{
"address": "10.42.0.7:8090"
"address": "10.42.0.6:8090"
}
]
},
+15 -7
View File
@@ -57,9 +57,8 @@
"metadata": {
"ingress": {
"namespace": "default",
"ingressName": "test.ingress",
"serviceName": "whoami",
"servicePort": "http"
"name": "test.ingress",
"kind": "Ingress"
}
}
},
@@ -111,10 +110,10 @@
"loadBalancer": {
"servers": [
{
"url": "http://10.42.0.3:80"
"url": "http://10.42.0.8:80"
},
{
"url": "http://10.42.0.5:80"
"url": "http://10.42.0.4:80"
}
],
"strategy": "wrr",
@@ -123,13 +122,22 @@
"flushInterval": "100ms"
}
},
"observability": {
"metadata": {
"kubernetes": {
"namespace": "default",
"name": "whoami",
"port": "http"
}
}
},
"status": "enabled",
"usedBy": [
"default-test-ingress-whoami-test-whoami@kubernetes"
],
"serverStatus": {
"http://10.42.0.3:80": "UP",
"http://10.42.0.5:80": "UP"
"http://10.42.0.4:80": "UP",
"http://10.42.0.8:80": "UP"
}
},
"noop@internal": {
+34 -20
View File
@@ -57,9 +57,8 @@
"metadata": {
"ingress": {
"namespace": "default",
"ingressName": "test.ingress.https",
"serviceName": "whoami",
"servicePort": "http"
"name": "test.ingress.https",
"kind": "Ingress"
}
}
},
@@ -83,9 +82,8 @@
"metadata": {
"ingress": {
"namespace": "default",
"ingressName": "test.ingress",
"serviceName": "whoami",
"servicePort": "http"
"name": "test.ingress",
"kind": "Ingress"
}
}
},
@@ -109,9 +107,8 @@
"metadata": {
"ingress": {
"namespace": "default",
"ingressName": "whoami-drop-route",
"serviceName": "whoami",
"servicePort": "80"
"name": "whoami-drop-route",
"kind": "Ingress"
}
}
},
@@ -135,9 +132,8 @@
"metadata": {
"ingress": {
"namespace": "default",
"ingressName": "whoami-keep-route",
"serviceName": "whoami",
"servicePort": "80"
"name": "whoami-keep-route",
"kind": "Ingress"
}
}
},
@@ -189,10 +185,10 @@
"loadBalancer": {
"servers": [
{
"url": "http://10.42.0.3:80"
"url": "http://10.42.0.8:80"
},
{
"url": "http://10.42.0.5:80"
"url": "http://10.42.0.4:80"
}
],
"strategy": "wrr",
@@ -201,24 +197,33 @@
"flushInterval": "100ms"
}
},
"observability": {
"metadata": {
"kubernetes": {
"namespace": "default",
"name": "whoami",
"port": "80"
}
}
},
"status": "enabled",
"usedBy": [
"default-whoami-drop-route-whoami-test-drop-drop@kubernetes",
"default-whoami-keep-route-whoami-test-keep-keep@kubernetes"
],
"serverStatus": {
"http://10.42.0.3:80": "UP",
"http://10.42.0.5:80": "UP"
"http://10.42.0.4:80": "UP",
"http://10.42.0.8:80": "UP"
}
},
"default-whoami-http@kubernetes": {
"loadBalancer": {
"servers": [
{
"url": "http://10.42.0.3:80"
"url": "http://10.42.0.8:80"
},
{
"url": "http://10.42.0.5:80"
"url": "http://10.42.0.4:80"
}
],
"strategy": "wrr",
@@ -227,14 +232,23 @@
"flushInterval": "100ms"
}
},
"observability": {
"metadata": {
"kubernetes": {
"namespace": "default",
"name": "whoami",
"port": "http"
}
}
},
"status": "enabled",
"usedBy": [
"default-test-ingress-https-whoami-test-https-whoami@kubernetes",
"default-test-ingress-whoami-test-whoami@kubernetes"
],
"serverStatus": {
"http://10.42.0.3:80": "UP",
"http://10.42.0.5:80": "UP"
"http://10.42.0.4:80": "UP",
"http://10.42.0.8:80": "UP"
}
},
"noop@internal": {
+15 -7
View File
@@ -57,9 +57,8 @@
"metadata": {
"ingress": {
"namespace": "default",
"ingressName": "whoami-keep-route",
"serviceName": "whoami",
"servicePort": "80"
"name": "whoami-keep-route",
"kind": "Ingress"
}
}
},
@@ -111,10 +110,10 @@
"loadBalancer": {
"servers": [
{
"url": "http://10.42.0.3:80"
"url": "http://10.42.0.8:80"
},
{
"url": "http://10.42.0.5:80"
"url": "http://10.42.0.4:80"
}
],
"strategy": "wrr",
@@ -123,13 +122,22 @@
"flushInterval": "100ms"
}
},
"observability": {
"metadata": {
"kubernetes": {
"namespace": "default",
"name": "whoami",
"port": "80"
}
}
},
"status": "enabled",
"usedBy": [
"default-whoami-keep-route-whoami-test-keep-keep@kubernetes"
],
"serverStatus": {
"http://10.42.0.3:80": "UP",
"http://10.42.0.5:80": "UP"
"http://10.42.0.4:80": "UP",
"http://10.42.0.8:80": "UP"
}
},
"noop@internal": {
+36 -7
View File
@@ -66,6 +66,10 @@ type Service struct {
Weighted *WeightedRoundRobin `json:"weighted,omitempty" toml:"weighted,omitempty" yaml:"weighted,omitempty" label:"-" export:"true"`
Mirroring *Mirroring `json:"mirroring,omitempty" toml:"mirroring,omitempty" yaml:"mirroring,omitempty" label:"-" export:"true"`
Failover *Failover `json:"failover,omitempty" toml:"failover,omitempty" yaml:"failover,omitempty" label:"-" export:"true"`
// Observability carries provider-populated metadata surfaced per request
// in access logs and future metric labels. Mirrors the Router.Observability
// shape. Not user-configurable.
Observability *ServiceObservabilityConfig `json:"observability,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-"`
}
// Merge merges another Service into this one.
@@ -185,17 +189,42 @@ func (r *RouterObservabilityConfig) SetDefaults() {
// ObservabilityMetadata holds the observability metadata configuration.
type ObservabilityMetadata struct {
Ingress *KubernetesIngressMetadata `json:"ingress,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-"`
Ingress *KubernetesMetadata `json:"ingress,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-"`
}
// +k8s:deepcopy-gen=true
// KubernetesIngressMetadata holds the Kubernetes Ingress metadata.
type KubernetesIngressMetadata struct {
Namespace string `json:"namespace,omitempty"`
IngressName string `json:"ingressName,omitempty"`
ServiceName string `json:"serviceName,omitempty"`
ServicePort string `json:"servicePort,omitempty"`
// KubernetesMetadata holds the Kubernetes Ingress metadata.
type KubernetesMetadata struct {
Namespace string `json:"namespace,omitempty"`
Name string `json:"name,omitempty"`
Kind string `json:"kind,omitempty"`
}
// +k8s:deepcopy-gen=true
// ServiceObservabilityConfig holds the observability configuration of a service.
type ServiceObservabilityConfig struct {
// Metadata carries provider-populated identity for the leaf service.
Metadata *ServiceObservabilityMetadata `json:"metadata,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-"`
}
// +k8s:deepcopy-gen=true
// ServiceObservabilityMetadata carries provider-populated identity for a leaf service.
type ServiceObservabilityMetadata struct {
Kubernetes *KubernetesServiceMetadata `json:"kubernetes,omitempty"`
}
// +k8s:deepcopy-gen=true
// KubernetesServiceMetadata holds the Kubernetes Service identity backing a
// leaf load balancer (name, namespace and port after cross-namespace resolution).
// Populated by Kubernetes providers.
type KubernetesServiceMetadata struct {
Namespace string `json:"namespace,omitempty"`
Name string `json:"name,omitempty"`
Port string `json:"port,omitempty"`
}
// +k8s:deepcopy-gen=true
+68 -5
View File
@@ -989,17 +989,33 @@ func (in *InFlightReq) DeepCopy() *InFlightReq {
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *KubernetesIngressMetadata) DeepCopyInto(out *KubernetesIngressMetadata) {
func (in *KubernetesMetadata) DeepCopyInto(out *KubernetesMetadata) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesIngressMetadata.
func (in *KubernetesIngressMetadata) DeepCopy() *KubernetesIngressMetadata {
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesMetadata.
func (in *KubernetesMetadata) DeepCopy() *KubernetesMetadata {
if in == nil {
return nil
}
out := new(KubernetesIngressMetadata)
out := new(KubernetesMetadata)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *KubernetesServiceMetadata) DeepCopyInto(out *KubernetesServiceMetadata) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesServiceMetadata.
func (in *KubernetesServiceMetadata) DeepCopy() *KubernetesServiceMetadata {
if in == nil {
return nil
}
out := new(KubernetesServiceMetadata)
in.DeepCopyInto(out)
return out
}
@@ -1307,7 +1323,7 @@ func (in *ObservabilityMetadata) DeepCopyInto(out *ObservabilityMetadata) {
*out = *in
if in.Ingress != nil {
in, out := &in.Ingress, &out.Ingress
*out = new(KubernetesIngressMetadata)
*out = new(KubernetesMetadata)
**out = **in
}
return
@@ -1937,6 +1953,11 @@ func (in *Service) DeepCopyInto(out *Service) {
*out = new(Failover)
(*in).DeepCopyInto(*out)
}
if in.Observability != nil {
in, out := &in.Observability, &out.Observability
*out = new(ServiceObservabilityConfig)
(*in).DeepCopyInto(*out)
}
return
}
@@ -1950,6 +1971,48 @@ func (in *Service) DeepCopy() *Service {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ServiceObservabilityConfig) DeepCopyInto(out *ServiceObservabilityConfig) {
*out = *in
if in.Metadata != nil {
in, out := &in.Metadata, &out.Metadata
*out = new(ServiceObservabilityMetadata)
(*in).DeepCopyInto(*out)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceObservabilityConfig.
func (in *ServiceObservabilityConfig) DeepCopy() *ServiceObservabilityConfig {
if in == nil {
return nil
}
out := new(ServiceObservabilityConfig)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ServiceObservabilityMetadata) DeepCopyInto(out *ServiceObservabilityMetadata) {
*out = *in
if in.Kubernetes != nil {
in, out := &in.Kubernetes, &out.Kubernetes
*out = new(KubernetesServiceMetadata)
**out = **in
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceObservabilityMetadata.
func (in *ServiceObservabilityMetadata) DeepCopy() *ServiceObservabilityMetadata {
if in == nil {
return nil
}
out := new(ServiceObservabilityMetadata)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Snippet) DeepCopyInto(out *Snippet) {
*out = *in
+44
View File
@@ -542,6 +542,50 @@ func (c *Configuration) ValidateConfiguration() error {
return nil
}
const protocolTCP = "tcp"
// HasTCPEntryPoint reports whether at least one entryPoint carries TCP traffic,
// and therefore can serve HTTP requests.
func (c *Configuration) HasTCPEntryPoint() bool {
for _, ep := range c.EntryPoints {
protocol, err := ep.GetProtocol()
if err == nil && protocol == protocolTCP {
return true
}
}
return false
}
// HasDeniedEncodedCharacters reports whether an entryPoint serving HTTP requests
// disallows at least one encoded character in the request path.
// UDP entryPoints are left out because they never parse a request path.
func (c *Configuration) HasDeniedEncodedCharacters() bool {
for _, ep := range c.EntryPoints {
protocol, err := ep.GetProtocol()
if err != nil || protocol != protocolTCP {
continue
}
encodedCharacters := ep.HTTP.EncodedCharacters
if encodedCharacters == nil {
continue
}
if !encodedCharacters.AllowEncodedSlash ||
!encodedCharacters.AllowEncodedBackSlash ||
!encodedCharacters.AllowEncodedNullCharacter ||
!encodedCharacters.AllowEncodedSemicolon ||
!encodedCharacters.AllowEncodedPercent ||
!encodedCharacters.AllowEncodedQuestionMark ||
!encodedCharacters.AllowEncodedHash {
return true
}
}
return false
}
func (c *Configuration) hasUserDefinedEntrypoint() bool {
return len(c.EntryPoints) != 0
}
+151
View File
@@ -41,6 +41,157 @@ func TestHasEntrypoint(t *testing.T) {
}
}
func TestHasTCPEntryPoint(t *testing.T) {
tests := []struct {
desc string
entryPoints map[string]*EntryPoint
want bool
}{
{
desc: "empty configuration creates a default TCP entryPoint",
want: true,
},
{
desc: "entryPoint without an explicit protocol",
entryPoints: map[string]*EntryPoint{
"web": {Address: ":80"},
},
want: true,
},
{
desc: "UDP-only entryPoint",
entryPoints: map[string]*EntryPoint{
"dns": {Address: ":53/udp"},
},
want: false,
},
{
desc: "UDP and TCP entryPoints",
entryPoints: map[string]*EntryPoint{
"dns": {Address: ":53/udp"},
"web": {Address: ":80/tcp"},
},
want: true,
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
cfg := &Configuration{
EntryPoints: test.entryPoints,
Providers: &Providers{},
}
cfg.SetEffectiveConfiguration()
assert.Equal(t, test.want, cfg.HasTCPEntryPoint())
})
}
}
func TestHasDeniedEncodedCharacters(t *testing.T) {
allowAll := &EncodedCharacters{}
allowAll.SetDefaults()
denySlash := &EncodedCharacters{}
denySlash.SetDefaults()
denySlash.AllowEncodedSlash = false
denyHash := &EncodedCharacters{}
denyHash.SetDefaults()
denyHash.AllowEncodedHash = false
tests := []struct {
desc string
api *API
entryPoints map[string]*EntryPoint
want bool
}{
{
desc: "empty configuration creates a default TCP entryPoint without encoded characters configuration",
want: false,
},
{
desc: "TCP entryPoint allowing all encoded characters",
entryPoints: map[string]*EntryPoint{
"web": {
Address: ":80/tcp",
HTTP: HTTPConfig{EncodedCharacters: allowAll},
},
},
want: false,
},
{
desc: "TCP entryPoint disallowing an encoded character",
entryPoints: map[string]*EntryPoint{
"web": {
Address: ":80/tcp",
HTTP: HTTPConfig{EncodedCharacters: denySlash},
},
},
want: true,
},
{
desc: "one TCP entryPoint among others disallowing an encoded character",
entryPoints: map[string]*EntryPoint{
"web": {
Address: ":80/tcp",
HTTP: HTTPConfig{EncodedCharacters: allowAll},
},
"websecure": {
Address: ":443/tcp",
HTTP: HTTPConfig{EncodedCharacters: denyHash},
},
},
want: true,
},
{
desc: "UDP entryPoint disallowing an encoded character",
entryPoints: map[string]*EntryPoint{
"dns": {
Address: ":53/udp",
HTTP: HTTPConfig{EncodedCharacters: denySlash},
},
"web": {
Address: ":80/tcp",
HTTP: HTTPConfig{EncodedCharacters: allowAll},
},
},
want: false,
},
{
desc: "insecure API adds an internal TCP entryPoint without encoded characters configuration",
api: &API{Insecure: true},
entryPoints: map[string]*EntryPoint{
"web": {
Address: ":80/tcp",
HTTP: HTTPConfig{EncodedCharacters: denySlash},
},
},
want: true,
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
cfg := &Configuration{
API: test.api,
EntryPoints: test.entryPoints,
Providers: &Providers{},
}
cfg.SetEffectiveConfiguration()
if test.api != nil && test.api.Insecure {
assert.Contains(t, cfg.EntryPoints, DefaultInternalEntryPointName)
}
assert.Equal(t, test.want, cfg.HasDeniedEncodedCharacters())
})
}
}
func TestConfiguration_SetEffectiveConfiguration(t *testing.T) {
testCases := []struct {
desc string
+18 -2
View File
@@ -94,10 +94,26 @@ const (
KubernetesIngressNamespace = "KubernetesIngressNamespace"
// KubernetesIngressName is the name of the Kubernetes Ingress resource the router handles.
KubernetesIngressName = "KubernetesIngressName"
// KubernetesServiceName is the name of the Kubernetes service associated with Ingress the router handles.
// Kubernetes Service fields.
// KubernetesServiceNamespace is the namespace of the Kubernetes Service that handled the request.
// Stamped per request from the leaf load balancer, so it reflects the actual backend chosen
// by the load balancing strategy under multi-service routes or TraefikService fan-out.
KubernetesServiceNamespace = "KubernetesServiceNamespace"
// KubernetesServiceName is the name of the Kubernetes Service that handled the request.
// See KubernetesServiceNamespace for the per-request semantics.
KubernetesServiceName = "KubernetesServiceName"
// KubernetesServicePort is the port of the Kubernetes service associated with Ingress the router handles.
// KubernetesServicePort is the port of the Kubernetes Service that handled the request.
// See KubernetesServiceNamespace for the per-request semantics.
KubernetesServicePort = "KubernetesServicePort"
// Kubernetes IngressRoute fields.
// KubernetesIngressRouteNamespace is the namespace of the Kubernetes IngressRoute CRD the router handles.
KubernetesIngressRouteNamespace = "KubernetesIngressRouteNamespace"
// KubernetesIngressRouteName is the name of the Kubernetes IngressRoute CRD the router handles.
KubernetesIngressRouteName = "KubernetesIngressRouteName"
)
// These are written out in the default case when no config is provided to specify keys of interest.
+24 -5
View File
@@ -209,11 +209,18 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request, next http
}
if metadata := observability.GetObservabilityMetadata(req.Context()); metadata != nil {
if metadata.Ingress != nil {
logDataTable.Core[KubernetesIngressNamespace] = metadata.Ingress.Namespace
logDataTable.Core[KubernetesIngressName] = metadata.Ingress.IngressName
logDataTable.Core[KubernetesServiceName] = metadata.Ingress.ServiceName
logDataTable.Core[KubernetesServicePort] = metadata.Ingress.ServicePort
// Dispatch on the source resource kind so each kind maps to its own
// stable access-log field names. A single generic struct backs every
// Kubernetes provider; only the kind differs.
if k := metadata.Ingress; k != nil {
switch k.Kind {
case "Ingress":
logDataTable.Core[KubernetesIngressNamespace] = k.Namespace
logDataTable.Core[KubernetesIngressName] = k.Name
case "IngressRoute":
logDataTable.Core[KubernetesIngressRouteNamespace] = k.Namespace
logDataTable.Core[KubernetesIngressRouteName] = k.Name
}
}
}
@@ -286,6 +293,18 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request, next http
logDataTable.DownstreamResponse.size = capt.ResponseSize()
logDataTable.Request.size = capt.RequestSize()
// Service-level observability metadata is written by the leaf service
// handler while the chain runs (the state container is shared by
// pointer via the request context). Read it once here so the fields
// reflect the backend the load balancer actually dispatched to.
if state := observability.GetServiceObservabilityState(reqWithDataTable.Context()); state != nil && state.Metadata != nil {
if k := state.Metadata.Kubernetes; k != nil {
core[KubernetesServiceName] = k.Name
core[KubernetesServiceNamespace] = k.Namespace
core[KubernetesServicePort] = k.Port
}
}
if _, ok := core[ClientUsername]; !ok {
core[ClientUsername] = usernameIfPresent(reqWithDataTable.URL)
}
+25 -5
View File
@@ -708,6 +708,7 @@ func TestLoggerJSON(t *testing.T) {
"StartUTC": assertNotEmpty(),
KubernetesIngressNamespace: assertString("test-namespace"),
KubernetesIngressName: assertString("test-ingress"),
KubernetesServiceNamespace: assertString("test-namespace"),
KubernetesServiceName: assertString("test-service"),
KubernetesServicePort: assertString("test-port"),
},
@@ -1381,11 +1382,10 @@ func doLoggingTLSOpt(t *testing.T, config *otypes.AccessLog, enableTLS, tracing,
if metadata {
obs.Metadata = &dynamic.ObservabilityMetadata{
Ingress: &dynamic.KubernetesIngressMetadata{
Namespace: "test-namespace",
IngressName: "test-ingress",
ServiceName: "test-service",
ServicePort: "test-port",
Ingress: &dynamic.KubernetesMetadata{
Kind: "Ingress",
Namespace: "test-namespace",
Name: "test-ingress",
},
}
}
@@ -1393,6 +1393,26 @@ func doLoggingTLSOpt(t *testing.T, config *otypes.AccessLog, enableTLS, tracing,
return observability.WithObservabilityHandler(next, obs), nil
})
if metadata {
// Seed the per-request service observability state and publish the
// leaf service metadata, mirroring what pkg/server/service does at
// runtime when a Kubernetes-backed service handles the request.
chain = chain.Append(func(next http.Handler) (http.Handler, error) {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
ctx := observability.WithServiceObservabilityState(r.Context())
state := observability.GetServiceObservabilityState(ctx)
state.Metadata = &dynamic.ServiceObservabilityMetadata{
Kubernetes: &dynamic.KubernetesServiceMetadata{
Namespace: "test-namespace",
Name: "test-service",
Port: "test-port",
},
}
next.ServeHTTP(rw, r.WithContext(ctx))
}), nil
})
}
chain = chain.Append(logger.AliceConstructor())
handler, err := chain.Then(http.HandlerFunc(logWriterTestHandlerFunc))
require.NoError(t, err)
@@ -57,10 +57,11 @@ func New(ctx context.Context, next http.Handler, config dynamic.RewriteTarget, n
return nil, fmt.Errorf("compiling regular expression %s: %w", config.Regex, err)
}
// When rewrite-target is a full URL and there's no capture group,
// append .* to match the entire path and avoid leaking unmatched suffix.
// When rewrite-target is a full URL, append .* to consume the entire path
// and avoid leaking an unmatched suffix into the redirect URL.
// Capture groups are greedy, so they still capture their content before the trailing .*.
// See https://github.com/traefik/traefik/issues/12931
if re.NumSubexp() == 0 && absoluteURLRedirect {
if absoluteURLRedirect {
re, err = regexp.Compile("(?i)" + strings.TrimSpace(config.Regex) + ".*")
if err != nil {
return nil, fmt.Errorf("compiling regular expression for absolute URL %s: %w", config.Regex, err)
@@ -184,6 +184,65 @@ func TestRewriteTarget(t *testing.T) {
expectedStatusCode: http.StatusFound,
expectedRedirectURL: "https://bar.example.org/",
},
{
desc: "regex with optional capture group and full URL replacement does not leak suffix",
path: "/originalfoo",
config: dynamic.RewriteTarget{
Regex: `/original(?:/(.*))?`,
Replacement: "https://bar.example.org/$1",
},
expectedStatusCode: http.StatusFound,
expectedRedirectURL: "https://bar.example.org/",
},
{
desc: "regex with optional capture group and full URL replacement keeps capture",
path: "/original/foo",
config: dynamic.RewriteTarget{
Regex: `/original(?:/(.*))?`,
Replacement: "https://bar.example.org/$1",
},
expectedStatusCode: http.StatusFound,
expectedRedirectURL: "https://bar.example.org/foo",
},
{
desc: "regex with optional capture group and full URL replacement matches bare path",
path: "/original",
config: dynamic.RewriteTarget{
Regex: `/original(?:/(.*))?`,
Replacement: "https://bar.example.org/$1",
},
expectedStatusCode: http.StatusFound,
expectedRedirectURL: "https://bar.example.org/",
},
{
desc: "trailing group regex with path replacement keeps capture",
path: "/original/foo",
config: dynamic.RewriteTarget{
Regex: `/original/(.*)`,
Replacement: "/newpath/$1",
},
expectedPath: "/newpath/foo",
},
{
desc: "trailing group regex with path replacement does not match bare path",
path: "/original",
config: dynamic.RewriteTarget{
Regex: `/original/(.*)`,
Replacement: "/newpath/$1",
},
expectedPath: "/original",
expectedRawPath: "",
},
{
desc: "trailing group regex with path replacement does not match prefix without slash",
path: "/originalfoo",
config: dynamic.RewriteTarget{
Regex: `/original/(.*)`,
Replacement: "/newpath/$1",
},
expectedPath: "/originalfoo",
expectedRawPath: "",
},
{
desc: "full URL replacement preserves incoming query",
path: "/some/path?foo=bar&baz=qux",
@@ -75,6 +75,45 @@ func GetObservabilityMetadata(ctx context.Context) *dynamic.ObservabilityMetadat
return nil
}
type serviceObservabilityKey struct{}
// ServiceObservabilityState carries the service-level observability metadata
// produced by the service handler during request processing. The pointer is
// seeded once upstream (at the entrypoint chain) and mutated by the leaf
// service handler when the load balancer dispatches; access-log middleware
// reads the final state at log emission. Mirrors how *LogData is shared.
type ServiceObservabilityState struct {
Metadata *dynamic.ServiceObservabilityMetadata
}
// WithServiceObservabilityState seeds an empty state container in the context
// so downstream handlers and the access-log middleware share the same pointer.
func WithServiceObservabilityState(ctx context.Context) context.Context {
return context.WithValue(ctx, serviceObservabilityKey{}, &ServiceObservabilityState{})
}
// GetServiceObservabilityState returns the per-request state container, or nil
// if none has been seeded upstream.
func GetServiceObservabilityState(ctx context.Context) *ServiceObservabilityState {
s, _ := ctx.Value(serviceObservabilityKey{}).(*ServiceObservabilityState)
return s
}
// NewServiceMetadataHandler wraps the service handler so that when the leaf is
// reached at request time, its metadata is published into the per-request state
// container. A no-op when the service has no observability metadata attached.
func NewServiceMetadataHandler(cfg *dynamic.ServiceObservabilityConfig, next http.Handler) http.Handler {
if cfg == nil || cfg.Metadata == nil {
return next
}
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
if state := GetServiceObservabilityState(req.Context()); state != nil {
state.Metadata = cfg.Metadata
}
next.ServeHTTP(rw, req)
})
}
// SetStatusErrorf flags the span as in error and log an event.
func SetStatusErrorf(ctx context.Context, format string, args ...any) {
if span := trace.SpanFromContext(ctx); span != nil {
+2
View File
@@ -12,6 +12,8 @@ func CheckFile(name string) (bool, error) {
if err != nil {
return false, err
}
defer f.Close()
return false, f.Chmod(0o600)
}
return false, err
@@ -43,10 +43,6 @@ import (
// It's backed by a very simple object tracker that processes creates, updates and deletions as-is,
// without applying any field management, validations and/or defaults. It shouldn't be considered a replacement
// for a real clientset and is mostly useful in simple unit tests.
//
// Deprecated: NewClientset replaces this with support for field management, which significantly improves
// server side apply testing. NewClientset is only available when apply configurations are generated (e.g.
// via --with-applyconfig).
func NewSimpleClientset(objects ...runtime.Object) *Clientset {
o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder())
for _, obj := range objects {
@@ -92,7 +88,7 @@ func (c *Clientset) Tracker() testing.ObjectTracker {
return c.tracker
}
// IsWatchListSemanticsSupported informs the reflector that this client
// IsWatchListSemanticsUnSupported informs the reflector that this client
// doesn't support WatchList semantics.
//
// This is a synthetic method whose sole purpose is to satisfy the optional
@@ -107,6 +103,10 @@ func (c *Clientset) IsWatchListSemanticsUnSupported() bool {
// It's backed by a very simple object tracker that processes creates, updates and deletions as-is,
// without applying any validations and/or defaults. It shouldn't be considered a replacement
// for a real clientset and is mostly useful in simple unit tests.
//
// Compared to NewSimpleClientset, the Clientset returned here supports field tracking and thus
// server-side apply. Beware though that support in that for CRDs is missing
// (https://github.com/kubernetes/kubernetes/issues/126850).
func NewClientset(objects ...runtime.Object) *Clientset {
o := testing.NewFieldManagedObjectTracker(
scheme,
@@ -27,6 +27,7 @@ THE SOFTWARE.
package externalversions
import (
context "context"
reflect "reflect"
sync "sync"
time "time"
@@ -37,6 +38,7 @@ import (
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
schema "k8s.io/apimachinery/pkg/runtime/schema"
wait "k8s.io/apimachinery/pkg/util/wait"
cache "k8s.io/client-go/tools/cache"
)
@@ -51,6 +53,7 @@ type sharedInformerFactory struct {
defaultResync time.Duration
customResync map[reflect.Type]time.Duration
transform cache.TransformFunc
informerName *cache.InformerName
informers map[reflect.Type]cache.SharedIndexInformer
// startedInformers is used for tracking which informers have been started.
@@ -97,6 +100,21 @@ func WithTransform(transform cache.TransformFunc) SharedInformerOption {
}
}
// WithInformerName sets the InformerName for informer identity used in metrics.
// The InformerName must be created via cache.NewInformerName() at startup,
// which validates global uniqueness. Each informer type will register its
// GVR under this name.
func WithInformerName(informerName *cache.InformerName) SharedInformerOption {
return func(factory *sharedInformerFactory) *sharedInformerFactory {
factory.informerName = informerName
return factory
}
}
func (f *sharedInformerFactory) InformerName() *cache.InformerName {
return f.informerName
}
// NewSharedInformerFactory constructs a new instance of sharedInformerFactory for all namespaces.
func NewSharedInformerFactory(client versioned.Interface, defaultResync time.Duration) SharedInformerFactory {
return NewSharedInformerFactoryWithOptions(client, defaultResync)
@@ -131,6 +149,10 @@ func NewSharedInformerFactoryWithOptions(client versioned.Interface, defaultResy
}
func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) {
f.StartWithContext(wait.ContextForChannel(stopCh))
}
func (f *sharedInformerFactory) StartWithContext(ctx context.Context) {
f.lock.Lock()
defer f.lock.Unlock()
@@ -140,15 +162,9 @@ func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) {
for informerType, informer := range f.informers {
if !f.startedInformers[informerType] {
f.wg.Add(1)
// We need a new variable in each loop iteration,
// otherwise the goroutine would use the loop variable
// and that keeps changing.
informer := informer
go func() {
defer f.wg.Done()
informer.Run(stopCh)
}()
f.wg.Go(func() {
informer.RunWithContext(ctx)
})
f.startedInformers[informerType] = true
}
}
@@ -161,9 +177,15 @@ func (f *sharedInformerFactory) Shutdown() {
// Will return immediately if there is nothing to wait for.
f.wg.Wait()
f.informerName.Release()
}
func (f *sharedInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool {
result := f.WaitForCacheSyncWithContext(wait.ContextForChannel(stopCh))
return result.Synced
}
func (f *sharedInformerFactory) WaitForCacheSyncWithContext(ctx context.Context) cache.SyncResult {
informers := func() map[reflect.Type]cache.SharedIndexInformer {
f.lock.Lock()
defer f.lock.Unlock()
@@ -177,10 +199,31 @@ func (f *sharedInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[ref
return informers
}()
res := map[reflect.Type]bool{}
for informType, informer := range informers {
res[informType] = cache.WaitForCacheSync(stopCh, informer.HasSynced)
// Wait for informers to sync, without polling.
cacheSyncs := make([]cache.DoneChecker, 0, len(informers))
for _, informer := range informers {
cacheSyncs = append(cacheSyncs, informer.HasSyncedChecker())
}
cache.WaitFor(ctx, "" /* no logging */, cacheSyncs...)
res := cache.SyncResult{
Synced: make(map[reflect.Type]bool, len(informers)),
}
failed := false
for informType, informer := range informers {
hasSynced := informer.HasSynced()
if !hasSynced {
failed = true
}
res.Synced[informType] = hasSynced
}
if failed {
// context.Cause is more informative than ctx.Err().
// This must be non-nil, otherwise WaitFor wouldn't have stopped
// prematurely.
res.Err = context.Cause(ctx)
}
return res
}
@@ -202,7 +245,9 @@ func (f *sharedInformerFactory) InformerFor(obj runtime.Object, newFunc internal
}
informer = newFunc(f.client, resyncPeriod)
informer.SetTransform(f.transform)
if f.transform != nil {
informer.SetTransform(f.transform)
}
f.informers[informerType] = informer
return informer
@@ -219,27 +264,46 @@ func (f *sharedInformerFactory) InformerFor(obj runtime.Object, newFunc internal
// defer factory.WaitForStop() // Returns immediately if nothing was started.
// genericInformer := factory.ForResource(resource)
// typedInformer := factory.SomeAPIGroup().V1().SomeType()
// factory.Start(ctx.Done()) // Start processing these informers.
// synced := factory.WaitForCacheSync(ctx.Done())
// for v, ok := range synced {
// if !ok {
// fmt.Fprintf(os.Stderr, "caches failed to sync: %v", v)
// return
// }
// handle, err := typeInformer.Informer().AddEventHandler(...)
// if err != nil {
// return fmt.Errorf("register event handler: %v", err)
// }
// defer typeInformer.Informer().RemoveEventHandler(handle) // Avoids leaking goroutines.
// factory.StartWithContext(ctx) // Start processing these informers.
// synced := factory.WaitForCacheSyncWithContext(ctx)
// if err := synced.AsError(); err != nil {
// return err
// }
// for v := range synced {
// // Only if desired log some information similar to this.
// fmt.Fprintf(os.Stdout, "cache synced: %s", v)
// }
//
// // Also make sure that all of the initial cache events have been delivered.
// if !WaitFor(ctx, "event handler sync", handle.HasSyncedChecker()) {
// // Must have failed because of context.
// return fmt.Errorf("sync event handler: %w", context.Cause(ctx))
// }
//
// // Creating informers can also be created after Start, but then
// // Start must be called again:
// anotherGenericInformer := factory.ForResource(resource)
// factory.Start(ctx.Done())
// factory.StartWithContext(ctx)
type SharedInformerFactory interface {
internalinterfaces.SharedInformerFactory
// Start initializes all requested informers. They are handled in goroutines
// which run until the stop channel gets closed.
// Warning: Start does not block. When run in a go-routine, it will race with a later WaitForCacheSync.
//
// Contextual logging: StartWithContext should be used instead of Start in code which supports contextual logging.
Start(stopCh <-chan struct{})
// StartWithContext initializes all requested informers. They are handled in goroutines
// which run until the context gets canceled.
// Warning: StartWithContext does not block. When run in a go-routine, it will race with a later WaitForCacheSync.
StartWithContext(ctx context.Context)
// Shutdown marks a factory as shutting down. At that point no new
// informers can be started anymore and Start will return without
// doing anything.
@@ -254,8 +318,14 @@ type SharedInformerFactory interface {
// WaitForCacheSync blocks until all started informers' caches were synced
// or the stop channel gets closed.
//
// Contextual logging: WaitForCacheSync should be used instead of WaitForCacheSync in code which supports contextual logging. It also returns a more useful result.
WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool
// WaitForCacheSyncWithContext blocks until all started informers' caches were synced
// or the context gets canceled.
WaitForCacheSyncWithContext(ctx context.Context) cache.SyncResult
// ForResource gives generic access to a shared informer of the matching type.
ForResource(resource schema.GroupVersionResource) (GenericInformer, error)
@@ -42,7 +42,26 @@ type NewInformerFunc func(versioned.Interface, time.Duration) cache.SharedIndexI
type SharedInformerFactory interface {
Start(stopCh <-chan struct{})
InformerFor(obj runtime.Object, newFunc NewInformerFunc) cache.SharedIndexInformer
InformerName() *cache.InformerName
}
// TweakListOptionsFunc is a function that transforms a v1.ListOptions.
type TweakListOptionsFunc func(*v1.ListOptions)
// InformerOptions holds the options for creating an informer.
type InformerOptions struct {
// ResyncPeriod is the resync period for this informer.
// If not set, defaults to 0 (no resync).
ResyncPeriod time.Duration
// Indexers are the indexers for this informer.
Indexers cache.Indexers
// InformerName is used to uniquely identify this informer for metrics.
// If not set, metrics will not be published for this informer.
// Use cache.NewInformerName() to create an InformerName at startup.
InformerName *cache.InformerName
// TweakListOptions is an optional function to modify the list options.
TweakListOptions TweakListOptionsFunc
}
@@ -36,6 +36,7 @@ import (
crdtraefikiov1alpha1 "github.com/traefik/traefik/v3/pkg/provider/kubernetes/crd/traefikio/v1alpha1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
schema "k8s.io/apimachinery/pkg/runtime/schema"
watch "k8s.io/apimachinery/pkg/watch"
cache "k8s.io/client-go/tools/cache"
)
@@ -57,48 +58,61 @@ type ingressRouteInformer struct {
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewIngressRouteInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
return NewFilteredIngressRouteInformer(client, namespace, resyncPeriod, indexers, nil)
return NewIngressRouteInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: indexers})
}
// NewFilteredIngressRouteInformer constructs a new informer for IngressRoute type.
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewFilteredIngressRouteInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
return cache.NewSharedIndexInformer(
return NewIngressRouteInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: indexers, TweakListOptions: tweakListOptions})
}
// NewIngressRouteInformerWithOptions constructs a new informer for IngressRoute type with additional options.
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewIngressRouteInformerWithOptions(client versioned.Interface, namespace string, options internalinterfaces.InformerOptions) cache.SharedIndexInformer {
gvr := schema.GroupVersionResource{Group: "traefik.io", Version: "v1alpha1", Resource: "ingressroutes"}
identifier := options.InformerName.WithResource(gvr)
tweakListOptions := options.TweakListOptions
return cache.NewSharedIndexInformerWithOptions(
cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{
ListFunc: func(options v1.ListOptions) (runtime.Object, error) {
ListFunc: func(opts v1.ListOptions) (runtime.Object, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
tweakListOptions(&opts)
}
return client.TraefikV1alpha1().IngressRoutes(namespace).List(context.Background(), options)
return client.TraefikV1alpha1().IngressRoutes(namespace).List(context.Background(), opts)
},
WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
WatchFunc: func(opts v1.ListOptions) (watch.Interface, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
tweakListOptions(&opts)
}
return client.TraefikV1alpha1().IngressRoutes(namespace).Watch(context.Background(), options)
return client.TraefikV1alpha1().IngressRoutes(namespace).Watch(context.Background(), opts)
},
ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) {
ListWithContextFunc: func(ctx context.Context, opts v1.ListOptions) (runtime.Object, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
tweakListOptions(&opts)
}
return client.TraefikV1alpha1().IngressRoutes(namespace).List(ctx, options)
return client.TraefikV1alpha1().IngressRoutes(namespace).List(ctx, opts)
},
WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) {
WatchFuncWithContext: func(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
tweakListOptions(&opts)
}
return client.TraefikV1alpha1().IngressRoutes(namespace).Watch(ctx, options)
return client.TraefikV1alpha1().IngressRoutes(namespace).Watch(ctx, opts)
},
}, client),
&crdtraefikiov1alpha1.IngressRoute{},
resyncPeriod,
indexers,
cache.SharedIndexInformerOptions{
ResyncPeriod: options.ResyncPeriod,
Indexers: options.Indexers,
Identifier: identifier,
},
)
}
func (f *ingressRouteInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
return NewFilteredIngressRouteInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
return NewIngressRouteInformerWithOptions(client, f.namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, InformerName: f.factory.InformerName(), TweakListOptions: f.tweakListOptions})
}
func (f *ingressRouteInformer) Informer() cache.SharedIndexInformer {
@@ -36,6 +36,7 @@ import (
crdtraefikiov1alpha1 "github.com/traefik/traefik/v3/pkg/provider/kubernetes/crd/traefikio/v1alpha1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
schema "k8s.io/apimachinery/pkg/runtime/schema"
watch "k8s.io/apimachinery/pkg/watch"
cache "k8s.io/client-go/tools/cache"
)
@@ -57,48 +58,61 @@ type ingressRouteTCPInformer struct {
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewIngressRouteTCPInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
return NewFilteredIngressRouteTCPInformer(client, namespace, resyncPeriod, indexers, nil)
return NewIngressRouteTCPInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: indexers})
}
// NewFilteredIngressRouteTCPInformer constructs a new informer for IngressRouteTCP type.
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewFilteredIngressRouteTCPInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
return cache.NewSharedIndexInformer(
return NewIngressRouteTCPInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: indexers, TweakListOptions: tweakListOptions})
}
// NewIngressRouteTCPInformerWithOptions constructs a new informer for IngressRouteTCP type with additional options.
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewIngressRouteTCPInformerWithOptions(client versioned.Interface, namespace string, options internalinterfaces.InformerOptions) cache.SharedIndexInformer {
gvr := schema.GroupVersionResource{Group: "traefik.io", Version: "v1alpha1", Resource: "ingressroutetcps"}
identifier := options.InformerName.WithResource(gvr)
tweakListOptions := options.TweakListOptions
return cache.NewSharedIndexInformerWithOptions(
cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{
ListFunc: func(options v1.ListOptions) (runtime.Object, error) {
ListFunc: func(opts v1.ListOptions) (runtime.Object, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
tweakListOptions(&opts)
}
return client.TraefikV1alpha1().IngressRouteTCPs(namespace).List(context.Background(), options)
return client.TraefikV1alpha1().IngressRouteTCPs(namespace).List(context.Background(), opts)
},
WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
WatchFunc: func(opts v1.ListOptions) (watch.Interface, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
tweakListOptions(&opts)
}
return client.TraefikV1alpha1().IngressRouteTCPs(namespace).Watch(context.Background(), options)
return client.TraefikV1alpha1().IngressRouteTCPs(namespace).Watch(context.Background(), opts)
},
ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) {
ListWithContextFunc: func(ctx context.Context, opts v1.ListOptions) (runtime.Object, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
tweakListOptions(&opts)
}
return client.TraefikV1alpha1().IngressRouteTCPs(namespace).List(ctx, options)
return client.TraefikV1alpha1().IngressRouteTCPs(namespace).List(ctx, opts)
},
WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) {
WatchFuncWithContext: func(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
tweakListOptions(&opts)
}
return client.TraefikV1alpha1().IngressRouteTCPs(namespace).Watch(ctx, options)
return client.TraefikV1alpha1().IngressRouteTCPs(namespace).Watch(ctx, opts)
},
}, client),
&crdtraefikiov1alpha1.IngressRouteTCP{},
resyncPeriod,
indexers,
cache.SharedIndexInformerOptions{
ResyncPeriod: options.ResyncPeriod,
Indexers: options.Indexers,
Identifier: identifier,
},
)
}
func (f *ingressRouteTCPInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
return NewFilteredIngressRouteTCPInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
return NewIngressRouteTCPInformerWithOptions(client, f.namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, InformerName: f.factory.InformerName(), TweakListOptions: f.tweakListOptions})
}
func (f *ingressRouteTCPInformer) Informer() cache.SharedIndexInformer {
@@ -36,6 +36,7 @@ import (
crdtraefikiov1alpha1 "github.com/traefik/traefik/v3/pkg/provider/kubernetes/crd/traefikio/v1alpha1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
schema "k8s.io/apimachinery/pkg/runtime/schema"
watch "k8s.io/apimachinery/pkg/watch"
cache "k8s.io/client-go/tools/cache"
)
@@ -57,48 +58,61 @@ type ingressRouteUDPInformer struct {
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewIngressRouteUDPInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
return NewFilteredIngressRouteUDPInformer(client, namespace, resyncPeriod, indexers, nil)
return NewIngressRouteUDPInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: indexers})
}
// NewFilteredIngressRouteUDPInformer constructs a new informer for IngressRouteUDP type.
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewFilteredIngressRouteUDPInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
return cache.NewSharedIndexInformer(
return NewIngressRouteUDPInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: indexers, TweakListOptions: tweakListOptions})
}
// NewIngressRouteUDPInformerWithOptions constructs a new informer for IngressRouteUDP type with additional options.
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewIngressRouteUDPInformerWithOptions(client versioned.Interface, namespace string, options internalinterfaces.InformerOptions) cache.SharedIndexInformer {
gvr := schema.GroupVersionResource{Group: "traefik.io", Version: "v1alpha1", Resource: "ingressrouteudps"}
identifier := options.InformerName.WithResource(gvr)
tweakListOptions := options.TweakListOptions
return cache.NewSharedIndexInformerWithOptions(
cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{
ListFunc: func(options v1.ListOptions) (runtime.Object, error) {
ListFunc: func(opts v1.ListOptions) (runtime.Object, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
tweakListOptions(&opts)
}
return client.TraefikV1alpha1().IngressRouteUDPs(namespace).List(context.Background(), options)
return client.TraefikV1alpha1().IngressRouteUDPs(namespace).List(context.Background(), opts)
},
WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
WatchFunc: func(opts v1.ListOptions) (watch.Interface, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
tweakListOptions(&opts)
}
return client.TraefikV1alpha1().IngressRouteUDPs(namespace).Watch(context.Background(), options)
return client.TraefikV1alpha1().IngressRouteUDPs(namespace).Watch(context.Background(), opts)
},
ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) {
ListWithContextFunc: func(ctx context.Context, opts v1.ListOptions) (runtime.Object, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
tweakListOptions(&opts)
}
return client.TraefikV1alpha1().IngressRouteUDPs(namespace).List(ctx, options)
return client.TraefikV1alpha1().IngressRouteUDPs(namespace).List(ctx, opts)
},
WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) {
WatchFuncWithContext: func(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
tweakListOptions(&opts)
}
return client.TraefikV1alpha1().IngressRouteUDPs(namespace).Watch(ctx, options)
return client.TraefikV1alpha1().IngressRouteUDPs(namespace).Watch(ctx, opts)
},
}, client),
&crdtraefikiov1alpha1.IngressRouteUDP{},
resyncPeriod,
indexers,
cache.SharedIndexInformerOptions{
ResyncPeriod: options.ResyncPeriod,
Indexers: options.Indexers,
Identifier: identifier,
},
)
}
func (f *ingressRouteUDPInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
return NewFilteredIngressRouteUDPInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
return NewIngressRouteUDPInformerWithOptions(client, f.namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, InformerName: f.factory.InformerName(), TweakListOptions: f.tweakListOptions})
}
func (f *ingressRouteUDPInformer) Informer() cache.SharedIndexInformer {
@@ -36,6 +36,7 @@ import (
crdtraefikiov1alpha1 "github.com/traefik/traefik/v3/pkg/provider/kubernetes/crd/traefikio/v1alpha1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
schema "k8s.io/apimachinery/pkg/runtime/schema"
watch "k8s.io/apimachinery/pkg/watch"
cache "k8s.io/client-go/tools/cache"
)
@@ -57,48 +58,61 @@ type middlewareInformer struct {
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewMiddlewareInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
return NewFilteredMiddlewareInformer(client, namespace, resyncPeriod, indexers, nil)
return NewMiddlewareInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: indexers})
}
// NewFilteredMiddlewareInformer constructs a new informer for Middleware type.
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewFilteredMiddlewareInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
return cache.NewSharedIndexInformer(
return NewMiddlewareInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: indexers, TweakListOptions: tweakListOptions})
}
// NewMiddlewareInformerWithOptions constructs a new informer for Middleware type with additional options.
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewMiddlewareInformerWithOptions(client versioned.Interface, namespace string, options internalinterfaces.InformerOptions) cache.SharedIndexInformer {
gvr := schema.GroupVersionResource{Group: "traefik.io", Version: "v1alpha1", Resource: "middlewares"}
identifier := options.InformerName.WithResource(gvr)
tweakListOptions := options.TweakListOptions
return cache.NewSharedIndexInformerWithOptions(
cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{
ListFunc: func(options v1.ListOptions) (runtime.Object, error) {
ListFunc: func(opts v1.ListOptions) (runtime.Object, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
tweakListOptions(&opts)
}
return client.TraefikV1alpha1().Middlewares(namespace).List(context.Background(), options)
return client.TraefikV1alpha1().Middlewares(namespace).List(context.Background(), opts)
},
WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
WatchFunc: func(opts v1.ListOptions) (watch.Interface, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
tweakListOptions(&opts)
}
return client.TraefikV1alpha1().Middlewares(namespace).Watch(context.Background(), options)
return client.TraefikV1alpha1().Middlewares(namespace).Watch(context.Background(), opts)
},
ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) {
ListWithContextFunc: func(ctx context.Context, opts v1.ListOptions) (runtime.Object, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
tweakListOptions(&opts)
}
return client.TraefikV1alpha1().Middlewares(namespace).List(ctx, options)
return client.TraefikV1alpha1().Middlewares(namespace).List(ctx, opts)
},
WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) {
WatchFuncWithContext: func(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
tweakListOptions(&opts)
}
return client.TraefikV1alpha1().Middlewares(namespace).Watch(ctx, options)
return client.TraefikV1alpha1().Middlewares(namespace).Watch(ctx, opts)
},
}, client),
&crdtraefikiov1alpha1.Middleware{},
resyncPeriod,
indexers,
cache.SharedIndexInformerOptions{
ResyncPeriod: options.ResyncPeriod,
Indexers: options.Indexers,
Identifier: identifier,
},
)
}
func (f *middlewareInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
return NewFilteredMiddlewareInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
return NewMiddlewareInformerWithOptions(client, f.namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, InformerName: f.factory.InformerName(), TweakListOptions: f.tweakListOptions})
}
func (f *middlewareInformer) Informer() cache.SharedIndexInformer {
@@ -36,6 +36,7 @@ import (
crdtraefikiov1alpha1 "github.com/traefik/traefik/v3/pkg/provider/kubernetes/crd/traefikio/v1alpha1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
schema "k8s.io/apimachinery/pkg/runtime/schema"
watch "k8s.io/apimachinery/pkg/watch"
cache "k8s.io/client-go/tools/cache"
)
@@ -57,48 +58,61 @@ type middlewareTCPInformer struct {
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewMiddlewareTCPInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
return NewFilteredMiddlewareTCPInformer(client, namespace, resyncPeriod, indexers, nil)
return NewMiddlewareTCPInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: indexers})
}
// NewFilteredMiddlewareTCPInformer constructs a new informer for MiddlewareTCP type.
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewFilteredMiddlewareTCPInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
return cache.NewSharedIndexInformer(
return NewMiddlewareTCPInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: indexers, TweakListOptions: tweakListOptions})
}
// NewMiddlewareTCPInformerWithOptions constructs a new informer for MiddlewareTCP type with additional options.
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewMiddlewareTCPInformerWithOptions(client versioned.Interface, namespace string, options internalinterfaces.InformerOptions) cache.SharedIndexInformer {
gvr := schema.GroupVersionResource{Group: "traefik.io", Version: "v1alpha1", Resource: "middlewaretcps"}
identifier := options.InformerName.WithResource(gvr)
tweakListOptions := options.TweakListOptions
return cache.NewSharedIndexInformerWithOptions(
cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{
ListFunc: func(options v1.ListOptions) (runtime.Object, error) {
ListFunc: func(opts v1.ListOptions) (runtime.Object, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
tweakListOptions(&opts)
}
return client.TraefikV1alpha1().MiddlewareTCPs(namespace).List(context.Background(), options)
return client.TraefikV1alpha1().MiddlewareTCPs(namespace).List(context.Background(), opts)
},
WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
WatchFunc: func(opts v1.ListOptions) (watch.Interface, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
tweakListOptions(&opts)
}
return client.TraefikV1alpha1().MiddlewareTCPs(namespace).Watch(context.Background(), options)
return client.TraefikV1alpha1().MiddlewareTCPs(namespace).Watch(context.Background(), opts)
},
ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) {
ListWithContextFunc: func(ctx context.Context, opts v1.ListOptions) (runtime.Object, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
tweakListOptions(&opts)
}
return client.TraefikV1alpha1().MiddlewareTCPs(namespace).List(ctx, options)
return client.TraefikV1alpha1().MiddlewareTCPs(namespace).List(ctx, opts)
},
WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) {
WatchFuncWithContext: func(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
tweakListOptions(&opts)
}
return client.TraefikV1alpha1().MiddlewareTCPs(namespace).Watch(ctx, options)
return client.TraefikV1alpha1().MiddlewareTCPs(namespace).Watch(ctx, opts)
},
}, client),
&crdtraefikiov1alpha1.MiddlewareTCP{},
resyncPeriod,
indexers,
cache.SharedIndexInformerOptions{
ResyncPeriod: options.ResyncPeriod,
Indexers: options.Indexers,
Identifier: identifier,
},
)
}
func (f *middlewareTCPInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
return NewFilteredMiddlewareTCPInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
return NewMiddlewareTCPInformerWithOptions(client, f.namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, InformerName: f.factory.InformerName(), TweakListOptions: f.tweakListOptions})
}
func (f *middlewareTCPInformer) Informer() cache.SharedIndexInformer {
@@ -36,6 +36,7 @@ import (
crdtraefikiov1alpha1 "github.com/traefik/traefik/v3/pkg/provider/kubernetes/crd/traefikio/v1alpha1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
schema "k8s.io/apimachinery/pkg/runtime/schema"
watch "k8s.io/apimachinery/pkg/watch"
cache "k8s.io/client-go/tools/cache"
)
@@ -57,48 +58,61 @@ type serversTransportInformer struct {
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewServersTransportInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
return NewFilteredServersTransportInformer(client, namespace, resyncPeriod, indexers, nil)
return NewServersTransportInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: indexers})
}
// NewFilteredServersTransportInformer constructs a new informer for ServersTransport type.
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewFilteredServersTransportInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
return cache.NewSharedIndexInformer(
return NewServersTransportInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: indexers, TweakListOptions: tweakListOptions})
}
// NewServersTransportInformerWithOptions constructs a new informer for ServersTransport type with additional options.
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewServersTransportInformerWithOptions(client versioned.Interface, namespace string, options internalinterfaces.InformerOptions) cache.SharedIndexInformer {
gvr := schema.GroupVersionResource{Group: "traefik.io", Version: "v1alpha1", Resource: "serverstransports"}
identifier := options.InformerName.WithResource(gvr)
tweakListOptions := options.TweakListOptions
return cache.NewSharedIndexInformerWithOptions(
cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{
ListFunc: func(options v1.ListOptions) (runtime.Object, error) {
ListFunc: func(opts v1.ListOptions) (runtime.Object, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
tweakListOptions(&opts)
}
return client.TraefikV1alpha1().ServersTransports(namespace).List(context.Background(), options)
return client.TraefikV1alpha1().ServersTransports(namespace).List(context.Background(), opts)
},
WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
WatchFunc: func(opts v1.ListOptions) (watch.Interface, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
tweakListOptions(&opts)
}
return client.TraefikV1alpha1().ServersTransports(namespace).Watch(context.Background(), options)
return client.TraefikV1alpha1().ServersTransports(namespace).Watch(context.Background(), opts)
},
ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) {
ListWithContextFunc: func(ctx context.Context, opts v1.ListOptions) (runtime.Object, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
tweakListOptions(&opts)
}
return client.TraefikV1alpha1().ServersTransports(namespace).List(ctx, options)
return client.TraefikV1alpha1().ServersTransports(namespace).List(ctx, opts)
},
WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) {
WatchFuncWithContext: func(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
tweakListOptions(&opts)
}
return client.TraefikV1alpha1().ServersTransports(namespace).Watch(ctx, options)
return client.TraefikV1alpha1().ServersTransports(namespace).Watch(ctx, opts)
},
}, client),
&crdtraefikiov1alpha1.ServersTransport{},
resyncPeriod,
indexers,
cache.SharedIndexInformerOptions{
ResyncPeriod: options.ResyncPeriod,
Indexers: options.Indexers,
Identifier: identifier,
},
)
}
func (f *serversTransportInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
return NewFilteredServersTransportInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
return NewServersTransportInformerWithOptions(client, f.namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, InformerName: f.factory.InformerName(), TweakListOptions: f.tweakListOptions})
}
func (f *serversTransportInformer) Informer() cache.SharedIndexInformer {
@@ -36,6 +36,7 @@ import (
crdtraefikiov1alpha1 "github.com/traefik/traefik/v3/pkg/provider/kubernetes/crd/traefikio/v1alpha1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
schema "k8s.io/apimachinery/pkg/runtime/schema"
watch "k8s.io/apimachinery/pkg/watch"
cache "k8s.io/client-go/tools/cache"
)
@@ -57,48 +58,61 @@ type serversTransportTCPInformer struct {
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewServersTransportTCPInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
return NewFilteredServersTransportTCPInformer(client, namespace, resyncPeriod, indexers, nil)
return NewServersTransportTCPInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: indexers})
}
// NewFilteredServersTransportTCPInformer constructs a new informer for ServersTransportTCP type.
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewFilteredServersTransportTCPInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
return cache.NewSharedIndexInformer(
return NewServersTransportTCPInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: indexers, TweakListOptions: tweakListOptions})
}
// NewServersTransportTCPInformerWithOptions constructs a new informer for ServersTransportTCP type with additional options.
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewServersTransportTCPInformerWithOptions(client versioned.Interface, namespace string, options internalinterfaces.InformerOptions) cache.SharedIndexInformer {
gvr := schema.GroupVersionResource{Group: "traefik.io", Version: "v1alpha1", Resource: "serverstransporttcps"}
identifier := options.InformerName.WithResource(gvr)
tweakListOptions := options.TweakListOptions
return cache.NewSharedIndexInformerWithOptions(
cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{
ListFunc: func(options v1.ListOptions) (runtime.Object, error) {
ListFunc: func(opts v1.ListOptions) (runtime.Object, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
tweakListOptions(&opts)
}
return client.TraefikV1alpha1().ServersTransportTCPs(namespace).List(context.Background(), options)
return client.TraefikV1alpha1().ServersTransportTCPs(namespace).List(context.Background(), opts)
},
WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
WatchFunc: func(opts v1.ListOptions) (watch.Interface, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
tweakListOptions(&opts)
}
return client.TraefikV1alpha1().ServersTransportTCPs(namespace).Watch(context.Background(), options)
return client.TraefikV1alpha1().ServersTransportTCPs(namespace).Watch(context.Background(), opts)
},
ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) {
ListWithContextFunc: func(ctx context.Context, opts v1.ListOptions) (runtime.Object, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
tweakListOptions(&opts)
}
return client.TraefikV1alpha1().ServersTransportTCPs(namespace).List(ctx, options)
return client.TraefikV1alpha1().ServersTransportTCPs(namespace).List(ctx, opts)
},
WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) {
WatchFuncWithContext: func(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
tweakListOptions(&opts)
}
return client.TraefikV1alpha1().ServersTransportTCPs(namespace).Watch(ctx, options)
return client.TraefikV1alpha1().ServersTransportTCPs(namespace).Watch(ctx, opts)
},
}, client),
&crdtraefikiov1alpha1.ServersTransportTCP{},
resyncPeriod,
indexers,
cache.SharedIndexInformerOptions{
ResyncPeriod: options.ResyncPeriod,
Indexers: options.Indexers,
Identifier: identifier,
},
)
}
func (f *serversTransportTCPInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
return NewFilteredServersTransportTCPInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
return NewServersTransportTCPInformerWithOptions(client, f.namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, InformerName: f.factory.InformerName(), TweakListOptions: f.tweakListOptions})
}
func (f *serversTransportTCPInformer) Informer() cache.SharedIndexInformer {
@@ -36,6 +36,7 @@ import (
crdtraefikiov1alpha1 "github.com/traefik/traefik/v3/pkg/provider/kubernetes/crd/traefikio/v1alpha1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
schema "k8s.io/apimachinery/pkg/runtime/schema"
watch "k8s.io/apimachinery/pkg/watch"
cache "k8s.io/client-go/tools/cache"
)
@@ -57,48 +58,61 @@ type tLSOptionInformer struct {
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewTLSOptionInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
return NewFilteredTLSOptionInformer(client, namespace, resyncPeriod, indexers, nil)
return NewTLSOptionInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: indexers})
}
// NewFilteredTLSOptionInformer constructs a new informer for TLSOption type.
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewFilteredTLSOptionInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
return cache.NewSharedIndexInformer(
return NewTLSOptionInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: indexers, TweakListOptions: tweakListOptions})
}
// NewTLSOptionInformerWithOptions constructs a new informer for TLSOption type with additional options.
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewTLSOptionInformerWithOptions(client versioned.Interface, namespace string, options internalinterfaces.InformerOptions) cache.SharedIndexInformer {
gvr := schema.GroupVersionResource{Group: "traefik.io", Version: "v1alpha1", Resource: "tlsoptions"}
identifier := options.InformerName.WithResource(gvr)
tweakListOptions := options.TweakListOptions
return cache.NewSharedIndexInformerWithOptions(
cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{
ListFunc: func(options v1.ListOptions) (runtime.Object, error) {
ListFunc: func(opts v1.ListOptions) (runtime.Object, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
tweakListOptions(&opts)
}
return client.TraefikV1alpha1().TLSOptions(namespace).List(context.Background(), options)
return client.TraefikV1alpha1().TLSOptions(namespace).List(context.Background(), opts)
},
WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
WatchFunc: func(opts v1.ListOptions) (watch.Interface, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
tweakListOptions(&opts)
}
return client.TraefikV1alpha1().TLSOptions(namespace).Watch(context.Background(), options)
return client.TraefikV1alpha1().TLSOptions(namespace).Watch(context.Background(), opts)
},
ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) {
ListWithContextFunc: func(ctx context.Context, opts v1.ListOptions) (runtime.Object, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
tweakListOptions(&opts)
}
return client.TraefikV1alpha1().TLSOptions(namespace).List(ctx, options)
return client.TraefikV1alpha1().TLSOptions(namespace).List(ctx, opts)
},
WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) {
WatchFuncWithContext: func(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
tweakListOptions(&opts)
}
return client.TraefikV1alpha1().TLSOptions(namespace).Watch(ctx, options)
return client.TraefikV1alpha1().TLSOptions(namespace).Watch(ctx, opts)
},
}, client),
&crdtraefikiov1alpha1.TLSOption{},
resyncPeriod,
indexers,
cache.SharedIndexInformerOptions{
ResyncPeriod: options.ResyncPeriod,
Indexers: options.Indexers,
Identifier: identifier,
},
)
}
func (f *tLSOptionInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
return NewFilteredTLSOptionInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
return NewTLSOptionInformerWithOptions(client, f.namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, InformerName: f.factory.InformerName(), TweakListOptions: f.tweakListOptions})
}
func (f *tLSOptionInformer) Informer() cache.SharedIndexInformer {
@@ -36,6 +36,7 @@ import (
crdtraefikiov1alpha1 "github.com/traefik/traefik/v3/pkg/provider/kubernetes/crd/traefikio/v1alpha1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
schema "k8s.io/apimachinery/pkg/runtime/schema"
watch "k8s.io/apimachinery/pkg/watch"
cache "k8s.io/client-go/tools/cache"
)
@@ -57,48 +58,61 @@ type tLSStoreInformer struct {
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewTLSStoreInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
return NewFilteredTLSStoreInformer(client, namespace, resyncPeriod, indexers, nil)
return NewTLSStoreInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: indexers})
}
// NewFilteredTLSStoreInformer constructs a new informer for TLSStore type.
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewFilteredTLSStoreInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
return cache.NewSharedIndexInformer(
return NewTLSStoreInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: indexers, TweakListOptions: tweakListOptions})
}
// NewTLSStoreInformerWithOptions constructs a new informer for TLSStore type with additional options.
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewTLSStoreInformerWithOptions(client versioned.Interface, namespace string, options internalinterfaces.InformerOptions) cache.SharedIndexInformer {
gvr := schema.GroupVersionResource{Group: "traefik.io", Version: "v1alpha1", Resource: "tlsstores"}
identifier := options.InformerName.WithResource(gvr)
tweakListOptions := options.TweakListOptions
return cache.NewSharedIndexInformerWithOptions(
cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{
ListFunc: func(options v1.ListOptions) (runtime.Object, error) {
ListFunc: func(opts v1.ListOptions) (runtime.Object, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
tweakListOptions(&opts)
}
return client.TraefikV1alpha1().TLSStores(namespace).List(context.Background(), options)
return client.TraefikV1alpha1().TLSStores(namespace).List(context.Background(), opts)
},
WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
WatchFunc: func(opts v1.ListOptions) (watch.Interface, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
tweakListOptions(&opts)
}
return client.TraefikV1alpha1().TLSStores(namespace).Watch(context.Background(), options)
return client.TraefikV1alpha1().TLSStores(namespace).Watch(context.Background(), opts)
},
ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) {
ListWithContextFunc: func(ctx context.Context, opts v1.ListOptions) (runtime.Object, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
tweakListOptions(&opts)
}
return client.TraefikV1alpha1().TLSStores(namespace).List(ctx, options)
return client.TraefikV1alpha1().TLSStores(namespace).List(ctx, opts)
},
WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) {
WatchFuncWithContext: func(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
tweakListOptions(&opts)
}
return client.TraefikV1alpha1().TLSStores(namespace).Watch(ctx, options)
return client.TraefikV1alpha1().TLSStores(namespace).Watch(ctx, opts)
},
}, client),
&crdtraefikiov1alpha1.TLSStore{},
resyncPeriod,
indexers,
cache.SharedIndexInformerOptions{
ResyncPeriod: options.ResyncPeriod,
Indexers: options.Indexers,
Identifier: identifier,
},
)
}
func (f *tLSStoreInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
return NewFilteredTLSStoreInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
return NewTLSStoreInformerWithOptions(client, f.namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, InformerName: f.factory.InformerName(), TweakListOptions: f.tweakListOptions})
}
func (f *tLSStoreInformer) Informer() cache.SharedIndexInformer {
@@ -36,6 +36,7 @@ import (
crdtraefikiov1alpha1 "github.com/traefik/traefik/v3/pkg/provider/kubernetes/crd/traefikio/v1alpha1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
schema "k8s.io/apimachinery/pkg/runtime/schema"
watch "k8s.io/apimachinery/pkg/watch"
cache "k8s.io/client-go/tools/cache"
)
@@ -57,48 +58,61 @@ type traefikServiceInformer struct {
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewTraefikServiceInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
return NewFilteredTraefikServiceInformer(client, namespace, resyncPeriod, indexers, nil)
return NewTraefikServiceInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: indexers})
}
// NewFilteredTraefikServiceInformer constructs a new informer for TraefikService type.
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewFilteredTraefikServiceInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
return cache.NewSharedIndexInformer(
return NewTraefikServiceInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: indexers, TweakListOptions: tweakListOptions})
}
// NewTraefikServiceInformerWithOptions constructs a new informer for TraefikService type with additional options.
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewTraefikServiceInformerWithOptions(client versioned.Interface, namespace string, options internalinterfaces.InformerOptions) cache.SharedIndexInformer {
gvr := schema.GroupVersionResource{Group: "traefik.io", Version: "v1alpha1", Resource: "traefikservices"}
identifier := options.InformerName.WithResource(gvr)
tweakListOptions := options.TweakListOptions
return cache.NewSharedIndexInformerWithOptions(
cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{
ListFunc: func(options v1.ListOptions) (runtime.Object, error) {
ListFunc: func(opts v1.ListOptions) (runtime.Object, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
tweakListOptions(&opts)
}
return client.TraefikV1alpha1().TraefikServices(namespace).List(context.Background(), options)
return client.TraefikV1alpha1().TraefikServices(namespace).List(context.Background(), opts)
},
WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
WatchFunc: func(opts v1.ListOptions) (watch.Interface, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
tweakListOptions(&opts)
}
return client.TraefikV1alpha1().TraefikServices(namespace).Watch(context.Background(), options)
return client.TraefikV1alpha1().TraefikServices(namespace).Watch(context.Background(), opts)
},
ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) {
ListWithContextFunc: func(ctx context.Context, opts v1.ListOptions) (runtime.Object, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
tweakListOptions(&opts)
}
return client.TraefikV1alpha1().TraefikServices(namespace).List(ctx, options)
return client.TraefikV1alpha1().TraefikServices(namespace).List(ctx, opts)
},
WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) {
WatchFuncWithContext: func(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
tweakListOptions(&opts)
}
return client.TraefikV1alpha1().TraefikServices(namespace).Watch(ctx, options)
return client.TraefikV1alpha1().TraefikServices(namespace).Watch(ctx, opts)
},
}, client),
&crdtraefikiov1alpha1.TraefikService{},
resyncPeriod,
indexers,
cache.SharedIndexInformerOptions{
ResyncPeriod: options.ResyncPeriod,
Indexers: options.Indexers,
Identifier: identifier,
},
)
}
func (f *traefikServiceInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
return NewFilteredTraefikServiceInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
return NewTraefikServiceInformerWithOptions(client, f.namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, InformerName: f.factory.InformerName(), TweakListOptions: f.tweakListOptions})
}
func (f *traefikServiceInformer) Informer() cache.SharedIndexInformer {
+35 -7
View File
@@ -154,15 +154,18 @@ func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Cli
Rule: route.Match,
Service: serviceName,
ParentRefs: parentRouterNames,
Observability: &dynamic.RouterObservabilityConfig{
Metadata: &dynamic.ObservabilityMetadata{
Ingress: buildIngressRouteMetadata(ingressRoute),
},
},
}
if route.Observability != nil {
r.Observability = &dynamic.RouterObservabilityConfig{
AccessLogs: route.Observability.AccessLogs,
Metrics: route.Observability.Metrics,
Tracing: route.Observability.Tracing,
TraceVerbosity: route.Observability.TraceVerbosity,
}
r.Observability.AccessLogs = route.Observability.AccessLogs
r.Observability.Metrics = route.Observability.Metrics
r.Observability.Tracing = route.Observability.Tracing
r.Observability.TraceVerbosity = route.Observability.TraceVerbosity
}
if ingressRoute.Spec.TLS != nil {
@@ -192,6 +195,20 @@ func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Cli
return conf
}
// buildIngressRouteMetadata returns the IngressRoute identity attached to
// generated routers so that access logs can expose the originating IngressRoute
// kind, name and namespace. The backing Kubernetes Service identity is
// intentionally not carried here: under multi-service routes, TraefikService
// weighted/mirroring/HRW/failover, the actual backend is only known per request
// and is stamped at runtime from the leaf Service.Observability.
func buildIngressRouteMetadata(ingressRoute *traefikv1alpha1.IngressRoute) *dynamic.KubernetesMetadata {
return &dynamic.KubernetesMetadata{
Kind: "IngressRoute",
Namespace: ingressRoute.Namespace,
Name: ingressRoute.Name,
}
}
func makeMiddlewareKeys(ctx context.Context, ingRouteNamespace string, middlewares []traefikv1alpha1.MiddlewareRef, crossProviderNamespaces []string, allowCrossNamespace bool, nb nameBuilder) ([]string, error) {
var mds []string
@@ -520,7 +537,18 @@ func (c configBuilder) buildServersLB(ctx context.Context, svc traefikv1alpha1.L
return nil, err
}
service := &dynamic.Service{LoadBalancer: lb}
service := &dynamic.Service{
LoadBalancer: lb,
Observability: &dynamic.ServiceObservabilityConfig{
Metadata: &dynamic.ServiceObservabilityMetadata{
Kubernetes: &dynamic.KubernetesServiceMetadata{
Namespace: svc.Namespace,
Name: svc.Name,
Port: svc.Port.String(),
},
},
},
}
if len(svc.Middlewares) > 0 {
mds, err := makeMiddlewareKeys(ctx, svc.Namespace, svc.Middlewares, c.crossProviderNamespaces, c.allowCrossNamespace, c.nameBuilder)
if err != nil {
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,39 @@
---
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: traefik
spec:
controllerName: traefik.io/gateway-controller
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: traefik-internal
namespace: default
spec:
gatewayClassName: traefik
listeners:
- name: http
protocol: HTTP
port: 9080
allowedRoutes:
namespaces:
from: Same
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: traefik-external
namespace: default
spec:
gatewayClassName: traefik
listeners:
- name: http
protocol: HTTP
port: 9080
allowedRoutes:
namespaces:
from: Same
@@ -0,0 +1,71 @@
---
kind: GatewayClass
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway-class
spec:
controllerName: traefik.io/gateway-controller
---
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway
namespace: default
spec:
gatewayClassName: my-gateway-class
listeners:
- name: http
protocol: HTTP
port: 9080
allowedRoutes:
namespaces:
from: Same
---
kind: GRPCRoute
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: grpc-app-priority
namespace: default
creationTimestamp: "2024-01-01T00:00:01Z"
spec:
parentRefs:
- name: my-gateway
kind: Gateway
group: gateway.networking.k8s.io
hostnames:
- "foo.com"
rules:
- backendRefs:
- name: whoami
port: 80
weight: 1
kind: Service
group: ""
---
kind: HTTPRoute
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: http-app-priority
namespace: default
creationTimestamp: "2024-01-01T00:00:02Z"
spec:
parentRefs:
- name: my-gateway
kind: Gateway
group: gateway.networking.k8s.io
hostnames:
- "foo.com"
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: whoami
port: 80
weight: 1
kind: Service
group: ""
@@ -0,0 +1,71 @@
---
kind: GatewayClass
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway-class
spec:
controllerName: traefik.io/gateway-controller
---
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway
namespace: default
spec:
gatewayClassName: my-gateway-class
listeners:
- name: http
protocol: HTTP
port: 9080
allowedRoutes:
namespaces:
from: Same
---
kind: HTTPRoute
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: http-app-conflict
namespace: default
creationTimestamp: "2024-01-01T00:00:01Z"
spec:
parentRefs:
- name: my-gateway
kind: Gateway
group: gateway.networking.k8s.io
hostnames:
- "foo.com"
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: whoami
port: 80
weight: 1
kind: Service
group: ""
---
kind: GRPCRoute
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: grpc-app-conflict
namespace: default
creationTimestamp: "2024-01-01T00:00:02Z"
spec:
parentRefs:
- name: my-gateway
kind: Gateway
group: gateway.networking.k8s.io
hostnames:
- "foo.com"
rules:
- backendRefs:
- name: whoami
port: 80
weight: 1
kind: Service
group: ""
+73 -67
View File
@@ -20,89 +20,95 @@ import (
gatev1 "sigs.k8s.io/gateway-api/apis/v1"
)
// TODO: as described in the specification https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io%2fv1.GRPCRoute, we should check for hostname conflicts between HTTP and GRPC routes.
func (p *Provider) loadGRPCRoutes(ctx context.Context, gateways []gatewayWithListeners, conf *dynamic.Configuration, statusReport *statusReport) {
routes, err := p.client.ListGRPCRoutes()
if err != nil {
log.Ctx(ctx).Error().Err(err).Msg("Unable to list GRPCRoutes")
func (p *Provider) loadGRPCRoute(ctx context.Context, gateways []gatewayWithListeners, route *gatev1.GRPCRoute, conf *dynamic.Configuration, attachedRoutes attachedRoutes, statusReport *statusReport) {
logger := log.Ctx(ctx).With().
Str("grpc_route", route.Name).
Str("namespace", route.Namespace).
Logger()
routeParentRefs := matchingGatewayListenersForParentRef(gateways, route.Namespace, route.Spec.ParentRefs)
if len(routeParentRefs) == 0 {
return
}
for _, route := range routes {
logger := log.Ctx(ctx).With().
Str("grpc_route", route.Name).
Str("namespace", route.Namespace).
Logger()
routeParentRefs := matchingGatewayListenersForParentRef(gateways, route.Namespace, route.Spec.ParentRefs)
if len(routeParentRefs) == 0 {
continue
for _, match := range routeParentRefs {
acceptedCondition := metav1.Condition{
Type: string(gatev1.RouteConditionAccepted),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonNoMatchingParent),
}
for _, match := range routeParentRefs {
acceptedCondition := metav1.Condition{
Type: string(gatev1.RouteConditionAccepted),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonNoMatchingParent),
var resolvedRefCondition *metav1.Condition
for _, listener := range match.Listeners {
// A parentRef can target specific listeners through its SectionName or Port.
accepted := matchListener(listener, match.ParentRef)
if accepted && !allowRoute(listener, route.Namespace, kindGRPCRoute) {
if acceptedCondition.Status == metav1.ConditionFalse {
acceptedCondition.Reason = string(gatev1.RouteReasonNotAllowedByListeners)
}
accepted = false
}
var resolvedRefCondition *metav1.Condition
for _, listener := range match.listeners {
// A parentRef can target specific listeners through its SectionName or Port.
accepted := matchListener(listener, match.parentRef)
if accepted && !allowRoute(listener, route.Namespace, kindGRPCRoute) {
if acceptedCondition.Status == metav1.ConditionFalse {
acceptedCondition.Reason = string(gatev1.RouteReasonNotAllowedByListeners)
}
accepted = false
}
hostnames, ok := findMatchingHostnames(listener.Hostname, route.Spec.Hostnames)
if accepted && !ok {
if acceptedCondition.Status == metav1.ConditionFalse {
acceptedCondition.Reason = string(gatev1.RouteReasonNoMatchingListenerHostname)
}
accepted = false
}
if accepted {
// Gateway listener should have AttachedRoutes set even when Gateway has unresolved refs.
listener.Status.AttachedRoutes++
}
// The ResolvedRefs condition must be reported for every parentRef,
// even when the route does not attach to the listener.
routeConf, condition := p.loadGRPCRoute(logger.WithContext(ctx), match.gatewayName, match.gatewayNamespace, listener, route, hostnames, statusReport)
if resolvedRefCondition == nil || resolvedRefCondition.Status == metav1.ConditionTrue {
resolvedRefCondition = new(condition)
}
if accepted && listener.Attached {
mergeHTTPConfiguration(routeConf, conf)
// Only consider the route attached if the listener is in an "attached" state.
acceptedCondition.Reason = string(gatev1.RouteReasonAccepted)
acceptedCondition.Status = metav1.ConditionTrue
hostnames, ok := findMatchingHostnames(listener.Hostname, route.Spec.Hostnames)
if accepted && !ok {
if acceptedCondition.Status == metav1.ConditionFalse {
acceptedCondition.Reason = string(gatev1.RouteReasonNoMatchingListenerHostname)
}
accepted = false
}
parentStatusConditions := []metav1.Condition{acceptedCondition}
if resolvedRefCondition != nil {
parentStatusConditions = append(parentStatusConditions, *resolvedRefCondition)
ref := listenerRef{
GatewayNamespace: match.GatewayNamespace,
GatewayName: match.GatewayName,
Name: listener.Name,
}
if accepted && attachedRoutes.Conflicts(ref, kindGRPCRoute, hostnames) {
if acceptedCondition.Status == metav1.ConditionFalse {
acceptedCondition.Reason = string(routeReasonHostnameConflict)
}
accepted = false
}
statusReport.RecordGRPCRouteStatus(ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, gatev1.RouteParentStatus{
ParentRef: match.parentRef,
ControllerName: controllerName,
Conditions: parentStatusConditions,
})
if accepted {
// Gateway listener should have AttachedRoutes set even when Gateway has unresolved refs.
listener.Status.AttachedRoutes++
attachedRoutes.Record(ref, kindGRPCRoute, hostnames)
}
// The ResolvedRefs condition must be reported for every parentRef,
// even when the route does not attach to the listener.
routeConf, condition := p.loadGRPCRouteConfiguration(logger.WithContext(ctx), match.GatewayName, match.GatewayNamespace, listener, route, hostnames, statusReport)
if resolvedRefCondition == nil || resolvedRefCondition.Status == metav1.ConditionTrue {
resolvedRefCondition = new(condition)
}
if accepted && listener.Attached {
mergeHTTPConfiguration(routeConf, conf)
// Only consider the route attached if the listener is in an "attached" state.
acceptedCondition.Reason = string(gatev1.RouteReasonAccepted)
acceptedCondition.Status = metav1.ConditionTrue
}
}
parentStatusConditions := []metav1.Condition{acceptedCondition}
if resolvedRefCondition != nil {
parentStatusConditions = append(parentStatusConditions, *resolvedRefCondition)
}
statusReport.RecordGRPCRouteStatus(ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, gatev1.RouteParentStatus{
ParentRef: match.ParentRef,
ControllerName: controllerName,
Conditions: parentStatusConditions,
})
}
}
func (p *Provider) loadGRPCRoute(ctx context.Context, gatewayName, gatewayNamespace string, listener gatewayListener, route *gatev1.GRPCRoute, hostnames []gatev1.Hostname, statusReport *statusReport) (*dynamic.Configuration, metav1.Condition) {
func (p *Provider) loadGRPCRouteConfiguration(ctx context.Context, gatewayName, gatewayNamespace string, listener gatewayListener, route *gatev1.GRPCRoute, hostnames []gatev1.Hostname, statusReport *statusReport) (*dynamic.Configuration, metav1.Condition) {
conf := &dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Routers: make(map[string]*dynamic.Router),
+73 -67
View File
@@ -24,89 +24,95 @@ import (
gatev1 "sigs.k8s.io/gateway-api/apis/v1"
)
func (p *Provider) loadHTTPRoutes(ctx context.Context, gateways []gatewayWithListeners, conf *dynamic.Configuration, statusReport *statusReport) {
routes, err := p.client.ListHTTPRoutes()
if err != nil {
log.Ctx(ctx).Error().Err(err).Msg("Unable to list HTTPRoutes")
func (p *Provider) loadHTTPRoute(ctx context.Context, gateways []gatewayWithListeners, route *gatev1.HTTPRoute, conf *dynamic.Configuration, attachedRoutes attachedRoutes, statusReport *statusReport) {
logger := log.Ctx(ctx).With().
Str("http_route", route.Name).
Str("namespace", route.Namespace).
Logger()
routeParentRefs := matchingGatewayListenersForParentRef(gateways, route.Namespace, route.Spec.ParentRefs)
if len(routeParentRefs) == 0 {
return
}
for _, route := range routes {
logger := log.Ctx(ctx).With().
Str("http_route", route.Name).
Str("namespace", route.Namespace).
Logger()
routeParentRefs := matchingGatewayListenersForParentRef(gateways, route.Namespace, route.Spec.ParentRefs)
if len(routeParentRefs) == 0 {
continue
for _, match := range routeParentRefs {
acceptedCondition := metav1.Condition{
Type: string(gatev1.RouteConditionAccepted),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonNoMatchingParent),
}
for _, match := range routeParentRefs {
acceptedCondition := metav1.Condition{
Type: string(gatev1.RouteConditionAccepted),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonNoMatchingParent),
var resolvedRefCondition *metav1.Condition
for _, listener := range match.Listeners {
// A parentRef can target specific listeners through its SectionName or Port.
accepted := matchListener(listener, match.ParentRef)
if accepted && !allowRoute(listener, route.Namespace, kindHTTPRoute) {
if acceptedCondition.Status == metav1.ConditionFalse {
acceptedCondition.Reason = string(gatev1.RouteReasonNotAllowedByListeners)
}
accepted = false
}
var resolvedRefCondition *metav1.Condition
for _, listener := range match.listeners {
// A parentRef can target specific listeners through its SectionName or Port.
accepted := matchListener(listener, match.parentRef)
if accepted && !allowRoute(listener, route.Namespace, kindHTTPRoute) {
if acceptedCondition.Status == metav1.ConditionFalse {
acceptedCondition.Reason = string(gatev1.RouteReasonNotAllowedByListeners)
}
accepted = false
}
hostnames, ok := findMatchingHostnames(listener.Hostname, route.Spec.Hostnames)
if accepted && !ok {
if acceptedCondition.Status == metav1.ConditionFalse {
acceptedCondition.Reason = string(gatev1.RouteReasonNoMatchingListenerHostname)
}
accepted = false
}
if accepted {
// Gateway listener should have AttachedRoutes set even when Gateway has unresolved refs.
listener.Status.AttachedRoutes++
}
// The ResolvedRefs condition must be reported for every parentRef,
// even when the route does not attach to the listener.
routeConf, condition := p.loadHTTPRoute(logger.WithContext(ctx), match.gatewayName, match.gatewayNamespace, listener, route, hostnames, statusReport)
if resolvedRefCondition == nil || resolvedRefCondition.Status == metav1.ConditionTrue {
resolvedRefCondition = new(condition)
}
if accepted && listener.Attached {
mergeHTTPConfiguration(routeConf, conf)
// Only consider the route attached if the listener is in an "attached" state.
acceptedCondition.Reason = string(gatev1.RouteReasonAccepted)
acceptedCondition.Status = metav1.ConditionTrue
hostnames, ok := findMatchingHostnames(listener.Hostname, route.Spec.Hostnames)
if accepted && !ok {
if acceptedCondition.Status == metav1.ConditionFalse {
acceptedCondition.Reason = string(gatev1.RouteReasonNoMatchingListenerHostname)
}
accepted = false
}
parentStatusConditions := []metav1.Condition{acceptedCondition}
if resolvedRefCondition != nil {
parentStatusConditions = append(parentStatusConditions, *resolvedRefCondition)
ref := listenerRef{
GatewayNamespace: match.GatewayNamespace,
GatewayName: match.GatewayName,
Name: listener.Name,
}
if accepted && attachedRoutes.Conflicts(ref, kindHTTPRoute, hostnames) {
if acceptedCondition.Status == metav1.ConditionFalse {
acceptedCondition.Reason = string(routeReasonHostnameConflict)
}
accepted = false
}
statusReport.RecordHTTPRouteStatus(ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, gatev1.RouteParentStatus{
ParentRef: match.parentRef,
ControllerName: controllerName,
Conditions: parentStatusConditions,
})
if accepted {
// Gateway listener should have AttachedRoutes set even when Gateway has unresolved refs.
listener.Status.AttachedRoutes++
attachedRoutes.Record(ref, kindHTTPRoute, hostnames)
}
// The ResolvedRefs condition must be reported for every parentRef,
// even when the route does not attach to the listener.
routeConf, condition := p.loadHTTPRouteConfiguration(logger.WithContext(ctx), match.GatewayName, match.GatewayNamespace, listener, route, hostnames, statusReport)
if resolvedRefCondition == nil || resolvedRefCondition.Status == metav1.ConditionTrue {
resolvedRefCondition = new(condition)
}
if accepted && listener.Attached {
mergeHTTPConfiguration(routeConf, conf)
// Only consider the route attached if the listener is in an "attached" state.
acceptedCondition.Reason = string(gatev1.RouteReasonAccepted)
acceptedCondition.Status = metav1.ConditionTrue
}
}
parentStatusConditions := []metav1.Condition{acceptedCondition}
if resolvedRefCondition != nil {
parentStatusConditions = append(parentStatusConditions, *resolvedRefCondition)
}
statusReport.RecordHTTPRouteStatus(ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, gatev1.RouteParentStatus{
ParentRef: match.ParentRef,
ControllerName: controllerName,
Conditions: parentStatusConditions,
})
}
}
func (p *Provider) loadHTTPRoute(ctx context.Context, gatewayName, gatewayNamespace string, listener gatewayListener, route *gatev1.HTTPRoute, hostnames []gatev1.Hostname, statusReport *statusReport) (*dynamic.Configuration, metav1.Condition) {
func (p *Provider) loadHTTPRouteConfiguration(ctx context.Context, gatewayName, gatewayNamespace string, listener gatewayListener, route *gatev1.HTTPRoute, hostnames []gatev1.Hostname, statusReport *statusReport) (*dynamic.Configuration, metav1.Condition) {
conf := &dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Routers: make(map[string]*dynamic.Router),
+150 -30
View File
@@ -62,23 +62,35 @@ const (
schemeHTTP = "http"
schemeHTTPS = "https"
schemeH2C = "h2c"
// routeReasonHostnameConflict is raised when an HTTP and a GRPC route are
// attached to the same listener with intersecting hostnames.
routeReasonHostnameConflict gatev1.RouteConditionReason = "HostnameConflict"
)
// NamespacedName holds a Kubernetes resource reference with namespace and name.
type NamespacedName struct {
Namespace string `description:"Defines the resource namespace." json:"namespace,omitempty" toml:"namespace,omitempty" yaml:"namespace,omitempty"`
Name string `description:"Defines the resource name." json:"name,omitempty" toml:"name,omitempty" yaml:"name,omitempty"`
}
// Provider holds configurations of the provider.
type Provider struct {
Endpoint string `description:"Kubernetes server endpoint (required for external cluster client)." json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty"`
Token types.FileOrContent `description:"Kubernetes bearer token (not needed for in-cluster client). It accepts either a token value or a file path to the token." json:"token,omitempty" toml:"token,omitempty" yaml:"token,omitempty" loggable:"false"`
QPS int `description:"Defines the maximum QPS to the Kubernetes API server. Setting this to a negative value will disable client-side ratelimiting." json:"qps,omitempty" toml:"qps,omitempty" yaml:"qps,omitempty" export:"true"`
Burst int `description:"Defines the maximum burst of requests to the Kubernetes API server." json:"burst,omitempty" toml:"burst,omitempty" yaml:"burst,omitempty" export:"true"`
CertAuthFilePath string `description:"Kubernetes certificate authority file path (not needed for in-cluster client)." json:"certAuthFilePath,omitempty" toml:"certAuthFilePath,omitempty" yaml:"certAuthFilePath,omitempty"`
Namespaces []string `description:"Kubernetes namespaces." json:"namespaces,omitempty" toml:"namespaces,omitempty" yaml:"namespaces,omitempty" export:"true"`
LabelSelector string `description:"Kubernetes label selector to select specific GatewayClasses." json:"labelSelector,omitempty" toml:"labelSelector,omitempty" yaml:"labelSelector,omitempty" export:"true"`
ThrottleDuration ptypes.Duration `description:"Kubernetes refresh throttle duration" json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,omitempty" export:"true"`
ExperimentalChannel bool `description:"Toggles Experimental Channel resources support. Requires the Experimental Channel CRDs." json:"experimentalChannel,omitempty" toml:"experimentalChannel,omitempty" yaml:"experimentalChannel,omitempty" export:"true"`
StatusAddress *StatusAddress `description:"Defines the Kubernetes Gateway status address." json:"statusAddress,omitempty" toml:"statusAddress,omitempty" yaml:"statusAddress,omitempty" export:"true"`
NativeLBByDefault bool `description:"Defines whether to use Native Kubernetes load-balancing by default." json:"nativeLBByDefault,omitempty" toml:"nativeLBByDefault,omitempty" yaml:"nativeLBByDefault,omitempty" export:"true"`
CrossProviderNamespaces []string `description:"List of namespaces from which Gateway API routes are allowed to declare TraefikService backendRef references." json:"crossProviderNamespaces,omitempty" toml:"crossProviderNamespaces,omitempty" yaml:"crossProviderNamespaces,omitempty" export:"true"`
EntryPoints map[string]Entrypoint `json:"-" toml:"-" yaml:"-" label:"-" file:"-"`
Endpoint string `description:"Kubernetes server endpoint (required for external cluster client)." json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty"`
Token types.FileOrContent `description:"Kubernetes bearer token (not needed for in-cluster client). It accepts either a token value or a file path to the token." json:"token,omitempty" toml:"token,omitempty" yaml:"token,omitempty" loggable:"false"`
QPS int `description:"Defines the maximum QPS to the Kubernetes API server. Setting this to a negative value will disable client-side ratelimiting." json:"qps,omitempty" toml:"qps,omitempty" yaml:"qps,omitempty" export:"true"`
Burst int `description:"Defines the maximum burst of requests to the Kubernetes API server." json:"burst,omitempty" toml:"burst,omitempty" yaml:"burst,omitempty" export:"true"`
CertAuthFilePath string `description:"Kubernetes certificate authority file path (not needed for in-cluster client)." json:"certAuthFilePath,omitempty" toml:"certAuthFilePath,omitempty" yaml:"certAuthFilePath,omitempty"`
Namespaces []string `description:"Kubernetes namespaces." json:"namespaces,omitempty" toml:"namespaces,omitempty" yaml:"namespaces,omitempty" export:"true"`
LabelSelector string `description:"Kubernetes label selector to select specific GatewayClasses." json:"labelSelector,omitempty" toml:"labelSelector,omitempty" yaml:"labelSelector,omitempty" export:"true"`
Gateways []NamespacedName `description:"Scopes the provider to specific Gateways." json:"gateways,omitempty" toml:"gateways,omitempty" yaml:"gateways,omitempty" export:"true"`
ThrottleDuration ptypes.Duration `description:"Kubernetes refresh throttle duration" json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,omitempty" export:"true"`
ExperimentalChannel bool `description:"Toggles Experimental Channel resources support. Requires the Experimental Channel CRDs." json:"experimentalChannel,omitempty" toml:"experimentalChannel,omitempty" yaml:"experimentalChannel,omitempty" export:"true"`
StatusAddress *StatusAddress `description:"Defines the Kubernetes Gateway status address." json:"statusAddress,omitempty" toml:"statusAddress,omitempty" yaml:"statusAddress,omitempty" export:"true"`
NativeLBByDefault bool `description:"Defines whether to use Native Kubernetes load-balancing by default." json:"nativeLBByDefault,omitempty" toml:"nativeLBByDefault,omitempty" yaml:"nativeLBByDefault,omitempty" export:"true"`
CrossProviderNamespaces []string `description:"List of namespaces from which Gateway API routes are allowed to declare TraefikService backendRef references." json:"crossProviderNamespaces,omitempty" toml:"crossProviderNamespaces,omitempty" yaml:"crossProviderNamespaces,omitempty" export:"true"`
EntryPoints map[string]Entrypoint `json:"-" toml:"-" yaml:"-" label:"-" file:"-"`
// groupKindFilterFuncs is the list of allowed Group and Kinds for the Filter ExtensionRef objects.
groupKindFilterFuncs map[string]map[string]BuildFilterFunc
@@ -104,7 +116,7 @@ type Entrypoint struct {
type StatusAddress struct {
IP string `description:"IP used to set Kubernetes Gateway status address." json:"ip,omitempty" toml:"ip,omitempty" yaml:"ip,omitempty"`
Hostname string `description:"Hostname used for Kubernetes Gateway status address." json:"hostname,omitempty" toml:"hostname,omitempty" yaml:"hostname,omitempty"`
Service ServiceRef `description:"Published Kubernetes Service to copy status addresses from." json:"service,omitempty" toml:"service,omitempty" yaml:"service,omitempty"`
Service ServiceRef `description:"Published Kubernetes Service to copy status addresses from." json:"service" toml:"service,omitempty" yaml:"service,omitempty"`
}
// ServiceRef holds a Kubernetes service reference.
@@ -277,6 +289,22 @@ func (p *Provider) applyRouterTransform(ctx context.Context, rt *dynamic.Router,
}
}
// managesGateway reports whether the provider should reconcile the given Gateways.
// When the Gateways scoping option is set, only the referenced namespace/name entries are managed.
func (p *Provider) managesGateway(gateway *gatev1.Gateway) bool {
if len(p.Gateways) == 0 {
return true
}
for _, g := range p.Gateways {
if gateway.Namespace == g.Namespace && gateway.Name == g.Name {
return true
}
}
return false
}
func (p *Provider) newK8sClient(ctx context.Context) (*clientWrapper, error) {
// Label selector validation
_, err := labels.Parse(p.LabelSelector)
@@ -380,6 +408,9 @@ func (p *Provider) loadConfigurationFromGateways(ctx context.Context) (*dynamic.
if _, ok := gatewayClassNames[string(gateway.Spec.GatewayClassName)]; !ok {
continue
}
if !p.managesGateway(gateway) {
continue
}
gateways = append(gateways, gateway)
}
@@ -402,9 +433,7 @@ func (p *Provider) loadConfigurationFromGateways(ctx context.Context) (*dynamic.
// The isolation of a listener depends on the other listeners of its entry point.
listenerRouters := p.buildListenerRouters(selectedGateways, conf)
p.loadHTTPRoutes(ctx, selectedGateways, conf, statusReport)
p.loadGRPCRoutes(ctx, selectedGateways, conf, statusReport)
p.loadHTTPAndGRPCRoutes(ctx, selectedGateways, conf, statusReport)
p.loadTLSRoutes(ctx, selectedGateways, conf, statusReport)
@@ -448,6 +477,47 @@ func (p *Provider) loadConfigurationFromGateways(ctx context.Context) (*dynamic.
return conf, statusReport, nil
}
// loadHTTPAndGRPCRoutes loads the HTTPRoutes and the GRPCRoutes together, ordered by
// creation timestamp then by "{namespace}/{name}".
// As stated in the specification, when an HTTPRoute and a GRPCRoute attached to the same
// listener have intersecting hostnames, only the first one in that order is accepted.
func (p *Provider) loadHTTPAndGRPCRoutes(ctx context.Context, gateways []gatewayWithListeners, conf *dynamic.Configuration, statusReport *statusReport) {
routes := make([]metav1.Object, 0)
httpRoutes, err := p.client.ListHTTPRoutes()
if err != nil {
log.Ctx(ctx).Error().Err(err).Msg("Unable to list HTTPRoutes")
}
for _, route := range httpRoutes {
routes = append(routes, route)
}
grpcRoutes, err := p.client.ListGRPCRoutes()
if err != nil {
log.Ctx(ctx).Error().Err(err).Msg("Unable to list GRPCRoutes")
}
for _, route := range grpcRoutes {
routes = append(routes, route)
}
slices.SortStableFunc(routes, func(a, b metav1.Object) int {
if c := a.GetCreationTimestamp().Time.Compare(b.GetCreationTimestamp().Time); c != 0 {
return c
}
return cmp.Compare(a.GetNamespace()+"/"+a.GetName(), b.GetNamespace()+"/"+b.GetName())
})
attached := make(attachedRoutes)
for _, route := range routes {
switch route := route.(type) {
case *gatev1.HTTPRoute:
p.loadHTTPRoute(ctx, gateways, route, conf, attached, statusReport)
case *gatev1.GRPCRoute:
p.loadGRPCRoute(ctx, gateways, route, conf, attached, statusReport)
}
}
}
func (p *Provider) loadGatewayListeners(ctx context.Context, gateway *gatev1.Gateway, conf *dynamic.Configuration) []gatewayListener {
tlsCerts := make(map[string]*tls.CertAndStores)
allocatedListeners := make(map[string]struct{})
@@ -1258,20 +1328,20 @@ func findMatchingHostnames(listenerHostname *gatev1.Hostname, routeHostnames []g
return []gatev1.Hostname{*listenerHostname}, true
}
var matches []gatev1.Hostname
matches := map[gatev1.Hostname]struct{}{}
for _, routeHostname := range routeHostnames {
if match := findMatchingHostname(*listenerHostname, routeHostname); match != "" {
matches = append(matches, match)
matches[match] = struct{}{}
continue
}
if match := findMatchingHostname(routeHostname, *listenerHostname); match != "" {
matches = append(matches, match)
matches[match] = struct{}{}
continue
}
}
return matches, len(matches) > 0
return slices.Sorted(maps.Keys(matches)), len(matches) > 0
}
func findMatchingHostname(h1, h2 gatev1.Hostname) gatev1.Hostname {
@@ -1304,15 +1374,65 @@ func allowRoute(listener gatewayListener, routeNamespace, routeKind string) bool
})
}
// gatewayListenersForParentRef associates a route parentRef with the listeners of
// listenerRef identifies a listener of a Gateway.
type listenerRef struct {
Name string
GatewayNamespace string
GatewayName string
}
// attachedRoutes holds, for each listener, the kind of the route already attached to a hostname.
// It allows detecting the hostname Conflicts between the HTTPRoutes and the GRPCRoutes attached to the same listener.
type attachedRoutes map[listenerRef]map[gatev1.Hostname]string
func (ar attachedRoutes) Record(ref listenerRef, kind string, hostnames []gatev1.Hostname) {
if ar[ref] == nil {
ar[ref] = make(map[gatev1.Hostname]string)
}
// A route without hostname matches all of them.
if len(hostnames) == 0 {
ar[ref][""] = kind
return
}
for _, hostname := range hostnames {
ar[ref][hostname] = kind
}
}
// Conflicts returns whether one of the given hostnames is already attached to the listener by a route of another kind.
func (ar attachedRoutes) Conflicts(ref listenerRef, kind string, hostnames []gatev1.Hostname) bool {
for attachedHostname, attachedKind := range ar[ref] {
if attachedKind == kind {
continue
}
// The empty hostname stands for all the hostnames,
// so an empty hostname on either side means they intersect.
if attachedHostname == "" || len(hostnames) == 0 {
return true
}
for _, hostname := range hostnames {
if findMatchingHostname(attachedHostname, hostname) != "" || findMatchingHostname(hostname, attachedHostname) != "" {
return true
}
}
}
return false
}
// gatewayListenersForParentRef associates a route ParentRef with the Listeners of
// the Gateway it refers to, among the Gateways managed by this controller.
type gatewayListenersForParentRef struct {
parentRef gatev1.ParentReference
ParentRef gatev1.ParentReference
gatewayName string
gatewayNamespace string
GatewayName string
GatewayNamespace string
listeners []gatewayListener
Listeners []gatewayListener
}
// matchingGatewayListenersForParentRef returns, for each parentRef referring to a
@@ -1352,10 +1472,10 @@ func matchingGatewayListenersForParentRef(gateways []gatewayWithListeners, route
// Port) is decided when loading the route, so that ResolvedRefs is reported
// even for parentRefs that match no listener.
matches = append(matches, gatewayListenersForParentRef{
parentRef: parentRef,
gatewayName: matchingGateway.Name,
gatewayNamespace: matchingGateway.Namespace,
listeners: matchingGateway.listeners,
ParentRef: parentRef,
GatewayName: matchingGateway.Name,
GatewayNamespace: matchingGateway.Namespace,
Listeners: matchingGateway.listeners,
})
}
}
@@ -108,6 +108,46 @@ func TestGatewayClassLabelSelector(t *testing.T) {
assert.Equal(t, "1.2.3.4", gw.Status.Addresses[0].Value)
}
func TestGatewayScoping(t *testing.T) {
k8sObjects, gwObjects := readResources(t, []string{"gateway_scoping.yaml"})
kubeClient := kubefake.NewClientset(k8sObjects...)
gwClient := newGatewaySimpleClientSet(t, gwObjects...)
client := newClientImpl(kubeClient, gwClient)
eventCh, err := client.WatchAll(nil, make(chan struct{}))
require.NoError(t, err)
if len(k8sObjects) > 0 || len(gwObjects) > 0 {
<-eventCh
}
p := Provider{
EntryPoints: map[string]Entrypoint{"http": {Address: ":9080"}},
StatusAddress: &StatusAddress{IP: "1.2.3.4"},
// Scope the provider to a single Gateway.
Gateways: []NamespacedName{{Namespace: "default", Name: "traefik-internal"}},
client: client,
}
_, statusReport, err := p.loadConfigurationFromGateways(t.Context())
require.NoError(t, err)
statusReport.Flush(t.Context(), p.client)
// The scoped Gateway is managed and gets a status address.
gw, err := gwClient.GatewayV1().Gateways("default").Get(t.Context(), "traefik-internal", metav1.GetOptions{})
require.NoError(t, err)
require.Len(t, gw.Status.Addresses, 1)
assert.Equal(t, "1.2.3.4", gw.Status.Addresses[0].Value)
// The other Gateway is ignored.
gw, err = gwClient.GatewayV1().Gateways("default").Get(t.Context(), "traefik-external", metav1.GetOptions{})
require.NoError(t, err)
assert.Empty(t, gw.Status.Addresses)
}
func TestLoadHTTPRoutes(t *testing.T) {
testCases := []struct {
desc string
@@ -7125,7 +7165,7 @@ func TestLoadTLSRoutes(t *testing.T) {
EntryPoints: []string{"tls"},
Service: "tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-0-3d1ac395a1a425187f7e-wrr",
Priority: 15,
Rule: `HostSNI("foo.example.com") || HostSNI("bar.example.com")`,
Rule: `HostSNI("bar.example.com") || HostSNI("foo.example.com")`,
RuleSyntax: "default",
TLS: &dynamic.RouterTCPTLSConfig{
Passthrough: true,
@@ -9226,6 +9266,134 @@ func TestLoadMixedRoutes(t *testing.T) {
},
},
},
{
desc: "GRPCRoute created first wins the conflicting hostname over the HTTPRoute",
paths: []string{"services.yml", "mixed/with_grpcroute_priority_over_httproute.yml"},
entryPoints: map[string]Entrypoint{
"web": {Address: ":9080"},
},
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"listener-web-http-5b3226d4ebd42b7200f1": {
EntryPoints: []string{"web"},
Rule: `Host("*")`,
},
"grpcroute-default-grpc-app-priority-gw-default-my-gateway-ep-web-0-46e5641ba0ccfa2eff8a": {
ParentRefs: []string{"listener-web-http-5b3226d4ebd42b7200f1"},
Service: "grpcroute-default-grpc-app-priority-gw-default-my-gateway-ep-web-0-46e5641ba0ccfa2eff8a-wrr",
Rule: `Host("foo.com") && PathPrefix("/")`,
Priority: 22,
RuleSyntax: "default",
},
},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"grpcroute-default-grpc-app-priority-gw-default-my-gateway-ep-web-0-46e5641ba0ccfa2eff8a-wrr": {
Weighted: &dynamic.WeightedRoundRobin{
Services: []dynamic.WRRService{
{
Name: "grpcroute-default-grpc-app-priority-gw-default-my-gateway-ep-web-0-46e5641ba0ccfa2eff8a-svc-default-whoami-0",
Weight: new(1),
},
},
},
},
"grpcroute-default-grpc-app-priority-gw-default-my-gateway-ep-web-0-46e5641ba0ccfa2eff8a-svc-default-whoami-0": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Strategy: dynamic.BalancerStrategyWRR,
Servers: []dynamic.Server{
{URL: "h2c://10.10.0.1:80"},
{URL: "h2c://10.10.0.2:80"},
},
PassHostHeader: new(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
TLS: &dynamic.TLSConfiguration{
Options: map[string]tls.Options{},
},
},
},
{
desc: "HTTPRoute created first wins the conflicting hostname over the GRPCRoute",
paths: []string{"services.yml", "mixed/with_httproute_priority_over_grpcroute.yml"},
entryPoints: map[string]Entrypoint{
"web": {Address: ":9080"},
},
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"listener-web-http-5b3226d4ebd42b7200f1": {
EntryPoints: []string{"web"},
Rule: `Host("*")`,
},
"httproute-default-http-app-conflict-gw-default-my-gateway-ep-web-0-3fcf93756cef1b546fd2": {
ParentRefs: []string{"listener-web-http-5b3226d4ebd42b7200f1"},
Service: "httproute-default-http-app-conflict-gw-default-my-gateway-ep-web-0-3fcf93756cef1b546fd2-wrr",
Rule: `Host("foo.com") && PathPrefix("/")`,
Priority: 9,
RuleSyntax: "default",
},
},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"httproute-default-http-app-conflict-gw-default-my-gateway-ep-web-0-3fcf93756cef1b546fd2-wrr": {
Weighted: &dynamic.WeightedRoundRobin{
Services: []dynamic.WRRService{
{
Name: "httproute-default-http-app-conflict-gw-default-my-gateway-ep-web-0-3fcf93756cef1b546fd2-svc-default-whoami-0",
Weight: new(1),
},
},
},
},
"httproute-default-http-app-conflict-gw-default-my-gateway-ep-web-0-3fcf93756cef1b546fd2-svc-default-whoami-0": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Strategy: dynamic.BalancerStrategyWRR,
Servers: []dynamic.Server{
{URL: "http://10.10.0.1:80"},
{URL: "http://10.10.0.2:80"},
},
PassHostHeader: new(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
TLS: &dynamic.TLSConfiguration{
Options: map[string]tls.Options{},
},
},
},
}
for _, test := range testCases {
@@ -9714,15 +9882,15 @@ func Test_matchingGatewayListener(t *testing.T) {
Kind: new(gatev1.Kind("Gateway")),
}},
want: []gatewayListenersForParentRef{{
parentRef: gatev1.ParentReference{
ParentRef: gatev1.ParentReference{
Name: "gateway",
Namespace: new(gatev1.Namespace("default")),
Group: new(gatev1.Group(gatev1.GroupName)),
Kind: new(gatev1.Kind("Gateway")),
},
gatewayName: "gateway",
gatewayNamespace: "default",
listeners: []gatewayListener{{}},
GatewayName: "gateway",
GatewayNamespace: "default",
Listeners: []gatewayListener{{}},
}},
},
{
@@ -9739,14 +9907,14 @@ func Test_matchingGatewayListener(t *testing.T) {
Kind: new(gatev1.Kind("Gateway")),
}},
want: []gatewayListenersForParentRef{{
parentRef: gatev1.ParentReference{
ParentRef: gatev1.ParentReference{
Name: "gateway",
Group: new(gatev1.Group(gatev1.GroupName)),
Kind: new(gatev1.Kind("Gateway")),
},
gatewayName: "gateway",
gatewayNamespace: "default",
listeners: []gatewayListener{{}},
GatewayName: "gateway",
GatewayNamespace: "default",
Listeners: []gatewayListener{{}},
}},
},
{
@@ -9774,15 +9942,15 @@ func Test_matchingGatewayListener(t *testing.T) {
},
},
want: []gatewayListenersForParentRef{{
parentRef: gatev1.ParentReference{
ParentRef: gatev1.ParentReference{
Name: "gateway",
Namespace: new(gatev1.Namespace("default")),
Group: new(gatev1.Group(gatev1.GroupName)),
Kind: new(gatev1.Kind("Gateway")),
},
gatewayName: "gateway",
gatewayNamespace: "default",
listeners: []gatewayListener{{}},
GatewayName: "gateway",
GatewayNamespace: "default",
Listeners: []gatewayListener{{}},
}},
},
{
@@ -9802,15 +9970,15 @@ func Test_matchingGatewayListener(t *testing.T) {
Kind: new(gatev1.Kind("Gateway")),
}},
want: []gatewayListenersForParentRef{{
parentRef: gatev1.ParentReference{
ParentRef: gatev1.ParentReference{
Name: "gateway",
Namespace: new(gatev1.Namespace("default")),
Group: new(gatev1.Group(gatev1.GroupName)),
Kind: new(gatev1.Kind("Gateway")),
},
gatewayName: "gateway",
gatewayNamespace: "default",
listeners: []gatewayListener{
GatewayName: "gateway",
GatewayNamespace: "default",
Listeners: []gatewayListener{
{Name: "web"},
{Name: "websecure"},
},
@@ -9839,15 +10007,15 @@ func Test_matchingGatewayListener(t *testing.T) {
Kind: new(gatev1.Kind("Gateway")),
}},
want: []gatewayListenersForParentRef{{
parentRef: gatev1.ParentReference{
ParentRef: gatev1.ParentReference{
Name: "gateway-a",
Namespace: new(gatev1.Namespace("default")),
Group: new(gatev1.Group(gatev1.GroupName)),
Kind: new(gatev1.Kind("Gateway")),
},
gatewayName: "gateway-a",
gatewayNamespace: "default",
listeners: []gatewayListener{{Name: "web"}},
GatewayName: "gateway-a",
GatewayNamespace: "default",
Listeners: []gatewayListener{{Name: "web"}},
}},
},
{
@@ -9868,16 +10036,16 @@ func Test_matchingGatewayListener(t *testing.T) {
Port: new(gatev1.PortNumber(8080)),
}},
want: []gatewayListenersForParentRef{{
parentRef: gatev1.ParentReference{
ParentRef: gatev1.ParentReference{
Name: "gateway",
Namespace: new(gatev1.Namespace("default")),
Group: new(gatev1.Group(gatev1.GroupName)),
Kind: new(gatev1.Kind("Gateway")),
Port: new(gatev1.PortNumber(8080)),
},
gatewayName: "gateway",
gatewayNamespace: "default",
listeners: []gatewayListener{{Name: "web", Port: 80}},
GatewayName: "gateway",
GatewayNamespace: "default",
Listeners: []gatewayListener{{Name: "web", Port: 80}},
}},
},
}
@@ -10010,6 +10178,208 @@ func Test_loadRoutes_multipleGatewaysParentRefs(t *testing.T) {
}
}
func Test_loadHTTPAndGRPCRoutes_hostnameConflict(t *testing.T) {
testCases := []struct {
desc string
path string
acceptedRoute string
acceptedIsHTTP bool
conflictedRoute string
}{
{
desc: "HTTPRoute created first",
path: "mixed/with_httproute_priority_over_grpcroute.yml",
acceptedRoute: "http-app-conflict",
acceptedIsHTTP: true,
conflictedRoute: "grpc-app-conflict",
},
{
desc: "GRPCRoute created first",
path: "mixed/with_grpcroute_priority_over_httproute.yml",
acceptedRoute: "grpc-app-priority",
conflictedRoute: "http-app-priority",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
k8sObjects, gwObjects := readResources(t, []string{"services.yml", test.path})
gwClient := newGatewaySimpleClientSet(t, gwObjects...)
client := newClientImpl(kubefake.NewClientset(k8sObjects...), gwClient)
eventCh, err := client.WatchAll(nil, make(chan struct{}))
require.NoError(t, err)
<-eventCh
p := Provider{
EntryPoints: map[string]Entrypoint{"web": {Address: ":9080"}},
client: client,
}
_, statusReport, err := p.loadConfigurationFromGateways(t.Context())
require.NoError(t, err)
statusReport.Flush(t.Context(), client)
getParents := func(name string, isHTTP bool) []gatev1.RouteParentStatus {
t.Helper()
if isHTTP {
route, err := gwClient.GatewayV1().HTTPRoutes("default").Get(t.Context(), name, metav1.GetOptions{})
require.NoError(t, err)
return route.Status.Parents
}
route, err := gwClient.GatewayV1().GRPCRoutes("default").Get(t.Context(), name, metav1.GetOptions{})
require.NoError(t, err)
return route.Status.Parents
}
acceptedParents := getParents(test.acceptedRoute, test.acceptedIsHTTP)
require.Len(t, acceptedParents, 1)
acceptedCondition := findCondition(acceptedParents[0].Conditions, gatev1.RouteConditionAccepted)
require.NotNil(t, acceptedCondition)
assert.Equal(t, metav1.ConditionTrue, acceptedCondition.Status)
assert.Equal(t, string(gatev1.RouteReasonAccepted), acceptedCondition.Reason)
conflictedParents := getParents(test.conflictedRoute, !test.acceptedIsHTTP)
require.Len(t, conflictedParents, 1)
conflictedCondition := findCondition(conflictedParents[0].Conditions, gatev1.RouteConditionAccepted)
require.NotNil(t, conflictedCondition)
assert.Equal(t, metav1.ConditionFalse, conflictedCondition.Status)
assert.Equal(t, string(routeReasonHostnameConflict), conflictedCondition.Reason)
gateway, err := gwClient.GatewayV1().Gateways("default").Get(t.Context(), "my-gateway", metav1.GetOptions{})
require.NoError(t, err)
require.Len(t, gateway.Status.Listeners, 1)
assert.Equal(t, int32(1), gateway.Status.Listeners[0].AttachedRoutes)
})
}
}
func Test_attachedRoutes_conflicts(t *testing.T) {
testCases := []struct {
desc string
attachedKind string
attachedHostnames []gatev1.Hostname
attachedListener listenerRef
kind string
hostnames []gatev1.Hostname
expected bool
}{
{
desc: "same kind",
attachedKind: kindHTTPRoute,
attachedHostnames: []gatev1.Hostname{"foo.com"},
attachedListener: listenerRef{
GatewayNamespace: "default",
GatewayName: "my-gateway",
Name: "web",
},
kind: kindHTTPRoute,
hostnames: []gatev1.Hostname{"foo.com"},
},
{
desc: "another listener",
attachedKind: kindHTTPRoute,
attachedHostnames: []gatev1.Hostname{"foo.com"},
attachedListener: listenerRef{
GatewayNamespace: "default",
GatewayName: "my-gateway",
Name: "websecure",
},
kind: kindGRPCRoute,
hostnames: []gatev1.Hostname{"foo.com"},
},
{
desc: "disjoint hostnames",
attachedKind: kindHTTPRoute,
attachedHostnames: []gatev1.Hostname{"foo.com"},
attachedListener: listenerRef{
GatewayNamespace: "default",
GatewayName: "my-gateway",
Name: "web",
},
kind: kindGRPCRoute,
hostnames: []gatev1.Hostname{"bar.com"},
},
{
desc: "same hostname",
attachedKind: kindHTTPRoute,
attachedHostnames: []gatev1.Hostname{"foo.com"},
attachedListener: listenerRef{
GatewayNamespace: "default",
GatewayName: "my-gateway",
Name: "web",
},
kind: kindGRPCRoute,
hostnames: []gatev1.Hostname{"bar.com", "foo.com"},
expected: true,
},
{
desc: "attached wildcard hostname",
attachedKind: kindGRPCRoute,
attachedHostnames: []gatev1.Hostname{"*.foo.com"},
attachedListener: listenerRef{
GatewayNamespace: "default",
GatewayName: "my-gateway",
Name: "web",
},
kind: kindHTTPRoute,
hostnames: []gatev1.Hostname{"bar.foo.com"},
expected: true,
},
{
desc: "wildcard hostname",
attachedKind: kindGRPCRoute,
attachedHostnames: []gatev1.Hostname{"bar.foo.com"},
attachedListener: listenerRef{
GatewayNamespace: "default",
GatewayName: "my-gateway",
Name: "web",
},
kind: kindHTTPRoute,
hostnames: []gatev1.Hostname{"*.foo.com"},
expected: true,
},
{
desc: "attached without hostname",
attachedKind: kindHTTPRoute,
attachedHostnames: nil,
attachedListener: listenerRef{
GatewayNamespace: "default",
GatewayName: "my-gateway",
Name: "web",
},
kind: kindGRPCRoute,
hostnames: []gatev1.Hostname{"foo.com"},
expected: true,
},
{
desc: "without hostname",
attachedKind: kindHTTPRoute,
attachedHostnames: []gatev1.Hostname{"foo.com"},
attachedListener: listenerRef{
GatewayNamespace: "default",
GatewayName: "my-gateway",
Name: "web",
},
kind: kindGRPCRoute,
hostnames: nil,
expected: true,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
attached := make(attachedRoutes)
attached.Record(test.attachedListener, test.attachedKind, test.attachedHostnames)
listener := listenerRef{GatewayNamespace: "default", GatewayName: "my-gateway", Name: "web"}
assert.Equal(t, test.expected, attached.Conflicts(listener, test.kind, test.hostnames))
})
}
}
// Test_mergeRouteParentStatuses ensures the merge keeps the parentStatuses Traefik
// does not own: those written by another controller, and those written by another
// Traefik instance (our controllerName but a parentRef not targeting a managed
@@ -10091,13 +10461,92 @@ func Test_mergeRouteParentStatuses(t *testing.T) {
}
}
func findCondition(conditions []metav1.Condition, conditionType gatev1.RouteConditionType) *metav1.Condition {
for i := range conditions {
if conditions[i].Type == string(conditionType) {
return &conditions[i]
}
func Test_managesGateway(t *testing.T) {
testCases := []struct {
desc string
gateways []NamespacedName
gw *gatev1.Gateway
want bool
}{
{
desc: "no scoping manages everything",
gw: &gatev1.Gateway{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "foo",
},
},
want: true,
},
{
desc: "matching ref",
gateways: []NamespacedName{{Namespace: "default", Name: "foo"}},
gw: &gatev1.Gateway{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "foo",
},
},
want: true,
},
{
desc: "different name",
gateways: []NamespacedName{{Namespace: "default", Name: "foo"}},
gw: &gatev1.Gateway{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "bar",
},
},
want: false,
},
{
desc: "different namespace",
gateways: []NamespacedName{{Namespace: "default", Name: "foo"}},
gw: &gatev1.Gateway{
ObjectMeta: metav1.ObjectMeta{
Namespace: "other",
Name: "foo",
},
},
want: false,
},
{
desc: "list matches first",
gateways: []NamespacedName{
{Namespace: "default", Name: "foo"},
{Namespace: "other", Name: "bar"},
},
gw: &gatev1.Gateway{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "foo",
},
},
want: true,
},
{
desc: "list matches none",
gateways: []NamespacedName{
{Namespace: "default", Name: "foo"},
{Namespace: "other", Name: "bar"},
},
gw: &gatev1.Gateway{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "baz",
},
},
want: false,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
p := Provider{Gateways: test.gateways}
assert.Equal(t, test.want, p.managesGateway(test.gw))
})
}
return nil
}
func Test_matchListener(t *testing.T) {
@@ -10359,7 +10808,7 @@ func Test_findMatchingHostnames(t *testing.T) {
desc: "Multiple route hostnames with multiple matching route hostnames",
listenerHostname: new(gatev1.Hostname("*.foo.com")),
routeHostnames: []gatev1.Hostname{"toto.foo.com", "test.foo.com", "test.buz.com"},
want: []gatev1.Hostname{"toto.foo.com", "test.foo.com"},
want: []gatev1.Hostname{"test.foo.com", "toto.foo.com"},
wantOk: true,
},
{
@@ -10369,6 +10818,13 @@ func Test_findMatchingHostnames(t *testing.T) {
want: []gatev1.Hostname{"*.bar.foo.com"},
wantOk: true,
},
{
desc: "Duplicate intersections are returned once",
listenerHostname: new(gatev1.Hostname("bar.foo.com")),
routeHostnames: []gatev1.Hostname{"*.foo.com", "bar.foo.com"},
want: []gatev1.Hostname{"bar.foo.com"},
wantOk: true,
},
}
for _, test := range testCases {
@@ -10894,49 +11350,6 @@ func Test_gatewayAddresses(t *testing.T) {
}
}
// We cannot use the gateway-api fake.NewClientset due to Gateway being pluralized as "gatewaies" instead of "gateways".
func newGatewaySimpleClientSet(t *testing.T, objects ...runtime.Object) *gatefake.Clientset {
t.Helper()
client := gatefake.NewSimpleClientset(objects...)
for _, object := range objects {
gateway, ok := object.(*gatev1.Gateway)
if !ok {
continue
}
_, err := client.GatewayV1().Gateways(gateway.Namespace).Create(t.Context(), gateway, metav1.CreateOptions{})
require.NoError(t, err)
}
return client
}
func readResources(t *testing.T, paths []string) ([]runtime.Object, []runtime.Object) {
t.Helper()
var k8sObjects []runtime.Object
var gwObjects []runtime.Object
for _, path := range paths {
yamlContent, err := os.ReadFile(filepath.FromSlash("./fixtures/" + path))
if err != nil {
panic(err)
}
objects := k8s.MustParseYaml(yamlContent)
for _, obj := range objects {
switch obj.GetObjectKind().GroupVersionKind().Group {
case "gateway.networking.k8s.io":
gwObjects = append(gwObjects, obj)
default:
k8sObjects = append(k8sObjects, obj)
}
}
}
return k8sObjects, gwObjects
}
func Test_makeGatewayStatus(t *testing.T) {
acceptedListener := func() gatewayListener {
return gatewayListener{Status: &gatev1.ListenerStatus{Name: "valid"}}
@@ -11271,3 +11684,55 @@ func TestCrossProviderNamespaces_TLSRoute(t *testing.T) {
})
}
}
func findCondition(conditions []metav1.Condition, conditionType gatev1.RouteConditionType) *metav1.Condition {
for i := range conditions {
if conditions[i].Type == string(conditionType) {
return &conditions[i]
}
}
return nil
}
// We cannot use the gateway-api fake.NewClientset due to Gateway being pluralized as "gatewaies" instead of "gateways".
func newGatewaySimpleClientSet(t *testing.T, objects ...runtime.Object) *gatefake.Clientset {
t.Helper()
client := gatefake.NewSimpleClientset(objects...)
for _, object := range objects {
gateway, ok := object.(*gatev1.Gateway)
if !ok {
continue
}
_, err := client.GatewayV1().Gateways(gateway.Namespace).Create(t.Context(), gateway, metav1.CreateOptions{})
require.NoError(t, err)
}
return client
}
func readResources(t *testing.T, paths []string) ([]runtime.Object, []runtime.Object) {
t.Helper()
var k8sObjects []runtime.Object
var gwObjects []runtime.Object
for _, path := range paths {
yamlContent, err := os.ReadFile(filepath.FromSlash("./fixtures/" + path))
if err != nil {
panic(err)
}
objects := k8s.MustParseYaml(yamlContent)
for _, obj := range objects {
switch obj.GetObjectKind().GroupVersionKind().Group {
case "gateway.networking.k8s.io":
gwObjects = append(gwObjects, obj)
default:
k8sObjects = append(k8sObjects, obj)
}
}
}
return k8sObjects, gwObjects
}
+4 -4
View File
@@ -58,9 +58,9 @@ func (p *Provider) loadTCPRoutes(ctx context.Context, gateways []gatewayWithList
}
var resolvedRefCondition *metav1.Condition
for _, listener := range match.listeners {
for _, listener := range match.Listeners {
// A parentRef can target specific listeners through its SectionName or Port.
accepted := matchListener(listener, match.parentRef)
accepted := matchListener(listener, match.ParentRef)
if accepted && !allowRoute(listener, route.Namespace, kindTCPRoute) {
if acceptedCondition.Status == metav1.ConditionFalse {
@@ -75,7 +75,7 @@ func (p *Provider) loadTCPRoutes(ctx context.Context, gateways []gatewayWithList
// The ResolvedRefs condition must be reported for every parentRef,
// even when the route does not attach to the listener.
routeConf, condition := p.loadTCPRoute(match.gatewayName, match.gatewayNamespace, listener, route)
routeConf, condition := p.loadTCPRoute(match.GatewayName, match.GatewayNamespace, listener, route)
if resolvedRefCondition == nil || resolvedRefCondition.Status == metav1.ConditionTrue {
resolvedRefCondition = new(condition)
}
@@ -101,7 +101,7 @@ func (p *Provider) loadTCPRoutes(ctx context.Context, gateways []gatewayWithList
}
statusReport.RecordTCPRouteStatus(ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, gatev1.RouteParentStatus{
ParentRef: match.parentRef,
ParentRef: match.ParentRef,
ControllerName: controllerName,
Conditions: parentStatusConditions,
})
+7 -5
View File
@@ -45,9 +45,9 @@ func (p *Provider) loadTLSRoutes(ctx context.Context, gateways []gatewayWithList
}
var resolvedRefCondition *metav1.Condition
for _, listener := range match.listeners {
for _, listener := range match.Listeners {
// A parentRef can target specific listeners through its SectionName or Port.
accepted := matchListener(listener, match.parentRef)
accepted := matchListener(listener, match.ParentRef)
if accepted && !allowRoute(listener, route.Namespace, kindTLSRoute) {
if acceptedCondition.Status == metav1.ConditionFalse {
@@ -70,7 +70,7 @@ func (p *Provider) loadTLSRoutes(ctx context.Context, gateways []gatewayWithList
// The ResolvedRefs condition must be reported for every parentRef,
// even when the route does not attach to the listener.
routeConf, condition := p.loadTLSRoute(match.gatewayName, match.gatewayNamespace, listener, route, hostnames, statusReport)
routeConf, condition := p.loadTLSRoute(match.GatewayName, match.GatewayNamespace, listener, route, hostnames, statusReport)
if resolvedRefCondition == nil || resolvedRefCondition.Status == metav1.ConditionTrue {
resolvedRefCondition = new(condition)
}
@@ -90,7 +90,7 @@ func (p *Provider) loadTLSRoutes(ctx context.Context, gateways []gatewayWithList
}
statusReport.RecordTLSRouteStatus(ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, gatev1.RouteParentStatus{
ParentRef: match.parentRef,
ParentRef: match.ParentRef,
ControllerName: controllerName,
Conditions: parentStatusConditions,
})
@@ -591,7 +591,9 @@ func hostSNIRule(hostnames []gatev1.Hostname) (string, int) {
continue
}
host = strings.Replace(regexp.QuoteMeta(host), `\*\.`, `[a-z0-9-]+\.`, 1)
// A wildcard label matches one or more labels: *.com matches both
// example.com and www.example.com.
host = strings.Replace(regexp.QuoteMeta(host), `\*\.`, `[a-z0-9-\.]+\.`, 1)
rules = append(rules, fmt.Sprintf("HostSNIRegexp(%q)", fmt.Sprintf("^%s$", host)))
}
@@ -29,7 +29,7 @@ func Test_hostSNIRule(t *testing.T) {
{
desc: "Supported wildcard",
hostnames: []gatev1.Hostname{"*.foo"},
expectedRule: `HostSNIRegexp("^[a-z0-9-]+\\.foo$")`,
expectedRule: `HostSNIRegexp("^[a-z0-9-\\.]+\\.foo$")`,
expectedPriority: 4,
},
{
@@ -53,7 +53,7 @@ func Test_hostSNIRule(t *testing.T) {
{
desc: "Multiple valid hostnames with wildcard",
hostnames: []gatev1.Hostname{"bar.foo", "foo.foo", "*.foo"},
expectedRule: `HostSNI("bar.foo") || HostSNI("foo.foo") || HostSNIRegexp("^[a-z0-9-]+\\.foo$")`,
expectedRule: `HostSNI("bar.foo") || HostSNI("foo.foo") || HostSNIRegexp("^[a-z0-9-\\.]+\\.foo$")`,
expectedPriority: 7,
},
{
+27 -15
View File
@@ -233,9 +233,11 @@ func (p *Provider) build(ctx context.Context, ingressClasses []*netv1.IngressCla
canaryBackendName := provider.Normalize(canaryIngress.Namespace + "-" + pa.Backend.Service.Name + "-" + portString(pa.Backend.Service.Port))
if _, exists := mc.Backends[canaryBackendName]; !exists {
mc.Backends[canaryBackendName] = &backend{
Name: canaryBackendName,
Namespace: canaryIngress.Namespace,
Endpoints: endpoints,
Name: canaryBackendName,
Namespace: canaryIngress.Namespace,
ServiceName: pa.Backend.Service.Name,
ServicePort: portString(pa.Backend.Service.Port),
Endpoints: endpoints,
}
}
@@ -306,9 +308,11 @@ func (p *Provider) build(ctx context.Context, ingressClasses []*netv1.IngressCla
ptBackendName := provider.Normalize(ing.Namespace + "-" + ingBackend.Service.Name + "-" + portString(ingBackend.Service.Port))
if _, exists := mc.Backends[ptBackendName]; !exists {
mc.Backends[ptBackendName] = &backend{
Name: ptBackendName,
Namespace: ing.Namespace,
Endpoints: endpoints,
Name: ptBackendName,
Namespace: ing.Namespace,
ServiceName: ingBackend.Service.Name,
ServicePort: portString(ingBackend.Service.Port),
Endpoints: endpoints,
}
}
@@ -414,9 +418,11 @@ func (p *Provider) build(ctx context.Context, ingressClasses []*netv1.IngressCla
backendName := provider.Normalize(ing.Namespace + "-" + ing.Name + "-" + pa.Backend.Service.Name + "-" + portString(pa.Backend.Service.Port))
if _, exists := mc.Backends[backendName]; !exists {
mc.Backends[backendName] = &backend{
Name: backendName,
Namespace: ing.Namespace,
Endpoints: endpoints,
Name: backendName,
Namespace: ing.Namespace,
ServiceName: pa.Backend.Service.Name,
ServicePort: portString(pa.Backend.Service.Port),
Endpoints: endpoints,
}
}
@@ -486,9 +492,11 @@ func (p *Provider) build(ctx context.Context, ingressClasses []*netv1.IngressCla
ing.config)
if err == nil {
mc.Backends[errBackendName] = &backend{
Name: errBackendName,
Namespace: ing.Namespace,
Endpoints: endpoints,
Name: errBackendName,
Namespace: ing.Namespace,
ServiceName: defaultSvcName,
ServicePort: "0",
Endpoints: endpoints,
}
}
}
@@ -555,6 +563,7 @@ func (p *Provider) build(ctx context.Context, ingressClasses []*netv1.IngressCla
Name: defaultBackendName,
Namespace: ing.Namespace,
ServiceName: db.Service.Name,
ServicePort: portString(db.Service.Port),
Endpoints: endpoints,
}
mc.Backends[defaultBackendName] = bk
@@ -570,9 +579,11 @@ func (p *Provider) build(ctx context.Context, ingressClasses []*netv1.IngressCla
ingDefaultBackendName := provider.Normalize(ing.Namespace + "-" + ing.Name + "-default-backend")
if _, exists := mc.Backends[ingDefaultBackendName]; !exists {
mc.Backends[ingDefaultBackendName] = &backend{
Name: ingDefaultBackendName,
Namespace: ing.Namespace,
Endpoints: endpoints,
Name: ingDefaultBackendName,
Namespace: ing.Namespace,
ServiceName: db.Service.Name,
ServicePort: portString(db.Service.Port),
Endpoints: endpoints,
}
}
@@ -793,6 +804,7 @@ func (p *Provider) resolveBackend(namespace string, ingBackend netv1.IngressBack
Name: name,
Namespace: namespace,
ServiceName: ingBackend.Service.Name,
ServicePort: portString(ingBackend.Service.Port),
Endpoints: endpoints,
}, nil
}
@@ -0,0 +1,28 @@
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-with-cors-wildcard-origin
namespace: default
annotations:
nginx.ingress.kubernetes.io/enable-cors: "true"
nginx.ingress.kubernetes.io/cors-allow-credentials: "true"
nginx.ingress.kubernetes.io/cors-expose-headers: "X-Forwarded-For, X-Forwarded-Host"
nginx.ingress.kubernetes.io/cors-allow-headers: "X-Foo"
nginx.ingress.kubernetes.io/cors-allow-methods: "PUT, GET, POST, OPTIONS"
nginx.ingress.kubernetes.io/cors-allow-origin: "https://*.example.com, https://exact.example.org, https://*evil.com"
nginx.ingress.kubernetes.io/cors-max-age: "42"
spec:
ingressClassName: nginx
rules:
- host: cors-wildcard.localhost
http:
paths:
- path: /
pathType: Exact
backend:
service:
name: whoami
port:
number: 80
@@ -0,0 +1,23 @@
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-with-rewrite-target-absolute-url
namespace: default
annotations:
nginx.ingress.kubernetes.io/use-regex: "true"
nginx.ingress.kubernetes.io/rewrite-target: https://bar.example.org/$1
spec:
ingressClassName: nginx
rules:
- host: rewrite-target-absolute-url.localhost
http:
paths:
- path: /original/(.*)
pathType: ImplementationSpecific
backend:
service:
name: whoami
port:
number: 80
@@ -0,0 +1,23 @@
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-with-rewrite-target-trailing-group
namespace: default
annotations:
nginx.ingress.kubernetes.io/use-regex: "true"
nginx.ingress.kubernetes.io/rewrite-target: /newpath/$1
spec:
ingressClassName: nginx
rules:
- host: rewrite-target-trailing-group.localhost
http:
paths:
- path: /original/(.*)
pathType: ImplementationSpecific
backend:
service:
name: whoami
port:
number: 80
File diff suppressed because it is too large. Load diff
@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"net/http"
"regexp"
"slices"
"strconv"
"strings"
@@ -14,6 +15,7 @@ import (
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/tls"
"github.com/traefik/traefik/v3/pkg/types"
netv1 "k8s.io/api/networking/v1"
"k8s.io/utils/ptr"
)
@@ -256,18 +258,46 @@ func (p *Provider) buildIPAllowList(loc *location) {
}
}
// corsWildcardOriginRegexp is the same cors-allow-origin regexp used in ingress-nginx, with the
// wildcard group made mandatory, so that it only matches the protocol://*.domain[:port] form:
// https://github.com/kubernetes/ingress-nginx/blob/main/internal/ingress/annotations/cors/main.go#L48
var corsWildcardOriginRegexp = regexp.MustCompile(`^[a-z]+://\*\.[A-Za-z0-9\-.]*(:\d+)?$`)
// wildcardOriginRegex converts a single-level wildcard origin (e.g. https://*.example.com)
// into a regular expression, mirroring ingress-nginx semantics where '*' matches exactly
// one DNS label.
func wildcardOriginRegex(origin string) string {
quoted := regexp.QuoteMeta(origin)
return "^(?i)" + strings.Replace(quoted, `\*`, `[A-Za-z0-9-]+`, 1) + "$"
}
func (p *Provider) buildCORS(loc *location) {
if !ptr.Deref(loc.Config.EnableCORS, false) {
return
}
// ingress-nginx supports single-level wildcard origins, which the exact-match origin
// list cannot express; route them to the regex matcher to preserve behavior on migration.
// Values with a wildcard anywhere else are skipped, as ingress-nginx does.
var originList, originRegexList []string
for _, origin := range ptr.Deref(loc.Config.CORSAllowOrigin, []string{"*"}) {
if origin != "*" && strings.Contains(origin, "*") {
if corsWildcardOriginRegexp.MatchString(origin) {
originRegexList = append(originRegexList, wildcardOriginRegex(origin))
}
continue
}
originList = append(originList, origin)
}
loc.CORS = &dynamic.Headers{
AccessControlAllowCredentials: ptr.Deref(loc.Config.EnableCORSAllowCredentials, true),
AccessControlExposeHeaders: ptr.Deref(loc.Config.CORSExposeHeaders, []string{}),
AccessControlAllowHeaders: ptr.Deref(loc.Config.CORSAllowHeaders, []string{"DNT", "Keep-Alive", "User-Agent", "X-Requested-With", "If-Modified-Since", "Cache-Control", "Content-Type", "Range", "Authorization"}),
AccessControlAllowMethods: ptr.Deref(loc.Config.CORSAllowMethods, []string{"GET", "PUT", "POST", "DELETE", "PATCH", "OPTIONS"}),
AccessControlAllowOriginList: ptr.Deref(loc.Config.CORSAllowOrigin, []string{"*"}),
AccessControlMaxAge: new(int64(ptr.Deref(loc.Config.CORSMaxAge, 1728000))),
AccessControlAllowCredentials: ptr.Deref(loc.Config.EnableCORSAllowCredentials, true),
AccessControlExposeHeaders: ptr.Deref(loc.Config.CORSExposeHeaders, []string{}),
AccessControlAllowHeaders: ptr.Deref(loc.Config.CORSAllowHeaders, []string{"DNT", "Keep-Alive", "User-Agent", "X-Requested-With", "If-Modified-Since", "Cache-Control", "Content-Type", "Range", "Authorization"}),
AccessControlAllowMethods: ptr.Deref(loc.Config.CORSAllowMethods, []string{"GET", "PUT", "POST", "DELETE", "PATCH", "OPTIONS"}),
AccessControlAllowOriginList: originList,
AccessControlAllowOriginListRegex: originRegexList,
AccessControlMaxAge: new(int64(ptr.Deref(loc.Config.CORSMaxAge, 1728000))),
}
}
@@ -278,6 +308,9 @@ func (p *Provider) buildRewriteTarget(loc *location) {
}
regex := loc.Path
if ptr.Deref(loc.PathType, netv1.PathTypePrefix) == netv1.PathTypeImplementationSpecific && hasAbsoluteRewriteTarget(loc) {
regex = makeTrailingGroupOptional(loc.Path)
}
xfp := ""
if loc.Config.XForwardedPrefix != nil && regexPathWithCapture.MatchString(*loc.Config.XForwardedPrefix) {
@@ -4,6 +4,8 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
netv1 "k8s.io/api/networking/v1"
)
func TestBuildFromToWwwRedirect(t *testing.T) {
@@ -76,3 +78,100 @@ func TestBuildFromToWwwRedirect(t *testing.T) {
})
}
}
func TestBuildCORS(t *testing.T) {
testCases := []struct {
desc string
allowOrigin string
expectedList []string
expectedRegexList []string
}{
{
desc: "no annotation defaults to any origin",
expectedList: []string{"*"},
},
{
desc: "any origin",
allowOrigin: "*",
expectedList: []string{"*"},
},
{
desc: "exact origin",
allowOrigin: "https://example.com",
expectedList: []string{"https://example.com"},
},
{
desc: "single-level wildcard origin",
allowOrigin: "https://*.example.com",
expectedRegexList: []string{`^(?i)https://[A-Za-z0-9-]+\.example\.com$`},
},
{
desc: "single-level wildcard origin with a port",
allowOrigin: "https://*.example.com:8443",
expectedRegexList: []string{`^(?i)https://[A-Za-z0-9-]+\.example\.com:8443$`},
},
{
desc: "single-level wildcard origin on a subdomain",
allowOrigin: "https://*.foo.example.com",
expectedRegexList: []string{`^(?i)https://[A-Za-z0-9-]+\.foo\.example\.com$`},
},
{
desc: "exact and wildcard origins",
allowOrigin: "https://*.example.com, https://exact.example.org",
expectedList: []string{"https://exact.example.org"},
expectedRegexList: []string{`^(?i)https://[A-Za-z0-9-]+\.example\.com$`},
},
{
desc: "unsupported wildcard not followed by a label separator",
allowOrigin: "https://*example.com",
},
{
desc: "unsupported wildcard in the middle of the host",
allowOrigin: "https://foo.*.example.com",
},
{
desc: "unsupported multiple wildcards",
allowOrigin: "https://*.*.example.com",
},
{
desc: "unsupported wildcard without a scheme",
allowOrigin: "*.example.com",
},
{
desc: "unsupported wildcard with an uppercase scheme",
allowOrigin: "HTTPS://*.example.com",
},
{
desc: "unsupported wildcard with a path",
allowOrigin: "https://*.example.com/foo",
},
{
desc: "unsupported wildcard alongside an exact origin",
allowOrigin: "https://*example.com, https://exact.example.org",
expectedList: []string{"https://exact.example.org"},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
annotations := map[string]string{"nginx.ingress.kubernetes.io/enable-cors": "true"}
if test.allowOrigin != "" {
annotations["nginx.ingress.kubernetes.io/cors-allow-origin"] = test.allowOrigin
}
var ing netv1.Ingress
ing.SetAnnotations(annotations)
loc := &location{Config: parseIngressConfig(&ing)}
var p Provider
p.buildCORS(loc)
require.NotNil(t, loc.CORS)
assert.Equal(t, test.expectedList, loc.CORS.AccessControlAllowOriginList)
assert.Equal(t, test.expectedRegexList, loc.CORS.AccessControlAllowOriginListRegex)
})
}
}
@@ -58,6 +58,9 @@ type backend struct {
// ServiceName is the original Kubernetes service name (without namespace or port).
ServiceName string
// ServicePort is the original port specification (number or name) from the Ingress rule.
ServicePort string
// Endpoints holds the resolved pod addresses.
Endpoints []endpoint
}
@@ -6,6 +6,7 @@ import (
"maps"
"math"
"net/http"
"net/url"
"regexp"
"slices"
"strings"
@@ -52,24 +53,25 @@ func (p *Provider) translate(ctx context.Context, mc *model) *dynamic.Configurat
if mc.DefaultBackend != nil {
obs := &dynamic.RouterObservabilityConfig{
Metadata: &dynamic.ObservabilityMetadata{
Ingress: &dynamic.KubernetesIngressMetadata{
Namespace: mc.DefaultBackend.Namespace,
ServiceName: mc.DefaultBackend.ServiceName,
Ingress: &dynamic.KubernetesMetadata{
Kind: "Ingress",
Namespace: mc.DefaultBackend.Namespace,
},
},
}
var serversTransportName string
if loc := mc.DefaultBackendLocation; loc != nil {
obs.Metadata.Ingress.IngressName = loc.IngressName
obs.Metadata.Ingress.ServicePort = loc.ServicePort
obs.Metadata.Ingress.Name = loc.IngressName
if loc.ServersTransport != nil && loc.ServersTransportName != "" {
serversTransportName = loc.ServersTransportName
conf.HTTP.ServersTransports[loc.ServersTransportName] = loc.ServersTransport
}
}
conf.HTTP.Services[defaultBackendName] = buildService(mc.DefaultBackend, serversTransportName)
defaultBackendSvc := buildService(mc.DefaultBackend, serversTransportName)
defaultBackendSvc.Observability = buildServiceObservability(mc.DefaultBackend)
conf.HTTP.Services[defaultBackendName] = defaultBackendSvc
rt := &dynamic.Router{
EntryPoints: p.NonTLSEntryPoints,
@@ -196,15 +198,16 @@ func (p *Provider) translate(ctx context.Context, mc *model) *dynamic.Configurat
}
primarySvcName := loc.BackendName
conf.HTTP.Services[primarySvcName] = buildServiceWithLocConfig(backend, loc.ServersTransportName, loc.Config)
primarySvc := buildServiceWithLocConfig(backend, loc.ServersTransportName, loc.Config)
primarySvc.Observability = buildServiceObservability(backend)
conf.HTTP.Services[primarySvcName] = primarySvc
obs := &dynamic.RouterObservabilityConfig{
Metadata: &dynamic.ObservabilityMetadata{
Ingress: &dynamic.KubernetesIngressMetadata{
Namespace: loc.Namespace,
IngressName: loc.IngressName,
ServiceName: loc.ServiceName,
ServicePort: loc.ServicePort,
Ingress: &dynamic.KubernetesMetadata{
Kind: "Ingress",
Namespace: loc.Namespace,
Name: loc.IngressName,
},
},
}
@@ -217,6 +220,7 @@ func (p *Provider) translate(ctx context.Context, mc *model) *dynamic.Configurat
canaryWRRName := primarySvcName + "-wrr"
canarySvc := buildServiceWithLocConfig(canaryBackend, loc.ServersTransportName, loc.Config)
canarySvc.Observability = buildServiceObservability(canaryBackend)
conf.HTTP.Services[canarySvcName] = canarySvc
conf.HTTP.Services[canaryWRRName] = &dynamic.Service{
@@ -345,6 +349,21 @@ func (p *Provider) addNonTLSRouter(conf *dynamic.Configuration, key string, rt *
return true
}
func buildServiceObservability(b *backend) *dynamic.ServiceObservabilityConfig {
if b == nil {
return nil
}
return &dynamic.ServiceObservabilityConfig{
Metadata: &dynamic.ServiceObservabilityMetadata{
Kubernetes: &dynamic.KubernetesServiceMetadata{
Namespace: b.Namespace,
Name: b.ServiceName,
Port: b.ServicePort,
},
},
}
}
func buildService(backend *backend, serversTransportName string) *dynamic.Service {
lb := &dynamic.ServersLoadBalancer{}
lb.SetDefaults()
@@ -477,7 +496,9 @@ func (p *Provider) applyMiddlewares(mc *model, loc *location, routerKey string,
if e.ErrorBackendName != "" {
errorSvcName = "default-backend-" + routerKey
if errBackend, ok := mc.Backends[e.ErrorBackendName]; ok {
conf.HTTP.Services[errorSvcName] = buildServiceWithLocConfig(errBackend, "", loc.Config)
errSvc := buildServiceWithLocConfig(errBackend, "", loc.Config)
errSvc.Observability = buildServiceObservability(errBackend)
conf.HTTP.Services[errorSvcName] = errSvc
}
}
headers := http.Header{
@@ -655,8 +676,13 @@ func buildRule(host string, loc *location) string {
if len(loc.Path) > 0 {
pathType := ptr.Deref(loc.PathType, netv1.PathTypePrefix)
regexPath := loc.Path
if pathType == netv1.PathTypeImplementationSpecific {
pathType = netv1.PathTypePrefix
if hasAbsoluteRewriteTarget(loc) {
regexPath = makeTrailingGroupOptional(loc.Path)
}
}
switch pathType {
@@ -664,7 +690,7 @@ func buildRule(host string, loc *location) string {
rules = append(rules, fmt.Sprintf("Path(%q)", loc.Path))
case netv1.PathTypePrefix:
if loc.UseRegex {
rules = append(rules, fmt.Sprintf("PathRegexp(%q)", "(?i)^"+loc.Path))
rules = append(rules, fmt.Sprintf("PathRegexp(%q)", "(?i)^"+regexPath))
} else {
rules = append(rules, buildPrefixRule(loc.Path))
}
@@ -687,3 +713,54 @@ func buildPrefixRule(path string) string {
path = strings.TrimSuffix(path, "/")
return fmt.Sprintf("(Path(%q) || PathPrefix(%q))", path, path+"/")
}
// hasAbsoluteRewriteTarget reports whether the location's rewrite-target
// annotation is an absolute URL (e.g. https://bar.example.org/$1).
// With an absolute rewrite-target, requests that do not match the nginx
// location fall through to the implicit catch-all whose rewrite still issues
// the redirect, so nginx answers 302 for any path on the host. Traefik has no
// such catch-all: widening the route regex with makeTrailingGroupOptional
// approximates it for paths sharing the location prefix. With a non-absolute
// rewrite-target, the catch-all proxies to the default backend (404), so the
// route must not be widened.
func hasAbsoluteRewriteTarget(loc *location) bool {
rewrite := ptr.Deref(loc.Config.RewriteTarget, "")
if rewrite == "" {
return false
}
parsed, err := url.Parse(rewrite)
return err == nil && parsed.Scheme != ""
}
// makeTrailingGroupOptional converts an ImplementationSpecific regex path like /foo/(.*)
// into /foo(?:/(.*))? so it also matches the bare /foo (without trailing slash).
// Only applies when the path starts with a literal prefix (not a capture group),
// and the group opened after the last "/(" closes at the very end of the path:
// otherwise trailing literals (e.g. /foo/(.*)/bar) would become optional too.
func makeTrailingGroupOptional(path string) string {
if strings.HasPrefix(path, "/(") {
return path
}
idx := strings.LastIndex(path, "/(")
if idx < 0 {
return path
}
depth := 0
for i := idx + 1; i < len(path); i++ {
switch path[i] {
case '(':
depth++
case ')':
depth--
if depth == 0 && i != len(path)-1 {
return path
}
}
}
if depth != 0 {
return path
}
return path[:idx] + "(?:" + path[idx:] + ")?"
}
@@ -156,3 +156,78 @@ func TestApplyFromToWwwRedirect(t *testing.T) {
})
}
}
func TestMakeTrailingGroupOptional(t *testing.T) {
testCases := []struct {
desc string
path string
expected string
}{
{
desc: "trailing capture group",
path: "/original/(.*)",
expected: "/original(?:/(.*))?",
},
{
desc: "multiple groups wraps only the last one",
path: "/foo/(bar)/(.*)",
expected: "/foo/(bar)(?:/(.*))?",
},
{
desc: "path starting with a capture group is left unchanged",
path: "/(.*)",
expected: "/(.*)",
},
{
desc: "path starting with a capture group followed by another group is left unchanged",
path: "/(something)(/.+)",
expected: "/(something)(/.+)",
},
{
desc: "documented ingress-nginx rewrite pattern is left unchanged",
path: "/something(/|$)(.*)",
expected: "/something(/|$)(.*)",
},
{
desc: "literal after the group is left unchanged",
path: "/foo/(.*)/bar",
expected: "/foo/(.*)/bar",
},
{
desc: "escaped literal after the group is left unchanged",
path: `/foo/(.*)\.json`,
expected: `/foo/(.*)\.json`,
},
{
desc: "anchor after the group is left unchanged",
path: "/foo/(.*)$",
expected: "/foo/(.*)$",
},
{
desc: "nested groups closing at the end",
path: "/foo/((a|b)/.*)",
expected: "/foo(?:/((a|b)/.*))?",
},
{
desc: "unbalanced group is left unchanged",
path: "/foo/((.*)",
expected: "/foo/((.*)",
},
{
desc: "path without capture group",
path: "/foo",
expected: "/foo",
},
{
desc: "path with trailing slash",
path: "/foo/",
expected: "/foo/",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
assert.Equal(t, test.expected, makeTrailingGroupOptional(test.path))
})
}
}
+20 -11
View File
@@ -343,11 +343,10 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl
Service: "default-backend",
Observability: &dynamic.RouterObservabilityConfig{
Metadata: &dynamic.ObservabilityMetadata{
Ingress: &dynamic.KubernetesIngressMetadata{
Namespace: ingress.Namespace,
IngressName: ingress.Name,
ServiceName: ingress.Spec.DefaultBackend.Service.Name,
ServicePort: portString(ingress.Spec.DefaultBackend.Service.Port),
Ingress: &dynamic.KubernetesMetadata{
Kind: "Ingress",
Namespace: ingress.Namespace,
Name: ingress.Name,
},
},
},
@@ -613,7 +612,18 @@ func (p *Provider) loadService(client Client, namespace string, backend netv1.In
lb := &dynamic.ServersLoadBalancer{}
lb.SetDefaults()
svc := &dynamic.Service{LoadBalancer: lb}
svc := &dynamic.Service{
LoadBalancer: lb,
Observability: &dynamic.ServiceObservabilityConfig{
Metadata: &dynamic.ServiceObservabilityMetadata{
Kubernetes: &dynamic.KubernetesServiceMetadata{
Namespace: namespace,
Name: backend.Service.Name,
Port: portString(backend.Service.Port),
},
},
},
}
svcConfig, err := parseServiceConfig(service.Annotations)
if err != nil {
@@ -764,11 +774,10 @@ func (p *Provider) loadRouter(ingress *netv1.Ingress, rule netv1.IngressRule, pa
Service: serviceName,
Observability: &dynamic.RouterObservabilityConfig{
Metadata: &dynamic.ObservabilityMetadata{
Ingress: &dynamic.KubernetesIngressMetadata{
Namespace: ingress.Namespace,
IngressName: ingress.Name,
ServiceName: pa.Backend.Service.Name,
ServicePort: portString(pa.Backend.Service.Port),
Ingress: &dynamic.KubernetesMetadata{
Kind: "Ingress",
Namespace: ingress.Namespace,
Name: ingress.Name,
},
},
},
File diff suppressed because it is too large. Load diff
+9
View File
@@ -55,6 +55,15 @@ func (o *ObservabilityMgr) BuildEPChain(ctx context.Context, entryPointName stri
return o.observabilityContextHandler(next, internal, config), nil
})
// Seed the per-request service observability state container. The leaf
// service handler mutates it; the access-log middleware reads the final
// state at log emission.
chain = chain.Append(func(next http.Handler) (http.Handler, error) {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
next.ServeHTTP(rw, req.WithContext(observability.WithServiceObservabilityState(req.Context())))
}), nil
})
// Capture middleware for accessLogs or metrics.
if o.shouldAccessLog(internal, config) || o.shouldMeter(internal, config) || o.shouldMeterSemConv(internal, config) {
chain = chain.Append(capture.Wrap)
@@ -15,6 +15,7 @@ import (
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/healthcheck"
"github.com/traefik/traefik/v3/pkg/middlewares/accesslog"
"github.com/traefik/traefik/v3/pkg/middlewares/observability"
"github.com/traefik/traefik/v3/pkg/safe"
)
@@ -110,6 +111,12 @@ func (m *Mirroring) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
// Therefore, we reset any potential datatable key in the new context that we pass around.
ctx := context.WithValue(r.Context(), accesslog.DataTableKey, nil)
// For the same reason, the mirrored services must not publish their metadata
// into the state container seeded for the mirrored request, as the access log
// middleware still reads it to emit the log of the main handler.
// Therefore, we seed a new, throwaway container for the mirrors.
ctx = observability.WithServiceObservabilityState(ctx)
// When a request served by m.handler is successful, req.Context will be canceled,
// which would trigger a cancellation of the ongoing mirrored requests.
// Therefore, we give a new, non-cancellable context to each of the mirrored calls,
+18 -5
View File
@@ -123,7 +123,11 @@ func (m *Manager) BuildHTTP(rootCtx context.Context, serviceName string) (http.H
value := reflect.ValueOf(*conf.Service)
var count int
for i := range value.NumField() {
if value.Type().Field(i).Name != "Middlewares" && !value.Field(i).IsNil() {
name := value.Type().Field(i).Name
if name == "Middlewares" || name == "Observability" {
continue
}
if !value.Field(i).IsNil() {
count++
}
}
@@ -183,22 +187,31 @@ func (m *Manager) BuildHTTP(rootCtx context.Context, serviceName string) (http.H
return nil, sErr
}
// We keep the originalLB and if it is StatusUpdater to allow wrapping to keep the state.
originalLB, isStatusUpdater := lb.(healthcheck.StatusUpdater)
if len(conf.Middlewares) > 0 {
if m.middlewareChainBuilder == nil {
// This should happen only in tests.
return nil, errors.New("chain builder not defined")
}
chain := m.middlewareChainBuilder.BuildMiddlewareChain(ctx, conf.Middlewares)
originalLB := lb
var err error
lb, err = chain.Then(lb)
if err != nil {
conf.AddError(err, true)
return nil, err
}
if su, ok := originalLB.(healthcheck.StatusUpdater); ok {
lb = &statusUpdaterHandler{Handler: lb, statusUpdater: su}
}
}
// Publish the service-level observability metadata to the per-request state
// container when this service is reached. No-op for services without metadata
// (Weighted/Mirroring/etc.), a published value for leaves with a backend
// identity (e.g. the Kubernetes Service name/namespace/port).
lb = observability.NewServiceMetadataHandler(conf.Observability, lb)
if isStatusUpdater {
lb = &statusUpdaterHandler{Handler: lb, statusUpdater: originalLB}
}
m.services[serviceName] = lb
+37 -7
View File
@@ -363,8 +363,38 @@ func (t *TransportManager) createRoundTripper(cfg *dynamic.ServersTransport, tls
}, nil
}
type stickyRoundTripper struct {
RoundTripper http.RoundTripper
// stickyRoundTrippers is keyed by owner, as there is one kerberosRoundTripper per ServersTransport and a dedicated
// round tripper carries the whole transport configuration (TLS client configuration, timeouts, connection pool) of
// the ServersTransport that created it, hence cannot be shared with another one.
// The map is lazily allocated, as most client connections never reach a Kerberos or NTLM backend.
type stickyRoundTrippers struct {
mu sync.Mutex
roundTrippers map[*kerberosRoundTripper]http.RoundTripper
}
func (s *stickyRoundTrippers) get(owner *kerberosRoundTripper) http.RoundTripper {
s.mu.Lock()
defer s.mu.Unlock()
return s.roundTrippers[owner]
}
// stick keeps the round tripper already stored for the given owner, as concurrent requests on the same client
// connection (HTTP/2 streams) can race on the same owner, and replacing an in-use round tripper would orphan the
// backend connections it holds.
func (s *stickyRoundTrippers) stick(owner *kerberosRoundTripper) {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.roundTrippers[owner]; ok {
return
}
if s.roundTrippers == nil {
s.roundTrippers = make(map[*kerberosRoundTripper]http.RoundTripper)
}
s.roundTrippers[owner] = owner.new()
}
type transportKeyType string
@@ -372,7 +402,7 @@ type transportKeyType string
var transportKey transportKeyType = "transport"
func AddTransportOnContext(ctx context.Context) context.Context {
return context.WithValue(ctx, transportKey, &stickyRoundTripper{})
return context.WithValue(ctx, transportKey, &stickyRoundTrippers{})
}
type kerberosRoundTripper struct {
@@ -381,13 +411,13 @@ type kerberosRoundTripper struct {
}
func (k *kerberosRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
value, ok := request.Context().Value(transportKey).(*stickyRoundTripper)
connRoundTrippers, ok := request.Context().Value(transportKey).(*stickyRoundTrippers)
if !ok {
return k.OriginalRoundTripper.RoundTrip(request)
}
if value.RoundTripper != nil {
return value.RoundTripper.RoundTrip(request)
if roundTripper := connRoundTrippers.get(k); roundTripper != nil {
return roundTripper.RoundTrip(request)
}
resp, err := k.OriginalRoundTripper.RoundTrip(request)
@@ -396,7 +426,7 @@ func (k *kerberosRoundTripper) RoundTrip(request *http.Request) (*http.Response,
// We put a dedicated roundTripper in the ConnContext.
// This will stick the next calls to the same connection with the backend.
if err == nil && containsNTLMorNegotiate(resp.Header.Values("WWW-Authenticate")) {
value.RoundTripper = k.new()
connRoundTrippers.stick(k)
}
return resp, err
}
+114
View File
@@ -12,6 +12,7 @@ import (
"net/http"
"net/http/httptest"
"net/url"
"sync"
"sync/atomic"
"testing"
"time"
@@ -721,6 +722,119 @@ func TestKerberosRoundTripper(t *testing.T) {
}
}
func TestKerberosRoundTripperDoesNotLeakAcrossServersTransports(t *testing.T) {
ntlmSrv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.Header().Set("WWW-Authenticate", "NTLM")
rw.WriteHeader(http.StatusUnauthorized)
}))
t.Cleanup(ntlmSrv.Close)
// The httptest certificate is not signed by the root CA pinned below.
untrustedSrv := httptest.NewTLSServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusOK)
}))
t.Cleanup(untrustedSrv.Close)
transportManager := NewTransportManager(nil)
transportManager.Update(map[string]*dynamic.ServersTransport{
"insecure": {InsecureSkipVerify: true},
"pinned": {
ServerName: "example.com",
RootCAs: []types.FileOrContent{types.FileOrContent(localhostCert)},
},
})
insecureRT, err := transportManager.GetRoundTripper("insecure")
require.NoError(t, err)
pinnedRT, err := transportManager.GetRoundTripper("pinned")
require.NoError(t, err)
// Every request below carries this context, that is they all belong to the same client connection.
ctx := AddTransportOnContext(t.Context())
var certErr x509.UnknownAuthorityError
// As long as nothing is stuck on the connection, the pinned ServersTransport enforces its own root CA.
req, err := http.NewRequestWithContext(ctx, http.MethodGet, untrustedSrv.URL, http.NoBody)
require.NoError(t, err)
_, err = pinnedRT.RoundTrip(req)
require.ErrorAs(t, err, &certErr)
// The NTLM challenge sticks a dedicated round tripper for the insecure ServersTransport on this connection.
req, err = http.NewRequestWithContext(ctx, http.MethodGet, ntlmSrv.URL, http.NoBody)
require.NoError(t, err)
resp, err := insecureRT.RoundTrip(req)
require.NoError(t, err)
t.Cleanup(func() { _ = resp.Body.Close() })
require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
// The pinned ServersTransport must not be served by the round tripper stuck for the insecure one.
req, err = http.NewRequestWithContext(ctx, http.MethodGet, untrustedSrv.URL, http.NoBody)
require.NoError(t, err)
_, err = pinnedRT.RoundTrip(req)
assert.ErrorAs(t, err, &certErr)
}
func TestStickyRoundTrippersStickOnlyOnce(t *testing.T) {
var newCalls int
owner := &kerberosRoundTripper{
new: func() http.RoundTripper {
newCalls++
return http.DefaultTransport
},
}
connRoundTrippers := &stickyRoundTrippers{}
connRoundTrippers.stick(owner)
connRoundTrippers.stick(owner)
assert.Equal(t, 1, newCalls)
assert.NotNil(t, connRoundTrippers.get(owner))
}
func TestKerberosRoundTripperSticksOnceOnConcurrentRequests(t *testing.T) {
var newCalls atomic.Int32
rt := kerberosRoundTripper{
new: func() http.RoundTripper {
newCalls.Add(1)
return roundTripperFn(func(req *http.Request) (*http.Response, error) {
return &http.Response{StatusCode: http.StatusOK}, nil
})
},
OriginalRoundTripper: roundTripperFn(func(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusUnauthorized,
Header: map[string][]string{"Www-Authenticate": {"NTLM"}},
}, nil
}),
}
ctx := AddTransportOnContext(t.Context())
var wg sync.WaitGroup
for range 8 {
wg.Go(func() {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://127.0.0.1", http.NoBody)
assert.NoError(t, err)
_, err = rt.RoundTrip(req)
assert.NoError(t, err)
})
}
wg.Wait()
assert.EqualValues(t, 1, newCalls.Load())
}
func TestPeerCertSANs(t *testing.T) {
testCases := []struct {
desc string
+6 -3
View File
@@ -4,20 +4,23 @@ set -e -o pipefail
PROJECT_MODULE="github.com/traefik/traefik"
MODULE_VERSION="v3"
KUBE_VERSION=v0.35.2
KUBE_VERSION=v0.36.3
CURRENT_DIR="$(pwd)"
go install "k8s.io/code-generator/cmd/deepcopy-gen@${KUBE_VERSION}"
go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.19.0
go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.21.0
CODEGEN_PKG="$(go env GOPATH)/pkg/mod/k8s.io/code-generator@${KUBE_VERSION}"
# shellcheck disable=SC1091 # Cannot check source of this file
source "${CODEGEN_PKG}/kube_codegen.sh"
echo "# Generating Traefik clientset and deepcopy code ..."
# Scoped to pkg/ because the generator scans the input directory with a plain
# recursive grep: pointing it at the repository root makes it pick up unrelated
# Go trees living in the working directory (git worktrees, local plugins, ...).
kube::codegen::gen_helpers \
--boilerplate "$(dirname "${BASH_SOURCE[0]}")/boilerplate.go.tmpl" \
"${CURRENT_DIR}"
"${CURRENT_DIR}/pkg"
kube::codegen::gen_client \
--with-applyconfig \
+1 -1
View File
@@ -1 +1 @@
24.11.0
24.21.0
+3 -2
View File
@@ -1,4 +1,5 @@
FROM node:24-alpine3.22
# syntax=docker/dockerfile:1
FROM node:24-alpine3.24
ENV WEBUI_DIR=/src/webui
RUN mkdir -p $WEBUI_DIR
@@ -10,7 +11,7 @@ ENV VITE_APP_BASE_API_URL="/api"
WORKDIR $WEBUI_DIR
RUN corepack enable
RUN npm i -g corepack
RUN yarn workspaces focus --all --production
COPY . $WEBUI_DIR/
Loaded 100 of 147 files, more files were not shown because too many files have changed in this diff. Show more