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
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
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;
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
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 h := f.GetRewrite(); h != nil {
michaelbeaumont marked this conversation as resolved.
Show resolved Hide resolved
rewrite := route.Rewrite{}

if p := h.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 {
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
}
26 changes: 26 additions & 0 deletions test/e2e/gateway/gateway_kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"encoding/base64"
"fmt"
"net"
"net/url"
"path"
"strings"
"text/template"

Expand Down Expand Up @@ -169,6 +171,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 +289,16 @@ spec:
client.FromKubernetesPod(ClientNamespace, "gateway-client"))
})

It("should rewrite HTTP requests", func() {
expectedPath := path.Join("/test", GinkgoT().Name())
targetPath := path.Join("prefix", "/test", url.PathEscape(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
14 changes: 9 additions & 5 deletions test/e2e/gateway/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,20 +42,24 @@ func ProxyTcpRequest(cluster framework.Cluster, input, expectedResponse string,
}, "60s", "1s").Should(Succeed())
}

// ProxySimpleRequests tests that basic HTTP requests are proxied to the echo-server.
func ProxySimpleRequests(cluster framework.Cluster, instance string, gateway string, opts ...client.CollectResponsesOptsFn) {
targetPath := path.Join("test", url.PathEscape(GinkgoT().Name()))
ProxyHTTPRequests(cluster, instance, gateway, targetPath, targetPath, "example.kuma.io", opts...)
}

// ProxySimpleRequests tests that basic HTTP requests are proxied to the echo-server.
func ProxyHTTPRequests(cluster framework.Cluster, instance, gateway, targetPath, expectedPath, expectedHostname string, opts ...client.CollectResponsesOptsFn) {
framework.Logf("expecting 200 response from %q", gateway)
Eventually(func(g Gomega) {
target := fmt.Sprintf("http://%s/%s",
gateway, path.Join("test", url.PathEscape(GinkgoT().Name())),
)
target := fmt.Sprintf("http://%s/%s", gateway, targetPath)

opts = append(opts, client.WithHeader("Host", "example.kuma.io"))
response, err := client.CollectResponse(cluster, "gateway-client", target, opts...)

g.Expect(err).To(Succeed())
g.Expect(response.Instance).To(Equal(instance))
g.Expect(response.Received.Headers["Host"]).To(ContainElement("example.kuma.io"))
g.Expect(response.Received.Headers["Host"]).To(ContainElement(expectedHostname))
g.Expect(response.Received.Path).To(HavePrefix(expectedPath))
}, "60s", "1s").Should(Succeed())
}

Expand Down