Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(gateway): support URL rewriting #4638

Merged
351 changes: 239 additions & 112 deletions api/mesh/v1alpha1/gateway_route.pb.go

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions api/mesh/v1alpha1/gateway_route.proto
Expand Up @@ -245,10 +245,18 @@ message MeshGatewayRoute {
];
};

message Rewrite {
oneof path {
string replace_full = 1;
lobkovilya marked this conversation as resolved.
Show resolved Hide resolved
string replace_prefix_match = 2;
michaelbeaumont marked this conversation as resolved.
Show resolved Hide resolved
}
}

oneof filter {
RequestHeader request_header = 1;
Mirror mirror = 2;
Redirect redirect = 3;
Rewrite rewrite = 4;
}
};

Expand Down
15 changes: 14 additions & 1 deletion pkg/core/resources/apis/mesh/gateway_route_validator.go
Expand Up @@ -101,6 +101,7 @@ func validateMeshGatewayRouteHTTPRule(
conf *mesh_proto.MeshGatewayRoute_HttpRoute_Rule,
) validators.ValidationError {
var hasRedirect bool
var hasPrefixMatch bool

if len(conf.GetMatches()) < 1 {
return validators.MakeRequiredFieldErr(path.Field("matches"))
Expand All @@ -110,14 +111,18 @@ func validateMeshGatewayRouteHTTPRule(

for i, m := range conf.GetMatches() {
err.Add(validateMeshGatewayRouteHTTPMatch(path.Field("matches").Index(i), m))

if p := m.GetPath(); p != nil && p.GetMatch() == mesh_proto.MeshGatewayRoute_HttpRoute_Match_Path_PREFIX {
hasPrefixMatch = true
}
}

for i, f := range conf.GetFilters() {
if f.GetRedirect() != nil {
hasRedirect = true
}

err.Add(validateMeshGatewayRouteHTTPFilter(path.Field("filters").Index(i), f))
err.Add(validateMeshGatewayRouteHTTPFilter(path.Field("filters").Index(i), f, hasPrefixMatch))
}

// It doesn't make sense to redirect and also mirror or rewrite request headers.
Expand Down Expand Up @@ -199,6 +204,7 @@ func validateMeshGatewayRouteHTTPMatch(
func validateMeshGatewayRouteHTTPFilter(
path validators.PathBuilder,
conf *mesh_proto.MeshGatewayRoute_HttpRoute_Filter,
hasPrefixMatch bool,
) validators.ValidationError {
var err validators.ValidationError

Expand Down Expand Up @@ -267,6 +273,13 @@ func validateMeshGatewayRouteHTTPFilter(
))
}

if m := conf.GetRewrite(); m != nil {
path := path.Field("rewrite")
if _, ok := m.GetPath().(*mesh_proto.MeshGatewayRoute_HttpRoute_Filter_Rewrite_ReplacePrefixMatch); ok && !hasPrefixMatch {
err.AddViolationAt(path.Field("replacePrefixMatch"), "cannot be used without a match on path prefix")
}
}

return err
}

Expand Down
24 changes: 24 additions & 0 deletions pkg/core/resources/apis/mesh/gateway_route_validator_test.go
Expand Up @@ -649,6 +649,30 @@ conf:
- weight: 5
destination:
kuma.io/service: target-2
`),
ErrorCase("prefix match replacement without prefix match filter", validators.Violation{
Field: "conf.http.rules[0].filters[0].rewrite.replacePrefixMatch",
Message: "cannot be used without a match on path prefix",
}, `
type: MeshGatewayRoute
name: route
mesh: default
selectors:
- match:
kuma.io/service: gateway
conf:
http:
rules:
- matches:
- path:
value: /exact_path
filters:
- rewrite:
replacePrefixMatch: "/"
backends:
- weight: 5
destination:
kuma.io/service: target-2
`),
)
})
13 changes: 13 additions & 0 deletions pkg/plugins/runtime/gateway/gateway_route_generator.go
Expand Up @@ -192,6 +192,19 @@ func makeHttpRouteEntry(name string, rule *mesh_proto.MeshGatewayRoute_HttpRoute

entry.RequestHeaders.Delete = append(
entry.RequestHeaders.Delete, h.GetRemove()...)
} else if r := f.GetRewrite(); r != nil {
rewrite := route.Rewrite{}

if p := r.GetPath(); p != nil {
switch t := p.(type) {
case *mesh_proto.MeshGatewayRoute_HttpRoute_Filter_Rewrite_ReplaceFull:
rewrite.ReplaceFullPath = &t.ReplaceFull
case *mesh_proto.MeshGatewayRoute_HttpRoute_Filter_Rewrite_ReplacePrefixMatch:
rewrite.ReplacePrefixMatch = &t.ReplacePrefixMatch
}
}

entry.Rewrite = &rewrite
}
}

Expand Down
36 changes: 36 additions & 0 deletions pkg/plugins/runtime/gateway/route/configurers.go
Expand Up @@ -323,6 +323,42 @@ func RouteActionRedirect(redirect *Redirection) RouteConfigurer {
})
}

func RouteRewrite(rewrite *Rewrite) RouteConfigurer {
if rewrite == nil {
return RouteConfigureFunc(nil)
}

return RouteConfigureFunc(func(r *envoy_config_route.Route) error {
if r.GetAction() == nil {
return errors.New("cannot configure rewrite before the route action")
}

action := r.GetRoute()

if action == nil {
return errors.New("cannot configure rewrite on a non-forwarding route")
}

if rewrite.ReplaceFullPath != nil {
action.RegexRewrite = &envoy_type_matcher.RegexMatchAndSubstitute{
Pattern: &envoy_type_matcher.RegexMatcher{
EngineType: &envoy_type_matcher.RegexMatcher_GoogleRe2{
GoogleRe2: &envoy_type_matcher.RegexMatcher_GoogleRE2{},
},
Regex: `.*`,
},
Substitution: *rewrite.ReplaceFullPath,
}
}

if rewrite.ReplacePrefixMatch != nil {
action.PrefixRewrite = *rewrite.ReplacePrefixMatch
}

return nil
})
}

// RouteActionForward configures the route to forward traffic to the
// given destinations, with the appropriate weights. This replaces any
// previous action specification.
Expand Down
8 changes: 8 additions & 0 deletions pkg/plugins/runtime/gateway/route/table.go
Expand Up @@ -36,6 +36,8 @@ type Entry struct {
// RequestHeaders specifies transformations on the HTTP
// request headers.
RequestHeaders *Headers

Rewrite *Rewrite
}

// KeyValue is a generic pairing of key and value strings. Route table
Expand Down Expand Up @@ -115,6 +117,12 @@ type Headers struct {
Delete []string
}

type Rewrite struct {
ReplaceFullPath *string

ReplacePrefixMatch *string
}

// Mirror specifies a traffic mirroring operation.
type Mirror struct {
Forward Destination
Expand Down
2 changes: 2 additions & 0 deletions pkg/plugins/runtime/gateway/route_table_generator.go
Expand Up @@ -124,6 +124,8 @@ func GenerateVirtualHost(
routeBuilder.Configure(route.RouteMirror(m.Percentage, m.Forward))
}

routeBuilder.Configure(route.RouteRewrite(e.Rewrite))

vh.Configure(route.VirtualHostRoute(&routeBuilder))
}

Expand Down
Expand Up @@ -59,14 +59,14 @@ func (r *HTTPRouteReconciler) gapiToKumaRule(
var filters []*mesh_proto.MeshGatewayRoute_HttpRoute_Filter

for _, filter := range rule.Filters {
kumaFilter, filterCondition, err := r.gapiToKumaFilter(ctx, mesh, route.Namespace, filter)
kumaFilters, filterCondition, err := r.gapiToKumaFilters(ctx, mesh, route.Namespace, filter)
if err != nil {
return nil, condition, err
}
if filterCondition != nil {
condition = filterCondition
} else {
filters = append(filters, kumaFilter)
filters = append(filters, kumaFilters...)
}
}

Expand Down Expand Up @@ -296,10 +296,30 @@ func gapiToKumaMatch(match gatewayapi.HTTPRouteMatch) (*mesh_proto.MeshGatewayRo
return kumaMatch, nil
}

func (r *HTTPRouteReconciler) gapiToKumaFilter(
func pathRewriteToKuma(modifier gatewayapi.HTTPPathModifier) mesh_proto.MeshGatewayRoute_HttpRoute_Filter {
rewrite := mesh_proto.MeshGatewayRoute_HttpRoute_Filter_Rewrite{}

switch modifier.Type {
case gatewayapi.FullPathHTTPPathModifier:
rewrite.Path = &mesh_proto.MeshGatewayRoute_HttpRoute_Filter_Rewrite_ReplaceFull{
ReplaceFull: *modifier.ReplaceFullPath,
}
case gatewayapi.PrefixMatchHTTPPathModifier:
rewrite.Path = &mesh_proto.MeshGatewayRoute_HttpRoute_Filter_Rewrite_ReplacePrefixMatch{
ReplacePrefixMatch: *modifier.ReplacePrefixMatch,
}
}
return mesh_proto.MeshGatewayRoute_HttpRoute_Filter{
Filter: &mesh_proto.MeshGatewayRoute_HttpRoute_Filter_Rewrite_{
Rewrite: &rewrite,
},
}
}

func (r *HTTPRouteReconciler) gapiToKumaFilters(
ctx context.Context, mesh string, namespace string, filter gatewayapi.HTTPRouteFilter,
) (*mesh_proto.MeshGatewayRoute_HttpRoute_Filter, *ResolvedRefsConditionFalse, error) {
var kumaFilter *mesh_proto.MeshGatewayRoute_HttpRoute_Filter
) ([]*mesh_proto.MeshGatewayRoute_HttpRoute_Filter, *ResolvedRefsConditionFalse, error) {
var kumaFilters []*mesh_proto.MeshGatewayRoute_HttpRoute_Filter

var condition *ResolvedRefsConditionFalse

Expand All @@ -319,11 +339,11 @@ func (r *HTTPRouteReconciler) gapiToKumaFilter(

requestHeader.Remove = filter.Remove

kumaFilter = &mesh_proto.MeshGatewayRoute_HttpRoute_Filter{
kumaFilters = append(kumaFilters, &mesh_proto.MeshGatewayRoute_HttpRoute_Filter{
Filter: &mesh_proto.MeshGatewayRoute_HttpRoute_Filter_RequestHeader_{
RequestHeader: &requestHeader,
},
}
})
case gatewayapi.HTTPRouteFilterRequestMirror:
filter := filter.RequestMirror

Expand All @@ -342,14 +362,19 @@ func (r *HTTPRouteReconciler) gapiToKumaFilter(
Percentage: util_proto.Double(100),
}

kumaFilter = &mesh_proto.MeshGatewayRoute_HttpRoute_Filter{
kumaFilters = append(kumaFilters, &mesh_proto.MeshGatewayRoute_HttpRoute_Filter{
Filter: &mesh_proto.MeshGatewayRoute_HttpRoute_Filter_Mirror_{
Mirror: &mirror,
},
}
})
case gatewayapi.HTTPRouteFilterRequestRedirect:
filter := filter.RequestRedirect

if p := filter.Path; p != nil {
michaelbeaumont marked this conversation as resolved.
Show resolved Hide resolved
filter := pathRewriteToKuma(*p)
kumaFilters = append(kumaFilters, &filter)
}

redirect := mesh_proto.MeshGatewayRoute_HttpRoute_Filter_Redirect{}

if s := filter.Scheme; s != nil {
Expand All @@ -368,14 +393,34 @@ func (r *HTTPRouteReconciler) gapiToKumaFilter(
redirect.StatusCode = uint32(*sc)
}

kumaFilter = &mesh_proto.MeshGatewayRoute_HttpRoute_Filter{
kumaFilters = append(kumaFilters, &mesh_proto.MeshGatewayRoute_HttpRoute_Filter{
Filter: &mesh_proto.MeshGatewayRoute_HttpRoute_Filter_Redirect_{
Redirect: &redirect,
},
})
case gatewayapi.HTTPRouteFilterURLRewrite:
filter := filter.URLRewrite

if filter.Hostname != nil {
var requestHeader mesh_proto.MeshGatewayRoute_HttpRoute_Filter_RequestHeader
requestHeader.Set = append(requestHeader.Set, &mesh_proto.MeshGatewayRoute_HttpRoute_Filter_RequestHeader_Header{
Name: "Host",
Value: string(*filter.Hostname),
})
kumaFilters = append(kumaFilters, &mesh_proto.MeshGatewayRoute_HttpRoute_Filter{
Filter: &mesh_proto.MeshGatewayRoute_HttpRoute_Filter_RequestHeader_{
RequestHeader: &requestHeader,
},
})
}

if p := filter.Path; p != nil {
filter := pathRewriteToKuma(*p)
kumaFilters = append(kumaFilters, &filter)
}
default:
return nil, nil, fmt.Errorf("unsupported filter type %q", filter.Type)
}

return kumaFilter, condition, nil
return kumaFilters, condition, nil
}
25 changes: 25 additions & 0 deletions test/e2e/gateway/gateway_kubernetes.go
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/base64"
"fmt"
"net"
"path"
"strings"
"text/template"

Expand Down Expand Up @@ -169,6 +170,20 @@ spec:
conf:
http:
rules:
- matches:
- path:
match: PREFIX
value: /prefix/
filters:
- requestHeader:
set:
- name: Host
value: other.example.kuma.io
- rewrite:
replacePrefixMatch: "/"
backends:
- destination:
kuma.io/service: echo-server_kuma-test_svc_80 # Matches the echo-server we deployed.
- matches:
- path:
match: PREFIX
Expand Down Expand Up @@ -273,6 +288,16 @@ spec:
client.FromKubernetesPod(ClientNamespace, "gateway-client"))
})

It("should rewrite HTTP requests", func() {
expectedPath := path.Join("/test", GinkgoT().Name())
targetPath := path.Join("prefix", "/test", GinkgoT().Name())
expectedHostname := "other.example.kuma.io"
ProxyHTTPRequests(cluster, "kubernetes",
net.JoinHostPort(GatewayAddress("edge-gateway"), GatewayPort),
targetPath, expectedPath, expectedHostname,
client.FromKubernetesPod(ClientNamespace, "gateway-client"))
})

It("should proxy TCP connections", func() {
ProxyTcpRequest(cluster, "request", "response",
net.JoinHostPort(GatewayAddress("edge-gateway"), "8081"),
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/gateway/gatewayapi/gateway_api.go
Expand Up @@ -18,7 +18,7 @@ func GatewayAPICRDs(cluster Cluster) error {
return k8s.RunKubectlE(
cluster.GetTesting(),
cluster.GetKubectlOptions(),
"apply", "-f", "https://github.com/kubernetes-sigs/gateway-api/releases/download/v0.5.0/standard-install.yaml")
"apply", "-f", "https://github.com/kubernetes-sigs/gateway-api/releases/download/v0.5.0/experimental-install.yaml")
}

const GatewayClass = `
Expand Down