diff --git a/api/v1alpha1/http_listener_policy_types.go b/api/v1alpha1/http_listener_policy_types.go index eae1a7c584b..23630218020 100644 --- a/api/v1alpha1/http_listener_policy_types.go +++ b/api/v1alpha1/http_listener_policy_types.go @@ -124,6 +124,13 @@ type HTTPListenerPolicySpec struct { // +optional // +kubebuilder:validation:MinLength=1 DefaultHostForHttp10 *string `json:"defaultHostForHttp10,omitempty"` + + // EarlyRequestHeaderModifier defines header modifications to be applied early in the request processing, + // before route selection. + // For example, if you use ExternalAuthz to add a header, you may want to remove it here, to make + // sure it did not come from the client. + // +optional + EarlyRequestHeaderModifier *gwv1.HTTPHeaderFilter `json:"earlyRequestHeaderModifier,omitempty"` } // AccessLog represents the top-level access log configuration. diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index ea2136e732a..b97763ea5b6 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -3597,6 +3597,11 @@ func (in *HTTPListenerPolicySpec) DeepCopyInto(out *HTTPListenerPolicySpec) { *out = new(string) **out = **in } + if in.EarlyRequestHeaderModifier != nil { + in, out := &in.EarlyRequestHeaderModifier, &out.EarlyRequestHeaderModifier + *out = new(apisv1.HTTPHeaderFilter) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPListenerPolicySpec. diff --git a/install/helm/kgateway-crds/templates/gateway.kgateway.dev_httplistenerpolicies.yaml b/install/helm/kgateway-crds/templates/gateway.kgateway.dev_httplistenerpolicies.yaml index 38c91a107fb..27df2259683 100644 --- a/install/helm/kgateway-crds/templates/gateway.kgateway.dev_httplistenerpolicies.yaml +++ b/install/helm/kgateway-crds/templates/gateway.kgateway.dev_httplistenerpolicies.yaml @@ -1143,6 +1143,137 @@ spec: See here for more information: https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto#config-core-v3-http1protocoloptions minLength: 1 type: string + earlyRequestHeaderModifier: + description: |- + EarlyRequestHeaderModifier defines header modifications to be applied early in the request processing, + before route selection. + For example, if you use ExternalAuthz to add a header, you may want to remove it here, to make + sure it did not come from the client. + 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 healthCheck: description: HealthCheck configures [Envoy health checks](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/http/health_check/v3/health_check.proto) properties: diff --git a/internal/kgateway/extensions2/plugins/httplistenerpolicy/httplistener_plugin.go b/internal/kgateway/extensions2/plugins/httplistenerpolicy/httplistener_plugin.go index 65a0878488b..f2d1b649fba 100644 --- a/internal/kgateway/extensions2/plugins/httplistenerpolicy/httplistener_plugin.go +++ b/internal/kgateway/extensions2/plugins/httplistenerpolicy/httplistener_plugin.go @@ -13,6 +13,7 @@ import ( envoytracev3 "github.com/envoyproxy/go-control-plane/envoy/config/trace/v3" healthcheckv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/health_check/v3" envoy_hcm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" + envoy_header_mutationv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/http/early_header_mutation/header_mutation/v3" preserve_case_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/http/header_formatters/preserve_case/v3" envoymatcherv3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3" "google.golang.org/protobuf/proto" @@ -22,8 +23,10 @@ import ( "istio.io/istio/pkg/kube/krt" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/utils/ptr" + gwv1 "sigs.k8s.io/gateway-api/apis/v1" "github.com/kgateway-dev/kgateway/v2/api/v1alpha1" + "github.com/kgateway-dev/kgateway/v2/internal/kgateway/extensions2/pluginutils" "github.com/kgateway-dev/kgateway/v2/internal/kgateway/utils" "github.com/kgateway-dev/kgateway/v2/internal/kgateway/wellknown" "github.com/kgateway-dev/kgateway/v2/pkg/krtcollections" @@ -61,10 +64,11 @@ type httpListenerPolicy struct { // Since the gateway name can only be determined during translation, the tracing config is split into the provider // and the actual config. During translation, the default serviceName is set if not already provided // and the final config is then marshalled. - tracingProvider *envoytracev3.OpenTelemetryConfig - tracingConfig *envoy_hcm.HttpConnectionManager_Tracing - acceptHttp10 *bool - defaultHostForHttp10 *string + tracingProvider *envoytracev3.OpenTelemetryConfig + tracingConfig *envoy_hcm.HttpConnectionManager_Tracing + acceptHttp10 *bool + defaultHostForHttp10 *string + earlyHeaderMutationExtensions []*envoycorev3.TypedExtensionConfig } func (d *httpListenerPolicy) CreationTime() time.Time { @@ -157,6 +161,11 @@ func (d *httpListenerPolicy) Equals(in any) bool { return false } + if !slices.EqualFunc(d.earlyHeaderMutationExtensions, d2.earlyHeaderMutationExtensions, func(a, b *envoycorev3.TypedExtensionConfig) bool { + return proto.Equal(a, b) + }) { + return false + } return true } @@ -234,21 +243,22 @@ func NewPlugin(ctx context.Context, commoncol *collections.CommonCollections) sd ObjectSource: objSrc, Policy: i, PolicyIR: &httpListenerPolicy{ - ct: i.CreationTimestamp.Time, - accessLogConfig: accessLog, - accessLogPolicies: i.Spec.AccessLog, - tracingProvider: tracingProvider, - tracingConfig: tracingConfig, - upgradeConfigs: upgradeConfigs, - useRemoteAddress: i.Spec.UseRemoteAddress, - xffNumTrustedHops: xffNumTrustedHops, - serverHeaderTransformation: serverHeaderTransformation, - streamIdleTimeout: streamIdleTimeout, - idleTimeout: idleTimeout, - healthCheckPolicy: healthCheckPolicy, - preserveHttp1HeaderCase: i.Spec.PreserveHttp1HeaderCase, - acceptHttp10: i.Spec.AcceptHttp10, - defaultHostForHttp10: i.Spec.DefaultHostForHttp10, + ct: i.CreationTimestamp.Time, + accessLogConfig: accessLog, + accessLogPolicies: i.Spec.AccessLog, + tracingProvider: tracingProvider, + tracingConfig: tracingConfig, + upgradeConfigs: upgradeConfigs, + useRemoteAddress: i.Spec.UseRemoteAddress, + xffNumTrustedHops: xffNumTrustedHops, + serverHeaderTransformation: serverHeaderTransformation, + streamIdleTimeout: streamIdleTimeout, + idleTimeout: idleTimeout, + healthCheckPolicy: healthCheckPolicy, + preserveHttp1HeaderCase: i.Spec.PreserveHttp1HeaderCase, + acceptHttp10: i.Spec.AcceptHttp10, + defaultHostForHttp10: i.Spec.DefaultHostForHttp10, + earlyHeaderMutationExtensions: convertHeaderMutations(i.Spec.EarlyRequestHeaderModifier), }, TargetRefs: pluginsdkutils.TargetRefsToPolicyRefs(i.Spec.TargetRefs, i.Spec.TargetSelectors), Errors: errs, @@ -347,6 +357,10 @@ func (p *httpListenerPolicyPluginGwPass) ApplyHCM( if policy.streamIdleTimeout != nil { out.StreamIdleTimeout = durationpb.New(*policy.streamIdleTimeout) } + // early request header modifier + if len(policy.earlyHeaderMutationExtensions) != 0 { + out.EarlyHeaderMutationExtensions = append(out.EarlyHeaderMutationExtensions, policy.earlyHeaderMutationExtensions...) + } // translate idleTimeout if policy.idleTimeout != nil { @@ -476,3 +490,19 @@ func convertHealthCheckPolicy(policy *v1alpha1.HTTPListenerPolicy) *healthcheckv } return nil } + +func convertHeaderMutations(spec *gwv1.HTTPHeaderFilter) []*envoycorev3.TypedExtensionConfig { + mutations := pluginutils.ConvertMutations(spec) + if len(mutations) == 0 { + return nil + } + + policy := &envoy_header_mutationv3.HeaderMutation{ + Mutations: mutations, + } + + return []*envoycorev3.TypedExtensionConfig{{ + Name: "envoy.http.early_header_mutation.header_mutation", + TypedConfig: utils.MustMessageToAny(policy), + }} +} diff --git a/internal/kgateway/extensions2/plugins/httplistenerpolicy/merge.go b/internal/kgateway/extensions2/plugins/httplistenerpolicy/merge.go index 9d369309da0..b7ac405d43a 100644 --- a/internal/kgateway/extensions2/plugins/httplistenerpolicy/merge.go +++ b/internal/kgateway/extensions2/plugins/httplistenerpolicy/merge.go @@ -32,6 +32,7 @@ func mergePolicies( mergePreserveHttp1HeaderCase, mergeAcceptHttp10, mergeDefaultHostForHttp10, + mergeEarlyHeaderMutation, } for _, mergeFunc := range mergeFuncs { @@ -227,3 +228,18 @@ func mergeHealthCheckPolicy( p1.healthCheckPolicy = p2.healthCheckPolicy mergeOrigins.SetOne("healthCheckPolicy", p2Ref, p2MergeOrigins) } + +func mergeEarlyHeaderMutation( + p1, p2 *httpListenerPolicy, + p2Ref *ir.AttachedPolicyRef, + p2MergeOrigins ir.MergeOrigins, + opts policy.MergeOptions, + mergeOrigins ir.MergeOrigins, +) { + if !policy.IsMergeable(p1.earlyHeaderMutationExtensions, p2.earlyHeaderMutationExtensions, opts) { + return + } + + p1.earlyHeaderMutationExtensions = slices.Clone(p2.earlyHeaderMutationExtensions) + mergeOrigins.SetOne("earlyHeaderMutationExtensions", p2Ref, p2MergeOrigins) +} diff --git a/internal/kgateway/extensions2/plugins/trafficpolicy/header_modifiers.go b/internal/kgateway/extensions2/plugins/trafficpolicy/header_modifiers.go index a11c92e2aa8..53686a08dbf 100644 --- a/internal/kgateway/extensions2/plugins/trafficpolicy/header_modifiers.go +++ b/internal/kgateway/extensions2/plugins/trafficpolicy/header_modifiers.go @@ -1,12 +1,11 @@ package trafficpolicy import ( - mutation_rulesv3 "github.com/envoyproxy/go-control-plane/envoy/config/common/mutation_rules/v3" - envoycorev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" header_mutationv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/header_mutation/v3" "google.golang.org/protobuf/proto" "github.com/kgateway-dev/kgateway/v2/api/v1alpha1" + "github.com/kgateway-dev/kgateway/v2/internal/kgateway/extensions2/pluginutils" "github.com/kgateway-dev/kgateway/v2/pkg/pluginsdk/ir" ) @@ -80,81 +79,8 @@ func buildHeaderModifiersPolicy( policy := &header_mutationv3.HeaderMutationPerRoute{} policy.Mutations = &header_mutationv3.Mutations{} - if spec.Request != nil { - for _, h := range spec.Request.Add { - policy.Mutations.RequestMutations = append(policy.Mutations.RequestMutations, &mutation_rulesv3.HeaderMutation{ - Action: &mutation_rulesv3.HeaderMutation_Append{ - Append: &envoycorev3.HeaderValueOption{ - Header: &envoycorev3.HeaderValue{ - Key: string(h.Name), - Value: h.Value, - }, - AppendAction: envoycorev3.HeaderValueOption_APPEND_IF_EXISTS_OR_ADD, - }, - }, - }) - } - - for _, h := range spec.Request.Set { - policy.Mutations.RequestMutations = append(policy.Mutations.RequestMutations, &mutation_rulesv3.HeaderMutation{ - Action: &mutation_rulesv3.HeaderMutation_Append{ - Append: &envoycorev3.HeaderValueOption{ - Header: &envoycorev3.HeaderValue{ - Key: string(h.Name), - Value: h.Value, - }, - AppendAction: envoycorev3.HeaderValueOption_OVERWRITE_IF_EXISTS_OR_ADD, - }, - }, - }) - } - - for _, h := range spec.Request.Remove { - policy.Mutations.RequestMutations = append(policy.Mutations.RequestMutations, &mutation_rulesv3.HeaderMutation{ - Action: &mutation_rulesv3.HeaderMutation_Remove{ - Remove: h, - }, - }) - } - } - - if spec.Response != nil { - for _, h := range spec.Response.Add { - policy.Mutations.ResponseMutations = append(policy.Mutations.ResponseMutations, &mutation_rulesv3.HeaderMutation{ - Action: &mutation_rulesv3.HeaderMutation_Append{ - Append: &envoycorev3.HeaderValueOption{ - Header: &envoycorev3.HeaderValue{ - Key: string(h.Name), - Value: h.Value, - }, - AppendAction: envoycorev3.HeaderValueOption_APPEND_IF_EXISTS_OR_ADD, - }, - }, - }) - } - - for _, h := range spec.Response.Set { - policy.Mutations.ResponseMutations = append(policy.Mutations.ResponseMutations, &mutation_rulesv3.HeaderMutation{ - Action: &mutation_rulesv3.HeaderMutation_Append{ - Append: &envoycorev3.HeaderValueOption{ - Header: &envoycorev3.HeaderValue{ - Key: string(h.Name), - Value: h.Value, - }, - AppendAction: envoycorev3.HeaderValueOption_OVERWRITE_IF_EXISTS_OR_ADD, - }, - }, - }) - } - - for _, h := range spec.Response.Remove { - policy.Mutations.ResponseMutations = append(policy.Mutations.ResponseMutations, &mutation_rulesv3.HeaderMutation{ - Action: &mutation_rulesv3.HeaderMutation_Remove{ - Remove: h, - }, - }) - } - } + policy.Mutations.RequestMutations = append(policy.Mutations.RequestMutations, pluginutils.ConvertMutations(spec.Request)...) + policy.Mutations.ResponseMutations = append(policy.Mutations.ResponseMutations, pluginutils.ConvertMutations(spec.Response)...) if len(policy.Mutations.RequestMutations) == 0 && len(policy.Mutations.ResponseMutations) == 0 { policy.Mutations = nil diff --git a/internal/kgateway/extensions2/pluginutils/headers.go b/internal/kgateway/extensions2/pluginutils/headers.go new file mode 100644 index 00000000000..2fbe520b0be --- /dev/null +++ b/internal/kgateway/extensions2/pluginutils/headers.go @@ -0,0 +1,56 @@ +package pluginutils + +import ( + mutation_rulesv3 "github.com/envoyproxy/go-control-plane/envoy/config/common/mutation_rules/v3" + envoycorev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + gwv1 "sigs.k8s.io/gateway-api/apis/v1" +) + +func ConvertMutations(filter *gwv1.HTTPHeaderFilter) []*mutation_rulesv3.HeaderMutation { + if filter == nil { + return nil + } + + if len(filter.Add) == 0 && len(filter.Set) == 0 && len(filter.Remove) == 0 { + return nil + } + + var mutations []*mutation_rulesv3.HeaderMutation + + for _, h := range filter.Add { + mutations = append(mutations, &mutation_rulesv3.HeaderMutation{ + Action: &mutation_rulesv3.HeaderMutation_Append{ + Append: &envoycorev3.HeaderValueOption{ + Header: &envoycorev3.HeaderValue{ + Key: string(h.Name), + Value: h.Value, + }, + AppendAction: envoycorev3.HeaderValueOption_APPEND_IF_EXISTS_OR_ADD, + }, + }, + }) + } + + for _, h := range filter.Set { + mutations = append(mutations, &mutation_rulesv3.HeaderMutation{ + Action: &mutation_rulesv3.HeaderMutation_Append{ + Append: &envoycorev3.HeaderValueOption{ + Header: &envoycorev3.HeaderValue{ + Key: string(h.Name), + Value: h.Value, + }, + AppendAction: envoycorev3.HeaderValueOption_OVERWRITE_IF_EXISTS_OR_ADD, + }, + }, + }) + } + + for _, h := range filter.Remove { + mutations = append(mutations, &mutation_rulesv3.HeaderMutation{ + Action: &mutation_rulesv3.HeaderMutation_Remove{ + Remove: h, + }, + }) + } + return mutations +} diff --git a/internal/kgateway/translator/gateway/gateway_translator_test.go b/internal/kgateway/translator/gateway/gateway_translator_test.go index bc11c10e55f..a3ab5fe9d39 100644 --- a/internal/kgateway/translator/gateway/gateway_translator_test.go +++ b/internal/kgateway/translator/gateway/gateway_translator_test.go @@ -943,6 +943,17 @@ func TestBasic(t *testing.T) { }) }) + t.Run("HTTPListenerPolicy with early header mutations (add/set/remove)", func(t *testing.T) { + test(t, translatorTestCase{ + inputFile: "httplistenerpolicy/early-header-mutation.yaml", + outputFile: "httplistenerpolicy/early-header-mutation.yaml", + gwNN: types.NamespacedName{ + Namespace: "default", + Name: "example-gateway", + }, + }) + }) + t.Run("Service with appProtocol=kubernetes.io/h2c", func(t *testing.T) { test(t, translatorTestCase{ inputFile: "backend-protocol/svc-h2c.yaml", diff --git a/internal/kgateway/translator/gateway/testutils/inputs/httplistenerpolicy/early-header-mutation.yaml b/internal/kgateway/translator/gateway/testutils/inputs/httplistenerpolicy/early-header-mutation.yaml new file mode 100644 index 00000000000..1ba5163c8e7 --- /dev/null +++ b/internal/kgateway/translator/gateway/testutils/inputs/httplistenerpolicy/early-header-mutation.yaml @@ -0,0 +1,57 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: example-gateway +spec: + gatewayClassName: example-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 +--- +apiVersion: v1 +kind: Service +metadata: + name: example-svc +spec: + selector: + app: test + ports: + - protocol: TCP + port: 80 + targetPort: 8080 +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: example-route +spec: + parentRefs: + - name: example-gateway + hostnames: + - "example.com" + rules: + - backendRefs: + - name: example-svc + port: 80 +--- +apiVersion: gateway.kgateway.dev/v1alpha1 +kind: HTTPListenerPolicy +metadata: + name: early-header-mutation +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: example-gateway + earlyRequestHeaderModifier: + add: + - name: "x-added-one" + value: "v1" + - name: "x-added-two" + value: "v2" + set: + - name: "x-set" + value: "s1" + remove: + - "x-remove" diff --git a/internal/kgateway/translator/gateway/testutils/outputs/httplistenerpolicy/early-header-mutation.yaml b/internal/kgateway/translator/gateway/testutils/outputs/httplistenerpolicy/early-header-mutation.yaml new file mode 100644 index 00000000000..778fed9c70e --- /dev/null +++ b/internal/kgateway/translator/gateway/testutils/outputs/httplistenerpolicy/early-header-mutation.yaml @@ -0,0 +1,169 @@ +Clusters: +- connectTimeout: 5s + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + ignoreHealthOnHostRemoval: true + metadata: {} + name: kube_default_example-svc_80 + type: EDS +- connectTimeout: 5s + metadata: {} + name: test-backend-plugin_default_example-svc_80 +Listeners: +- address: + socketAddress: + address: '::' + ipv4Compat: true + portValue: 80 + filterChains: + - filters: + - name: envoy.filters.network.http_connection_manager + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + 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: x-added-one + value: v1 + - append: + header: + key: x-added-two + value: v2 + - append: + appendAction: OVERWRITE_IF_EXISTS_OR_ADD + header: + key: x-set + value: s1 + - remove: x-remove + httpFilters: + - name: envoy.filters.http.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + mergeSlashes: true + normalizePath: true + rds: + configSource: + ads: {} + resourceApiVersion: V3 + routeConfigName: listener~80 + statPrefix: http + useRemoteAddress: true + name: listener~80 + metadata: + filterMetadata: + merge.HTTPListenerPolicy.gateway.kgateway.dev: + earlyHeaderMutationExtensions: + - gateway.kgateway.dev/HTTPListenerPolicy/default/early-header-mutation + name: listener~80 +Routes: +- ignorePortInHostMatching: true + metadata: + filterMetadata: + merge.HTTPListenerPolicy.gateway.kgateway.dev: + earlyHeaderMutationExtensions: + - gateway.kgateway.dev/HTTPListenerPolicy/default/early-header-mutation + name: listener~80 + virtualHosts: + - domains: + - example.com + name: listener~80~example_com + routes: + - match: + prefix: / + name: listener~80~example_com-route-0-httproute-example-route-default-0-0-matcher-0 + route: + cluster: kube_default_example-svc_80 + clusterNotFoundResponseCode: INTERNAL_SERVER_ERROR +Statuses: + gateways: + default/example-gateway: + conditions: + - lastTransitionTime: null + message: "" + reason: ListenerSetsNotAllowed + status: Unknown + type: AttachedListenerSets + - lastTransitionTime: null + message: Successfully accepted Gateway + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Successfully programmed Gateway + reason: Programmed + status: "True" + type: Programmed + listeners: + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Successfully accepted Listener + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Successfully verified that Listener has no conflicts + reason: NoConflicts + status: "False" + type: Conflicted + - lastTransitionTime: null + message: Successfully resolved all references + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + - lastTransitionTime: null + message: Successfully programmed Listener + reason: Programmed + status: "True" + type: Programmed + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute + httpRoutes: + default/example-route: + parents: + - conditions: + - lastTransitionTime: null + message: Successfully accepted Route + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Successfully resolved all references + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: kgateway + parentRef: + group: "" + kind: "" + name: example-gateway + policies: + HTTPListenerPolicy/default/early-header-mutation: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: example-gateway + namespace: default + conditions: + - lastTransitionTime: null + message: Policy accepted + reason: Valid + status: "True" + type: Accepted + - lastTransitionTime: null + message: Attached to all targets + reason: Attached + status: "True" + type: Attached + controllerName: kgateway.dev/kgateway diff --git a/test/e2e/features/http_listener_policy/suite.go b/test/e2e/features/http_listener_policy/suite.go index 69324548371..2217527571f 100644 --- a/test/e2e/features/http_listener_policy/suite.go +++ b/test/e2e/features/http_listener_policy/suite.go @@ -64,6 +64,7 @@ func (s *testingSuite) SetupSuite() { "TestPreserveHttp1HeaderCase": {gatewayManifest, preserveHttp1HeaderCaseManifest}, "TestAccessLogEmittedToStdout": {gatewayManifest, httpRouteManifest, accessLogManifest}, "TestHttpListenerPolicyClearStaleStatus": {gatewayManifest, httpRouteManifest, serverHeaderManifest}, + "TestEarlyRequestHeaderModifier": {gatewayManifest, earlyHeaderMutationManifest}, } } @@ -328,3 +329,20 @@ func (s *testingSuite) assertAncestorStatuses(ancestorName string, expectedContr } }, currentTimeout, pollingInterval).Should(gomega.Succeed()) } + +func (s *testingSuite) TestEarlyRequestHeaderModifier() { + // Route matches only when a specific header is present. The policy adds it early. + s.testInstallation.Assertions.AssertEventualCurlResponse( + s.ctx, + testdefaults.CurlPodExecOpt, + []curl.Option{ + curl.WithHost(kubeutils.ServiceFQDN(proxyService.ObjectMeta)), + curl.WithHostHeader("example.com"), + // No manual header provided; listener policy adds it early so route matches + }, + &matchers.HttpResponse{ + StatusCode: http.StatusOK, + Body: gomega.ContainSubstring("Welcome to nginx!"), + }, + ) +} diff --git a/test/e2e/features/http_listener_policy/testdata/http-listener-policy-early-header-route-match.yaml b/test/e2e/features/http_listener_policy/testdata/http-listener-policy-early-header-route-match.yaml new file mode 100644 index 00000000000..9d644472111 --- /dev/null +++ b/test/e2e/features/http_listener_policy/testdata/http-listener-policy-early-header-route-match.yaml @@ -0,0 +1,40 @@ +apiVersion: gateway.kgateway.dev/v1alpha1 +kind: HTTPListenerPolicy +metadata: + name: early-header-route-match + namespace: default +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: gw + earlyRequestHeaderModifier: + add: + - name: "x-route-match" + value: "allow" +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: route-requires-header + namespace: default +spec: + hostnames: + - example.com + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: gw + namespace: default + sectionName: http + rules: + - matches: + - headers: + - name: x-route-match + value: allow + path: + type: PathPrefix + value: / + backendRefs: + - name: example-svc + port: 8080 diff --git a/test/e2e/features/http_listener_policy/types.go b/test/e2e/features/http_listener_policy/types.go index 886afdefa63..7e893318af8 100644 --- a/test/e2e/features/http_listener_policy/types.go +++ b/test/e2e/features/http_listener_policy/types.go @@ -21,6 +21,7 @@ var ( preserveHttp1HeaderCaseManifest = filepath.Join(fsutils.MustGetThisDir(), "testdata", "preserve-http1-header-case.yaml") accessLogManifest = filepath.Join(fsutils.MustGetThisDir(), "testdata", "http-listener-policy-access-log.yaml") httpListenerPolicyMissingTargetManifest = filepath.Join(fsutils.MustGetThisDir(), "testdata", "http-listener-policy-missing-target.yaml") + earlyHeaderMutationManifest = filepath.Join(fsutils.MustGetThisDir(), "testdata", "http-listener-policy-early-header-route-match.yaml") // When we apply the setup file, we expect resources to be created with this metadata proxyObjectMeta = metav1.ObjectMeta{