diff --git a/api/v1alpha1/clienttrafficpolicy_types.go b/api/v1alpha1/clienttrafficpolicy_types.go
index 347eb946353..63b2c91fb2e 100644
--- a/api/v1alpha1/clienttrafficpolicy_types.go
+++ b/api/v1alpha1/clienttrafficpolicy_types.go
@@ -7,6 +7,7 @@ package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
)
@@ -134,6 +135,12 @@ type HeaderSettings struct {
//
// +optional
PreserveXRequestID *bool `json:"preserveXRequestID,omitempty"`
+
+ // EarlyRequestHeaders defines settings for early request header modification, before envoy performs
+ // routing, tracing and built-in header manipulation.
+ //
+ // +optional
+ EarlyRequestHeaders *gwapiv1.HTTPHeaderFilter `json:"earlyRequestHeaders,omitempty"`
}
// WithUnderscoresAction configures the action to take when an HTTP header with underscores
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index 79c8e98a525..62fa950a3e8 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -2714,6 +2714,11 @@ func (in *HeaderSettings) DeepCopyInto(out *HeaderSettings) {
*out = new(bool)
**out = **in
}
+ if in.EarlyRequestHeaders != nil {
+ in, out := &in.EarlyRequestHeaders, &out.EarlyRequestHeaders
+ *out = new(apisv1.HTTPHeaderFilter)
+ (*in).DeepCopyInto(*out)
+ }
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HeaderSettings.
diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml
index 0dd5ac6a980..5483ff78e64 100644
--- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml
+++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml
@@ -159,6 +159,148 @@ spec:
DisableRateLimitHeaders configures Envoy Proxy to omit the "X-RateLimit-" response headers
when rate limiting is enabled.
type: boolean
+ earlyRequestHeaders:
+ description: |-
+ EarlyRequestHeaders defines settings for early request header modification, before envoy performs
+ routing, tracing and built-in header manipulation.
+ properties:
+ add:
+ description: |-
+ Add adds the given header(s) (name, value) to the request
+ before the action. It appends to any existing values associated
+ with the header name.
+
+
+ Input:
+ GET /foo HTTP/1.1
+ my-header: foo
+
+
+ Config:
+ add:
+ - name: "my-header"
+ value: "bar,baz"
+
+
+ Output:
+ GET /foo HTTP/1.1
+ my-header: foo,bar,baz
+ items:
+ description: HTTPHeader represents an HTTP Header name and
+ value as defined by RFC 7230.
+ properties:
+ name:
+ description: |-
+ Name is the name of the HTTP Header to be matched. Name matching MUST be
+ case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2).
+
+
+ If multiple entries specify equivalent header names, the first entry with
+ an equivalent name MUST be considered for a match. Subsequent entries
+ with an equivalent header name MUST be ignored. Due to the
+ case-insensitivity of header names, "foo" and "Foo" are considered
+ equivalent.
+ maxLength: 256
+ minLength: 1
+ pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$
+ type: string
+ value:
+ description: Value is the value of HTTP Header to be
+ matched.
+ maxLength: 4096
+ minLength: 1
+ type: string
+ required:
+ - name
+ - value
+ type: object
+ maxItems: 16
+ type: array
+ x-kubernetes-list-map-keys:
+ - name
+ x-kubernetes-list-type: map
+ remove:
+ description: |-
+ Remove the given header(s) from the HTTP request before the action. The
+ value of Remove is a list of HTTP header names. Note that the header
+ names are case-insensitive (see
+ https://datatracker.ietf.org/doc/html/rfc2616#section-4.2).
+
+
+ Input:
+ GET /foo HTTP/1.1
+ my-header1: foo
+ my-header2: bar
+ my-header3: baz
+
+
+ Config:
+ remove: ["my-header1", "my-header3"]
+
+
+ Output:
+ GET /foo HTTP/1.1
+ my-header2: bar
+ items:
+ type: string
+ maxItems: 16
+ type: array
+ x-kubernetes-list-type: set
+ set:
+ description: |-
+ Set overwrites the request with the given header (name, value)
+ before the action.
+
+
+ Input:
+ GET /foo HTTP/1.1
+ my-header: foo
+
+
+ Config:
+ set:
+ - name: "my-header"
+ value: "bar"
+
+
+ Output:
+ GET /foo HTTP/1.1
+ my-header: bar
+ items:
+ description: HTTPHeader represents an HTTP Header name and
+ value as defined by RFC 7230.
+ properties:
+ name:
+ description: |-
+ Name is the name of the HTTP Header to be matched. Name matching MUST be
+ case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2).
+
+
+ If multiple entries specify equivalent header names, the first entry with
+ an equivalent name MUST be considered for a match. Subsequent entries
+ with an equivalent header name MUST be ignored. Due to the
+ case-insensitivity of header names, "foo" and "Foo" are considered
+ equivalent.
+ maxLength: 256
+ minLength: 1
+ pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$
+ type: string
+ value:
+ description: Value is the value of HTTP Header to be
+ matched.
+ maxLength: 4096
+ minLength: 1
+ type: string
+ required:
+ - name
+ - value
+ type: object
+ maxItems: 16
+ type: array
+ x-kubernetes-list-map-keys:
+ - name
+ x-kubernetes-list-type: map
+ type: object
enableEnvoyHeaders:
description: |-
EnableEnvoyHeaders configures Envoy Proxy to add the "X-Envoy-" headers to requests
diff --git a/internal/gatewayapi/clienttrafficpolicy.go b/internal/gatewayapi/clienttrafficpolicy.go
index c1877ffa653..44d813c255c 100644
--- a/internal/gatewayapi/clienttrafficpolicy.go
+++ b/internal/gatewayapi/clienttrafficpolicy.go
@@ -18,6 +18,7 @@ import (
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/utils/ptr"
+ gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
@@ -432,7 +433,10 @@ func (t *Translator) translateClientTrafficPolicyForListener(policy *egv1a1.Clie
translateClientIPDetection(policy.Spec.ClientIPDetection, httpIR)
// Translate Header Settings
- translateListenerHeaderSettings(policy.Spec.Headers, httpIR)
+ if err = translateListenerHeaderSettings(policy.Spec.Headers, httpIR); err != nil {
+ err = perr.WithMessage(err, "Headers")
+ errs = errors.Join(errs, err)
+ }
// Translate Path Settings
translatePathSettings(policy.Spec.Path, httpIR)
@@ -613,9 +617,9 @@ func translateClientIPDetection(clientIPDetection *egv1a1.ClientIPDetectionSetti
httpIR.ClientIPDetection = (*ir.ClientIPDetectionSettings)(clientIPDetection)
}
-func translateListenerHeaderSettings(headerSettings *egv1a1.HeaderSettings, httpIR *ir.HTTPListener) {
+func translateListenerHeaderSettings(headerSettings *egv1a1.HeaderSettings, httpIR *ir.HTTPListener) error {
if headerSettings == nil {
- return
+ return nil
}
httpIR.Headers = &ir.HeaderSettings{
EnableEnvoyHeaders: ptr.Deref(headerSettings.EnableEnvoyHeaders, false),
@@ -634,6 +638,16 @@ func translateListenerHeaderSettings(headerSettings *egv1a1.HeaderSettings, http
httpIR.Headers.XForwardedClientCert.CertDetailsToAdd = headerSettings.XForwardedClientCert.CertDetailsToAdd
}
}
+
+ if headerSettings.EarlyRequestHeaders != nil {
+ headersToAdd, headersToRemove, err := translateEarlyRequestHeaders(headerSettings.EarlyRequestHeaders)
+ if err != nil {
+ return err
+ }
+ httpIR.Headers.EarlyAddRequestHeaders = headersToAdd
+ httpIR.Headers.EarlyRemoveRequestHeaders = headersToRemove
+ }
+ return nil
}
func translateHTTP1Settings(http1Settings *egv1a1.HTTP1Settings, httpIR *ir.HTTPListener) error {
@@ -869,3 +883,130 @@ func buildConnection(connection *egv1a1.ClientConnection) (*ir.ClientConnection,
return irConnection, nil
}
+
+func translateEarlyRequestHeaders(headerModifier *gwapiv1.HTTPHeaderFilter) ([]ir.AddHeader, []string, error) {
+ // Make sure the header modifier config actually exists
+ if headerModifier == nil {
+ return nil, nil, nil
+ }
+ var errs error
+ emptyFilterConfig := true // keep track of whether the provided config is empty or not
+
+ var AddRequestHeaders []ir.AddHeader
+ var RemoveRequestHeaders []string
+
+ // Add request headers
+ if headersToAdd := headerModifier.Add; headersToAdd != nil {
+ if len(headersToAdd) > 0 {
+ emptyFilterConfig = false
+ }
+ for _, addHeader := range headersToAdd {
+ emptyFilterConfig = false
+ if addHeader.Name == "" {
+ errs = errors.Join(errs, fmt.Errorf("EarlyRequestHeaders cannot add a header with an empty name"))
+ // try to process the rest of the headers and produce a valid config.
+ continue
+ }
+ // Per Gateway API specification on HTTPHeaderName, : and / are invalid characters in header names
+ if strings.ContainsAny(string(addHeader.Name), "/:") {
+ errs = errors.Join(errs, fmt.Errorf("EarlyRequestHeaders Filter cannot set headers with a '/' or ':' character in them. Header: %q", string(addHeader.Name)))
+ continue
+ }
+ // Check if the header is a duplicate
+ headerKey := string(addHeader.Name)
+ canAddHeader := true
+ for _, h := range AddRequestHeaders {
+ if strings.EqualFold(h.Name, headerKey) {
+ canAddHeader = false
+ break
+ }
+ }
+
+ if !canAddHeader {
+ continue
+ }
+
+ newHeader := ir.AddHeader{
+ Name: headerKey,
+ Append: true,
+ Value: strings.Split(addHeader.Value, ","),
+ }
+
+ AddRequestHeaders = append(AddRequestHeaders, newHeader)
+ }
+ }
+
+ // Set headers
+ if headersToSet := headerModifier.Set; headersToSet != nil {
+ if len(headersToSet) > 0 {
+ emptyFilterConfig = false
+ }
+ for _, setHeader := range headersToSet {
+
+ if setHeader.Name == "" {
+ errs = errors.Join(errs, fmt.Errorf("EarlyRequestHeaders cannot set a header with an empty name"))
+ continue
+ }
+ // Per Gateway API specification on HTTPHeaderName, : and / are invalid characters in header names
+ if strings.ContainsAny(string(setHeader.Name), "/:") {
+ errs = errors.Join(errs, fmt.Errorf("EarlyRequestHeaders cannot set headers with a '/' or ':' character in them. Header: '%s'", string(setHeader.Name)))
+ continue
+ }
+
+ // Check if the header to be set has already been configured
+ headerKey := string(setHeader.Name)
+ canAddHeader := true
+ for _, h := range AddRequestHeaders {
+ if strings.EqualFold(h.Name, headerKey) {
+ canAddHeader = false
+ break
+ }
+ }
+ if !canAddHeader {
+ continue
+ }
+ newHeader := ir.AddHeader{
+ Name: string(setHeader.Name),
+ Append: false,
+ Value: strings.Split(setHeader.Value, ","),
+ }
+
+ AddRequestHeaders = append(AddRequestHeaders, newHeader)
+ }
+ }
+
+ // Remove request headers
+ // As far as Envoy is concerned, it is ok to configure a header to be added/set and also in the list of
+ // headers to remove. It will remove the original header if present and then add/set the header after.
+ if headersToRemove := headerModifier.Remove; headersToRemove != nil {
+ if len(headersToRemove) > 0 {
+ emptyFilterConfig = false
+ }
+ for _, removedHeader := range headersToRemove {
+ if removedHeader == "" {
+ errs = errors.Join(errs, fmt.Errorf("EarlyRequestHeaders cannot remove a header with an empty name"))
+ continue
+ }
+
+ canRemHeader := true
+ for _, h := range RemoveRequestHeaders {
+ if strings.EqualFold(h, removedHeader) {
+ canRemHeader = false
+ break
+ }
+ }
+ if !canRemHeader {
+ continue
+ }
+
+ RemoveRequestHeaders = append(RemoveRequestHeaders, removedHeader)
+ }
+ }
+
+ // Update the status if the filter failed to configure any valid headers to add/remove
+ if len(AddRequestHeaders) == 0 && len(RemoveRequestHeaders) == 0 && !emptyFilterConfig {
+ errs = errors.Join(errs, fmt.Errorf("EarlyRequestHeaders did not provide valid configuration to add/set/remove any headers"))
+ }
+
+ return AddRequestHeaders, RemoveRequestHeaders, errs
+}
diff --git a/internal/gatewayapi/testdata/clienttrafficpolicy-headers-error.in.yaml b/internal/gatewayapi/testdata/clienttrafficpolicy-headers-error.in.yaml
new file mode 100644
index 00000000000..3b2331bba7f
--- /dev/null
+++ b/internal/gatewayapi/testdata/clienttrafficpolicy-headers-error.in.yaml
@@ -0,0 +1,43 @@
+clientTrafficPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: ClientTrafficPolicy
+ metadata:
+ namespace: envoy-gateway
+ name: target-gateway-1
+ spec:
+ headers:
+ enableEnvoyHeaders: true
+ withUnderscoresAction: Allow
+ preserveXRequestID: true
+ earlyRequestHeaders:
+ add:
+ - name: ""
+ value: "empty"
+ - name: "invalid"
+ value: ":/"
+ set:
+ - name: ""
+ value: "empty"
+ - name: "invalid"
+ value: ":/"
+ remove:
+ - ""
+ targetRef:
+ group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-1
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: Gateway
+ metadata:
+ namespace: envoy-gateway
+ name: gateway-1
+ spec:
+ gatewayClassName: envoy-gateway-class
+ listeners:
+ - name: http-1
+ protocol: HTTP
+ port: 80
+ allowedRoutes:
+ namespaces:
+ from: Same
diff --git a/internal/gatewayapi/testdata/clienttrafficpolicy-headers-error.out.yaml b/internal/gatewayapi/testdata/clienttrafficpolicy-headers-error.out.yaml
new file mode 100644
index 00000000000..9eee58d7df7
--- /dev/null
+++ b/internal/gatewayapi/testdata/clienttrafficpolicy-headers-error.out.yaml
@@ -0,0 +1,127 @@
+clientTrafficPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: ClientTrafficPolicy
+ metadata:
+ creationTimestamp: null
+ name: target-gateway-1
+ namespace: envoy-gateway
+ spec:
+ headers:
+ earlyRequestHeaders:
+ add:
+ - name: ""
+ value: empty
+ - name: invalid
+ value: :/
+ remove:
+ - ""
+ set:
+ - name: ""
+ value: empty
+ - name: invalid
+ value: :/
+ enableEnvoyHeaders: true
+ preserveXRequestID: true
+ withUnderscoresAction: Allow
+ targetRef:
+ group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-1
+ status:
+ ancestors:
+ - ancestorRef:
+ group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-1
+ namespace: envoy-gateway
+ conditions:
+ - lastTransitionTime: null
+ message: |-
+ Headers: EarlyRequestHeaders cannot add a header with an empty name
+ EarlyRequestHeaders cannot set a header with an empty name
+ EarlyRequestHeaders cannot remove a header with an empty name.
+ reason: Invalid
+ status: "False"
+ type: Accepted
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: Gateway
+ metadata:
+ creationTimestamp: null
+ name: gateway-1
+ namespace: envoy-gateway
+ spec:
+ gatewayClassName: envoy-gateway-class
+ listeners:
+ - allowedRoutes:
+ namespaces:
+ from: Same
+ name: http-1
+ port: 80
+ protocol: HTTP
+ status:
+ listeners:
+ - attachedRoutes: 0
+ conditions:
+ - lastTransitionTime: null
+ message: Sending translated listener configuration to the data plane
+ reason: Programmed
+ status: "True"
+ type: Programmed
+ - lastTransitionTime: null
+ message: Listener has been successfully translated
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ - lastTransitionTime: null
+ message: Listener references have been resolved
+ reason: ResolvedRefs
+ status: "True"
+ type: ResolvedRefs
+ name: http-1
+ supportedKinds:
+ - group: gateway.networking.k8s.io
+ kind: HTTPRoute
+ - group: gateway.networking.k8s.io
+ kind: GRPCRoute
+infraIR:
+ envoy-gateway/gateway-1:
+ proxy:
+ listeners:
+ - address: null
+ name: envoy-gateway/gateway-1/http-1
+ ports:
+ - containerPort: 10080
+ name: http-80
+ protocol: HTTP
+ servicePort: 80
+ metadata:
+ labels:
+ gateway.envoyproxy.io/owning-gateway-name: gateway-1
+ gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+ name: envoy-gateway/gateway-1
+xdsIR:
+ envoy-gateway/gateway-1:
+ accessLog:
+ text:
+ - path: /dev/stdout
+ http:
+ - address: 0.0.0.0
+ headers:
+ enableEnvoyHeaders: true
+ preserveXRequestID: true
+ withUnderscoresAction: Allow
+ hostnames:
+ - '*'
+ isHTTP2: false
+ metadata:
+ kind: Gateway
+ name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http-1
+ name: envoy-gateway/gateway-1/http-1
+ path:
+ escapedSlashesAction: UnescapeAndRedirect
+ mergeSlashes: true
+ port: 10080
diff --git a/internal/gatewayapi/testdata/clienttrafficpolicy-headers.in.yaml b/internal/gatewayapi/testdata/clienttrafficpolicy-headers.in.yaml
index 6d73bee1a16..3234aed7da8 100644
--- a/internal/gatewayapi/testdata/clienttrafficpolicy-headers.in.yaml
+++ b/internal/gatewayapi/testdata/clienttrafficpolicy-headers.in.yaml
@@ -9,6 +9,20 @@ clientTrafficPolicies:
enableEnvoyHeaders: true
withUnderscoresAction: Allow
preserveXRequestID: true
+ earlyRequestHeaders:
+ add:
+ - name: "my-added-header"
+ value: "my-added-header-value"
+ - name: "my-added-header"
+ value: "my-added-header-value"
+ set:
+ - name: "my-set-header"
+ value: "my-set-header-value"
+ - name: "my-set-header"
+ value: "my-set-header-value"
+ remove:
+ - "my-removed-header"
+ - "my-removed-header"
targetRef:
group: gateway.networking.k8s.io
kind: Gateway
diff --git a/internal/gatewayapi/testdata/clienttrafficpolicy-headers.out.yaml b/internal/gatewayapi/testdata/clienttrafficpolicy-headers.out.yaml
index 8b32bb192da..4e66bd91c64 100644
--- a/internal/gatewayapi/testdata/clienttrafficpolicy-headers.out.yaml
+++ b/internal/gatewayapi/testdata/clienttrafficpolicy-headers.out.yaml
@@ -7,6 +7,20 @@ clientTrafficPolicies:
namespace: envoy-gateway
spec:
headers:
+ earlyRequestHeaders:
+ add:
+ - name: my-added-header
+ value: my-added-header-value
+ - name: my-added-header
+ value: my-added-header-value
+ remove:
+ - my-removed-header
+ - my-removed-header
+ set:
+ - name: my-set-header
+ value: my-set-header-value
+ - name: my-set-header
+ value: my-set-header-value
enableEnvoyHeaders: true
preserveXRequestID: true
withUnderscoresAction: Allow
@@ -129,6 +143,17 @@ xdsIR:
http:
- address: 0.0.0.0
headers:
+ earlyAddRequestHeaders:
+ - append: true
+ name: my-added-header
+ value:
+ - my-added-header-value
+ - append: false
+ name: my-set-header
+ value:
+ - my-set-header-value
+ earlyRemoveRequestHeaders:
+ - my-removed-header
enableEnvoyHeaders: true
preserveXRequestID: true
withUnderscoresAction: Allow
@@ -147,6 +172,17 @@ xdsIR:
port: 10080
- address: 0.0.0.0
headers:
+ earlyAddRequestHeaders:
+ - append: true
+ name: my-added-header
+ value:
+ - my-added-header-value
+ - append: false
+ name: my-set-header
+ value:
+ - my-set-header-value
+ earlyRemoveRequestHeaders:
+ - my-removed-header
enableEnvoyHeaders: true
preserveXRequestID: true
withUnderscoresAction: Allow
diff --git a/internal/ir/xds.go b/internal/ir/xds.go
index 56c1bef0958..821d4bd1c70 100644
--- a/internal/ir/xds.go
+++ b/internal/ir/xds.go
@@ -494,6 +494,12 @@ type HeaderSettings struct {
// (Edge request is the request from external clients to front Envoy) and not reset it, which is the current Envoy behaviour.
// It defaults to false.
PreserveXRequestID bool `json:"preserveXRequestID,omitempty" yaml:"preserveXRequestID,omitempty"`
+
+ // EarlyAddRequestHeaders defines headers that would be added before envoy request processing.
+ EarlyAddRequestHeaders []AddHeader `json:"earlyAddRequestHeaders,omitempty" yaml:"earlyAddRequestHeaders,omitempty"`
+
+ // EarlyRemoveRequestHeaders defines headers that would be removed before envoy request processing.
+ EarlyRemoveRequestHeaders []string `json:"earlyRemoveRequestHeaders,omitempty" yaml:"earlyRemoveRequestHeaders,omitempty"`
}
// ClientTimeout sets the timeout configuration for downstream connections
diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go
index 5f8f879e6a0..d38cd3b825c 100644
--- a/internal/ir/zz_generated.deepcopy.go
+++ b/internal/ir/zz_generated.deepcopy.go
@@ -1481,6 +1481,18 @@ func (in *HeaderSettings) DeepCopyInto(out *HeaderSettings) {
*out = new(XForwardedClientCert)
(*in).DeepCopyInto(*out)
}
+ if in.EarlyAddRequestHeaders != nil {
+ in, out := &in.EarlyAddRequestHeaders, &out.EarlyAddRequestHeaders
+ *out = make([]AddHeader, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+ if in.EarlyRemoveRequestHeaders != nil {
+ in, out := &in.EarlyRemoveRequestHeaders, &out.EarlyRemoveRequestHeaders
+ *out = make([]string, len(*in))
+ copy(*out, *in)
+ }
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HeaderSettings.
diff --git a/internal/xds/translator/listener.go b/internal/xds/translator/listener.go
index 0812010ade1..9b442c75105 100644
--- a/internal/xds/translator/listener.go
+++ b/internal/xds/translator/listener.go
@@ -12,6 +12,7 @@ import (
xdscore "github.com/cncf/xds/go/xds/core/v3"
matcher "github.com/cncf/xds/go/xds/type/matcher/v3"
+ mutation_rulesv3 "github.com/envoyproxy/go-control-plane/envoy/config/common/mutation_rules/v3"
corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
listenerv3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
tls_inspectorv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/listener/tls_inspector/v3"
@@ -19,6 +20,7 @@ import (
hcmv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
tcpv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/tcp_proxy/v3"
udpv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/udp/udp_proxy/v3"
+ early_header_mutationv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/http/early_header_mutation/header_mutation/v3"
preservecasev3 "github.com/envoyproxy/go-control-plane/envoy/extensions/http/header_formatters/preserve_case/v3"
customheaderv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/http/original_ip_detection/custom_header/v3"
quicv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/quic/v3"
@@ -274,9 +276,10 @@ func (t *Translator) addHCMToXDSListener(xdsListener *listenerv3.Listener, irLis
CommonHttpProtocolOptions: &corev3.HttpProtocolOptions{
HeadersWithUnderscoresAction: buildHeadersWithUnderscoresAction(irListener.Headers),
},
- Tracing: hcmTracing,
- ForwardClientCertDetails: buildForwardClientCertDetailsAction(irListener.Headers),
- PreserveExternalRequestId: ptr.Deref(irListener.Headers, ir.HeaderSettings{}).PreserveXRequestID,
+ Tracing: hcmTracing,
+ ForwardClientCertDetails: buildForwardClientCertDetailsAction(irListener.Headers),
+ PreserveExternalRequestId: ptr.Deref(irListener.Headers, ir.HeaderSettings{}).PreserveXRequestID,
+ EarlyHeaderMutationExtensions: buildEarlyHeaderMutation(irListener.Headers),
}
if mgr.ForwardClientCertDetails == hcmv3.HttpConnectionManager_APPEND_FORWARD || mgr.ForwardClientCertDetails == hcmv3.HttpConnectionManager_SANITIZE_SET {
@@ -365,6 +368,73 @@ func (t *Translator) addHCMToXDSListener(xdsListener *listenerv3.Listener, irLis
return nil
}
+func buildEarlyHeaderMutation(headers *ir.HeaderSettings) []*corev3.TypedExtensionConfig {
+ if headers == nil || (len(headers.EarlyAddRequestHeaders) == 0 && len(headers.EarlyRemoveRequestHeaders) == 0) {
+ return nil
+ }
+
+ var mutationRules []*mutation_rulesv3.HeaderMutation
+
+ for _, header := range headers.EarlyAddRequestHeaders {
+ var appendAction corev3.HeaderValueOption_HeaderAppendAction
+ if header.Append {
+ appendAction = corev3.HeaderValueOption_APPEND_IF_EXISTS_OR_ADD
+ } else {
+ appendAction = corev3.HeaderValueOption_OVERWRITE_IF_EXISTS_OR_ADD
+ }
+ // Allow empty headers to be set, but don't add the config to do so unless necessary
+ if len(header.Value) == 0 {
+ mutationRules = append(mutationRules, &mutation_rulesv3.HeaderMutation{
+ Action: &mutation_rulesv3.HeaderMutation_Append{
+ Append: &corev3.HeaderValueOption{
+ Header: &corev3.HeaderValue{
+ Key: header.Name,
+ },
+ AppendAction: appendAction,
+ KeepEmptyValue: true,
+ },
+ },
+ })
+ } else {
+ for _, val := range header.Value {
+ mutationRules = append(mutationRules, &mutation_rulesv3.HeaderMutation{
+ Action: &mutation_rulesv3.HeaderMutation_Append{
+ Append: &corev3.HeaderValueOption{
+ Header: &corev3.HeaderValue{
+ Key: header.Name,
+ Value: val,
+ },
+ AppendAction: appendAction,
+ KeepEmptyValue: val == "",
+ },
+ },
+ })
+ }
+ }
+ }
+
+ for _, header := range headers.EarlyRemoveRequestHeaders {
+ mr := &mutation_rulesv3.HeaderMutation{
+ Action: &mutation_rulesv3.HeaderMutation_Remove{
+ Remove: header,
+ },
+ }
+
+ mutationRules = append(mutationRules, mr)
+ }
+
+ earlyHeaderMutationAny, _ := anypb.New(&early_header_mutationv3.HeaderMutation{
+ Mutations: mutationRules,
+ })
+
+ return []*corev3.TypedExtensionConfig{
+ {
+ Name: "envoy.http.early_header_mutation.header_mutation",
+ TypedConfig: earlyHeaderMutationAny,
+ },
+ }
+}
+
func addServerNamesMatch(xdsListener *listenerv3.Listener, filterChain *listenerv3.FilterChain, hostnames []string) error {
// Dont add a filter chain match if the hostname is a wildcard character.
if len(hostnames) > 0 && hostnames[0] != "*" {
diff --git a/internal/xds/translator/testdata/in/xds-ir/http-early-header-mutation.yaml b/internal/xds/translator/testdata/in/xds-ir/http-early-header-mutation.yaml
new file mode 100644
index 00000000000..6301153cd1c
--- /dev/null
+++ b/internal/xds/translator/testdata/in/xds-ir/http-early-header-mutation.yaml
@@ -0,0 +1,59 @@
+http:
+- name: "first-listener"
+ address: "0.0.0.0"
+ port: 10080
+ hostnames:
+ - "*"
+ http1:
+ preserveHeaderCase: true
+ path:
+ mergeSlashes: true
+ escapedSlashesAction: UnescapeAndRedirect
+ routes:
+ - name: "first-route"
+ hostname: "*"
+ destination:
+ name: "first-route-dest"
+ settings:
+ - endpoints:
+ - host: "1.2.3.4"
+ port: 50000
+- name: "second-listener"
+ address: "0.0.0.0"
+ port: 10081
+ hostnames:
+ - "*"
+ headers:
+ earlyAddRequestHeaders:
+ - name: "some-header"
+ value:
+ - "some-value1"
+ - "some-value2"
+ append: true
+ - name: "some-header-2"
+ value:
+ - "some-value"
+ append: true
+ - name: "some-header3"
+ value:
+ - "some-value"
+ append: false
+ - name: "some-header4"
+ value:
+ - "some-value"
+ append: false
+ - name: "empty-header"
+ value:
+ append: false
+ earlyRemoveRequestHeaders:
+ - "some-header5"
+ - "some-header6"
+ routes:
+ - name: "second-route"
+ hostname: "*"
+ destination:
+ name: "second-route-dest"
+ settings:
+ - endpoints:
+ - host: "1.2.3.5"
+ port: 50000
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-early-header-mutation.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/http-early-header-mutation.clusters.yaml
new file mode 100644
index 00000000000..22e6727066a
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/http-early-header-mutation.clusters.yaml
@@ -0,0 +1,44 @@
+- circuitBreakers:
+ thresholds:
+ - maxRetries: 1024
+ commonLbConfig:
+ localityWeightedLbConfig: {}
+ connectTimeout: 10s
+ dnsLookupFamily: V4_ONLY
+ edsClusterConfig:
+ edsConfig:
+ ads: {}
+ resourceApiVersion: V3
+ serviceName: first-route-dest
+ lbPolicy: LEAST_REQUEST
+ name: first-route-dest
+ outlierDetection: {}
+ perConnectionBufferLimitBytes: 32768
+ type: EDS
+ typedExtensionProtocolOptions:
+ envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
+ '@type': type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
+ explicitHttpConfig:
+ httpProtocolOptions:
+ headerKeyFormat:
+ statefulFormatter:
+ name: preserve_case
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.http.header_formatters.preserve_case.v3.PreserveCaseFormatterConfig
+- circuitBreakers:
+ thresholds:
+ - maxRetries: 1024
+ commonLbConfig:
+ localityWeightedLbConfig: {}
+ connectTimeout: 10s
+ dnsLookupFamily: V4_ONLY
+ edsClusterConfig:
+ edsConfig:
+ ads: {}
+ resourceApiVersion: V3
+ serviceName: second-route-dest
+ lbPolicy: LEAST_REQUEST
+ name: second-route-dest
+ outlierDetection: {}
+ perConnectionBufferLimitBytes: 32768
+ type: EDS
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-early-header-mutation.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/http-early-header-mutation.endpoints.yaml
new file mode 100644
index 00000000000..28a57caf3b5
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/http-early-header-mutation.endpoints.yaml
@@ -0,0 +1,24 @@
+- clusterName: first-route-dest
+ endpoints:
+ - lbEndpoints:
+ - endpoint:
+ address:
+ socketAddress:
+ address: 1.2.3.4
+ portValue: 50000
+ loadBalancingWeight: 1
+ loadBalancingWeight: 1
+ locality:
+ region: first-route-dest/backend/0
+- clusterName: second-route-dest
+ endpoints:
+ - lbEndpoints:
+ - endpoint:
+ address:
+ socketAddress:
+ address: 1.2.3.5
+ portValue: 50000
+ loadBalancingWeight: 1
+ loadBalancingWeight: 1
+ locality:
+ region: second-route-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-early-header-mutation.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/http-early-header-mutation.listeners.yaml
new file mode 100644
index 00000000000..69c2612a5f8
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/http-early-header-mutation.listeners.yaml
@@ -0,0 +1,108 @@
+- address:
+ socketAddress:
+ address: 0.0.0.0
+ portValue: 10080
+ defaultFilterChain:
+ filters:
+ - name: envoy.filters.network.http_connection_manager
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+ commonHttpProtocolOptions:
+ headersWithUnderscoresAction: REJECT_REQUEST
+ http2ProtocolOptions:
+ initialConnectionWindowSize: 1048576
+ initialStreamWindowSize: 65536
+ maxConcurrentStreams: 100
+ httpFilters:
+ - name: envoy.filters.http.router
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
+ suppressEnvoyHeaders: true
+ httpProtocolOptions:
+ headerKeyFormat:
+ statefulFormatter:
+ name: preserve_case
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.http.header_formatters.preserve_case.v3.PreserveCaseFormatterConfig
+ mergeSlashes: true
+ normalizePath: true
+ pathWithEscapedSlashesAction: UNESCAPE_AND_REDIRECT
+ rds:
+ configSource:
+ ads: {}
+ resourceApiVersion: V3
+ routeConfigName: first-listener
+ serverHeaderTransformation: PASS_THROUGH
+ statPrefix: http-10080
+ useRemoteAddress: true
+ name: first-listener
+ drainType: MODIFY_ONLY
+ name: first-listener
+ perConnectionBufferLimitBytes: 32768
+- address:
+ socketAddress:
+ address: 0.0.0.0
+ portValue: 10081
+ defaultFilterChain:
+ filters:
+ - name: envoy.filters.network.http_connection_manager
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+ commonHttpProtocolOptions:
+ headersWithUnderscoresAction: REJECT_REQUEST
+ earlyHeaderMutationExtensions:
+ - name: envoy.http.early_header_mutation.header_mutation
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.http.early_header_mutation.header_mutation.v3.HeaderMutation
+ mutations:
+ - append:
+ header:
+ key: some-header
+ value: some-value1
+ - append:
+ header:
+ key: some-header
+ value: some-value2
+ - append:
+ header:
+ key: some-header-2
+ value: some-value
+ - append:
+ appendAction: OVERWRITE_IF_EXISTS_OR_ADD
+ header:
+ key: some-header3
+ value: some-value
+ - append:
+ appendAction: OVERWRITE_IF_EXISTS_OR_ADD
+ header:
+ key: some-header4
+ value: some-value
+ - append:
+ appendAction: OVERWRITE_IF_EXISTS_OR_ADD
+ header:
+ key: empty-header
+ keepEmptyValue: true
+ - remove: some-header5
+ - remove: some-header6
+ http2ProtocolOptions:
+ initialConnectionWindowSize: 1048576
+ initialStreamWindowSize: 65536
+ maxConcurrentStreams: 100
+ httpFilters:
+ - name: envoy.filters.http.router
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
+ suppressEnvoyHeaders: true
+ normalizePath: true
+ rds:
+ configSource:
+ ads: {}
+ resourceApiVersion: V3
+ routeConfigName: second-listener
+ serverHeaderTransformation: PASS_THROUGH
+ statPrefix: http-10081
+ useRemoteAddress: true
+ name: second-listener
+ drainType: MODIFY_ONLY
+ name: second-listener
+ perConnectionBufferLimitBytes: 32768
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-early-header-mutation.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/http-early-header-mutation.routes.yaml
new file mode 100644
index 00000000000..ff93cfff360
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/http-early-header-mutation.routes.yaml
@@ -0,0 +1,28 @@
+- ignorePortInHostMatching: true
+ name: first-listener
+ virtualHosts:
+ - domains:
+ - '*'
+ name: first-listener/*
+ routes:
+ - match:
+ prefix: /
+ name: first-route
+ route:
+ cluster: first-route-dest
+ upgradeConfigs:
+ - upgradeType: websocket
+- ignorePortInHostMatching: true
+ name: second-listener
+ virtualHosts:
+ - domains:
+ - '*'
+ name: second-listener/*
+ routes:
+ - match:
+ prefix: /
+ name: second-route
+ route:
+ cluster: second-route-dest
+ upgradeConfigs:
+ - upgradeType: websocket
diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md
index 99caabc363e..3e6f75d074a 100644
--- a/site/content/en/latest/api/extension_types.md
+++ b/site/content/en/latest/api/extension_types.md
@@ -1962,6 +1962,7 @@ _Appears in:_
| `xForwardedClientCert` | _[XForwardedClientCert](#xforwardedclientcert)_ | false | XForwardedClientCert configures how Envoy Proxy handle the x-forwarded-client-cert (XFCC) HTTP header.
x-forwarded-client-cert (XFCC) is an HTTP header used to forward the certificate
information of part or all of the clients or proxies that a request has flowed through,
on its way from the client to the server.
Envoy proxy may choose to sanitize/append/forward the XFCC header before proxying the request.
If not set, the default behavior is sanitizing the XFCC header. |
| `withUnderscoresAction` | _[WithUnderscoresAction](#withunderscoresaction)_ | false | WithUnderscoresAction configures the action to take when an HTTP header with underscores
is encountered. The default action is to reject the request. |
| `preserveXRequestID` | _boolean_ | false | PreserveXRequestID configures Envoy to keep the X-Request-ID header if passed for a request that is edge
(Edge request is the request from external clients to front Envoy) and not reset it, which is the current Envoy behaviour.
It defaults to false. |
+| `earlyRequestHeaders` | _[HTTPHeaderFilter](#httpheaderfilter)_ | false | EarlyRequestHeaders defines settings for early request header modification, before envoy performs
routing, tracing and built-in header manipulation. |
diff --git a/site/content/en/latest/tasks/traffic/http-request-headers.md b/site/content/en/latest/tasks/traffic/http-request-headers.md
index 9cd60281cdf..5b73bfaf8d3 100644
--- a/site/content/en/latest/tasks/traffic/http-request-headers.md
+++ b/site/content/en/latest/tasks/traffic/http-request-headers.md
@@ -442,7 +442,179 @@ spec:
{{% /tab %}}
{{< /tabpane >}}
+## Early Header Modification
+
+In some cases, it could be necessary to modify headers before the proxy performs any sort of processing, routing or tracing. Envoy Gateway supports this functionality using the [ClientTrafficPolicy][] API.
+
+A ClientTrafficPolicy resource can be attached to a Gateway resource to configure early header modifications for all its routes. In the following example we will demonstrate how early header modification can be configured.
+
+{{< tabpane text=true >}}
+{{% tab header="Apply from stdin" %}}
+
+```shell
+cat <}}
+
+
+Querying `headers.example/get` should result in a `200` response from the example Gateway and the output from the
+example app should indicate that the upstream example app received the following headers:
+- `early-added-header` contains early (ClientTrafficPolicy) and late (RouteFilter) values
+- `early-set-header` contains only early (ClientTrafficPolicy) and late (RouteFilter) values, since the early modification overwritten the client value.
+- `early-removed-header` contains only the late (RouteFilter) value, since the early modification deleted the client value.
+
+```console
+$ curl -vvv --header "Host: headers.example" "http://${GATEWAY_HOST}/get" --header "early-added-header: client" --header "early-set-header: client" --header "early-removed-header: client"
+...
+> GET /get HTTP/1.1
+> Host: headers.example
+> User-Agent: curl/7.81.0
+> Accept: */*
+> add-header: something
+>
+* Mark bundle as not supporting multiuse
+< HTTP/1.1 200 OK
+< content-type: application/json
+< x-content-type-options: nosniff
+< content-length: 474
+< x-envoy-upstream-service-time: 0
+< server: envoy
+<
+
+ "headers": {
+ "Accept": [
+ "*/*"
+ ],
+ "Early-Added-Header": [
+ "client",
+ "early",
+ "late"
+ ],
+ "Early-Set-Header": [
+ "early",
+ "late"
+ ],
+ "Early-removed-Header": [
+ "late"
+ ]
+...
+```
+
[HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute/
[HTTPRoute filters]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPRouteFilter
[Gateway API documentation]: https://gateway-api.sigs.k8s.io/
[req_filter]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPHeaderFilter
+[ClientTrafficPolicy]: ../../../api/extension_types#clienttrafficpolicy
diff --git a/site/content/zh/latest/api/extension_types.md b/site/content/zh/latest/api/extension_types.md
index 99caabc363e..3e6f75d074a 100644
--- a/site/content/zh/latest/api/extension_types.md
+++ b/site/content/zh/latest/api/extension_types.md
@@ -1962,6 +1962,7 @@ _Appears in:_
| `xForwardedClientCert` | _[XForwardedClientCert](#xforwardedclientcert)_ | false | XForwardedClientCert configures how Envoy Proxy handle the x-forwarded-client-cert (XFCC) HTTP header.
x-forwarded-client-cert (XFCC) is an HTTP header used to forward the certificate
information of part or all of the clients or proxies that a request has flowed through,
on its way from the client to the server.
Envoy proxy may choose to sanitize/append/forward the XFCC header before proxying the request.
If not set, the default behavior is sanitizing the XFCC header. |
| `withUnderscoresAction` | _[WithUnderscoresAction](#withunderscoresaction)_ | false | WithUnderscoresAction configures the action to take when an HTTP header with underscores
is encountered. The default action is to reject the request. |
| `preserveXRequestID` | _boolean_ | false | PreserveXRequestID configures Envoy to keep the X-Request-ID header if passed for a request that is edge
(Edge request is the request from external clients to front Envoy) and not reset it, which is the current Envoy behaviour.
It defaults to false. |
+| `earlyRequestHeaders` | _[HTTPHeaderFilter](#httpheaderfilter)_ | false | EarlyRequestHeaders defines settings for early request header modification, before envoy performs
routing, tracing and built-in header manipulation. |
diff --git a/test/e2e/testdata/header-settings.yaml b/test/e2e/testdata/header-settings.yaml
new file mode 100644
index 00000000000..dab686f29c7
--- /dev/null
+++ b/test/e2e/testdata/header-settings.yaml
@@ -0,0 +1,47 @@
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: ClientTrafficPolicy
+metadata:
+ name: early-header-modifier-ctp
+ namespace: gateway-conformance-infra
+spec:
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: Gateway
+ name: same-namespace
+ headers:
+ earlyRequestHeaders:
+ add:
+ - name: "early-added-header"
+ value: "early"
+ set:
+ - name: "early-set-header"
+ value: "early"
+ remove:
+ - "early-removed-header"
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+ name: http-with-early-headers
+ namespace: gateway-conformance-infra
+spec:
+ parentRefs:
+ - name: same-namespace
+ rules:
+ - matches:
+ - path:
+ type: PathPrefix
+ value: /early-header
+ filters:
+ - type: RequestHeaderModifier
+ requestHeaderModifier:
+ add:
+ - name: early-added-header
+ value: late
+ - name: early-set-header
+ value: late
+ - name: early-removed-header
+ value: late
+ backendRefs:
+ - name: infra-backend-v1
+ port: 8080
diff --git a/test/e2e/tests/header_settings.go b/test/e2e/tests/header_settings.go
new file mode 100644
index 00000000000..32f0d731089
--- /dev/null
+++ b/test/e2e/tests/header_settings.go
@@ -0,0 +1,75 @@
+// Copyright Envoy Gateway Authors
+// SPDX-License-Identifier: Apache-2.0
+// The full text of the Apache license is available in the LICENSE file at
+// the root of the repo.
+
+//go:build e2e
+// +build e2e
+
+package tests
+
+import (
+ "testing"
+
+ "k8s.io/apimachinery/pkg/types"
+ gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
+ gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
+ "sigs.k8s.io/gateway-api/conformance/utils/http"
+ "sigs.k8s.io/gateway-api/conformance/utils/kubernetes"
+ "sigs.k8s.io/gateway-api/conformance/utils/suite"
+
+ "github.com/envoyproxy/gateway/internal/gatewayapi"
+)
+
+func init() {
+ ConformanceTests = append(ConformanceTests, HeaderSettingsTest)
+}
+
+var HeaderSettingsTest = suite.ConformanceTest{
+ ShortName: "HeaderSettings",
+ Description: "Modify headers before regular processing",
+ Manifests: []string{"testdata/header-settings.yaml"},
+ Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
+ t.Run("Early header modifications should apply", func(t *testing.T) {
+ ns := "gateway-conformance-infra"
+ routeNN := types.NamespacedName{Name: "http-with-early-headers", Namespace: ns}
+ gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns}
+ gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN)
+
+ ancestorRef := gwapiv1a2.ParentReference{
+ Group: gatewayapi.GroupPtr(gwapiv1.GroupName),
+ Kind: gatewayapi.KindPtr(gatewayapi.KindGateway),
+ Namespace: gatewayapi.NamespacePtr(gwNN.Namespace),
+ Name: gwapiv1.ObjectName(gwNN.Name),
+ }
+ ClientTrafficPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "early-header-modifier-ctp", Namespace: ns}, suite.ControllerName, ancestorRef)
+
+ expected := http.ExpectedResponse{
+ Request: http.Request{
+ Path: "/early-header",
+ Headers: map[string]string{
+ "early-added-header": "client",
+ "early-set-header": "client",
+ "early-removed-header": "client",
+ },
+ },
+ ExpectedRequest: &http.ExpectedRequest{
+ Request: http.Request{
+ Path: "/early-header",
+ Headers: map[string]string{
+ "early-added-header": "client,early,late", // client, early and late are all added to header
+ "early-set-header": "early,late", // early set overwrites client value
+ "early-removed-header": "late", // removed by early, so only late value exists
+ },
+ },
+ },
+ Response: http.Response{
+ StatusCode: 200,
+ },
+ Namespace: ns,
+ }
+
+ http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expected)
+ })
+ },
+}