Add support for Kubernetes Gateway API RequestHeaderModifier filter
Co-authored-by: Baptiste Mayelle <baptiste.mayelle@traefik.io>
This commit is contained in:
parent
ac1753a614
commit
f69fd43122
11 changed files with 499 additions and 32 deletions
|
@ -251,39 +251,51 @@ Kubernetes cluster before creating `HTTPRoute` objects.
|
||||||
requestRedirect: # [27]
|
requestRedirect: # [27]
|
||||||
scheme: https # [28]
|
scheme: https # [28]
|
||||||
statusCode: 301 # [29]
|
statusCode: 301 # [29]
|
||||||
|
- type: RequestHeaderModifier # [30]
|
||||||
|
requestHeaderModifier: # [31]
|
||||||
|
set:
|
||||||
|
- name: X-Foo
|
||||||
|
value: Bar
|
||||||
|
add:
|
||||||
|
- name: X-Bar
|
||||||
|
value: Foo
|
||||||
|
remove:
|
||||||
|
- X-Baz
|
||||||
```
|
```
|
||||||
|
|
||||||
| Ref | Attribute | Description |
|
| Ref | Attribute | Description |
|
||||||
|------|-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|------|-------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| [1] | `parentRefs` | References the resources (usually Gateways) that a Route wants to be attached to. |
|
| [1] | `parentRefs` | References the resources (usually Gateways) that a Route wants to be attached to. |
|
||||||
| [2] | `name` | Name of the referent. |
|
| [2] | `name` | Name of the referent. |
|
||||||
| [3] | `namespace` | Namespace of the referent. When unspecified (or empty string), this refers to the local namespace of the Route. |
|
| [3] | `namespace` | Namespace of the referent. When unspecified (or empty string), this refers to the local namespace of the Route. |
|
||||||
| [4] | `sectionName` | Name of a section within the target resource (the Listener name). |
|
| [4] | `sectionName` | Name of a section within the target resource (the Listener name). |
|
||||||
| [5] | `hostnames` | A set of hostname that should match against the HTTP Host header to select a HTTPRoute to process the request. |
|
| [5] | `hostnames` | A set of hostname that should match against the HTTP Host header to select a HTTPRoute to process the request. |
|
||||||
| [6] | `rules` | A list of HTTP matchers, filters and actions. |
|
| [6] | `rules` | A list of HTTP matchers, filters and actions. |
|
||||||
| [7] | `matches` | Conditions used for matching the rule against incoming HTTP requests. Each match is independent, i.e. this rule will be matched if **any** one of the matches is satisfied. |
|
| [7] | `matches` | Conditions used for matching the rule against incoming HTTP requests. Each match is independent, i.e. this rule will be matched if **any** one of the matches is satisfied. |
|
||||||
| [8] | `path` | An HTTP request path matcher. If this field is not specified, a default prefix match on the "/" path is provided. |
|
| [8] | `path` | An HTTP request path matcher. If this field is not specified, a default prefix match on the "/" path is provided. |
|
||||||
| [9] | `type` | Type of match against the path Value (supported types: `Exact`, `Prefix`). |
|
| [9] | `type` | Type of match against the path Value (supported types: `Exact`, `Prefix`). |
|
||||||
| [10] | `value` | The value of the HTTP path to match against. |
|
| [10] | `value` | The value of the HTTP path to match against. |
|
||||||
| [11] | `headers` | Conditions to select a HTTP route by matching HTTP request headers. |
|
| [11] | `headers` | Conditions to select a HTTP route by matching HTTP request headers. |
|
||||||
| [12] | `name` | Name of the HTTP header to be matched. |
|
| [12] | `name` | Name of the HTTP header to be matched. |
|
||||||
| [13] | `value` | Value of HTTP Header to be matched. |
|
| [13] | `value` | Value of HTTP Header to be matched. |
|
||||||
| [14] | `backendRefs` | Defines the backend(s) where matching requests should be sent. |
|
| [14] | `backendRefs` | Defines the backend(s) where matching requests should be sent. |
|
||||||
| [15] | `name` | The name of the referent service. |
|
| [15] | `name` | The name of the referent service. |
|
||||||
| [16] | `weight` | The proportion of traffic forwarded to a targetRef, computed as weight/(sum of all weights in targetRefs). |
|
| [16] | `weight` | The proportion of traffic forwarded to a targetRef, computed as weight/(sum of all weights in targetRefs). |
|
||||||
| [17] | `port` | The port of the referent service. |
|
| [17] | `port` | The port of the referent service. |
|
||||||
| [18] | `group` | Group is the group of the referent. Only `traefik.io` and `gateway.networking.k8s.io` values are supported. |
|
| [18] | `group` | Group is the group of the referent. Only `traefik.io` and `gateway.networking.k8s.io` values are supported. |
|
||||||
| [19] | `kind` | Kind is kind of the referent. Only `TraefikService` and `Service` values are supported. |
|
| [19] | `kind` | Kind is kind of the referent. Only `TraefikService` and `Service` values are supported. |
|
||||||
| [20] | `filters` | Defines the filters (middlewares) applied to the route. |
|
| [20] | `filters` | Defines the filters (middlewares) applied to the route. |
|
||||||
| [21] | `type` | Defines the type of filter; ExtensionRef is used for configuring custom HTTP filters. |
|
| [21] | `type` | Defines the type of filter; ExtensionRef is used for configuring custom HTTP filters. |
|
||||||
| [22] | `extensionRef` | Configuration of the custom HTTP filter. |
|
| [22] | `extensionRef` | Configuration of the custom HTTP filter. |
|
||||||
| [23] | `group` | Group of the kubernetes object to reference. |
|
| [23] | `group` | Group of the kubernetes object to reference. |
|
||||||
| [24] | `kind` | Kind of the kubernetes object to reference. |
|
| [24] | `kind` | Kind of the kubernetes object to reference. |
|
||||||
| [25] | `name` | Name of the kubernetes object to reference. |
|
| [25] | `name` | Name of the kubernetes object to reference. |
|
||||||
| [26] | `type` | Defines the type of filter; RequestRedirect redirects a request to another location. |
|
| [26] | `type` | Defines the type of filter; RequestRedirect redirects a request to another location. |
|
||||||
| [27] | `requestRedirect` | Configuration of redirect filter. |
|
| [27] | `requestRedirect` | Configuration of redirect filter. |
|
||||||
| [28] | `scheme` | Scheme is the scheme to be used in the value of the Location header in the response. |
|
| [28] | `scheme` | Scheme is the scheme to be used in the value of the Location header in the response. |
|
||||||
| [29] | `statusCode` | StatusCode is the HTTP status code to be used in response. |
|
| [29] | `statusCode` | StatusCode is the HTTP status code to be used in response. |
|
||||||
|
| [30] | `type` | Defines the type of filter; RequestHeaderModifier modifies request headers. |
|
||||||
|
| [31] | `requestHeaderModifier` | Configuration of RequestHeaderModifier filter. |
|
||||||
|
|
||||||
### Kind: `TCPRoute`
|
### Kind: `TCPRoute`
|
||||||
|
|
||||||
|
|
|
@ -207,7 +207,13 @@ func clean(element any) {
|
||||||
|
|
||||||
var svcFieldNames []string
|
var svcFieldNames []string
|
||||||
for i := range valueSvcRoot.NumField() {
|
for i := range valueSvcRoot.NumField() {
|
||||||
svcFieldNames = append(svcFieldNames, valueSvcRoot.Type().Field(i).Name)
|
field := valueSvcRoot.Type().Field(i)
|
||||||
|
// do not create empty node for hidden config.
|
||||||
|
if field.Tag.Get("file") == "-" && field.Tag.Get("kv") == "-" && field.Tag.Get("label") == "-" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
svcFieldNames = append(svcFieldNames, field.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Strings(svcFieldNames)
|
sort.Strings(svcFieldNames)
|
||||||
|
|
|
@ -39,6 +39,9 @@ type Middleware struct {
|
||||||
GrpcWeb *GrpcWeb `json:"grpcWeb,omitempty" toml:"grpcWeb,omitempty" yaml:"grpcWeb,omitempty" export:"true"`
|
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"`
|
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
|
// +k8s:deepcopy-gen=true
|
||||||
|
@ -673,3 +676,12 @@ type TLSClientCertificateSubjectDNInfo struct {
|
||||||
|
|
||||||
// Users holds a list of users.
|
// Users holds a list of users.
|
||||||
type Users []string
|
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"`
|
||||||
|
}
|
||||||
|
|
|
@ -859,6 +859,11 @@ func (in *Middleware) DeepCopyInto(out *Middleware) {
|
||||||
(*out)[key] = *val.DeepCopy()
|
(*out)[key] = *val.DeepCopy()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if in.RequestHeaderModifier != nil {
|
||||||
|
in, out := &in.RequestHeaderModifier, &out.RequestHeaderModifier
|
||||||
|
*out = new(RequestHeaderModifier)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1067,6 +1072,41 @@ 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 *ResponseForwarding) DeepCopyInto(out *ResponseForwarding) {
|
func (in *ResponseForwarding) DeepCopyInto(out *ResponseForwarding) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
|
56
pkg/middlewares/headermodifier/request_header_modifier.go
Normal file
56
pkg/middlewares/headermodifier/request_header_modifier.go
Normal 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)
|
||||||
|
}
|
121
pkg/middlewares/headermodifier/request_header_modifier_test.go
Normal file
121
pkg/middlewares/headermodifier/request_header_modifier_test.go
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
@ -1921,6 +1921,11 @@ func (p *Provider) loadMiddlewares(listener gatev1.Listener, namespace string, p
|
||||||
}
|
}
|
||||||
|
|
||||||
middlewares[name] = middleware
|
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:
|
default:
|
||||||
// As per the spec:
|
// As per the spec:
|
||||||
// https://gateway-api.sigs.k8s.io/api-types/httproute/#filters-optional
|
// 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)
|
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) {
|
func createRedirectRegexMiddleware(scheme string, filter *gatev1.HTTPRequestRedirectFilter) (*dynamic.Middleware, error) {
|
||||||
// Use the HTTPRequestRedirectFilter scheme if defined.
|
// Use the HTTPRequestRedirectFilter scheme if defined.
|
||||||
filterScheme := scheme
|
filterScheme := scheme
|
||||||
|
|
|
@ -1517,6 +1517,75 @@ func TestLoadHTTPRoutes(t *testing.T) {
|
||||||
TLS: &dynamic.TLSConfiguration{},
|
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",
|
desc: "Simple HTTPRoute, redirect HTTP to HTTPS",
|
||||||
paths: []string{"services.yml", "httproute/filter_http_to_https.yml"},
|
paths: []string{"services.yml", "httproute/filter_http_to_https.yml"},
|
||||||
|
|
|
@ -21,6 +21,7 @@ import (
|
||||||
"github.com/traefik/traefik/v3/pkg/middlewares/contenttype"
|
"github.com/traefik/traefik/v3/pkg/middlewares/contenttype"
|
||||||
"github.com/traefik/traefik/v3/pkg/middlewares/customerrors"
|
"github.com/traefik/traefik/v3/pkg/middlewares/customerrors"
|
||||||
"github.com/traefik/traefik/v3/pkg/middlewares/grpcweb"
|
"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/headers"
|
||||||
"github.com/traefik/traefik/v3/pkg/middlewares/inflightreq"
|
"github.com/traefik/traefik/v3/pkg/middlewares/inflightreq"
|
||||||
"github.com/traefik/traefik/v3/pkg/middlewares/ipallowlist"
|
"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 {
|
if middleware == nil {
|
||||||
return nil, fmt.Errorf("invalid middleware %q configuration: invalid middleware type or middleware does not exist", middlewareName)
|
return nil, fmt.Errorf("invalid middleware %q configuration: invalid middleware type or middleware does not exist", middlewareName)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1491,6 +1491,61 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
|
<!-- EXTRA FIELDS FROM MIDDLEWARES - [requestHeaderModifier] - set -->
|
||||||
|
<q-card-section v-if="middleware.requestHeaderModifier">
|
||||||
|
<div class="row items-start no-wrap">
|
||||||
|
<div class="col">
|
||||||
|
<div class="text-subtitle2">
|
||||||
|
Set
|
||||||
|
</div>
|
||||||
|
<q-chip
|
||||||
|
v-for="(val, key) in exData(middleware).set"
|
||||||
|
:key="key"
|
||||||
|
dense
|
||||||
|
class="app-chip app-chip-green"
|
||||||
|
>
|
||||||
|
{{ key }}: {{ val }}
|
||||||
|
</q-chip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<!-- EXTRA FIELDS FROM MIDDLEWARES - [requestHeaderModifier] - add -->
|
||||||
|
<q-card-section v-if="middleware.requestHeaderModifier">
|
||||||
|
<div class="row items-start no-wrap">
|
||||||
|
<div class="col">
|
||||||
|
<div class="text-subtitle2">
|
||||||
|
Add
|
||||||
|
</div>
|
||||||
|
<q-chip
|
||||||
|
v-for="(val, key) in exData(middleware).add"
|
||||||
|
:key="key"
|
||||||
|
dense
|
||||||
|
class="app-chip app-chip-green"
|
||||||
|
>
|
||||||
|
{{ key }}: {{ val }}
|
||||||
|
</q-chip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<!-- EXTRA FIELDS FROM MIDDLEWARES - [requestHeaderModifier] - remove -->
|
||||||
|
<q-card-section v-if="middleware.requestHeaderModifier">
|
||||||
|
<div class="row items-start no-wrap">
|
||||||
|
<div class="col">
|
||||||
|
<div class="text-subtitle2">
|
||||||
|
Remove
|
||||||
|
</div>
|
||||||
|
<q-chip
|
||||||
|
v-for="(name, key) in exData(middleware).remove"
|
||||||
|
:key="key"
|
||||||
|
dense
|
||||||
|
class="app-chip app-chip-green"
|
||||||
|
>
|
||||||
|
{{ name }}
|
||||||
|
</q-chip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
<q-card-section v-if="protocol === 'tcp'">
|
<q-card-section v-if="protocol === 'tcp'">
|
||||||
|
|
Loading…
Reference in a new issue