diff --git a/internal/mesh/internal/mappers/sidecarproxymapper/destinations_mapper_test.go b/internal/mesh/internal/mappers/sidecarproxymapper/destinations_mapper_test.go index 69937fb34d41..5e5babc8c79a 100644 --- a/internal/mesh/internal/mappers/sidecarproxymapper/destinations_mapper_test.go +++ b/internal/mesh/internal/mappers/sidecarproxymapper/destinations_mapper_test.go @@ -25,24 +25,36 @@ import ( func TestMapDestinationsToProxyStateTemplate(t *testing.T) { client := svctest.RunResourceService(t, types.Register, catalog.RegisterTypes) webWorkload1 := resourcetest.Resource(catalog.WorkloadType, "web-abc"). + WithTenancy(resource.DefaultNamespacedTenancy()). WithData(t, &pbcatalog.Workload{ Addresses: []*pbcatalog.WorkloadAddress{{Host: "10.0.0.1"}}, Ports: map[string]*pbcatalog.WorkloadPort{"tcp": {Port: 8081, Protocol: pbcatalog.Protocol_PROTOCOL_TCP}}, }). Write(t, client) webWorkload2 := resourcetest.Resource(catalog.WorkloadType, "web-def"). + WithTenancy(resource.DefaultNamespacedTenancy()). WithData(t, &pbcatalog.Workload{ Addresses: []*pbcatalog.WorkloadAddress{{Host: "10.0.0.2"}}, Ports: map[string]*pbcatalog.WorkloadPort{"tcp": {Port: 8081, Protocol: pbcatalog.Protocol_PROTOCOL_TCP}}, }). Write(t, client) webWorkload3 := resourcetest.Resource(catalog.WorkloadType, "non-prefix-web"). + WithTenancy(resource.DefaultNamespacedTenancy()). WithData(t, &pbcatalog.Workload{ Addresses: []*pbcatalog.WorkloadAddress{{Host: "10.0.0.3"}}, Ports: map[string]*pbcatalog.WorkloadPort{"tcp": {Port: 8081, Protocol: pbcatalog.Protocol_PROTOCOL_TCP}}, }). Write(t, client) + var ( + api1ServiceRef = resourcetest.Resource(catalog.ServiceType, "api-1"). + WithTenancy(resource.DefaultNamespacedTenancy()). + ReferenceNoSection() + api2ServiceRef = resourcetest.Resource(catalog.ServiceType, "api-2"). + WithTenancy(resource.DefaultNamespacedTenancy()). + ReferenceNoSection() + ) + webDestinationsData := &pbmesh.Upstreams{ Workloads: &pbcatalog.WorkloadSelector{ Names: []string{"non-prefix-web"}, @@ -50,21 +62,22 @@ func TestMapDestinationsToProxyStateTemplate(t *testing.T) { }, Upstreams: []*pbmesh.Upstream{ { - DestinationRef: resourcetest.Resource(catalog.ServiceType, "api-1").ReferenceNoSection(), + DestinationRef: api1ServiceRef, DestinationPort: "tcp", }, { - DestinationRef: resourcetest.Resource(catalog.ServiceType, "api-2").ReferenceNoSection(), + DestinationRef: api2ServiceRef, DestinationPort: "tcp1", }, { - DestinationRef: resourcetest.Resource(catalog.ServiceType, "api-2").ReferenceNoSection(), + DestinationRef: api2ServiceRef, DestinationPort: "tcp2", }, }, } webDestinations := resourcetest.Resource(types.UpstreamsType, "web-destinations"). + WithTenancy(resource.DefaultNamespacedTenancy()). WithData(t, webDestinationsData). Write(t, client) @@ -81,10 +94,14 @@ func TestMapDestinationsToProxyStateTemplate(t *testing.T) { require.NoError(t, err) prototest.AssertElementsMatch(t, expRequests, requests) - //var expDestinations []*intermediate.CombinedDestinationRef - proxy1ID := resourcetest.Resource(types.ProxyStateTemplateType, webWorkload1.Id.Name).ID() - proxy2ID := resourcetest.Resource(types.ProxyStateTemplateType, webWorkload2.Id.Name).ID() - proxy3ID := resourcetest.Resource(types.ProxyStateTemplateType, webWorkload3.Id.Name).ID() + var ( + proxy1ID = resourcetest.Resource(types.ProxyStateTemplateType, webWorkload1.Id.Name). + WithTenancy(resource.DefaultNamespacedTenancy()).ID() + proxy2ID = resourcetest.Resource(types.ProxyStateTemplateType, webWorkload2.Id.Name). + WithTenancy(resource.DefaultNamespacedTenancy()).ID() + proxy3ID = resourcetest.Resource(types.ProxyStateTemplateType, webWorkload3.Id.Name). + WithTenancy(resource.DefaultNamespacedTenancy()).ID() + ) for _, u := range webDestinationsData.Upstreams { expDestination := intermediate.CombinedDestinationRef{ ServiceRef: u.DestinationRef, diff --git a/internal/mesh/internal/types/grpc_route.go b/internal/mesh/internal/types/grpc_route.go index a7a839f35902..a5cb8de30b30 100644 --- a/internal/mesh/internal/types/grpc_route.go +++ b/internal/mesh/internal/types/grpc_route.go @@ -30,14 +30,45 @@ var ( func RegisterGRPCRoute(r resource.Registry) { r.Register(resource.Registration{ - Type: GRPCRouteV1Alpha1Type, - Proto: &pbmesh.GRPCRoute{}, - Scope: resource.ScopeNamespace, - // TODO(rb): normalize parent/backend ref tenancies in a Mutate hook + Type: GRPCRouteV1Alpha1Type, + Proto: &pbmesh.GRPCRoute{}, + Scope: resource.ScopeNamespace, + Mutate: MutateGRPCRoute, Validate: ValidateGRPCRoute, }) } +func MutateGRPCRoute(res *pbresource.Resource) error { + var route pbmesh.GRPCRoute + + if err := res.Data.UnmarshalTo(&route); err != nil { + return resource.NewErrDataParse(&route, err) + } + + changed := false + + if mutateParentRefs(res.Id.Tenancy, route.ParentRefs) { + changed = true + } + + for _, rule := range route.Rules { + for _, backend := range rule.BackendRefs { + if backend.BackendRef == nil || backend.BackendRef.Ref == nil { + continue + } + if mutateXRouteRef(res.Id.Tenancy, backend.BackendRef.Ref) { + changed = true + } + } + } + + if !changed { + return nil + } + + return res.Data.MarshalFrom(&route) +} + func ValidateGRPCRoute(res *pbresource.Resource) error { var route pbmesh.GRPCRoute @@ -170,14 +201,6 @@ func ValidateGRPCRoute(res *pbresource.Resource) error { } if len(rule.BackendRefs) == 0 { - /* - BackendRefs (optional)¶ - - BackendRefs defines API objects where matching requests should be - sent. If unspecified, the rule performs no forwarding. If - unspecified and no filters are specified that would result in a - response being sent, a 404 error code is returned. - */ merr = multierror.Append(merr, wrapRuleErr( resource.ErrInvalidField{ Name: "backend_refs", @@ -193,13 +216,15 @@ func ValidateGRPCRoute(res *pbresource.Resource) error { Wrapped: err, }) } - for _, err := range validateBackendRef(hbref.BackendRef) { - merr = multierror.Append(merr, wrapBackendRefErr( - resource.ErrInvalidField{ - Name: "backend_ref", - Wrapped: err, - }, - )) + + wrapBackendRefFieldErr := func(err error) error { + return wrapBackendRefErr(resource.ErrInvalidField{ + Name: "backend_ref", + Wrapped: err, + }) + } + if err := validateBackendRef(hbref.BackendRef, wrapBackendRefFieldErr); err != nil { + merr = multierror.Append(merr, err) } if len(hbref.Filters) > 0 { diff --git a/internal/mesh/internal/types/grpc_route_test.go b/internal/mesh/internal/types/grpc_route_test.go index e9abc118c654..24ab8a17fe27 100644 --- a/internal/mesh/internal/types/grpc_route_test.go +++ b/internal/mesh/internal/types/grpc_route_test.go @@ -7,14 +7,99 @@ import ( "testing" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" "github.com/hashicorp/consul/internal/catalog" "github.com/hashicorp/consul/internal/resource/resourcetest" pbmesh "github.com/hashicorp/consul/proto-public/pbmesh/v1alpha1" + "github.com/hashicorp/consul/proto-public/pbresource" "github.com/hashicorp/consul/proto/private/prototest" "github.com/hashicorp/consul/sdk/testutil" ) +func TestMutateGRPCRoute(t *testing.T) { + type testcase struct { + routeTenancy *pbresource.Tenancy + route *pbmesh.GRPCRoute + expect *pbmesh.GRPCRoute + } + + cases := map[string]testcase{} + + // Add common parent refs test cases. + for name, parentTC := range getXRouteParentRefMutateTestCases() { + cases["parent-ref: "+name] = testcase{ + routeTenancy: parentTC.routeTenancy, + route: &pbmesh.GRPCRoute{ + ParentRefs: parentTC.refs, + }, + expect: &pbmesh.GRPCRoute{ + ParentRefs: parentTC.expect, + }, + } + } + // add common backend ref test cases. + for name, backendTC := range getXRouteBackendRefMutateTestCases() { + var ( + refs []*pbmesh.GRPCBackendRef + expect []*pbmesh.GRPCBackendRef + ) + for _, br := range backendTC.refs { + refs = append(refs, &pbmesh.GRPCBackendRef{ + BackendRef: br, + }) + } + for _, br := range backendTC.expect { + expect = append(expect, &pbmesh.GRPCBackendRef{ + BackendRef: br, + }) + } + cases["backend-ref: "+name] = testcase{ + routeTenancy: backendTC.routeTenancy, + route: &pbmesh.GRPCRoute{ + ParentRefs: []*pbmesh.ParentReference{ + newParentRef(catalog.ServiceType, "web", ""), + }, + Rules: []*pbmesh.GRPCRouteRule{ + {BackendRefs: refs}, + }, + }, + expect: &pbmesh.GRPCRoute{ + ParentRefs: []*pbmesh.ParentReference{ + newParentRef(catalog.ServiceType, "web", ""), + }, + Rules: []*pbmesh.GRPCRouteRule{ + {BackendRefs: expect}, + }, + }, + } + } + + run := func(t *testing.T, tc testcase) { + res := resourcetest.Resource(GRPCRouteType, "api"). + WithTenancy(tc.routeTenancy). + WithData(t, tc.route). + Build() + + err := MutateGRPCRoute(res) + require.NoError(t, err) + + got := resourcetest.MustDecode[*pbmesh.GRPCRoute](t, res) + + if tc.expect == nil { + tc.expect = proto.Clone(tc.route).(*pbmesh.GRPCRoute) + } + + prototest.AssertDeepEqual(t, tc.expect, got.Data) + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + run(t, tc) + }) + } +} + func TestValidateGRPCRoute(t *testing.T) { type testcase struct { route *pbmesh.GRPCRoute @@ -26,7 +111,15 @@ func TestValidateGRPCRoute(t *testing.T) { WithData(t, tc.route). Build() - err := ValidateGRPCRoute(res) + // Ensure things are properly mutated and updated in the inputs. + err := MutateGRPCRoute(res) + require.NoError(t, err) + { + mutated := resourcetest.MustDecode[*pbmesh.GRPCRoute](t, res) + tc.route = mutated.Data + } + + err = ValidateGRPCRoute(res) // Verify that validate didn't actually change the object. got := resourcetest.MustDecode[*pbmesh.GRPCRoute](t, res) diff --git a/internal/mesh/internal/types/http_route.go b/internal/mesh/internal/types/http_route.go index 9d94128eb864..a2d55631cfb9 100644 --- a/internal/mesh/internal/types/http_route.go +++ b/internal/mesh/internal/types/http_route.go @@ -49,6 +49,10 @@ func MutateHTTPRoute(res *pbresource.Resource) error { changed := false + if mutateParentRefs(res.Id.Tenancy, route.ParentRefs) { + changed = true + } + for _, rule := range route.Rules { for _, match := range rule.Matches { if match.Method != "" { @@ -59,10 +63,16 @@ func MutateHTTPRoute(res *pbresource.Resource) error { } } } + for _, backend := range rule.BackendRefs { + if backend.BackendRef == nil || backend.BackendRef.Ref == nil { + continue + } + if mutateXRouteRef(res.Id.Tenancy, backend.BackendRef.Ref) { + changed = true + } + } } - // TODO(rb): normalize parent/backend ref tenancies - if !changed { return nil } @@ -264,14 +274,6 @@ func ValidateHTTPRoute(res *pbresource.Resource) error { } if len(rule.BackendRefs) == 0 { - /* - BackendRefs (optional)¶ - - BackendRefs defines API objects where matching requests should be - sent. If unspecified, the rule performs no forwarding. If - unspecified and no filters are specified that would result in a - response being sent, a 404 error code is returned. - */ merr = multierror.Append(merr, wrapRuleErr( resource.ErrInvalidField{ Name: "backend_refs", @@ -288,13 +290,14 @@ func ValidateHTTPRoute(res *pbresource.Resource) error { }) } - for _, err := range validateBackendRef(hbref.BackendRef) { - merr = multierror.Append(merr, wrapBackendRefErr( - resource.ErrInvalidField{ - Name: "backend_ref", - Wrapped: err, - }, - )) + wrapBackendRefFieldErr := func(err error) error { + return wrapBackendRefErr(resource.ErrInvalidField{ + Name: "backend_ref", + Wrapped: err, + }) + } + if err := validateBackendRef(hbref.BackendRef, wrapBackendRefFieldErr); err != nil { + merr = multierror.Append(merr, err) } if len(hbref.Filters) > 0 { diff --git a/internal/mesh/internal/types/http_route_test.go b/internal/mesh/internal/types/http_route_test.go index 5b9a0ee42c85..49ab8ee83ba4 100644 --- a/internal/mesh/internal/types/http_route_test.go +++ b/internal/mesh/internal/types/http_route_test.go @@ -4,6 +4,7 @@ package types import ( + "strings" "testing" "time" @@ -23,13 +24,15 @@ import ( func TestMutateHTTPRoute(t *testing.T) { type testcase struct { - route *pbmesh.HTTPRoute - expect *pbmesh.HTTPRoute - expectErr string + routeTenancy *pbresource.Tenancy + route *pbmesh.HTTPRoute + expect *pbmesh.HTTPRoute + expectErr string } run := func(t *testing.T, tc testcase) { res := resourcetest.Resource(HTTPRouteType, "api"). + WithTenancy(tc.routeTenancy). WithData(t, tc.route). Build() @@ -139,6 +142,55 @@ func TestMutateHTTPRoute(t *testing.T) { }, } + // Add common parent refs test cases. + for name, parentTC := range getXRouteParentRefMutateTestCases() { + cases["parent-ref: "+name] = testcase{ + routeTenancy: parentTC.routeTenancy, + route: &pbmesh.HTTPRoute{ + ParentRefs: parentTC.refs, + }, + expect: &pbmesh.HTTPRoute{ + ParentRefs: parentTC.expect, + }, + } + } + // add common backend ref test cases. + for name, backendTC := range getXRouteBackendRefMutateTestCases() { + var ( + refs []*pbmesh.HTTPBackendRef + expect []*pbmesh.HTTPBackendRef + ) + for _, br := range backendTC.refs { + refs = append(refs, &pbmesh.HTTPBackendRef{ + BackendRef: br, + }) + } + for _, br := range backendTC.expect { + expect = append(expect, &pbmesh.HTTPBackendRef{ + BackendRef: br, + }) + } + cases["backend-ref: "+name] = testcase{ + routeTenancy: backendTC.routeTenancy, + route: &pbmesh.HTTPRoute{ + ParentRefs: []*pbmesh.ParentReference{ + newParentRef(catalog.ServiceType, "web", ""), + }, + Rules: []*pbmesh.HTTPRouteRule{ + {BackendRefs: refs}, + }, + }, + expect: &pbmesh.HTTPRoute{ + ParentRefs: []*pbmesh.ParentReference{ + newParentRef(catalog.ServiceType, "web", ""), + }, + Rules: []*pbmesh.HTTPRouteRule{ + {BackendRefs: expect}, + }, + }, + } + } + for name, tc := range cases { t.Run(name, func(t *testing.T) { run(t, tc) @@ -157,8 +209,13 @@ func TestValidateHTTPRoute(t *testing.T) { WithData(t, tc.route). Build() + // Ensure things are properly mutated and updated in the inputs. err := MutateHTTPRoute(res) require.NoError(t, err) + { + mutated := resourcetest.MustDecode[*pbmesh.HTTPRoute](t, res) + tc.route = mutated.Data + } err = ValidateHTTPRoute(res) @@ -761,6 +818,80 @@ func TestValidateHTTPRoute(t *testing.T) { } } +type xRouteParentRefMutateTestcase struct { + routeTenancy *pbresource.Tenancy + refs []*pbmesh.ParentReference + expect []*pbmesh.ParentReference +} + +func getXRouteParentRefMutateTestCases() map[string]xRouteParentRefMutateTestcase { + newRef := func(typ *pbresource.Type, tenancyStr, name string) *pbresource.Reference { + return resourcetest.Resource(typ, name). + WithTenancy(newTestTenancy(tenancyStr)). + Reference("") + } + + newParentRef := func(typ *pbresource.Type, tenancyStr, name, port string) *pbmesh.ParentReference { + return &pbmesh.ParentReference{ + Ref: newRef(typ, tenancyStr, name), + Port: port, + } + } + + return map[string]xRouteParentRefMutateTestcase{ + "parent ref tenancies defaulted": { + routeTenancy: newTestTenancy("foo.bar"), + refs: []*pbmesh.ParentReference{ + newParentRef(catalog.ServiceType, "", "api", ""), + newParentRef(catalog.ServiceType, ".zim", "api", ""), + newParentRef(catalog.ServiceType, "gir.zim", "api", ""), + }, + expect: []*pbmesh.ParentReference{ + newParentRef(catalog.ServiceType, "foo.bar", "api", ""), + newParentRef(catalog.ServiceType, "foo.zim", "api", ""), + newParentRef(catalog.ServiceType, "gir.zim", "api", ""), + }, + }, + } +} + +type xRouteBackendRefMutateTestcase struct { + routeTenancy *pbresource.Tenancy + refs []*pbmesh.BackendReference + expect []*pbmesh.BackendReference +} + +func getXRouteBackendRefMutateTestCases() map[string]xRouteBackendRefMutateTestcase { + newRef := func(typ *pbresource.Type, tenancyStr, name string) *pbresource.Reference { + return resourcetest.Resource(typ, name). + WithTenancy(newTestTenancy(tenancyStr)). + Reference("") + } + + newBackendRef := func(typ *pbresource.Type, tenancyStr, name, port string) *pbmesh.BackendReference { + return &pbmesh.BackendReference{ + Ref: newRef(typ, tenancyStr, name), + Port: port, + } + } + + return map[string]xRouteBackendRefMutateTestcase{ + "backend ref tenancies defaulted": { + routeTenancy: newTestTenancy("foo.bar"), + refs: []*pbmesh.BackendReference{ + newBackendRef(catalog.ServiceType, "", "api", ""), + newBackendRef(catalog.ServiceType, ".zim", "api", ""), + newBackendRef(catalog.ServiceType, "gir.zim", "api", ""), + }, + expect: []*pbmesh.BackendReference{ + newBackendRef(catalog.ServiceType, "foo.bar", "api", ""), + newBackendRef(catalog.ServiceType, "foo.zim", "api", ""), + newBackendRef(catalog.ServiceType, "gir.zim", "api", ""), + }, + }, + } +} + type xRouteParentRefTestcase struct { refs []*pbmesh.ParentReference expectErr string @@ -786,7 +917,7 @@ func getXRouteParentRefTestCases() map[string]xRouteParentRefTestcase { newParentRef(catalog.ServiceType, "api", ""), newParentRef(catalog.WorkloadType, "api", ""), }, - expectErr: `invalid element at index 1 of list "parent_refs": invalid "ref" field: reference must have type catalog.v1alpha1.Service`, + expectErr: `invalid element at index 1 of list "parent_refs": invalid "ref" field: invalid "type" field: reference must have type catalog.v1alpha1.Service`, }, "parent ref with section": { refs: []*pbmesh.ParentReference{ @@ -796,35 +927,35 @@ func getXRouteParentRefTestCases() map[string]xRouteParentRefTestcase { Port: "http", }, }, - expectErr: `invalid element at index 1 of list "parent_refs": invalid "ref" field: invalid "section" field: section not supported for service parent refs`, + expectErr: `invalid element at index 1 of list "parent_refs": invalid "ref" field: invalid "section" field: section cannot be set here`, }, "duplicate exact parents": { refs: []*pbmesh.ParentReference{ newParentRef(catalog.ServiceType, "api", "http"), newParentRef(catalog.ServiceType, "api", "http"), }, - expectErr: `invalid element at index 1 of list "parent_refs": invalid "ref" field: parent ref "catalog.v1alpha1.Service/default.local.default/api" for port "http" exists twice`, + expectErr: `invalid element at index 1 of list "parent_refs": invalid "port" field: parent ref "catalog.v1alpha1.Service/default.local.default/api" for port "http" exists twice`, }, "duplicate wild parents": { refs: []*pbmesh.ParentReference{ newParentRef(catalog.ServiceType, "api", ""), newParentRef(catalog.ServiceType, "api", ""), }, - expectErr: `invalid element at index 1 of list "parent_refs": invalid "ref" field: parent ref "catalog.v1alpha1.Service/default.local.default/api" for wildcard port exists twice`, + expectErr: `invalid element at index 1 of list "parent_refs": invalid "port" field: parent ref "catalog.v1alpha1.Service/default.local.default/api" for wildcard port exists twice`, }, "duplicate parents via exact+wild overlap": { refs: []*pbmesh.ParentReference{ newParentRef(catalog.ServiceType, "api", "http"), newParentRef(catalog.ServiceType, "api", ""), }, - expectErr: `invalid element at index 1 of list "parent_refs": invalid "ref" field: parent ref "catalog.v1alpha1.Service/default.local.default/api" for ports [http] covered by wildcard port already`, + expectErr: `invalid element at index 1 of list "parent_refs": invalid "port" field: parent ref "catalog.v1alpha1.Service/default.local.default/api" for ports [http] covered by wildcard port already`, }, "duplicate parents via exact+wild overlap (reversed)": { refs: []*pbmesh.ParentReference{ newParentRef(catalog.ServiceType, "api", ""), newParentRef(catalog.ServiceType, "api", "http"), }, - expectErr: `invalid element at index 1 of list "parent_refs": invalid "ref" field: parent ref "catalog.v1alpha1.Service/default.local.default/api" for port "http" covered by wildcard port already`, + expectErr: `invalid element at index 1 of list "parent_refs": invalid "port" field: parent ref "catalog.v1alpha1.Service/default.local.default/api" for port "http" covered by wildcard port already`, }, "good single parent ref": { refs: []*pbmesh.ParentReference{ @@ -865,7 +996,7 @@ func getXRouteBackendRefTestCases() map[string]xRouteBackendRefTestcase { newBackendRef(catalog.ServiceType, "api", ""), newBackendRef(catalog.WorkloadType, "api", ""), }, - expectErr: `invalid element at index 0 of list "rules": invalid element at index 1 of list "backend_refs": invalid "backend_ref" field: invalid "ref" field: reference must have type catalog.v1alpha1.Service`, + expectErr: `invalid element at index 0 of list "rules": invalid element at index 1 of list "backend_refs": invalid "backend_ref" field: invalid "ref" field: invalid "type" field: reference must have type catalog.v1alpha1.Service`, }, "backend ref with section": { refs: []*pbmesh.BackendReference{ @@ -875,7 +1006,7 @@ func getXRouteBackendRefTestCases() map[string]xRouteBackendRefTestcase { Port: "http", }, }, - expectErr: `invalid element at index 0 of list "rules": invalid element at index 1 of list "backend_refs": invalid "backend_ref" field: invalid "ref" field: invalid "section" field: section not supported for service backend refs`, + expectErr: `invalid element at index 0 of list "rules": invalid element at index 1 of list "backend_refs": invalid "backend_ref" field: invalid "ref" field: invalid "section" field: section cannot be set here`, }, "backend ref with datacenter": { refs: []*pbmesh.BackendReference{ @@ -958,8 +1089,15 @@ func getXRouteRetriesTestCases() map[string]xRouteRetriesTestcase { } func newRef(typ *pbresource.Type, name string) *pbresource.Reference { + return newRefWithTenancy(typ, nil, name) +} + +func newRefWithTenancy(typ *pbresource.Type, tenancy *pbresource.Tenancy, name string) *pbresource.Reference { + if tenancy == nil { + tenancy = resource.DefaultNamespacedTenancy() + } return resourcetest.Resource(typ, name). - WithTenancy(resource.DefaultNamespacedTenancy()). + WithTenancy(tenancy). Reference("") } @@ -971,8 +1109,31 @@ func newBackendRef(typ *pbresource.Type, name, port string) *pbmesh.BackendRefer } func newParentRef(typ *pbresource.Type, name, port string) *pbmesh.ParentReference { + return newParentRefWithTenancy(typ, nil, name, port) +} + +func newParentRefWithTenancy(typ *pbresource.Type, tenancy *pbresource.Tenancy, name, port string) *pbmesh.ParentReference { return &pbmesh.ParentReference{ - Ref: newRef(typ, name), + Ref: newRefWithTenancy(typ, tenancy, name), Port: port, } } + +func newTestTenancy(s string) *pbresource.Tenancy { + parts := strings.Split(s, ".") + switch len(parts) { + case 0: + return resource.DefaultClusteredTenancy() + case 1: + v := resource.DefaultPartitionedTenancy() + v.Partition = parts[0] + return v + case 2: + v := resource.DefaultNamespacedTenancy() + v.Partition = parts[0] + v.Namespace = parts[1] + return v + default: + return &pbresource.Tenancy{Partition: "BAD", Namespace: "BAD", PeerName: "BAD"} + } +} diff --git a/internal/mesh/internal/types/tcp_route.go b/internal/mesh/internal/types/tcp_route.go index 8549b1c33f37..bfd3d5235d59 100644 --- a/internal/mesh/internal/types/tcp_route.go +++ b/internal/mesh/internal/types/tcp_route.go @@ -29,14 +29,45 @@ var ( func RegisterTCPRoute(r resource.Registry) { r.Register(resource.Registration{ - Type: TCPRouteV1Alpha1Type, - Proto: &pbmesh.TCPRoute{}, - Scope: resource.ScopeNamespace, - // TODO(rb): normalize parent/backend ref tenancies in a Mutate hook + Type: TCPRouteV1Alpha1Type, + Proto: &pbmesh.TCPRoute{}, + Scope: resource.ScopeNamespace, + Mutate: MutateTCPRoute, Validate: ValidateTCPRoute, }) } +func MutateTCPRoute(res *pbresource.Resource) error { + var route pbmesh.TCPRoute + + if err := res.Data.UnmarshalTo(&route); err != nil { + return resource.NewErrDataParse(&route, err) + } + + changed := false + + if mutateParentRefs(res.Id.Tenancy, route.ParentRefs) { + changed = true + } + + for _, rule := range route.Rules { + for _, backend := range rule.BackendRefs { + if backend.BackendRef == nil || backend.BackendRef.Ref == nil { + continue + } + if mutateXRouteRef(res.Id.Tenancy, backend.BackendRef.Ref) { + changed = true + } + } + } + + if !changed { + return nil + } + + return res.Data.MarshalFrom(&route) +} + func ValidateTCPRoute(res *pbresource.Resource) error { var route pbmesh.TCPRoute @@ -67,14 +98,6 @@ func ValidateTCPRoute(res *pbresource.Resource) error { } if len(rule.BackendRefs) == 0 { - /* - BackendRefs (optional)¶ - - BackendRefs defines API objects where matching requests should be - sent. If unspecified, the rule performs no forwarding. If - unspecified and no filters are specified that would result in a - response being sent, a 404 error code is returned. - */ merr = multierror.Append(merr, wrapRuleErr( resource.ErrInvalidField{ Name: "backend_refs", @@ -90,13 +113,15 @@ func ValidateTCPRoute(res *pbresource.Resource) error { Wrapped: err, }) } - for _, err := range validateBackendRef(hbref.BackendRef) { - merr = multierror.Append(merr, wrapBackendRefErr( - resource.ErrInvalidField{ - Name: "backend_ref", - Wrapped: err, - }, - )) + + wrapBackendRefFieldErr := func(err error) error { + return wrapBackendRefErr(resource.ErrInvalidField{ + Name: "backend_ref", + Wrapped: err, + }) + } + if err := validateBackendRef(hbref.BackendRef, wrapBackendRefFieldErr); err != nil { + merr = multierror.Append(merr, err) } } } diff --git a/internal/mesh/internal/types/tcp_route_test.go b/internal/mesh/internal/types/tcp_route_test.go index 6469ada83b50..1f667adde191 100644 --- a/internal/mesh/internal/types/tcp_route_test.go +++ b/internal/mesh/internal/types/tcp_route_test.go @@ -7,15 +7,100 @@ import ( "testing" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" "github.com/hashicorp/consul/internal/catalog" "github.com/hashicorp/consul/internal/resource" "github.com/hashicorp/consul/internal/resource/resourcetest" pbmesh "github.com/hashicorp/consul/proto-public/pbmesh/v1alpha1" + "github.com/hashicorp/consul/proto-public/pbresource" "github.com/hashicorp/consul/proto/private/prototest" "github.com/hashicorp/consul/sdk/testutil" ) +func TestMutateTCPRoute(t *testing.T) { + type testcase struct { + routeTenancy *pbresource.Tenancy + route *pbmesh.TCPRoute + expect *pbmesh.TCPRoute + } + + cases := map[string]testcase{} + + // Add common parent refs test cases. + for name, parentTC := range getXRouteParentRefMutateTestCases() { + cases["parent-ref: "+name] = testcase{ + routeTenancy: parentTC.routeTenancy, + route: &pbmesh.TCPRoute{ + ParentRefs: parentTC.refs, + }, + expect: &pbmesh.TCPRoute{ + ParentRefs: parentTC.expect, + }, + } + } + // add common backend ref test cases. + for name, backendTC := range getXRouteBackendRefMutateTestCases() { + var ( + refs []*pbmesh.TCPBackendRef + expect []*pbmesh.TCPBackendRef + ) + for _, br := range backendTC.refs { + refs = append(refs, &pbmesh.TCPBackendRef{ + BackendRef: br, + }) + } + for _, br := range backendTC.expect { + expect = append(expect, &pbmesh.TCPBackendRef{ + BackendRef: br, + }) + } + cases["backend-ref: "+name] = testcase{ + routeTenancy: backendTC.routeTenancy, + route: &pbmesh.TCPRoute{ + ParentRefs: []*pbmesh.ParentReference{ + newParentRef(catalog.ServiceType, "web", ""), + }, + Rules: []*pbmesh.TCPRouteRule{ + {BackendRefs: refs}, + }, + }, + expect: &pbmesh.TCPRoute{ + ParentRefs: []*pbmesh.ParentReference{ + newParentRef(catalog.ServiceType, "web", ""), + }, + Rules: []*pbmesh.TCPRouteRule{ + {BackendRefs: expect}, + }, + }, + } + } + + run := func(t *testing.T, tc testcase) { + res := resourcetest.Resource(TCPRouteType, "api"). + WithTenancy(tc.routeTenancy). + WithData(t, tc.route). + Build() + + err := MutateTCPRoute(res) + require.NoError(t, err) + + got := resourcetest.MustDecode[*pbmesh.TCPRoute](t, res) + + if tc.expect == nil { + tc.expect = proto.Clone(tc.route).(*pbmesh.TCPRoute) + } + + prototest.AssertDeepEqual(t, tc.expect, got.Data) + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + run(t, tc) + }) + } +} + func TestValidateTCPRoute(t *testing.T) { type testcase struct { route *pbmesh.TCPRoute @@ -28,7 +113,15 @@ func TestValidateTCPRoute(t *testing.T) { WithData(t, tc.route). Build() - err := ValidateTCPRoute(res) + // Ensure things are properly mutated and updated in the inputs. + err := MutateTCPRoute(res) + require.NoError(t, err) + { + mutated := resourcetest.MustDecode[*pbmesh.TCPRoute](t, res) + tc.route = mutated.Data + } + + err = ValidateTCPRoute(res) // Verify that validate didn't actually change the object. got := resourcetest.MustDecode[*pbmesh.TCPRoute](t, res) diff --git a/internal/mesh/internal/types/upstreams.go b/internal/mesh/internal/types/upstreams.go index 1ef73e181e01..54baeceebff7 100644 --- a/internal/mesh/internal/types/upstreams.go +++ b/internal/mesh/internal/types/upstreams.go @@ -4,6 +4,10 @@ package types import ( + "github.com/hashicorp/go-multierror" + "google.golang.org/protobuf/proto" + + "github.com/hashicorp/consul/internal/catalog" "github.com/hashicorp/consul/internal/resource" pbmesh "github.com/hashicorp/consul/proto-public/pbmesh/v1alpha1" "github.com/hashicorp/consul/proto-public/pbresource" @@ -28,6 +32,86 @@ func RegisterUpstreams(r resource.Registry) { Type: UpstreamsV1Alpha1Type, Proto: &pbmesh.Upstreams{}, Scope: resource.ScopeNamespace, - Validate: nil, + Mutate: MutateUpstreams, + Validate: ValidateUpstreams, }) } + +func MutateUpstreams(res *pbresource.Resource) error { + var destinations pbmesh.Upstreams + + if err := res.Data.UnmarshalTo(&destinations); err != nil { + return resource.NewErrDataParse(&destinations, err) + } + + changed := false + + for _, dest := range destinations.Upstreams { + if dest.DestinationRef == nil { + continue // skip; let the validation hook error out instead + } + if dest.DestinationRef.Tenancy != nil && !isLocalPeer(dest.DestinationRef.Tenancy.PeerName) { + // TODO(peering/v2): remove this bypass when we know what to do with + // non-local peer references. + continue + } + orig := proto.Clone(dest.DestinationRef).(*pbresource.Reference) + resource.DefaultReferenceTenancy( + dest.DestinationRef, + res.Id.GetTenancy(), + resource.DefaultNamespacedTenancy(), // Services are all namespace scoped. + ) + + if !proto.Equal(orig, dest.DestinationRef) { + changed = true + } + } + + if !changed { + return nil + } + + return res.Data.MarshalFrom(&destinations) +} + +func isLocalPeer(p string) bool { + return p == "local" || p == "" +} + +func ValidateUpstreams(res *pbresource.Resource) error { + var destinations pbmesh.Upstreams + + if err := res.Data.UnmarshalTo(&destinations); err != nil { + return resource.NewErrDataParse(&destinations, err) + } + + var merr error + + for i, dest := range destinations.Upstreams { + wrapDestErr := func(err error) error { + return resource.ErrInvalidListElement{ + Name: "upstreams", + Index: i, + Wrapped: err, + } + } + + wrapRefErr := func(err error) error { + return wrapDestErr(resource.ErrInvalidField{ + Name: "destination_ref", + Wrapped: err, + }) + } + + if refErr := catalog.ValidateLocalServiceRefNoSection(dest.DestinationRef, wrapRefErr); refErr != nil { + merr = multierror.Append(merr, refErr) + } + + // TODO(v2): validate port name using catalog validator + // TODO(v2): validate ListenAddr + } + + // TODO(v2): validate workload selectors + + return merr +} diff --git a/internal/mesh/internal/types/upstreams_test.go b/internal/mesh/internal/types/upstreams_test.go new file mode 100644 index 000000000000..ca4b2ce9b4f1 --- /dev/null +++ b/internal/mesh/internal/types/upstreams_test.go @@ -0,0 +1,198 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package types + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/hashicorp/consul/internal/catalog" + "github.com/hashicorp/consul/internal/resource" + "github.com/hashicorp/consul/internal/resource/resourcetest" + pbmesh "github.com/hashicorp/consul/proto-public/pbmesh/v1alpha1" + "github.com/hashicorp/consul/proto-public/pbresource" + "github.com/hashicorp/consul/proto/private/prototest" + "github.com/hashicorp/consul/sdk/testutil" +) + +func TestMutateUpstreams(t *testing.T) { + type testcase struct { + tenancy *pbresource.Tenancy + data *pbmesh.Upstreams + expect *pbmesh.Upstreams + expectErr string + } + + run := func(t *testing.T, tc testcase) { + res := resourcetest.Resource(UpstreamsType, "api"). + WithTenancy(tc.tenancy). + WithData(t, tc.data). + Build() + + err := MutateUpstreams(res) + + got := resourcetest.MustDecode[*pbmesh.Upstreams](t, res) + + if tc.expectErr == "" { + require.NoError(t, err) + prototest.AssertDeepEqual(t, tc.expect, got.Data) + } else { + testutil.RequireErrorContains(t, err, tc.expectErr) + } + } + + cases := map[string]testcase{ + "empty-1": { + data: &pbmesh.Upstreams{}, + expect: &pbmesh.Upstreams{}, + }, + "invalid/nil dest ref": { + data: &pbmesh.Upstreams{ + Upstreams: []*pbmesh.Upstream{ + {DestinationRef: nil}, + }, + }, + expect: &pbmesh.Upstreams{ // untouched + Upstreams: []*pbmesh.Upstream{ + {DestinationRef: nil}, + }, + }, + }, + "dest ref tenancy defaulting": { + tenancy: newTestTenancy("foo.bar"), + data: &pbmesh.Upstreams{ + Upstreams: []*pbmesh.Upstream{ + {DestinationRef: newRefWithTenancy(catalog.ServiceType, newTestTenancy(""), "api")}, + {DestinationRef: newRefWithTenancy(catalog.ServiceType, newTestTenancy(".zim"), "api")}, + {DestinationRef: newRefWithTenancy(catalog.ServiceType, newTestTenancy("gir.zim"), "api")}, + }, + }, + expect: &pbmesh.Upstreams{ + Upstreams: []*pbmesh.Upstream{ + {DestinationRef: newRefWithTenancy(catalog.ServiceType, newTestTenancy("foo.bar"), "api")}, + {DestinationRef: newRefWithTenancy(catalog.ServiceType, newTestTenancy("foo.zim"), "api")}, + {DestinationRef: newRefWithTenancy(catalog.ServiceType, newTestTenancy("gir.zim"), "api")}, + }, + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + run(t, tc) + }) + } +} + +func TestValidateUpstreams(t *testing.T) { + type testcase struct { + data *pbmesh.Upstreams + skipMutate bool + expectErr string + } + + run := func(t *testing.T, tc testcase) { + res := resourcetest.Resource(UpstreamsType, "api"). + WithTenancy(resource.DefaultNamespacedTenancy()). + WithData(t, tc.data). + Build() + + if !tc.skipMutate { + require.NoError(t, MutateUpstreams(res)) + + // Verify that mutate didn't actually change the object. + got := resourcetest.MustDecode[*pbmesh.Upstreams](t, res) + prototest.AssertDeepEqual(t, tc.data, got.Data) + } + + err := ValidateUpstreams(res) + + // Verify that validate didn't actually change the object. + got := resourcetest.MustDecode[*pbmesh.Upstreams](t, res) + prototest.AssertDeepEqual(t, tc.data, got.Data) + + if tc.expectErr == "" { + require.NoError(t, err) + } else { + testutil.RequireErrorContains(t, err, tc.expectErr) + } + } + + cases := map[string]testcase{ + // emptiness + "empty": { + data: &pbmesh.Upstreams{}, + }, + "dest/nil ref": { + skipMutate: true, + data: &pbmesh.Upstreams{ + Upstreams: []*pbmesh.Upstream{ + {DestinationRef: nil}, + }, + }, + expectErr: `invalid element at index 0 of list "upstreams": invalid "destination_ref" field: missing required field`, + }, + "dest/bad type": { + skipMutate: true, + data: &pbmesh.Upstreams{ + Upstreams: []*pbmesh.Upstream{ + {DestinationRef: newRefWithTenancy(catalog.WorkloadType, nil, "api")}, + }, + }, + expectErr: `invalid element at index 0 of list "upstreams": invalid "destination_ref" field: invalid "type" field: reference must have type catalog.v1alpha1.Service`, + }, + "dest/nil tenancy": { + skipMutate: true, + data: &pbmesh.Upstreams{ + Upstreams: []*pbmesh.Upstream{ + {DestinationRef: &pbresource.Reference{Type: catalog.ServiceType, Name: "api"}}, + }, + }, + expectErr: `invalid element at index 0 of list "upstreams": invalid "destination_ref" field: invalid "tenancy" field: missing required field`, + }, + "dest/bad dest tenancy/partition": { + skipMutate: true, + data: &pbmesh.Upstreams{ + Upstreams: []*pbmesh.Upstream{ + {DestinationRef: newRefWithTenancy(catalog.ServiceType, newTestTenancy(".bar"), "api")}, + }, + }, + expectErr: `invalid element at index 0 of list "upstreams": invalid "destination_ref" field: invalid "tenancy" field: invalid "partition" field: cannot be empty`, + }, + "dest/bad dest tenancy/namespace": { + skipMutate: true, + data: &pbmesh.Upstreams{ + Upstreams: []*pbmesh.Upstream{ + {DestinationRef: newRefWithTenancy(catalog.ServiceType, newTestTenancy("foo"), "api")}, + }, + }, + expectErr: `invalid element at index 0 of list "upstreams": invalid "destination_ref" field: invalid "tenancy" field: invalid "namespace" field: cannot be empty`, + }, + "dest/bad dest tenancy/peer_name": { + skipMutate: true, + data: &pbmesh.Upstreams{ + Upstreams: []*pbmesh.Upstream{ + {DestinationRef: newRefWithTenancy(catalog.ServiceType, &pbresource.Tenancy{Partition: "foo", Namespace: "bar"}, "api")}, + }, + }, + expectErr: `invalid element at index 0 of list "upstreams": invalid "destination_ref" field: invalid "tenancy" field: invalid "peer_name" field: must be set to "local"`, + }, + "normal": { + data: &pbmesh.Upstreams{ + Upstreams: []*pbmesh.Upstream{ + {DestinationRef: newRefWithTenancy(catalog.ServiceType, newTestTenancy("foo.bar"), "api")}, + {DestinationRef: newRefWithTenancy(catalog.ServiceType, newTestTenancy("foo.zim"), "api")}, + {DestinationRef: newRefWithTenancy(catalog.ServiceType, newTestTenancy("gir.zim"), "api")}, + }, + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + run(t, tc) + }) + } +} diff --git a/internal/mesh/internal/types/xroute.go b/internal/mesh/internal/types/xroute.go index b29f9a61064b..dec2290179a2 100644 --- a/internal/mesh/internal/types/xroute.go +++ b/internal/mesh/internal/types/xroute.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/consul/internal/catalog" "github.com/hashicorp/consul/internal/resource" pbmesh "github.com/hashicorp/consul/proto-public/pbmesh/v1alpha1" + "github.com/hashicorp/consul/proto-public/pbresource" ) type XRouteData interface { @@ -30,6 +31,33 @@ type portedRefKey struct { Port string } +func mutateParentRefs(xrouteTenancy *pbresource.Tenancy, parentRefs []*pbmesh.ParentReference) (changed bool) { + for _, parent := range parentRefs { + if parent.Ref == nil { + continue + } + changedThis := mutateXRouteRef(xrouteTenancy, parent.Ref) + if changedThis { + changed = true + } + } + return changed +} + +func mutateXRouteRef(xrouteTenancy *pbresource.Tenancy, ref *pbresource.Reference) (changed bool) { + if ref == nil { + return false + } + orig := proto.Clone(ref).(*pbresource.Reference) + resource.DefaultReferenceTenancy( + ref, + xrouteTenancy, + resource.DefaultNamespacedTenancy(), // All xRoutes are namespace scoped. + ) + + return !proto.Equal(orig, ref) +} + func validateParentRefs(parentRefs []*pbmesh.ParentReference) error { var merr error if len(parentRefs) == 0 { @@ -51,46 +79,17 @@ func validateParentRefs(parentRefs []*pbmesh.ParentReference) error { Wrapped: err, } } - if parent.Ref == nil { - merr = multierror.Append(merr, wrapErr( - resource.ErrInvalidField{ - Name: "ref", - Wrapped: resource.ErrMissing, - }, - )) - } else { - if !IsServiceType(parent.Ref.Type) { - merr = multierror.Append(merr, wrapErr( - resource.ErrInvalidField{ - Name: "ref", - Wrapped: resource.ErrInvalidReferenceType{ - AllowedType: catalog.ServiceType, - }, - }, - )) - } - if parent.Ref.Section != "" { - merr = multierror.Append(merr, wrapErr( - resource.ErrInvalidField{ - Name: "ref", - Wrapped: resource.ErrInvalidField{ - Name: "section", - Wrapped: errors.New("section not supported for service parent refs"), - }, - }, - )) - } - if parent.Ref.Name == "" { - merr = multierror.Append(merr, resource.ErrInvalidField{ - Name: "ref", - Wrapped: resource.ErrInvalidField{ - Name: "name", - Wrapped: resource.ErrMissing, - }, - }) - } + wrapRefErr := func(err error) error { + return wrapErr(resource.ErrInvalidField{ + Name: "ref", + Wrapped: err, + }) + } + if err := catalog.ValidateLocalServiceRefNoSection(parent.Ref, wrapRefErr); err != nil { + merr = multierror.Append(merr, err) + } else { prk := portedRefKey{ Key: resource.NewReferenceKey(parent.Ref), Port: parent.Port, @@ -104,7 +103,7 @@ func validateParentRefs(parentRefs []*pbmesh.ParentReference) error { if portExist { // check for duplicate wild merr = multierror.Append(merr, wrapErr( resource.ErrInvalidField{ - Name: "ref", + Name: "port", Wrapped: fmt.Errorf( "parent ref %q for wildcard port exists twice", resource.ReferenceToString(parent.Ref), @@ -114,7 +113,7 @@ func validateParentRefs(parentRefs []*pbmesh.ParentReference) error { } else if exactExists { // check for existing exact merr = multierror.Append(merr, wrapErr( resource.ErrInvalidField{ - Name: "ref", + Name: "port", Wrapped: fmt.Errorf( "parent ref %q for ports %v covered by wildcard port already", resource.ReferenceToString(parent.Ref), @@ -134,7 +133,7 @@ func validateParentRefs(parentRefs []*pbmesh.ParentReference) error { if portExist { // check for duplicate exact merr = multierror.Append(merr, wrapErr( resource.ErrInvalidField{ - Name: "ref", + Name: "port", Wrapped: fmt.Errorf( "parent ref %q for port %q exists twice", resource.ReferenceToString(parent.Ref), @@ -145,7 +144,7 @@ func validateParentRefs(parentRefs []*pbmesh.ParentReference) error { } else if wildExist { // check for existing wild merr = multierror.Append(merr, wrapErr( resource.ErrInvalidField{ - Name: "ref", + Name: "port", Wrapped: fmt.Errorf( "parent ref %q for port %q covered by wildcard port already", resource.ReferenceToString(parent.Ref), @@ -164,55 +163,30 @@ func validateParentRefs(parentRefs []*pbmesh.ParentReference) error { return merr } -func validateBackendRef(backendRef *pbmesh.BackendReference) []error { - var errs []error +func validateBackendRef(backendRef *pbmesh.BackendReference, wrapErr func(error) error) error { if backendRef == nil { - errs = append(errs, resource.ErrMissing) + return wrapErr(resource.ErrMissing) + } + + var merr error - } else if backendRef.Ref == nil { - errs = append(errs, resource.ErrInvalidField{ + wrapRefErr := func(err error) error { + return wrapErr(resource.ErrInvalidField{ Name: "ref", - Wrapped: resource.ErrMissing, + Wrapped: err, }) - - } else { - if !IsServiceType(backendRef.Ref.Type) { - errs = append(errs, resource.ErrInvalidField{ - Name: "ref", - Wrapped: resource.ErrInvalidReferenceType{ - AllowedType: catalog.ServiceType, - }, - }) - } - - if backendRef.Ref.Name == "" { - errs = append(errs, resource.ErrInvalidField{ - Name: "ref", - Wrapped: resource.ErrInvalidField{ - Name: "name", - Wrapped: resource.ErrMissing, - }, - }) - } - - if backendRef.Ref.Section != "" { - errs = append(errs, resource.ErrInvalidField{ - Name: "ref", - Wrapped: resource.ErrInvalidField{ - Name: "section", - Wrapped: errors.New("section not supported for service backend refs"), - }, - }) - } - - if backendRef.Datacenter != "" { - errs = append(errs, resource.ErrInvalidField{ - Name: "datacenter", - Wrapped: errors.New("datacenter is not yet supported on backend refs"), - }) - } } - return errs + if err := catalog.ValidateLocalServiceRefNoSection(backendRef.Ref, wrapRefErr); err != nil { + merr = multierror.Append(merr, err) + } + if backendRef.Datacenter != "" { + merr = multierror.Append(merr, wrapErr(resource.ErrInvalidField{ + Name: "datacenter", + Wrapped: errors.New("datacenter is not yet supported on backend refs"), + })) + } + + return merr } func validateHeaderMatchType(typ pbmesh.HeaderMatchType) error {