Skip to content

Commit

Permalink
gateway-api: Support HTTP request mirror
Browse files Browse the repository at this point in the history
[ upstream commit cda37f6 ]

All validation logic in HTTPRoute is applied the same as with the normal
backend, as backend for mirror requests is consolidated and merged with
other backends in the same HTTPRoute.

Signed-off-by: Tam Mach <tam.mach@cilium.io>
  • Loading branch information
sayboras authored and joestringer committed Aug 23, 2023
1 parent 1179c26 commit 1fd9281
Show file tree
Hide file tree
Showing 12 changed files with 338 additions and 28 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/conformance-gateway-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ jobs:
fi
echo sha=${SHA} >> $GITHUB_OUTPUT
SUPPORTED_FEATURES="ReferenceGrant,HTTPRoute,TLSRoute,HTTPRouteQueryParamMatching,HTTPRouteMethodMatching,GatewayClassObservedGenerationBump,HTTPRouteHostRewrite,HTTPRoutePathRewrite,HTTPRouteSchemeRedirect,HTTPRoutePathRedirect,HTTPRoutePortRedirect"
SUPPORTED_FEATURES="ReferenceGrant,HTTPRoute,TLSRoute,HTTPRouteQueryParamMatching,HTTPRouteMethodMatching,GatewayClassObservedGenerationBump,HTTPRouteHostRewrite,HTTPRoutePathRewrite,HTTPRouteSchemeRedirect,HTTPRoutePathRedirect,HTTPRoutePortRedirect,HTTPRouteRequestMirror"
if [ ${{ matrix.crd-channel }} == "experimental" ]; then
SUPPORTED_FEATURES+=",HTTPResponseHeaderModification,RouteDestinationPortMatching"
fi
Expand Down
7 changes: 7 additions & 0 deletions operator/pkg/gateway-api/conformance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"testing"

