diff --git a/internal/mode/static/nginx/config/http/config.go b/internal/mode/static/nginx/config/http/config.go index b81a6771b8..d79fe708e9 100644 --- a/internal/mode/static/nginx/config/http/config.go +++ b/internal/mode/static/nginx/config/http/config.go @@ -5,8 +5,9 @@ import ( ) const ( - InternalRoutePathPrefix = "/_ngf-internal" - HTTPSScheme = "https" + InternalRoutePathPrefix = "/_ngf-internal" + InternalMirrorRoutePathPrefix = InternalRoutePathPrefix + "-mirror" + HTTPSScheme = "https" ) // Server holds all configuration for an HTTP server. @@ -41,6 +42,7 @@ type Location struct { Return *Return ResponseHeaders ResponseHeaders Rewrites []string + MirrorPaths []string Includes []shared.Include GRPC bool } diff --git a/internal/mode/static/nginx/config/servers.go b/internal/mode/static/nginx/config/servers.go index f7d76e2e80..d4f8d9b14f 100644 --- a/internal/mode/static/nginx/config/servers.go +++ b/internal/mode/static/nginx/config/servers.go @@ -237,7 +237,7 @@ func createLocations( matchPairs := make(httpMatchPairs) var rootPathExists bool - var grpc bool + var grpcServer bool for pathRuleIdx, rule := range server.PathRules { matches := make([]routeMatch, 0, len(rule.MatchRules)) @@ -247,7 +247,7 @@ func createLocations( } if rule.GRPC { - grpc = true + grpcServer = true } extLocations := initializeExternalLocations(rule, pathsAndTypes) @@ -277,7 +277,7 @@ func createLocations( internalLocations := make([]http.Location, 0, len(rule.MatchRules)) for matchRuleIdx, r := range rule.MatchRules { - intLocation, match := initializeInternalLocation(pathRuleIdx, matchRuleIdx, r.Match, grpc) + intLocation, match := initializeInternalLocation(pathRuleIdx, matchRuleIdx, r.Match, rule.GRPC) intLocation.Includes = createIncludesFromPolicyGenerateResult( generator.GenerateForInternalLocation(rule.Policies), ) @@ -313,7 +313,7 @@ func createLocations( locs = append(locs, createDefaultRootLocation()) } - return locs, matchPairs, grpc + return locs, matchPairs, grpcServer } func needsInternalLocations(rule dataplane.PathRule) bool { @@ -433,6 +433,13 @@ func updateLocation( return location } + if strings.HasPrefix(path, http.InternalMirrorRoutePathPrefix) { + location.Type = http.InternalLocationType + if grpc { + location.Rewrites = []string{"^ $request_uri break"} + } + } + location.Includes = append(location.Includes, createIncludesFromLocationSnippetsFilters(filters.SnippetsFilters)...) if filters.RequestRedirect != nil { @@ -445,6 +452,20 @@ func updateLocation( } rewrites := createRewritesValForRewriteFilter(filters.RequestURLRewrite, path) + if rewrites != nil { + if location.Type == http.InternalLocationType && rewrites.InternalRewrite != "" { + location.Rewrites = append(location.Rewrites, rewrites.InternalRewrite) + } + if rewrites.MainRewrite != "" { + location.Rewrites = append(location.Rewrites, rewrites.MainRewrite) + } + } + + for _, filter := range filters.RequestMirrors { + if filter.Target != nil { + location.MirrorPaths = append(location.MirrorPaths, *filter.Target) + } + } extraHeaders := make([]http.Header, 0, 3) if grpc { @@ -457,15 +478,6 @@ func updateLocation( proxySetHeaders := generateProxySetHeaders(&matchRule.Filters, createBaseProxySetHeaders(extraHeaders...)) responseHeaders := generateResponseHeaders(&matchRule.Filters) - if rewrites != nil { - if location.Type == http.InternalLocationType && rewrites.InternalRewrite != "" { - location.Rewrites = append(location.Rewrites, rewrites.InternalRewrite) - } - if rewrites.MainRewrite != "" { - location.Rewrites = append(location.Rewrites, rewrites.MainRewrite) - } - } - location.ProxySetHeaders = proxySetHeaders location.ProxySSLVerify = createProxyTLSFromBackends(matchRule.BackendGroup.Backends) proxyPass := createProxyPass( diff --git a/internal/mode/static/nginx/config/servers_template.go b/internal/mode/static/nginx/config/servers_template.go index c3873ef105..6676426b86 100644 --- a/internal/mode/static/nginx/config/servers_template.go +++ b/internal/mode/static/nginx/config/servers_template.go @@ -101,6 +101,10 @@ server { rewrite {{ $r }}; {{- end }} + {{- range $m := $l.MirrorPaths }} + mirror {{ $m }}; + {{- end }} + {{- if $l.Return }} return {{ $l.Return.Code }} "{{ $l.Return.Body }}"; {{- end }} diff --git a/internal/mode/static/nginx/config/servers_test.go b/internal/mode/static/nginx/config/servers_test.go index 56da44f1fa..70991ecb54 100644 --- a/internal/mode/static/nginx/config/servers_test.go +++ b/internal/mode/static/nginx/config/servers_test.go @@ -7,6 +7,7 @@ import ( "testing" . "github.com/onsi/gomega" + "github.com/onsi/gomega/format" "k8s.io/apimachinery/pkg/types" "github.com/nginx/nginx-gateway-fabric/internal/framework/helpers" @@ -862,6 +863,66 @@ func TestCreateServers(t *testing.T) { }, }, }, + { + Path: "/mirror", + PathType: dataplane.PathTypePrefix, + MatchRules: []dataplane.MatchRule{ + { + Match: dataplane.Match{}, + Filters: dataplane.HTTPFilters{ + RequestMirrors: []*dataplane.HTTPRequestMirrorFilter{ + { + Name: helpers.GetPointer("mirror-filter"), + Namespace: helpers.GetPointer("test-ns"), + Target: helpers.GetPointer(http.InternalMirrorRoutePathPrefix + "-my-backend"), + }, + }, + }, + BackendGroup: fooGroup, + }, + }, + }, + { + Path: http.InternalMirrorRoutePathPrefix + "-my-backend", + PathType: dataplane.PathTypeExact, + MatchRules: []dataplane.MatchRule{ + { + Match: dataplane.Match{}, + BackendGroup: fooGroup, + }, + }, + }, + { + Path: "/grpc/mirror", + PathType: dataplane.PathTypeExact, + MatchRules: []dataplane.MatchRule{ + { + Match: dataplane.Match{}, + Filters: dataplane.HTTPFilters{ + RequestMirrors: []*dataplane.HTTPRequestMirrorFilter{ + { + Name: helpers.GetPointer("grpc-mirror-filter"), + Namespace: helpers.GetPointer("test-ns"), + Target: helpers.GetPointer(http.InternalMirrorRoutePathPrefix + "-my-grpc-backend"), + }, + }, + }, + BackendGroup: fooGroup, + }, + }, + GRPC: true, + }, + { + Path: http.InternalMirrorRoutePathPrefix + "-my-grpc-backend", + PathType: dataplane.PathTypeExact, + MatchRules: []dataplane.MatchRule{ + { + Match: dataplane.Match{}, + BackendGroup: fooGroup, + }, + }, + GRPC: true, + }, { Path: "/invalid-filter", PathType: dataplane.PathTypePrefix, @@ -1093,25 +1154,25 @@ func TestCreateServers(t *testing.T) { RedirectPath: "/_ngf-internal-rule8-route0", }, }, - "1_10": { + "1_14": { { Headers: []string{"filter:Exact:this"}, - RedirectPath: "/_ngf-internal-rule10-route0", + RedirectPath: "/_ngf-internal-rule14-route0", }, }, - "1_12": { + "1_16": { { Method: "GET", - RedirectPath: "/_ngf-internal-rule12-route0", + RedirectPath: "/_ngf-internal-rule16-route0", Headers: nil, QueryParams: nil, Any: false, }, }, - "1_17": { + "1_21": { { Method: "GET", - RedirectPath: "/_ngf-internal-rule17-route0", + RedirectPath: "/_ngf-internal-rule21-route0", }, }, } @@ -1342,6 +1403,47 @@ func TestCreateServers(t *testing.T) { Type: http.InternalLocationType, Includes: internalIncludes, }, + { + Path: "/mirror/", + ProxyPass: "http://test_foo_80$request_uri", + ProxySetHeaders: httpBaseHeaders, + MirrorPaths: []string{"/_ngf-internal-mirror-my-backend"}, + Type: http.ExternalLocationType, + Includes: externalIncludes, + }, + { + Path: "= /mirror", + ProxyPass: "http://test_foo_80$request_uri", + ProxySetHeaders: httpBaseHeaders, + MirrorPaths: []string{"/_ngf-internal-mirror-my-backend"}, + Type: http.ExternalLocationType, + Includes: externalIncludes, + }, + { + Path: "= /_ngf-internal-mirror-my-backend", + ProxyPass: "http://test_foo_80$request_uri", + ProxySetHeaders: httpBaseHeaders, + Type: http.InternalLocationType, + Includes: externalIncludes, + }, + { + Path: "= /grpc/mirror", + GRPC: true, + ProxyPass: "grpc://test_foo_80", + ProxySetHeaders: grpcBaseHeaders, + MirrorPaths: []string{"/_ngf-internal-mirror-my-grpc-backend"}, + Type: http.ExternalLocationType, + Includes: externalIncludes, + }, + { + Path: "= /_ngf-internal-mirror-my-grpc-backend", + GRPC: true, + ProxyPass: "grpc://test_foo_80", + Rewrites: []string{"^ $request_uri break"}, + ProxySetHeaders: grpcBaseHeaders, + Type: http.InternalLocationType, + Includes: externalIncludes, + }, { Path: "/invalid-filter/", Return: &http.Return{ @@ -1360,18 +1462,18 @@ func TestCreateServers(t *testing.T) { }, { Path: "/invalid-filter-with-headers/", - HTTPMatchKey: ssl + "1_10", + HTTPMatchKey: ssl + "1_14", Type: http.RedirectLocationType, Includes: externalIncludes, }, { Path: "= /invalid-filter-with-headers", - HTTPMatchKey: ssl + "1_10", + HTTPMatchKey: ssl + "1_14", Type: http.RedirectLocationType, Includes: externalIncludes, }, { - Path: "/_ngf-internal-rule10-route0", + Path: "/_ngf-internal-rule14-route0", Return: &http.Return{ Code: http.StatusInternalServerError, }, @@ -1387,12 +1489,12 @@ func TestCreateServers(t *testing.T) { }, { Path: "= /test", - HTTPMatchKey: ssl + "1_12", + HTTPMatchKey: ssl + "1_16", Type: http.RedirectLocationType, Includes: externalIncludes, }, { - Path: "/_ngf-internal-rule12-route0", + Path: "/_ngf-internal-rule16-route0", ProxyPass: "http://test_foo_80$request_uri", ProxySetHeaders: httpBaseHeaders, Type: http.InternalLocationType, @@ -1471,15 +1573,14 @@ func TestCreateServers(t *testing.T) { }, { Path: "= /include-header-match", - HTTPMatchKey: ssl + "1_17", + HTTPMatchKey: ssl + "1_21", Type: http.RedirectLocationType, Includes: externalIncludes, }, { - Path: "/_ngf-internal-rule17-route0", + Path: "/_ngf-internal-rule21-route0", ProxyPass: "http://test_foo_80$request_uri", ProxySetHeaders: httpBaseHeaders, - Rewrites: []string{"^ $request_uri break"}, Type: http.InternalLocationType, Includes: internalIncludes, }, @@ -1572,6 +1673,7 @@ func TestCreateServers(t *testing.T) { result, httpMatchPair := createServers(conf, fakeGenerator, keepAliveCheck) + format.MaxLength = 10000 g.Expect(httpMatchPair).To(Equal(allExpMatchPair)) g.Expect(helpers.Diff(expectedServers, result)).To(BeEmpty()) } diff --git a/internal/mode/static/nginx/config/validation/http_validator.go b/internal/mode/static/nginx/config/validation/http_validator.go index 29da381ad6..a17cfdaa00 100644 --- a/internal/mode/static/nginx/config/validation/http_validator.go +++ b/internal/mode/static/nginx/config/validation/http_validator.go @@ -15,4 +15,6 @@ type HTTPValidator struct { HTTPPathValidator } +func (HTTPValidator) SkipValidation() bool { return false } + var _ validation.HTTPFieldsValidator = HTTPValidator{} diff --git a/internal/mode/static/state/change_processor_test.go b/internal/mode/static/state/change_processor_test.go index 18d3d044cf..d71ebd5212 100644 --- a/internal/mode/static/state/change_processor_test.go +++ b/internal/mode/static/state/change_processor_test.go @@ -1029,19 +1029,22 @@ var _ = Describe("ChangeProcessor", func() { // no ref grant exists yet for the routes expGraph.Routes[httpRouteKey1].Conditions = []conditions.Condition{ staticConds.NewRouteBackendRefRefNotPermitted( - "Backend ref to Service service-ns/service not permitted by any ReferenceGrant", + "spec.rules[0].backendRefs[0].namespace: Forbidden: " + + "Backend ref to Service service-ns/service not permitted by any ReferenceGrant", ), } expGraph.Routes[grpcRouteKey1].Conditions = []conditions.Condition{ staticConds.NewRouteBackendRefRefNotPermitted( - "Backend ref to Service grpc-service-ns/grpc-service not permitted by any ReferenceGrant", + "spec.rules[0].backendRefs[0].namespace: Forbidden: Backend ref to Service " + + "grpc-service-ns/grpc-service not permitted by any ReferenceGrant", ), } expGraph.L4Routes[trKey1].Conditions = []conditions.Condition{ staticConds.NewRouteBackendRefRefNotPermitted( - "Backend ref to Service tls-service-ns/tls-service not permitted by any ReferenceGrant", + "spec.rules[0].backendRefs[0].namespace: Forbidden: Backend ref to Service " + + "tls-service-ns/tls-service not permitted by any ReferenceGrant", ), } @@ -1118,7 +1121,8 @@ var _ = Describe("ChangeProcessor", func() { expGraph.Routes[httpRouteKey1].Conditions = []conditions.Condition{ staticConds.NewRouteInvalidListener(), staticConds.NewRouteBackendRefRefNotPermitted( - "Backend ref to Service service-ns/service not permitted by any ReferenceGrant", + "spec.rules[0].backendRefs[0].namespace: Forbidden: Backend ref to Service " + + "service-ns/service not permitted by any ReferenceGrant", ), } expGraph.Routes[httpRouteKey1].ParentRefs[0].Attachment = expAttachment80 @@ -1128,7 +1132,8 @@ var _ = Describe("ChangeProcessor", func() { expGraph.Routes[grpcRouteKey1].Conditions = []conditions.Condition{ staticConds.NewRouteInvalidListener(), staticConds.NewRouteBackendRefRefNotPermitted( - "Backend ref to Service grpc-service-ns/grpc-service not permitted by any ReferenceGrant", + "spec.rules[0].backendRefs[0].namespace: Forbidden: Backend ref to Service " + + "grpc-service-ns/grpc-service not permitted by any ReferenceGrant", ), } expGraph.Routes[grpcRouteKey1].ParentRefs[0].Attachment = expAttachment80 @@ -1137,7 +1142,8 @@ var _ = Describe("ChangeProcessor", func() { // no ref grant exists yet for tr1 expGraph.L4Routes[trKey1].Conditions = []conditions.Condition{ staticConds.NewRouteBackendRefRefNotPermitted( - "Backend ref to Service tls-service-ns/tls-service not permitted by any ReferenceGrant", + "spec.rules[0].backendRefs[0].namespace: Forbidden: Backend ref to Service " + + "tls-service-ns/tls-service not permitted by any ReferenceGrant", ), } @@ -1158,21 +1164,24 @@ var _ = Describe("ChangeProcessor", func() { // no ref grant exists yet for hr1 expGraph.Routes[httpRouteKey1].Conditions = []conditions.Condition{ staticConds.NewRouteBackendRefRefNotPermitted( - "Backend ref to Service service-ns/service not permitted by any ReferenceGrant", + "spec.rules[0].backendRefs[0].namespace: Forbidden: Backend ref to Service " + + "service-ns/service not permitted by any ReferenceGrant", ), } // no ref grant exists yet for gr1 expGraph.Routes[grpcRouteKey1].Conditions = []conditions.Condition{ staticConds.NewRouteBackendRefRefNotPermitted( - "Backend ref to Service grpc-service-ns/grpc-service not permitted by any ReferenceGrant", + "spec.rules[0].backendRefs[0].namespace: Forbidden: Backend ref to Service " + + "grpc-service-ns/grpc-service not permitted by any ReferenceGrant", ), } // no ref grant exists yet for tr1 expGraph.L4Routes[trKey1].Conditions = []conditions.Condition{ staticConds.NewRouteBackendRefRefNotPermitted( - "Backend ref to Service tls-service-ns/tls-service not permitted by any ReferenceGrant", + "spec.rules[0].backendRefs[0].namespace: Forbidden: Backend ref to Service " + + "tls-service-ns/tls-service not permitted by any ReferenceGrant", ), } @@ -1203,7 +1212,8 @@ var _ = Describe("ChangeProcessor", func() { // no ref grant exists yet for gr1 expGraph.Routes[grpcRouteKey1].Conditions = []conditions.Condition{ staticConds.NewRouteBackendRefRefNotPermitted( - "Backend ref to Service grpc-service-ns/grpc-service not permitted by any ReferenceGrant", + "spec.rules[0].backendRefs[0].namespace: Forbidden: Backend ref to Service " + + "grpc-service-ns/grpc-service not permitted by any ReferenceGrant", ), } delete(expGraph.ReferencedServices, refGRPCSvc) @@ -1212,7 +1222,8 @@ var _ = Describe("ChangeProcessor", func() { // no ref grant exists yet for tr1 expGraph.L4Routes[trKey1].Conditions = []conditions.Condition{ staticConds.NewRouteBackendRefRefNotPermitted( - "Backend ref to Service tls-service-ns/tls-service not permitted by any ReferenceGrant", + "spec.rules[0].backendRefs[0].namespace: Forbidden: Backend ref to Service " + + "tls-service-ns/tls-service not permitted by any ReferenceGrant", ), } delete(expGraph.ReferencedServices, refTLSSvc) @@ -1240,7 +1251,8 @@ var _ = Describe("ChangeProcessor", func() { // no ref grant exists yet for tr1 expGraph.L4Routes[trKey1].Conditions = []conditions.Condition{ staticConds.NewRouteBackendRefRefNotPermitted( - "Backend ref to Service tls-service-ns/tls-service not permitted by any ReferenceGrant", + "spec.rules[0].backendRefs[0].namespace: Forbidden: Backend ref to Service " + + "tls-service-ns/tls-service not permitted by any ReferenceGrant", ), } delete(expGraph.ReferencedServices, types.NamespacedName{Namespace: "tls-service-ns", Name: "tls-service"}) diff --git a/internal/mode/static/state/dataplane/configuration.go b/internal/mode/static/state/dataplane/configuration.go index d3c86fcb3e..84dc4b0cdb 100644 --- a/internal/mode/static/state/dataplane/configuration.go +++ b/internal/mode/static/state/dataplane/configuration.go @@ -345,6 +345,10 @@ func newBackendGroup(refs []graph.BackendRef, sourceNsName types.NamespacedName, } for _, ref := range refs { + if ref.IsMirrorBackend { + continue + } + backends = append(backends, Backend{ UpstreamName: ref.ServicePortReference(), Weight: ref.Weight, @@ -506,14 +510,14 @@ func (hpr *hostPathRules) upsertRoute( } } - for i, rule := range route.Spec.Rules { + for idx, rule := range route.Spec.Rules { if !rule.ValidMatches { continue } var filters HTTPFilters if rule.Filters.Valid { - filters = createHTTPFilters(rule.Filters.Filters) + filters = createHTTPFilters(rule.Filters.Filters, idx) } else { filters = HTTPFilters{ InvalidFilter: &InvalidHTTPFilter{}, @@ -544,7 +548,7 @@ func (hpr *hostPathRules) upsertRoute( hostRule.MatchRules = append(hostRule.MatchRules, MatchRule{ Source: objectSrc, - BackendGroup: newBackendGroup(rule.BackendRefs, routeNsName, i), + BackendGroup: newBackendGroup(rule.BackendRefs, routeNsName, idx), Filters: filters, Match: convertMatch(m), }) @@ -741,7 +745,7 @@ func getPath(path *v1.HTTPPathMatch) string { return *path.Value } -func createHTTPFilters(filters []graph.Filter) HTTPFilters { +func createHTTPFilters(filters []graph.Filter, ruleIdx int) HTTPFilters { var result HTTPFilters for _, f := range filters { @@ -756,6 +760,11 @@ func createHTTPFilters(filters []graph.Filter) HTTPFilters { // using the first filter result.RequestURLRewrite = convertHTTPURLRewriteFilter(f.URLRewrite) } + case graph.FilterRequestMirror: + result.RequestMirrors = append( + result.RequestMirrors, + convertHTTPRequestMirrorFilter(f.RequestMirror, ruleIdx), + ) case graph.FilterRequestHeaderModifier: if result.RequestHeaderModifiers == nil { // using the first filter diff --git a/internal/mode/static/state/dataplane/configuration_test.go b/internal/mode/static/state/dataplane/configuration_test.go index d8219a1c9a..b13a7845a5 100644 --- a/internal/mode/static/state/dataplane/configuration_test.go +++ b/internal/mode/static/state/dataplane/configuration_test.go @@ -668,6 +668,52 @@ func TestBuildConfiguration(t *testing.T) { l7HTTPSRouteWithPolicy.Policies = []*graph.Policy{hrPolicy2, invalidPolicy} + hrWithMirror, expHRWithMirrorGroups, routeHRWithMirror := createTestResources( + "hr-with-mirror", + "foo.example.com", + "listener-80-1", + pathAndType{ + path: "/mirror", + pathType: prefix, + }, + ) + + mirrorUpstreamName := "test_mirror-backend_80" + mirrorUpstream := Upstream{ + Name: mirrorUpstreamName, + Endpoints: []resolver.Endpoint{ + { + Address: "10.0.0.1", + Port: 8080, + }, + }, + } + + fakeResolver.ResolveStub = func( + _ context.Context, + nsName types.NamespacedName, + _ apiv1.ServicePort, + _ []discoveryV1.AddressType, + ) ([]resolver.Endpoint, error) { + if nsName.Name == "mirror-backend" { + return mirrorUpstream.Endpoints, nil + } + return fooEndpoints, nil + } + + addFilters(routeHRWithMirror, []graph.Filter{ + { + FilterType: graph.FilterRequestMirror, + RequestMirror: &v1.HTTPRequestMirrorFilter{ + BackendRef: v1.BackendObjectReference{ + Group: helpers.GetPointer(v1.Group("core")), + Kind: helpers.GetPointer(v1.Kind("Service")), + Name: v1.ObjectName("mirror-backend"), + }, + }, + }, + }) + secret1NsName := types.NamespacedName{Namespace: "test", Name: "secret-1"} secret1 := &graph.Secret{ Source: &apiv1.Secret{ @@ -2057,6 +2103,56 @@ func TestBuildConfiguration(t *testing.T) { }), msg: "https listener with httproute with backend that has a backend TLS policy with binaryData attached", }, + { + graph: getModifiedGraph(func(g *graph.Graph) *graph.Graph { + g.Gateway.Listeners = append(g.Gateway.Listeners, &graph.Listener{ + Name: "listener-80-1", + Source: listener80, + Valid: true, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(hrWithMirror): routeHRWithMirror, + }, + }) + g.Routes = map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(hrWithMirror): routeHRWithMirror, + } + return g + }), + expConf: getModifiedExpectedConfiguration(func(conf Configuration) Configuration { + conf.HTTPServers = append(conf.HTTPServers, []VirtualServer{ + { + Hostname: "foo.example.com", + PathRules: []PathRule{ + { + Path: "/mirror", + PathType: PathTypePrefix, + MatchRules: []MatchRule{ + { + BackendGroup: expHRWithMirrorGroups[0], + Source: &hrWithMirror.ObjectMeta, + Filters: HTTPFilters{ + RequestMirrors: []*HTTPRequestMirrorFilter{ + { + Name: helpers.GetPointer("mirror-backend"), + Target: helpers.GetPointer("/_ngf-internal-mirror-mirror-backend-0"), + }, + }, + }, + }, + }, + }, + }, + Port: 80, + }, + }...) + conf.SSLServers = []VirtualServer{} + conf.Upstreams = []Upstream{fooUpstream} + conf.BackendGroups = []BackendGroup{expHRWithMirrorGroups[0]} + conf.SSLKeyPairs = map[SSLKeyPairID]SSLKeyPair{} + return conf + }), + msg: "one http listener with one route containing a request mirror filter", + }, { graph: getModifiedGraph(func(g *graph.Graph) *graph.Graph { g.Gateway.Source.ObjectMeta = metav1.ObjectMeta{ @@ -2562,6 +2658,22 @@ func TestBuildConfiguration_Plus(t *testing.T) { } } +func TestNewBackendGroup_Mirror(t *testing.T) { + t.Parallel() + g := NewWithT(t) + + backendRef := graph.BackendRef{ + SvcNsName: types.NamespacedName{Name: "mirror-backend", Namespace: "test"}, + ServicePort: apiv1.ServicePort{Port: 80}, + Valid: true, + IsMirrorBackend: true, + } + + group := newBackendGroup([]graph.BackendRef{backendRef}, types.NamespacedName{}, 0) + + g.Expect(group.Backends).To(BeEmpty()) +} + func TestGetPath(t *testing.T) { t.Parallel() tests := []struct { @@ -2628,6 +2740,26 @@ func TestCreateFilters(t *testing.T) { Hostname: helpers.GetPointer[v1.PreciseHostname]("bar.example.com"), }, } + mirror1 := graph.Filter{ + FilterType: graph.FilterRequestMirror, + RequestMirror: &v1.HTTPRequestMirrorFilter{ + BackendRef: v1.BackendObjectReference{ + Group: helpers.GetPointer(v1.Group("core")), + Kind: helpers.GetPointer(v1.Kind("Service")), + Name: v1.ObjectName("mirror-backend"), + }, + }, + } + mirror2 := graph.Filter{ + FilterType: graph.FilterRequestMirror, + RequestMirror: &v1.HTTPRequestMirrorFilter{ + BackendRef: v1.BackendObjectReference{ + Group: helpers.GetPointer(v1.Group("core")), + Kind: helpers.GetPointer(v1.Kind("Service")), + Name: v1.ObjectName("mirror-backend2"), + }, + }, + } requestHeaderModifiers1 := graph.Filter{ FilterType: graph.FilterRequestHeaderModifier, RequestHeaderModifier: &v1.HTTPHeaderFilter{ @@ -2681,6 +2813,16 @@ func TestCreateFilters(t *testing.T) { expectedRewrite1 := HTTPURLRewriteFilter{ Hostname: helpers.GetPointer("foo.example.com"), } + + expectedMirror1 := HTTPRequestMirrorFilter{ + Name: helpers.GetPointer("mirror-backend"), + Target: helpers.GetPointer("/_ngf-internal-mirror-mirror-backend-0"), + } + expectedMirror2 := HTTPRequestMirrorFilter{ + Name: helpers.GetPointer("mirror-backend2"), + Target: helpers.GetPointer("/_ngf-internal-mirror-mirror-backend2-0"), + } + expectedHeaderModifier1 := HTTPHeaderFilter{ Set: []HTTPHeader{ { @@ -2780,6 +2922,8 @@ func TestCreateFilters(t *testing.T) { redirect2, rewrite1, rewrite2, + mirror1, + mirror2, requestHeaderModifiers1, requestHeaderModifiers2, responseHeaderModifiers1, @@ -2788,8 +2932,12 @@ func TestCreateFilters(t *testing.T) { snippetsFilter2, }, expected: HTTPFilters{ - RequestRedirect: &expectedRedirect1, - RequestURLRewrite: &expectedRewrite1, + RequestRedirect: &expectedRedirect1, + RequestURLRewrite: &expectedRewrite1, + RequestMirrors: []*HTTPRequestMirrorFilter{ + &expectedMirror1, + &expectedMirror2, + }, RequestHeaderModifiers: &expectedHeaderModifier1, ResponseHeaderModifiers: &expectedresponseHeaderModifier, SnippetsFilters: []SnippetsFilter{ @@ -2827,7 +2975,7 @@ func TestCreateFilters(t *testing.T) { }, }, }, - msg: "two of each filter, first value for each standard filter wins, all ext ref filters added", + msg: "two of each filter, first value for each standard filter wins, all mirror and ext ref filters added", }, } @@ -2835,7 +2983,7 @@ func TestCreateFilters(t *testing.T) { t.Run(test.msg, func(t *testing.T) { t.Parallel() g := NewWithT(t) - result := createHTTPFilters(test.filters) + result := createHTTPFilters(test.filters, 0) g.Expect(helpers.Diff(test.expected, result)).To(BeEmpty()) }) diff --git a/internal/mode/static/state/dataplane/convert.go b/internal/mode/static/state/dataplane/convert.go index d44a47ed7a..b1b1a4c635 100644 --- a/internal/mode/static/state/dataplane/convert.go +++ b/internal/mode/static/state/dataplane/convert.go @@ -7,7 +7,9 @@ import ( v1 "sigs.k8s.io/gateway-api/apis/v1" ngfAPI "github.com/nginx/nginx-gateway-fabric/apis/v1alpha1" + "github.com/nginx/nginx-gateway-fabric/internal/framework/helpers" "github.com/nginx/nginx-gateway-fabric/internal/mode/static/state/graph" + "github.com/nginx/nginx-gateway-fabric/internal/mode/static/state/mirror" ) func convertMatch(m v1.HTTPRouteMatch) Match { @@ -60,6 +62,25 @@ func convertHTTPURLRewriteFilter(filter *v1.HTTPURLRewriteFilter) *HTTPURLRewrit } } +func convertHTTPRequestMirrorFilter(filter *v1.HTTPRequestMirrorFilter, ruleIdx int) *HTTPRequestMirrorFilter { + if filter.BackendRef.Name == "" { + return &HTTPRequestMirrorFilter{} + } + + result := &HTTPRequestMirrorFilter{ + Name: helpers.GetPointer(string(filter.BackendRef.Name)), + } + + namespace := (*string)(filter.BackendRef.Namespace) + if namespace != nil && len(*namespace) > 0 { + result.Namespace = namespace + } + + result.Target = mirror.BackendPath(ruleIdx, namespace, *result.Name) + + return result +} + func convertHTTPHeaderFilter(filter *v1.HTTPHeaderFilter) *HTTPHeaderFilter { result := &HTTPHeaderFilter{ Remove: filter.Remove, diff --git a/internal/mode/static/state/dataplane/convert_test.go b/internal/mode/static/state/dataplane/convert_test.go index f3116fab79..77b4c689ee 100644 --- a/internal/mode/static/state/dataplane/convert_test.go +++ b/internal/mode/static/state/dataplane/convert_test.go @@ -316,6 +316,57 @@ func TestConvertHTTPURLRewriteFilter(t *testing.T) { } } +func TestConvertHTTPMirrorFilter(t *testing.T) { + tests := []struct { + filter *v1.HTTPRequestMirrorFilter + expected *HTTPRequestMirrorFilter + name string + }{ + { + filter: &v1.HTTPRequestMirrorFilter{}, + expected: &HTTPRequestMirrorFilter{}, + name: "empty", + }, + { + filter: &v1.HTTPRequestMirrorFilter{ + BackendRef: v1.BackendObjectReference{ + Name: "backend", + Namespace: nil, + }, + }, + expected: &HTTPRequestMirrorFilter{ + Name: helpers.GetPointer("backend"), + Namespace: nil, + Target: helpers.GetPointer("/_ngf-internal-mirror-backend-0"), + }, + name: "missing namespace", + }, + { + filter: &v1.HTTPRequestMirrorFilter{ + BackendRef: v1.BackendObjectReference{ + Name: "backend", + Namespace: helpers.GetPointer[v1.Namespace]("namespace"), + }, + }, + expected: &HTTPRequestMirrorFilter{ + Name: helpers.GetPointer("backend"), + Namespace: helpers.GetPointer("namespace"), + Target: helpers.GetPointer("/_ngf-internal-mirror-namespace/backend-0"), + }, + name: "full", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewWithT(t) + + result := convertHTTPRequestMirrorFilter(test.filter, 0) + g.Expect(result).To(Equal(test.expected)) + }) + } +} + func TestConvertHTTPHeaderFilter(t *testing.T) { t.Parallel() tests := []struct { diff --git a/internal/mode/static/state/dataplane/types.go b/internal/mode/static/state/dataplane/types.go index d94f8e6032..55e5476f51 100644 --- a/internal/mode/static/state/dataplane/types.go +++ b/internal/mode/static/state/dataplane/types.go @@ -149,6 +149,8 @@ type HTTPFilters struct { RequestRedirect *HTTPRequestRedirectFilter // RequestURLRewrite holds the HTTPURLRewriteFilter. RequestURLRewrite *HTTPURLRewriteFilter + // RequestMirrors holds the HTTPRequestMirrorFilters. There could be more than one specified. + RequestMirrors []*HTTPRequestMirrorFilter // RequestHeaderModifiers holds the HTTPHeaderFilter. RequestHeaderModifiers *HTTPHeaderFilter // ResponseHeaderModifiers holds the HTTPHeaderFilter. @@ -207,6 +209,16 @@ type HTTPURLRewriteFilter struct { Path *HTTPPathModifier } +// HTTPRequestMirrorFilter mirrors HTTP requests. +type HTTPRequestMirrorFilter struct { + // Name is the service name. + Name *string + // Namespace is the namespace of the service. + Namespace *string + // Target is the target of the mirror (path with hostname and service name). + Target *string +} + // PathModifierType is the type of the PathModifier in a redirect or rewrite rule. type PathModifierType string diff --git a/internal/mode/static/state/graph/backend_refs.go b/internal/mode/static/state/graph/backend_refs.go index 76ee3ab35e..ad676f59c5 100644 --- a/internal/mode/static/state/graph/backend_refs.go +++ b/internal/mode/static/state/graph/backend_refs.go @@ -32,6 +32,8 @@ type BackendRef struct { // Valid indicates whether the backendRef is valid. // No configuration should be generated for an invalid BackendRef. Valid bool + // IsMirrorBackend indicates whether the BackendGroup is for a mirrored backend. + IsMirrorBackend bool } // ServicePortReference returns a string representation for the service and port that is referenced by the BackendRef. @@ -83,7 +85,11 @@ func addBackendRefsToRules( backendRefs := make([]BackendRef, 0, len(rule.RouteBackendRefs)) for refIdx, ref := range rule.RouteBackendRefs { - refPath := field.NewPath("spec").Child("rules").Index(idx).Child("backendRefs").Index(refIdx) + basePath := field.NewPath("spec").Child("rules").Index(idx) + refPath := basePath.Child("backendRefs").Index(refIdx) + if ref.MirrorBackendIdx != nil { + refPath = basePath.Child("filters").Index(*ref.MirrorBackendIdx).Child("backendRef") + } routeNs := route.Source.GetNamespace() ref, cond := createBackendRef( @@ -143,8 +149,9 @@ func createBackendRef( valid, cond := validateRouteBackendRef(ref, sourceNamespace, refGrantResolver, refPath) if !valid { backendRef = BackendRef{ - Weight: weight, - Valid: false, + Weight: weight, + Valid: false, + IsMirrorBackend: ref.MirrorBackendIdx != nil, } return backendRef, &cond @@ -158,10 +165,11 @@ func createBackendRef( svcIPFamily, svcPort, err := getIPFamilyAndPortFromRef(ref.BackendRef, svcNsName, services, refPath) if err != nil { backendRef = BackendRef{ - Weight: weight, - Valid: false, - SvcNsName: svcNsName, - ServicePort: v1.ServicePort{}, + Weight: weight, + Valid: false, + SvcNsName: svcNsName, + ServicePort: v1.ServicePort{}, + IsMirrorBackend: ref.MirrorBackendIdx != nil, } cond := staticConds.NewRouteBackendRefRefBackendNotFound(err.Error()) @@ -170,10 +178,11 @@ func createBackendRef( if err := verifyIPFamily(npCfg, svcIPFamily); err != nil { backendRef = BackendRef{ - SvcNsName: svcNsName, - ServicePort: svcPort, - Weight: weight, - Valid: false, + SvcNsName: svcNsName, + ServicePort: svcPort, + Weight: weight, + Valid: false, + IsMirrorBackend: ref.MirrorBackendIdx != nil, } cond := staticConds.NewRouteInvalidIPFamily(err.Error()) @@ -188,10 +197,11 @@ func createBackendRef( ) if err != nil { backendRef = BackendRef{ - SvcNsName: svcNsName, - ServicePort: svcPort, - Weight: weight, - Valid: false, + SvcNsName: svcNsName, + ServicePort: svcPort, + Weight: weight, + Valid: false, + IsMirrorBackend: ref.MirrorBackendIdx != nil, } cond := staticConds.NewRouteBackendRefUnsupportedValue(err.Error()) @@ -204,6 +214,7 @@ func createBackendRef( ServicePort: svcPort, Valid: true, Weight: weight, + IsMirrorBackend: ref.MirrorBackendIdx != nil, } return backendRef, nil @@ -385,8 +396,9 @@ func validateBackendRef( if !refGrantResolver(toService(refNsName)) { msg := fmt.Sprintf("Backend ref to Service %s not permitted by any ReferenceGrant", refNsName) + valErr := field.Forbidden(path.Child("namespace"), msg) - return false, staticConds.NewRouteBackendRefRefNotPermitted(msg) + return false, staticConds.NewRouteBackendRefRefNotPermitted(valErr.Error()) } } diff --git a/internal/mode/static/state/graph/backend_refs_test.go b/internal/mode/static/state/graph/backend_refs_test.go index ba6e2a2d2d..0d43456eed 100644 --- a/internal/mode/static/state/graph/backend_refs_test.go +++ b/internal/mode/static/state/graph/backend_refs_test.go @@ -176,7 +176,7 @@ func TestValidateBackendRef(t *testing.T) { refGrantResolver: alwaysFalseRefGrantResolver, expectedValid: false, expectedCondition: staticConds.NewRouteBackendRefRefNotPermitted( - "Backend ref to Service invalid/service1 not permitted by any ReferenceGrant", + "test.namespace: Forbidden: Backend ref to Service invalid/service1 not permitted by any ReferenceGrant", ), }, { @@ -1024,14 +1024,15 @@ func TestCreateBackend(t *testing.T) { refPath := field.NewPath("test") + alwaysTrueRefGrantResolver := func(_ toResource) bool { return true } + for _, test := range tests { t.Run(test.name, func(t *testing.T) { t.Parallel() g := NewWithT(t) - alwaysTrueRefGrantResolver := func(_ toResource) bool { return true } - rbr := RouteBackendRef{ + nil, test.ref.BackendRef, []any{}, } @@ -1052,6 +1053,27 @@ func TestCreateBackend(t *testing.T) { g.Expect(servicePortRef).To(Equal(test.expectedServicePortReference)) }) } + + // test mirror backend case + g := NewWithT(t) + ref := RouteBackendRef{ + helpers.GetPointer(0), // mirrorFilterIdx + getNormalRef(), + []any{}, + } + + backend, conds := createBackendRef( + ref, + "test-ns", + alwaysTrueRefGrantResolver, + services, + refPath, + policies, + nil, + ) + + g.Expect(conds).To(BeNil()) + g.Expect(backend.IsMirrorBackend).To(BeTrue()) } func TestGetServicePort(t *testing.T) { diff --git a/internal/mode/static/state/graph/common_filter.go b/internal/mode/static/state/graph/common_filter.go index 6bd71cf8e2..c67d233cda 100644 --- a/internal/mode/static/state/graph/common_filter.go +++ b/internal/mode/static/state/graph/common_filter.go @@ -160,6 +160,7 @@ func processRouteRuleFilters( var supportedGRPCFilterTypes = []FilterType{ FilterResponseHeaderModifier, FilterRequestHeaderModifier, + FilterRequestMirror, FilterExtensionRef, } @@ -169,6 +170,7 @@ var supportedHTTPFilterTypes = []FilterType{ FilterExtensionRef, FilterRequestRedirect, FilterURLRewrite, + FilterRequestMirror, } func validateFilterType(filter Filter, filterPath *field.Path) *field.Error { @@ -200,6 +202,8 @@ func validateFilter( return validateFilterRedirect(validator, filter.RequestRedirect, filterPath) case FilterURLRewrite: return validateFilterRewrite(validator, filter.URLRewrite, filterPath) + case FilterRequestMirror: + return validateFilterMirror(filter.RequestMirror, filterPath) case FilterRequestHeaderModifier: return validateFilterHeaderModifier( validator, @@ -219,6 +223,16 @@ func validateFilter( } } +func validateFilterMirror(mirror *v1.HTTPRequestMirrorFilter, filterPath *field.Path) field.ErrorList { + mirrorPath := filterPath.Child("requestMirror") + + if mirror == nil { + return field.ErrorList{field.Required(mirrorPath, "cannot be nil")} + } + + return nil +} + func validateFilterHeaderModifier( validator validation.HTTPFieldsValidator, headerModifier *v1.HTTPHeaderFilter, diff --git a/internal/mode/static/state/graph/common_filter_test.go b/internal/mode/static/state/graph/common_filter_test.go index 680938ea2c..f365a2cde0 100644 --- a/internal/mode/static/state/graph/common_filter_test.go +++ b/internal/mode/static/state/graph/common_filter_test.go @@ -39,6 +39,24 @@ func TestValidateFilter(t *testing.T) { expectErrCount: 0, name: "valid HTTP rewrite filter", }, + { + filter: Filter{ + RouteType: RouteTypeHTTP, + FilterType: FilterRequestMirror, + RequestMirror: &gatewayv1.HTTPRequestMirrorFilter{}, + }, + expectErrCount: 0, + name: "valid HTTP mirror filter", + }, + { + filter: Filter{ + RouteType: RouteTypeHTTP, + FilterType: FilterRequestMirror, + RequestMirror: nil, + }, + expectErrCount: 1, + name: "invalid HTTP mirror filter", + }, { filter: Filter{ RouteType: RouteTypeHTTP, @@ -73,11 +91,20 @@ func TestValidateFilter(t *testing.T) { { filter: Filter{ RouteType: RouteTypeHTTP, - FilterType: FilterRequestMirror, + FilterType: "invalid-filter", }, expectErrCount: 1, name: "unsupported HTTP filter type", }, + { + filter: Filter{ + RouteType: RouteTypeGRPC, + FilterType: FilterRequestMirror, + RequestMirror: &gatewayv1.HTTPRequestMirrorFilter{}, + }, + expectErrCount: 0, + name: "valid GRPC mirror filter", + }, { filter: Filter{ RouteType: RouteTypeGRPC, diff --git a/internal/mode/static/state/graph/extension_ref_filter.go b/internal/mode/static/state/graph/extension_ref_filter.go index 7587a69c0d..b758542465 100644 --- a/internal/mode/static/state/graph/extension_ref_filter.go +++ b/internal/mode/static/state/graph/extension_ref_filter.go @@ -28,7 +28,7 @@ func validateExtensionRefFilter(ref *v1.LocalObjectReference, path *field.Path) extRefPath := path.Child("extensionRef") if ref == nil { - return field.ErrorList{field.Required(extRefPath, "extensionRef cannot be nil")} + return field.ErrorList{field.Required(extRefPath, "cannot be nil")} } if ref.Name == "" { diff --git a/internal/mode/static/state/graph/extension_ref_filter_test.go b/internal/mode/static/state/graph/extension_ref_filter_test.go index bcc5539b73..ae485a664f 100644 --- a/internal/mode/static/state/graph/extension_ref_filter_test.go +++ b/internal/mode/static/state/graph/extension_ref_filter_test.go @@ -26,7 +26,7 @@ func TestValidateExtensionRefFilter(t *testing.T) { ref: nil, expErrCount: 1, errSubString: []string{ - `test.extensionRef: Required value: extensionRef cannot be nil`, + `test.extensionRef: Required value: cannot be nil`, }, }, { diff --git a/internal/mode/static/state/graph/grpcroute.go b/internal/mode/static/state/graph/grpcroute.go index 7da7bf20d6..aaacdb0fff 100644 --- a/internal/mode/static/state/graph/grpcroute.go +++ b/internal/mode/static/state/graph/grpcroute.go @@ -1,13 +1,18 @@ package graph import ( + "fmt" + "strings" + "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/validation/field" v1 "sigs.k8s.io/gateway-api/apis/v1" "github.com/nginx/nginx-gateway-fabric/internal/framework/conditions" "github.com/nginx/nginx-gateway-fabric/internal/framework/helpers" + "github.com/nginx/nginx-gateway-fabric/internal/mode/static/nginx/config/http" staticConds "github.com/nginx/nginx-gateway-fabric/internal/mode/static/state/conditions" + "github.com/nginx/nginx-gateway-fabric/internal/mode/static/state/mirror" "github.com/nginx/nginx-gateway-fabric/internal/mode/static/state/validation" ) @@ -69,6 +74,92 @@ func buildGRPCRoute( return r } +func buildGRPCMirrorRoutes( + routes map[RouteKey]*L7Route, + l7route *L7Route, + route *v1.GRPCRoute, + gatewayNsNames []types.NamespacedName, + snippetsFilters map[types.NamespacedName]*SnippetsFilter, + http2disabled bool, +) { + for idx, rule := range l7route.Spec.Rules { + if rule.Filters.Valid { + for _, filter := range rule.Filters.Filters { + if filter.RequestMirror == nil { + continue + } + + objectMeta := route.ObjectMeta.DeepCopy() + backendRef := filter.RequestMirror.BackendRef + namespace := route.GetNamespace() + if backendRef.Namespace != nil { + namespace = string(*backendRef.Namespace) + } + name := mirror.RouteName(route.GetName(), string(backendRef.Name), namespace, idx) + objectMeta.SetName(name) + + tmpMirrorRoute := &v1.GRPCRoute{ + ObjectMeta: *objectMeta, + Spec: v1.GRPCRouteSpec{ + CommonRouteSpec: route.Spec.CommonRouteSpec, + Hostnames: route.Spec.Hostnames, + Rules: buildGRPCMirrorRouteRule(idx, route.Spec.Rules[idx].Filters, filter), + }, + } + + mirrorRoute := buildGRPCRoute( + validation.SkipValidator{}, + tmpMirrorRoute, + gatewayNsNames, + http2disabled, + snippetsFilters, + ) + + if mirrorRoute != nil { + routes[CreateRouteKey(tmpMirrorRoute)] = mirrorRoute + } + } + } + } +} + +func buildGRPCMirrorRouteRule( + ruleIdx int, + filters []v1.GRPCRouteFilter, + filter Filter, +) []v1.GRPCRouteRule { + return []v1.GRPCRouteRule{ + { + Matches: []v1.GRPCRouteMatch{ + { + Method: &v1.GRPCMethodMatch{ + Type: helpers.GetPointer(v1.GRPCMethodMatchExact), + Service: mirror.PathWithBackendRef(ruleIdx, filter.RequestMirror.BackendRef), + }, + }, + }, + Filters: removeGRPCMirrorFilters(filters), + BackendRefs: []v1.GRPCBackendRef{ + { + BackendRef: v1.BackendRef{ + BackendObjectReference: filter.RequestMirror.BackendRef, + }, + }, + }, + }, + } +} + +func removeGRPCMirrorFilters(filters []v1.GRPCRouteFilter) []v1.GRPCRouteFilter { + var newFilters []v1.GRPCRouteFilter + for _, filter := range filters { + if filter.Type != v1.GRPCRouteFilterRequestMirror { + newFilters = append(newFilters, filter) + } + } + return newFilters +} + func processGRPCRouteRule( specRule v1.GRPCRouteRule, rulePath *field.Path, @@ -116,6 +207,22 @@ func processGRPCRouteRule( backendRefs = append(backendRefs, rbr) } + if routeFilters.Valid { + for i, filter := range routeFilters.Filters { + if filter.RequestMirror == nil { + continue + } + + rbr := RouteBackendRef{ + BackendRef: v1.BackendRef{ + BackendObjectReference: filter.RequestMirror.BackendRef, + }, + MirrorBackendIdx: helpers.GetPointer(i), + } + backendRefs = append(backendRefs, rbr) + } + } + return RouteRule{ ValidMatches: validMatches, Matches: ConvertGRPCMatches(specRule.Matches), @@ -139,7 +246,12 @@ func processGRPCRouteRules( for i, rule := range specRules { rulePath := field.NewPath("spec").Child("rules").Index(i) - rr, errors := processGRPCRouteRule(rule, rulePath, validator, resolveExtRefFunc) + rr, errors := processGRPCRouteRule( + rule, + rulePath, + validator, + resolveExtRefFunc, + ) if rr.ValidMatches && rr.Filters.Valid { atLeastOneValid = true @@ -204,12 +316,19 @@ func ConvertGRPCMatches(grpcMatches []v1.GRPCRouteMatch) []v1.HTTPRouteMatch { } hm.Headers = hmHeaders - if gm.Method != nil && gm.Method.Service != nil && gm.Method.Method != nil { - // if method match is provided, service and method are required - // as the only method type supported is exact. - // Validation has already been done at this point, and the condition will - // have been added there if required. - pathValue = "/" + *gm.Method.Service + "/" + *gm.Method.Method + if gm.Method != nil && gm.Method.Service != nil { + // service path used in mirror routes are special case; method is not specified + if strings.HasPrefix(*gm.Method.Service, http.InternalMirrorRoutePathPrefix) { + pathValue = *gm.Method.Service + } + + if gm.Method.Method != nil { + // if method match is provided, service and method are required + // as the only method type supported is exact. + // Validation has already been done at this point, and the condition will + // have been added there if required. + pathValue = "/" + *gm.Method.Service + "/" + *gm.Method.Method + } pathType = v1.PathMatchType("Exact") } hm.Path = &v1.HTTPPathMatch{ @@ -243,6 +362,11 @@ func validateGRPCMatch( ) field.ErrorList { var allErrs field.ErrorList + // for internally-created routes used for request mirroring, we don't need to validate + if validator.SkipValidation() { + return nil + } + methodPath := matchPath.Child("method") allErrs = append(allErrs, validateGRPCMethodMatch(validator, match.Method, methodPath)...) @@ -275,6 +399,14 @@ func validateGRPCMethodMatch( if method.Service == nil || *method.Service == "" { allErrs = append(allErrs, field.Required(methodServicePath, "service is required")) } else { + if strings.HasPrefix(*method.Service, http.InternalRoutePathPrefix) { + msg := fmt.Sprintf( + "service cannot start with %s. This prefix is reserved for internal use", + http.InternalRoutePathPrefix, + ) + return field.ErrorList{field.Invalid(methodPath.Child("service"), *method.Service, msg)} + } + pathValue := "/" + *method.Service if err := validator.ValidatePathInMatch(pathValue); err != nil { valErr := field.Invalid(methodServicePath, *method.Service, err.Error()) diff --git a/internal/mode/static/state/graph/grpcroute_test.go b/internal/mode/static/state/graph/grpcroute_test.go index 2adf83ee12..5417208d9e 100644 --- a/internal/mode/static/state/graph/grpcroute_test.go +++ b/internal/mode/static/state/graph/grpcroute_test.go @@ -15,6 +15,7 @@ import ( "github.com/nginx/nginx-gateway-fabric/internal/framework/helpers" "github.com/nginx/nginx-gateway-fabric/internal/framework/kinds" staticConds "github.com/nginx/nginx-gateway-fabric/internal/mode/static/state/conditions" + "github.com/nginx/nginx-gateway-fabric/internal/mode/static/state/mirror" "github.com/nginx/nginx-gateway-fabric/internal/mode/static/state/validation/validationfakes" ) @@ -338,7 +339,7 @@ func TestBuildGRPCRoute(t *testing.T) { grInvalidFilterRule.Filters = []v1.GRPCRouteFilter{ { - Type: "RequestMirror", + Type: "InvalidFilter", }, } @@ -902,8 +903,8 @@ func TestBuildGRPCRoute(t *testing.T) { Conditions: []conditions.Condition{ staticConds.NewRouteUnsupportedValue( `All rules are invalid: spec.rules[0].filters[0].type: Unsupported value: ` + - `"RequestMirror": supported values: "ResponseHeaderModifier", ` + - `"RequestHeaderModifier", "ExtensionRef"`, + `"InvalidFilter": supported values: "ResponseHeaderModifier", ` + + `"RequestHeaderModifier", "RequestMirror", "ExtensionRef"`, ), }, Spec: L7RouteSpec{ @@ -1090,6 +1091,150 @@ func TestBuildGRPCRoute(t *testing.T) { } } +func TestBuildGRPCRouteWithMirrorRoutes(t *testing.T) { + t.Parallel() + gatewayNsName := types.NamespacedName{Namespace: "test", Name: "gateway"} + + // Create a route with a request mirror filter and another random filter + mirrorFilter := v1.GRPCRouteFilter{ + Type: v1.GRPCRouteFilterRequestMirror, + RequestMirror: &v1.HTTPRequestMirrorFilter{ + BackendRef: v1.BackendObjectReference{ + Name: "mirror-backend", + }, + }, + } + + headerFilter := v1.GRPCRouteFilter{ + Type: v1.GRPCRouteFilterRequestHeaderModifier, + RequestHeaderModifier: &v1.HTTPHeaderFilter{ + Add: []v1.HTTPHeader{ + {Name: "X-Custom-Header", Value: "some-value"}, + }, + }, + } + + gr := createGRPCRoute( + "gr", + gatewayNsName.Name, + "example.com", + []v1.GRPCRouteRule{ + { + Matches: []v1.GRPCRouteMatch{ + { + Method: &v1.GRPCMethodMatch{ + Type: helpers.GetPointer(v1.GRPCMethodMatchExact), + Service: helpers.GetPointer("svc1"), + Method: helpers.GetPointer("method"), + }, + }, + }, + Filters: []v1.GRPCRouteFilter{mirrorFilter, headerFilter}, + }, + }, + ) + + // Expected mirror route + expectedMirrorRoute := &L7Route{ + RouteType: RouteTypeGRPC, + Source: &v1.GRPCRoute{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: mirror.RouteName("gr", "mirror-backend", "test", 0), + }, + Spec: v1.GRPCRouteSpec{ + CommonRouteSpec: gr.Spec.CommonRouteSpec, + Hostnames: gr.Spec.Hostnames, + Rules: []v1.GRPCRouteRule{ + { + Matches: []v1.GRPCRouteMatch{ + { + Method: &v1.GRPCMethodMatch{ + Type: helpers.GetPointer(v1.GRPCMethodMatchExact), + Service: helpers.GetPointer("/_ngf-internal-mirror-mirror-backend-0"), + }, + }, + }, + Filters: []v1.GRPCRouteFilter{headerFilter}, + BackendRefs: []v1.GRPCBackendRef{ + { + BackendRef: v1.BackendRef{ + BackendObjectReference: v1.BackendObjectReference{ + Name: "mirror-backend", + }, + }, + }, + }, + }, + }, + }, + }, + ParentRefs: []ParentRef{ + { + Idx: 0, + Gateway: gatewayNsName, + SectionName: gr.Spec.ParentRefs[0].SectionName, + }, + }, + Valid: true, + Attachable: true, + Spec: L7RouteSpec{ + Hostnames: gr.Spec.Hostnames, + Rules: []RouteRule{ + { + ValidMatches: true, + Filters: RouteRuleFilters{ + Valid: true, + Filters: []Filter{ + { + RouteType: RouteTypeGRPC, + FilterType: FilterRequestHeaderModifier, + RequestHeaderModifier: headerFilter.RequestHeaderModifier, + }, + }, + }, + Matches: []v1.HTTPRouteMatch{ + { + Path: &v1.HTTPPathMatch{ + Type: helpers.GetPointer(v1.PathMatchExact), + Value: helpers.GetPointer("/_ngf-internal-mirror-mirror-backend-0"), + }, + Headers: []v1.HTTPHeaderMatch{}, + }, + }, + RouteBackendRefs: []RouteBackendRef{ + { + BackendRef: v1.BackendRef{ + BackendObjectReference: v1.BackendObjectReference{ + Name: "mirror-backend", + }, + }, + }, + }, + }, + }, + }, + } + + validator := &validationfakes.FakeHTTPFieldsValidator{} + gatewayNsNames := []types.NamespacedName{gatewayNsName} + snippetsFilters := map[types.NamespacedName]*SnippetsFilter{} + + g := NewWithT(t) + + routes := map[RouteKey]*L7Route{} + l7route := buildGRPCRoute(validator, gr, gatewayNsNames, false, snippetsFilters) + g.Expect(l7route).NotTo(BeNil()) + + buildGRPCMirrorRoutes(routes, l7route, gr, gatewayNsNames, snippetsFilters, false) + + obj, ok := expectedMirrorRoute.Source.(*v1.GRPCRoute) + g.Expect(ok).To(BeTrue()) + mirrorRouteKey := CreateRouteKey(obj) + g.Expect(routes).To(HaveKey(mirrorRouteKey)) + g.Expect(helpers.Diff(expectedMirrorRoute, routes[mirrorRouteKey])).To(BeEmpty()) +} + func TestConvertGRPCMatches(t *testing.T) { t.Parallel() methodMatch := createGRPCMethodMatch("myService", "myMethod", "Exact").Matches diff --git a/internal/mode/static/state/graph/httproute.go b/internal/mode/static/state/graph/httproute.go index dd3258a4cd..f10df6965b 100644 --- a/internal/mode/static/state/graph/httproute.go +++ b/internal/mode/static/state/graph/httproute.go @@ -9,9 +9,10 @@ import ( v1 "sigs.k8s.io/gateway-api/apis/v1" "github.com/nginx/nginx-gateway-fabric/internal/framework/conditions" - + "github.com/nginx/nginx-gateway-fabric/internal/framework/helpers" "github.com/nginx/nginx-gateway-fabric/internal/mode/static/nginx/config/http" staticConds "github.com/nginx/nginx-gateway-fabric/internal/mode/static/state/conditions" + "github.com/nginx/nginx-gateway-fabric/internal/mode/static/state/mirror" "github.com/nginx/nginx-gateway-fabric/internal/mode/static/state/validation" ) @@ -70,6 +71,90 @@ func buildHTTPRoute( return r } +func buildHTTPMirrorRoutes( + routes map[RouteKey]*L7Route, + l7route *L7Route, + route *v1.HTTPRoute, + gatewayNsNames []types.NamespacedName, + snippetsFilters map[types.NamespacedName]*SnippetsFilter, +) { + for idx, rule := range l7route.Spec.Rules { + if rule.Filters.Valid { + for _, filter := range rule.Filters.Filters { + if filter.RequestMirror == nil { + continue + } + + objectMeta := route.ObjectMeta.DeepCopy() + backendRef := filter.RequestMirror.BackendRef + namespace := route.GetNamespace() + if backendRef.Namespace != nil { + namespace = string(*backendRef.Namespace) + } + name := mirror.RouteName(route.GetName(), string(backendRef.Name), namespace, idx) + objectMeta.SetName(name) + + tmpMirrorRoute := &v1.HTTPRoute{ + ObjectMeta: *objectMeta, + Spec: v1.HTTPRouteSpec{ + CommonRouteSpec: route.Spec.CommonRouteSpec, + Hostnames: route.Spec.Hostnames, + Rules: buildHTTPMirrorRouteRule(idx, route.Spec.Rules[idx].Filters, filter), + }, + } + + mirrorRoute := buildHTTPRoute( + validation.SkipValidator{}, + tmpMirrorRoute, + gatewayNsNames, + snippetsFilters, + ) + + if mirrorRoute != nil { + routes[CreateRouteKey(tmpMirrorRoute)] = mirrorRoute + } + } + } + } +} + +func buildHTTPMirrorRouteRule( + ruleIdx int, + filters []v1.HTTPRouteFilter, + filter Filter, +) []v1.HTTPRouteRule { + return []v1.HTTPRouteRule{ + { + Matches: []v1.HTTPRouteMatch{ + { + Path: &v1.HTTPPathMatch{ + Type: helpers.GetPointer(v1.PathMatchExact), + Value: mirror.PathWithBackendRef(ruleIdx, filter.RequestMirror.BackendRef), + }, + }, + }, + Filters: removeHTTPMirrorFilters(filters), + BackendRefs: []v1.HTTPBackendRef{ + { + BackendRef: v1.BackendRef{ + BackendObjectReference: filter.RequestMirror.BackendRef, + }, + }, + }, + }, + } +} + +func removeHTTPMirrorFilters(filters []v1.HTTPRouteFilter) []v1.HTTPRouteFilter { + var newFilters []v1.HTTPRouteFilter + for _, filter := range filters { + if filter.Type != v1.HTTPRouteFilterRequestMirror { + newFilters = append(newFilters, filter) + } + } + return newFilters +} + func processHTTPRouteRule( specRule v1.HTTPRouteRule, rulePath *field.Path, @@ -117,6 +202,22 @@ func processHTTPRouteRule( backendRefs = append(backendRefs, rbr) } + if routeFilters.Valid { + for i, filter := range routeFilters.Filters { + if filter.RequestMirror == nil { + continue + } + + rbr := RouteBackendRef{ + BackendRef: v1.BackendRef{ + BackendObjectReference: filter.RequestMirror.BackendRef, + }, + MirrorBackendIdx: helpers.GetPointer[int](i), + } + backendRefs = append(backendRefs, rbr) + } + } + return RouteRule{ ValidMatches: validMatches, Matches: specRule.Matches, @@ -140,7 +241,12 @@ func processHTTPRouteRules( for i, rule := range specRules { rulePath := field.NewPath("spec").Child("rules").Index(i) - rr, errors := processHTTPRouteRule(rule, rulePath, validator, resolveExtRefFunc) + rr, errors := processHTTPRouteRule( + rule, + rulePath, + validator, + resolveExtRefFunc, + ) if rr.ValidMatches && rr.Filters.Valid { atLeastOneValid = true @@ -183,6 +289,11 @@ func validateMatch( ) field.ErrorList { var allErrs field.ErrorList + // for internally-created routes used for request mirroring, we don't need to validate + if validator.SkipValidation() { + return nil + } + pathPath := matchPath.Child("path") allErrs = append(allErrs, validatePathMatch(validator, match.Path, pathPath)...) diff --git a/internal/mode/static/state/graph/httproute_test.go b/internal/mode/static/state/graph/httproute_test.go index d08f00b9ed..ce0f794bc6 100644 --- a/internal/mode/static/state/graph/httproute_test.go +++ b/internal/mode/static/state/graph/httproute_test.go @@ -16,6 +16,7 @@ import ( "github.com/nginx/nginx-gateway-fabric/internal/framework/helpers" "github.com/nginx/nginx-gateway-fabric/internal/framework/kinds" staticConds "github.com/nginx/nginx-gateway-fabric/internal/mode/static/state/conditions" + "github.com/nginx/nginx-gateway-fabric/internal/mode/static/state/mirror" "github.com/nginx/nginx-gateway-fabric/internal/mode/static/state/validation/validationfakes" ) @@ -912,6 +913,129 @@ func TestBuildHTTPRoute(t *testing.T) { } } +func TestBuildHTTPRouteWithMirrorRoutes(t *testing.T) { + t.Parallel() + gatewayNsName := types.NamespacedName{Namespace: "test", Name: "gateway"} + + // Create a route with a request mirror filter and another random filter + mirrorFilter := gatewayv1.HTTPRouteFilter{ + Type: gatewayv1.HTTPRouteFilterRequestMirror, + RequestMirror: &gatewayv1.HTTPRequestMirrorFilter{ + BackendRef: gatewayv1.BackendObjectReference{ + Name: "mirror-backend", + }, + }, + } + urlRewriteFilter := gatewayv1.HTTPRouteFilter{ + Type: gatewayv1.HTTPRouteFilterURLRewrite, + URLRewrite: &gatewayv1.HTTPURLRewriteFilter{ + Hostname: helpers.GetPointer[gatewayv1.PreciseHostname]("hostname"), + }, + } + hr := createHTTPRoute("hr", gatewayNsName.Name, "example.com", "/mirror") + addFilterToPath(hr, "/mirror", mirrorFilter) + addFilterToPath(hr, "/mirror", urlRewriteFilter) + + // Expected mirror route + expectedMirrorRoute := &L7Route{ + RouteType: RouteTypeHTTP, + Source: &gatewayv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: mirror.RouteName("hr", "mirror-backend", "test", 0), + }, + Spec: gatewayv1.HTTPRouteSpec{ + CommonRouteSpec: hr.Spec.CommonRouteSpec, + Hostnames: hr.Spec.Hostnames, + Rules: []gatewayv1.HTTPRouteRule{ + { + Matches: []gatewayv1.HTTPRouteMatch{ + { + Path: &gatewayv1.HTTPPathMatch{ + Type: helpers.GetPointer(gatewayv1.PathMatchExact), + Value: helpers.GetPointer("/_ngf-internal-mirror-mirror-backend-0"), + }, + }, + }, + Filters: []gatewayv1.HTTPRouteFilter{urlRewriteFilter}, + BackendRefs: []gatewayv1.HTTPBackendRef{ + { + BackendRef: gatewayv1.BackendRef{ + BackendObjectReference: gatewayv1.BackendObjectReference{ + Name: "mirror-backend", + }, + }, + }, + }, + }, + }, + }, + }, + ParentRefs: []ParentRef{ + { + Idx: 0, + Gateway: gatewayNsName, + SectionName: hr.Spec.ParentRefs[0].SectionName, + }, + }, + Valid: true, + Attachable: true, + Spec: L7RouteSpec{ + Hostnames: hr.Spec.Hostnames, + Rules: []RouteRule{ + { + ValidMatches: true, + Filters: RouteRuleFilters{ + Valid: true, + Filters: []Filter{ + { + RouteType: RouteTypeHTTP, + FilterType: FilterURLRewrite, + URLRewrite: urlRewriteFilter.URLRewrite, + }, + }, + }, + Matches: []gatewayv1.HTTPRouteMatch{ + { + Path: &gatewayv1.HTTPPathMatch{ + Type: helpers.GetPointer(gatewayv1.PathMatchExact), + Value: helpers.GetPointer("/_ngf-internal-mirror-mirror-backend-0"), + }, + }, + }, + RouteBackendRefs: []RouteBackendRef{ + { + BackendRef: gatewayv1.BackendRef{ + BackendObjectReference: gatewayv1.BackendObjectReference{ + Name: "mirror-backend", + }, + }, + }, + }, + }, + }, + }, + } + + validator := &validationfakes.FakeHTTPFieldsValidator{} + gatewayNsNames := []types.NamespacedName{gatewayNsName} + snippetsFilters := map[types.NamespacedName]*SnippetsFilter{} + + g := NewWithT(t) + + routes := map[RouteKey]*L7Route{} + l7route := buildHTTPRoute(validator, hr, gatewayNsNames, snippetsFilters) + g.Expect(l7route).NotTo(BeNil()) + + buildHTTPMirrorRoutes(routes, l7route, hr, gatewayNsNames, snippetsFilters) + + obj, ok := expectedMirrorRoute.Source.(*gatewayv1.HTTPRoute) + g.Expect(ok).To(BeTrue()) + mirrorRouteKey := CreateRouteKey(obj) + g.Expect(routes).To(HaveKey(mirrorRouteKey)) + g.Expect(helpers.Diff(expectedMirrorRoute, routes[mirrorRouteKey])).To(BeEmpty()) +} + func TestValidateMatch(t *testing.T) { t.Parallel() createAllValidValidator := func() *validationfakes.FakeHTTPFieldsValidator { @@ -920,6 +1044,9 @@ func TestValidateMatch(t *testing.T) { return v } + skipValidator := validationfakes.FakeHTTPFieldsValidator{} + skipValidator.SkipValidationReturns(true) + tests := []struct { match gatewayv1.HTTPRouteMatch validator *validationfakes.FakeHTTPFieldsValidator @@ -1165,6 +1292,11 @@ func TestValidateMatch(t *testing.T) { expectErrCount: 3, name: "multiple errors", }, + { + validator: &skipValidator, + expectErrCount: 0, + name: "skip validation", + }, } for _, test := range tests { diff --git a/internal/mode/static/state/graph/route_common.go b/internal/mode/static/state/graph/route_common.go index 0e50c19f9c..f146a55adb 100644 --- a/internal/mode/static/state/graph/route_common.go +++ b/internal/mode/static/state/graph/route_common.go @@ -139,6 +139,9 @@ type RouteRule struct { // RouteBackendRef is a wrapper for v1.BackendRef and any BackendRef filters from the HTTPRoute or GRPCRoute. type RouteBackendRef struct { + // If this backend is defined in a RequestMirror filter, this value will indicate the filter's index. + MirrorBackendIdx *int + v1.BackendRef Filters []any } @@ -229,16 +232,26 @@ func buildRoutesForGateways( for _, route := range httpRoutes { r := buildHTTPRoute(validator, route, gatewayNsNames, snippetsFilters) - if r != nil { - routes[CreateRouteKey(route)] = r + if r == nil { + continue } + + routes[CreateRouteKey(route)] = r + + // if this route has a RequestMirror filter, build a duplicate route for the mirror + buildHTTPMirrorRoutes(routes, r, route, gatewayNsNames, snippetsFilters) } for _, route := range grpcRoutes { r := buildGRPCRoute(validator, route, gatewayNsNames, http2disabled, snippetsFilters) - if r != nil { - routes[CreateRouteKey(route)] = r + if r == nil { + continue } + + routes[CreateRouteKey(route)] = r + + // if this route has a RequestMirror filter, build a duplicate route for the mirror + buildGRPCMirrorRoutes(routes, r, route, gatewayNsNames, snippetsFilters, http2disabled) } return routes diff --git a/internal/mode/static/state/graph/tlsroute.go b/internal/mode/static/state/graph/tlsroute.go index 0acf1cadd7..78b2378c36 100644 --- a/internal/mode/static/state/graph/tlsroute.go +++ b/internal/mode/static/state/graph/tlsroute.go @@ -67,7 +67,8 @@ func buildTLSRoute( return r } -func validateBackendRefTLSRoute(gtr *v1alpha2.TLSRoute, +func validateBackendRefTLSRoute( + gtr *v1alpha2.TLSRoute, services map[types.NamespacedName]*apiv1.Service, npCfg *NginxProxy, refGrantResolver func(resource toResource) bool, diff --git a/internal/mode/static/state/graph/tlsroute_test.go b/internal/mode/static/state/graph/tlsroute_test.go index ae4555ef24..73cd8758a1 100644 --- a/internal/mode/static/state/graph/tlsroute_test.go +++ b/internal/mode/static/state/graph/tlsroute_test.go @@ -429,7 +429,8 @@ func TestBuildTLSRoute(t *testing.T) { }, }, Conditions: []conditions.Condition{staticConds.NewRouteBackendRefRefNotPermitted( - "Backend ref to Service diff/hi not permitted by any ReferenceGrant", + "spec.rules[0].backendRefs[0].namespace: Forbidden: Backend ref to Service " + + "diff/hi not permitted by any ReferenceGrant", )}, Attachable: true, Valid: true, diff --git a/internal/mode/static/state/mirror/mirror.go b/internal/mode/static/state/mirror/mirror.go new file mode 100644 index 0000000000..485fe8ccf3 --- /dev/null +++ b/internal/mode/static/state/mirror/mirror.go @@ -0,0 +1,40 @@ +package mirror + +import ( + "fmt" + "strings" + + v1 "sigs.k8s.io/gateway-api/apis/v1" + + "github.com/nginx/nginx-gateway-fabric/internal/mode/static/nginx/config/http" +) + +// RouteName builds the name for the internal mirror route, using the user route name, +// service namespace/name, and index of the rule. +// The prefix is used to prevent a user from creating a route with a conflicting name. +func RouteName(routeName, service, namespace string, idx int) string { + prefix := strings.TrimPrefix(http.InternalMirrorRoutePathPrefix, "/") + return fmt.Sprintf("%s-%s-%s/%s-%d", prefix, routeName, namespace, service, idx) +} + +// BackendPath builds the path for the internal mirror location, using the BackendRef. +func PathWithBackendRef(idx int, backendRef v1.BackendObjectReference) *string { + svcName := string(backendRef.Name) + if backendRef.Namespace == nil { + return BackendPath(idx, nil, svcName) + } + return BackendPath(idx, (*string)(backendRef.Namespace), svcName) +} + +// BackendPath builds the path for the internal mirror location. +func BackendPath(idx int, namespace *string, service string) *string { + var mirrorPath string + + if namespace != nil { + mirrorPath = fmt.Sprintf("%s-%s/%s-%d", http.InternalMirrorRoutePathPrefix, *namespace, service, idx) + } else { + mirrorPath = fmt.Sprintf("%s-%s-%d", http.InternalMirrorRoutePathPrefix, service, idx) + } + + return &mirrorPath +} diff --git a/internal/mode/static/state/mirror/mirror_test.go b/internal/mode/static/state/mirror/mirror_test.go new file mode 100644 index 0000000000..06c63e7adb --- /dev/null +++ b/internal/mode/static/state/mirror/mirror_test.go @@ -0,0 +1,94 @@ +package mirror + +import ( + "testing" + + . "github.com/onsi/gomega" + v1 "sigs.k8s.io/gateway-api/apis/v1" + + "github.com/nginx/nginx-gateway-fabric/internal/framework/helpers" +) + +func TestRouteName(t *testing.T) { + t.Parallel() + g := NewWithT(t) + + result := RouteName("route1", "service1", "namespace1", 1) + g.Expect(result).To(Equal("_ngf-internal-mirror-route1-namespace1/service1-1")) +} + +func TestPathWithBackendRef(t *testing.T) { + t.Parallel() + + tests := []struct { + backendRef v1.BackendObjectReference + expected *string + name string + idx int + }{ + { + name: "with namespace", + idx: 1, + backendRef: v1.BackendObjectReference{ + Name: "service1", + Namespace: helpers.GetPointer[v1.Namespace]("namespace1"), + }, + expected: helpers.GetPointer("/_ngf-internal-mirror-namespace1/service1-1"), + }, + { + name: "without namespace", + idx: 2, + backendRef: v1.BackendObjectReference{ + Name: "service2", + }, + expected: helpers.GetPointer("/_ngf-internal-mirror-service2-2"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + g := NewWithT(t) + + result := PathWithBackendRef(tt.idx, tt.backendRef) + g.Expect(result).To(Equal(tt.expected)) + }) + } +} + +func TestBackendPath(t *testing.T) { + t.Parallel() + + tests := []struct { + namespace *string + expected *string + name string + service string + idx int + }{ + { + name: "With namespace", + idx: 1, + namespace: helpers.GetPointer("namespace1"), + service: "service1", + expected: helpers.GetPointer("/_ngf-internal-mirror-namespace1/service1-1"), + }, + { + name: "Without namespace", + idx: 2, + namespace: nil, + service: "service2", + expected: helpers.GetPointer("/_ngf-internal-mirror-service2-2"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + g := NewWithT(t) + + result := BackendPath(tt.idx, tt.namespace, tt.service) + g.Expect(result).To(Equal(tt.expected)) + }) + } +} diff --git a/internal/mode/static/state/validation/validationfakes/fake_httpfields_validator.go b/internal/mode/static/state/validation/validationfakes/fake_httpfields_validator.go index 490018358f..71a907acbf 100644 --- a/internal/mode/static/state/validation/validationfakes/fake_httpfields_validator.go +++ b/internal/mode/static/state/validation/validationfakes/fake_httpfields_validator.go @@ -8,6 +8,16 @@ import ( ) type FakeHTTPFieldsValidator struct { + SkipValidationStub func() bool + skipValidationMutex sync.RWMutex + skipValidationArgsForCall []struct { + } + skipValidationReturns struct { + result1 bool + } + skipValidationReturnsOnCall map[int]struct { + result1 bool + } ValidateFilterHeaderNameStub func(string) error validateFilterHeaderNameMutex sync.RWMutex validateFilterHeaderNameArgsForCall []struct { @@ -161,6 +171,59 @@ type FakeHTTPFieldsValidator struct { invocationsMutex sync.RWMutex } +func (fake *FakeHTTPFieldsValidator) SkipValidation() bool { + fake.skipValidationMutex.Lock() + ret, specificReturn := fake.skipValidationReturnsOnCall[len(fake.skipValidationArgsForCall)] + fake.skipValidationArgsForCall = append(fake.skipValidationArgsForCall, struct { + }{}) + stub := fake.SkipValidationStub + fakeReturns := fake.skipValidationReturns + fake.recordInvocation("SkipValidation", []interface{}{}) + fake.skipValidationMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeHTTPFieldsValidator) SkipValidationCallCount() int { + fake.skipValidationMutex.RLock() + defer fake.skipValidationMutex.RUnlock() + return len(fake.skipValidationArgsForCall) +} + +func (fake *FakeHTTPFieldsValidator) SkipValidationCalls(stub func() bool) { + fake.skipValidationMutex.Lock() + defer fake.skipValidationMutex.Unlock() + fake.SkipValidationStub = stub +} + +func (fake *FakeHTTPFieldsValidator) SkipValidationReturns(result1 bool) { + fake.skipValidationMutex.Lock() + defer fake.skipValidationMutex.Unlock() + fake.SkipValidationStub = nil + fake.skipValidationReturns = struct { + result1 bool + }{result1} +} + +func (fake *FakeHTTPFieldsValidator) SkipValidationReturnsOnCall(i int, result1 bool) { + fake.skipValidationMutex.Lock() + defer fake.skipValidationMutex.Unlock() + fake.SkipValidationStub = nil + if fake.skipValidationReturnsOnCall == nil { + fake.skipValidationReturnsOnCall = make(map[int]struct { + result1 bool + }) + } + fake.skipValidationReturnsOnCall[i] = struct { + result1 bool + }{result1} +} + func (fake *FakeHTTPFieldsValidator) ValidateFilterHeaderName(arg1 string) error { fake.validateFilterHeaderNameMutex.Lock() ret, specificReturn := fake.validateFilterHeaderNameReturnsOnCall[len(fake.validateFilterHeaderNameArgsForCall)] @@ -966,6 +1029,8 @@ func (fake *FakeHTTPFieldsValidator) ValidateRedirectStatusCodeReturnsOnCall(i i func (fake *FakeHTTPFieldsValidator) Invocations() map[string][][]interface{} { fake.invocationsMutex.RLock() defer fake.invocationsMutex.RUnlock() + fake.skipValidationMutex.RLock() + defer fake.skipValidationMutex.RUnlock() fake.validateFilterHeaderNameMutex.RLock() defer fake.validateFilterHeaderNameMutex.RUnlock() fake.validateFilterHeaderValueMutex.RLock() diff --git a/internal/mode/static/state/validation/validator.go b/internal/mode/static/state/validation/validator.go index dd48573902..26bf281b70 100644 --- a/internal/mode/static/state/validation/validator.go +++ b/internal/mode/static/state/validation/validator.go @@ -22,6 +22,7 @@ type Validators struct { // //counterfeiter:generate . HTTPFieldsValidator type HTTPFieldsValidator interface { + SkipValidation() bool ValidatePathInMatch(path string) error ValidateHeaderNameInMatch(name string) error ValidateHeaderValueInMatch(value string) error @@ -58,3 +59,22 @@ type PolicyValidator interface { // Conflicts returns true if the two Policies conflict. Conflicts(a, b policies.Policy) bool } + +// SkipValidator is used to skip validation on internally-created routes for request mirroring. +type SkipValidator struct{} + +func (SkipValidator) SkipValidation() bool { return true } + +func (SkipValidator) ValidatePathInMatch(string) error { return nil } +func (SkipValidator) ValidateHeaderNameInMatch(string) error { return nil } +func (SkipValidator) ValidateHeaderValueInMatch(string) error { return nil } +func (SkipValidator) ValidateQueryParamNameInMatch(string) error { return nil } +func (SkipValidator) ValidateQueryParamValueInMatch(string) error { return nil } +func (SkipValidator) ValidateMethodInMatch(string) (bool, []string) { return true, nil } +func (SkipValidator) ValidateRedirectScheme(string) (bool, []string) { return true, nil } +func (SkipValidator) ValidateRedirectPort(int32) error { return nil } +func (SkipValidator) ValidateRedirectStatusCode(int) (bool, []string) { return true, nil } +func (SkipValidator) ValidateHostname(string) error { return nil } +func (SkipValidator) ValidateFilterHeaderName(string) error { return nil } +func (SkipValidator) ValidateFilterHeaderValue(string) error { return nil } +func (SkipValidator) ValidatePath(string) error { return nil }