From 8425e09806336f11f43916e3679561612c4487ba Mon Sep 17 00:00:00 2001 From: Julien Salleyron Date: Thu, 29 Jan 2026 17:16:04 +0100 Subject: [PATCH] Services middleware and Gateway API filters on HTTP backends --- Makefile | 2 +- .../kubernetes-crd-definition-v1.yml | 113 ++++++++++++++++++ .../traefik.io_ingressroutes.yaml | 19 +++ .../traefik.io_middlewares.yaml | 19 +++ .../traefik.io_traefikservices.yaml | 75 ++++++++++++ .../other-providers/file.toml | 24 ++-- .../other-providers/file.yaml | 6 +- integration/fixtures/k8s/01-traefik-crd.yml | 113 ++++++++++++++++++ integration/fixtures/service_middleware.toml | 35 ++++++ ... => experimental-v3.7-default-report.yaml} | 6 +- integration/simple_test.go | 34 ++++++ pkg/config/dynamic/http_config.go | 1 + pkg/config/dynamic/zz_generated.deepcopy.go | 5 + pkg/middlewares/chain/chain.go | 8 +- .../with_traefik_service_middleware.yml | 45 +++++++ .../traefikio/v1alpha1/loadbalancerspec.go | 14 +++ .../traefikio/v1alpha1/mirroring.go | 13 ++ .../traefikio/v1alpha1/mirrorservice.go | 13 ++ .../traefikio/v1alpha1/service.go | 13 ++ pkg/provider/kubernetes/crd/kubernetes.go | 6 +- .../kubernetes/crd/kubernetes_http.go | 28 +++-- .../kubernetes/crd/kubernetes_test.go | 65 ++++++++++ .../crd/traefikio/v1alpha1/ingressroute.go | 2 + .../v1alpha1/zz_generated.deepcopy.go | 5 + pkg/provider/kubernetes/gateway/features.go | 1 + ...backend_filter_request_header_modifier.yml | 62 ++++++++++ pkg/provider/kubernetes/gateway/httproute.go | 27 ++++- .../kubernetes/gateway/kubernetes_test.go | 72 +++++++++++ .../kubernetes/ingress/annotations.go | 1 + .../kubernetes/ingress/annotations_test.go | 12 ++ pkg/provider/kubernetes/ingress/kubernetes.go | 1 + pkg/server/middleware/middlewares.go | 4 +- pkg/server/middleware/middlewares_test.go | 6 +- pkg/server/router/router.go | 10 +- pkg/server/router/router_test.go | 2 +- pkg/server/routerfactory.go | 2 + pkg/server/service/service.go | 37 ++++-- 37 files changed, 846 insertions(+), 55 deletions(-) create mode 100644 integration/fixtures/service_middleware.toml rename integration/gateway-api-conformance-reports/v1.4.0/{experimental-v3.6-default-report.yaml => experimental-v3.7-default-report.yaml} (98%) create mode 100644 pkg/provider/kubernetes/crd/fixtures/with_traefik_service_middleware.yml create mode 100644 pkg/provider/kubernetes/gateway/fixtures/httproute/backend_filter_request_header_modifier.yml diff --git a/Makefile b/Makefile index 7f361cfb2..24a2b6fc9 100644 --- a/Makefile +++ b/Makefile @@ -103,7 +103,7 @@ test-integration: #? test-gateway-api-conformance: Run the Gateway API conformance tests test-gateway-api-conformance: build-image-dirty # In case of a new Minor/Major version, the traefikVersion needs to be updated. - GOOS=$(GOOS) GOARCH=$(GOARCH) go test ./integration -v -tags gatewayAPIConformance -test.run GatewayAPIConformanceSuite -traefikVersion="v3.6" $(TESTFLAGS) + GOOS=$(GOOS) GOARCH=$(GOARCH) go test ./integration -v -tags gatewayAPIConformance -test.run GatewayAPIConformanceSuite -traefikVersion="v3.7" $(TESTFLAGS) .PHONY: test-knative-conformance #? test-knative-conformance: Run the Knative conformance tests diff --git a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml index d392b67a5..33e15bef5 100644 --- a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml +++ b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml @@ -222,6 +222,25 @@ spec: - Service - TraefikService type: string + middlewares: + description: Middlewares defines the list of references + to Middleware resources to apply to the service. + items: + description: MiddlewareRef is a reference to a Middleware + resource. + properties: + name: + description: Name defines the name of the referenced + Middleware resource. + type: string + namespace: + description: Namespace defines the namespace of + the referenced Middleware resource. + type: string + required: + - name + type: object + type: array name: description: |- Name defines the name of the referenced Kubernetes Service or TraefikService. @@ -1232,6 +1251,25 @@ spec: - Service - TraefikService type: string + middlewares: + description: Middlewares defines the list of references to + Middleware resources to apply to the service. + items: + description: MiddlewareRef is a reference to a Middleware + resource. + properties: + name: + description: Name defines the name of the referenced + Middleware resource. + type: string + namespace: + description: Namespace defines the namespace of the + referenced Middleware resource. + type: string + required: + - name + type: object + type: array name: description: |- Name defines the name of the referenced Kubernetes Service or TraefikService. @@ -2974,6 +3012,25 @@ spec: - Service - TraefikService type: string + middlewares: + description: Middlewares defines the list of references + to Middleware resources to apply to the service. + items: + description: MiddlewareRef is a reference to a Middleware + resource. + properties: + name: + description: Name defines the name of the referenced + Middleware resource. + type: string + namespace: + description: Namespace defines the namespace of the + referenced Middleware resource. + type: string + required: + - name + type: object + type: array name: description: |- Name defines the name of the referenced Kubernetes Service or TraefikService. @@ -3211,6 +3268,24 @@ spec: Default value is -1, which means unlimited size. format: int64 type: integer + middlewares: + description: Middlewares defines the list of references to Middleware + resources to apply to the service. + items: + description: MiddlewareRef is a reference to a Middleware resource. + properties: + name: + description: Name defines the name of the referenced Middleware + resource. + type: string + namespace: + description: Namespace defines the namespace of the referenced + Middleware resource. + type: string + required: + - name + type: object + type: array mirrorBody: description: |- MirrorBody defines whether the body of the request should be mirrored. @@ -3298,6 +3373,25 @@ spec: - Service - TraefikService type: string + middlewares: + description: Middlewares defines the list of references + to Middleware resources to apply to the service. + items: + description: MiddlewareRef is a reference to a Middleware + resource. + properties: + name: + description: Name defines the name of the referenced + Middleware resource. + type: string + namespace: + description: Namespace defines the namespace of the + referenced Middleware resource. + type: string + required: + - name + type: object + type: array name: description: |- Name defines the name of the referenced Kubernetes Service or TraefikService. @@ -3686,6 +3780,25 @@ spec: - Service - TraefikService type: string + middlewares: + description: Middlewares defines the list of references + to Middleware resources to apply to the service. + items: + description: MiddlewareRef is a reference to a Middleware + resource. + properties: + name: + description: Name defines the name of the referenced + Middleware resource. + type: string + namespace: + description: Namespace defines the namespace of the + referenced Middleware resource. + type: string + required: + - name + type: object + type: array name: description: |- Name defines the name of the referenced Kubernetes Service or TraefikService. diff --git a/docs/content/reference/dynamic-configuration/traefik.io_ingressroutes.yaml b/docs/content/reference/dynamic-configuration/traefik.io_ingressroutes.yaml index 87fbdb544..d6d433f94 100644 --- a/docs/content/reference/dynamic-configuration/traefik.io_ingressroutes.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.io_ingressroutes.yaml @@ -223,6 +223,25 @@ spec: - Service - TraefikService type: string + middlewares: + description: Middlewares defines the list of references + to Middleware resources to apply to the service. + items: + description: MiddlewareRef is a reference to a Middleware + resource. + properties: + name: + description: Name defines the name of the referenced + Middleware resource. + type: string + namespace: + description: Namespace defines the namespace of + the referenced Middleware resource. + type: string + required: + - name + type: object + type: array name: description: |- Name defines the name of the referenced Kubernetes Service or TraefikService. diff --git a/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml b/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml index 7ca937bb1..4ce03061f 100644 --- a/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml @@ -387,6 +387,25 @@ spec: - Service - TraefikService type: string + middlewares: + description: Middlewares defines the list of references to + Middleware resources to apply to the service. + items: + description: MiddlewareRef is a reference to a Middleware + resource. + properties: + name: + description: Name defines the name of the referenced + Middleware resource. + type: string + namespace: + description: Namespace defines the namespace of the + referenced Middleware resource. + type: string + required: + - name + type: object + type: array name: description: |- Name defines the name of the referenced Kubernetes Service or TraefikService. diff --git a/docs/content/reference/dynamic-configuration/traefik.io_traefikservices.yaml b/docs/content/reference/dynamic-configuration/traefik.io_traefikservices.yaml index a51ddc0a4..11f302e71 100644 --- a/docs/content/reference/dynamic-configuration/traefik.io_traefikservices.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.io_traefikservices.yaml @@ -131,6 +131,25 @@ spec: - Service - TraefikService type: string + middlewares: + description: Middlewares defines the list of references + to Middleware resources to apply to the service. + items: + description: MiddlewareRef is a reference to a Middleware + resource. + properties: + name: + description: Name defines the name of the referenced + Middleware resource. + type: string + namespace: + description: Namespace defines the namespace of the + referenced Middleware resource. + type: string + required: + - name + type: object + type: array name: description: |- Name defines the name of the referenced Kubernetes Service or TraefikService. @@ -368,6 +387,24 @@ spec: Default value is -1, which means unlimited size. format: int64 type: integer + middlewares: + description: Middlewares defines the list of references to Middleware + resources to apply to the service. + items: + description: MiddlewareRef is a reference to a Middleware resource. + properties: + name: + description: Name defines the name of the referenced Middleware + resource. + type: string + namespace: + description: Namespace defines the namespace of the referenced + Middleware resource. + type: string + required: + - name + type: object + type: array mirrorBody: description: |- MirrorBody defines whether the body of the request should be mirrored. @@ -455,6 +492,25 @@ spec: - Service - TraefikService type: string + middlewares: + description: Middlewares defines the list of references + to Middleware resources to apply to the service. + items: + description: MiddlewareRef is a reference to a Middleware + resource. + properties: + name: + description: Name defines the name of the referenced + Middleware resource. + type: string + namespace: + description: Namespace defines the namespace of the + referenced Middleware resource. + type: string + required: + - name + type: object + type: array name: description: |- Name defines the name of the referenced Kubernetes Service or TraefikService. @@ -843,6 +899,25 @@ spec: - Service - TraefikService type: string + middlewares: + description: Middlewares defines the list of references + to Middleware resources to apply to the service. + items: + description: MiddlewareRef is a reference to a Middleware + resource. + properties: + name: + description: Name defines the name of the referenced + Middleware resource. + type: string + namespace: + description: Namespace defines the namespace of the + referenced Middleware resource. + type: string + required: + - name + type: object + type: array name: description: |- Name defines the name of the referenced Kubernetes Service or TraefikService. diff --git a/docs/content/reference/routing-configuration/other-providers/file.toml b/docs/content/reference/routing-configuration/other-providers/file.toml index 9a6c18e59..2f94a07dd 100644 --- a/docs/content/reference/routing-configuration/other-providers/file.toml +++ b/docs/content/reference/routing-configuration/other-providers/file.toml @@ -112,31 +112,33 @@ [http.services.Service03.loadBalancer.responseForwarding] flushInterval = "42s" [http.services.Service04] - [http.services.Service04.mirroring] + middlewares = ["foobar", "foobar"] + [http.services.Service05] + [http.services.Service05.mirroring] service = "foobar" mirrorBody = true maxBodySize = 42 - [[http.services.Service04.mirroring.mirrors]] + [[http.services.Service05.mirroring.mirrors]] name = "foobar" percent = 42 - [[http.services.Service04.mirroring.mirrors]] + [[http.services.Service05.mirroring.mirrors]] name = "foobar" percent = 42 - [http.services.Service04.mirroring.healthCheck] - [http.services.Service05] - [http.services.Service05.weighted] + [http.services.Service05.mirroring.healthCheck] + [http.services.Service06] + [http.services.Service06.weighted] - [[http.services.Service05.weighted.services]] + [[http.services.Service06.weighted.services]] name = "foobar" weight = 42 - [[http.services.Service05.weighted.services]] + [[http.services.Service06.weighted.services]] name = "foobar" weight = 42 - [http.services.Service05.weighted.sticky] - [http.services.Service05.weighted.sticky.cookie] + [http.services.Service06.weighted.sticky] + [http.services.Service06.weighted.sticky.cookie] name = "foobar" secure = true httpOnly = true @@ -144,7 +146,7 @@ maxAge = 42 path = "foobar" domain = "foobar" - [http.services.Service05.weighted.healthCheck] + [http.services.Service06.weighted.healthCheck] [http.middlewares] [http.middlewares.Middleware01] [http.middlewares.Middleware01.addPrefix] diff --git a/docs/content/reference/routing-configuration/other-providers/file.yaml b/docs/content/reference/routing-configuration/other-providers/file.yaml index c15d2e423..50200a4fb 100644 --- a/docs/content/reference/routing-configuration/other-providers/file.yaml +++ b/docs/content/reference/routing-configuration/other-providers/file.yaml @@ -120,6 +120,10 @@ http: flushInterval: 42s serversTransport: foobar Service04: + middlewares: + - foobar + - foobar + Service05: mirroring: service: foobar mirrorBody: true @@ -130,7 +134,7 @@ http: - name: foobar percent: 42 healthCheck: {} - Service05: + Service06: weighted: services: - name: foobar diff --git a/integration/fixtures/k8s/01-traefik-crd.yml b/integration/fixtures/k8s/01-traefik-crd.yml index be4c1f2f4..ab1c56ea5 100644 --- a/integration/fixtures/k8s/01-traefik-crd.yml +++ b/integration/fixtures/k8s/01-traefik-crd.yml @@ -223,6 +223,25 @@ spec: - Service - TraefikService type: string + middlewares: + description: Middlewares defines the list of references + to Middleware resources to apply to the service. + items: + description: MiddlewareRef is a reference to a Middleware + resource. + properties: + name: + description: Name defines the name of the referenced + Middleware resource. + type: string + namespace: + description: Namespace defines the namespace of + the referenced Middleware resource. + type: string + required: + - name + type: object + type: array name: description: |- Name defines the name of the referenced Kubernetes Service or TraefikService. @@ -1233,6 +1252,25 @@ spec: - Service - TraefikService type: string + middlewares: + description: Middlewares defines the list of references to + Middleware resources to apply to the service. + items: + description: MiddlewareRef is a reference to a Middleware + resource. + properties: + name: + description: Name defines the name of the referenced + Middleware resource. + type: string + namespace: + description: Namespace defines the namespace of the + referenced Middleware resource. + type: string + required: + - name + type: object + type: array name: description: |- Name defines the name of the referenced Kubernetes Service or TraefikService. @@ -2975,6 +3013,25 @@ spec: - Service - TraefikService type: string + middlewares: + description: Middlewares defines the list of references + to Middleware resources to apply to the service. + items: + description: MiddlewareRef is a reference to a Middleware + resource. + properties: + name: + description: Name defines the name of the referenced + Middleware resource. + type: string + namespace: + description: Namespace defines the namespace of the + referenced Middleware resource. + type: string + required: + - name + type: object + type: array name: description: |- Name defines the name of the referenced Kubernetes Service or TraefikService. @@ -3212,6 +3269,24 @@ spec: Default value is -1, which means unlimited size. format: int64 type: integer + middlewares: + description: Middlewares defines the list of references to Middleware + resources to apply to the service. + items: + description: MiddlewareRef is a reference to a Middleware resource. + properties: + name: + description: Name defines the name of the referenced Middleware + resource. + type: string + namespace: + description: Namespace defines the namespace of the referenced + Middleware resource. + type: string + required: + - name + type: object + type: array mirrorBody: description: |- MirrorBody defines whether the body of the request should be mirrored. @@ -3299,6 +3374,25 @@ spec: - Service - TraefikService type: string + middlewares: + description: Middlewares defines the list of references + to Middleware resources to apply to the service. + items: + description: MiddlewareRef is a reference to a Middleware + resource. + properties: + name: + description: Name defines the name of the referenced + Middleware resource. + type: string + namespace: + description: Namespace defines the namespace of the + referenced Middleware resource. + type: string + required: + - name + type: object + type: array name: description: |- Name defines the name of the referenced Kubernetes Service or TraefikService. @@ -3687,6 +3781,25 @@ spec: - Service - TraefikService type: string + middlewares: + description: Middlewares defines the list of references + to Middleware resources to apply to the service. + items: + description: MiddlewareRef is a reference to a Middleware + resource. + properties: + name: + description: Name defines the name of the referenced + Middleware resource. + type: string + namespace: + description: Namespace defines the namespace of the + referenced Middleware resource. + type: string + required: + - name + type: object + type: array name: description: |- Name defines the name of the referenced Kubernetes Service or TraefikService. diff --git a/integration/fixtures/service_middleware.toml b/integration/fixtures/service_middleware.toml new file mode 100644 index 000000000..066985b85 --- /dev/null +++ b/integration/fixtures/service_middleware.toml @@ -0,0 +1,35 @@ +[global] + checkNewVersion = false + sendAnonymousUsage = false + +[api] + insecure = true + +[log] + level = "DEBUG" + noColor = true + +[entryPoints] + [entryPoints.web] + address = ":8000" + +[providers.file] + filename = "{{ .SelfFilename }}" + +## dynamic configuration ## + +[http.routers] + [http.routers.router1] + service = "service1" + rule = "Path(`/whoami`)" + +[http.middlewares] + [http.middlewares.add-header.headers.customRequestHeaders] + X-Custom-Header = "service-middleware-test" + +[http.services] + [http.services.service1] + middlewares = ["add-header"] + [http.services.service1.loadBalancer] + [[http.services.service1.loadBalancer.servers]] + url = "{{ .Server }}" \ No newline at end of file diff --git a/integration/gateway-api-conformance-reports/v1.4.0/experimental-v3.6-default-report.yaml b/integration/gateway-api-conformance-reports/v1.4.0/experimental-v3.7-default-report.yaml similarity index 98% rename from integration/gateway-api-conformance-reports/v1.4.0/experimental-v3.6-default-report.yaml rename to integration/gateway-api-conformance-reports/v1.4.0/experimental-v3.7-default-report.yaml index 429592e4a..e6961a03d 100644 --- a/integration/gateway-api-conformance-reports/v1.4.0/experimental-v3.6-default-report.yaml +++ b/integration/gateway-api-conformance-reports/v1.4.0/experimental-v3.7-default-report.yaml @@ -8,7 +8,7 @@ implementation: organization: traefik project: traefik url: https://traefik.io/ - version: v3.6 + version: v3.7 kind: ConformanceReport mode: default profiles: @@ -30,12 +30,13 @@ profiles: result: success statistics: Failed: 0 - Passed: 13 + Passed: 15 Skipped: 0 supportedFeatures: - GatewayPort8080 - HTTPRouteBackendProtocolH2C - HTTPRouteBackendProtocolWebSocket + - HTTPRouteBackendRequestHeaderModification - HTTPRouteDestinationPortMatching - HTTPRouteHostRewrite - HTTPRouteMethodMatching @@ -50,7 +51,6 @@ profiles: - GatewayHTTPListenerIsolation - GatewayInfrastructurePropagation - GatewayStaticAddresses - - HTTPRouteBackendRequestHeaderModification - HTTPRouteBackendTimeout - HTTPRouteCORS - HTTPRouteNamedRouteRule diff --git a/integration/simple_test.go b/integration/simple_test.go index c1eb0eb17..1d1e1a818 100644 --- a/integration/simple_test.go +++ b/integration/simple_test.go @@ -2364,3 +2364,37 @@ func (s *SimpleSuite) TestEncodedCharactersDifferentEntryPoints() { require.NoError(s.T(), err) } } + +func (s *SimpleSuite) TestServiceMiddleware() { + s.createComposeProject("base") + + s.composeUp() + defer s.composeDown() + + whoamiIP := s.getComposeServiceIP("whoami1") + + file := s.adaptFile("fixtures/service_middleware.toml", struct { + Server string + }{Server: "http://" + whoamiIP}) + + s.traefikCmd(withConfigFile(file)) + + // Wait for Traefik to be ready + err := try.GetRequest("http://127.0.0.1:8080/api/http/services", 2*time.Second, try.BodyContains("service1")) + require.NoError(s.T(), err) + + // Make a request and verify the middleware added the custom header + req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/whoami", nil) + require.NoError(s.T(), err) + + response, err := http.DefaultClient.Do(req) + require.NoError(s.T(), err) + assert.Equal(s.T(), http.StatusOK, response.StatusCode) + + // Read the response body to check if the whoami service received the custom header + body, err := io.ReadAll(response.Body) + require.NoError(s.T(), err) + + // The whoami service should have received the X-Custom-Header that was added by the service middleware + assert.Contains(s.T(), string(body), "X-Custom-Header: service-middleware-test") +} diff --git a/pkg/config/dynamic/http_config.go b/pkg/config/dynamic/http_config.go index a209f6b10..b1ccd08b0 100644 --- a/pkg/config/dynamic/http_config.go +++ b/pkg/config/dynamic/http_config.go @@ -55,6 +55,7 @@ type Model struct { // Service holds a service configuration (can only be of one type at the same time). type Service struct { + Middlewares []string `json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty" export:"true"` LoadBalancer *ServersLoadBalancer `json:"loadBalancer,omitempty" toml:"loadBalancer,omitempty" yaml:"loadBalancer,omitempty" export:"true"` HighestRandomWeight *HighestRandomWeight `json:"highestRandomWeight,omitempty" toml:"highestRandomWeight,omitempty" yaml:"highestRandomWeight,omitempty" label:"-" export:"true"` Weighted *WeightedRoundRobin `json:"weighted,omitempty" toml:"weighted,omitempty" yaml:"weighted,omitempty" label:"-" export:"true"` diff --git a/pkg/config/dynamic/zz_generated.deepcopy.go b/pkg/config/dynamic/zz_generated.deepcopy.go index f19b3448c..41ffec75d 100644 --- a/pkg/config/dynamic/zz_generated.deepcopy.go +++ b/pkg/config/dynamic/zz_generated.deepcopy.go @@ -1672,6 +1672,11 @@ func (in *ServersTransport) DeepCopy() *ServersTransport { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Service) DeepCopyInto(out *Service) { *out = *in + if in.Middlewares != nil { + in, out := &in.Middlewares, &out.Middlewares + *out = make([]string, len(*in)) + copy(*out, *in) + } if in.LoadBalancer != nil { in, out := &in.LoadBalancer, &out.LoadBalancer *out = new(ServersLoadBalancer) diff --git a/pkg/middlewares/chain/chain.go b/pkg/middlewares/chain/chain.go index d87596b42..e7b88381e 100644 --- a/pkg/middlewares/chain/chain.go +++ b/pkg/middlewares/chain/chain.go @@ -13,14 +13,14 @@ const ( typeName = "Chain" ) -type chainBuilder interface { - BuildChain(ctx context.Context, middlewares []string) *alice.Chain +type middlewareChainBuilder interface { + BuildMiddlewareChain(ctx context.Context, middlewares []string) *alice.Chain } // New creates a chain middleware. -func New(ctx context.Context, next http.Handler, config dynamic.Chain, builder chainBuilder, name string) (http.Handler, error) { +func New(ctx context.Context, next http.Handler, config dynamic.Chain, builder middlewareChainBuilder, name string) (http.Handler, error) { middlewares.GetLogger(ctx, name, typeName).Debug().Msg("Creating middleware") - middlewareChain := builder.BuildChain(ctx, config.Middlewares) + middlewareChain := builder.BuildMiddlewareChain(ctx, config.Middlewares) return middlewareChain.Then(next) } diff --git a/pkg/provider/kubernetes/crd/fixtures/with_traefik_service_middleware.yml b/pkg/provider/kubernetes/crd/fixtures/with_traefik_service_middleware.yml new file mode 100644 index 000000000..6989e5dc1 --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/with_traefik_service_middleware.yml @@ -0,0 +1,45 @@ +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: stripprefix + namespace: default + +spec: + stripPrefix: + prefixes: + - /tobestripped + +--- +apiVersion: traefik.io/v1alpha1 +kind: TraefikService +metadata: + name: test-weighted + namespace: default + +spec: + weighted: + services: + - name: whoami + port: 80 + weight: 1 + middlewares: + - name: stripprefix + +--- +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: test.route + namespace: default + +spec: + entryPoints: + - web + + routes: + - match: Host(`foo.com`) && PathPrefix(`/bar`) + kind: Rule + priority: 12 + services: + - name: test-weighted + kind: TraefikService \ No newline at end of file diff --git a/pkg/provider/kubernetes/crd/generated/applyconfiguration/traefikio/v1alpha1/loadbalancerspec.go b/pkg/provider/kubernetes/crd/generated/applyconfiguration/traefikio/v1alpha1/loadbalancerspec.go index 726f425a2..d4fffd444 100644 --- a/pkg/provider/kubernetes/crd/generated/applyconfiguration/traefikio/v1alpha1/loadbalancerspec.go +++ b/pkg/provider/kubernetes/crd/generated/applyconfiguration/traefikio/v1alpha1/loadbalancerspec.go @@ -37,6 +37,7 @@ type LoadBalancerSpecApplyConfiguration struct { Name *string `json:"name,omitempty"` Kind *string `json:"kind,omitempty"` Namespace *string `json:"namespace,omitempty"` + Middlewares []MiddlewareRefApplyConfiguration `json:"middlewares,omitempty"` Sticky *dynamic.Sticky `json:"sticky,omitempty"` Port *intstr.IntOrString `json:"port,omitempty"` Scheme *string `json:"scheme,omitempty"` @@ -81,6 +82,19 @@ func (b *LoadBalancerSpecApplyConfiguration) WithNamespace(value string) *LoadBa return b } +// WithMiddlewares adds the given value to the Middlewares field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Middlewares field. +func (b *LoadBalancerSpecApplyConfiguration) WithMiddlewares(values ...*MiddlewareRefApplyConfiguration) *LoadBalancerSpecApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithMiddlewares") + } + b.Middlewares = append(b.Middlewares, *values[i]) + } + return b +} + // WithSticky sets the Sticky field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Sticky field is set to the value of the last call. diff --git a/pkg/provider/kubernetes/crd/generated/applyconfiguration/traefikio/v1alpha1/mirroring.go b/pkg/provider/kubernetes/crd/generated/applyconfiguration/traefikio/v1alpha1/mirroring.go index 7140f1c37..1c7981fbf 100644 --- a/pkg/provider/kubernetes/crd/generated/applyconfiguration/traefikio/v1alpha1/mirroring.go +++ b/pkg/provider/kubernetes/crd/generated/applyconfiguration/traefikio/v1alpha1/mirroring.go @@ -70,6 +70,19 @@ func (b *MirroringApplyConfiguration) WithNamespace(value string) *MirroringAppl return b } +// WithMiddlewares adds the given value to the Middlewares field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Middlewares field. +func (b *MirroringApplyConfiguration) WithMiddlewares(values ...*MiddlewareRefApplyConfiguration) *MirroringApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithMiddlewares") + } + b.LoadBalancerSpecApplyConfiguration.Middlewares = append(b.LoadBalancerSpecApplyConfiguration.Middlewares, *values[i]) + } + return b +} + // WithSticky sets the Sticky field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Sticky field is set to the value of the last call. diff --git a/pkg/provider/kubernetes/crd/generated/applyconfiguration/traefikio/v1alpha1/mirrorservice.go b/pkg/provider/kubernetes/crd/generated/applyconfiguration/traefikio/v1alpha1/mirrorservice.go index f7fde8d3d..79dd60441 100644 --- a/pkg/provider/kubernetes/crd/generated/applyconfiguration/traefikio/v1alpha1/mirrorservice.go +++ b/pkg/provider/kubernetes/crd/generated/applyconfiguration/traefikio/v1alpha1/mirrorservice.go @@ -68,6 +68,19 @@ func (b *MirrorServiceApplyConfiguration) WithNamespace(value string) *MirrorSer return b } +// WithMiddlewares adds the given value to the Middlewares field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Middlewares field. +func (b *MirrorServiceApplyConfiguration) WithMiddlewares(values ...*MiddlewareRefApplyConfiguration) *MirrorServiceApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithMiddlewares") + } + b.LoadBalancerSpecApplyConfiguration.Middlewares = append(b.LoadBalancerSpecApplyConfiguration.Middlewares, *values[i]) + } + return b +} + // WithSticky sets the Sticky field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Sticky field is set to the value of the last call. diff --git a/pkg/provider/kubernetes/crd/generated/applyconfiguration/traefikio/v1alpha1/service.go b/pkg/provider/kubernetes/crd/generated/applyconfiguration/traefikio/v1alpha1/service.go index d0b8342c1..3b3dcfd41 100644 --- a/pkg/provider/kubernetes/crd/generated/applyconfiguration/traefikio/v1alpha1/service.go +++ b/pkg/provider/kubernetes/crd/generated/applyconfiguration/traefikio/v1alpha1/service.go @@ -67,6 +67,19 @@ func (b *ServiceApplyConfiguration) WithNamespace(value string) *ServiceApplyCon return b } +// WithMiddlewares adds the given value to the Middlewares field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Middlewares field. +func (b *ServiceApplyConfiguration) WithMiddlewares(values ...*MiddlewareRefApplyConfiguration) *ServiceApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithMiddlewares") + } + b.LoadBalancerSpecApplyConfiguration.Middlewares = append(b.LoadBalancerSpecApplyConfiguration.Middlewares, *values[i]) + } + return b +} + // WithSticky sets the Sticky field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Sticky field is set to the value of the last call. diff --git a/pkg/provider/kubernetes/crd/kubernetes.go b/pkg/provider/kubernetes/crd/kubernetes.go index e1bf9dbbb..b26d9925c 100644 --- a/pkg/provider/kubernetes/crd/kubernetes.go +++ b/pkg/provider/kubernetes/crd/kubernetes.go @@ -266,7 +266,7 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client) continue } - errorPage, errorPageService, err := p.createErrorPageMiddleware(client, middleware.Namespace, middleware.Spec.Errors) + errorPage, errorPageService, err := p.createErrorPageMiddleware(ctxMid, client, middleware.Namespace, middleware.Spec.Errors) if err != nil { logger.Error().Err(err).Msg("Error while reading error page middleware") continue @@ -645,7 +645,7 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client) return conf } -func (p *Provider) createErrorPageMiddleware(client Client, namespace string, errorPage *traefikv1alpha1.ErrorPage) (*dynamic.ErrorPage, *dynamic.Service, error) { +func (p *Provider) createErrorPageMiddleware(ctx context.Context, client Client, namespace string, errorPage *traefikv1alpha1.ErrorPage) (*dynamic.ErrorPage, *dynamic.Service, error) { if errorPage == nil { return nil, nil, nil } @@ -663,7 +663,7 @@ func (p *Provider) createErrorPageMiddleware(client Client, namespace string, er allowEmptyServices: p.AllowEmptyServices, } - balancerServerHTTP, err := cb.buildServersLB(namespace, errorPage.Service.LoadBalancerSpec) + balancerServerHTTP, err := cb.buildServersLB(ctx, namespace, errorPage.Service.LoadBalancerSpec) if err != nil { return nil, nil, err } diff --git a/pkg/provider/kubernetes/crd/kubernetes_http.go b/pkg/provider/kubernetes/crd/kubernetes_http.go index cb77b4c4c..d9b3845b4 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_http.go +++ b/pkg/provider/kubernetes/crd/kubernetes_http.go @@ -82,7 +82,7 @@ func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Cli serviceKey := makeServiceKey(route.Match, ingressName) - mds, err := p.makeMiddlewareKeys(ctx, ingressRoute.Namespace, route.Middlewares) + mds, err := makeMiddlewareKeys(ctx, ingressRoute.Namespace, route.Middlewares, p.AllowCrossNamespace) if err != nil { logger.Error().Err(err).Msg("Failed to create middleware keys") continue @@ -172,13 +172,13 @@ func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Cli return conf } -func (p *Provider) makeMiddlewareKeys(ctx context.Context, ingRouteNamespace string, middlewares []traefikv1alpha1.MiddlewareRef) ([]string, error) { +func makeMiddlewareKeys(ctx context.Context, namespace string, middlewares []traefikv1alpha1.MiddlewareRef, allowCrossNamespace bool) ([]string, error) { var mds []string for _, mi := range middlewares { name := mi.Name - if !p.AllowCrossNamespace && strings.HasSuffix(mi.Name, providerNamespaceSeparator+providerName) { + if !allowCrossNamespace && strings.HasSuffix(mi.Name, providerNamespaceSeparator+providerName) { // Since we are not able to know if another namespace is in the name (namespace-name@kubernetescrd), // if the provider namespace kubernetescrd is used, // we don't allow this format to avoid cross namespace references. @@ -196,10 +196,10 @@ func (p *Provider) makeMiddlewareKeys(ctx context.Context, ingRouteNamespace str continue } - ns := ingRouteNamespace + ns := namespace if len(mi.Namespace) > 0 { - if !isNamespaceAllowed(p.AllowCrossNamespace, ingRouteNamespace, mi.Namespace) { - return nil, fmt.Errorf("middleware %s/%s is not in the IngressRoute namespace %s", mi.Namespace, mi.Name, ingRouteNamespace) + if !isNamespaceAllowed(allowCrossNamespace, namespace, mi.Namespace) { + return nil, fmt.Errorf("middleware %s/%s is not in the parent namespace %s", mi.Namespace, mi.Name, namespace) } ns = mi.Namespace @@ -333,6 +333,7 @@ func (c configBuilder) buildServicesLB(ctx context.Context, namespace string, tS Sticky: sticky, }, } + return nil } @@ -378,7 +379,7 @@ func (c configBuilder) buildMirroring(ctx context.Context, tService *traefikv1al } // buildServersLB creates the configuration for the load-balancer of servers defined by svc. -func (c configBuilder) buildServersLB(namespace string, svc traefikv1alpha1.LoadBalancerSpec) (*dynamic.Service, error) { +func (c configBuilder) buildServersLB(ctx context.Context, namespace string, svc traefikv1alpha1.LoadBalancerSpec) (*dynamic.Service, error) { lb := &dynamic.ServersLoadBalancer{} lb.SetDefaults() @@ -501,7 +502,16 @@ func (c configBuilder) buildServersLB(namespace string, svc traefikv1alpha1.Load return nil, err } - return &dynamic.Service{LoadBalancer: lb}, nil + service := &dynamic.Service{LoadBalancer: lb} + if len(svc.Middlewares) > 0 { + mds, err := makeMiddlewareKeys(ctx, namespace, svc.Middlewares, c.allowCrossNamespace) + if err != nil { + return nil, fmt.Errorf("could not create middleware keys: %w", err) + } + service.Middlewares = mds + } + + return service, nil } func (c configBuilder) makeServersTransportKey(parentNamespace string, serversTransportName string) (string, error) { @@ -687,7 +697,7 @@ func (c configBuilder) nameAndService(ctx context.Context, parentNamespace strin switch service.Kind { case "", "Service": - serversLB, err := c.buildServersLB(namespace, service) + serversLB, err := c.buildServersLB(ctx, namespace, service) if err != nil { return "", nil, err } diff --git a/pkg/provider/kubernetes/crd/kubernetes_test.go b/pkg/provider/kubernetes/crd/kubernetes_test.go index 39b9fafb3..64b922509 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_test.go +++ b/pkg/provider/kubernetes/crd/kubernetes_test.go @@ -3279,6 +3279,71 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, }, + { + desc: "TraefikService with service middleware", + paths: []string{"services.yml", "with_traefik_service_middleware.yml"}, + expected: &dynamic.Configuration{ + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + TLS: &dynamic.TLSConfiguration{}, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + ServersTransports: map[string]*dynamic.TCPServersTransport{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "default-test-route-6b204d94623b3df4370c": { + EntryPoints: []string{"web"}, + Service: "default-test-weighted", + Rule: "Host(`foo.com`) && PathPrefix(`/bar`)", + Priority: 12, + }, + }, + Middlewares: map[string]*dynamic.Middleware{ + "default-stripprefix": { + StripPrefix: &dynamic.StripPrefix{ + Prefixes: []string{"/tobestripped"}, + }, + }, + }, + Services: map[string]*dynamic.Service{ + "default-test-weighted": { + Weighted: &dynamic.WeightedRoundRobin{ + Services: []dynamic.WRRService{ + { + Name: "default-whoami-80", + Weight: pointer(1), + }, + }, + }, + }, + "default-whoami-80": { + Middlewares: []string{"default-stripprefix"}, + LoadBalancer: &dynamic.ServersLoadBalancer{ + Strategy: dynamic.BalancerStrategyWRR, + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:80", + }, + { + URL: "http://10.10.0.2:80", + }, + }, + PassHostHeader: pointer(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, { desc: "one kube services in a highest random weight", paths: []string{"with_highest_random_weight.yml"}, diff --git a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroute.go b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroute.go index 2ac5876f4..0c9151188 100644 --- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroute.go +++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroute.go @@ -110,6 +110,8 @@ type LoadBalancerSpec struct { Kind string `json:"kind,omitempty"` // Namespace defines the namespace of the referenced Kubernetes Service or TraefikService. Namespace string `json:"namespace,omitempty"` + // Middlewares defines the list of references to Middleware resources to apply to the service. + Middlewares []MiddlewareRef `json:"middlewares,omitempty"` // Sticky defines the sticky sessions configuration. // More info: https://doc.traefik.io/traefik/v3.6/reference/routing-configuration/http/load-balancing/service/#sticky-sessions Sticky *dynamic.Sticky `json:"sticky,omitempty"` diff --git a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go index e0cddac6b..c50c12230 100644 --- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go @@ -685,6 +685,11 @@ func (in *IngressRouteUDPSpec) DeepCopy() *IngressRouteUDPSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LoadBalancerSpec) DeepCopyInto(out *LoadBalancerSpec) { *out = *in + if in.Middlewares != nil { + in, out := &in.Middlewares, &out.Middlewares + *out = make([]MiddlewareRef, len(*in)) + copy(*out, *in) + } if in.Sticky != nil { in, out := &in.Sticky, &out.Sticky *out = new(dynamic.Sticky) diff --git a/pkg/provider/kubernetes/gateway/features.go b/pkg/provider/kubernetes/gateway/features.go index 88dedd573..5c63c4078 100644 --- a/pkg/provider/kubernetes/gateway/features.go +++ b/pkg/provider/kubernetes/gateway/features.go @@ -44,5 +44,6 @@ func extendedHTTPRouteFeatures() sets.Set[features.Feature] { features.HTTPRouteBackendProtocolH2CFeature, features.HTTPRouteBackendProtocolWebSocketFeature, features.HTTPRouteDestinationPortMatchingFeature, + features.HTTPRouteBackendRequestHeaderModificationFeature, ) } diff --git a/pkg/provider/kubernetes/gateway/fixtures/httproute/backend_filter_request_header_modifier.yml b/pkg/provider/kubernetes/gateway/fixtures/httproute/backend_filter_request_header_modifier.yml new file mode 100644 index 000000000..1d0e7be4c --- /dev/null +++ b/pkg/provider/kubernetes/gateway/fixtures/httproute/backend_filter_request_header_modifier.yml @@ -0,0 +1,62 @@ +--- +kind: GatewayClass +apiVersion: gateway.networking.k8s.io/v1 +metadata: + name: my-gateway-class +spec: + controllerName: traefik.io/gateway-controller + +--- +kind: Gateway +apiVersion: gateway.networking.k8s.io/v1 +metadata: + name: my-gateway + namespace: default +spec: + gatewayClassName: my-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + kinds: + - kind: HTTPRoute + group: gateway.networking.k8s.io + namespaces: + from: Same + +--- +kind: HTTPRoute +apiVersion: gateway.networking.k8s.io/v1 +metadata: + name: http-app-1 + namespace: default +spec: + parentRefs: + - name: my-gateway + kind: Gateway + group: gateway.networking.k8s.io + hostnames: + - "foo.com" + rules: + - matches: + - path: + type: PathPrefix + value: /bar + backendRefs: + - name: whoami + port: 80 + weight: 1 + kind: Service + group: "" + filters: + - type: RequestHeaderModifier + requestHeaderModifier: + set: + - name: X-Foo + value: Bar + add: + - name: X-Bar + value: Foo + remove: + - X-Baz \ No newline at end of file diff --git a/pkg/provider/kubernetes/gateway/httproute.go b/pkg/provider/kubernetes/gateway/httproute.go index 158014af8..9028b024c 100644 --- a/pkg/provider/kubernetes/gateway/httproute.go +++ b/pkg/provider/kubernetes/gateway/httproute.go @@ -140,6 +140,7 @@ func (p *Provider) loadHTTPRoute(ctx context.Context, listener gatewayListener, var err error routerName := makeRouterName(rule, routeKey) + // TODO loadMiddlewares errors could change the condition. router.Middlewares, err = p.loadMiddlewares(conf, route.Namespace, routerName, routeRule.Filters, match.Path) switch { case err != nil: @@ -164,7 +165,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) + router.Service, serviceCondition = p.loadWRRService(ctx, listener, conf, routerName, routeRule, route, match.Path) if serviceCondition != nil { condition = *serviceCondition } @@ -179,7 +180,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) (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 @@ -188,7 +189,9 @@ func (p *Provider) loadWRRService(ctx context.Context, listener gatewayListener, var wrr dynamic.WeightedRoundRobin var condition *metav1.Condition for _, backendRef := range routeRule.BackendRefs { - svcName, errCondition := p.loadService(ctx, listener, conf, route, backendRef) + // 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(ctx, listener, conf, route, backendRef, pathMatch) weight := ptr.To(int(ptr.Deref(backendRef.Weight, 1))) if errCondition != nil { log.Ctx(ctx).Error(). @@ -215,7 +218,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(ctx context.Context, listener gatewayListener, conf *dynamic.Configuration, route *gatev1.HTTPRoute, backendRef gatev1.HTTPBackendRef) (string, *metav1.Condition) { +func (p *Provider) loadService(ctx context.Context, listener gatewayListener, conf *dynamic.Configuration, route *gatev1.HTTPRoute, backendRef gatev1.HTTPBackendRef, pathMatch *gatev1.HTTPPathMatch) (string, *metav1.Condition) { kind := ptr.Deref(backendRef.Kind, kindService) group := groupCore @@ -241,6 +244,19 @@ func (p *Provider) loadService(ctx context.Context, listener gatewayListener, co } } + middlewares, err := p.loadMiddlewares(conf, namespace, serviceName, backendRef.Filters, pathMatch) + if err != nil { + return serviceName, &metav1.Condition{ + Type: string(gatev1.RouteConditionResolvedRefs), + Status: metav1.ConditionFalse, + ObservedGeneration: route.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.RouteReasonInvalidKind), + Message: fmt.Sprintf("Cannot load filters on HTTPBackendRef %s/%s/%s/%s: %s", group, kind, namespace, backendRef.Name, err), + } + } + + // TODO may be we could incorporate this "ignored" case into the loadHTTPBackendRef. if group != groupCore || kind != kindService { name, service, err := p.loadHTTPBackendRef(namespace, backendRef) if err != nil { @@ -255,6 +271,7 @@ func (p *Provider) loadService(ctx context.Context, listener gatewayListener, co } if service != nil { + service.Middlewares = middlewares conf.HTTP.Services[name] = service } @@ -286,7 +303,7 @@ func (p *Provider) loadService(ctx context.Context, listener gatewayListener, co conf.HTTP.ServersTransports[serviceName] = st } - conf.HTTP.Services[serviceName] = &dynamic.Service{LoadBalancer: lb} + conf.HTTP.Services[serviceName] = &dynamic.Service{LoadBalancer: lb, Middlewares: middlewares} return serviceName, nil } diff --git a/pkg/provider/kubernetes/gateway/kubernetes_test.go b/pkg/provider/kubernetes/gateway/kubernetes_test.go index 1f5c1df8f..b6b19d80e 100644 --- a/pkg/provider/kubernetes/gateway/kubernetes_test.go +++ b/pkg/provider/kubernetes/gateway/kubernetes_test.go @@ -1917,6 +1917,77 @@ func TestLoadHTTPRoutes(t *testing.T) { TLS: &dynamic.TLSConfiguration{}, }, }, + { + desc: "Simple HTTPRoute, backend filter request header modifier", + paths: []string{"services.yml", "httproute/backend_filter_request_header_modifier.yml"}, + entryPoints: map[string]Entrypoint{"web": { + Address: ":80", + }}, + expected: &dynamic.Configuration{ + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + ServersTransports: map[string]*dynamic.TCPServersTransport{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-99f4d29346f69ccb6fc3": { + EntryPoints: []string{"web"}, + Service: "httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-99f4d29346f69ccb6fc3-wrr", + Rule: "Host(`foo.com`) && (Path(`/bar`) || PathPrefix(`/bar/`))", + Priority: 10408, + RuleSyntax: "default", + }, + }, + Middlewares: map[string]*dynamic.Middleware{ + "default-whoami-http-requestheadermodifier-0": { + RequestHeaderModifier: &dynamic.HeaderModifier{ + Set: map[string]string{"X-Foo": "Bar"}, + Add: map[string]string{"X-Bar": "Foo"}, + Remove: []string{"X-Baz"}, + }, + }, + }, + Services: map[string]*dynamic.Service{ + "httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-99f4d29346f69ccb6fc3-wrr": { + Weighted: &dynamic.WeightedRoundRobin{ + Services: []dynamic.WRRService{ + { + Name: "default-whoami-http-80", + Weight: ptr.To(1), + }, + }, + }, + }, + "default-whoami-http-80": { + Middlewares: []string{"default-whoami-http-requestheadermodifier-0"}, + LoadBalancer: &dynamic.ServersLoadBalancer{ + Strategy: dynamic.BalancerStrategyWRR, + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:80", + }, + { + URL: "http://10.10.0.2:80", + }, + }, + PassHostHeader: ptr.To(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + TLS: &dynamic.TLSConfiguration{}, + }, + }, { desc: "Simple HTTPRoute, redirect HTTP to HTTPS", paths: []string{"services.yml", "httproute/filter_http_to_https.yml"}, @@ -1954,6 +2025,7 @@ func TestLoadHTTPRoutes(t *testing.T) { }, }, }, + Services: map[string]*dynamic.Service{ "httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-364ce6ec04c3d49b19c4-wrr": { Weighted: &dynamic.WeightedRoundRobin{}, diff --git a/pkg/provider/kubernetes/ingress/annotations.go b/pkg/provider/kubernetes/ingress/annotations.go index fe7f52d52..bbdc4b8d6 100644 --- a/pkg/provider/kubernetes/ingress/annotations.go +++ b/pkg/provider/kubernetes/ingress/annotations.go @@ -46,6 +46,7 @@ type ServiceIng struct { ServersScheme string `json:"serversScheme,omitempty"` ServersTransport string `json:"serversTransport,omitempty"` PassHostHeader *bool `json:"passHostHeader"` + Middlewares []string `json:"middlewares,omitempty"` Sticky *dynamic.Sticky `json:"sticky,omitempty" label:"allowEmpty"` NativeLB *bool `json:"nativeLB,omitempty"` NodePortLB bool `json:"nodePortLB,omitempty"` diff --git a/pkg/provider/kubernetes/ingress/annotations_test.go b/pkg/provider/kubernetes/ingress/annotations_test.go index 1f86f51c7..cf6df8ad8 100644 --- a/pkg/provider/kubernetes/ingress/annotations_test.go +++ b/pkg/provider/kubernetes/ingress/annotations_test.go @@ -161,6 +161,18 @@ func Test_parseServiceConfig(t *testing.T) { }, }, }, + { + desc: "service middlewares annotation", + annotations: map[string]string{ + "traefik.ingress.kubernetes.io/service.middlewares": "middleware1,middleware2", + }, + expected: &ServiceConfig{ + Service: &ServiceIng{ + Middlewares: []string{"middleware1", "middleware2"}, + PassHostHeader: pointer(true), + }, + }, + }, { desc: "empty map", annotations: map[string]string{}, diff --git a/pkg/provider/kubernetes/ingress/kubernetes.go b/pkg/provider/kubernetes/ingress/kubernetes.go index 714aebdfb..de4073214 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes.go +++ b/pkg/provider/kubernetes/ingress/kubernetes.go @@ -563,6 +563,7 @@ func (p *Provider) loadService(client Client, namespace string, backend netv1.In if svcConfig != nil && svcConfig.Service != nil { svc.LoadBalancer.Sticky = svcConfig.Service.Sticky + svc.Middlewares = svcConfig.Service.Middlewares if svcConfig.Service.PassHostHeader != nil { svc.LoadBalancer.PassHostHeader = svcConfig.Service.PassHostHeader diff --git a/pkg/server/middleware/middlewares.go b/pkg/server/middleware/middlewares.go index f871e93c4..88707eb70 100644 --- a/pkg/server/middleware/middlewares.go +++ b/pkg/server/middleware/middlewares.go @@ -56,8 +56,8 @@ func NewBuilder(configs map[string]*runtime.MiddlewareInfo, serviceBuilder servi return &Builder{configs: configs, serviceBuilder: serviceBuilder, pluginBuilder: pluginBuilder} } -// BuildChain creates a middleware chain. -func (b *Builder) BuildChain(ctx context.Context, middlewares []string) *alice.Chain { +// BuildMiddlewareChain creates a middleware chain. +func (b *Builder) BuildMiddlewareChain(ctx context.Context, middlewares []string) *alice.Chain { chain := alice.New() for _, name := range middlewares { middlewareName := provider.GetQualifiedName(ctx, name) diff --git a/pkg/server/middleware/middlewares_test.go b/pkg/server/middleware/middlewares_test.go index facaa716c..a972f33b2 100644 --- a/pkg/server/middleware/middlewares_test.go +++ b/pkg/server/middleware/middlewares_test.go @@ -19,7 +19,7 @@ func TestBuilder_BuildChainNilConfig(t *testing.T) { } middlewaresBuilder := NewBuilder(testConfig, nil, nil) - chain := middlewaresBuilder.BuildChain(t.Context(), []string{"empty"}) + chain := middlewaresBuilder.BuildMiddlewareChain(t.Context(), []string{"empty"}) _, err := chain.Then(nil) require.Error(t, err) } @@ -30,7 +30,7 @@ func TestBuilder_BuildChainNonExistentChain(t *testing.T) { } middlewaresBuilder := NewBuilder(testConfig, nil, nil) - chain := middlewaresBuilder.BuildChain(t.Context(), []string{"empty"}) + chain := middlewaresBuilder.BuildMiddlewareChain(t.Context(), []string{"empty"}) _, err := chain.Then(nil) require.Error(t, err) } @@ -271,7 +271,7 @@ func TestBuilder_BuildChainWithContext(t *testing.T) { }) builder := NewBuilder(rtConf.Middlewares, nil, nil) - result := builder.BuildChain(ctx, test.buildChain) + result := builder.BuildMiddlewareChain(ctx, test.buildChain) handlers, err := result.Then(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })) if test.expectedError != nil { diff --git a/pkg/server/router/router.go b/pkg/server/router/router.go index fc9190e4b..594b77efe 100644 --- a/pkg/server/router/router.go +++ b/pkg/server/router/router.go @@ -28,8 +28,8 @@ import ( const maxUserPriority = math.MaxInt - 1000 -type middlewareBuilder interface { - BuildChain(ctx context.Context, names []string) *alice.Chain +type middlewareChainBuilder interface { + BuildMiddlewareChain(ctx context.Context, names []string) *alice.Chain } type serviceManager interface { @@ -42,7 +42,7 @@ type Manager struct { routerHandlers map[string]http.Handler serviceManager serviceManager observabilityMgr *middleware.ObservabilityMgr - middlewaresBuilder middlewareBuilder + middlewaresBuilder middlewareChainBuilder conf *runtime.Configuration tlsManager *tls.Manager parser httpmuxer.SyntaxParser @@ -51,7 +51,7 @@ type Manager struct { // NewManager creates a new Manager. func NewManager(conf *runtime.Configuration, serviceManager serviceManager, - middlewaresBuilder middlewareBuilder, + middlewaresBuilder middlewareChainBuilder, observabilityMgr *middleware.ObservabilityMgr, tlsManager *tls.Manager, parser httpmuxer.SyntaxParser, @@ -372,7 +372,7 @@ func (m *Manager) buildHTTPHandler(ctx context.Context, router *runtime.RouterIn }) } - mHandler := m.middlewaresBuilder.BuildChain(ctx, router.Middlewares) + mHandler := m.middlewaresBuilder.BuildMiddlewareChain(ctx, router.Middlewares) return chain.Extend(*mHandler).Then(nextHandler) } diff --git a/pkg/server/router/router_test.go b/pkg/server/router/router_test.go index eb334d183..6b058c3ad 100644 --- a/pkg/server/router/router_test.go +++ b/pkg/server/router/router_test.go @@ -2000,7 +2000,7 @@ func (m *mockServiceManager) LaunchHealthCheck(_ context.Context) {} type mockMiddlewareBuilder struct{} -func (m *mockMiddlewareBuilder) BuildChain(_ context.Context, _ []string) *alice.Chain { +func (m *mockMiddlewareBuilder) BuildMiddlewareChain(_ context.Context, _ []string) *alice.Chain { chain := alice.New() return &chain } diff --git a/pkg/server/routerfactory.go b/pkg/server/routerfactory.go index 698de82b4..503a5c23a 100644 --- a/pkg/server/routerfactory.go +++ b/pkg/server/routerfactory.go @@ -104,6 +104,8 @@ func (f *RouterFactory) CreateRouters(rtConf *runtime.Configuration) (map[string middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, f.pluginBuilder) + serviceManager.SetMiddlewareChainBuilder(middlewaresBuilder) + routerManager := router.NewManager(rtConf, serviceManager, middlewaresBuilder, f.observabilityMgr, f.tlsManager, f.parser) routerManager.ParseRouterTree() diff --git a/pkg/server/service/service.go b/pkg/server/service/service.go index 23d1daf6d..599c943e0 100644 --- a/pkg/server/service/service.go +++ b/pkg/server/service/service.go @@ -48,6 +48,10 @@ type ServiceBuilder interface { BuildHTTP(rootCtx context.Context, serviceName string) (http.Handler, error) } +type middlewareChainBuilder interface { + BuildMiddlewareChain(ctx context.Context, middlewares []string) *alice.Chain +} + // Manager The service manager. type Manager struct { routinePool *safe.Pool @@ -56,10 +60,11 @@ type Manager struct { proxyBuilder ProxyBuilder serviceBuilders []ServiceBuilder - services map[string]http.Handler - configs map[string]*runtime.ServiceInfo - healthCheckers map[string]*healthcheck.ServiceHealthChecker - rand *rand.Rand // For the initial shuffling of load-balancers. + services map[string]http.Handler + configs map[string]*runtime.ServiceInfo + healthCheckers map[string]*healthcheck.ServiceHealthChecker + rand *rand.Rand // For the initial shuffling of load-balancers. + middlewareChainBuilder middlewareChainBuilder } // NewManager creates a new Manager. @@ -77,6 +82,11 @@ func NewManager(configs map[string]*runtime.ServiceInfo, observabilityMgr *middl } } +// SetMiddlewareChainBuilder sets the MiddlewareChainBuilder. +func (m *Manager) SetMiddlewareChainBuilder(middlewareChainBuilder middlewareChainBuilder) { + m.middlewareChainBuilder = middlewareChainBuilder +} + // BuildHTTP Creates a http.Handler for a service configuration. func (m *Manager) BuildHTTP(rootCtx context.Context, serviceName string) (http.Handler, error) { serviceName = provider.GetQualifiedName(rootCtx, serviceName) @@ -113,7 +123,7 @@ func (m *Manager) BuildHTTP(rootCtx context.Context, serviceName string) (http.H value := reflect.ValueOf(*conf.Service) var count int for i := range value.NumField() { - if !value.Field(i).IsNil() { + if value.Type().Field(i).Name != "Middlewares" && !value.Field(i).IsNil() { count++ } } @@ -173,9 +183,22 @@ func (m *Manager) BuildHTTP(rootCtx context.Context, serviceName string) (http.H return nil, sErr } - m.services[serviceName] = lb + if len(conf.Middlewares) > 0 { + if m.middlewareChainBuilder == nil { + // This should happen only in tests. + return nil, errors.New("chain builder not defined") + } + chain := m.middlewareChainBuilder.BuildMiddlewareChain(ctx, conf.Middlewares) + var err error + lb, err = chain.Then(lb) + if err != nil { + conf.AddError(err, true) + return nil, err + } + } - return lb, nil + m.services[serviceName] = lb + return m.services[serviceName], nil } // LaunchHealthCheck launches the health checks.