Skip to content

Commit

Permalink
feat(gateway): support URL rewriting (#4638)
Browse files Browse the repository at this point in the history
Closes #4179, closes #3908

* feat(gateway): support URL rewriting
* test(e2e): enable experimental channel in tests
* test(e2e): add gatewy URL rewrite test
* chore(api): remove unused Messages
* refactor(gatewayapi): extract helper method
* test(e2e): fix path handling
* refactor(gateway): var naming
* feat(gateway): add validation for replacePrefixMatch

Signed-off-by: Mike Beaumont <mjboamail@gmail.com>
  • Loading branch information
michaelbeaumont authored Jul 19, 2022
1 parent ed7a22e commit fe586e6
Show file tree
Hide file tree
Showing 13 changed files with 458 additions and 137 deletions.
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
Original file line number Diff line number Diff line change
Expand Up @@ -245,10 +245,18 @@ message MeshGatewayRoute {
];
};

message Rewrite {
oneof path {
string replace_full = 1;
string replace_prefix_match = 2;
}
}

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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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 {
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading

0 comments on commit fe586e6

Please sign in to comment.