Compare commits

..

3 Commits

38 changed files with 824 additions and 2071 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ on:
jobs:
sync:
runs-on: ubuntu-latest
timeout-minutes: 30
timeout-minutes: 15
permissions:
packages: write
contents: read
-6
View File
@@ -93,12 +93,6 @@ For a complete list of supported annotations and behavioral differences, see the
The Kubernetes Ingress NGINX provider requires **Traefik v3.6.2 or later**.
!!! info "Legacy Scheme Headers"
If your applications still depend on ingress-nginx's legacy `X-Forwarded-Scheme` or `X-Scheme` headers,
enable `entryPoints.<name>.forwardedHeaders.addXForwardedSchemeHeaders=true` on the entrypoints that receive this traffic.
This keeps `X-Forwarded-Proto` unchanged and restores the compatibility headers at the entrypoint level for every provider.
---
## Prerequisites
@@ -85,7 +85,6 @@ THIS FILE MUST NOT BE EDITED BY HAND
| <a id="opt-entrypoints-name-address" href="#opt-entrypoints-name-address" title="#opt-entrypoints-name-address">entrypoints._name_.address</a> | Entry point address. | |
| <a id="opt-entrypoints-name-allowacmebypass" href="#opt-entrypoints-name-allowacmebypass" title="#opt-entrypoints-name-allowacmebypass">entrypoints._name_.allowacmebypass</a> | Enables handling of ACME TLS and HTTP challenges with custom routers. | false |
| <a id="opt-entrypoints-name-asdefault" href="#opt-entrypoints-name-asdefault" title="#opt-entrypoints-name-asdefault">entrypoints._name_.asdefault</a> | Adds this EntryPoint to the list of default EntryPoints to be used on routers that don't have any Entrypoint defined. | false |
| <a id="opt-entrypoints-name-forwardedheaders-addxforwardedschemeheaders" href="#opt-entrypoints-name-forwardedheaders-addxforwardedschemeheaders" title="#opt-entrypoints-name-forwardedheaders-addxforwardedschemeheaders">entrypoints._name_.forwardedheaders.addxforwardedschemeheaders</a> | Add the X-Forwarded-Scheme and X-Scheme headers. | false |
| <a id="opt-entrypoints-name-forwardedheaders-connection" href="#opt-entrypoints-name-forwardedheaders-connection" title="#opt-entrypoints-name-forwardedheaders-connection">entrypoints._name_.forwardedheaders.connection</a> | List of Connection headers that are allowed to pass through the middleware chain before being removed. | |
| <a id="opt-entrypoints-name-forwardedheaders-insecure" href="#opt-entrypoints-name-forwardedheaders-insecure" title="#opt-entrypoints-name-forwardedheaders-insecure">entrypoints._name_.forwardedheaders.insecure</a> | Trust all forwarded headers. | false |
| <a id="opt-entrypoints-name-forwardedheaders-notappendxforwardedfor" href="#opt-entrypoints-name-forwardedheaders-notappendxforwardedfor" title="#opt-entrypoints-name-forwardedheaders-notappendxforwardedfor">entrypoints._name_.forwardedheaders.notappendxforwardedfor</a> | Disable appending RemoteAddr to X-Forwarded-For header. Defaults to false (appending is enabled). | false |
@@ -396,7 +395,6 @@ THIS FILE MUST NOT BE EDITED BY HAND
| <a id="opt-providers-kubernetesingress-labelselector" href="#opt-providers-kubernetesingress-labelselector" title="#opt-providers-kubernetesingress-labelselector">providers.kubernetesingress.labelselector</a> | Kubernetes Ingress label selector to use. | |
| <a id="opt-providers-kubernetesingress-namespaces" href="#opt-providers-kubernetesingress-namespaces" title="#opt-providers-kubernetesingress-namespaces">providers.kubernetesingress.namespaces</a> | Kubernetes namespaces. | |
| <a id="opt-providers-kubernetesingress-nativelbbydefault" href="#opt-providers-kubernetesingress-nativelbbydefault" title="#opt-providers-kubernetesingress-nativelbbydefault">providers.kubernetesingress.nativelbbydefault</a> | Defines whether to use Native Kubernetes load-balancing mode by default. | false |
| <a id="opt-providers-kubernetesingress-reportnodeinternalips" href="#opt-providers-kubernetesingress-reportnodeinternalips" title="#opt-providers-kubernetesingress-reportnodeinternalips">providers.kubernetesingress.reportnodeinternalips</a> | Report node internal IPs in Ingress status. | false |
| <a id="opt-providers-kubernetesingress-strictprefixmatching" href="#opt-providers-kubernetesingress-strictprefixmatching" title="#opt-providers-kubernetesingress-strictprefixmatching">providers.kubernetesingress.strictprefixmatching</a> | Make prefix matching strictly comply with the Kubernetes Ingress specification (path-element-wise matching instead of character-by-character string matching). | false |
| <a id="opt-providers-kubernetesingress-throttleduration" href="#opt-providers-kubernetesingress-throttleduration" title="#opt-providers-kubernetesingress-throttleduration">providers.kubernetesingress.throttleduration</a> | Ingress refresh throttle duration | 0 |
| <a id="opt-providers-kubernetesingress-token" href="#opt-providers-kubernetesingress-token" title="#opt-providers-kubernetesingress-token">providers.kubernetesingress.token</a> | Kubernetes bearer token (not needed for in-cluster client). It accepts either a token value or a file path to the token. | |
@@ -89,7 +89,6 @@ additionalArguments:
| <a id="opt-asDefault" href="#opt-asDefault" title="#opt-asDefault">`asDefault`</a> | Mark the `entryPoint` to be in the list of default `entryPoints`.<br /> `entryPoints`in this list are used (by default) on HTTP and TCP routers that do not define their own `entryPoints` option.<br /> More information [here](#asdefault). | false | No |
| <a id="opt-allowACMEByPass" href="#opt-allowACMEByPass" title="#opt-allowACMEByPass">`allowACMEByPass`</a> | Enables handling of ACME TLS and HTTP challenges with custom routers instead of the internal ACME router. | false | No |
| <a id="opt-forwardedHeaders-connection" href="#opt-forwardedHeaders-connection" title="#opt-forwardedHeaders-connection">`forwardedHeaders.`<br />`connection`</a> | List of Connection headers that are allowed to pass through the middleware chain before being removed. | false | No |
| <a id="opt-forwardedHeaders-addXForwardedSchemeHeaders" href="#opt-forwardedHeaders-addXForwardedSchemeHeaders" title="#opt-forwardedHeaders-addXForwardedSchemeHeaders">`forwardedHeaders.`<br />`addXForwardedSchemeHeaders`</a> | Add the compatibility headers `X-Forwarded-Scheme` and `X-Scheme`. | false | No |
| <a id="opt-forwardedHeaders-insecure" href="#opt-forwardedHeaders-insecure" title="#opt-forwardedHeaders-insecure">`forwardedHeaders.`<br />`insecure`</a> | Set the insecure mode to always trust the forwarded headers information (`X-Forwarded-*`).<br />We recommend to use this option only for tests purposes, not in production. | false | No |
| <a id="opt-forwardedHeaders-trustedIPs" href="#opt-forwardedHeaders-trustedIPs" title="#opt-forwardedHeaders-trustedIPs">`forwardedHeaders.`<br />`trustedIPs`</a> | Set the IPs or CIDR from where Traefik trusts the forwarded headers information (`X-Forwarded-*`). | - | No |
| <a id="opt-forwardedHeaders-notAppendXForwardedFor" href="#opt-forwardedHeaders-notAppendXForwardedFor" title="#opt-forwardedHeaders-notAppendXForwardedFor">`forwardedHeaders.`<br />`notAppendXForwardedFor`</a> | When set to `true`, Traefik will not append the client's `RemoteAddr` to the `X-Forwarded-For` header. The existing header is preserved as-is. If no `X-Forwarded-For` header exists, none will be added. | false | No |
@@ -393,37 +392,6 @@ You can configure Traefik to trust the forwarded headers information (`X-Forward
--entryPoints.web.forwardedHeaders.connection=foobar
```
??? info "`forwardedHeaders.addXForwardedSchemeHeaders`"
Add the compatibility headers `X-Forwarded-Scheme` and `X-Scheme` next to `X-Forwarded-Proto`.
This is primarily useful when migrating from ingress-nginx and your applications still rely on these legacy headers.
When enabled, these compatibility headers follow the same value as `X-Forwarded-Proto`.
```yaml tab="File (YAML)"
## Static configuration
entryPoints:
websecure:
address: ":443"
forwardedHeaders:
addXForwardedSchemeHeaders: true
```
```toml tab="File (TOML)"
## Static configuration
[entryPoints]
[entryPoints.websecure]
address = ":443"
[entryPoints.websecure.forwardedHeaders]
addXForwardedSchemeHeaders = true
```
```bash tab="CLI"
## Static configuration
--entryPoints.websecure.address=:443
--entryPoints.websecure.forwardedHeaders.addXForwardedSchemeHeaders=true
```
### HTTP3
As HTTP/3 actually uses UDP, when Traefik is configured with a TCP `entryPoint`
@@ -58,13 +58,12 @@ which in turn creates the resulting routers, services, handlers, etc.
| <a id="opt-providers-kubernetesIngress-ingressEndpoint-hostname" href="#opt-providers-kubernetesIngress-ingressEndpoint-hostname" title="#opt-providers-kubernetesIngress-ingressEndpoint-hostname">`providers.kubernetesIngress.`<br />`ingressEndpoint.hostname`</a> | Hostname used for Kubernetes Ingress endpoints. | "" | No |
| <a id="opt-providers-kubernetesIngress-ingressEndpoint-ip" href="#opt-providers-kubernetesIngress-ingressEndpoint-ip" title="#opt-providers-kubernetesIngress-ingressEndpoint-ip">`providers.kubernetesIngress.`<br />`ingressEndpoint.ip`</a> | This IP will get copied to the Ingress `status.loadbalancer.ip`, and currently only supports one IP value (IPv4 or IPv6). | "" | No |
| <a id="opt-providers-kubernetesIngress-ingressEndpoint-publishedService" href="#opt-providers-kubernetesIngress-ingressEndpoint-publishedService" title="#opt-providers-kubernetesIngress-ingressEndpoint-publishedService">`providers.kubernetesIngress.`<br />`ingressEndpoint.publishedService`</a> | The Kubernetes service to copy status from.<br />More information [here](#ingressendpointpublishedservice). | "" | No |
| <a id="opt-providers-kubernetesIngress-reportNodeInternalIPs" href="#opt-providers-kubernetesIngress-reportNodeInternalIPs" title="#opt-providers-kubernetesIngress-reportNodeInternalIPs">`providers.kubernetesIngress.reportNodeInternalIPs`</a> | Report node internal IPs in Ingress status.<br />Incompatible with `ingressEndpoint` and `disableClusterScopeResources`.<br />More information [here](#reportnodeinternalips). | false | No |
| <a id="opt-providers-kubernetesIngress-throttleDuration" href="#opt-providers-kubernetesIngress-throttleDuration" title="#opt-providers-kubernetesIngress-throttleDuration">`providers.kubernetesIngress.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-kubernetesIngress-allowEmptyServices" href="#opt-providers-kubernetesIngress-allowEmptyServices" title="#opt-providers-kubernetesIngress-allowEmptyServices">`providers.kubernetesIngress.allowEmptyServices`</a> | Allows creating a route to reach a service that has no endpoint available.<br />It allows Traefik to handle the requests and responses targeting this service (applying middleware or observability operations) before returning a `503` HTTP Status. | false | No |
| <a id="opt-providers-kubernetesIngress-allowExternalNameServices" href="#opt-providers-kubernetesIngress-allowExternalNameServices" title="#opt-providers-kubernetesIngress-allowExternalNameServices">`providers.kubernetesIngress.allowExternalNameServices`</a> | Allows the `Ingress` to reference ExternalName services. | false | No |
| <a id="opt-providers-kubernetesIngress-crossProviderNamespaces" href="#opt-providers-kubernetesIngress-crossProviderNamespaces" title="#opt-providers-kubernetesIngress-crossProviderNamespaces">`providers.kubernetesIngress.crossProviderNamespaces`</a> | List of namespaces from which Ingresses or Services are allowed to use `traefik.ingress.kubernetes.io/router.middlewares`, `traefik.ingress.kubernetes.io/router.tls.options`, or `traefik.ingress.kubernetes.io/service.serverstransport` annotations.<br />When unset, all namespaces are allowed. When set to `[]`, every cross-provider reference is rejected. | [] | No |
| <a id="opt-providers-kubernetesIngress-nativeLBByDefault" href="#opt-providers-kubernetesIngress-nativeLBByDefault" title="#opt-providers-kubernetesIngress-nativeLBByDefault">`providers.kubernetesIngress.nativeLBByDefault`</a> | Allow using the Kubernetes Service load balancing between the pods instead of the one provided by Traefik for every `Ingress` by default.<br />It can be overridden in the [`Service`](../../../../reference/routing-configuration/kubernetes/crd/http/service.md#opt-nativeLB) | false | No |
| <a id="opt-providers-kubernetesIngress-disableClusterScopeResources" href="#opt-providers-kubernetesIngress-disableClusterScopeResources" title="#opt-providers-kubernetesIngress-disableClusterScopeResources">`providers.kubernetesIngress.disableClusterScopeResources`</a> | Prevent from discovering cluster scope resources (`IngressClass` and `Nodes`).<br />By doing so, it alleviates the requirement of giving Traefik the rights to look up for cluster resources.<br />Furthermore, Traefik will not handle Ingresses with IngressClass references, therefore such Ingresses will be ignored (please note that annotations are not affected by this option).<br />This will also prevent from using the `NodePortLB` options on services and is incompatible with `reportNodeInternalIPs`. | false | No |
| <a id="opt-providers-kubernetesIngress-disableClusterScopeResources" href="#opt-providers-kubernetesIngress-disableClusterScopeResources" title="#opt-providers-kubernetesIngress-disableClusterScopeResources">`providers.kubernetesIngress.disableClusterScopeResources`</a> | Prevent from discovering cluster scope resources (`IngressClass` and `Nodes`).<br />By doing so, it alleviates the requirement of giving Traefik the rights to look up for cluster resources.<br />Furthermore, Traefik will not handle Ingresses with IngressClass references, therefore such Ingresses will be ignored (please note that annotations are not affected by this option).<br />This will also prevent from using the `NodePortLB` options on services. | false | No |
| <a id="opt-providers-kubernetesIngress-strictPrefixMatching" href="#opt-providers-kubernetesIngress-strictPrefixMatching" title="#opt-providers-kubernetesIngress-strictPrefixMatching">`providers.kubernetesIngress.strictPrefixMatching`</a> | Make prefix matching strictly comply with the Kubernetes Ingress specification (path-element-wise matching instead of character-by-character string matching). For example, a PathPrefix of `/foo` will match `/foo`, `/foo/`, and `/foo/bar` but not `/foobar`. | false | No |
<!-- markdownlint-enable MD013 -->
@@ -139,31 +138,6 @@ providers:
--providers.kubernetesingress.ingressendpoint.publishedservice=namespace/foo-service
```
### `reportNodeInternalIPs`
When set to `true`, Traefik reports the internal IPs of all nodes in the cluster into the `status.loadBalancer.ingress` field of each managed Ingress resource.
This is the equivalent of ingress-nginx's `--report-node-internal-ip-address` flag and is the recommended approach for bare-metal Kubernetes deployments where Traefik runs as a DaemonSet without a cloud LoadBalancer or MetalLB.
This option requires cluster-scope access to Node resources and is mutually exclusive with `ingressEndpoint` and `disableClusterScopeResources`.
```yaml tab="File (YAML)"
providers:
kubernetesIngress:
reportNodeInternalIPs: true
# ...
```
```toml tab="File (TOML)"
[providers.kubernetesIngress]
reportNodeInternalIPs = true
# ...
```
```bash tab="CLI"
--providers.kubernetesingress.reportnodeinternalips=true
```
## Routing Configuration
See the dedicated section in [routing](../../../../reference/routing-configuration/kubernetes/ingress.md).
@@ -41,7 +41,6 @@ creating the corresponding routers, services, middlewares, and other components
Important differences in default behaviors:
- **Request buffering**: NGINX enables `proxy-request-buffering` by default, while Traefik requires explicit opt-in via the provider's `proxyRequestBuffering` option.
- **Legacy scheme headers**: If your applications depend on `X-Forwarded-Scheme` or `X-Scheme`, enable `entryPoints.<name>.forwardedHeaders.addXForwardedSchemeHeaders=true` on the relevant entrypoints.
To ensure consistent behavior during migration,
review and configure Traefik's provider-level options to match your current NGINX ConfigMap settings.
@@ -50,7 +50,6 @@
insecure = true
trustedIPs = ["foobar", "foobar"]
connection = ["foobar", "foobar"]
addXForwardedSchemeHeaders = true
[entryPoints.EntryPoint0.http]
middlewares = ["foobar", "foobar"]
encodeQuerySemicolons = true
@@ -61,7 +61,6 @@ entryPoints:
connection:
- foobar
- foobar
addXForwardedSchemeHeaders: true
http:
redirections:
entryPoint:
Generated
+45 -8
View File
@@ -1,16 +1,37 @@
{
"nodes": {
"nixpkgs": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1778036283,
"narHash": "sha256-GW2cEd/cLcVbbCes8iQuoY2qGIeCA7UiaD351hpkXfI=",
"rev": "ed67bc86e84e51d4a88e73c7fd36006dc876476f",
"type": "tarball",
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-26.05pre993032.ed67bc86e84e/nixexprs.tar.xz"
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"type": "tarball",
"url": "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz"
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1772963539,
"narHash": "sha256-9jVDGZnvCckTGdYT53d/EfznygLskyLQXYwJLKMPsZs=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9dcb002ca1690658be4a04645215baea8b95f31d",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-golangci": {
@@ -47,10 +68,26 @@
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"nixpkgs-golangci": "nixpkgs-golangci",
"nixpkgs-kct": "nixpkgs-kct"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
+25 -26
View File
@@ -3,7 +3,7 @@
inputs = {
# Main nixpkgs (used for gnused)
nixpkgs.url = "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
# Pinned nixpkgs for kubernetes-controller-tools
# Search: https://www.nixhub.io/packages/kubernetes-controller-tools
@@ -12,34 +12,33 @@
# Pinned nixpkgs for golangci-lint
# Search: https://www.nixhub.io/packages/golangci-lint
nixpkgs-golangci.url = "github:NixOS/nixpkgs/80d901ec0377e19ac3f7bb8c035201e2e098cc97";
flake-utils.url = "github:numtide/flake-utils";
};
outputs =
{
nixpkgs,
nixpkgs-kct,
nixpkgs-golangci,
...
}:
let
inherit (nixpkgs.lib) genAttrs;
forEachSystem = genAttrs nixpkgs.lib.systems.flakeExposed;
outputs = { self, nixpkgs, nixpkgs-kct, nixpkgs-golangci, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs {
inherit system;
};
pkgsForEach = nixpkgs.legacyPackages;
pkgsKctForEach = nixpkgs-kct.legacyPackages;
pkgsGolangCiForEach = nixpkgs-golangci.legacyPackages;
in
{
devShells = forEachSystem (system: {
default = pkgsForEach.${system}.mkShell {
pkgs-kct = import nixpkgs-kct {
inherit system;
};
pkgs-golangci = import nixpkgs-golangci {
inherit system;
};
in
{
devShells.default = pkgs.mkShell {
packages = [
pkgsForEach.${system}.gnused
pkgsKctForEach.${system}.kubernetes-controller-tools
pkgsGolangCiForEach.${system}.golangci-lint
pkgs-kct.kubernetes-controller-tools
pkgs.gnused
pkgs-golangci.golangci-lint
];
};
});
formatter = forEachSystem (system: pkgsForEach.${system}.nixfmt);
};
}
}
);
}
@@ -30,11 +30,14 @@ profiles:
result: success
statistics:
Failed: 0
Passed: 20
Passed: 23
Skipped: 0
supportedFeatures:
- BackendTLSPolicy
- GatewayPort8080
- HTTPRoute303RedirectStatusCode
- HTTPRoute307RedirectStatusCode
- HTTPRoute308RedirectStatusCode
- HTTPRouteBackendProtocolH2C
- HTTPRouteBackendProtocolWebSocket
- HTTPRouteBackendRequestHeaderModification
@@ -57,9 +60,6 @@ profiles:
- GatewayHTTPSListenerDetectMisdirectedRequests
- GatewayInfrastructurePropagation
- GatewayStaticAddresses
- HTTPRoute303RedirectStatusCode
- HTTPRoute307RedirectStatusCode
- HTTPRoute308RedirectStatusCode
- HTTPRouteBackendTimeout
- HTTPRouteCORS
- HTTPRouteNamedRouteRule
@@ -100,5 +100,8 @@ profiles:
name: GATEWAY-TLS
summary: Core tests succeeded. Extended tests succeeded.
succeededProvisionalTests:
- HTTPRoute303Redirect
- HTTPRoute307Redirect
- HTTPRoute308Redirect
- TLSRouteMixedTerminationSameNamespace
- TLSRouteTerminateSimpleSameNamespace
+2 -1
View File
@@ -78,7 +78,8 @@ func (s *GatewayAPIConformanceSuite) SetupSuite() {
s.T().Fatal("Traefik image is not present")
}
s.k3sContainer, err = k3s.Run(ctx,
s.k3sContainer, err = k3s.Run(
ctx,
k3sImage,
k3s.WithManifest("./fixtures/gateway-api-conformance/00-experimental-v1.5.1.yml"),
k3s.WithManifest("./fixtures/gateway-api-conformance/01-rbac.yml"),
+52 -13
View File
@@ -120,7 +120,7 @@
"dashboard@internal"
]
},
"default-whoami-http-80@kubernetesgateway": {
"httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-af329269dd38031b03e3-svc-default-whoami-0@kubernetesgateway": {
"loadBalancer": {
"servers": [
{
@@ -146,7 +146,7 @@
"weighted": {
"services": [
{
"name": "default-whoami-http-80",
"name": "httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-af329269dd38031b03e3-svc-default-whoami-0",
"weight": 1
}
]
@@ -156,11 +156,33 @@
"httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-af329269dd38031b03e3@kubernetesgateway"
]
},
"httproute-default-http-app-1-gw-default-my-https-gateway-ep-websecure-0-af329269dd38031b03e3-svc-default-whoami-0@kubernetesgateway": {
"loadBalancer": {
"servers": [
{
"url": "http://10.42.0.4:80"
},
{
"url": "http://10.42.0.8:80"
}
],
"strategy": "wrr",
"passHostHeader": true,
"responseForwarding": {
"flushInterval": "100ms"
}
},
"status": "enabled",
"serverStatus": {
"http://10.42.0.4:80": "UP",
"http://10.42.0.8:80": "UP"
}
},
"httproute-default-http-app-1-gw-default-my-https-gateway-ep-websecure-0-af329269dd38031b03e3-wrr@kubernetesgateway": {
"weighted": {
"services": [
{
"name": "default-whoami-http-80",
"name": "httproute-default-http-app-1-gw-default-my-https-gateway-ep-websecure-0-af329269dd38031b03e3-svc-default-whoami-0",
"weight": 1
}
]
@@ -229,7 +251,14 @@
}
},
"tcpServices": {
"default-whoamitcp-8080@kubernetesgateway": {
"deny-unknown-host@kubernetesgateway": {
"loadBalancer": {},
"status": "enabled",
"usedBy": [
"deny-unknown-host@kubernetesgateway"
]
},
"tcproute-default-tcp-app-1-gw-default-my-tcp-gateway-ep-footcp-0-e3b0c44298fc1c149afb-svc-default-whoamitcp-0@kubernetesgateway": {
"loadBalancer": {
"servers": [
{
@@ -246,18 +275,11 @@
"10.42.0.9:8080": "UP"
}
},
"deny-unknown-host@kubernetesgateway": {
"loadBalancer": {},
"status": "enabled",
"usedBy": [
"deny-unknown-host@kubernetesgateway"
]
},
"tcproute-default-tcp-app-1-gw-default-my-tcp-gateway-ep-footcp-0-e3b0c44298fc1c149afb-wrr@kubernetesgateway": {
"weighted": {
"services": [
{
"name": "default-whoamitcp-8080",
"name": "tcproute-default-tcp-app-1-gw-default-my-tcp-gateway-ep-footcp-0-e3b0c44298fc1c149afb-svc-default-whoamitcp-0",
"weight": 1
}
]
@@ -267,11 +289,28 @@
"tcproute-default-tcp-app-1-gw-default-my-tcp-gateway-ep-footcp-0-e3b0c44298fc1c149afb@kubernetesgateway"
]
},
"tlsroute-default-tls-app-1-gw-default-my-tls-gateway-ep-footlspassthrough-0-e3b0c44298fc1c149afb-svc-default-whoamitcp-0@kubernetesgateway": {
"loadBalancer": {
"servers": [
{
"address": "10.42.0.6:8080"
},
{
"address": "10.42.0.9:8080"
}
]
},
"status": "enabled",
"serverStatus": {
"10.42.0.6:8080": "UP",
"10.42.0.9:8080": "UP"
}
},
"tlsroute-default-tls-app-1-gw-default-my-tls-gateway-ep-footlspassthrough-0-e3b0c44298fc1c149afb-wrr@kubernetesgateway": {
"weighted": {
"services": [
{
"name": "default-whoamitcp-8080",
"name": "tlsroute-default-tls-app-1-gw-default-my-tls-gateway-ep-footlspassthrough-0-e3b0c44298fc1c149afb-svc-default-whoamitcp-0",
"weight": 1
}
]
+4 -5
View File
@@ -150,11 +150,10 @@ type TLSConfig struct {
// ForwardedHeaders Trust client forwarding headers.
type ForwardedHeaders struct {
Insecure bool `description:"Trust all forwarded headers." json:"insecure,omitempty" toml:"insecure,omitempty" yaml:"insecure,omitempty" export:"true"`
TrustedIPs []string `description:"Trust only forwarded headers from selected IPs." json:"trustedIPs,omitempty" toml:"trustedIPs,omitempty" yaml:"trustedIPs,omitempty"`
Connection []string `description:"List of Connection headers that are allowed to pass through the middleware chain before being removed." json:"connection,omitempty" toml:"connection,omitempty" yaml:"connection,omitempty"`
NotAppendXForwardedFor bool `description:"Disable appending RemoteAddr to X-Forwarded-For header. Defaults to false (appending is enabled)." json:"notAppendXForwardedFor,omitempty" toml:"notAppendXForwardedFor,omitempty" yaml:"notAppendXForwardedFor,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
AddXForwardedSchemeHeaders bool `description:"Add the X-Forwarded-Scheme and X-Scheme headers." json:"addXForwardedSchemeHeaders,omitempty" toml:"addXForwardedSchemeHeaders,omitempty" yaml:"addXForwardedSchemeHeaders,omitempty" export:"true"`
Insecure bool `description:"Trust all forwarded headers." json:"insecure,omitempty" toml:"insecure,omitempty" yaml:"insecure,omitempty" export:"true"`
TrustedIPs []string `description:"Trust only forwarded headers from selected IPs." json:"trustedIPs,omitempty" toml:"trustedIPs,omitempty" yaml:"trustedIPs,omitempty"`
Connection []string `description:"List of Connection headers that are allowed to pass through the middleware chain before being removed." json:"connection,omitempty" toml:"connection,omitempty" yaml:"connection,omitempty"`
NotAppendXForwardedFor bool `description:"Disable appending RemoteAddr to X-Forwarded-For header. Defaults to false (appending is enabled)." json:"notAppendXForwardedFor,omitempty" toml:"notAppendXForwardedFor,omitempty" yaml:"notAppendXForwardedFor,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
}
// ProxyProtocol contains Proxy-Protocol configuration.
+6
View File
@@ -301,6 +301,12 @@ func (fa *forwardAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
return
}
// Only the operator-listed authResponseHeaders are stripped and replaced with
// the auth server's verified values. Any other header the client sends is
// forwarded to the backend unchanged, mirroring ingress-nginx's
// auth-response-headers semantics. By design, Traefik asserts no identity the
// operator did not opt into: trusting unlisted client headers downstream is a
// backend misconfiguration, not a spoofing flaw here.
for _, headerName := range fa.authResponseHeaders {
headerKey := http.CanonicalHeaderKey(headerName)
req.Header.Del(headerKey)
@@ -18,14 +18,12 @@ const (
XForwardedFor = "X-Forwarded-For"
XForwardedHost = "X-Forwarded-Host"
XForwardedPort = "X-Forwarded-Port"
xForwardedScheme = "X-Forwarded-Scheme"
xForwardedServer = "X-Forwarded-Server"
XForwardedURI = "X-Forwarded-Uri"
XForwardedMethod = "X-Forwarded-Method"
XForwardedPrefix = "X-Forwarded-Prefix"
xForwardedTLSClientCert = "X-Forwarded-Tls-Client-Cert"
xForwardedTLSClientCertInfo = "X-Forwarded-Tls-Client-Cert-Info"
xScheme = "X-Scheme"
xRealIP = "X-Real-Ip"
connection = "Connection"
upgrade = "Upgrade"
@@ -36,7 +34,6 @@ const (
// that Go's HTTP server preserves (e.g. X_Forwarded_Proto).
var XHeadersSet = map[string]struct{}{
XForwardedProto: {},
xForwardedScheme: {},
XForwardedFor: {},
XForwardedHost: {},
XForwardedPort: {},
@@ -46,7 +43,6 @@ var XHeadersSet = map[string]struct{}{
XForwardedPrefix: {},
xForwardedTLSClientCert: {},
xForwardedTLSClientCertInfo: {},
xScheme: {},
xRealIP: {},
}
@@ -74,18 +70,17 @@ func isManagedXHeader(key string) bool {
// Unless insecure is set,
// it first removes all the existing values for those headers if the remote address is not one of the trusted ones.
type XForwarded struct {
insecure bool
trustedIPs []string
connectionHeaders []string
notAppendXForwardedFor bool
addXForwardedSchemeHeaders bool
ipChecker *ip.Checker
next http.Handler
hostname string
insecure bool
trustedIPs []string
connectionHeaders []string
notAppendXForwardedFor bool
ipChecker *ip.Checker
next http.Handler
hostname string
}
// NewXForwarded creates a new XForwarded.
func NewXForwarded(insecure bool, trustedIPs []string, connectionHeaders []string, notAppendXForwardedFor bool, addXForwardedSchemeHeaders bool, next http.Handler) (*XForwarded, error) {
func NewXForwarded(insecure bool, trustedIPs []string, connectionHeaders []string, notAppendXForwardedFor bool, next http.Handler) (*XForwarded, error) {
var ipChecker *ip.Checker
if len(trustedIPs) > 0 {
var err error
@@ -106,14 +101,13 @@ func NewXForwarded(insecure bool, trustedIPs []string, connectionHeaders []strin
}
return &XForwarded{
insecure: insecure,
trustedIPs: trustedIPs,
connectionHeaders: canonicalConnectionHeaders,
notAppendXForwardedFor: notAppendXForwardedFor,
addXForwardedSchemeHeaders: addXForwardedSchemeHeaders,
ipChecker: ipChecker,
next: next,
hostname: hostname,
insecure: insecure,
trustedIPs: trustedIPs,
connectionHeaders: canonicalConnectionHeaders,
notAppendXForwardedFor: notAppendXForwardedFor,
ipChecker: ipChecker,
next: next,
hostname: hostname,
}, nil
}
@@ -174,12 +168,6 @@ func (x *XForwarded) rewrite(outreq *http.Request) {
unsafeHeader(outreq.Header).Set(XForwardedPort, forwardedPort(outreq))
}
if x.addXForwardedSchemeHeaders {
scheme := unsafeHeader(outreq.Header).Get(XForwardedProto)
unsafeHeader(outreq.Header).Set(xForwardedScheme, scheme)
unsafeHeader(outreq.Header).Set(xScheme, scheme)
}
if xfHost := unsafeHeader(outreq.Header).Get(XForwardedHost); xfHost == "" && outreq.Host != "" {
unsafeHeader(outreq.Header).Set(XForwardedHost, outreq.Host)
}
@@ -17,14 +17,12 @@ func TestServeHTTP(t *testing.T) {
insecure bool
trustedIps []string
connectionHeaders []string
addSchemeHeaders bool
incomingHeaders map[string][]string
remoteAddr string
expectedHeaders map[string]string
tls bool
websocket bool
host string
absentHeaders []string
}{
{
desc: "all Empty",
@@ -232,24 +230,6 @@ func TestServeHTTP(t *testing.T) {
XForwardedProto: "https",
},
},
{
desc: "xForwardedScheme headers with tls",
tls: true,
addSchemeHeaders: true,
expectedHeaders: map[string]string{
XForwardedProto: "https",
xForwardedScheme: "https",
xScheme: "https",
},
},
{
desc: "xForwardedScheme headers disabled keeps legacy headers absent",
tls: true,
expectedHeaders: map[string]string{
XForwardedProto: "https",
},
absentHeaders: []string{xForwardedScheme, xScheme},
},
{
desc: "xForwardedProto with websocket",
tls: false,
@@ -258,16 +238,6 @@ func TestServeHTTP(t *testing.T) {
XForwardedProto: "ws",
},
},
{
desc: "xForwardedScheme headers with websocket",
websocket: true,
addSchemeHeaders: true,
expectedHeaders: map[string]string{
XForwardedProto: "ws",
xForwardedScheme: "ws",
xScheme: "ws",
},
},
{
desc: "xForwardedProto with websocket and tls",
tls: true,
@@ -276,17 +246,6 @@ func TestServeHTTP(t *testing.T) {
XForwardedProto: "wss",
},
},
{
desc: "xForwardedScheme headers with websocket and tls",
tls: true,
websocket: true,
addSchemeHeaders: true,
expectedHeaders: map[string]string{
XForwardedProto: "wss",
xForwardedScheme: "wss",
xScheme: "wss",
},
},
{
desc: "xForwardedProto with websocket and tls and already x-forwarded-proto with wss",
tls: true,
@@ -298,21 +257,6 @@ func TestServeHTTP(t *testing.T) {
XForwardedProto: "wss",
},
},
{
desc: "xForwardedScheme headers overwrite in insecure mode",
insecure: true,
addSchemeHeaders: true,
incomingHeaders: map[string][]string{
XForwardedProto: {"https"},
xForwardedScheme: {"external-https"},
xScheme: {"external-https"},
},
expectedHeaders: map[string]string{
XForwardedProto: "https",
xForwardedScheme: "https",
xScheme: "https",
},
},
{
desc: "xForwardedPort with explicit port",
host: "foo.com:8080",
@@ -699,7 +643,7 @@ func TestServeHTTP(t *testing.T) {
}
}
m, err := NewXForwarded(test.insecure, test.trustedIps, test.connectionHeaders, false, test.addSchemeHeaders,
m, err := NewXForwarded(test.insecure, test.trustedIps, test.connectionHeaders, false,
http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {}))
require.NoError(t, err)
@@ -712,10 +656,6 @@ func TestServeHTTP(t *testing.T) {
for k, v := range test.expectedHeaders {
assert.Equal(t, v, req.Header.Get(k))
}
for _, header := range test.absentHeaders {
assert.NotContains(t, req.Header, http.CanonicalHeaderKey(header))
}
})
}
}
@@ -842,7 +782,7 @@ func TestConnection(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
forwarded, err := NewXForwarded(true, nil, test.connectionHeaders, false, false, nil)
forwarded, err := NewXForwarded(true, nil, test.connectionHeaders, false, nil)
require.NoError(t, err)
req := httptest.NewRequest(http.MethodGet, "https://localhost", nil)
@@ -35,7 +35,13 @@ func NewRequestRedirect(ctx context.Context, next http.Handler, conf dynamic.Req
if statusCode == 0 {
statusCode = http.StatusFound
}
if statusCode != http.StatusMovedPermanently && statusCode != http.StatusFound {
// Comply with HTTPRequestRedirectFilter.StatusCode
if statusCode != http.StatusMovedPermanently &&
statusCode != http.StatusFound &&
statusCode != http.StatusSeeOther &&
statusCode != http.StatusTemporaryRedirect &&
statusCode != http.StatusPermanentRedirect {
return nil, fmt.Errorf("unsupported status code: %d", statusCode)
}
@@ -136,6 +136,36 @@ func TestRequestRedirectHandler(t *testing.T) {
wantURL: "http://foo",
wantStatus: http.StatusMovedPermanently,
},
{
desc: "303 See Other",
config: dynamic.RequestRedirect{
Scheme: ptr.To("https"),
StatusCode: http.StatusSeeOther,
},
url: "http://foo",
wantURL: "https://foo",
wantStatus: http.StatusSeeOther,
},
{
desc: "307 Temporary Redirect",
config: dynamic.RequestRedirect{
Scheme: ptr.To("https"),
StatusCode: http.StatusTemporaryRedirect,
},
url: "http://foo",
wantURL: "https://foo",
wantStatus: http.StatusTemporaryRedirect,
},
{
desc: "308 Permanent Redirect",
config: dynamic.RequestRedirect{
Scheme: ptr.To("https"),
StatusCode: http.StatusPermanentRedirect,
},
url: "http://foo",
wantURL: "https://foo",
wantStatus: http.StatusPermanentRedirect,
},
{
desc: "HTTP to HTTPS",
config: dynamic.RequestRedirect{
@@ -201,7 +231,7 @@ func TestRequestRedirectHandler(t *testing.T) {
assert.Equal(t, test.wantStatus, recorder.Code)
switch test.wantStatus {
case http.StatusMovedPermanently, http.StatusFound:
case http.StatusMovedPermanently, http.StatusFound, http.StatusSeeOther, http.StatusTemporaryRedirect, http.StatusPermanentRedirect:
location, err := recorder.Result().Location()
require.NoError(t, err)
@@ -476,7 +476,13 @@ func (s *Snippet) executeForwardAuth(rw http.ResponseWriter, req *http.Request,
return true, nil
}
// Copy auth response headers to the original request for downstream processing
// Only the headers listed in the auth-response-headers annotation are stripped
// and replaced with the auth server's verified values. Any other header the
// client sends is forwarded to the backend unchanged, strictly reproducing
// ingress-nginx, where the annotation is the only switch and is unset in the
// upstream auth-url example. By design this provider asserts no identity the
// operator did not opt into: trusting unlisted client headers downstream is a
// backend misconfiguration, not a spoofing flaw here.
for _, headerName := range s.authResponseHeaders {
headerKey := http.CanonicalHeaderKey(headerName)
req.Header.Del(headerKey)
@@ -574,6 +580,14 @@ func WriteResponse(rw http.ResponseWriter, req *http.Request, ctx *actionContext
_, _ = rw.Write([]byte(ctx.body))
}
// writeHeader builds the auth subrequest headers. It deliberately does not
// strip incoming X-Forwarded-* headers (nor their underscore aliases) and
// exposes no trustForwardHeader knob. Trusting or sanitizing forwarded headers
// is an entrypoint-level concern, handled once by forwardedheaders.XForwarded
// (forwardedHeaders.insecure / trustedIPs) before any middleware runs: untrusted
// sources already have these headers removed upstream. Re-deciding that trust
// per middleware was a mistake in the legacy ForwardAuth path and is not
// replicated here.
func writeHeader(req, forwardReq *http.Request) {
utils.CopyHeaders(forwardReq.Header, req.Header)
@@ -727,6 +727,14 @@ func (c *clientWrapper) UpdateBackendTLSPolicyStatus(ctx context.Context, policy
ancestorStatuses = append(ancestorStatuses, ancestorStatus)
continue
}
// Keep statuses added by Traefik for other ancestors.
// A BackendTLSPolicy can target services attached to different listeners.
if !slices.ContainsFunc(status.Ancestors, func(s gatev1.PolicyAncestorStatus) bool {
return reflect.DeepEqual(s.AncestorRef, ancestorStatus.AncestorRef)
}) {
ancestorStatuses = append(ancestorStatuses, ancestorStatus)
}
}
if len(ancestorStatuses) > 16 {
+4 -1
View File
@@ -44,11 +44,14 @@ func extendedHTTPRouteFeatures() sets.Set[features.Feature] {
return sets.New(
features.HTTPRouteQueryParamMatchingFeature,
features.HTTPRouteMethodMatchingFeature,
features.HTTPRoutePathRedirectFeature,
features.HTTPRoutePortRedirectFeature,
features.HTTPRouteSchemeRedirectFeature,
features.HTTPRoute303RedirectStatusCodeFeature,
features.HTTPRoute307RedirectStatusCodeFeature,
features.HTTPRoute308RedirectStatusCodeFeature,
features.HTTPRouteHostRewriteFeature,
features.HTTPRoutePathRewriteFeature,
features.HTTPRoutePathRedirectFeature,
features.HTTPRouteResponseHeaderModificationFeature,
features.HTTPRouteBackendProtocolH2CFeature,
features.HTTPRouteBackendProtocolWebSocketFeature,
@@ -1,108 +0,0 @@
---
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: # Use GatewayClass defaults for listener definition.
- name: web
protocol: HTTP
port: 80
allowedRoutes:
kinds:
- kind: GRPCRoute
group: gateway.networking.k8s.io
namespaces:
from: Same
---
kind: GRPCRoute
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: grpc-app-1
namespace: default
spec:
parentRefs:
- name: my-gateway
kind: Gateway
group: gateway.networking.k8s.io
hostnames:
- foo.com
rules:
- backendRefs:
- name: whoami
port: 80
weight: 1
---
kind: BackendTLSPolicy
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: backend-tls-policy
namespace: default
spec:
targetRefs:
- group: ""
kind: Service
name: whoami
validation:
hostname: whoami
caCertificateRefs:
- group: ""
kind: ConfigMap
name: ca-file
- group: core
kind: ConfigMap
name: ca-file-2
- group: ""
kind: Secret
name: ca-file
- group: core
kind: Secret
name: ca-file-2
---
apiVersion: v1
kind: ConfigMap
metadata:
name: ca-file
namespace: default
data:
ca.crt: "CA1"
---
apiVersion: v1
kind: ConfigMap
metadata:
name: ca-file-2
namespace: default
data:
ca.crt: "CA2"
---
apiVersion: v1
kind: Secret
metadata:
name: ca-file
namespace: default
data:
ca.crt: Q0ExLXNlY3JldA==
---
apiVersion: v1
kind: Secret
metadata:
name: ca-file-2
namespace: default
data:
ca.crt: Q0EyLXNlY3JldA==
@@ -1,60 +0,0 @@
---
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: # Use GatewayClass defaults for listener definition.
- name: http
protocol: HTTP
port: 80
allowedRoutes:
kinds:
- kind: GRPCRoute
group: gateway.networking.k8s.io
namespaces:
from: Same
---
kind: GRPCRoute
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: grpc-app-1
namespace: default
spec:
parentRefs:
- name: my-gateway
kind: Gateway
group: gateway.networking.k8s.io
hostnames:
- foo.com
rules:
- backendRefs:
- name: whoami
port: 80
weight: 1
---
kind: BackendTLSPolicy
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: backend-tls-policy
namespace: default
spec:
targetRefs:
- group: core
kind: Service
name: whoami
validation:
hostname: whoami
wellKnownCACertificates: System
@@ -1,124 +0,0 @@
---
apiVersion: v1
kind: Secret
metadata:
name: supersecret
namespace: default
data:
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJxRENDQVU2Z0F3SUJBZ0lVWU9zcjBRZ0hPQnE0a1lSQ0w1K1REZFZ0NmJRd0NnWUlLb1pJemowRUF3SXcKRmpFVU1CSUdBMVVFQXd3TFpYaGhiWEJzWlM1amIyMHdIaGNOTWpVeE1ERXdNRGN4TnpNd1doY05NelV4TURBNApNRGN4TnpNd1dqQVdNUlF3RWdZRFZRUUREQXRsZUdGdGNHeGxMbU52YlRCWk1CTUdCeXFHU000OUFnRUdDQ3FHClNNNDlBd0VIQTBJQUJET3JpdzNaUTd3SWhXcmJQUzZKRlFUM2JUb05DRjAwdlNWNWZhYjZUYlh5TDh0bHNHcmUKVFJJRjJFd2dzdGVNT2t4R0tLU2xEdnVhRHdxOHAvcVYrMHVqZWpCNE1CMEdBMVVkRGdRV0JCUk1Fa3VleFhRaApVdERnUmcxS0J2NzJDRHErRXpBZkJnTlZIU01FR0RBV2dCUk1Fa3VleFhRaFV0RGdSZzFLQnY3MkNEcStFekFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUNVR0ExVWRFUVFlTUJ5Q0MyVjRZVzF3YkdVdVkyOXRnZzBxTG1WNFlXMXcKYkdVdVkyOXRNQW9HQ0NxR1NNNDlCQU1DQTBnQU1FVUNJUURzODdWazBzd0E2SGdPSmpST3llMW14RDgzcWNHeQpwZUZnb3hWOTNEeStjd0lnVjBNTUVKSmJWc1R5WkszRVErK1hjNXJFTDc4bnJKK1lJRVYrckNVV2o1VT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ==
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JR0hBZ0VBTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEJHMHdhd0lCQVFRZ253Z0w1RFk0VUIxNHNNNmYKRGlrUWR0cWgyUVcxQXJmRjRmYzFVRnppZmRHaFJBTkNBQVF6cTRzTjJVTzhDSVZxMnowdWlSVUU5MjA2RFFoZApOTDBsZVgybStrMjE4aS9MWmJCcTNrMFNCZGhNSUxMWGpEcE1SaWlrcFE3N21nOEt2S2Y2bGZ0TAotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0t
---
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: # Use GatewayClass defaults for listener definition.
- name: tls
protocol: TLS
port: 9001
hostname: foo.com
tls:
mode: Terminate # Default mode
certificateRefs:
- kind: Secret
name: supersecret
group: ""
allowedRoutes:
kinds:
- kind: TLSRoute
group: gateway.networking.k8s.io
namespaces:
from: Same
---
kind: TLSRoute
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: tls-app-1
namespace: default
spec:
parentRefs:
- name: my-gateway
kind: Gateway
group: gateway.networking.k8s.io
rules:
- backendRefs:
- name: whoami
port: 80
weight: 1
---
kind: BackendTLSPolicy
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: policy-1
namespace: default
spec:
targetRefs:
- group: ""
kind: Service
name: whoami
validation:
hostname: whoami
caCertificateRefs:
- group: ""
kind: ConfigMap
name: ca-file
- group: core
kind: ConfigMap
name: ca-file-2
- group: ""
kind: Secret
name: ca-file
- group: core
kind: Secret
name: ca-file-2
---
apiVersion: v1
kind: ConfigMap
metadata:
name: ca-file
namespace: default
data:
ca.crt: "CA1"
---
apiVersion: v1
kind: ConfigMap
metadata:
name: ca-file-2
namespace: default
data:
ca.crt: "CA2"
---
apiVersion: v1
kind: Secret
metadata:
name: ca-file
namespace: default
data:
ca.crt: Q0ExLXNlY3JldA==
---
apiVersion: v1
kind: Secret
metadata:
name: ca-file-2
namespace: default
data:
ca.crt: Q0EyLXNlY3JldA==
@@ -1,78 +0,0 @@
---
apiVersion: v1
kind: Secret
metadata:
name: supersecret
namespace: default
data:
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJxRENDQVU2Z0F3SUJBZ0lVWU9zcjBRZ0hPQnE0a1lSQ0w1K1REZFZ0NmJRd0NnWUlLb1pJemowRUF3SXcKRmpFVU1CSUdBMVVFQXd3TFpYaGhiWEJzWlM1amIyMHdIaGNOTWpVeE1ERXdNRGN4TnpNd1doY05NelV4TURBNApNRGN4TnpNd1dqQVdNUlF3RWdZRFZRUUREQXRsZUdGdGNHeGxMbU52YlRCWk1CTUdCeXFHU000OUFnRUdDQ3FHClNNNDlBd0VIQTBJQUJET3JpdzNaUTd3SWhXcmJQUzZKRlFUM2JUb05DRjAwdlNWNWZhYjZUYlh5TDh0bHNHcmUKVFJJRjJFd2dzdGVNT2t4R0tLU2xEdnVhRHdxOHAvcVYrMHVqZWpCNE1CMEdBMVVkRGdRV0JCUk1Fa3VleFhRaApVdERnUmcxS0J2NzJDRHErRXpBZkJnTlZIU01FR0RBV2dCUk1Fa3VleFhRaFV0RGdSZzFLQnY3MkNEcStFekFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUNVR0ExVWRFUVFlTUJ5Q0MyVjRZVzF3YkdVdVkyOXRnZzBxTG1WNFlXMXcKYkdVdVkyOXRNQW9HQ0NxR1NNNDlCQU1DQTBnQU1FVUNJUURzODdWazBzd0E2SGdPSmpST3llMW14RDgzcWNHeQpwZUZnb3hWOTNEeStjd0lnVjBNTUVKSmJWc1R5WkszRVErK1hjNXJFTDc4bnJKK1lJRVYrckNVV2o1VT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ==
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JR0hBZ0VBTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEJHMHdhd0lCQVFRZ253Z0w1RFk0VUIxNHNNNmYKRGlrUWR0cWgyUVcxQXJmRjRmYzFVRnppZmRHaFJBTkNBQVF6cTRzTjJVTzhDSVZxMnowdWlSVUU5MjA2RFFoZApOTDBsZVgybStrMjE4aS9MWmJCcTNrMFNCZGhNSUxMWGpEcE1SaWlrcFE3N21nOEt2S2Y2bGZ0TAotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0t
---
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: # Use GatewayClass defaults for listener definition.
- name: tls
protocol: TLS
port: 9001
hostname: foo.com
tls:
mode: Terminate # Default mode
certificateRefs:
- kind: Secret
name: supersecret
group: ""
allowedRoutes:
kinds:
- kind: TLSRoute
group: gateway.networking.k8s.io
namespaces:
from: Same
---
kind: TLSRoute
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: tls-app-1
namespace: default
spec:
parentRefs:
- name: my-gateway
kind: Gateway
group: gateway.networking.k8s.io
rules:
- backendRefs:
- name: whoami
port: 80
weight: 1
kind: Service
group: ""
---
kind: BackendTLSPolicy
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: policy-1
namespace: default
spec:
targetRefs:
- group: core
kind: Service
name: whoami
validation:
hostname: whoami
wellKnownCACertificates: System
+40 -140
View File
@@ -6,7 +6,6 @@ import (
"fmt"
"net"
"regexp"
"slices"
"strconv"
"strings"
@@ -22,7 +21,7 @@ import (
)
// 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, gatewayListeners []gatewayListener, conf *dynamic.Configuration, statusReport *statusReport) {
func (p *Provider) loadGRPCRoutes(ctx context.Context, gatewayListeners []gatewayListener, conf *dynamic.Configuration) {
routes, err := p.client.ListGRPCRoutes()
if err != nil {
log.Ctx(ctx).Error().Err(err).Msg("Unable to list GRPCRoutes")
@@ -40,8 +39,9 @@ func (p *Provider) loadGRPCRoutes(ctx context.Context, gatewayListeners []gatewa
continue
}
var parentStatuses []gatev1.RouteParentStatus
for _, parentRef := range route.Spec.ParentRefs {
parentStatus := gatev1.RouteParentStatus{
parentStatus := &gatev1.RouteParentStatus{
ParentRef: parentRef,
ControllerName: controllerName,
Conditions: []metav1.Condition{
@@ -77,7 +77,7 @@ func (p *Provider) loadGRPCRoutes(ctx context.Context, gatewayListeners []gatewa
}
}
routeConf, resolveRefCondition := p.loadGRPCRoute(logger.WithContext(ctx), listener, route, hostnames, statusReport)
routeConf, resolveRefCondition := p.loadGRPCRoute(logger.WithContext(ctx), listener, route, hostnames)
if accepted && listener.Attached {
mergeHTTPConfiguration(routeConf, conf)
}
@@ -85,12 +85,23 @@ func (p *Provider) loadGRPCRoutes(ctx context.Context, gatewayListeners []gatewa
parentStatus.Conditions = upsertRouteConditionResolvedRefs(parentStatus.Conditions, resolveRefCondition)
}
statusReport.RecordGRPCRouteStatus(ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, parentStatus)
parentStatuses = append(parentStatuses, *parentStatus)
}
status := gatev1.GRPCRouteStatus{
RouteStatus: gatev1.RouteStatus{
Parents: parentStatuses,
},
}
if err := p.client.UpdateGRPCRouteStatus(ctx, ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, status); err != nil {
logger.Warn().
Err(err).
Msg("Unable to update GRPCRoute status")
}
}
}
func (p *Provider) loadGRPCRoute(ctx context.Context, listener gatewayListener, route *gatev1.GRPCRoute, hostnames []gatev1.Hostname, statusReport *statusReport) (*dynamic.Configuration, metav1.Condition) {
func (p *Provider) loadGRPCRoute(ctx context.Context, listener gatewayListener, route *gatev1.GRPCRoute, hostnames []gatev1.Hostname) (*dynamic.Configuration, metav1.Condition) {
conf := &dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Routers: make(map[string]*dynamic.Router),
@@ -157,7 +168,7 @@ func (p *Provider) loadGRPCRoute(ctx context.Context, listener gatewayListener,
default:
var serviceCondition *metav1.Condition
router.Service, serviceCondition = p.loadGRPCService(listener, conf, routerName, routeRule, route, statusReport)
router.Service, serviceCondition = p.loadGRPCService(conf, routerName, routeRule, route)
if serviceCondition != nil {
condition = *serviceCondition
}
@@ -170,7 +181,7 @@ func (p *Provider) loadGRPCRoute(ctx context.Context, listener gatewayListener,
return conf, condition
}
func (p *Provider) loadGRPCService(listener gatewayListener, conf *dynamic.Configuration, routeKey string, routeRule gatev1.GRPCRouteRule, route *gatev1.GRPCRoute, statusReport *statusReport) (string, *metav1.Condition) {
func (p *Provider) loadGRPCService(conf *dynamic.Configuration, routeKey string, routeRule gatev1.GRPCRouteRule, route *gatev1.GRPCRoute) (string, *metav1.Condition) {
name := routeKey + "-wrr"
if _, ok := conf.HTTP.Services[name]; ok {
return name, nil
@@ -178,8 +189,8 @@ func (p *Provider) loadGRPCService(listener gatewayListener, conf *dynamic.Confi
var wrr dynamic.WeightedRoundRobin
var condition *metav1.Condition
for _, backendRef := range routeRule.BackendRefs {
svcName, svc, errCondition := p.loadGRPCBackendRef(listener, conf, route, backendRef, statusReport)
for bi, backendRef := range routeRule.BackendRefs {
svcName, svc, errCondition := p.loadGRPCBackendRef(routeKey, route, bi, backendRef)
weight := ptr.To(int(ptr.Deref(backendRef.Weight, 1)))
if errCondition != nil {
condition = errCondition
@@ -208,7 +219,7 @@ func (p *Provider) loadGRPCService(listener gatewayListener, conf *dynamic.Confi
return name, condition
}
func (p *Provider) loadGRPCBackendRef(listener gatewayListener, conf *dynamic.Configuration, route *gatev1.GRPCRoute, backendRef gatev1.GRPCBackendRef, statusReport *statusReport) (string, *dynamic.Service, *metav1.Condition) {
func (p *Provider) loadGRPCBackendRef(routeKey string, route *gatev1.GRPCRoute, backendIndex int, backendRef gatev1.GRPCBackendRef) (string, *dynamic.Service, *metav1.Condition) {
kind := ptr.Deref(backendRef.Kind, kindService)
group := groupCore
@@ -221,7 +232,7 @@ func (p *Provider) loadGRPCBackendRef(listener gatewayListener, conf *dynamic.Co
namespace = string(*backendRef.Namespace)
}
serviceName := provider.Normalize(namespace + "-" + string(backendRef.Name))
serviceName := fmt.Sprintf("%s-svc-%s-%s-%d", routeKey, namespace, string(backendRef.Name), backendIndex)
if group != groupCore || kind != kindService {
return serviceName, nil, &metav1.Condition{
@@ -257,19 +268,11 @@ func (p *Provider) loadGRPCBackendRef(listener gatewayListener, conf *dynamic.Co
}
}
portStr := strconv.FormatInt(int64(port), 10)
serviceName = provider.Normalize(serviceName + "-" + portStr + "-grpc")
lb, st, errCondition := p.loadGRPCServers(namespace, route, backendRef, listener, statusReport)
lb, errCondition := p.loadGRPCServers(namespace, route, backendRef)
if errCondition != nil {
return serviceName, nil, errCondition
}
if st != nil {
lb.ServersTransport = serviceName
conf.HTTP.ServersTransports[serviceName] = st
}
return serviceName, &dynamic.Service{LoadBalancer: lb}, nil
}
@@ -319,10 +322,10 @@ func (p *Provider) loadGRPCMiddlewares(conf *dynamic.Configuration, namespace, r
return middlewareNames, nil
}
func (p *Provider) loadGRPCServers(namespace string, route *gatev1.GRPCRoute, backendRef gatev1.GRPCBackendRef, listener gatewayListener, statusReport *statusReport) (*dynamic.ServersLoadBalancer, *dynamic.ServersTransport, *metav1.Condition) {
func (p *Provider) loadGRPCServers(namespace string, route *gatev1.GRPCRoute, backendRef gatev1.GRPCBackendRef) (*dynamic.ServersLoadBalancer, *metav1.Condition) {
backendAddresses, svcPort, err := p.getBackendAddresses(namespace, backendRef.BackendRef)
if err != nil {
return nil, nil, &metav1.Condition{
return nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
@@ -332,128 +335,26 @@ func (p *Provider) loadGRPCServers(namespace string, route *gatev1.GRPCRoute, ba
}
}
backendTLSPolicies, err := p.client.ListBackendTLSPoliciesForService(namespace, string(backendRef.Name))
if err != nil {
return nil, nil, &metav1.Condition{
if svcPort.Protocol != corev1.ProtocolTCP {
return nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonRefNotPermitted),
Message: fmt.Sprintf("Cannot list BackendTLSPolicies for Service %s/%s: %s", namespace, string(backendRef.Name), err),
Reason: string(gatev1.RouteReasonUnsupportedProtocol),
Message: fmt.Sprintf("Cannot load GRPCBackendRef %s/%s: only TCP protocol is supported", namespace, backendRef.Name),
}
}
// Sort BackendTLSPolicies by creation timestamp, then by name to match the BackendTLSPolicy requirements.
slices.SortStableFunc(backendTLSPolicies, func(a, b *gatev1.BackendTLSPolicy) int {
cmpTime := a.CreationTimestamp.Time.Compare(b.CreationTimestamp.Time)
if cmpTime == 0 {
return strings.Compare(a.Name, b.Name)
}
return cmpTime
})
var serversTransport *dynamic.ServersTransport
for _, policy := range backendTLSPolicies {
for _, targetRef := range policy.Spec.TargetRefs {
// Skip targetRefs that doesn't match the backendRef,
// since a BackendTLSPolicy can select multiple services.
if targetRef.Name != backendRef.Name {
continue
}
// Skip the targetRef if the sectionName doesn't match the backendRef port.
if targetRef.SectionName != nil && svcPort.Name != string(*targetRef.SectionName) {
continue
}
policyAncestorStatus := gatev1.PolicyAncestorStatus{
AncestorRef: gatev1.ParentReference{
Group: ptr.To(gatev1.Group(groupGateway)),
Kind: ptr.To(gatev1.Kind(kindGateway)),
Namespace: ptr.To(gatev1.Namespace(namespace)),
Name: gatev1.ObjectName(listener.GWName),
SectionName: ptr.To(gatev1.SectionName(listener.Name)),
},
ControllerName: controllerName,
}
// Multiple BackendTLSPolicies can match the same service port, meaning that there is a conflict.
if serversTransport != nil {
policyAncestorStatus.Conditions = append(policyAncestorStatus.Conditions,
metav1.Condition{
Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.BackendTLSPolicyReasonResolvedRefs),
},
metav1.Condition{
Type: string(gatev1.PolicyConditionAccepted),
Status: metav1.ConditionFalse,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.PolicyReasonConflicted),
},
)
statusReport.RecordBackendTLSPolicyStatus(ktypes.NamespacedName{Namespace: policy.Namespace, Name: policy.Name}, policyAncestorStatus)
continue
}
var resolvedRefCondition metav1.Condition
serversTransport, resolvedRefCondition = p.loadServersTransport(namespace, policy)
policyAncestorStatus.Conditions = append(policyAncestorStatus.Conditions, resolvedRefCondition)
if resolvedRefCondition.Status == metav1.ConditionFalse {
policyAncestorStatus.Conditions = append(policyAncestorStatus.Conditions, metav1.Condition{
Type: string(gatev1.PolicyConditionAccepted),
Status: metav1.ConditionFalse,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.BackendTLSPolicyReasonNoValidCACertificate),
})
} else {
policyAncestorStatus.Conditions = append(policyAncestorStatus.Conditions, metav1.Condition{
Type: string(gatev1.PolicyConditionAccepted),
Status: metav1.ConditionTrue,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.PolicyReasonAccepted),
})
}
statusReport.RecordBackendTLSPolicyStatus(ktypes.NamespacedName{Namespace: policy.Namespace, Name: policy.Name}, policyAncestorStatus)
// When something went wrong during the loading of a ServersTransport,
// we stop here and return a route condition error.
if resolvedRefCondition.Status == metav1.ConditionFalse {
return nil, nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonRefNotPermitted),
Message: fmt.Sprintf("Cannot apply BackendTLSPolicy for Service %s/%s: %s", namespace, string(backendRef.Name), resolvedRefCondition.Message),
}
}
}
}
// If a ServersTransport is set, it means a BackendTLSPolicy matched the service port, and we can safely assume the protocol is HTTPS.
// When no ServersTransport is set, we need to determine the protocol based on the service port.
protocol := "https"
if serversTransport == nil {
protocol, err = getGRPCServiceProtocol(svcPort)
if err != nil {
return nil, nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonUnsupportedProtocol),
Message: fmt.Sprintf("Cannot load GRPCBackendRef %s/%s: %s", namespace, backendRef.Name, err),
}
protocol, err := getGRPCServiceProtocol(svcPort)
if err != nil {
return nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonUnsupportedProtocol),
Message: fmt.Sprintf("Cannot load GRPCBackendRef %s/%s: only \"kubernetes.io/h2c\" and \"https\" appProtocol is supported", namespace, backendRef.Name),
}
}
@@ -465,8 +366,7 @@ func (p *Provider) loadGRPCServers(namespace string, route *gatev1.GRPCRoute, ba
URL: fmt.Sprintf("%s://%s", protocol, net.JoinHostPort(ba.IP, strconv.Itoa(int(ba.Port)))),
})
}
return lb, serversTransport, nil
return lb, nil
}
func buildGRPCMatchRule(hostnames []gatev1.Hostname, match gatev1.GRPCRouteMatch) (string, int) {
+44 -24
View File
@@ -23,7 +23,7 @@ import (
gatev1 "sigs.k8s.io/gateway-api/apis/v1"
)
func (p *Provider) loadHTTPRoutes(ctx context.Context, gatewayListeners []gatewayListener, conf *dynamic.Configuration, statusReport *statusReport) {
func (p *Provider) loadHTTPRoutes(ctx context.Context, gatewayListeners []gatewayListener, conf *dynamic.Configuration) {
routes, err := p.client.ListHTTPRoutes()
if err != nil {
log.Ctx(ctx).Error().Err(err).Msg("Unable to list HTTPRoutes")
@@ -41,8 +41,9 @@ func (p *Provider) loadHTTPRoutes(ctx context.Context, gatewayListeners []gatewa
continue
}
var parentStatuses []gatev1.RouteParentStatus
for _, parentRef := range route.Spec.ParentRefs {
parentStatus := gatev1.RouteParentStatus{
parentStatus := &gatev1.RouteParentStatus{
ParentRef: parentRef,
ControllerName: controllerName,
Conditions: []metav1.Condition{
@@ -78,7 +79,7 @@ func (p *Provider) loadHTTPRoutes(ctx context.Context, gatewayListeners []gatewa
}
}
routeConf, resolveRefCondition := p.loadHTTPRoute(logger.WithContext(ctx), listener, route, hostnames, statusReport)
routeConf, resolveRefCondition := p.loadHTTPRoute(logger.WithContext(ctx), listener, route, hostnames)
if accepted && listener.Attached {
mergeHTTPConfiguration(routeConf, conf)
}
@@ -86,12 +87,23 @@ func (p *Provider) loadHTTPRoutes(ctx context.Context, gatewayListeners []gatewa
parentStatus.Conditions = upsertRouteConditionResolvedRefs(parentStatus.Conditions, resolveRefCondition)
}
statusReport.RecordHTTPRouteStatus(ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, parentStatus)
parentStatuses = append(parentStatuses, *parentStatus)
}
status := gatev1.HTTPRouteStatus{
RouteStatus: gatev1.RouteStatus{
Parents: parentStatuses,
},
}
if err := p.client.UpdateHTTPRouteStatus(ctx, ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, status); err != nil {
logger.Warn().
Err(err).
Msg("Unable to update HTTPRoute status")
}
}
}
func (p *Provider) loadHTTPRoute(ctx context.Context, listener gatewayListener, route *gatev1.HTTPRoute, hostnames []gatev1.Hostname, statusReport *statusReport) (*dynamic.Configuration, metav1.Condition) {
func (p *Provider) loadHTTPRoute(ctx context.Context, listener gatewayListener, route *gatev1.HTTPRoute, hostnames []gatev1.Hostname) (*dynamic.Configuration, metav1.Condition) {
conf := &dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Routers: make(map[string]*dynamic.Router),
@@ -164,7 +176,7 @@ func (p *Provider) loadHTTPRoute(ctx context.Context, listener gatewayListener,
default:
var serviceCondition *metav1.Condition
router.Service, serviceCondition = p.loadWRRService(ctx, listener, conf, routerName, routeRule, route, match.Path, statusReport)
router.Service, serviceCondition = p.loadWRRService(ctx, listener, conf, routerName, routeRule, route, match.Path)
if serviceCondition != nil {
condition = *serviceCondition
}
@@ -179,7 +191,7 @@ func (p *Provider) loadHTTPRoute(ctx context.Context, listener gatewayListener,
return conf, condition
}
func (p *Provider) loadWRRService(ctx context.Context, listener gatewayListener, conf *dynamic.Configuration, routeKey string, routeRule gatev1.HTTPRouteRule, route *gatev1.HTTPRoute, pathMatch *gatev1.HTTPPathMatch, statusReport *statusReport) (string, *metav1.Condition) {
func (p *Provider) loadWRRService(ctx context.Context, listener gatewayListener, conf *dynamic.Configuration, routeKey string, routeRule gatev1.HTTPRouteRule, route *gatev1.HTTPRoute, pathMatch *gatev1.HTTPPathMatch) (string, *metav1.Condition) {
name := routeKey + "-wrr"
if _, ok := conf.HTTP.Services[name]; ok {
return name, nil
@@ -187,10 +199,10 @@ func (p *Provider) loadWRRService(ctx context.Context, listener gatewayListener,
var wrr dynamic.WeightedRoundRobin
var condition *metav1.Condition
for _, backendRef := range routeRule.BackendRefs {
for bi, backendRef := range routeRule.BackendRefs {
// TODO in loadService we need to always return a non-nil serviceName even when there is an error which is not the
// usual defacto.
svcName, errCondition := p.loadService(listener, conf, route, backendRef, pathMatch, statusReport)
svcName, errCondition := p.loadService(ctx, listener, conf, routeKey, route, bi, backendRef, pathMatch)
weight := ptr.To(int(ptr.Deref(backendRef.Weight, 1)))
if errCondition != nil {
log.Ctx(ctx).Error().
@@ -217,7 +229,7 @@ func (p *Provider) loadWRRService(ctx context.Context, listener gatewayListener,
// loadService returns a dynamic.Service config corresponding to the given gatev1.HTTPBackendRef.
// Note that the returned dynamic.Service config can be nil (for cross-provider, internal services, and backendFunc).
func (p *Provider) loadService(listener gatewayListener, conf *dynamic.Configuration, route *gatev1.HTTPRoute, backendRef gatev1.HTTPBackendRef, pathMatch *gatev1.HTTPPathMatch, statusReport *statusReport) (string, *metav1.Condition) {
func (p *Provider) loadService(ctx context.Context, listener gatewayListener, conf *dynamic.Configuration, routeKey string, route *gatev1.HTTPRoute, backendIndex int, backendRef gatev1.HTTPBackendRef, pathMatch *gatev1.HTTPPathMatch) (string, *metav1.Condition) {
kind := ptr.Deref(backendRef.Kind, kindService)
group := groupCore
@@ -230,7 +242,8 @@ func (p *Provider) loadService(listener gatewayListener, conf *dynamic.Configura
namespace = string(*backendRef.Namespace)
if strings.Contains(string(backendRef.Name), "@") {
return provider.Normalize(namespace + "-" + string(backendRef.Name) + "-http"), &metav1.Condition{
svcKey := fmt.Sprintf("%s-svc-%s-%s-%d", routeKey, namespace, string(backendRef.Name), backendIndex)
return provider.Normalize(svcKey), &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
@@ -241,7 +254,7 @@ func (p *Provider) loadService(listener gatewayListener, conf *dynamic.Configura
}
}
serviceName := provider.Normalize(namespace + "-" + string(backendRef.Name) + "-http")
serviceName := fmt.Sprintf("%s-svc-%s-%s-%d", routeKey, namespace, string(backendRef.Name), backendIndex)
if err := p.isReferenceGranted(kindHTTPRoute, route.Namespace, group, string(kind), string(backendRef.Name), namespace); err != nil {
return serviceName, &metav1.Condition{
@@ -300,10 +313,7 @@ func (p *Provider) loadService(listener gatewayListener, conf *dynamic.Configura
}
}
portStr := strconv.FormatInt(int64(port), 10)
serviceName = provider.Normalize(serviceName + "-" + portStr)
lb, st, errCondition := p.loadHTTPServers(namespace, route, backendRef, listener, statusReport)
lb, st, errCondition := p.loadHTTPServers(ctx, namespace, route, backendRef, listener)
if errCondition != nil {
return serviceName, errCondition
}
@@ -340,7 +350,7 @@ func (p *Provider) loadHTTPBackendRef(namespace string, backendRef gatev1.HTTPBa
return backendFunc(string(backendRef.Name), namespace)
}
func (p *Provider) loadMiddlewares(conf *dynamic.Configuration, namespace, routerName string, filters []gatev1.HTTPRouteFilter, pathMatch *gatev1.HTTPPathMatch) ([]string, error) {
func (p *Provider) loadMiddlewares(conf *dynamic.Configuration, namespace, parentName string, filters []gatev1.HTTPRouteFilter, pathMatch *gatev1.HTTPPathMatch) ([]string, error) {
type namedMiddleware struct {
Name string
Config *dynamic.Middleware
@@ -353,7 +363,7 @@ func (p *Provider) loadMiddlewares(conf *dynamic.Configuration, namespace, route
var middlewares []namedMiddleware
for i, filter := range filters {
name := fmt.Sprintf("%s-%s-%d", routerName, strings.ToLower(string(filter.Type)), i)
name := fmt.Sprintf("%s-%s-%d", parentName, strings.ToLower(string(filter.Type)), i)
switch filter.Type {
case gatev1.HTTPRouteFilterRequestRedirect:
@@ -430,7 +440,7 @@ func (p *Provider) loadHTTPRouteFilterExtensionRef(namespace string, extensionRe
return filterFunc(string(extensionRef.Name), namespace)
}
func (p *Provider) loadHTTPServers(namespace string, route *gatev1.HTTPRoute, backendRef gatev1.HTTPBackendRef, listener gatewayListener, statusReport *statusReport) (*dynamic.ServersLoadBalancer, *dynamic.ServersTransport, *metav1.Condition) {
func (p *Provider) loadHTTPServers(ctx context.Context, namespace string, route *gatev1.HTTPRoute, backendRef gatev1.HTTPBackendRef, listener gatewayListener) (*dynamic.ServersLoadBalancer, *dynamic.ServersTransport, *metav1.Condition) {
backendAddresses, svcPort, err := p.getBackendAddresses(namespace, backendRef.BackendRef)
if err != nil {
return nil, nil, &metav1.Condition{
@@ -507,7 +517,12 @@ func (p *Provider) loadHTTPServers(namespace string, route *gatev1.HTTPRoute, ba
},
)
statusReport.RecordBackendTLSPolicyStatus(ktypes.NamespacedName{Namespace: policy.Namespace, Name: policy.Name}, policyAncestorStatus)
status := gatev1.PolicyStatus{
Ancestors: []gatev1.PolicyAncestorStatus{policyAncestorStatus},
}
if err := p.client.UpdateBackendTLSPolicyStatus(ctx, ktypes.NamespacedName{Namespace: policy.Namespace, Name: policy.Name}, status); err != nil {
log.Ctx(ctx).Warn().Err(err).Msg("Unable to update conflicting BackendTLSPolicy status")
}
continue
}
@@ -534,7 +549,12 @@ func (p *Provider) loadHTTPServers(namespace string, route *gatev1.HTTPRoute, ba
})
}
statusReport.RecordBackendTLSPolicyStatus(ktypes.NamespacedName{Namespace: policy.Namespace, Name: policy.Name}, policyAncestorStatus)
status := gatev1.PolicyStatus{
Ancestors: []gatev1.PolicyAncestorStatus{policyAncestorStatus},
}
if err := p.client.UpdateBackendTLSPolicyStatus(ctx, ktypes.NamespacedName{Namespace: policy.Namespace, Name: policy.Name}, status); err != nil {
log.Ctx(ctx).Warn().Err(err).Msg("Unable to update BackendTLSPolicy status")
}
// When something wen wrong during the loading of a ServersTransport,
// we stop here and return a route condition error.
@@ -595,7 +615,7 @@ func (p *Provider) loadServersTransport(namespace string, policy *gatev1.Backend
}
for _, caCertRef := range policy.Spec.Validation.CACertificateRefs {
if (caCertRef.Group != "" && caCertRef.Group != groupCore) || (caCertRef.Kind != kindConfigMap && caCertRef.Kind != kindSecret) {
if (caCertRef.Group != "" && caCertRef.Group != groupCore) || (caCertRef.Kind != "ConfigMap" && caCertRef.Kind != "Secret") {
return nil, metav1.Condition{
Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs),
Status: metav1.ConditionFalse,
@@ -608,7 +628,7 @@ func (p *Provider) loadServersTransport(namespace string, policy *gatev1.Backend
var caCRT string
switch caCertRef.Kind {
case kindConfigMap:
case "ConfigMap":
configmap, err := p.client.GetConfigMap(namespace, string(caCertRef.Name))
if err != nil {
return nil, metav1.Condition{
@@ -621,7 +641,7 @@ func (p *Provider) loadServersTransport(namespace string, policy *gatev1.Backend
}
}
caCRT = configmap.Data["ca.crt"]
case kindSecret:
case "Secret":
secret, err := p.client.GetSecret(namespace, string(caCertRef.Name))
if err != nil {
return nil, metav1.Condition{
+37 -37
View File
@@ -49,8 +49,6 @@ const (
kindTCPRoute = "TCPRoute"
kindTLSRoute = "TLSRoute"
kindService = "Service"
kindConfigMap = "ConfigMap"
kindSecret = "Secret"
appProtocolHTTP = "http"
appProtocolHTTPS = "https"
@@ -224,29 +222,20 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
// Note that event is the *first* event that came in during this throttling interval -- if we're hitting our throttle, we may have dropped events.
// This is fine, because we don't treat different event types differently.
// But if we do in the future, we'll need to track more information about the dropped events.
conf, statusReport, err := p.loadConfigurationFromGateways(ctxLog)
if err != nil {
logger.Error().Err(err).Msg("Unable to load configuration from Gateways")
} else {
confHash, err := hashstructure.Hash(conf, nil)
switch {
case err != nil:
logger.Error().Msg("Unable to hash the configuration")
case p.lastConfiguration.Get() == confHash:
logger.Debug().Msgf("Skipping Kubernetes event kind %T", event)
default:
p.lastConfiguration.Set(confHash)
configurationChan <- dynamic.Message{
ProviderName: ProviderName,
Configuration: conf,
}
}
conf := p.loadConfigurationFromGateways(ctxLog)
// Flush regardless of whether the dynamic configuration changed: the
// statusReport is independent of confHash and may carry writes even
// when the data plane has nothing new to consume (e.g. a GatewayClass
// that's now Accepted but has no Gateway pointing at it yet).
statusReport.Flush(ctxLog, p.client)
confHash, err := hashstructure.Hash(conf, nil)
switch {
case err != nil:
logger.Error().Msg("Unable to hash the configuration")
case p.lastConfiguration.Get() == confHash:
logger.Debug().Msgf("Skipping Kubernetes event kind %T", event)
default:
p.lastConfiguration.Set(confHash)
configurationChan <- dynamic.Message{
ProviderName: ProviderName,
Configuration: conf,
}
}
// If we're throttling,
@@ -313,8 +302,7 @@ func (p *Provider) newK8sClient(ctx context.Context) (*clientWrapper, error) {
}
// TODO Handle errors and update resources statuses (gatewayClass, gateway).
func (p *Provider) loadConfigurationFromGateways(ctx context.Context) (*dynamic.Configuration, *statusReport, error) {
statusReport := newStatusReport()
func (p *Provider) loadConfigurationFromGateways(ctx context.Context) *dynamic.Configuration {
conf := &dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{},
@@ -337,12 +325,14 @@ func (p *Provider) loadConfigurationFromGateways(ctx context.Context) (*dynamic.
addresses, err := p.gatewayAddresses()
if err != nil {
return nil, nil, fmt.Errorf("getting gateway addresses: %w", err)
log.Ctx(ctx).Error().Err(err).Msg("Unable to get Gateway status addresses")
return nil
}
gatewayClasses, err := p.client.ListGatewayClasses()
if err != nil {
return nil, nil, fmt.Errorf("listing gateway classes: %w", err)
log.Ctx(ctx).Error().Err(err).Msg("Unable to list GatewayClasses")
return nil
}
var supportedFeatures []gatev1.SupportedFeature
@@ -373,7 +363,13 @@ func (p *Provider) loadConfigurationFromGateways(ctx context.Context) (*dynamic.
SupportedFeatures: supportedFeatures,
}
statusReport.RecordGatewayClassStatus(gatewayClass.Name, status)
if err := p.client.UpdateGatewayClassStatus(ctx, gatewayClass.Name, status); err != nil {
log.Ctx(ctx).
Warn().
Err(err).
Str("gateway_class", gatewayClass.Name).
Msg("Unable to update GatewayClass status")
}
}
var gateways []*gatev1.Gateway
@@ -394,14 +390,14 @@ func (p *Provider) loadConfigurationFromGateways(ctx context.Context) (*dynamic.
gatewayListeners = append(gatewayListeners, p.loadGatewayListeners(logger.WithContext(ctx), gateway, conf)...)
}
p.loadHTTPRoutes(ctx, gatewayListeners, conf, statusReport)
p.loadHTTPRoutes(ctx, gatewayListeners, conf)
p.loadGRPCRoutes(ctx, gatewayListeners, conf, statusReport)
p.loadGRPCRoutes(ctx, gatewayListeners, conf)
p.loadTLSRoutes(ctx, gatewayListeners, conf, statusReport)
p.loadTLSRoutes(ctx, gatewayListeners, conf)
if p.ExperimentalChannel {
p.loadTCPRoutes(ctx, gatewayListeners, conf, statusReport)
p.loadTCPRoutes(ctx, gatewayListeners, conf)
}
for _, gateway := range gateways {
@@ -432,10 +428,14 @@ func (p *Provider) loadConfigurationFromGateways(ctx context.Context) (*dynamic.
Msg("Gateway Not Accepted")
}
statusReport.RecordGatewayStatus(ktypes.NamespacedName{Name: gateway.Name, Namespace: gateway.Namespace}, gatewayStatus)
if err = p.client.UpdateGatewayStatus(ctx, ktypes.NamespacedName{Name: gateway.Name, Namespace: gateway.Namespace}, gatewayStatus); err != nil {
logger.Warn().
Err(err).
Msg("Unable to update Gateway status")
}
}
return conf, statusReport, nil
return conf
}
func (p *Provider) loadGatewayListeners(ctx context.Context, gateway *gatev1.Gateway, conf *dynamic.Configuration) []gatewayListener {
@@ -591,7 +591,7 @@ func (p *Provider) loadGatewayListeners(ctx context.Context, gateway *gatev1.Gat
var errCertConditions []metav1.Condition
listenerTLSCerts := make(map[string]*tls.CertAndStores)
for _, certificateRef := range listener.TLS.CertificateRefs {
if certificateRef.Kind == nil || *certificateRef.Kind != kindSecret || certificateRef.Group == nil || (*certificateRef.Group != "" && *certificateRef.Group != groupCore) {
if certificateRef.Kind == nil || *certificateRef.Kind != "Secret" || certificateRef.Group == nil || (*certificateRef.Group != "" && *certificateRef.Group != groupCore) {
errCertConditions = append(errCertConditions, metav1.Condition{
Type: string(gatev1.ListenerConditionResolvedRefs),
Status: metav1.ConditionFalse,
@@ -604,7 +604,7 @@ func (p *Provider) loadGatewayListeners(ctx context.Context, gateway *gatev1.Gat
}
certificateNamespace := string(ptr.Deref(certificateRef.Namespace, gatev1.Namespace(gateway.Namespace)))
if err := p.isReferenceGranted(kindGateway, gateway.Namespace, groupCore, kindSecret, string(certificateRef.Name), certificateNamespace); err != nil {
if err := p.isReferenceGranted(kindGateway, gateway.Namespace, groupCore, "Secret", string(certificateRef.Name), certificateNamespace); err != nil {
errCertConditions = append(errCertConditions, metav1.Condition{
Type: string(gatev1.ListenerConditionResolvedRefs),
Status: metav1.ConditionFalse,
File diff suppressed because it is too large Load Diff
-136
View File
@@ -1,136 +0,0 @@
package gateway
import (
"context"
"reflect"
"github.com/rs/zerolog/log"
ktypes "k8s.io/apimachinery/pkg/types"
gatev1 "sigs.k8s.io/gateway-api/apis/v1"
gatev1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
)
// statusReport collects the status writes produced by a single rebuild so they
// can be flushed to the apiserver after the dynamic configuration has been published.
type statusReport struct {
gatewayClasses map[string]gatev1.GatewayClassStatus
gateways map[ktypes.NamespacedName]gatev1.GatewayStatus
httpRoutes map[ktypes.NamespacedName]gatev1.RouteStatus
grpcRoutes map[ktypes.NamespacedName]gatev1.RouteStatus
tcpRoutes map[ktypes.NamespacedName]gatev1.RouteStatus
tlsRoutes map[ktypes.NamespacedName]gatev1.RouteStatus
backendTLSPolicies map[ktypes.NamespacedName]gatev1.PolicyStatus
}
func newStatusReport() *statusReport {
return &statusReport{
gatewayClasses: map[string]gatev1.GatewayClassStatus{},
gateways: map[ktypes.NamespacedName]gatev1.GatewayStatus{},
httpRoutes: map[ktypes.NamespacedName]gatev1.RouteStatus{},
grpcRoutes: map[ktypes.NamespacedName]gatev1.RouteStatus{},
tcpRoutes: map[ktypes.NamespacedName]gatev1.RouteStatus{},
tlsRoutes: map[ktypes.NamespacedName]gatev1.RouteStatus{},
backendTLSPolicies: map[ktypes.NamespacedName]gatev1.PolicyStatus{},
}
}
// Flush sends every status write collected during the
// routing configuration build to the Kubernetes API server.
func (r *statusReport) Flush(ctx context.Context, client *clientWrapper) {
logger := log.Ctx(ctx)
for name, status := range r.gatewayClasses {
if err := client.UpdateGatewayClassStatus(ctx, name, status); err != nil {
logger.Warn().Err(err).Str("gateway_class", name).Msg("Unable to update GatewayClass status")
}
}
for name, status := range r.gateways {
if err := client.UpdateGatewayStatus(ctx, name, status); err != nil {
logger.Warn().Err(err).Str("gateway", name.Name).Str("namespace", name.Namespace).Msg("Unable to update Gateway status")
}
}
for name, routeStatus := range r.httpRoutes {
status := gatev1.HTTPRouteStatus{RouteStatus: routeStatus}
if err := client.UpdateHTTPRouteStatus(ctx, name, status); err != nil {
logger.Warn().Err(err).Str("http_route", name.Name).Str("namespace", name.Namespace).Msg("Unable to update HTTPRoute status")
}
}
for name, routeStatus := range r.grpcRoutes {
status := gatev1.GRPCRouteStatus{RouteStatus: routeStatus}
if err := client.UpdateGRPCRouteStatus(ctx, name, status); err != nil {
logger.Warn().Err(err).Str("grpc_route", name.Name).Str("namespace", name.Namespace).Msg("Unable to update GRPCRoute status")
}
}
for name, routeStatus := range r.tcpRoutes {
status := gatev1alpha2.TCPRouteStatus{RouteStatus: routeStatus}
if err := client.UpdateTCPRouteStatus(ctx, name, status); err != nil {
logger.Warn().Err(err).Str("tcp_route", name.Name).Str("namespace", name.Namespace).Msg("Unable to update TCPRoute status")
}
}
for name, routeStatus := range r.tlsRoutes {
status := gatev1.TLSRouteStatus{RouteStatus: routeStatus}
if err := client.UpdateTLSRouteStatus(ctx, name, status); err != nil {
logger.Warn().Err(err).Str("tls_route", name.Name).Str("namespace", name.Namespace).Msg("Unable to update TLSRoute status")
}
}
for name, policyStatus := range r.backendTLSPolicies {
if err := client.UpdateBackendTLSPolicyStatus(ctx, name, policyStatus); err != nil {
logger.Warn().Err(err).Str("backend_tls_policy", name.Name).Str("namespace", name.Namespace).Msg("Unable to update BackendTLSPolicy status")
}
}
}
func (r *statusReport) RecordGatewayClassStatus(gatewayClassName string, status gatev1.GatewayClassStatus) {
r.gatewayClasses[gatewayClassName] = status
}
func (r *statusReport) RecordGatewayStatus(gateway ktypes.NamespacedName, status gatev1.GatewayStatus) {
r.gateways[gateway] = status
}
func (r *statusReport) RecordHTTPRouteStatus(route ktypes.NamespacedName, status gatev1.RouteParentStatus) {
r.httpRoutes[route] = gatev1.RouteStatus{
Parents: append(r.httpRoutes[route].Parents, status),
}
}
func (r *statusReport) RecordGRPCRouteStatus(route ktypes.NamespacedName, status gatev1.RouteParentStatus) {
r.grpcRoutes[route] = gatev1.RouteStatus{
Parents: append(r.grpcRoutes[route].Parents, status),
}
}
func (r *statusReport) RecordTCPRouteStatus(route ktypes.NamespacedName, status gatev1.RouteParentStatus) {
r.tcpRoutes[route] = gatev1.RouteStatus{
Parents: append(r.tcpRoutes[route].Parents, status),
}
}
func (r *statusReport) RecordTLSRouteStatus(route ktypes.NamespacedName, status gatev1.RouteParentStatus) {
r.tlsRoutes[route] = gatev1.RouteStatus{
Parents: append(r.tlsRoutes[route].Parents, status),
}
}
func (r *statusReport) RecordBackendTLSPolicyStatus(policy ktypes.NamespacedName, status gatev1.PolicyAncestorStatus) {
var ancestors []gatev1.PolicyAncestorStatus
// Keep existing ancestor statuses, except if it matches the status to merge.
for _, existing := range r.backendTLSPolicies[policy].Ancestors {
if reflect.DeepEqual(existing.AncestorRef, status.AncestorRef) {
continue
}
ancestors = append(ancestors, existing)
}
r.backendTLSPolicies[policy] = gatev1.PolicyStatus{
Ancestors: append(ancestors, status), // Add the new status to the existing ancestors statuses.
}
}
@@ -1,152 +0,0 @@
package gateway
import (
"testing"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
ktypes "k8s.io/apimachinery/pkg/types"
gatev1 "sigs.k8s.io/gateway-api/apis/v1"
)
func TestStatusReport_RecordGatewayClassStatus(t *testing.T) {
report := newStatusReport()
accepted := gatev1.GatewayClassStatus{
Conditions: []metav1.Condition{{Type: string(gatev1.GatewayClassConditionStatusAccepted)}},
}
report.RecordGatewayClassStatus("traefik", accepted)
assert.Equal(t, accepted, report.gatewayClasses["traefik"])
// A later record for the same GatewayClass overwrites the previous one.
unsupported := gatev1.GatewayClassStatus{
Conditions: []metav1.Condition{{Type: string(gatev1.GatewayClassReasonUnsupportedVersion)}},
}
report.RecordGatewayClassStatus("traefik", unsupported)
assert.Equal(t, unsupported, report.gatewayClasses["traefik"])
}
func TestStatusReport_RecordGatewayStatus(t *testing.T) {
report := newStatusReport()
gateway := ktypes.NamespacedName{Namespace: "default", Name: "my-gateway"}
accepted := gatev1.GatewayStatus{
Conditions: []metav1.Condition{{Type: string(gatev1.GatewayConditionAccepted)}},
}
report.RecordGatewayStatus(gateway, accepted)
assert.Equal(t, accepted, report.gateways[gateway])
// A later record for the same Gateway overwrites the previous one.
programmed := gatev1.GatewayStatus{
Conditions: []metav1.Condition{{Type: string(gatev1.GatewayConditionProgrammed)}},
}
report.RecordGatewayStatus(gateway, programmed)
assert.Equal(t, programmed, report.gateways[gateway])
}
func TestStatusReport_RecordHTTPRouteStatus(t *testing.T) {
report := newStatusReport()
route := ktypes.NamespacedName{Namespace: "default", Name: "my-route"}
gatewayParent := gatev1.RouteParentStatus{ParentRef: gatev1.ParentReference{Name: "gateway"}}
otherParent := gatev1.RouteParentStatus{ParentRef: gatev1.ParentReference{Name: "other-gateway"}}
report.RecordHTTPRouteStatus(route, gatewayParent)
report.RecordHTTPRouteStatus(route, otherParent)
// Each parentRef accumulates as a distinct parent status.
assert.Equal(t, []gatev1.RouteParentStatus{gatewayParent, otherParent}, report.httpRoutes[route].Parents)
}
func TestStatusReport_RecordGRPCRouteStatus(t *testing.T) {
report := newStatusReport()
route := ktypes.NamespacedName{Namespace: "default", Name: "my-route"}
gatewayParent := gatev1.RouteParentStatus{ParentRef: gatev1.ParentReference{Name: "gateway"}}
otherParent := gatev1.RouteParentStatus{ParentRef: gatev1.ParentReference{Name: "other-gateway"}}
report.RecordGRPCRouteStatus(route, gatewayParent)
report.RecordGRPCRouteStatus(route, otherParent)
assert.Equal(t, []gatev1.RouteParentStatus{gatewayParent, otherParent}, report.grpcRoutes[route].Parents)
}
func TestStatusReport_RecordTCPRouteStatus(t *testing.T) {
report := newStatusReport()
route := ktypes.NamespacedName{Namespace: "default", Name: "my-route"}
gatewayParent := gatev1.RouteParentStatus{ParentRef: gatev1.ParentReference{Name: "gateway"}}
otherParent := gatev1.RouteParentStatus{ParentRef: gatev1.ParentReference{Name: "other-gateway"}}
report.RecordTCPRouteStatus(route, gatewayParent)
report.RecordTCPRouteStatus(route, otherParent)
assert.Equal(t, []gatev1.RouteParentStatus{gatewayParent, otherParent}, report.tcpRoutes[route].Parents)
}
func TestStatusReport_RecordTLSRouteStatus(t *testing.T) {
report := newStatusReport()
route := ktypes.NamespacedName{Namespace: "default", Name: "my-route"}
gatewayParent := gatev1.RouteParentStatus{ParentRef: gatev1.ParentReference{Name: "gateway"}}
otherParent := gatev1.RouteParentStatus{ParentRef: gatev1.ParentReference{Name: "other-gateway"}}
report.RecordTLSRouteStatus(route, gatewayParent)
report.RecordTLSRouteStatus(route, otherParent)
assert.Equal(t, []gatev1.RouteParentStatus{gatewayParent, otherParent}, report.tlsRoutes[route].Parents)
}
func TestStatusReport_RecordBackendTLSPolicyStatus(t *testing.T) {
gatewayAncestor := gatev1.PolicyAncestorStatus{
AncestorRef: gatev1.ParentReference{Name: "gateway"},
ControllerName: controllerName,
}
otherAncestor := gatev1.PolicyAncestorStatus{
AncestorRef: gatev1.ParentReference{Name: "other-gateway"},
ControllerName: controllerName,
}
testCases := []struct {
desc string
records []gatev1.PolicyAncestorStatus
expected []gatev1.PolicyAncestorStatus
}{
{
desc: "distinct ancestor refs accumulate",
records: []gatev1.PolicyAncestorStatus{gatewayAncestor, otherAncestor},
expected: []gatev1.PolicyAncestorStatus{gatewayAncestor, otherAncestor},
},
{
desc: "same ancestor ref is replaced, not duplicated",
records: []gatev1.PolicyAncestorStatus{
gatewayAncestor,
{
AncestorRef: gatev1.ParentReference{Name: "gateway"},
ControllerName: "another.io/controller",
},
},
expected: []gatev1.PolicyAncestorStatus{
{
AncestorRef: gatev1.ParentReference{Name: "gateway"},
ControllerName: "another.io/controller",
},
},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
report := newStatusReport()
policy := ktypes.NamespacedName{Namespace: "default", Name: "my-policy"}
for _, record := range test.records {
report.RecordBackendTLSPolicyStatus(policy, record)
}
assert.Equal(t, test.expected, report.backendTLSPolicies[policy].Ancestors)
})
}
}
+26 -16
View File
@@ -19,7 +19,7 @@ import (
gatev1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
)
func (p *Provider) loadTCPRoutes(ctx context.Context, gatewayListeners []gatewayListener, conf *dynamic.Configuration, statusReport *statusReport) {
func (p *Provider) loadTCPRoutes(ctx context.Context, gatewayListeners []gatewayListener, conf *dynamic.Configuration) {
logger := log.Ctx(ctx)
routes, err := p.client.ListTCPRoutes()
if err != nil {
@@ -28,13 +28,19 @@ func (p *Provider) loadTCPRoutes(ctx context.Context, gatewayListeners []gateway
}
for _, route := range routes {
logger := log.Ctx(ctx).With().
Str("tcp_route", route.Name).
Str("namespace", route.Namespace).
Logger()
routeListeners := matchingGatewayListeners(gatewayListeners, route.Namespace, route.Spec.ParentRefs)
if len(routeListeners) == 0 {
continue
}
var parentStatuses []gatev1alpha2.RouteParentStatus
for _, parentRef := range route.Spec.ParentRefs {
parentStatus := gatev1alpha2.RouteParentStatus{
parentStatus := &gatev1alpha2.RouteParentStatus{
ParentRef: parentRef,
ControllerName: controllerName,
Conditions: []metav1.Condition{
@@ -71,7 +77,18 @@ func (p *Provider) loadTCPRoutes(ctx context.Context, gatewayListeners []gateway
parentStatus.Conditions = upsertRouteConditionResolvedRefs(parentStatus.Conditions, resolveRefCondition)
}
statusReport.RecordTCPRouteStatus(ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, parentStatus)
parentStatuses = append(parentStatuses, *parentStatus)
}
routeStatus := gatev1alpha2.TCPRouteStatus{
RouteStatus: gatev1alpha2.RouteStatus{
Parents: parentStatuses,
},
}
if err := p.client.UpdateTCPRouteStatus(ctx, ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, routeStatus); err != nil {
logger.Warn().
Err(err).
Msg("Unable to update TCPRoute status")
}
}
}
@@ -158,8 +175,8 @@ func (p *Provider) loadTCPWRRService(conf *dynamic.Configuration, routeKey strin
var wrr dynamic.TCPWeightedRoundRobin
var condition *metav1.Condition
for _, backendRef := range backendRefs {
svcName, svc, errCondition := p.loadTCPService(route, backendRef)
for bi, backendRef := range backendRefs {
svcName, svc, errCondition := p.loadTCPService(routeKey, route, bi, backendRef)
weight := ptr.To(int(ptr.Deref(backendRef.Weight, 1)))
if errCondition != nil {
@@ -193,7 +210,7 @@ func (p *Provider) loadTCPWRRService(conf *dynamic.Configuration, routeKey strin
return name, condition
}
func (p *Provider) loadTCPService(route *gatev1alpha2.TCPRoute, backendRef gatev1.BackendRef) (string, *dynamic.TCPService, *metav1.Condition) {
func (p *Provider) loadTCPService(routeKey string, route *gatev1alpha2.TCPRoute, backendIndex int, backendRef gatev1.BackendRef) (string, *dynamic.TCPService, *metav1.Condition) {
kind := ptr.Deref(backendRef.Kind, kindService)
group := groupCore
@@ -206,7 +223,8 @@ func (p *Provider) loadTCPService(route *gatev1alpha2.TCPRoute, backendRef gatev
namespace = string(*backendRef.Namespace)
if strings.Contains(string(backendRef.Name), "@") {
return provider.Normalize(namespace + "-" + string(backendRef.Name)), nil, &metav1.Condition{
svcKey := fmt.Sprintf("%s-svc-%s-%s-%d", routeKey, namespace, string(backendRef.Name), backendIndex)
return provider.Normalize(svcKey), nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
@@ -217,7 +235,7 @@ func (p *Provider) loadTCPService(route *gatev1alpha2.TCPRoute, backendRef gatev
}
}
serviceName := provider.Normalize(namespace + "-" + string(backendRef.Name))
serviceName := fmt.Sprintf("%s-svc-%s-%s-%d", routeKey, namespace, string(backendRef.Name), backendIndex)
if err := p.isReferenceGranted(kindTCPRoute, route.Namespace, group, string(kind), string(backendRef.Name), namespace); err != nil {
return serviceName, nil, &metav1.Condition{
@@ -258,9 +276,6 @@ func (p *Provider) loadTCPService(route *gatev1alpha2.TCPRoute, backendRef gatev
}
}
portStr := strconv.FormatInt(int64(port), 10)
serviceName = provider.Normalize(serviceName + "-" + portStr)
lb, errCondition := p.loadTCPServers(namespace, route, backendRef)
if errCondition != nil {
return serviceName, nil, errCondition
@@ -341,9 +356,4 @@ func mergeTCPConfiguration(from, to *dynamic.Configuration) {
to.TCP.Services = map[string]*dynamic.TCPService{}
}
maps.Copy(to.TCP.Services, from.TCP.Services)
if to.TCP.ServersTransports == nil {
to.TCP.ServersTransports = map[string]*dynamic.TCPServersTransport{}
}
maps.Copy(to.TCP.ServersTransports, from.TCP.ServersTransports)
}
+34 -217
View File
@@ -5,14 +5,12 @@ import (
"fmt"
"net"
"regexp"
"slices"
"strconv"
"strings"
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/provider"
"github.com/traefik/traefik/v3/pkg/types"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
ktypes "k8s.io/apimachinery/pkg/types"
@@ -20,7 +18,7 @@ import (
gatev1 "sigs.k8s.io/gateway-api/apis/v1"
)
func (p *Provider) loadTLSRoutes(ctx context.Context, gatewayListeners []gatewayListener, conf *dynamic.Configuration, statusReport *statusReport) {
func (p *Provider) loadTLSRoutes(ctx context.Context, gatewayListeners []gatewayListener, conf *dynamic.Configuration) {
logger := log.Ctx(ctx)
routes, err := p.client.ListTLSRoutes()
if err != nil {
@@ -29,13 +27,18 @@ func (p *Provider) loadTLSRoutes(ctx context.Context, gatewayListeners []gateway
}
for _, route := range routes {
logger := log.Ctx(ctx).With().
Str("tls_route", route.Name).
Str("namespace", route.Namespace).Logger()
routeListeners := matchingGatewayListeners(gatewayListeners, route.Namespace, route.Spec.ParentRefs)
if len(routeListeners) == 0 {
continue
}
var parentStatuses []gatev1.RouteParentStatus
for _, parentRef := range route.Spec.ParentRefs {
parentStatus := gatev1.RouteParentStatus{
parentStatus := &gatev1.RouteParentStatus{
ParentRef: parentRef,
ControllerName: controllerName,
Conditions: []metav1.Condition{
@@ -70,14 +73,14 @@ func (p *Provider) loadTLSRoutes(ctx context.Context, gatewayListeners []gateway
}
}
routeConf, resolveRefCondition := p.loadTLSRoute(listener, route, hostnames, statusReport)
routeConf, resolveRefCondition := p.loadTLSRoute(listener, route, hostnames)
if accepted && listener.Attached {
mergeTCPConfiguration(routeConf, conf)
}
parentStatus.Conditions = upsertRouteConditionResolvedRefs(parentStatus.Conditions, resolveRefCondition)
}
statusReport.RecordTLSRouteStatus(ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, parentStatus)
parentStatuses = append(parentStatuses, *parentStatus)
}
// When there is at least one TLS listener, we add a default deny-all route to avoid accepting traffic for undefined hosts.
@@ -93,10 +96,21 @@ func (p *Provider) loadTLSRoutes(ctx context.Context, gatewayListeners []gateway
LoadBalancer: &dynamic.TCPServersLoadBalancer{},
}
}
routeStatus := gatev1.TLSRouteStatus{
RouteStatus: gatev1.RouteStatus{
Parents: parentStatuses,
},
}
if err := p.client.UpdateTLSRouteStatus(ctx, ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, routeStatus); err != nil {
logger.Warn().
Err(err).
Msg("Unable to update TLSRoute status")
}
}
}
func (p *Provider) loadTLSRoute(listener gatewayListener, route *gatev1.TLSRoute, hostnames []gatev1.Hostname, statusReport *statusReport) (*dynamic.Configuration, metav1.Condition) {
func (p *Provider) loadTLSRoute(listener gatewayListener, route *gatev1.TLSRoute, hostnames []gatev1.Hostname) (*dynamic.Configuration, metav1.Condition) {
conf := &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{
Routers: make(map[string]*dynamic.TCPRouter),
@@ -157,7 +171,7 @@ func (p *Provider) loadTLSRoute(listener gatewayListener, route *gatev1.TLSRoute
}
var serviceCondition *metav1.Condition
router.Service, serviceCondition = p.loadTLSWRRService(listener, conf, routerName, routeRule.BackendRefs, route, statusReport)
router.Service, serviceCondition = p.loadTLSWRRService(conf, routerName, routeRule.BackendRefs, route)
if serviceCondition != nil {
condition = *serviceCondition
}
@@ -169,7 +183,7 @@ func (p *Provider) loadTLSRoute(listener gatewayListener, route *gatev1.TLSRoute
}
// loadTLSWRRService is generating a WRR service, even when there is only one target.
func (p *Provider) loadTLSWRRService(listener gatewayListener, conf *dynamic.Configuration, routeKey string, backendRefs []gatev1.BackendRef, route *gatev1.TLSRoute, statusReport *statusReport) (string, *metav1.Condition) {
func (p *Provider) loadTLSWRRService(conf *dynamic.Configuration, routeKey string, backendRefs []gatev1.BackendRef, route *gatev1.TLSRoute) (string, *metav1.Condition) {
name := routeKey + "-wrr"
if _, ok := conf.TCP.Services[name]; ok {
return name, nil
@@ -177,8 +191,8 @@ func (p *Provider) loadTLSWRRService(listener gatewayListener, conf *dynamic.Con
var wrr dynamic.TCPWeightedRoundRobin
var condition *metav1.Condition
for _, backendRef := range backendRefs {
svcName, svc, errCondition := p.loadTLSService(listener, conf, route, backendRef, statusReport)
for bi, backendRef := range backendRefs {
svcName, svc, errCondition := p.loadTLSService(routeKey, route, bi, backendRef)
weight := ptr.To(int(ptr.Deref(backendRef.Weight, 1)))
if errCondition != nil {
@@ -212,7 +226,7 @@ func (p *Provider) loadTLSWRRService(listener gatewayListener, conf *dynamic.Con
return name, condition
}
func (p *Provider) loadTLSService(listener gatewayListener, conf *dynamic.Configuration, route *gatev1.TLSRoute, backendRef gatev1.BackendRef, statusReport *statusReport) (string, *dynamic.TCPService, *metav1.Condition) {
func (p *Provider) loadTLSService(routeKey string, route *gatev1.TLSRoute, backendIndex int, backendRef gatev1.BackendRef) (string, *dynamic.TCPService, *metav1.Condition) {
kind := ptr.Deref(backendRef.Kind, kindService)
group := groupCore
@@ -225,7 +239,8 @@ func (p *Provider) loadTLSService(listener gatewayListener, conf *dynamic.Config
namespace = string(*backendRef.Namespace)
if strings.Contains(string(backendRef.Name), "@") {
return provider.Normalize(namespace + "-" + string(backendRef.Name)), nil, &metav1.Condition{
svcKey := fmt.Sprintf("%s-svc-%s-%s-%d", routeKey, namespace, string(backendRef.Name), backendIndex)
return provider.Normalize(svcKey), nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
@@ -236,7 +251,7 @@ func (p *Provider) loadTLSService(listener gatewayListener, conf *dynamic.Config
}
}
serviceName := provider.Normalize(namespace + "-" + string(backendRef.Name))
serviceName := fmt.Sprintf("%s-svc-%s-%s-%d", routeKey, namespace, string(backendRef.Name), backendIndex)
if err := p.isReferenceGranted(kindTLSRoute, route.Namespace, group, string(kind), string(backendRef.Name), namespace); err != nil {
return serviceName, nil, &metav1.Condition{
@@ -277,26 +292,18 @@ func (p *Provider) loadTLSService(listener gatewayListener, conf *dynamic.Config
}
}
portStr := strconv.FormatInt(int64(port), 10)
serviceName = provider.Normalize(serviceName + "-" + portStr)
lb, st, errCondition := p.loadTLSServers(namespace, route, backendRef, listener, statusReport)
lb, errCondition := p.loadTLSServers(namespace, route, backendRef)
if errCondition != nil {
return serviceName, nil, errCondition
}
if st != nil {
lb.ServersTransport = serviceName
conf.TCP.ServersTransports[serviceName] = st
}
return serviceName, &dynamic.TCPService{LoadBalancer: lb}, nil
}
func (p *Provider) loadTLSServers(namespace string, route *gatev1.TLSRoute, backendRef gatev1.BackendRef, listener gatewayListener, statusReport *statusReport) (*dynamic.TCPServersLoadBalancer, *dynamic.TCPServersTransport, *metav1.Condition) {
func (p *Provider) loadTLSServers(namespace string, route *gatev1.TLSRoute, backendRef gatev1.BackendRef) (*dynamic.TCPServersLoadBalancer, *metav1.Condition) {
backendAddresses, svcPort, err := p.getBackendAddresses(namespace, backendRef)
if err != nil {
return nil, nil, &metav1.Condition{
return nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.GetGeneration(),
@@ -306,116 +313,8 @@ func (p *Provider) loadTLSServers(namespace string, route *gatev1.TLSRoute, back
}
}
backendTLSPolicies, err := p.client.ListBackendTLSPoliciesForService(namespace, string(backendRef.Name))
if err != nil {
return nil, nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonRefNotPermitted),
Message: fmt.Sprintf("Cannot list BackendTLSPolicies for Service %s/%s: %s", namespace, string(backendRef.Name), err),
}
}
// Sort BackendTLSPolicies by creation timestamp, then by name to match the BackendTLSPolicy requirements.
slices.SortStableFunc(backendTLSPolicies, func(a, b *gatev1.BackendTLSPolicy) int {
cmpTime := a.CreationTimestamp.Time.Compare(b.CreationTimestamp.Time)
if cmpTime == 0 {
return strings.Compare(a.Name, b.Name)
}
return cmpTime
})
var serversTransport *dynamic.TCPServersTransport
for _, policy := range backendTLSPolicies {
for _, targetRef := range policy.Spec.TargetRefs {
// Skip targetRefs that doesn't match the backendRef,
// since a BackendTLSPolicy can select multiple services.
if targetRef.Name != backendRef.Name {
continue
}
// Skip the targetRef if the sectionName doesn't match the backendRef port.
if targetRef.SectionName != nil && svcPort.Name != string(*targetRef.SectionName) {
continue
}
policyAncestorStatus := gatev1.PolicyAncestorStatus{
AncestorRef: gatev1.ParentReference{
Group: ptr.To(gatev1.Group(groupGateway)),
Kind: ptr.To(gatev1.Kind(kindGateway)),
Namespace: ptr.To(gatev1.Namespace(namespace)),
Name: gatev1.ObjectName(listener.GWName),
SectionName: ptr.To(gatev1.SectionName(listener.Name)),
},
ControllerName: controllerName,
}
// Multiple BackendTLSPolicies can match the same service port, meaning that there is a conflict.
if serversTransport != nil {
policyAncestorStatus.Conditions = append(policyAncestorStatus.Conditions,
metav1.Condition{
Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.BackendTLSPolicyReasonResolvedRefs),
},
metav1.Condition{
Type: string(gatev1.PolicyConditionAccepted),
Status: metav1.ConditionFalse,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.PolicyReasonConflicted),
},
)
statusReport.RecordBackendTLSPolicyStatus(ktypes.NamespacedName{Namespace: policy.Namespace, Name: policy.Name}, policyAncestorStatus)
continue
}
var resolvedRefCondition metav1.Condition
serversTransport, resolvedRefCondition = p.loadTCPServersTransport(namespace, policy)
policyAncestorStatus.Conditions = append(policyAncestorStatus.Conditions, resolvedRefCondition)
if resolvedRefCondition.Status == metav1.ConditionFalse {
policyAncestorStatus.Conditions = append(policyAncestorStatus.Conditions, metav1.Condition{
Type: string(gatev1.PolicyConditionAccepted),
Status: metav1.ConditionFalse,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.BackendTLSPolicyReasonNoValidCACertificate),
})
} else {
policyAncestorStatus.Conditions = append(policyAncestorStatus.Conditions, metav1.Condition{
Type: string(gatev1.PolicyConditionAccepted),
Status: metav1.ConditionTrue,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.PolicyReasonAccepted),
})
}
statusReport.RecordBackendTLSPolicyStatus(ktypes.NamespacedName{Namespace: policy.Namespace, Name: policy.Name}, policyAncestorStatus)
// When something went wrong during the loading of a ServersTransport,
// we stop here and return a route condition error.
if resolvedRefCondition.Status == metav1.ConditionFalse {
return nil, nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.RouteReasonRefNotPermitted),
Message: fmt.Sprintf("Cannot apply BackendTLSPolicy for Service %s/%s: %s", namespace, string(backendRef.Name), resolvedRefCondition.Message),
}
}
}
}
if svcPort.Protocol != corev1.ProtocolTCP {
return nil, nil, &metav1.Condition{
return nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.GetGeneration(),
@@ -433,89 +332,7 @@ func (p *Provider) loadTLSServers(namespace string, route *gatev1.TLSRoute, back
Address: net.JoinHostPort(ba.IP, strconv.Itoa(int(ba.Port))),
})
}
return lb, serversTransport, nil
}
func (p *Provider) loadTCPServersTransport(namespace string, policy *gatev1.BackendTLSPolicy) (*dynamic.TCPServersTransport, metav1.Condition) {
st := &dynamic.TCPServersTransport{
TLS: &dynamic.TLSClientConfig{
ServerName: string(policy.Spec.Validation.Hostname),
},
}
if policy.Spec.Validation.WellKnownCACertificates != nil {
return st, metav1.Condition{
Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs),
Status: metav1.ConditionTrue,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.BackendTLSPolicyReasonResolvedRefs),
}
}
for _, caCertRef := range policy.Spec.Validation.CACertificateRefs {
if (caCertRef.Group != "" && caCertRef.Group != groupCore) || (caCertRef.Kind != kindConfigMap && caCertRef.Kind != kindSecret) {
return nil, metav1.Condition{
Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.BackendTLSPolicyReasonInvalidKind),
Message: "Only ConfigMaps and Secrets are supported",
}
}
var caCRT string
switch caCertRef.Kind {
case kindConfigMap:
configmap, err := p.client.GetConfigMap(namespace, string(caCertRef.Name))
if err != nil {
return nil, metav1.Condition{
Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.BackendTLSPolicyReasonInvalidCACertificateRef),
Message: fmt.Sprintf("getting configmap %s/%s: %s", namespace, string(caCertRef.Name), err),
}
}
caCRT = configmap.Data["ca.crt"]
case kindSecret:
secret, err := p.client.GetSecret(namespace, string(caCertRef.Name))
if err != nil {
return nil, metav1.Condition{
Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.BackendTLSPolicyReasonInvalidCACertificateRef),
Message: fmt.Sprintf("getting secret %s/%s: %s", namespace, string(caCertRef.Name), err),
}
}
caCRT = string(secret.Data["ca.crt"])
}
if caCRT == "" {
return nil, metav1.Condition{
Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.BackendTLSPolicyReasonInvalidCACertificateRef),
Message: fmt.Sprintf("%s %s/%s does not have a ca.crt", caCertRef.Kind, namespace, string(caCertRef.Name)),
}
}
st.TLS.RootCAs = append(st.TLS.RootCAs, types.FileOrContent(caCRT))
}
return st, metav1.Condition{
Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs),
Status: metav1.ConditionTrue,
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.BackendTLSPolicyReasonResolvedRefs),
}
return lb, nil
}
func hostSNIRule(hostnames []gatev1.Hostname) (string, int) {
@@ -1,75 +0,0 @@
---
kind: Node
apiVersion: v1
metadata:
name: node1
status:
addresses:
- type: InternalIP
address: 10.0.0.1
- type: ExternalIP
address: 1.2.3.4
---
kind: Node
apiVersion: v1
metadata:
name: node2
status:
addresses:
- type: InternalIP
address: 10.0.0.2
---
kind: Ingress
apiVersion: networking.k8s.io/v1
metadata:
name: foo
namespace: default
spec:
rules:
- host: "*.foo.com"
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: service1
port:
number: 80
---
kind: Service
apiVersion: v1
metadata:
name: service1
namespace: default
spec:
ports:
- port: 80
clusterIP: 10.0.0.1
---
kind: EndpointSlice
apiVersion: discovery.k8s.io/v1
metadata:
name: service1-abc
namespace: default
labels:
kubernetes.io/service-name: service1
addressType: IPv4
ports:
- port: 8080
name: ""
endpoints:
- addresses:
- 10.10.0.1
conditions:
ready: true
+4 -37
View File
@@ -48,7 +48,6 @@ type Provider struct {
LabelSelector string `description:"Kubernetes Ingress label selector to use." json:"labelSelector,omitempty" toml:"labelSelector,omitempty" yaml:"labelSelector,omitempty" export:"true"`
IngressClass string `description:"Value of kubernetes.io/ingress.class annotation or IngressClass name to watch for." json:"ingressClass,omitempty" toml:"ingressClass,omitempty" yaml:"ingressClass,omitempty" export:"true"`
IngressEndpoint *EndpointIngress `description:"Kubernetes Ingress Endpoint." json:"ingressEndpoint,omitempty" toml:"ingressEndpoint,omitempty" yaml:"ingressEndpoint,omitempty" export:"true"`
ReportNodeInternalIPs bool `description:"Report node internal IPs in Ingress status." json:"reportNodeInternalIPs,omitempty" toml:"reportNodeInternalIPs,omitempty" yaml:"reportNodeInternalIPs,omitempty" export:"true"`
ThrottleDuration ptypes.Duration `description:"Ingress refresh throttle duration" json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,omitempty" export:"true"`
AllowEmptyServices bool `description:"Allow creation of services without endpoints." json:"allowEmptyServices,omitempty" toml:"allowEmptyServices,omitempty" yaml:"allowEmptyServices,omitempty" export:"true"`
AllowExternalNameServices bool `description:"Allow ExternalName services." json:"allowExternalNameServices,omitempty" toml:"allowExternalNameServices,omitempty" yaml:"allowExternalNameServices,omitempty" export:"true"`
@@ -73,14 +72,6 @@ func (p *Provider) SetRouterTransform(routerTransform k8s.RouterTransform) {
// Init the provider.
func (p *Provider) Init() error {
if p.ReportNodeInternalIPs && p.IngressEndpoint != nil {
return errors.New("reportNodeInternalIPs and ingressEndpoint are mutually exclusive")
}
if p.ReportNodeInternalIPs && p.DisableClusterScopeResources {
return errors.New("reportNodeInternalIPs and disableClusterScopeResources are mutually exclusive")
}
return nil
}
@@ -255,26 +246,6 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl
ingresses := client.GetIngresses()
var nodeIngressStatus []netv1.IngressLoadBalancerIngress
if p.ReportNodeInternalIPs {
nodes, _, err := client.GetNodes()
if err != nil {
log.Ctx(ctx).Error().Err(err).Msg("Error while getting nodes for ingress status")
} else {
for _, node := range nodes {
for _, address := range node.Status.Addresses {
if address.Type == corev1.NodeInternalIP {
nodeIngressStatus = append(nodeIngressStatus, netv1.IngressLoadBalancerIngress{IP: address.Address})
}
}
}
if len(nodeIngressStatus) == 0 {
log.Ctx(ctx).Error().Msg("No nodes with internal IP address found for ingress status")
}
}
}
certConfigs := make(map[string]*tls.CertAndStores)
for _, ingress := range ingresses {
logger := log.Ctx(ctx).With().Str("ingress", ingress.Name).Str("namespace", ingress.Namespace).Logger()
@@ -284,7 +255,7 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl
continue
}
if err := p.updateIngressStatus(ingress, client, nodeIngressStatus); err != nil {
if err := p.updateIngressStatus(ingress, client); err != nil {
logger.Error().Err(err).Msg("Error while updating ingress status")
}
@@ -471,11 +442,7 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl
return conf
}
func (p *Provider) updateIngressStatus(ing *netv1.Ingress, k8sClient Client, nodeIngressStatus []netv1.IngressLoadBalancerIngress) error {
if len(nodeIngressStatus) > 0 {
return k8sClient.UpdateIngressStatus(ing, nodeIngressStatus)
}
func (p *Provider) updateIngressStatus(ing *netv1.Ingress, k8sClient Client) error {
// Only process if an EndpointIngress has been configured.
if p.IngressEndpoint == nil {
return nil
@@ -656,12 +623,12 @@ func (p *Provider) loadService(client Client, namespace string, backend netv1.In
return nil, errors.New("nodes lookup is disabled")
}
nodes, _, nodesErr := client.GetNodes()
nodes, nodesExists, nodesErr := client.GetNodes()
if nodesErr != nil {
return nil, nodesErr
}
if len(nodes) == 0 {
if !nodesExists || len(nodes) == 0 {
return nil, fmt.Errorf("nodes not found in namespace %s", namespace)
}
@@ -3094,77 +3094,6 @@ func readResources(t *testing.T, paths []string) []runtime.Object {
return k8sObjects
}
func TestProviderInit(t *testing.T) {
p := Provider{
ReportNodeInternalIPs: true,
IngressEndpoint: &EndpointIngress{IP: "1.2.3.4"},
}
assert.EqualError(t, p.Init(), "reportNodeInternalIPs and ingressEndpoint are mutually exclusive")
p2 := Provider{
ReportNodeInternalIPs: true,
DisableClusterScopeResources: true,
}
assert.EqualError(t, p2.Init(), "reportNodeInternalIPs and disableClusterScopeResources are mutually exclusive")
p3 := Provider{ReportNodeInternalIPs: true}
assert.NoError(t, p3.Init())
}
func TestReportNodeInternalIPs(t *testing.T) {
testCases := []struct {
desc string
client clientMock
expectedEmpty bool
}{
{
desc: "nodes present",
client: newClientMock(generateTestFilename("Node Internal IP")),
},
{
desc: "GetNodes API error",
client: clientMock{apiNodesError: errors.New("api nodes error")},
expectedEmpty: true,
},
{
desc: "no nodes found",
client: clientMock{nodes: []*corev1.Node{}},
expectedEmpty: true,
},
{
desc: "nodes exist but none have an internal IP",
client: clientMock{
nodes: []*corev1.Node{
{
Status: corev1.NodeStatus{
Addresses: []corev1.NodeAddress{
{Type: corev1.NodeExternalIP, Address: "1.2.3.4"},
},
},
},
},
},
expectedEmpty: true,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
p := Provider{ReportNodeInternalIPs: true}
conf := p.loadConfigurationFromIngresses(t.Context(), test.client)
if test.expectedEmpty {
assert.Empty(t, conf.HTTP.Routers)
assert.Empty(t, conf.HTTP.Services)
} else {
assert.NotEmpty(t, conf.HTTP.Routers)
assert.NotEmpty(t, conf.HTTP.Services)
}
})
}
}
func TestStrictPrefixMatchingRule(t *testing.T) {
tests := []struct {
path string
-1
View File
@@ -643,7 +643,6 @@ func newHTTPServer(ctx context.Context, ln net.Listener, configuration *static.E
configuration.ForwardedHeaders.TrustedIPs,
configuration.ForwardedHeaders.Connection,
configuration.ForwardedHeaders.NotAppendXForwardedFor,
configuration.ForwardedHeaders.AddXForwardedSchemeHeaders,
next)
if err != nil {
return nil, err