1
0
Fork 0

Add support for Kubernetes Gateway API RequestHeaderModifier filter

Co-authored-by: Baptiste Mayelle <baptiste.mayelle@traefik.io>
This commit is contained in:
Romain 2024-04-05 17:18:03 +02:00 committed by GitHub
parent ac1753a614
commit f69fd43122
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 499 additions and 32 deletions

View file

@ -39,6 +39,9 @@ type Middleware struct {
GrpcWeb *GrpcWeb `json:"grpcWeb,omitempty" toml:"grpcWeb,omitempty" yaml:"grpcWeb,omitempty" export:"true"`
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"`
}
// +k8s:deepcopy-gen=true
@ -673,3 +676,12 @@ type TLSClientCertificateSubjectDNInfo struct {
// Users holds a list of users.
type Users []string
// +k8s:deepcopy-gen=true
// RequestHeaderModifier holds the request header modifier configuration.
type RequestHeaderModifier struct {
Set map[string]string `json:"set,omitempty"`
Add map[string]string `json:"add,omitempty"`
Remove []string `json:"remove,omitempty"`
}

View file

@ -859,6 +859,11 @@ func (in *Middleware) DeepCopyInto(out *Middleware) {
(*out)[key] = *val.DeepCopy()
}
}
if in.RequestHeaderModifier != nil {
in, out := &in.RequestHeaderModifier, &out.RequestHeaderModifier
*out = new(RequestHeaderModifier)
(*in).DeepCopyInto(*out)
}
return
}
@ -1067,6 +1072,41 @@ 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 *ResponseForwarding) DeepCopyInto(out *ResponseForwarding) {
*out = *in

View file

@ -0,0 +1,56 @@
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 typeName = "RequestHeaderModifier"
// requestHeaderModifier is a middleware used to modify the headers of an HTTP request.
type requestHeaderModifier struct {
next http.Handler
name string
set map[string]string
add map[string]string
remove []string
}
// NewRequestHeaderModifier creates a new request header modifier middleware.
func NewRequestHeaderModifier(ctx context.Context, next http.Handler, config dynamic.RequestHeaderModifier, name string) (http.Handler, error) {
logger := middlewares.GetLogger(ctx, name, typeName)
logger.Debug().Msg("Creating middleware")
return &requestHeaderModifier{
next: next,
name: name,
set: config.Set,
add: config.Add,
remove: config.Remove,
}, nil
}
func (r *requestHeaderModifier) GetTracingInformation() (string, string, trace.SpanKind) {
return r.name, typeName, trace.SpanKindUnspecified
}
func (r *requestHeaderModifier) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
for headerName, headerValue := range r.set {
req.Header.Set(headerName, headerValue)
}
for headerName, headerValue := range r.add {
req.Header.Add(headerName, headerValue)
}
for _, headerName := range r.remove {
req.Header.Del(headerName)
}
r.next.ServeHTTP(rw, req)
}

View file

@ -0,0 +1,121 @@
package headermodifier
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/testhelpers"
)
func TestRequestHeaderModifier(t *testing.T) {
testCases := []struct {
desc string
config dynamic.RequestHeaderModifier
requestHeaders http.Header
expectedHeaders http.Header
}{
{
desc: "no config",
config: dynamic.RequestHeaderModifier{},
expectedHeaders: map[string][]string{},
},
{
desc: "set header",
config: dynamic.RequestHeaderModifier{
Set: map[string]string{"Foo": "Bar"},
},
expectedHeaders: map[string][]string{"Foo": {"Bar"}},
},
{
desc: "set header with existing headers",
config: dynamic.RequestHeaderModifier{
Set: map[string]string{"Foo": "Bar"},
},
requestHeaders: map[string][]string{"Foo": {"Baz"}, "Bar": {"Foo"}},
expectedHeaders: map[string][]string{"Foo": {"Bar"}, "Bar": {"Foo"}},
},
{
desc: "set multiple headers with existing headers",
config: dynamic.RequestHeaderModifier{
Set: map[string]string{"Foo": "Bar", "Bar": "Foo"},
},
requestHeaders: map[string][]string{"Foo": {"Baz"}, "Bar": {"Foobar"}},
expectedHeaders: map[string][]string{"Foo": {"Bar"}, "Bar": {"Foo"}},
},
{
desc: "add header",
config: dynamic.RequestHeaderModifier{
Add: map[string]string{"Foo": "Bar"},
},
expectedHeaders: map[string][]string{"Foo": {"Bar"}},
},
{
desc: "add header with existing headers",
config: dynamic.RequestHeaderModifier{
Add: map[string]string{"Foo": "Bar"},
},
requestHeaders: 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.RequestHeaderModifier{
Add: map[string]string{"Foo": "Bar", "Bar": "Foo"},
},
requestHeaders: map[string][]string{"Foo": {"Baz"}, "Bar": {"Foobar"}},
expectedHeaders: map[string][]string{"Foo": {"Baz", "Bar"}, "Bar": {"Foobar", "Foo"}},
},
{
desc: "remove header",
config: dynamic.RequestHeaderModifier{
Remove: []string{"Foo"},
},
expectedHeaders: map[string][]string{},
},
{
desc: "remove header with existing headers",
config: dynamic.RequestHeaderModifier{
Remove: []string{"Foo"},
},
requestHeaders: map[string][]string{"Foo": {"Baz"}, "Bar": {"Foo"}},
expectedHeaders: map[string][]string{"Bar": {"Foo"}},
},
{
desc: "remove multiple headers with existing headers",
config: dynamic.RequestHeaderModifier{
Remove: []string{"Foo", "Bar"},
},
requestHeaders: 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 gotHeaders http.Header
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotHeaders = r.Header
})
handler, err := NewRequestHeaderModifier(context.Background(), next, test.config, "foo-request-header-modifier")
require.NoError(t, err)
req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost", nil)
if test.requestHeaders != nil {
req.Header = test.requestHeaders
}
resp := httptest.NewRecorder()
handler.ServeHTTP(resp, req)
assert.Equal(t, test.expectedHeaders, gotHeaders)
})
}
}

