Support ResponseHeaderModifier filter
This commit is contained in:
parent
78079377e8
commit
12a37346a4
12 changed files with 419 additions and 67 deletions
|
@ -86,8 +86,8 @@ func (s *K8sConformanceSuite) SetupSuite() {
|
||||||
s.T().Fatal("Traefik image is not present")
|
s.T().Fatal("Traefik image is not present")
|
||||||
}
|
}
|
||||||
|
|
||||||
s.k3sContainer, err = k3s.RunContainer(ctx,
|
s.k3sContainer, err = k3s.Run(ctx,
|
||||||
testcontainers.WithImage(k3sImage),
|
k3sImage,
|
||||||
k3s.WithManifest("./fixtures/k8s-conformance/00-experimental-v1.1.0.yml"),
|
k3s.WithManifest("./fixtures/k8s-conformance/00-experimental-v1.1.0.yml"),
|
||||||
k3s.WithManifest("./fixtures/k8s-conformance/01-rbac.yml"),
|
k3s.WithManifest("./fixtures/k8s-conformance/01-rbac.yml"),
|
||||||
k3s.WithManifest("./fixtures/k8s-conformance/02-traefik.yml"),
|
k3s.WithManifest("./fixtures/k8s-conformance/02-traefik.yml"),
|
||||||
|
@ -206,6 +206,7 @@ func (s *K8sConformanceSuite) TestK8sGatewayAPIConformance() {
|
||||||
features.SupportHTTPRouteHostRewrite,
|
features.SupportHTTPRouteHostRewrite,
|
||||||
features.SupportHTTPRoutePathRewrite,
|
features.SupportHTTPRoutePathRewrite,
|
||||||
features.SupportHTTPRoutePathRedirect,
|
features.SupportHTTPRoutePathRedirect,
|
||||||
|
features.SupportHTTPRouteResponseHeaderModification,
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
require.NoError(s.T(), err)
|
require.NoError(s.T(), err)
|
||||||
|
|
|
@ -40,10 +40,11 @@ type Middleware struct {
|
||||||
|
|
||||||
Plugin map[string]PluginConf `json:"plugin,omitempty" toml:"plugin,omitempty" yaml:"plugin,omitempty" export:"true"`
|
Plugin map[string]PluginConf `json:"plugin,omitempty" toml:"plugin,omitempty" yaml:"plugin,omitempty" export:"true"`
|
||||||
|
|
||||||
// Gateway API HTTPRoute filters middlewares.
|
// Gateway API filter middlewares.
|
||||||
RequestHeaderModifier *RequestHeaderModifier `json:"requestHeaderModifier,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"`
|
RequestHeaderModifier *HeaderModifier `json:"requestHeaderModifier,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"`
|
||||||
RequestRedirect *RequestRedirect `json:"requestRedirect,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"`
|
ResponseHeaderModifier *HeaderModifier `json:"responseHeaderModifier,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"`
|
||||||
URLRewrite *URLRewrite `json:"URLRewrite,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
|
// +k8s:deepcopy-gen=true
|
||||||
|
@ -694,8 +695,8 @@ type Users []string
|
||||||
|
|
||||||
// +k8s:deepcopy-gen=true
|
// +k8s:deepcopy-gen=true
|
||||||
|
|
||||||
// RequestHeaderModifier holds the request header modifier configuration.
|
// HeaderModifier holds the request/response header modifier configuration.
|
||||||
type RequestHeaderModifier struct {
|
type HeaderModifier struct {
|
||||||
Set map[string]string `json:"set,omitempty"`
|
Set map[string]string `json:"set,omitempty"`
|
||||||
Add map[string]string `json:"add,omitempty"`
|
Add map[string]string `json:"add,omitempty"`
|
||||||
Remove []string `json:"remove,omitempty"`
|
Remove []string `json:"remove,omitempty"`
|
||||||
|
|
|
@ -506,6 +506,41 @@ func (in *HTTPConfiguration) DeepCopy() *HTTPConfiguration {
|
||||||
return out
|
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.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *Headers) DeepCopyInto(out *Headers) {
|
func (in *Headers) DeepCopyInto(out *Headers) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
@ -866,7 +901,12 @@ func (in *Middleware) DeepCopyInto(out *Middleware) {
|
||||||
}
|
}
|
||||||
if in.RequestHeaderModifier != nil {
|
if in.RequestHeaderModifier != nil {
|
||||||
in, out := &in.RequestHeaderModifier, &out.RequestHeaderModifier
|
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)
|
(*in).DeepCopyInto(*out)
|
||||||
}
|
}
|
||||||
if in.RequestRedirect != nil {
|
if in.RequestRedirect != nil {
|
||||||
|
@ -1087,41 +1127,6 @@ func (in *ReplacePathRegex) DeepCopy() *ReplacePathRegex {
|
||||||
return out
|
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.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *RequestRedirect) DeepCopyInto(out *RequestRedirect) {
|
func (in *RequestRedirect) DeepCopyInto(out *RequestRedirect) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
"go.opentelemetry.io/otel/trace"
|
"go.opentelemetry.io/otel/trace"
|
||||||
)
|
)
|
||||||
|
|
||||||
const typeName = "RequestHeaderModifier"
|
const requestHeaderModifierTypeName = "RequestHeaderModifier"
|
||||||
|
|
||||||
// requestHeaderModifier is a middleware used to modify the headers of an HTTP request.
|
// requestHeaderModifier is a middleware used to modify the headers of an HTTP request.
|
||||||
type requestHeaderModifier struct {
|
type requestHeaderModifier struct {
|
||||||
|
@ -22,8 +22,8 @@ type requestHeaderModifier struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRequestHeaderModifier creates a new request header modifier middleware.
|
// NewRequestHeaderModifier creates a new request header modifier middleware.
|
||||||
func NewRequestHeaderModifier(ctx context.Context, next http.Handler, config dynamic.RequestHeaderModifier, name string) http.Handler {
|
func NewRequestHeaderModifier(ctx context.Context, next http.Handler, config dynamic.HeaderModifier, name string) http.Handler {
|
||||||
logger := middlewares.GetLogger(ctx, name, typeName)
|
logger := middlewares.GetLogger(ctx, name, requestHeaderModifierTypeName)
|
||||||
logger.Debug().Msg("Creating middleware")
|
logger.Debug().Msg("Creating middleware")
|
||||||
|
|
||||||
return &requestHeaderModifier{
|
return &requestHeaderModifier{
|
||||||
|
@ -36,7 +36,7 @@ func NewRequestHeaderModifier(ctx context.Context, next http.Handler, config dyn
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *requestHeaderModifier) GetTracingInformation() (string, string, trace.SpanKind) {
|
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) {
|
func (r *requestHeaderModifier) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
|
|
@ -14,25 +14,25 @@ import (
|
||||||
func TestRequestHeaderModifier(t *testing.T) {
|
func TestRequestHeaderModifier(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
desc string
|
desc string
|
||||||
config dynamic.RequestHeaderModifier
|
config dynamic.HeaderModifier
|
||||||
requestHeaders http.Header
|
requestHeaders http.Header
|
||||||
expectedHeaders http.Header
|
expectedHeaders http.Header
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
desc: "no config",
|
desc: "no config",
|
||||||
config: dynamic.RequestHeaderModifier{},
|
config: dynamic.HeaderModifier{},
|
||||||
expectedHeaders: map[string][]string{},
|
expectedHeaders: map[string][]string{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "set header",
|
desc: "set header",
|
||||||
config: dynamic.RequestHeaderModifier{
|
config: dynamic.HeaderModifier{
|
||||||
Set: map[string]string{"Foo": "Bar"},
|
Set: map[string]string{"Foo": "Bar"},
|
||||||
},
|
},
|
||||||
expectedHeaders: map[string][]string{"Foo": {"Bar"}},
|
expectedHeaders: map[string][]string{"Foo": {"Bar"}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "set header with existing headers",
|
desc: "set header with existing headers",
|
||||||
config: dynamic.RequestHeaderModifier{
|
config: dynamic.HeaderModifier{
|
||||||
Set: map[string]string{"Foo": "Bar"},
|
Set: map[string]string{"Foo": "Bar"},
|
||||||
},
|
},
|
||||||
requestHeaders: map[string][]string{"Foo": {"Baz"}, "Bar": {"Foo"}},
|
requestHeaders: map[string][]string{"Foo": {"Baz"}, "Bar": {"Foo"}},
|
||||||
|
@ -40,7 +40,7 @@ func TestRequestHeaderModifier(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "set multiple headers with existing headers",
|
desc: "set multiple headers with existing headers",
|
||||||
config: dynamic.RequestHeaderModifier{
|
config: dynamic.HeaderModifier{
|
||||||
Set: map[string]string{"Foo": "Bar", "Bar": "Foo"},
|
Set: map[string]string{"Foo": "Bar", "Bar": "Foo"},
|
||||||
},
|
},
|
||||||
requestHeaders: map[string][]string{"Foo": {"Baz"}, "Bar": {"Foobar"}},
|
requestHeaders: map[string][]string{"Foo": {"Baz"}, "Bar": {"Foobar"}},
|
||||||
|
@ -48,14 +48,14 @@ func TestRequestHeaderModifier(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "add header",
|
desc: "add header",
|
||||||
config: dynamic.RequestHeaderModifier{
|
config: dynamic.HeaderModifier{
|
||||||
Add: map[string]string{"Foo": "Bar"},
|
Add: map[string]string{"Foo": "Bar"},
|
||||||
},
|
},
|
||||||
expectedHeaders: map[string][]string{"Foo": {"Bar"}},
|
expectedHeaders: map[string][]string{"Foo": {"Bar"}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "add header with existing headers",
|
desc: "add header with existing headers",
|
||||||
config: dynamic.RequestHeaderModifier{
|
config: dynamic.HeaderModifier{
|
||||||
Add: map[string]string{"Foo": "Bar"},
|
Add: map[string]string{"Foo": "Bar"},
|
||||||
},
|
},
|
||||||
requestHeaders: map[string][]string{"Foo": {"Baz"}, "Bar": {"Foo"}},
|
requestHeaders: map[string][]string{"Foo": {"Baz"}, "Bar": {"Foo"}},
|
||||||
|
@ -63,7 +63,7 @@ func TestRequestHeaderModifier(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "add multiple headers with existing headers",
|
desc: "add multiple headers with existing headers",
|
||||||
config: dynamic.RequestHeaderModifier{
|
config: dynamic.HeaderModifier{
|
||||||
Add: map[string]string{"Foo": "Bar", "Bar": "Foo"},
|
Add: map[string]string{"Foo": "Bar", "Bar": "Foo"},
|
||||||
},
|
},
|
||||||
requestHeaders: map[string][]string{"Foo": {"Baz"}, "Bar": {"Foobar"}},
|
requestHeaders: map[string][]string{"Foo": {"Baz"}, "Bar": {"Foobar"}},
|
||||||
|
@ -71,14 +71,14 @@ func TestRequestHeaderModifier(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "remove header",
|
desc: "remove header",
|
||||||
config: dynamic.RequestHeaderModifier{
|
config: dynamic.HeaderModifier{
|
||||||
Remove: []string{"Foo"},
|
Remove: []string{"Foo"},
|
||||||
},
|
},
|
||||||
expectedHeaders: map[string][]string{},
|
expectedHeaders: map[string][]string{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "remove header with existing headers",
|
desc: "remove header with existing headers",
|
||||||
config: dynamic.RequestHeaderModifier{
|
config: dynamic.HeaderModifier{
|
||||||
Remove: []string{"Foo"},
|
Remove: []string{"Foo"},
|
||||||
},
|
},
|
||||||
requestHeaders: map[string][]string{"Foo": {"Baz"}, "Bar": {"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",
|
desc: "remove multiple headers with existing headers",
|
||||||
config: dynamic.RequestHeaderModifier{
|
config: dynamic.HeaderModifier{
|
||||||
Remove: []string{"Foo", "Bar"},
|
Remove: []string{"Foo", "Bar"},
|
||||||
},
|
},
|
||||||
requestHeaders: map[string][]string{"Foo": {"Bar"}, "Bar": {"Foo"}, "Baz": {"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")
|
handler := NewRequestHeaderModifier(context.Background(), next, test.config, "foo-request-header-modifier")
|
||||||
|
|
||||||
req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost", nil)
|
req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost", nil)
|
||||||
if test.requestHeaders != nil {
|
for h, v := range test.requestHeaders {
|
||||||
req.Header = test.requestHeaders
|
req.Header[h] = v
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := httptest.NewRecorder()
|
resp := httptest.NewRecorder()
|
||||||
|
|
||||||
handler.ServeHTTP(resp, req)
|
handler.ServeHTTP(resp, req)
|
||||||
|
|
||||||
assert.Equal(t, test.expectedHeaders, gotHeaders)
|
assert.Equal(t, test.expectedHeaders, gotHeaders)
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,9 +13,7 @@ import (
|
||||||
"go.opentelemetry.io/otel/trace"
|
"go.opentelemetry.io/otel/trace"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const typeName = "RequestRedirect"
|
||||||
typeName = "RequestRedirect"
|
|
||||||
)
|
|
||||||
|
|
||||||
type redirect struct {
|
type redirect struct {
|
||||||
name string
|
name string
|
||||||
|
|
|
@ -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
|
|
@ -316,6 +316,9 @@ func (p *Provider) loadMiddlewares(conf *dynamic.Configuration, namespace, route
|
||||||
case gatev1.HTTPRouteFilterRequestHeaderModifier:
|
case gatev1.HTTPRouteFilterRequestHeaderModifier:
|
||||||
middlewares[name] = createRequestHeaderModifier(filter.RequestHeaderModifier)
|
middlewares[name] = createRequestHeaderModifier(filter.RequestHeaderModifier)
|
||||||
|
|
||||||
|
case gatev1.HTTPRouteFilterResponseHeaderModifier:
|
||||||
|
middlewares[name] = createResponseHeaderModifier(filter.ResponseHeaderModifier)
|
||||||
|
|
||||||
case gatev1.HTTPRouteFilterExtensionRef:
|
case gatev1.HTTPRouteFilterExtensionRef:
|
||||||
name, middleware, err := p.loadHTTPRouteFilterExtensionRef(namespace, filter.ExtensionRef)
|
name, middleware, err := p.loadHTTPRouteFilterExtensionRef(namespace, filter.ExtensionRef)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -599,7 +602,29 @@ func createRequestHeaderModifier(filter *gatev1.HTTPHeaderFilter) *dynamic.Middl
|
||||||
}
|
}
|
||||||
|
|
||||||
return &dynamic.Middleware{
|
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,
|
Set: sets,
|
||||||
Add: adds,
|
Add: adds,
|
||||||
Remove: filter.Remove,
|
Remove: filter.Remove,
|
||||||
|
|
|
@ -1722,7 +1722,77 @@ func TestLoadHTTPRoutes(t *testing.T) {
|
||||||
},
|
},
|
||||||
Middlewares: map[string]*dynamic.Middleware{
|
Middlewares: map[string]*dynamic.Middleware{
|
||||||
"default-http-app-1-my-gateway-web-0-364ce6ec04c3d49b19c4-requestheadermodifier-0": {
|
"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"},
|
Set: map[string]string{"X-Foo": "Bar"},
|
||||||
Add: map[string]string{"X-Bar": "Foo"},
|
Add: map[string]string{"X-Bar": "Foo"},
|
||||||
Remove: []string{"X-Baz"},
|
Remove: []string{"X-Baz"},
|
||||||
|
|
|
@ -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 config.RequestRedirect != nil {
|
||||||
if middleware != nil {
|
if middleware != nil {
|
||||||
return nil, badConf
|
return nil, badConf
|
||||||
|
|
Loading…
Reference in a new issue