mirror of
https://github.com/containous/traefik.git
synced 2024-12-22 13:34:03 +03:00
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
@ -251,39 +251,51 @@ Kubernetes cluster before creating `HTTPRoute` objects.
|
||||
requestRedirect: # [27]
|
||||
scheme: https # [28]
|
||||
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 |
|
||||
|------|-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| [1] | `parentRefs` | References the resources (usually Gateways) that a Route wants to be attached to. |
|
||||
| [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. |
|
||||
| [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. |
|
||||
| [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. |
|
||||
| [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`). |
|
||||
| [10] | `value` | The value of the HTTP path to match against. |
|
||||
| [11] | `headers` | Conditions to select a HTTP route by matching HTTP request headers. |
|
||||
| [12] | `name` | Name of the 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. |
|
||||
| [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). |
|
||||
| [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. |
|
||||
| [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. |
|
||||
| [21] | `type` | Defines the type of filter; ExtensionRef is used for configuring custom HTTP filters. |
|
||||
| [22] | `extensionRef` | Configuration of the custom HTTP filter. |
|
||||
| [23] | `group` | Group of the kubernetes object to reference. |
|
||||
| [24] | `kind` | Kind 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. |
|
||||
| [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. |
|
||||
| [29] | `statusCode` | StatusCode is the HTTP status code to be used in response. |
|
||||
| Ref | Attribute | Description |
|
||||
|------|-------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| [1] | `parentRefs` | References the resources (usually Gateways) that a Route wants to be attached to. |
|
||||
| [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. |
|
||||
| [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. |
|
||||
| [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. |
|
||||
| [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`). |
|
||||
| [10] | `value` | The value of the HTTP path to match against. |
|
||||
| [11] | `headers` | Conditions to select a HTTP route by matching HTTP request headers. |
|
||||
| [12] | `name` | Name of the 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. |
|
||||
| [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). |
|
||||
| [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. |
|
||||
| [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. |
|
||||
| [21] | `type` | Defines the type of filter; ExtensionRef is used for configuring custom HTTP filters. |
|
||||
| [22] | `extensionRef` | Configuration of the custom HTTP filter. |
|
||||
| [23] | `group` | Group of the kubernetes object to reference. |
|
||||
| [24] | `kind` | Kind 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. |
|
||||
| [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. |
|
||||
| [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`
|
||||
|
||||
|
@ -207,7 +207,13 @@ func clean(element any) {
|
||||
|
||||
var svcFieldNames []string
|
||||
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)
|
||||
|
@ -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"`
|
||||
}
|
||||
|
@ -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
|
||||
|
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
|
||||
|
||||
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
|
||||
|
@ -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"},
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -1491,6 +1491,61 @@
|
||||
</div>
|
||||
</div>
|
||||
</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 v-if="protocol === 'tcp'">
|
||||
|
Loading…
Reference in New Issue
Block a user