View file

@ -0,0 +1,58 @@
---
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:
- 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

View file

@ -1921,6 +1921,11 @@ func (p *Provider) loadMiddlewares(listener gatev1.Listener, namespace string, p
}
middlewares[name] = middleware
case gatev1.HTTPRouteFilterRequestHeaderModifier:
middlewareName := provider.Normalize(fmt.Sprintf("%s-%s-%d", prefix, strings.ToLower(string(filter.Type)), i))
middlewares[middlewareName] = createRequestHeaderModifier(filter.RequestHeaderModifier)
default:
// As per the spec:
// https://gateway-api.sigs.k8s.io/api-types/httproute/#filters-optional
@ -1950,6 +1955,28 @@ func (p *Provider) loadHTTPRouteFilterExtensionRef(namespace string, extensionRe
return filterFunc(string(extensionRef.Name), namespace)
}
// createRequestHeaderModifier 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 createRequestHeaderModifier(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{
RequestHeaderModifier: &dynamic.RequestHeaderModifier{
Set: sets,
Add: adds,
Remove: filter.Remove,
},
}
}
func createRedirectRegexMiddleware(scheme string, filter *gatev1.HTTPRequestRedirectFilter) (*dynamic.Middleware, error) {
// Use the HTTPRequestRedirectFilter scheme if defined.
filterScheme := scheme

View file

@ -1517,6 +1517,75 @@ func TestLoadHTTPRoutes(t *testing.T) {
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "Simple HTTPRoute, request header modifier",
paths: []string{"services.yml", "httproute/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{
"default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4": {
EntryPoints: []string{"web"},
Service: "default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4-wrr",
Rule: "Host(`example.org`) && PathPrefix(`/`)",
RuleSyntax: "v3",
Middlewares: []string{"default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4-requestheadermodifier-0"},
},
},
Middlewares: map[string]*dynamic.Middleware{
"default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4-requestheadermodifier-0": {
RequestHeaderModifier: &dynamic.RequestHeaderModifier{
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-364ce6ec04c3d49b19c4-wrr": {
Weighted: &dynamic.WeightedRoundRobin{
Services: []dynamic.WRRService{
{
Name: "default-whoami-80",
Weight: func(i int) *int { return &i }(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, redirect HTTP to HTTPS",
paths: []string{"services.yml", "httproute/filter_http_to_https.yml"},

View file

@ -21,6 +21,7 @@ import (
"github.com/traefik/traefik/v3/pkg/middlewares/contenttype"
"github.com/traefik/traefik/v3/pkg/middlewares/customerrors"
"github.com/traefik/traefik/v3/pkg/middlewares/grpcweb"
"github.com/traefik/traefik/v3/pkg/middlewares/headermodifier"
"github.com/traefik/traefik/v3/pkg/middlewares/headers"
"github.com/traefik/traefik/v3/pkg/middlewares/inflightreq"
"github.com/traefik/traefik/v3/pkg/middlewares/ipallowlist"
@ -384,6 +385,16 @@ func (b *Builder) buildConstructor(ctx context.Context, middlewareName string) (
}
}
// Gateway API HTTPRoute filters middlewares.
if config.RequestHeaderModifier != nil {
if middleware != nil {
return nil, badConf
}
middleware = func(next http.Handler) (http.Handler, error) {
return headermodifier.NewRequestHeaderModifier(ctx, next, *config.RequestHeaderModifier, middlewareName)
}
}
if middleware == nil {
return nil, fmt.Errorf("invalid middleware %q configuration: invalid middleware type or middleware does not exist", middlewareName)
}