diff --git a/integration/k8s_conformance_test.go b/integration/k8s_conformance_test.go index 2768ce99e..948174d6f 100644 --- a/integration/k8s_conformance_test.go +++ b/integration/k8s_conformance_test.go @@ -86,8 +86,8 @@ func (s *K8sConformanceSuite) SetupSuite() { s.T().Fatal("Traefik image is not present") } - s.k3sContainer, err = k3s.RunContainer(ctx, - testcontainers.WithImage(k3sImage), + s.k3sContainer, err = k3s.Run(ctx, + k3sImage, k3s.WithManifest("./fixtures/k8s-conformance/00-experimental-v1.1.0.yml"), k3s.WithManifest("./fixtures/k8s-conformance/01-rbac.yml"), k3s.WithManifest("./fixtures/k8s-conformance/02-traefik.yml"), @@ -206,6 +206,7 @@ func (s *K8sConformanceSuite) TestK8sGatewayAPIConformance() { features.SupportHTTPRouteHostRewrite, features.SupportHTTPRoutePathRewrite, features.SupportHTTPRoutePathRedirect, + features.SupportHTTPRouteResponseHeaderModification, ), }) require.NoError(s.T(), err) diff --git a/pkg/config/dynamic/middlewares.go b/pkg/config/dynamic/middlewares.go index 36fc64104..9c65338a4 100644 --- a/pkg/config/dynamic/middlewares.go +++ b/pkg/config/dynamic/middlewares.go @@ -40,10 +40,11 @@ type Middleware struct { Plugin map[string]PluginConf `json:"plugin,omitempty" toml:"plugin,omitempty" yaml:"plugin,omitempty" export:"true"` - // Gateway API HTTPRoute filters middlewares. - RequestHeaderModifier *RequestHeaderModifier `json:"requestHeaderModifier,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"` - RequestRedirect *RequestRedirect `json:"requestRedirect,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"` - URLRewrite *URLRewrite `json:"URLRewrite,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"` + // Gateway API filter middlewares. + RequestHeaderModifier *HeaderModifier `json:"requestHeaderModifier,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"` + ResponseHeaderModifier *HeaderModifier `json:"responseHeaderModifier,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"` + RequestRedirect *RequestRedirect `json:"requestRedirect,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"` + URLRewrite *URLRewrite `json:"URLRewrite,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"` } // +k8s:deepcopy-gen=true @@ -694,8 +695,8 @@ type Users []string // +k8s:deepcopy-gen=true -// RequestHeaderModifier holds the request header modifier configuration. -type RequestHeaderModifier struct { +// HeaderModifier holds the request/response header modifier configuration. +type HeaderModifier struct { Set map[string]string `json:"set,omitempty"` Add map[string]string `json:"add,omitempty"` Remove []string `json:"remove,omitempty"` diff --git a/pkg/config/dynamic/zz_generated.deepcopy.go b/pkg/config/dynamic/zz_generated.deepcopy.go index 2011b2f6b..582136725 100644 --- a/pkg/config/dynamic/zz_generated.deepcopy.go +++ b/pkg/config/dynamic/zz_generated.deepcopy.go @@ -506,6 +506,41 @@ func (in *HTTPConfiguration) DeepCopy() *HTTPConfiguration { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HeaderModifier) DeepCopyInto(out *HeaderModifier) { + *out = *in + if in.Set != nil { + in, out := &in.Set, &out.Set + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Add != nil { + in, out := &in.Add, &out.Add + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Remove != nil { + in, out := &in.Remove, &out.Remove + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HeaderModifier. +func (in *HeaderModifier) DeepCopy() *HeaderModifier { + if in == nil { + return nil + } + out := new(HeaderModifier) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Headers) DeepCopyInto(out *Headers) { *out = *in @@ -866,7 +901,12 @@ func (in *Middleware) DeepCopyInto(out *Middleware) { } if in.RequestHeaderModifier != nil { in, out := &in.RequestHeaderModifier, &out.RequestHeaderModifier - *out = new(RequestHeaderModifier) + *out = new(HeaderModifier) + (*in).DeepCopyInto(*out) + } + if in.ResponseHeaderModifier != nil { + in, out := &in.ResponseHeaderModifier, &out.ResponseHeaderModifier + *out = new(HeaderModifier) (*in).DeepCopyInto(*out) } if in.RequestRedirect != nil { @@ -1087,41 +1127,6 @@ func (in *ReplacePathRegex) DeepCopy() *ReplacePathRegex { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RequestHeaderModifier) DeepCopyInto(out *RequestHeaderModifier) { - *out = *in - if in.Set != nil { - in, out := &in.Set, &out.Set - *out = make(map[string]string, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } - if in.Add != nil { - in, out := &in.Add, &out.Add - *out = make(map[string]string, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } - if in.Remove != nil { - in, out := &in.Remove, &out.Remove - *out = make([]string, len(*in)) - copy(*out, *in) - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RequestHeaderModifier. -func (in *RequestHeaderModifier) DeepCopy() *RequestHeaderModifier { - if in == nil { - return nil - } - out := new(RequestHeaderModifier) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RequestRedirect) DeepCopyInto(out *RequestRedirect) { *out = *in diff --git a/pkg/middlewares/gatewayapi/headermodifier/request_header_modifier.go b/pkg/middlewares/gatewayapi/headermodifier/request_header_modifier.go index 1f4536362..94d43211a 100644 --- a/pkg/middlewares/gatewayapi/headermodifier/request_header_modifier.go +++ b/pkg/middlewares/gatewayapi/headermodifier/request_header_modifier.go @@ -9,7 +9,7 @@ import ( "go.opentelemetry.io/otel/trace" ) -const typeName = "RequestHeaderModifier" +const requestHeaderModifierTypeName = "RequestHeaderModifier" // requestHeaderModifier is a middleware used to modify the headers of an HTTP request. type requestHeaderModifier struct { @@ -22,8 +22,8 @@ type requestHeaderModifier struct { } // NewRequestHeaderModifier creates a new request header modifier middleware. -func NewRequestHeaderModifier(ctx context.Context, next http.Handler, config dynamic.RequestHeaderModifier, name string) http.Handler { - logger := middlewares.GetLogger(ctx, name, typeName) +func NewRequestHeaderModifier(ctx context.Context, next http.Handler, config dynamic.HeaderModifier, name string) http.Handler { + logger := middlewares.GetLogger(ctx, name, requestHeaderModifierTypeName) logger.Debug().Msg("Creating middleware") return &requestHeaderModifier{ @@ -36,7 +36,7 @@ func NewRequestHeaderModifier(ctx context.Context, next http.Handler, config dyn } func (r *requestHeaderModifier) GetTracingInformation() (string, string, trace.SpanKind) { - return r.name, typeName, trace.SpanKindUnspecified + return r.name, requestHeaderModifierTypeName, trace.SpanKindUnspecified } func (r *requestHeaderModifier) ServeHTTP(rw http.ResponseWriter, req *http.Request) { diff --git a/pkg/middlewares/gatewayapi/headermodifier/request_header_modifier_test.go b/pkg/middlewares/gatewayapi/headermodifier/request_header_modifier_test.go index d522d9850..a9c19980f 100644 --- a/pkg/middlewares/gatewayapi/headermodifier/request_header_modifier_test.go +++ b/pkg/middlewares/gatewayapi/headermodifier/request_header_modifier_test.go @@ -14,25 +14,25 @@ import ( func TestRequestHeaderModifier(t *testing.T) { testCases := []struct { desc string - config dynamic.RequestHeaderModifier + config dynamic.HeaderModifier requestHeaders http.Header expectedHeaders http.Header }{ { desc: "no config", - config: dynamic.RequestHeaderModifier{}, + config: dynamic.HeaderModifier{}, expectedHeaders: map[string][]string{}, }, { desc: "set header", - config: dynamic.RequestHeaderModifier{ + config: dynamic.HeaderModifier{ Set: map[string]string{"Foo": "Bar"}, }, expectedHeaders: map[string][]string{"Foo": {"Bar"}}, }, { desc: "set header with existing headers", - config: dynamic.RequestHeaderModifier{ + config: dynamic.HeaderModifier{ Set: map[string]string{"Foo": "Bar"}, }, requestHeaders: map[string][]string{"Foo": {"Baz"}, "Bar": {"Foo"}}, @@ -40,7 +40,7 @@ func TestRequestHeaderModifier(t *testing.T) { }, { desc: "set multiple headers with existing headers", - config: dynamic.RequestHeaderModifier{ + config: dynamic.HeaderModifier{ Set: map[string]string{"Foo": "Bar", "Bar": "Foo"}, }, requestHeaders: map[string][]string{"Foo": {"Baz"}, "Bar": {"Foobar"}}, @@ -48,14 +48,14 @@ func TestRequestHeaderModifier(t *testing.T) { }, { desc: "add header", - config: dynamic.RequestHeaderModifier{ + config: dynamic.HeaderModifier{ Add: map[string]string{"Foo": "Bar"}, }, expectedHeaders: map[string][]string{"Foo": {"Bar"}}, }, { desc: "add header with existing headers", - config: dynamic.RequestHeaderModifier{ + config: dynamic.HeaderModifier{ Add: map[string]string{"Foo": "Bar"}, }, requestHeaders: map[string][]string{"Foo": {"Baz"}, "Bar": {"Foo"}}, @@ -63,7 +63,7 @@ func TestRequestHeaderModifier(t *testing.T) { }, { desc: "add multiple headers with existing headers", - config: dynamic.RequestHeaderModifier{ + config: dynamic.HeaderModifier{ Add: map[string]string{"Foo": "Bar", "Bar": "Foo"}, }, requestHeaders: map[string][]string{"Foo": {"Baz"}, "Bar": {"Foobar"}}, @@ -71,14 +71,14 @@ func TestRequestHeaderModifier(t *testing.T) { }, { desc: "remove header", - config: dynamic.RequestHeaderModifier{ + config: dynamic.HeaderModifier{ Remove: []string{"Foo"}, }, expectedHeaders: map[string][]string{}, }, { desc: "remove header with existing headers", - config: dynamic.RequestHeaderModifier{ + config: dynamic.HeaderModifier{ Remove: []string{"Foo"}, }, requestHeaders: map[string][]string{"Foo": {"Baz"}, "Bar": {"Foo"}}, @@ -86,7 +86,7 @@ func TestRequestHeaderModifier(t *testing.T) { }, { desc: "remove multiple headers with existing headers", - config: dynamic.RequestHeaderModifier{ + config: dynamic.HeaderModifier{ Remove: []string{"Foo", "Bar"}, }, requestHeaders: map[string][]string{"Foo": {"Bar"}, "Bar": {"Foo"}, "Baz": {"Bar"}}, @@ -106,11 +106,11 @@ func TestRequestHeaderModifier(t *testing.T) { handler := NewRequestHeaderModifier(context.Background(), next, test.config, "foo-request-header-modifier") req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost", nil) - if test.requestHeaders != nil { - req.Header = test.requestHeaders + for h, v := range test.requestHeaders { + req.Header[h] = v } - resp := httptest.NewRecorder() + handler.ServeHTTP(resp, req) assert.Equal(t, test.expectedHeaders, gotHeaders) diff --git a/pkg/middlewares/gatewayapi/headermodifier/response_header_modifier.go b/pkg/middlewares/gatewayapi/headermodifier/response_header_modifier.go new file mode 100644 index 000000000..2d55b686b --- /dev/null +++ b/pkg/middlewares/gatewayapi/headermodifier/response_header_modifier.go @@ -0,0 +1,60 @@ +package headermodifier + +import ( + "context" + "net/http" + + "github.com/traefik/traefik/v3/pkg/config/dynamic" + "github.com/traefik/traefik/v3/pkg/middlewares" + "go.opentelemetry.io/otel/trace" +) + +const responseHeaderModifierTypeName = "ResponseHeaderModifier" + +// requestHeaderModifier is a middleware used to modify the headers of an HTTP response. +type responseHeaderModifier struct { + next http.Handler + name string + + set map[string]string + add map[string]string + remove []string +} + +// NewResponseHeaderModifier creates a new response header modifier middleware. +func NewResponseHeaderModifier(ctx context.Context, next http.Handler, config dynamic.HeaderModifier, name string) http.Handler { + logger := middlewares.GetLogger(ctx, name, responseHeaderModifierTypeName) + logger.Debug().Msg("Creating middleware") + + return &responseHeaderModifier{ + next: next, + name: name, + set: config.Set, + add: config.Add, + remove: config.Remove, + } +} + +func (r *responseHeaderModifier) GetTracingInformation() (string, string, trace.SpanKind) { + return r.name, responseHeaderModifierTypeName, trace.SpanKindUnspecified +} + +func (r *responseHeaderModifier) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + r.next.ServeHTTP(middlewares.NewResponseModifier(rw, req, r.modifyResponseHeaders), req) +} + +func (r *responseHeaderModifier) modifyResponseHeaders(res *http.Response) error { + for headerName, headerValue := range r.set { + res.Header.Set(headerName, headerValue) + } + + for headerName, headerValue := range r.add { + res.Header.Add(headerName, headerValue) + } + + for _, headerName := range r.remove { + res.Header.Del(headerName) + } + + return nil +} diff --git a/pkg/middlewares/gatewayapi/headermodifier/response_header_modifier_test.go b/pkg/middlewares/gatewayapi/headermodifier/response_header_modifier_test.go new file mode 100644 index 000000000..ceea62ca6 --- /dev/null +++ b/pkg/middlewares/gatewayapi/headermodifier/response_header_modifier_test.go @@ -0,0 +1,121 @@ +package headermodifier + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/traefik/traefik/v3/pkg/config/dynamic" + "github.com/traefik/traefik/v3/pkg/testhelpers" +) + +func TestResponseHeaderModifier(t *testing.T) { + testCases := []struct { + desc string + config dynamic.HeaderModifier + responseHeaders http.Header + expectedHeaders http.Header + }{ + { + desc: "no config", + config: dynamic.HeaderModifier{}, + expectedHeaders: map[string][]string{}, + }, + { + desc: "set header", + config: dynamic.HeaderModifier{ + Set: map[string]string{"Foo": "Bar"}, + }, + expectedHeaders: map[string][]string{"Foo": {"Bar"}}, + }, + { + desc: "set header with existing headers", + config: dynamic.HeaderModifier{ + Set: map[string]string{"Foo": "Bar"}, + }, + responseHeaders: map[string][]string{"Foo": {"Baz"}, "Bar": {"Foo"}}, + expectedHeaders: map[string][]string{"Foo": {"Bar"}, "Bar": {"Foo"}}, + }, + { + desc: "set multiple headers with existing headers", + config: dynamic.HeaderModifier{ + Set: map[string]string{"Foo": "Bar", "Bar": "Foo"}, + }, + responseHeaders: map[string][]string{"Foo": {"Baz"}, "Bar": {"Foobar"}}, + expectedHeaders: map[string][]string{"Foo": {"Bar"}, "Bar": {"Foo"}}, + }, + { + desc: "add header", + config: dynamic.HeaderModifier{ + Add: map[string]string{"Foo": "Bar"}, + }, + expectedHeaders: map[string][]string{"Foo": {"Bar"}}, + }, + { + desc: "add header with existing headers", + config: dynamic.HeaderModifier{ + Add: map[string]string{"Foo": "Bar"}, + }, + responseHeaders: map[string][]string{"Foo": {"Baz"}, "Bar": {"Foo"}}, + expectedHeaders: map[string][]string{"Foo": {"Baz", "Bar"}, "Bar": {"Foo"}}, + }, + { + desc: "add multiple headers with existing headers", + config: dynamic.HeaderModifier{ + Add: map[string]string{"Foo": "Bar", "Bar": "Foo"}, + }, + responseHeaders: map[string][]string{"Foo": {"Baz"}, "Bar": {"Foobar"}}, + expectedHeaders: map[string][]string{"Foo": {"Baz", "Bar"}, "Bar": {"Foobar", "Foo"}}, + }, + { + desc: "remove header", + config: dynamic.HeaderModifier{ + Remove: []string{"Foo"}, + }, + expectedHeaders: map[string][]string{}, + }, + { + desc: "remove header with existing headers", + config: dynamic.HeaderModifier{ + Remove: []string{"Foo"}, + }, + responseHeaders: map[string][]string{"Foo": {"Baz"}, "Bar": {"Foo"}}, + expectedHeaders: map[string][]string{"Bar": {"Foo"}}, + }, + { + desc: "remove multiple headers with existing headers", + config: dynamic.HeaderModifier{ + Remove: []string{"Foo", "Bar"}, + }, + responseHeaders: map[string][]string{"Foo": {"Bar"}, "Bar": {"Foo"}, "Baz": {"Bar"}}, + expectedHeaders: map[string][]string{"Baz": {"Bar"}}, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + var nextCallCount int + next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + nextCallCount++ + rw.WriteHeader(http.StatusOK) + }) + + handler := NewResponseHeaderModifier(context.Background(), next, test.config, "foo-response-header-modifier") + + req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost", nil) + resp := httptest.NewRecorder() + for k, v := range test.responseHeaders { + resp.Header()[k] = v + } + + handler.ServeHTTP(resp, req) + + assert.Equal(t, 1, nextCallCount) + assert.Equal(t, test.expectedHeaders, resp.Header()) + }) + } +} diff --git a/pkg/middlewares/gatewayapi/redirect/request_redirect.go b/pkg/middlewares/gatewayapi/redirect/request_redirect.go index 9258b652f..e2d32e518 100644 --- a/pkg/middlewares/gatewayapi/redirect/request_redirect.go +++ b/pkg/middlewares/gatewayapi/redirect/request_redirect.go @@ -13,9 +13,7 @@ import ( "go.opentelemetry.io/otel/trace" ) -const ( - typeName = "RequestRedirect" -) +const typeName = "RequestRedirect" type redirect struct { name string diff --git a/pkg/provider/kubernetes/gateway/fixtures/httproute/filter_response_header_modifier.yml b/pkg/provider/kubernetes/gateway/fixtures/httproute/filter_response_header_modifier.yml new file mode 100644 index 000000000..06c647c60 --- /dev/null +++ b/pkg/provider/kubernetes/gateway/fixtures/httproute/filter_response_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: # Use GatewayClass defaults for listener definition. + - 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: + - "example.org" + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: whoami + port: 80 + weight: 1 + kind: Service + group: "" + filters: + - type: ResponseHeaderModifier + responseHeaderModifier: + set: + - name: X-Foo + value: Bar + add: + - name: X-Bar + value: Foo + remove: + - X-Baz diff --git a/pkg/provider/kubernetes/gateway/httproute.go b/pkg/provider/kubernetes/gateway/httproute.go index fc6c9cf1b..2e0c4ad30 100644 --- a/pkg/provider/kubernetes/gateway/httproute.go +++ b/pkg/provider/kubernetes/gateway/httproute.go @@ -316,6 +316,9 @@ func (p *Provider) loadMiddlewares(conf *dynamic.Configuration, namespace, route case gatev1.HTTPRouteFilterRequestHeaderModifier: middlewares[name] = createRequestHeaderModifier(filter.RequestHeaderModifier) + case gatev1.HTTPRouteFilterResponseHeaderModifier: + middlewares[name] = createResponseHeaderModifier(filter.ResponseHeaderModifier) + case gatev1.HTTPRouteFilterExtensionRef: name, middleware, err := p.loadHTTPRouteFilterExtensionRef(namespace, filter.ExtensionRef) if err != nil { @@ -599,7 +602,29 @@ func createRequestHeaderModifier(filter *gatev1.HTTPHeaderFilter) *dynamic.Middl } return &dynamic.Middleware{ - RequestHeaderModifier: &dynamic.RequestHeaderModifier{ + RequestHeaderModifier: &dynamic.HeaderModifier{ + Set: sets, + Add: adds, + Remove: filter.Remove, + }, + } +} + +// createResponseHeaderModifier does not enforce/check the configuration, +// as the spec indicates that either the webhook or CEL (since v1.0 GA Release) should enforce that. +func createResponseHeaderModifier(filter *gatev1.HTTPHeaderFilter) *dynamic.Middleware { + sets := map[string]string{} + for _, header := range filter.Set { + sets[string(header.Name)] = header.Value + } + + adds := map[string]string{} + for _, header := range filter.Add { + adds[string(header.Name)] = header.Value + } + + return &dynamic.Middleware{ + ResponseHeaderModifier: &dynamic.HeaderModifier{ Set: sets, Add: adds, Remove: filter.Remove, diff --git a/pkg/provider/kubernetes/gateway/kubernetes_test.go b/pkg/provider/kubernetes/gateway/kubernetes_test.go index 28d1c9591..414a85f19 100644 --- a/pkg/provider/kubernetes/gateway/kubernetes_test.go +++ b/pkg/provider/kubernetes/gateway/kubernetes_test.go @@ -1722,7 +1722,77 @@ func TestLoadHTTPRoutes(t *testing.T) { }, Middlewares: map[string]*dynamic.Middleware{ "default-http-app-1-my-gateway-web-0-364ce6ec04c3d49b19c4-requestheadermodifier-0": { - RequestHeaderModifier: &dynamic.RequestHeaderModifier{ + 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{ + "default-http-app-1-my-gateway-web-0-wrr": { + Weighted: &dynamic.WeightedRoundRobin{ + Services: []dynamic.WRRService{ + { + Name: "default-whoami-80", + Weight: ptr.To(1), + }, + }, + }, + }, + "default-whoami-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + 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, response header modifier", + paths: []string{"services.yml", "httproute/filter_response_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{ + "default-http-app-1-my-gateway-web-0-364ce6ec04c3d49b19c4": { + EntryPoints: []string{"web"}, + Service: "default-http-app-1-my-gateway-web-0-wrr", + Rule: "Host(`example.org`) && PathPrefix(`/`)", + Priority: 13, + RuleSyntax: "v3", + Middlewares: []string{"default-http-app-1-my-gateway-web-0-364ce6ec04c3d49b19c4-responseheadermodifier-0"}, + }, + }, + Middlewares: map[string]*dynamic.Middleware{ + "default-http-app-1-my-gateway-web-0-364ce6ec04c3d49b19c4-responseheadermodifier-0": { + ResponseHeaderModifier: &dynamic.HeaderModifier{ Set: map[string]string{"X-Foo": "Bar"}, Add: map[string]string{"X-Bar": "Foo"}, Remove: []string{"X-Baz"}, diff --git a/pkg/server/middleware/middlewares.go b/pkg/server/middleware/middlewares.go index 8b8ee16b9..9eebfd31c 100644 --- a/pkg/server/middleware/middlewares.go +++ b/pkg/server/middleware/middlewares.go @@ -397,6 +397,15 @@ func (b *Builder) buildConstructor(ctx context.Context, middlewareName string) ( } } + if config.ResponseHeaderModifier != nil { + if middleware != nil { + return nil, badConf + } + middleware = func(next http.Handler) (http.Handler, error) { + return headermodifier.NewResponseHeaderModifier(ctx, next, *config.ResponseHeaderModifier, middlewareName), nil + } + } + if config.RequestRedirect != nil { if middleware != nil { return nil, badConf