diff --git a/api/v1alpha1/httproutefilter_types.go b/api/v1alpha1/httproutefilter_types.go
index b8fe13f5296d..80eba4caa56a 100644
--- a/api/v1alpha1/httproutefilter_types.go
+++ b/api/v1alpha1/httproutefilter_types.go
@@ -56,14 +56,18 @@ const (
type ReplaceRegexMatch struct {
// Pattern matches a regular expression against the value of the HTTP Path.The regex string must
// adhere to the syntax documented in https://github.com/google/re2/wiki/Syntax.
+ // +kubebuilder:validation:MinLength=1
Pattern string `json:"pattern"`
// Substitution is an expression that replaces the matched portion.The expression may include numbered
// capture groups that adhere to syntax documented in https://github.com/google/re2/wiki/Syntax.
+ // +kubebuilder:validation:MinLength=1
Substitution string `json:"substitution"`
}
+// +kubebuilder:validation:XValidation:rule="self.type == 'ReplaceRegexMatch' ? has(self.replaceRegexMatch) : !has(self.replaceRegexMatch)",message="If HTTPPathModifier type is ReplaceRegexMatch, replaceRegexMatch field needs to be set."
type HTTPPathModifier struct {
- // +kubebuilder:validation:Enum=RegexHTTPPathModifier
+ // +kubebuilder:validation:Enum=ReplaceRegexMatch
+ // +kubebuilder:validation:Required
Type HTTPPathModifierType `json:"type"`
// ReplaceRegexMatch defines a path regex rewrite. The path portions matched by the regex pattern are replaced by the defined substitution.
// https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#envoy-v3-api-field-config-route-v3-routeaction-regex-rewrite
@@ -84,6 +88,7 @@ type HTTPPathModifier struct {
// pattern: (?i)/xxx/
// substitution: /yyy/
// Would transform path /aaa/XxX/bbb into /aaa/yyy/bbb (case-insensitive).
+ // +optional
ReplaceRegexMatch *ReplaceRegexMatch `json:"replaceRegexMatch,omitempty"`
}
diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_httproutefilters.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_httproutefilters.yaml
index 2bba4d20ff15..54a49c2b0ebf 100644
--- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_httproutefilters.yaml
+++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_httproutefilters.yaml
@@ -82,11 +82,13 @@ spec:
description: |-
Pattern matches a regular expression against the value of the HTTP Path.The regex string must
adhere to the syntax documented in https://github.com/google/re2/wiki/Syntax.
+ minLength: 1
type: string
substitution:
description: |-
Substitution is an expression that replaces the matched portion.The expression may include numbered
capture groups that adhere to syntax documented in https://github.com/google/re2/wiki/Syntax.
+ minLength: 1
type: string
required:
- pattern
@@ -96,11 +98,16 @@ spec:
description: HTTPPathModifierType defines the type of path
redirect or rewrite.
enum:
- - RegexHTTPPathModifier
+ - ReplaceRegexMatch
type: string
required:
- type
type: object
+ x-kubernetes-validations:
+ - message: If HTTPPathModifier type is ReplaceRegexMatch, replaceRegexMatch
+ field needs to be set.
+ rule: 'self.type == ''ReplaceRegexMatch'' ? has(self.replaceRegexMatch)
+ : !has(self.replaceRegexMatch)'
type: object
type: object
required:
diff --git a/charts/gateway-helm/templates/_rbac.tpl b/charts/gateway-helm/templates/_rbac.tpl
index fb9304e7d89f..27e90061b0ce 100644
--- a/charts/gateway-helm/templates/_rbac.tpl
+++ b/charts/gateway-helm/templates/_rbac.tpl
@@ -71,6 +71,7 @@ resources:
- securitypolicies
- envoyextensionpolicies
- backends
+- httproutefilters
verbs:
- get
- list
diff --git a/internal/gatewayapi/filters.go b/internal/gatewayapi/filters.go
index de6be3528158..a70b0c1b8343 100644
--- a/internal/gatewayapi/filters.go
+++ b/internal/gatewayapi/filters.go
@@ -7,11 +7,13 @@ package gatewayapi
import (
"fmt"
+ "regexp"
"strings"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
+ egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
"github.com/envoyproxy/gateway/internal/gatewayapi/resource"
"github.com/envoyproxy/gateway/internal/gatewayapi/status"
"github.com/envoyproxy/gateway/internal/ir"
@@ -150,16 +152,20 @@ func (t *Translator) processURLRewriteFilter(
filterContext *HTTPFiltersContext,
) {
if filterContext.URLRewrite != nil {
- routeStatus := GetRouteStatus(filterContext.Route)
- status.SetRouteStatusCondition(routeStatus,
- filterContext.ParentRef.routeParentStatusIdx,
- filterContext.Route.GetGeneration(),
- gwapiv1.RouteConditionAccepted,
- metav1.ConditionFalse,
- gwapiv1.RouteReasonUnsupportedValue,
- "Cannot configure multiple urlRewrite filters for a single HTTPRouteRule",
- )
- return
+ if filterContext.URLRewrite.Hostname != nil ||
+ filterContext.URLRewrite.Path.FullReplace != nil ||
+ filterContext.URLRewrite.Path.PrefixMatchReplace != nil {
+ routeStatus := GetRouteStatus(filterContext.Route)
+ status.SetRouteStatusCondition(routeStatus,
+ filterContext.ParentRef.routeParentStatusIdx,
+ filterContext.Route.GetGeneration(),
+ gwapiv1.RouteConditionAccepted,
+ metav1.ConditionFalse,
+ gwapiv1.RouteReasonUnsupportedValue,
+ "Cannot configure multiple urlRewrite filters for a single HTTPRouteRule",
+ )
+ return
+ }
}
if rewrite == nil {
@@ -215,8 +221,10 @@ func (t *Translator) processURLRewriteFilter(
return
}
if rewrite.Path.ReplaceFullPath != nil {
- newURLRewrite.Path = &ir.HTTPPathModifier{
- FullReplace: rewrite.Path.ReplaceFullPath,
+ newURLRewrite.Path = &ir.ExtendedHTTPPathModifier{
+ HTTPPathModifier: ir.HTTPPathModifier{
+ FullReplace: rewrite.Path.ReplaceFullPath,
+ },
}
}
case gwapiv1.PrefixMatchHTTPPathModifier:
@@ -247,8 +255,10 @@ func (t *Translator) processURLRewriteFilter(
return
}
if rewrite.Path.ReplacePrefixMatch != nil {
- newURLRewrite.Path = &ir.HTTPPathModifier{
- PrefixMatchReplace: rewrite.Path.ReplacePrefixMatch,
+ newURLRewrite.Path = &ir.ExtendedHTTPPathModifier{
+ HTTPPathModifier: ir.HTTPPathModifier{
+ PrefixMatchReplace: rewrite.Path.ReplacePrefixMatch,
+ },
}
}
default:
@@ -738,6 +748,85 @@ func (t *Translator) processExtensionRefHTTPFilter(extFilter *gwapiv1.LocalObjec
}
filterNs := filterContext.Route.GetNamespace()
+
+ if string(extFilter.Kind) == egv1a1.KindHTTPRouteFilter {
+ for _, hrf := range resources.HTTPRouteFilters {
+ if hrf.Namespace == filterNs && hrf.Name == string(extFilter.Name) &&
+ hrf.Spec.URLRewrite.Path.Type == egv1a1.RegexHTTPPathModifier {
+
+ if hrf.Spec.URLRewrite.Path.ReplaceRegexMatch == nil ||
+ hrf.Spec.URLRewrite.Path.ReplaceRegexMatch.Pattern == "" ||
+ hrf.Spec.URLRewrite.Path.ReplaceRegexMatch.Substitution == "" {
+ errMsg := "ReplaceRegexMatch Pattern and Substitution must be set when rewrite path type is \"ReplaceRegexMatch\""
+ routeStatus := GetRouteStatus(filterContext.Route)
+ status.SetRouteStatusCondition(routeStatus,
+ filterContext.ParentRef.routeParentStatusIdx,
+ filterContext.Route.GetGeneration(),
+ gwapiv1.RouteConditionAccepted,
+ metav1.ConditionFalse,
+ gwapiv1.RouteReasonUnsupportedValue,
+ errMsg,
+ )
+ return
+ } else if _, err := regexp.Compile(hrf.Spec.URLRewrite.Path.ReplaceRegexMatch.Pattern); err != nil {
+ // Avoid envoy NACKs due to invalid regex.
+ // Golang's regexp is almost identical to RE2: https://pkg.go.dev/regexp/syntax
+ errMsg := "ReplaceRegexMatch must be a valid RE2 regular expression"
+ routeStatus := GetRouteStatus(filterContext.Route)
+ status.SetRouteStatusCondition(routeStatus,
+ filterContext.ParentRef.routeParentStatusIdx,
+ filterContext.Route.GetGeneration(),
+ gwapiv1.RouteConditionAccepted,
+ metav1.ConditionFalse,
+ gwapiv1.RouteReasonUnsupportedValue,
+ errMsg,
+ )
+ return
+ }
+
+ rmr := &ir.RegexMatchReplace{
+ Pattern: hrf.Spec.URLRewrite.Path.ReplaceRegexMatch.Pattern,
+ Substitution: hrf.Spec.URLRewrite.Path.ReplaceRegexMatch.Substitution,
+ }
+
+ if filterContext.HTTPFilterIR.URLRewrite != nil {
+ // If path IR is already set - check for a conflict
+ if filterContext.HTTPFilterIR.URLRewrite.Path != nil {
+ path := filterContext.HTTPFilterIR.URLRewrite.Path
+ if path.RegexMatchReplace != nil || path.PrefixMatchReplace != nil || path.FullReplace != nil {
+ routeStatus := GetRouteStatus(filterContext.Route)
+ status.SetRouteStatusCondition(routeStatus,
+ filterContext.ParentRef.routeParentStatusIdx,
+ filterContext.Route.GetGeneration(),
+ gwapiv1.RouteConditionAccepted,
+ metav1.ConditionFalse,
+ gwapiv1.RouteReasonUnsupportedValue,
+ "Cannot configure multiple urlRewrite filters for a single HTTPRouteRule",
+ )
+ return
+ }
+ } else { // no path
+ filterContext.HTTPFilterIR.URLRewrite.Path = &ir.ExtendedHTTPPathModifier{
+ RegexMatchReplace: rmr,
+ }
+ return
+ }
+ } else { // no url rewrite
+ filterContext.HTTPFilterIR.URLRewrite = &ir.URLRewrite{
+ Path: &ir.ExtendedHTTPPathModifier{
+ RegexMatchReplace: rmr,
+ },
+ }
+ return
+ }
+ }
+ }
+ errMsg := fmt.Sprintf("Unable to translate HTTPRouteFilter: %s/%s", filterNs,
+ extFilter.Name)
+ t.processUnresolvedHTTPFilter(errMsg, filterContext)
+ return
+ }
+
// This list of resources will be empty unless an extension is loaded (and introduces resources)
for _, res := range resources.ExtensionRefFilters {
if res.GetKind() == string(extFilter.Kind) && res.GetName() == string(extFilter.Name) && res.GetNamespace() == filterNs {
diff --git a/internal/gatewayapi/helpers.go b/internal/gatewayapi/helpers.go
index 885077f5cc36..9c5626d75249 100644
--- a/internal/gatewayapi/helpers.go
+++ b/internal/gatewayapi/helpers.go
@@ -179,6 +179,9 @@ func ValidateHTTPRouteFilter(filter *gwapiv1.HTTPRouteFilter, extGKs ...schema.G
switch {
case filter.ExtensionRef == nil:
return errors.New("extensionRef field must be specified for an extended filter")
+ case string(filter.ExtensionRef.Group) == egv1a1.GroupVersion.Group &&
+ string(filter.ExtensionRef.Kind) == egv1a1.KindHTTPRouteFilter:
+ return nil
default:
for _, gk := range extGKs {
if filter.ExtensionRef.Group == gwapiv1.Group(gk.Group) &&
diff --git a/internal/gatewayapi/resource/load.go b/internal/gatewayapi/resource/load.go
index cacc3ebe61a0..317ad93418cf 100644
--- a/internal/gatewayapi/resource/load.go
+++ b/internal/gatewayapi/resource/load.go
@@ -265,6 +265,20 @@ func kubernetesYAMLToResources(str string, addMissingResources bool) (*Resources
Spec: typedSpec.(egv1a1.SecurityPolicySpec),
}
resources.SecurityPolicies = append(resources.SecurityPolicies, securityPolicy)
+ case KindHTTPRouteFilter:
+ typedSpec := spec.Interface()
+ httpRouteFilter := &egv1a1.HTTPRouteFilter{
+ TypeMeta: metav1.TypeMeta{
+ Kind: KindHTTPRouteFilter,
+ APIVersion: egv1a1.GroupVersion.String(),
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: namespace,
+ Name: name,
+ },
+ Spec: typedSpec.(egv1a1.HTTPRouteFilterSpec),
+ }
+ resources.HTTPRouteFilters = append(resources.HTTPRouteFilters, httpRouteFilter)
}
}
diff --git a/internal/gatewayapi/resource/resource.go b/internal/gatewayapi/resource/resource.go
index a5d9e6fffb57..97468511fa83 100644
--- a/internal/gatewayapi/resource/resource.go
+++ b/internal/gatewayapi/resource/resource.go
@@ -63,6 +63,7 @@ type Resources struct {
EnvoyExtensionPolicies []*egv1a1.EnvoyExtensionPolicy `json:"envoyExtensionPolicies,omitempty" yaml:"envoyExtensionPolicies,omitempty"`
ExtensionServerPolicies []unstructured.Unstructured `json:"extensionServerPolicies,omitempty" yaml:"extensionServerPolicies,omitempty"`
Backends []*egv1a1.Backend `json:"backends,omitempty" yaml:"backends,omitempty"`
+ HTTPRouteFilters []*egv1a1.HTTPRouteFilter `json:"httpFilters,omitempty" yaml:"httpFilters,omitempty"`
}
func NewResources() *Resources {
@@ -86,6 +87,7 @@ func NewResources() *Resources {
EnvoyExtensionPolicies: []*egv1a1.EnvoyExtensionPolicy{},
ExtensionServerPolicies: []unstructured.Unstructured{},
Backends: []*egv1a1.Backend{},
+ HTTPRouteFilters: []*egv1a1.HTTPRouteFilter{},
}
}
diff --git a/internal/gatewayapi/resource/supported_kind.go b/internal/gatewayapi/resource/supported_kind.go
index 5c2c21954a96..e9d76e66c3d8 100644
--- a/internal/gatewayapi/resource/supported_kind.go
+++ b/internal/gatewayapi/resource/supported_kind.go
@@ -26,4 +26,5 @@ const (
KindService = "Service"
KindServiceImport = "ServiceImport"
KindSecret = "Secret"
+ KindHTTPRouteFilter = "HTTPRouteFilter"
)
diff --git a/internal/gatewayapi/resource/zz_generated.deepcopy.go b/internal/gatewayapi/resource/zz_generated.deepcopy.go
index 61cf9dfb46e0..06925b1467d6 100644
--- a/internal/gatewayapi/resource/zz_generated.deepcopy.go
+++ b/internal/gatewayapi/resource/zz_generated.deepcopy.go
@@ -279,6 +279,17 @@ func (in *Resources) DeepCopyInto(out *Resources) {
}
}
}
+ if in.HTTPRouteFilters != nil {
+ in, out := &in.HTTPRouteFilters, &out.HTTPRouteFilters
+ *out = make([]*v1alpha1.HTTPRouteFilter, len(*in))
+ for i := range *in {
+ if (*in)[i] != nil {
+ in, out := &(*in)[i], &(*out)[i]
+ *out = new(v1alpha1.HTTPRouteFilter)
+ (*in).DeepCopyInto(*out)
+ }
+ }
+ }
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Resources.
diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-full-path-replace-http.out.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-full-path-replace-http.out.yaml
index 3d8c69a6178c..01a20d2b5d6d 100644
--- a/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-full-path-replace-http.out.yaml
+++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-full-path-replace-http.out.yaml
@@ -146,3 +146,4 @@ xdsIR:
path:
fullReplace: /rewrite
prefixMatchReplace: null
+ regexMatchReplace: null
diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-hostname-prefix-replace.out.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-hostname-prefix-replace.out.yaml
index 1577ab27e64b..b796e6993a7f 100644
--- a/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-hostname-prefix-replace.out.yaml
+++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-hostname-prefix-replace.out.yaml
@@ -148,3 +148,4 @@ xdsIR:
path:
fullReplace: null
prefixMatchReplace: /rewrite
+ regexMatchReplace: null
diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-prefix-replace-http.out.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-prefix-replace-http.out.yaml
index dd20383d2ea2..4d85479537d1 100644
--- a/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-prefix-replace-http.out.yaml
+++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-prefix-replace-http.out.yaml
@@ -146,3 +146,4 @@ xdsIR:
path:
fullReplace: null
prefixMatchReplace: /rewrite
+ regexMatchReplace: null
diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-regex-match-replace-http.in.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-regex-match-replace-http.in.yaml
new file mode 100644
index 000000000000..374cebe6f177
--- /dev/null
+++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-regex-match-replace-http.in.yaml
@@ -0,0 +1,111 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: Gateway
+ metadata:
+ namespace: envoy-gateway
+ name: gateway-1
+ spec:
+ gatewayClassName: envoy-gateway-class
+ listeners:
+ - name: http
+ protocol: HTTP
+ port: 80
+ hostname: "*.envoyproxy.io"
+ allowedRoutes:
+ namespaces:
+ from: All
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: HTTPRoute
+ metadata:
+ namespace: default
+ name: httproute-1
+ spec:
+ hostnames:
+ - gateway.envoyproxy.io
+ parentRefs:
+ - namespace: envoy-gateway
+ name: gateway-1
+ sectionName: http
+ rules:
+ - matches:
+ - path:
+ value: "/valid"
+ backendRefs:
+ - name: service-1
+ port: 8080
+ filters:
+ - type: ExtensionRef
+ extensionRef:
+ group: gateway.envoyproxy.io
+ kind: HTTPRouteFilter
+ name: valid
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: HTTPRoute
+ metadata:
+ namespace: default
+ name: httproute-2
+ spec:
+ hostnames:
+ - gateway.envoyproxy.io
+ parentRefs:
+ - namespace: envoy-gateway
+ name: gateway-1
+ sectionName: http
+ rules:
+ - matches:
+ - path:
+ value: "/host-and-regex-path"
+ backendRefs:
+ - name: service-1
+ port: 8080
+ filters:
+ - type: URLRewrite
+ urlRewrite:
+ hostname: "rewrite.com"
+ - type: ExtensionRef
+ extensionRef:
+ group: gateway.envoyproxy.io
+ kind: HTTPRouteFilter
+ name: valid
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: HTTPRoute
+ metadata:
+ namespace: default
+ name: httproute-3
+ spec:
+ hostnames:
+ - gateway.envoyproxy.io
+ parentRefs:
+ - namespace: envoy-gateway
+ name: gateway-1
+ sectionName: http
+ rules:
+ - matches:
+ - path:
+ value: "/regex-path-and-host"
+ backendRefs:
+ - name: service-1
+ port: 8080
+ filters:
+ - type: ExtensionRef
+ extensionRef:
+ group: gateway.envoyproxy.io
+ kind: HTTPRouteFilter
+ name: valid
+ - type: URLRewrite
+ urlRewrite:
+ hostname: "rewrite.com"
+httpFilters:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: HTTPRouteFilter
+ metadata:
+ name: valid
+ namespace: default
+ spec:
+ urlRewrite:
+ path:
+ type: ReplaceRegexMatch
+ replaceRegexMatch:
+ pattern: '.*'
+ substitution: foo
diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-regex-match-replace-http.out.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-regex-match-replace-http.out.yaml
new file mode 100644
index 000000000000..5ba0e22ad87b
--- /dev/null
+++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-regex-match-replace-http.out.yaml
@@ -0,0 +1,295 @@
+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: All
+ hostname: '*.envoyproxy.io'
+ name: http
+ port: 80
+ protocol: HTTP
+ status:
+ listeners:
+ - attachedRoutes: 3
+ 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
+ supportedKinds:
+ - group: gateway.networking.k8s.io
+ kind: HTTPRoute
+ - group: gateway.networking.k8s.io
+ kind: GRPCRoute
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: HTTPRoute
+ metadata:
+ creationTimestamp: null
+ name: httproute-1
+ namespace: default
+ spec:
+ hostnames:
+ - gateway.envoyproxy.io
+ parentRefs:
+ - name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http
+ rules:
+ - backendRefs:
+ - name: service-1
+ port: 8080
+ filters:
+ - extensionRef:
+ group: gateway.envoyproxy.io
+ kind: HTTPRouteFilter
+ name: valid
+ type: ExtensionRef
+ matches:
+ - path:
+ value: /valid
+ status:
+ parents:
+ - conditions:
+ - lastTransitionTime: null
+ message: Route is accepted
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ - lastTransitionTime: null
+ message: Resolved all the Object references for the Route
+ reason: ResolvedRefs
+ status: "True"
+ type: ResolvedRefs
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+ parentRef:
+ name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: HTTPRoute
+ metadata:
+ creationTimestamp: null
+ name: httproute-2
+ namespace: default
+ spec:
+ hostnames:
+ - gateway.envoyproxy.io
+ parentRefs:
+ - name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http
+ rules:
+ - backendRefs:
+ - name: service-1
+ port: 8080
+ filters:
+ - type: URLRewrite
+ urlRewrite:
+ hostname: rewrite.com
+ - extensionRef:
+ group: gateway.envoyproxy.io
+ kind: HTTPRouteFilter
+ name: valid
+ type: ExtensionRef
+ matches:
+ - path:
+ value: /host-and-regex-path
+ status:
+ parents:
+ - conditions:
+ - lastTransitionTime: null
+ message: Route is accepted
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ - lastTransitionTime: null
+ message: Resolved all the Object references for the Route
+ reason: ResolvedRefs
+ status: "True"
+ type: ResolvedRefs
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+ parentRef:
+ name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: HTTPRoute
+ metadata:
+ creationTimestamp: null
+ name: httproute-3
+ namespace: default
+ spec:
+ hostnames:
+ - gateway.envoyproxy.io
+ parentRefs:
+ - name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http
+ rules:
+ - backendRefs:
+ - name: service-1
+ port: 8080
+ filters:
+ - extensionRef:
+ group: gateway.envoyproxy.io
+ kind: HTTPRouteFilter
+ name: valid
+ type: ExtensionRef
+ - type: URLRewrite
+ urlRewrite:
+ hostname: rewrite.com
+ matches:
+ - path:
+ value: /regex-path-and-host
+ status:
+ parents:
+ - conditions:
+ - lastTransitionTime: null
+ message: Route is accepted
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ - lastTransitionTime: null
+ message: Resolved all the Object references for the Route
+ reason: ResolvedRefs
+ status: "True"
+ type: ResolvedRefs
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+ parentRef:
+ name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http
+infraIR:
+ envoy-gateway/gateway-1:
+ proxy:
+ listeners:
+ - address: null
+ name: envoy-gateway/gateway-1/http
+ 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
+ hostnames:
+ - '*.envoyproxy.io'
+ isHTTP2: false
+ metadata:
+ kind: Gateway
+ name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http
+ name: envoy-gateway/gateway-1/http
+ path:
+ escapedSlashesAction: UnescapeAndRedirect
+ mergeSlashes: true
+ port: 10080
+ routes:
+ - destination:
+ name: httproute/default/httproute-2/rule/0
+ settings:
+ - addressType: IP
+ endpoints:
+ - host: 7.7.7.7
+ port: 8080
+ protocol: HTTP
+ weight: 1
+ hostname: gateway.envoyproxy.io
+ isHTTP2: false
+ metadata:
+ kind: HTTPRoute
+ name: httproute-2
+ namespace: default
+ name: httproute/default/httproute-2/rule/0/match/0/gateway_envoyproxy_io
+ pathMatch:
+ distinct: false
+ name: ""
+ prefix: /host-and-regex-path
+ urlRewrite:
+ hostname: rewrite.com
+ path:
+ fullReplace: null
+ prefixMatchReplace: null
+ regexMatchReplace:
+ pattern: .*
+ substitution: foo
+ - destination:
+ name: httproute/default/httproute-3/rule/0
+ settings:
+ - addressType: IP
+ endpoints:
+ - host: 7.7.7.7
+ port: 8080
+ protocol: HTTP
+ weight: 1
+ hostname: gateway.envoyproxy.io
+ isHTTP2: false
+ metadata:
+ kind: HTTPRoute
+ name: httproute-3
+ namespace: default
+ name: httproute/default/httproute-3/rule/0/match/0/gateway_envoyproxy_io
+ pathMatch:
+ distinct: false
+ name: ""
+ prefix: /regex-path-and-host
+ urlRewrite:
+ hostname: rewrite.com
+ - destination:
+ name: httproute/default/httproute-1/rule/0
+ settings:
+ - addressType: IP
+ endpoints:
+ - host: 7.7.7.7
+ port: 8080
+ protocol: HTTP
+ weight: 1
+ hostname: gateway.envoyproxy.io
+ isHTTP2: false
+ metadata:
+ kind: HTTPRoute
+ name: httproute-1
+ namespace: default
+ name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io
+ pathMatch:
+ distinct: false
+ name: ""
+ prefix: /valid
+ urlRewrite:
+ path:
+ fullReplace: null
+ prefixMatchReplace: null
+ regexMatchReplace:
+ pattern: .*
+ substitution: foo
diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-regex-match-replace-invalid.in.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-regex-match-replace-invalid.in.yaml
new file mode 100644
index 000000000000..fc2a037755a8
--- /dev/null
+++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-regex-match-replace-invalid.in.yaml
@@ -0,0 +1,266 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: Gateway
+ metadata:
+ namespace: envoy-gateway
+ name: gateway-1
+ spec:
+ gatewayClassName: envoy-gateway-class
+ listeners:
+ - name: http
+ protocol: HTTP
+ port: 80
+ hostname: "*.envoyproxy.io"
+ allowedRoutes:
+ namespaces:
+ from: All
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: HTTPRoute
+ metadata:
+ namespace: default
+ name: httproute-invalid-pattern
+ spec:
+ hostnames:
+ - gateway.envoyproxy.io
+ parentRefs:
+ - namespace: envoy-gateway
+ name: gateway-1
+ sectionName: http
+ rules:
+ - matches:
+ - path:
+ value: "/invalid-pattern"
+ backendRefs:
+ - name: service-1
+ port: 8080
+ filters:
+ - type: ExtensionRef
+ extensionRef:
+ group: gateway.envoyproxy.io
+ kind: HTTPRouteFilter
+ name: invalid-pattern
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: HTTPRoute
+ metadata:
+ namespace: default
+ name: httproute-missing-pattern
+ spec:
+ hostnames:
+ - gateway.envoyproxy.io
+ parentRefs:
+ - namespace: envoy-gateway
+ name: gateway-1
+ sectionName: http
+ rules:
+ - matches:
+ - path:
+ value: "/missing-pattern"
+ backendRefs:
+ - name: service-1
+ port: 8080
+ filters:
+ - type: ExtensionRef
+ extensionRef:
+ group: gateway.envoyproxy.io
+ kind: HTTPRouteFilter
+ name: missing-pattern
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: HTTPRoute
+ metadata:
+ namespace: default
+ name: httproute-missing-substitution
+ spec:
+ hostnames:
+ - gateway.envoyproxy.io
+ parentRefs:
+ - namespace: envoy-gateway
+ name: gateway-1
+ sectionName: http
+ rules:
+ - matches:
+ - path:
+ value: "/missing-substitution"
+ backendRefs:
+ - name: service-1
+ port: 8080
+ filters:
+ - type: ExtensionRef
+ extensionRef:
+ group: gateway.envoyproxy.io
+ kind: HTTPRouteFilter
+ name: missing-substitution
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: HTTPRoute
+ metadata:
+ namespace: default
+ name: httproute-multiple-path-rewrites-1
+ spec:
+ hostnames:
+ - gateway.envoyproxy.io
+ parentRefs:
+ - namespace: envoy-gateway
+ name: gateway-1
+ sectionName: http
+ rules:
+ - matches:
+ - path:
+ value: "/ext-first"
+ backendRefs:
+ - name: service-1
+ port: 8080
+ filters:
+ - type: ExtensionRef
+ extensionRef:
+ group: gateway.envoyproxy.io
+ kind: HTTPRouteFilter
+ name: valid
+ - type: URLRewrite
+ urlRewrite:
+ path:
+ type: ReplacePrefixMatch
+ replacePrefixMatch: /rewrite
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: HTTPRoute
+ metadata:
+ namespace: default
+ name: httproute-multiple-path-rewrites-2
+ spec:
+ hostnames:
+ - gateway.envoyproxy.io
+ parentRefs:
+ - namespace: envoy-gateway
+ name: gateway-1
+ sectionName: http
+ rules:
+ - matches:
+ - path:
+ value: "/inline-first"
+ backendRefs:
+ - name: service-1
+ port: 8080
+ filters:
+ - type: URLRewrite
+ urlRewrite:
+ path:
+ type: ReplacePrefixMatch
+ replacePrefixMatch: /rewrite
+ - type: ExtensionRef
+ extensionRef:
+ group: gateway.envoyproxy.io
+ kind: HTTPRouteFilter
+ name: valid
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: HTTPRoute
+ metadata:
+ namespace: default
+ name: httproute-multiple-regex-path-rewrites
+ spec:
+ hostnames:
+ - gateway.envoyproxy.io
+ parentRefs:
+ - namespace: envoy-gateway
+ name: gateway-1
+ sectionName: http
+ rules:
+ - matches:
+ - path:
+ value: "/two-regex"
+ backendRefs:
+ - name: service-1
+ port: 8080
+ filters:
+ - type: ExtensionRef
+ extensionRef:
+ group: gateway.envoyproxy.io
+ kind: HTTPRouteFilter
+ name: valid
+ - type: ExtensionRef
+ extensionRef:
+ group: gateway.envoyproxy.io
+ kind: HTTPRouteFilter
+ name: valid-2
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: HTTPRoute
+ metadata:
+ namespace: default
+ name: httproute-not-found
+ spec:
+ hostnames:
+ - gateway.envoyproxy.io
+ parentRefs:
+ - namespace: envoy-gateway
+ name: gateway-1
+ sectionName: http
+ rules:
+ - matches:
+ - path:
+ value: "/notfound"
+ backendRefs:
+ - name: service-1
+ port: 8080
+ filters:
+ - type: ExtensionRef
+ extensionRef:
+ group: gateway.envoyproxy.io
+ kind: HTTPRouteFilter
+ name: notfound
+httpFilters:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: HTTPRouteFilter
+ metadata:
+ name: valid
+ namespace: default
+ spec:
+ urlRewrite:
+ path:
+ type: ReplaceRegexMatch
+ replaceRegexMatch:
+ pattern: '.*'
+ substitution: foo
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: HTTPRouteFilter
+ metadata:
+ name: valid-2
+ namespace: default
+ spec:
+ urlRewrite:
+ path:
+ type: ReplaceRegexMatch
+ replaceRegexMatch:
+ pattern: '.*'
+ substitution: foo
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: HTTPRouteFilter
+ metadata:
+ name: invalid-pattern
+ namespace: default
+ spec:
+ urlRewrite:
+ path:
+ type: ReplaceRegexMatch
+ replaceRegexMatch:
+ pattern: '"([a-z]+)"*+?'
+ substitution: foo
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: HTTPRouteFilter
+ metadata:
+ name: missing-pattern
+ namespace: default
+ spec:
+ urlRewrite:
+ path:
+ type: ReplaceRegexMatch
+ replaceRegexMatch:
+ substitution: foo
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: HTTPRouteFilter
+ metadata:
+ name: missing-substitution
+ namespace: default
+ spec:
+ urlRewrite:
+ path:
+ type: ReplaceRegexMatch
+ replaceRegexMatch:
+ pattern: '.*'
diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-regex-match-replace-invalid.out.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-regex-match-replace-invalid.out.yaml
new file mode 100644
index 000000000000..d5ffd3a93e7e
--- /dev/null
+++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-regex-match-replace-invalid.out.yaml
@@ -0,0 +1,430 @@
+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: All
+ hostname: '*.envoyproxy.io'
+ name: http
+ port: 80
+ protocol: HTTP
+ status:
+ listeners:
+ - attachedRoutes: 7
+ 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
+ supportedKinds:
+ - group: gateway.networking.k8s.io
+ kind: HTTPRoute
+ - group: gateway.networking.k8s.io
+ kind: GRPCRoute
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: HTTPRoute
+ metadata:
+ creationTimestamp: null
+ name: httproute-invalid-pattern
+ namespace: default
+ spec:
+ hostnames:
+ - gateway.envoyproxy.io
+ parentRefs:
+ - name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http
+ rules:
+ - backendRefs:
+ - name: service-1
+ port: 8080
+ filters:
+ - extensionRef:
+ group: gateway.envoyproxy.io
+ kind: HTTPRouteFilter
+ name: invalid-pattern
+ type: ExtensionRef
+ matches:
+ - path:
+ value: /invalid-pattern
+ status:
+ parents:
+ - conditions:
+ - lastTransitionTime: null
+ message: ReplaceRegexMatch must be a valid RE2 regular expression
+ reason: UnsupportedValue
+ status: "False"
+ type: Accepted
+ - lastTransitionTime: null
+ message: Resolved all the Object references for the Route
+ reason: ResolvedRefs
+ status: "True"
+ type: ResolvedRefs
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+ parentRef:
+ name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: HTTPRoute
+ metadata:
+ creationTimestamp: null
+ name: httproute-missing-pattern
+ namespace: default
+ spec:
+ hostnames:
+ - gateway.envoyproxy.io
+ parentRefs:
+ - name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http
+ rules:
+ - backendRefs:
+ - name: service-1
+ port: 8080
+ filters:
+ - extensionRef:
+ group: gateway.envoyproxy.io
+ kind: HTTPRouteFilter
+ name: missing-pattern
+ type: ExtensionRef
+ matches:
+ - path:
+ value: /missing-pattern
+ status:
+ parents:
+ - conditions:
+ - lastTransitionTime: null
+ message: ReplaceRegexMatch Pattern and Substitution must be set when rewrite
+ path type is "ReplaceRegexMatch"
+ reason: UnsupportedValue
+ status: "False"
+ type: Accepted
+ - lastTransitionTime: null
+ message: Resolved all the Object references for the Route
+ reason: ResolvedRefs
+ status: "True"
+ type: ResolvedRefs
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+ parentRef:
+ name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: HTTPRoute
+ metadata:
+ creationTimestamp: null
+ name: httproute-missing-substitution
+ namespace: default
+ spec:
+ hostnames:
+ - gateway.envoyproxy.io
+ parentRefs:
+ - name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http
+ rules:
+ - backendRefs:
+ - name: service-1
+ port: 8080
+ filters:
+ - extensionRef:
+ group: gateway.envoyproxy.io
+ kind: HTTPRouteFilter
+ name: missing-substitution
+ type: ExtensionRef
+ matches:
+ - path:
+ value: /missing-substitution
+ status:
+ parents:
+ - conditions:
+ - lastTransitionTime: null
+ message: ReplaceRegexMatch Pattern and Substitution must be set when rewrite
+ path type is "ReplaceRegexMatch"
+ reason: UnsupportedValue
+ status: "False"
+ type: Accepted
+ - lastTransitionTime: null
+ message: Resolved all the Object references for the Route
+ reason: ResolvedRefs
+ status: "True"
+ type: ResolvedRefs
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+ parentRef:
+ name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: HTTPRoute
+ metadata:
+ creationTimestamp: null
+ name: httproute-multiple-path-rewrites-1
+ namespace: default
+ spec:
+ hostnames:
+ - gateway.envoyproxy.io
+ parentRefs:
+ - name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http
+ rules:
+ - backendRefs:
+ - name: service-1
+ port: 8080
+ filters:
+ - extensionRef:
+ group: gateway.envoyproxy.io
+ kind: HTTPRouteFilter
+ name: valid
+ type: ExtensionRef
+ - type: URLRewrite
+ urlRewrite:
+ path:
+ replacePrefixMatch: /rewrite
+ type: ReplacePrefixMatch
+ matches:
+ - path:
+ value: /ext-first
+ status:
+ parents:
+ - conditions:
+ - lastTransitionTime: null
+ message: Route is accepted
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ - lastTransitionTime: null
+ message: Resolved all the Object references for the Route
+ reason: ResolvedRefs
+ status: "True"
+ type: ResolvedRefs
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+ parentRef:
+ name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: HTTPRoute
+ metadata:
+ creationTimestamp: null
+ name: httproute-multiple-path-rewrites-2
+ namespace: default
+ spec:
+ hostnames:
+ - gateway.envoyproxy.io
+ parentRefs:
+ - name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http
+ rules:
+ - backendRefs:
+ - name: service-1
+ port: 8080
+ filters:
+ - type: URLRewrite
+ urlRewrite:
+ path:
+ replacePrefixMatch: /rewrite
+ type: ReplacePrefixMatch
+ - extensionRef:
+ group: gateway.envoyproxy.io
+ kind: HTTPRouteFilter
+ name: valid
+ type: ExtensionRef
+ matches:
+ - path:
+ value: /inline-first
+ status:
+ parents:
+ - conditions:
+ - lastTransitionTime: null
+ message: Cannot configure multiple urlRewrite filters for a single HTTPRouteRule
+ reason: UnsupportedValue
+ status: "False"
+ type: Accepted
+ - lastTransitionTime: null
+ message: Resolved all the Object references for the Route
+ reason: ResolvedRefs
+ status: "True"
+ type: ResolvedRefs
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+ parentRef:
+ name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: HTTPRoute
+ metadata:
+ creationTimestamp: null
+ name: httproute-multiple-regex-path-rewrites
+ namespace: default
+ spec:
+ hostnames:
+ - gateway.envoyproxy.io
+ parentRefs:
+ - name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http
+ rules:
+ - backendRefs:
+ - name: service-1
+ port: 8080
+ filters:
+ - extensionRef:
+ group: gateway.envoyproxy.io
+ kind: HTTPRouteFilter
+ name: valid
+ type: ExtensionRef
+ - extensionRef:
+ group: gateway.envoyproxy.io
+ kind: HTTPRouteFilter
+ name: valid-2
+ type: ExtensionRef
+ matches:
+ - path:
+ value: /two-regex
+ status:
+ parents:
+ - conditions:
+ - lastTransitionTime: null
+ message: Cannot configure multiple urlRewrite filters for a single HTTPRouteRule
+ reason: UnsupportedValue
+ status: "False"
+ type: Accepted
+ - lastTransitionTime: null
+ message: Resolved all the Object references for the Route
+ reason: ResolvedRefs
+ status: "True"
+ type: ResolvedRefs
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+ parentRef:
+ name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: HTTPRoute
+ metadata:
+ creationTimestamp: null
+ name: httproute-not-found
+ namespace: default
+ spec:
+ hostnames:
+ - gateway.envoyproxy.io
+ parentRefs:
+ - name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http
+ rules:
+ - backendRefs:
+ - name: service-1
+ port: 8080
+ filters:
+ - extensionRef:
+ group: gateway.envoyproxy.io
+ kind: HTTPRouteFilter
+ name: notfound
+ type: ExtensionRef
+ matches:
+ - path:
+ value: /notfound
+ status:
+ parents:
+ - conditions:
+ - lastTransitionTime: null
+ message: 'Unable to translate HTTPRouteFilter: default/notfound'
+ reason: UnsupportedValue
+ status: "False"
+ type: Accepted
+ - lastTransitionTime: null
+ message: 'Unable to translate HTTPRouteFilter: default/notfound'
+ reason: BackendNotFound
+ status: "False"
+ type: ResolvedRefs
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+ parentRef:
+ name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http
+infraIR:
+ envoy-gateway/gateway-1:
+ proxy:
+ listeners:
+ - address: null
+ name: envoy-gateway/gateway-1/http
+ 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
+ hostnames:
+ - '*.envoyproxy.io'
+ isHTTP2: false
+ metadata:
+ kind: Gateway
+ name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http
+ name: envoy-gateway/gateway-1/http
+ path:
+ escapedSlashesAction: UnescapeAndRedirect
+ mergeSlashes: true
+ port: 10080
+ routes:
+ - destination:
+ name: httproute/default/httproute-multiple-path-rewrites-1/rule/0
+ settings:
+ - addressType: IP
+ endpoints:
+ - host: 7.7.7.7
+ port: 8080
+ protocol: HTTP
+ weight: 1
+ hostname: gateway.envoyproxy.io
+ isHTTP2: false
+ metadata:
+ kind: HTTPRoute
+ name: httproute-multiple-path-rewrites-1
+ namespace: default
+ name: httproute/default/httproute-multiple-path-rewrites-1/rule/0/match/0/gateway_envoyproxy_io
+ pathMatch:
+ distinct: false
+ name: ""
+ prefix: /ext-first
+ urlRewrite:
+ path:
+ fullReplace: null
+ prefixMatchReplace: /rewrite
+ regexMatchReplace: null
diff --git a/internal/ir/xds.go b/internal/ir/xds.go
index a40cd8547665..80123ed1844d 100644
--- a/internal/ir/xds.go
+++ b/internal/ir/xds.go
@@ -51,8 +51,9 @@ var (
ErrDirectResponseStatusInvalid = errors.New("only HTTP status codes 100 - 599 are supported for DirectResponse")
ErrRedirectUnsupportedStatus = errors.New("only HTTP status codes 301 and 302 are supported for redirect filters")
ErrRedirectUnsupportedScheme = errors.New("only http and https are supported for the scheme in redirect filters")
- ErrHTTPPathModifierDoubleReplace = errors.New("redirect filter cannot have a path modifier that supplies both fullPathReplace and prefixMatchReplace")
- ErrHTTPPathModifierNoReplace = errors.New("redirect filter cannot have a path modifier that does not supply either fullPathReplace or prefixMatchReplace")
+ ErrHTTPPathModifierDoubleReplace = errors.New("redirect filter cannot have a path modifier that supplies more than one of fullPathReplace, prefixMatchReplace and regexMatchReplace")
+ ErrHTTPPathModifierNoReplace = errors.New("redirect filter cannot have a path modifier that does not supply either fullPathReplace, prefixMatchReplace or regexMatchReplace")
+ ErrHTTPPathRegexModifierNoSetting = errors.New("redirect filter cannot have a path modifier that does not supply either fullPathReplace, prefixMatchReplace or regexMatchReplace")
ErrAddHeaderEmptyName = errors.New("header modifier filter cannot configure a header without a name to be added")
ErrAddHeaderDuplicate = errors.New("header modifier filter attempts to add the same header more than once (case insensitive)")
ErrRemoveHeaderDuplicate = errors.New("header modifier filter attempts to remove the same header more than once (case insensitive)")
@@ -729,6 +730,18 @@ type UnstructuredRef struct {
Object *unstructured.Unstructured `json:"object,omitempty" yaml:"object,omitempty"`
}
+// RegexMatchReplace defines the schema for modifying HTTP request path using regex.
+//
+// +k8s:deepcopy-gen=true
+type RegexMatchReplace struct {
+ // Pattern matches a regular expression against the value of the HTTP Path.The regex string must
+ // adhere to the syntax documented in https://github.com/google/re2/wiki/Syntax.
+ Pattern string `json:"pattern" yaml:"pattern"`
+ // Substitution is an expression that replaces the matched portion.The expression may include numbered
+ // capture groups that adhere to syntax documented in https://github.com/google/re2/wiki/Syntax.
+ Substitution string `json:"substitution" yaml:"substitution"`
+}
+
// CORS holds the Cross-Origin Resource Sharing (CORS) policy for the route.
//
// +k8s:deepcopy-gen=true
@@ -1293,7 +1306,7 @@ func (r DirectResponse) Validate() error {
// +k8s:deepcopy-gen=true
type URLRewrite struct {
// Path contains config for rewriting the path of the request.
- Path *HTTPPathModifier `json:"path,omitempty" yaml:"path,omitempty"`
+ Path *ExtendedHTTPPathModifier `json:"path,omitempty" yaml:"path,omitempty"`
// Hostname configures the replacement of the request's hostname.
Hostname *string `json:"hostname,omitempty" yaml:"hostname,omitempty"`
}
@@ -1375,6 +1388,42 @@ func (r HTTPPathModifier) Validate() error {
return errs
}
+// ExtendedHTTPPathModifier holds instructions for how to modify the path of a request on a redirect response
+// with both core gateway-api and extended envoy gateway capabilities
+// +k8s:deepcopy-gen=true
+type ExtendedHTTPPathModifier struct {
+ HTTPPathModifier `json:",inline" yaml:",inline"`
+ // RegexMatchReplace provides a regex to match an a replacement to perform on the path.
+ RegexMatchReplace *RegexMatchReplace `json:"regexMatchReplace" yaml:"regexMatchReplace"`
+}
+
+// Validate the fields within the HTTPPathModifier structure
+func (r ExtendedHTTPPathModifier) Validate() error {
+ var errs error
+
+ rewrites := []bool{r.RegexMatchReplace != nil, r.PrefixMatchReplace != nil, r.FullReplace != nil}
+ rwc := 0
+ for _, rw := range rewrites {
+ if rw {
+ rwc++
+ }
+ }
+
+ if rwc > 1 {
+ errs = errors.Join(errs, ErrHTTPPathModifierDoubleReplace)
+ }
+
+ if r.FullReplace == nil && r.PrefixMatchReplace == nil && r.RegexMatchReplace == nil {
+ errs = errors.Join(errs, ErrHTTPPathModifierNoReplace)
+ }
+
+ if r.RegexMatchReplace != nil && (r.RegexMatchReplace.Pattern == "" || r.RegexMatchReplace.Substitution == "") {
+ errs = errors.Join(errs, ErrHTTPPathModifierNoReplace)
+ }
+
+ return errs
+}
+
// StringMatch holds the various match conditions.
// Only one of Exact, Prefix, SafeRegex or Distinct can be set.
// +k8s:deepcopy-gen=true
diff --git a/internal/ir/xds_test.go b/internal/ir/xds_test.go
index 882aa090e552..14b624f22f3f 100644
--- a/internal/ir/xds_test.go
+++ b/internal/ir/xds_test.go
@@ -309,8 +309,10 @@ var (
},
URLRewrite: &URLRewrite{
Hostname: ptr.To("rewrite.example.com"),
- Path: &HTTPPathModifier{
- FullReplace: ptr.To("/rewrite"),
+ Path: &ExtendedHTTPPathModifier{
+ HTTPPathModifier: HTTPPathModifier{
+ FullReplace: ptr.To("/rewrite"),
+ },
},
},
}
@@ -323,9 +325,11 @@ var (
},
URLRewrite: &URLRewrite{
Hostname: ptr.To("rewrite.example.com"),
- Path: &HTTPPathModifier{
- FullReplace: ptr.To("/rewrite"),
- PrefixMatchReplace: ptr.To("/rewrite"),
+ Path: &ExtendedHTTPPathModifier{
+ HTTPPathModifier: HTTPPathModifier{
+ FullReplace: ptr.To("/rewrite"),
+ PrefixMatchReplace: ptr.To("/rewrite"),
+ },
},
},
}
diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go
index 1639262be2e6..0b964d5fc41e 100644
--- a/internal/ir/zz_generated.deepcopy.go
+++ b/internal/ir/zz_generated.deepcopy.go
@@ -909,6 +909,27 @@ func (in *ExtProc) DeepCopy() *ExtProc {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ExtendedHTTPPathModifier) DeepCopyInto(out *ExtendedHTTPPathModifier) {
+ *out = *in
+ in.HTTPPathModifier.DeepCopyInto(&out.HTTPPathModifier)
+ if in.RegexMatchReplace != nil {
+ in, out := &in.RegexMatchReplace, &out.RegexMatchReplace
+ *out = new(RegexMatchReplace)
+ **out = **in
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtendedHTTPPathModifier.
+func (in *ExtendedHTTPPathModifier) DeepCopy() *ExtendedHTTPPathModifier {
+ if in == nil {
+ return nil
+ }
+ out := new(ExtendedHTTPPathModifier)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *FaultInjection) DeepCopyInto(out *FaultInjection) {
*out = *in
@@ -2296,6 +2317,21 @@ func (in *Redirect) DeepCopy() *Redirect {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *RegexMatchReplace) DeepCopyInto(out *RegexMatchReplace) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RegexMatchReplace.
+func (in *RegexMatchReplace) DeepCopy() *RegexMatchReplace {
+ if in == nil {
+ return nil
+ }
+ out := new(RegexMatchReplace)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ResourceMetadata) DeepCopyInto(out *ResourceMetadata) {
*out = *in
@@ -3136,7 +3172,7 @@ func (in *URLRewrite) DeepCopyInto(out *URLRewrite) {
*out = *in
if in.Path != nil {
in, out := &in.Path, &out.Path
- *out = new(HTTPPathModifier)
+ *out = new(ExtendedHTTPPathModifier)
(*in).DeepCopyInto(*out)
}
if in.Hostname != nil {
diff --git a/internal/provider/kubernetes/controller.go b/internal/provider/kubernetes/controller.go
index a07ca6ab1204..24543fbd4a58 100644
--- a/internal/provider/kubernetes/controller.go
+++ b/internal/provider/kubernetes/controller.go
@@ -148,6 +148,9 @@ type resourceMappings struct {
// The key is the namespaced name, group and kind of the filter and the value is the
// unstructured form of the resource.
extensionRefFilters map[utils.NamespacedNameWithGroupKind]unstructured.Unstructured
+ // httpRouteFilters is a map of HTTPRouteFilters, where the key is the namespaced name,
+ // group and kind of the HTTPFilter.
+ httpRouteFilters map[utils.NamespacedNameWithGroupKind]*egv1a1.HTTPRouteFilter
}
func newResourceMapping() *resourceMappings {
@@ -161,6 +164,7 @@ func newResourceMapping() *resourceMappings {
allAssociatedUDPRoutes: sets.New[string](),
allAssociatedBackendRefs: sets.New[gwapiv1.BackendObjectReference](),
extensionRefFilters: map[utils.NamespacedNameWithGroupKind]unstructured.Unstructured{},
+ httpRouteFilters: map[utils.NamespacedNameWithGroupKind]*egv1a1.HTTPRouteFilter{},
}
}
diff --git a/internal/provider/kubernetes/filters.go b/internal/provider/kubernetes/filters.go
index 985990fef147..11e62271d5ad 100644
--- a/internal/provider/kubernetes/filters.go
+++ b/internal/provider/kubernetes/filters.go
@@ -10,6 +10,8 @@ import (
"fmt"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+
+ egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
)
func (r *gatewayAPIReconciler) getExtensionRefFilters(ctx context.Context) ([]unstructured.Unstructured, error) {
@@ -43,3 +45,12 @@ func (r *gatewayAPIReconciler) getExtensionRefFilters(ctx context.Context) ([]un
return resourceItems, nil
}
+
+func (r *gatewayAPIReconciler) getHTTPFilters(ctx context.Context) ([]egv1a1.HTTPRouteFilter, error) {
+ httpFilterList := new(egv1a1.HTTPRouteFilterList)
+ if err := r.client.List(ctx, httpFilterList); err != nil {
+ return nil, fmt.Errorf("failed to list HTTPRouteFilters: %w", err)
+ }
+
+ return httpFilterList.Items, nil
+}
diff --git a/internal/provider/kubernetes/routes.go b/internal/provider/kubernetes/routes.go
index d298e7e0e465..a9835728b37b 100644
--- a/internal/provider/kubernetes/routes.go
+++ b/internal/provider/kubernetes/routes.go
@@ -16,6 +16,7 @@ import (
gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
+ egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
"github.com/envoyproxy/gateway/internal/gatewayapi"
"github.com/envoyproxy/gateway/internal/gatewayapi/resource"
"github.com/envoyproxy/gateway/internal/utils"
@@ -231,6 +232,15 @@ func (r *gatewayAPIReconciler) processHTTPRoutes(ctx context.Context, gatewayNam
) error {
httpRouteList := &gwapiv1.HTTPRouteList{}
+ httpFilters, err := r.getHTTPFilters(ctx)
+ if err != nil {
+ return err
+ }
+ for i := range httpFilters {
+ filter := httpFilters[i]
+ resourceMap.httpRouteFilters[utils.GetNamespacedNameWithGroupKind(&filter)] = &filter
+ }
+
extensionRefFilters, err := r.getExtensionRefFilters(ctx)
if err != nil {
return err
@@ -385,17 +395,29 @@ func (r *gatewayAPIReconciler) processHTTPRoutes(ctx context.Context, gatewayNam
Kind: string(filter.ExtensionRef.Kind),
},
}
- extRefFilter, ok := resourceMap.extensionRefFilters[key]
- if !ok {
- r.log.Error(
- errors.New("filter not found; bypassing rule"),
- "Filter not found; bypassing rule",
- "name", filter.ExtensionRef.Name,
- "index", i)
- continue
- }
- resourceTree.ExtensionRefFilters = append(resourceTree.ExtensionRefFilters, extRefFilter)
+ switch string(filter.ExtensionRef.Kind) {
+ case egv1a1.KindHTTPRouteFilter:
+ httpFilter, ok := resourceMap.httpRouteFilters[key]
+ if !ok {
+ r.log.Error(err, "HTTPRouteFilters not found; bypassing rule", "index", i)
+ continue
+ }
+
+ resourceTree.HTTPRouteFilters = append(resourceTree.HTTPRouteFilters, httpFilter)
+ default:
+ extRefFilter, ok := resourceMap.extensionRefFilters[key]
+ if !ok {
+ r.log.Error(
+ errors.New("filter not found; bypassing rule"),
+ "Filter not found; bypassing rule",
+ "name", filter.ExtensionRef.Name,
+ "index", i)
+ continue
+ }
+
+ resourceTree.ExtensionRefFilters = append(resourceTree.ExtensionRefFilters, extRefFilter)
+ }
}
}
}
diff --git a/internal/xds/translator/route.go b/internal/xds/translator/route.go
index 42f17ff94f18..a8ec4a291d5b 100644
--- a/internal/xds/translator/route.go
+++ b/internal/xds/translator/route.go
@@ -386,14 +386,15 @@ func buildXdsURLRewriteAction(destName string, urlRewrite *ir.URLRewrite, pathMa
}
if urlRewrite.Path != nil {
- if urlRewrite.Path.FullReplace != nil {
+ switch {
+ case urlRewrite.Path.FullReplace != nil:
routeAction.RegexRewrite = &matcherv3.RegexMatchAndSubstitute{
Pattern: &matcherv3.RegexMatcher{
Regex: "^/.*$",
},
Substitution: *urlRewrite.Path.FullReplace,
}
- } else if urlRewrite.Path.PrefixMatchReplace != nil {
+ case urlRewrite.Path.PrefixMatchReplace != nil:
// Circumvent the case of "//" when the replace string is "/"
// An empty replace string does not seem to solve the issue so we are using
// a regex match and replace instead
@@ -406,6 +407,13 @@ func buildXdsURLRewriteAction(destName string, urlRewrite *ir.URLRewrite, pathMa
// and the urlRewrite.Path.PrefixMatchReplace suffix with / the upstream will get unwanted /
routeAction.PrefixRewrite = strings.TrimSuffix(*urlRewrite.Path.PrefixMatchReplace, "/")
}
+ case urlRewrite.Path.RegexMatchReplace != nil:
+ routeAction.RegexRewrite = &matcherv3.RegexMatchAndSubstitute{
+ Pattern: &matcherv3.RegexMatcher{
+ Regex: urlRewrite.Path.RegexMatchReplace.Pattern,
+ },
+ Substitution: urlRewrite.Path.RegexMatchReplace.Substitution,
+ }
}
}
diff --git a/internal/xds/translator/testdata/in/xds-ir/http-route-rewrite-url-regex.yaml b/internal/xds/translator/testdata/in/xds-ir/http-route-rewrite-url-regex.yaml
new file mode 100644
index 000000000000..0389201186eb
--- /dev/null
+++ b/internal/xds/translator/testdata/in/xds-ir/http-route-rewrite-url-regex.yaml
@@ -0,0 +1,26 @@
+name: "http-route"
+http:
+- name: "first-listener"
+ address: "0.0.0.0"
+ port: 10080
+ hostnames:
+ - "*"
+ path:
+ mergeSlashes: true
+ escapedSlashesAction: UnescapeAndRedirect
+ routes:
+ - name: "rewrite-route"
+ pathMatch:
+ prefix: "/origin"
+ hostname: gateway.envoyproxy.io
+ destination:
+ name: "rewrite-route"
+ settings:
+ - endpoints:
+ - host: "1.2.3.4"
+ port: 50000
+ urlRewrite:
+ path:
+ regexMatchReplace:
+ pattern: '^/service/([^/]+)(/.*)$'
+ substitution: '\2/instance/\1'
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-regex.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-regex.clusters.yaml
new file mode 100644
index 000000000000..3a2b7308d8e1
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-regex.clusters.yaml
@@ -0,0 +1,17 @@
+- circuitBreakers:
+ thresholds:
+ - maxRetries: 1024
+ commonLbConfig:
+ localityWeightedLbConfig: {}
+ connectTimeout: 10s
+ dnsLookupFamily: V4_ONLY
+ edsClusterConfig:
+ edsConfig:
+ ads: {}
+ resourceApiVersion: V3
+ serviceName: rewrite-route
+ lbPolicy: LEAST_REQUEST
+ name: rewrite-route
+ outlierDetection: {}
+ perConnectionBufferLimitBytes: 32768
+ type: EDS
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-regex.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-regex.endpoints.yaml
new file mode 100644
index 000000000000..ca1ef21c989e
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-regex.endpoints.yaml
@@ -0,0 +1,12 @@
+- clusterName: rewrite-route
+ endpoints:
+ - lbEndpoints:
+ - endpoint:
+ address:
+ socketAddress:
+ address: 1.2.3.4
+ portValue: 50000
+ loadBalancingWeight: 1
+ loadBalancingWeight: 1
+ locality:
+ region: rewrite-route/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-regex.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-regex.listeners.yaml
new file mode 100644
index 000000000000..c3fb113017a9
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-regex.listeners.yaml
@@ -0,0 +1,34 @@
+- 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
+ 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
+ name: first-listener
+ perConnectionBufferLimitBytes: 32768
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-regex.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-regex.routes.yaml
new file mode 100644
index 000000000000..20d4e99ef68c
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-regex.routes.yaml
@@ -0,0 +1,18 @@
+- ignorePortInHostMatching: true
+ name: first-listener
+ virtualHosts:
+ - domains:
+ - gateway.envoyproxy.io
+ name: first-listener/gateway_envoyproxy_io
+ routes:
+ - match:
+ pathSeparatedPrefix: /origin
+ name: rewrite-route
+ route:
+ cluster: rewrite-route
+ regexRewrite:
+ pattern:
+ regex: ^/service/([^/]+)(/.*)$
+ substitution: \2/instance/\1
+ upgradeConfigs:
+ - upgradeType: websocket
diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md
index 70b7608406e6..92edccbbe238 100644
--- a/site/content/en/latest/api/extension_types.md
+++ b/site/content/en/latest/api/extension_types.md
@@ -1905,7 +1905,7 @@ _Appears in:_
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `type` | _[HTTPPathModifierType](#httppathmodifiertype)_ | true | |
-| `replaceRegexMatch` | _[ReplaceRegexMatch](#replaceregexmatch)_ | true | ReplaceRegexMatch defines a path regex rewrite. The path portions matched by the regex pattern are replaced by the defined substitution.
https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#envoy-v3-api-field-config-route-v3-routeaction-regex-rewrite
Some examples:
(1) replaceRegexMatch:
pattern: ^/service/([^/]+)(/.*)$
substitution: \2/instance/\1
Would transform /service/foo/v1/api into /v1/api/instance/foo.
(2) replaceRegexMatch:
pattern: one
substitution: two
Would transform /xxx/one/yyy/one/zzz into /xxx/two/yyy/two/zzz.
(3) replaceRegexMatch:
pattern: ^(.*?)one(.*)$
substitution: \1two\2
Would transform /xxx/one/yyy/one/zzz into /xxx/two/yyy/one/zzz.
(3) replaceRegexMatch:
pattern: (?i)/xxx/
substitution: /yyy/
Would transform path /aaa/XxX/bbb into /aaa/yyy/bbb (case-insensitive). |
+| `replaceRegexMatch` | _[ReplaceRegexMatch](#replaceregexmatch)_ | false | ReplaceRegexMatch defines a path regex rewrite. The path portions matched by the regex pattern are replaced by the defined substitution.
https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#envoy-v3-api-field-config-route-v3-routeaction-regex-rewrite
Some examples:
(1) replaceRegexMatch:
pattern: ^/service/([^/]+)(/.*)$
substitution: \2/instance/\1
Would transform /service/foo/v1/api into /v1/api/instance/foo.
(2) replaceRegexMatch:
pattern: one
substitution: two
Would transform /xxx/one/yyy/one/zzz into /xxx/two/yyy/two/zzz.
(3) replaceRegexMatch:
pattern: ^(.*?)one(.*)$
substitution: \1two\2
Would transform /xxx/one/yyy/one/zzz into /xxx/two/yyy/one/zzz.
(3) replaceRegexMatch:
pattern: (?i)/xxx/
substitution: /yyy/
Would transform path /aaa/XxX/bbb into /aaa/yyy/bbb (case-insensitive). |
#### HTTPPathModifierType
diff --git a/site/content/en/latest/tasks/traffic/http-urlrewrite.md b/site/content/en/latest/tasks/traffic/http-urlrewrite.md
index 67915f93fcfe..a643d775a570 100644
--- a/site/content/en/latest/tasks/traffic/http-urlrewrite.md
+++ b/site/content/en/latest/tasks/traffic/http-urlrewrite.md
@@ -275,6 +275,160 @@ $ curl -L -vvv --header "Host: path.rewrite.example" "http://${GATEWAY_HOST}/get
You can see that the `X-Envoy-Original-Path` is `/get/origin/path/extra`, but the actual path is
`/force/replace/fullpath`.
+## Rewrite URL Path with Regex
+
+In addition to core Gateway-API rewrite options, Envoy Gateway supports extended rewrite options through the [HTTPRouteFilter][] API.
+The `HTTPRouteFilter` API can be configured to use [RE2][]-compatible regex matchers and substitutions to rewrite a portion of the url.
+In the example below, requests sent to `http://${GATEWAY_HOST}/service/xxx/yyy` (where `xxx` is a single path portion and `yyy` is one or more path portions)
+are rewritten to `http://${GATEWAY_HOST}/yyy/instance/xxx`. The entire path is matched and rewritten using capture groups.
+
+{{< tabpane text=true >}}
+{{% tab header="Apply from stdin" %}}
+
+```shell
+cat <}}
+
+The HTTPRoute status should indicate that it has been accepted and is bound to the example Gateway.
+
+```shell
+kubectl get httproute/http-filter-url-rewrite -o yaml
+```
+
+Querying `http://${GATEWAY_HOST}/get/origin/path/extra` should rewrite the request to
+`http://${GATEWAY_HOST}/force/replace/fullpath`.
+
+```console
+$ curl -L -vvv --header "Host: path.regex.rewrite.example" "http://${GATEWAY_HOST}/get/origin/path/extra"
+...
+> GET /service/foo/v1/api HTTP/1.1
+> Host: path.regex.rewrite.example
+> User-Agent: curl/8.7.1
+> Accept: */*
+>
+* Request completely sent off
+< HTTP/1.1 200 OK
+< content-type: application/json
+< x-content-type-options: nosniff
+< date: Mon, 16 Sep 2024 18:49:48 GMT
+< content-length: 482
+<
+{
+ "path": "/v1/api/instance/foo",
+ "host": "path.regex.rewrite.example",
+ "method": "GET",
+ "proto": "HTTP/1.1",
+ "headers": {
+ "Accept": [
+ "*/*"
+ ],
+ "User-Agent": [
+ "curl/8.7.1"
+ ],
+ "X-Envoy-Internal": [
+ "true"
+ ],
+ "X-Forwarded-For": [
+ "10.244.0.37"
+ ],
+ "X-Forwarded-Proto": [
+ "http"
+ ],
+ "X-Request-Id": [
+ "24a5958f-1bfa-4694-a9c1-807d5139a18a"
+ ]
+ },
+ "namespace": "default",
+ "ingress": "",
+ "service": "",
+ "pod": "backend-765694d47f-lzmpm"
+...
+```
+
+You can see that the path is rewritten from `/service/foo/v1/api`, to `/v1/api/instance/foo`.
+
## Rewrite Host Name
You can configure to rewrite the hostname like below. In this example, any requests sent to
@@ -402,3 +556,5 @@ $ curl -L -vvv --header "Host: path.rewrite.example" "http://${GATEWAY_HOST}/get
You can see that the `X-Forwarded-Host` is `path.rewrite.example`, but the actual host is `envoygateway.io`.
[HTTPURLRewriteFilter]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPURLRewriteFilter
+[HTTPRouteFilter]: ../../../api/extension_types#httproutefilter
+[RE2]: https://github.com/google/re2/wiki/Syntax
\ No newline at end of file
diff --git a/site/content/zh/latest/api/extension_types.md b/site/content/zh/latest/api/extension_types.md
index 70b7608406e6..92edccbbe238 100644
--- a/site/content/zh/latest/api/extension_types.md
+++ b/site/content/zh/latest/api/extension_types.md
@@ -1905,7 +1905,7 @@ _Appears in:_
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `type` | _[HTTPPathModifierType](#httppathmodifiertype)_ | true | |
-| `replaceRegexMatch` | _[ReplaceRegexMatch](#replaceregexmatch)_ | true | ReplaceRegexMatch defines a path regex rewrite. The path portions matched by the regex pattern are replaced by the defined substitution.
https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#envoy-v3-api-field-config-route-v3-routeaction-regex-rewrite
Some examples:
(1) replaceRegexMatch:
pattern: ^/service/([^/]+)(/.*)$
substitution: \2/instance/\1
Would transform /service/foo/v1/api into /v1/api/instance/foo.
(2) replaceRegexMatch:
pattern: one
substitution: two
Would transform /xxx/one/yyy/one/zzz into /xxx/two/yyy/two/zzz.
(3) replaceRegexMatch:
pattern: ^(.*?)one(.*)$
substitution: \1two\2
Would transform /xxx/one/yyy/one/zzz into /xxx/two/yyy/one/zzz.
(3) replaceRegexMatch:
pattern: (?i)/xxx/
substitution: /yyy/
Would transform path /aaa/XxX/bbb into /aaa/yyy/bbb (case-insensitive). |
+| `replaceRegexMatch` | _[ReplaceRegexMatch](#replaceregexmatch)_ | false | ReplaceRegexMatch defines a path regex rewrite. The path portions matched by the regex pattern are replaced by the defined substitution.
https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#envoy-v3-api-field-config-route-v3-routeaction-regex-rewrite
Some examples:
(1) replaceRegexMatch:
pattern: ^/service/([^/]+)(/.*)$
substitution: \2/instance/\1
Would transform /service/foo/v1/api into /v1/api/instance/foo.
(2) replaceRegexMatch:
pattern: one
substitution: two
Would transform /xxx/one/yyy/one/zzz into /xxx/two/yyy/two/zzz.
(3) replaceRegexMatch:
pattern: ^(.*?)one(.*)$
substitution: \1two\2
Would transform /xxx/one/yyy/one/zzz into /xxx/two/yyy/one/zzz.
(3) replaceRegexMatch:
pattern: (?i)/xxx/
substitution: /yyy/
Would transform path /aaa/XxX/bbb into /aaa/yyy/bbb (case-insensitive). |
#### HTTPPathModifierType
diff --git a/test/cel-validation/httproutefilter_test.go b/test/cel-validation/httproutefilter_test.go
new file mode 100644
index 000000000000..5d82f3c77663
--- /dev/null
+++ b/test/cel-validation/httproutefilter_test.go
@@ -0,0 +1,121 @@
+// 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 celvalidation
+// +build celvalidation
+
+package celvalidation
+
+import (
+ "context"
+ "fmt"
+ "strings"
+ "testing"
+ "time"
+
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+ egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
+)
+
+func TestHTTPRouteFilter(t *testing.T) {
+ ctx := context.Background()
+ baseHTTPRouteFilter := egv1a1.HTTPRouteFilter{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "httproutefilter",
+ Namespace: metav1.NamespaceDefault,
+ },
+ Spec: egv1a1.HTTPRouteFilterSpec{},
+ }
+
+ cases := []struct {
+ desc string
+ mutate func(httproutefilter *egv1a1.HTTPRouteFilter)
+ mutateStatus func(httproutefilter *egv1a1.HTTPRouteFilter)
+ wantErrors []string
+ }{
+ {
+ desc: "Valid RegexHTTPPathModifier",
+ mutate: func(httproutefilter *egv1a1.HTTPRouteFilter) {
+ httproutefilter.Spec = egv1a1.HTTPRouteFilterSpec{
+ URLRewrite: &egv1a1.HTTPURLRewriteFilter{
+ Path: &egv1a1.HTTPPathModifier{
+ Type: egv1a1.RegexHTTPPathModifier,
+ ReplaceRegexMatch: &egv1a1.ReplaceRegexMatch{
+ Pattern: "foo",
+ Substitution: "bar",
+ },
+ },
+ },
+ }
+ },
+ wantErrors: []string{},
+ },
+ {
+ desc: "invalid RegexHTTPPathModifier missing settings",
+ mutate: func(httproutefilter *egv1a1.HTTPRouteFilter) {
+ httproutefilter.Spec = egv1a1.HTTPRouteFilterSpec{
+ URLRewrite: &egv1a1.HTTPURLRewriteFilter{
+ Path: &egv1a1.HTTPPathModifier{
+ Type: egv1a1.RegexHTTPPathModifier,
+ },
+ },
+ }
+ },
+ wantErrors: []string{"spec.urlRewrite.path: Invalid value: \"object\": If HTTPPathModifier type is ReplaceRegexMatch, replaceRegexMatch field needs to be set."},
+ },
+ {
+ desc: "invalid RegexHTTPPathModifier missing pattern and substitution",
+ mutate: func(httproutefilter *egv1a1.HTTPRouteFilter) {
+ httproutefilter.Spec = egv1a1.HTTPRouteFilterSpec{
+ URLRewrite: &egv1a1.HTTPURLRewriteFilter{
+ Path: &egv1a1.HTTPPathModifier{
+ Type: egv1a1.RegexHTTPPathModifier,
+ ReplaceRegexMatch: &egv1a1.ReplaceRegexMatch{
+ Pattern: "",
+ Substitution: "",
+ },
+ },
+ },
+ }
+ },
+ wantErrors: []string{
+ "spec.urlRewrite.path.replaceRegexMatch.substitution: Invalid value: \"\": spec.urlRewrite.path.replaceRegexMatch.substitution in body should be at least 1 chars long",
+ "spec.urlRewrite.path.replaceRegexMatch.pattern: Invalid value: \"\": spec.urlRewrite.path.replaceRegexMatch.pattern in body should be at least 1 chars long",
+ },
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.desc, func(t *testing.T) {
+ httpRouteFilter := baseHTTPRouteFilter.DeepCopy()
+ httpRouteFilter.Name = fmt.Sprintf("httpRouteFilter-%v", time.Now().UnixNano())
+
+ if tc.mutate != nil {
+ tc.mutate(httpRouteFilter)
+ }
+ err := c.Create(ctx, httpRouteFilter)
+
+ if tc.mutateStatus != nil {
+ tc.mutateStatus(httpRouteFilter)
+ err = c.Status().Update(ctx, httpRouteFilter)
+ }
+
+ if (len(tc.wantErrors) != 0) != (err != nil) {
+ t.Fatalf("Unexpected response while creating HTTPRouteFilter; got err=\n%v\n;want error=%v", err, tc.wantErrors)
+ }
+
+ var missingErrorStrings []string
+ for _, wantError := range tc.wantErrors {
+ if !strings.Contains(strings.ToLower(err.Error()), strings.ToLower(wantError)) {
+ missingErrorStrings = append(missingErrorStrings, wantError)
+ }
+ }
+ if len(missingErrorStrings) != 0 {
+ t.Errorf("Unexpected response while creating HTTPRouteFilter; got err=\n%v\n;missing strings within error=%q", err, missingErrorStrings)
+ }
+ })
+ }
+}
diff --git a/test/e2e/testdata/httproute-rewrite-regex-path.yaml b/test/e2e/testdata/httproute-rewrite-regex-path.yaml
new file mode 100644
index 000000000000..821550d73759
--- /dev/null
+++ b/test/e2e/testdata/httproute-rewrite-regex-path.yaml
@@ -0,0 +1,35 @@
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+ name: rewrite-regex-path
+ namespace: gateway-conformance-infra
+spec:
+ parentRefs:
+ - name: same-namespace
+ rules:
+ - matches:
+ - path:
+ type: PathPrefix
+ value: /
+ filters:
+ - type: ExtensionRef
+ extensionRef:
+ group: gateway.envoyproxy.io
+ kind: HTTPRouteFilter
+ name: regex-path-rewrite
+ backendRefs:
+ - name: infra-backend-v1
+ port: 8080
+---
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: HTTPRouteFilter
+metadata:
+ name: regex-path-rewrite
+ namespace: gateway-conformance-infra
+spec:
+ urlRewrite:
+ path:
+ type: ReplaceRegexMatch
+ replaceRegexMatch:
+ pattern: '^/service/([^/]+)(/.*)$'
+ substitution: '\2/instance/\1'
diff --git a/test/e2e/tests/httproute_rewrite_regex_path.go b/test/e2e/tests/httproute_rewrite_regex_path.go
new file mode 100644
index 000000000000..975d2ec88680
--- /dev/null
+++ b/test/e2e/tests/httproute_rewrite_regex_path.go
@@ -0,0 +1,59 @@
+// 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"
+ "sigs.k8s.io/gateway-api/conformance/utils/http"
+ "sigs.k8s.io/gateway-api/conformance/utils/kubernetes"
+ "sigs.k8s.io/gateway-api/conformance/utils/suite"
+)
+
+func init() {
+ ConformanceTests = append(ConformanceTests, HTTPRouteRewriteRegexPath)
+}
+
+var HTTPRouteRewriteRegexPath = suite.ConformanceTest{
+ ShortName: "HTTPRouteRewriteRegexPath",
+ Description: "An HTTPRoute with path rewrite filter to replace a regex match",
+ Manifests: []string{"testdata/httproute-rewrite-regex-path.yaml"},
+ Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
+ ns := "gateway-conformance-infra"
+ routeNN := types.NamespacedName{Name: "rewrite-regex-path", Namespace: ns}
+ gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns}
+ gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN)
+ kubernetes.HTTPRouteMustHaveResolvedRefsConditionsTrue(t, suite.Client, suite.TimeoutConfig, routeNN, gwNN)
+
+ testCases := []http.ExpectedResponse{
+ {
+ Request: http.Request{
+ Path: "/service/foo/v1/api",
+ },
+ ExpectedRequest: &http.ExpectedRequest{
+ Request: http.Request{
+ Path: "/v1/api/instance/foo",
+ },
+ },
+ Backend: "infra-backend-v1",
+ Namespace: ns,
+ },
+ }
+ for i := range testCases {
+ // Declare tc here to avoid loop variable
+ // reuse issues across parallel tests.
+ tc := testCases[i]
+ t.Run(tc.GetTestCaseName(i), func(t *testing.T) {
+ t.Parallel()
+ http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, tc)
+ })
+ }
+ },
+}
diff --git a/test/helm/gateway-helm/certjen-custom-scheduling.out.yaml b/test/helm/gateway-helm/certjen-custom-scheduling.out.yaml
index 0bd873a34b9a..c280e54ca94d 100644
--- a/test/helm/gateway-helm/certjen-custom-scheduling.out.yaml
+++ b/test/helm/gateway-helm/certjen-custom-scheduling.out.yaml
@@ -127,6 +127,7 @@ rules:
- securitypolicies
- envoyextensionpolicies
- backends
+ - httproutefilters
verbs:
- get
- list
diff --git a/test/helm/gateway-helm/control-plane-with-pdb.out.yaml b/test/helm/gateway-helm/control-plane-with-pdb.out.yaml
index cd7ff1a53dcd..3db52f2bcbee 100644
--- a/test/helm/gateway-helm/control-plane-with-pdb.out.yaml
+++ b/test/helm/gateway-helm/control-plane-with-pdb.out.yaml
@@ -142,6 +142,7 @@ rules:
- securitypolicies
- envoyextensionpolicies
- backends
+ - httproutefilters
verbs:
- get
- list
diff --git a/test/helm/gateway-helm/default-config.out.yaml b/test/helm/gateway-helm/default-config.out.yaml
index 147c0f8ba707..0dd66b5c2099 100644
--- a/test/helm/gateway-helm/default-config.out.yaml
+++ b/test/helm/gateway-helm/default-config.out.yaml
@@ -127,6 +127,7 @@ rules:
- securitypolicies
- envoyextensionpolicies
- backends
+ - httproutefilters
verbs:
- get
- list
diff --git a/test/helm/gateway-helm/deployment-custom-topology.out.yaml b/test/helm/gateway-helm/deployment-custom-topology.out.yaml
index 7fdfef5e53e7..b9fc662515f6 100644
--- a/test/helm/gateway-helm/deployment-custom-topology.out.yaml
+++ b/test/helm/gateway-helm/deployment-custom-topology.out.yaml
@@ -127,6 +127,7 @@ rules:
- securitypolicies
- envoyextensionpolicies
- backends
+ - httproutefilters
verbs:
- get
- list
diff --git a/test/helm/gateway-helm/deployment-images-config.out.yaml b/test/helm/gateway-helm/deployment-images-config.out.yaml
index 5a2df408fe54..4da6c9a57f3c 100644
--- a/test/helm/gateway-helm/deployment-images-config.out.yaml
+++ b/test/helm/gateway-helm/deployment-images-config.out.yaml
@@ -127,6 +127,7 @@ rules:
- securitypolicies
- envoyextensionpolicies
- backends
+ - httproutefilters
verbs:
- get
- list
diff --git a/test/helm/gateway-helm/envoy-gateway-config.out.yaml b/test/helm/gateway-helm/envoy-gateway-config.out.yaml
index 233334d59721..056a0bde7d0b 100644
--- a/test/helm/gateway-helm/envoy-gateway-config.out.yaml
+++ b/test/helm/gateway-helm/envoy-gateway-config.out.yaml
@@ -129,6 +129,7 @@ rules:
- securitypolicies
- envoyextensionpolicies
- backends
+ - httproutefilters
verbs:
- get
- list
diff --git a/test/helm/gateway-helm/global-images-config.out.yaml b/test/helm/gateway-helm/global-images-config.out.yaml
index 95b98165d124..5eea20550703 100644
--- a/test/helm/gateway-helm/global-images-config.out.yaml
+++ b/test/helm/gateway-helm/global-images-config.out.yaml
@@ -131,6 +131,7 @@ rules:
- securitypolicies
- envoyextensionpolicies
- backends
+ - httproutefilters
verbs:
- get
- list