"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/client-go/kubernetes"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/config"
"sigs.k8s.io/gateway-api/apis/v1alpha2"
Expand Down Expand Up @@ -49,6 +50,11 @@ func TestConformance(t *testing.T) {
if err != nil {
t.Fatalf("Error initializing Kubernetes client: %v", err)
}
clientset, err := kubernetes.NewForConfig(cfg)
if err != nil {
t.Fatalf("Error initializing Kubernetes REST client: %v", err)
}

_ = v1alpha2.AddToScheme(c.Scheme())
_ = v1beta1.AddToScheme(c.Scheme())

Expand All @@ -62,6 +68,7 @@ func TestConformance(t *testing.T) {

cSuite := suite.New(suite.Options{
Client: c,
Clientset: clientset,
GatewayClassName: *flags.GatewayClassName,
Debug: *flags.ShowDebug,
CleanupBaseResources: *flags.CleanupBaseResources,
Expand Down
12 changes: 11 additions & 1 deletion operator/pkg/gateway-api/routechecks/httproute.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,19 @@ type HTTPRouteRule struct {
}

func (t *HTTPRouteRule) GetBackendRefs() []gatewayv1beta1.BackendRef {
refs := []gatewayv1beta1.BackendRef{}
var refs []gatewayv1beta1.BackendRef
for _, backend := range t.Rule.BackendRefs {
refs = append(refs, backend.BackendRef)
}
for _, f := range t.Rule.Filters {
if f.Type == gatewayv1beta1.HTTPRouteFilterRequestMirror {
if f.RequestMirror == nil {
continue
}
refs = append(refs, gatewayv1beta1.BackendRef{
BackendObjectReference: f.RequestMirror.BackendRef,
})
}
}
return refs
}
58 changes: 34 additions & 24 deletions operator/pkg/model/ingestion/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,31 +89,28 @@ func GatewayAPI(input Input) ([]model.HTTPListener, []model.TLSListener) {
var responseHeaderFilter *model.HTTPHeaderFilter
var requestRedirectFilter *model.HTTPRequestRedirectFilter
var rewriteFilter *model.HTTPURLRewriteFilter
if len(rule.Filters) > 0 {
for _, f := range rule.Filters {
if f.Type == gatewayv1beta1.HTTPRouteFilterRequestHeaderModifier {
requestHeaderFilter = &model.HTTPHeaderFilter{
HeadersToAdd: toHTTPHeaders(f.RequestHeaderModifier.Add),
HeadersToSet: toHTTPHeaders(f.RequestHeaderModifier.Set),
HeadersToRemove: f.RequestHeaderModifier.Remove,
}
var requestMirror *model.HTTPRequestMirror

for _, f := range rule.Filters {
switch f.Type {
case gatewayv1beta1.HTTPRouteFilterRequestHeaderModifier:
requestHeaderFilter = &model.HTTPHeaderFilter{
HeadersToAdd: toHTTPHeaders(f.RequestHeaderModifier.Add),
HeadersToSet: toHTTPHeaders(f.RequestHeaderModifier.Set),
HeadersToRemove: f.RequestHeaderModifier.Remove,
}

if f.Type == gatewayv1beta1.HTTPRouteFilterResponseHeaderModifier {
responseHeaderFilter = &model.HTTPHeaderFilter{
HeadersToAdd: toHTTPHeaders(f.ResponseHeaderModifier.Add),
HeadersToSet: toHTTPHeaders(f.ResponseHeaderModifier.Set),
HeadersToRemove: f.ResponseHeaderModifier.Remove,
}
}

if f.Type == gatewayv1beta1.HTTPRouteFilterRequestRedirect {
requestRedirectFilter = toHTTPRequestRedirectFilter(f.RequestRedirect)
}

if f.Type == gatewayv1beta1.HTTPRouteFilterURLRewrite {
rewriteFilter = toHTTPRewriteFilter(f.URLRewrite)
case gatewayv1beta1.HTTPRouteFilterResponseHeaderModifier:
responseHeaderFilter = &model.HTTPHeaderFilter{
HeadersToAdd: toHTTPHeaders(f.ResponseHeaderModifier.Add),
HeadersToSet: toHTTPHeaders(f.ResponseHeaderModifier.Set),
HeadersToRemove: f.ResponseHeaderModifier.Remove,
}
case gatewayv1beta1.HTTPRouteFilterRequestRedirect:
requestRedirectFilter = toHTTPRequestRedirectFilter(f.RequestRedirect)
case gatewayv1beta1.HTTPRouteFilterURLRewrite:
rewriteFilter = toHTTPRewriteFilter(f.URLRewrite)
case gatewayv1beta1.HTTPRouteFilterRequestMirror:
requestMirror = toHTTPRequestMirror(f.RequestMirror, r.Namespace)
}
}

Expand All @@ -126,6 +123,7 @@ func GatewayAPI(input Input) ([]model.HTTPListener, []model.TLSListener) {
ResponseHeaderModifier: responseHeaderFilter,
RequestRedirect: requestRedirectFilter,
Rewrite: rewriteFilter,
RequestMirror: requestMirror,
})
}

Expand All @@ -142,6 +140,7 @@ func GatewayAPI(input Input) ([]model.HTTPListener, []model.TLSListener) {
ResponseHeaderModifier: responseHeaderFilter,
RequestRedirect: requestRedirectFilter,
Rewrite: rewriteFilter,
RequestMirror: requestMirror,
})
}
}
Expand Down Expand Up @@ -281,6 +280,12 @@ func toHTTPRewriteFilter(rewrite *gatewayv1beta1.HTTPURLRewriteFilter) *model.HT
}
}

func toHTTPRequestMirror(mirror *gatewayv1beta1.HTTPRequestMirrorFilter, ns string) *model.HTTPRequestMirror {
return &model.HTTPRequestMirror{
Backend: model.AddressOf(backendRefToModelBackend(mirror.BackendRef, ns)),
}
}

func toHostname(hostname *gatewayv1beta1.Hostname) string {
if hostname != nil {
return (string)(*hostname)
Expand All @@ -298,6 +303,12 @@ func serviceExists(svcName, svcNamespace string, services []corev1.Service) bool
}

func backendToModelBackend(be gatewayv1beta1.BackendRef, defaultNamespace string) model.Backend {
res := backendRefToModelBackend(be.BackendObjectReference, defaultNamespace)
res.Weight = be.Weight
return res
}

func backendRefToModelBackend(be gatewayv1beta1.BackendObjectReference, defaultNamespace string) model.Backend {
ns := helpers.NamespaceDerefOr(be.Namespace, defaultNamespace)
var port *model.BackendPort

Expand All @@ -311,7 +322,6 @@ func backendToModelBackend(be gatewayv1beta1.BackendRef, defaultNamespace string
Name: string(be.Name),
Namespace: ns,
Port: port,
Weight: be.Weight,
}
}

Expand Down
53 changes: 53 additions & 0 deletions operator/pkg/model/ingestion/gateway_fixture_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2122,3 +2122,56 @@ var rewritePathHTTPRoutes = []gatewayv1beta1.HTTPRoute{
},
},
}

// https://github.com/kubernetes-sigs/gateway-api/blob/v0.7.1/conformance/tests/httproute-request-mirror.yaml
var mirrorPathHTTPRoutes = []gatewayv1beta1.HTTPRoute{
{
ObjectMeta: metav1.ObjectMeta{
Name: "request-mirror",
Namespace: "gateway-conformance-infra",
},
Spec: gatewayv1beta1.HTTPRouteSpec{
CommonRouteSpec: gatewayv1beta1.CommonRouteSpec{
ParentRefs: []gatewayv1beta1.ParentReference{
{
Name: "same-namespace",
},
},
},
Rules: []gatewayv1beta1.HTTPRouteRule{
{
Matches: []gatewayv1beta1.HTTPRouteMatch{
{
Path: &gatewayv1beta1.HTTPPathMatch{
Type: model.AddressOf[gatewayv1beta1.PathMatchType](gatewayv1beta1.PathMatchPathPrefix),
Value: model.AddressOf("/mirror"),
},
},
},
Filters: []gatewayv1beta1.HTTPRouteFilter{
{
Type: "RequestMirror",
RequestMirror: &gatewayv1beta1.HTTPRequestMirrorFilter{
BackendRef: gatewayv1beta1.BackendObjectReference{
Name: "infra-backend-v2",
Namespace: model.AddressOf[gatewayv1beta1.Namespace]("gateway-conformance-infra"),
Port: model.AddressOf[gatewayv1beta1.PortNumber](8080),
},
},
},
},
BackendRefs: []gatewayv1beta1.HTTPBackendRef{
{
BackendRef: gatewayv1beta1.BackendRef{
BackendObjectReference: gatewayv1beta1.BackendObjectReference{
Name: "infra-backend-v1",
Port: model.AddressOf[gatewayv1beta1.PortNumber](8080),
},
},
},
},
},
},
},
},
}
48 changes: 48 additions & 0 deletions operator/pkg/model/ingestion/gateway_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1533,6 +1533,50 @@ var rewritePathHTTPListeners = []model.HTTPListener{
},
}

var mirrorHTTPInput = Input{
GatewayClass: gatewayv1beta1.GatewayClass{},
Gateway: sameNamespaceGateway,
HTTPRoutes: mirrorPathHTTPRoutes,
Services: allServices,
}

var mirrorHTTPListeners = []model.HTTPListener{
{
Name: "http",
Sources: []model.FullyQualifiedResource{
{
Name: "same-namespace",
Namespace: "gateway-conformance-infra",
},
},
Port: 80,
Hostname: "*",
Routes: []model.HTTPRoute{
{
PathMatch: model.StringMatch{Prefix: "/mirror"},
Backends: []model.Backend{
{
Name: "infra-backend-v1",
Namespace: "gateway-conformance-infra",
Port: &model.BackendPort{
Port: 8080,
},
},
},
RequestMirror: &model.HTTPRequestMirror{
Backend: &model.Backend{
Name: "infra-backend-v2",
Namespace: "gateway-conformance-infra",
Port: &model.BackendPort{
Port: 8080,
},
},
},
},
},
},
}

func TestHTTPGatewayAPI(t *testing.T) {
tests := map[string]struct {
input Input
Expand Down Expand Up @@ -1602,6 +1646,10 @@ func TestHTTPGatewayAPI(t *testing.T) {
input: rewritePathHTTPInput,
want: rewritePathHTTPListeners,
},
"Conformance/HTTPRouteRequestMirror": {
input: mirrorHTTPInput,
want: mirrorHTTPListeners,
},
}

for name, tc := range tests {
Expand Down
9 changes: 9 additions & 0 deletions operator/pkg/model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,12 @@ type HTTPURLRewriteFilter struct {
Path *StringMatch `json:"path,omitempty"`
}

// HTTPRequestMirror defines configuration for the RequestMirror filter.
type HTTPRequestMirror struct {
// Backend is the backend handling the requests
Backend *Backend `json:"backend,omitempty"`
}

// HTTPRoute holds all the details needed to route HTTP traffic to a backend.
type HTTPRoute struct {
Name string `json:"name,omitempty"`
Expand Down Expand Up @@ -236,6 +242,9 @@ type HTTPRoute struct {

// Rewrite defines a schema for a filter that modifies the URL of the request.
Rewrite *HTTPURLRewriteFilter `json:"rewrite,omitempty"`

// RequestMirror defines a schema for a filter that mirrors HTTP requests
RequestMirror *HTTPRequestMirror `json:"request_mirror,omitempty"`
}

// GetMatchKey returns the key to be used for matching the backend.
Expand Down
19 changes: 17 additions & 2 deletions operator/pkg/model/translation/envoy_virtual_host.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ func envoyHTTPRoutes(httpRoutes []model.HTTPRoute, hostnames []string, hostNameS
if hRoutes[0].RequestRedirect != nil {
route.Action = getRouteRedirect(hRoutes[0].RequestRedirect, listenerPort)
} else {
route.Action = getRouteAction(backends, r.Rewrite)
route.Action = getRouteAction(backends, r.Rewrite, r.RequestMirror)
}
routes = append(routes, &route)
delete(matchBackendMap, r.GetMatchKey())
Expand Down Expand Up @@ -293,13 +293,28 @@ func pathFullReplaceMutation(rewrite *model.HTTPURLRewriteFilter) routeActionMut
}
}

func getRouteAction(backends []model.Backend, rewrite *model.HTTPURLRewriteFilter) *envoy_config_route_v3.Route_Route {
func requestMirrorMutation(mirror *model.HTTPRequestMirror) routeActionMutation {
return func(route *envoy_config_route_v3.Route_Route) *envoy_config_route_v3.Route_Route {
if mirror == nil || mirror.Backend == nil {
return route
}
route.Route.RequestMirrorPolicies = []*envoy_config_route_v3.RouteAction_RequestMirrorPolicy{
{
Cluster: fmt.Sprintf("%s/%s:%s", mirror.Backend.Namespace, mirror.Backend.Name, mirror.Backend.Port.GetPort()),
},
}
return route
}
}

func getRouteAction(backends []model.Backend, rewrite *model.HTTPURLRewriteFilter, mirror *model.HTTPRequestMirror) *envoy_config_route_v3.Route_Route {
var routeAction *envoy_config_route_v3.Route_Route

var mutators = []routeActionMutation{
hostRewriteMutation(rewrite),
pathPrefixMutation(rewrite),
pathFullReplaceMutation(rewrite),
requestMirrorMutation(mirror),
}

if len(backends) == 1 {
Expand Down
30 changes: 30 additions & 0 deletions operator/pkg/model/translation/envoy_virtual_host_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,33 @@ func Test_pathPrefixMutation(t *testing.T) {
require.Equal(t, res.Route.PrefixRewrite, "/prefix")
})
}

func Test_requestMirrorMutation(t *testing.T) {
t.Run("no mirror", func(t *testing.T) {
route := &envoy_config_route_v3.Route_Route{
Route: &envoy_config_route_v3.RouteAction{},
}
res := requestMirrorMutation(nil)(route)
require.Equal(t, route, res)
})

t.Run("with mirror", func(t *testing.T) {
route := &envoy_config_route_v3.Route_Route{
Route: &envoy_config_route_v3.RouteAction{},
}
mirror := &model.HTTPRequestMirror{
Backend: &model.Backend{
Name: "dummy-service",
Namespace: "default",
Port: &model.BackendPort{
Port: 8080,
Name: "http",
},
},
}

res := requestMirrorMutation(mirror)(route)
require.Len(t, res.Route.RequestMirrorPolicies, 1)
require.Equal(t, res.Route.RequestMirrorPolicies[0].Cluster, "default/dummy-service:8080")
})
}

0 comments on commit 1fd9281

Please sign in to comment.