added exported svc controller (#19589)

* added exported svc controller

* added license headers
This commit is contained in:
aahel 2023-11-10 07:27:53 +05:30 committed by GitHub
parent 4d64ef0961
commit 005e1b9926
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 763 additions and 3 deletions

View File

@ -20,6 +20,9 @@ import (
"time"
"github.com/armon/go-metrics"
"github.com/hashicorp/consul/internal/auth"
"github.com/hashicorp/consul/internal/mesh"
"github.com/hashicorp/consul/internal/multicluster"
"github.com/hashicorp/go-connlimit"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-memdb"
@ -70,10 +73,8 @@ import (
"github.com/hashicorp/consul/agent/rpc/peering"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/agent/token"
"github.com/hashicorp/consul/internal/auth"
"github.com/hashicorp/consul/internal/catalog"
"github.com/hashicorp/consul/internal/controller"
"github.com/hashicorp/consul/internal/mesh"
proxysnapshot "github.com/hashicorp/consul/internal/mesh/proxy-snapshot"
"github.com/hashicorp/consul/internal/resource"
"github.com/hashicorp/consul/internal/resource/demo"
@ -945,7 +946,7 @@ func (s *Server) registerControllers(deps Deps, proxyUpdater ProxyUpdater) error
if s.useV2Resources {
catalog.RegisterControllers(s.controllerManager, catalog.DefaultControllerDependencies())
multicluster.RegisterControllers(s.controllerManager)
defaultAllow, err := s.config.ACLResolverSettings.IsDefaultAllow()
if err != nil {
return err

View File

@ -43,4 +43,8 @@ flowchart TD
mesh/v2beta1/proxystatetemplate --> mesh/v2beta1/computedproxyconfiguration
mesh/v2beta1/proxystatetemplate --> mesh/v2beta1/computedroutes
mesh/v2beta1/tcproute
multicluster/v2beta1/computedexportedservices --> catalog/v2beta1/service
multicluster/v2beta1/computedexportedservices --> multicluster/v2beta1/exportedservices
multicluster/v2beta1/computedexportedservices --> multicluster/v2beta1/namespaceexportedservices
multicluster/v2beta1/computedexportedservices --> multicluster/v2beta1/partitionexportedservices
```

View File

@ -4,6 +4,8 @@
package multicluster
import (
"github.com/hashicorp/consul/internal/controller"
"github.com/hashicorp/consul/internal/multicluster/internal/controllers"
"github.com/hashicorp/consul/internal/multicluster/internal/types"
"github.com/hashicorp/consul/internal/resource"
)
@ -20,3 +22,9 @@ var (
func RegisterTypes(r resource.Registry) {
types.Register(r)
}
// RegisterControllers registers controllers for the multicluster types with
// the given controller Manager.
func RegisterControllers(mgr *controller.Manager) {
controllers.Register(mgr)
}

View File

@ -0,0 +1,295 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package exportedservices
import (
"context"
"fmt"
"sort"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/anypb"
"github.com/hashicorp/consul/internal/controller"
"github.com/hashicorp/consul/internal/multicluster/internal/types"
"github.com/hashicorp/consul/internal/resource"
"github.com/hashicorp/consul/internal/storage"
pbcatalog "github.com/hashicorp/consul/proto-public/pbcatalog/v2beta1"
pbmulticluster "github.com/hashicorp/consul/proto-public/pbmulticluster/v2beta1"
"github.com/hashicorp/consul/proto-public/pbresource"
)
func Controller() controller.Controller {
return controller.ForType(pbmulticluster.ComputedExportedServicesType).
WithWatch(pbmulticluster.ExportedServicesType, ReplaceTypeForComputedExportedServices()).
WithWatch(pbcatalog.ServiceType, ReplaceTypeForComputedExportedServices()).
WithWatch(pbmulticluster.NamespaceExportedServicesType, ReplaceTypeForComputedExportedServices()).
WithWatch(pbmulticluster.PartitionExportedServicesType, ReplaceTypeForComputedExportedServices()).
WithReconciler(&reconciler{})
}
type reconciler struct{}
type serviceExports struct {
ref *pbresource.Reference
partitions map[string]struct{}
peers map[string]struct{}
}
func (r *reconciler) Reconcile(ctx context.Context, rt controller.Runtime, req controller.Request) error {
rt.Logger = rt.Logger.With("resource-id", req.ID)
rt.Logger.Trace("reconciling exported services")
exportedServices, err := resource.ListDecodedResource[*pbmulticluster.ExportedServices](ctx, rt.Client, &pbresource.ListRequest{
Tenancy: &pbresource.Tenancy{
Namespace: storage.Wildcard,
Partition: req.ID.Tenancy.Partition,
PeerName: resource.DefaultPeerName,
},
Type: pbmulticluster.ExportedServicesType,
})
if err != nil {
rt.Logger.Error("error getting exported services", "error", err)
return err
}
namespaceExportedServices, err := resource.ListDecodedResource[*pbmulticluster.NamespaceExportedServices](ctx, rt.Client, &pbresource.ListRequest{
Tenancy: &pbresource.Tenancy{
Namespace: storage.Wildcard,
Partition: req.ID.Tenancy.Partition,
PeerName: resource.DefaultPeerName,
},
Type: pbmulticluster.NamespaceExportedServicesType,
})
if err != nil {
rt.Logger.Error("error getting namespace exported services", "error", err)
return err
}
partitionedExportedServices, err := resource.ListDecodedResource[*pbmulticluster.PartitionExportedServices](ctx, rt.Client, &pbresource.ListRequest{
Tenancy: &pbresource.Tenancy{
Partition: req.ID.Tenancy.Partition,
PeerName: resource.DefaultPeerName,
},
Type: pbmulticluster.PartitionExportedServicesType,
})
if err != nil {
rt.Logger.Error("error getting partitioned exported services", "error", err)
return err
}
//TODO: we are listing more services than required. this should be optimized
services, err := resource.ListDecodedResource[*pbcatalog.Service](ctx, rt.Client, &pbresource.ListRequest{
Tenancy: &pbresource.Tenancy{
Namespace: storage.Wildcard,
Partition: req.ID.Tenancy.Partition,
PeerName: resource.DefaultPeerName,
},
Type: pbcatalog.ServiceType,
})
if err != nil {
rt.Logger.Error("error getting services", "error", err)
return err
}
builder := newExportedServicesBuilder()
svcs := make(map[resource.ReferenceKey]struct{}, len(services))
for _, svc := range services {
svcs[resource.NewReferenceKey(svc.Id)] = struct{}{}
}
for _, es := range exportedServices {
for _, svc := range es.Data.Services {
id := &pbresource.ID{
Type: pbcatalog.ServiceType,
Tenancy: es.Id.Tenancy,
Name: svc,
}
if _, ok := svcs[resource.NewReferenceKey(id)]; ok {
builder.track(id, es.Data.Consumers)
}
}
}
for _, nes := range namespaceExportedServices {
for _, svc := range services {
if svc.Id.Tenancy.Namespace != nes.Id.Tenancy.Namespace {
continue
}
builder.track(svc.Id, nes.Data.Consumers)
}
}
for _, pes := range partitionedExportedServices {
for _, svc := range services {
builder.track(svc.Id, pes.Data.Consumers)
}
}
oldComputedExportedService, err := getOldComputedExportedService(ctx, rt, req)
if err != nil {
return err
}
newComputedExportedService := builder.build()
if oldComputedExportedService.GetResource() != nil && newComputedExportedService == nil {
rt.Logger.Trace("deleting computed exported services")
_, err := rt.Client.Delete(ctx, &pbresource.DeleteRequest{
Id: oldComputedExportedService.GetResource().GetId(),
Version: oldComputedExportedService.GetResource().GetVersion(),
})
if err != nil {
rt.Logger.Error("error deleting computed exported service", "error", err)
return err
}
return nil
}
if proto.Equal(newComputedExportedService, oldComputedExportedService.GetData()) {
rt.Logger.Trace("skip writing computed exported services")
return nil
}
newComputedExportedServiceData, err := anypb.New(newComputedExportedService)
if err != nil {
rt.Logger.Error("error marshalling latest computed exported service", "error", err)
return err
}
rt.Logger.Trace("writing computed exported services")
_, err = rt.Client.Write(ctx, &pbresource.WriteRequest{
Resource: &pbresource.Resource{
Id: req.ID,
Owner: nil,
Data: newComputedExportedServiceData,
},
})
if err != nil {
rt.Logger.Error("error writing computed exported service", "error", err)
return err
}
return nil
}
func ReplaceTypeForComputedExportedServices() controller.DependencyMapper {
return func(_ context.Context, _ controller.Runtime, res *pbresource.Resource) ([]controller.Request, error) {
return []controller.Request{
{
ID: &pbresource.ID{
Type: pbmulticluster.ComputedExportedServicesType,
Tenancy: &pbresource.Tenancy{
Partition: res.Id.Tenancy.Partition,
},
Name: "global",
},
},
}, nil
}
}
func getOldComputedExportedService(ctx context.Context, rt controller.Runtime, req controller.Request) (*resource.DecodedResource[*pbmulticluster.ComputedExportedServices], error) {
computedExpSvcID := &pbresource.ID{
Name: types.ComputedExportedServicesName,
Type: pbmulticluster.ComputedExportedServicesType,
Tenancy: &pbresource.Tenancy{
Partition: req.ID.Tenancy.Partition,
},
}
computedExpSvcRes, err := resource.GetDecodedResource[*pbmulticluster.ComputedExportedServices](ctx, rt.Client, computedExpSvcID)
if err != nil {
rt.Logger.Error("error fetching computed exported service", "error", err)
return nil, err
}
return computedExpSvcRes, nil
}
type exportedServicesBuilder struct {
data map[resource.ReferenceKey]*serviceExports
}
func newExportedServicesBuilder() *exportedServicesBuilder {
return &exportedServicesBuilder{
data: make(map[resource.ReferenceKey]*serviceExports),
}
}
func (b *exportedServicesBuilder) track(id *pbresource.ID, consumers []*pbmulticluster.ExportedServicesConsumer) error {
key := resource.NewReferenceKey(id)
exports, ok := b.data[key]
if !ok {
exports = &serviceExports{
ref: resource.Reference(id, ""),
partitions: make(map[string]struct{}),
peers: make(map[string]struct{}),
}
b.data[key] = exports
}
for _, c := range consumers {
switch v := c.ConsumerTenancy.(type) {
case *pbmulticluster.ExportedServicesConsumer_Peer:
exports.peers[v.Peer] = struct{}{}
case *pbmulticluster.ExportedServicesConsumer_Partition:
exports.partitions[v.Partition] = struct{}{}
case *pbmulticluster.ExportedServicesConsumer_SamenessGroup:
// TODO do we currently validate that sameness groups can't be set?
return fmt.Errorf("unexpected export to sameness group %q", v.SamenessGroup)
}
}
return nil
}
func (b *exportedServicesBuilder) build() *pbmulticluster.ComputedExportedServices {
if len(b.data) == 0 {
return nil
}
ces := &pbmulticluster.ComputedExportedServices{
Consumers: make([]*pbmulticluster.ComputedExportedService, 0, len(b.data)),
}
for _, svc := range sortRefValue(b.data) {
consumers := make([]*pbmulticluster.ComputedExportedServicesConsumer, 0, len(svc.peers)+len(svc.partitions))
for _, peer := range sortKeys(svc.peers) {
consumers = append(consumers, &pbmulticluster.ComputedExportedServicesConsumer{
ConsumerTenancy: &pbmulticluster.ComputedExportedServicesConsumer_Peer{
Peer: peer,
},
})
}
for _, partition := range sortKeys(svc.partitions) {
consumers = append(consumers, &pbmulticluster.ComputedExportedServicesConsumer{
ConsumerTenancy: &pbmulticluster.ComputedExportedServicesConsumer_Partition{
Partition: partition,
},
})
}
ces.Consumers = append(ces.Consumers, &pbmulticluster.ComputedExportedService{
TargetRef: svc.ref,
Consumers: consumers,
})
}
return ces
}
func sortKeys(m map[string]struct{}) []string {
keys := make([]string, 0, len(m))
for key := range m {
keys = append(keys, key)
}
sort.Strings(keys)
return keys
}
func sortRefValue(m map[resource.ReferenceKey]*serviceExports) []*serviceExports {
vals := make([]*serviceExports, 0, len(m))
for _, val := range m {
vals = append(vals, val)
}
sort.Slice(vals, func(i, j int) bool {
return resource.ReferenceToString(vals[i].ref) < resource.ReferenceToString(vals[j].ref)
})
return vals
}

View File

@ -0,0 +1,422 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package exportedservices
import (
"context"
"fmt"
"testing"
svc "github.com/hashicorp/consul/agent/grpc-external/services/resource"
svctest "github.com/hashicorp/consul/agent/grpc-external/services/resource/testing"
"github.com/hashicorp/consul/agent/structs"
cat "github.com/hashicorp/consul/internal/catalog"
"github.com/hashicorp/consul/internal/controller"
"github.com/hashicorp/consul/internal/multicluster/internal/types"
"github.com/hashicorp/consul/internal/resource"
rtest "github.com/hashicorp/consul/internal/resource/resourcetest"
pbcatalog "github.com/hashicorp/consul/proto-public/pbcatalog/v2beta1"
pbmulticluster "github.com/hashicorp/consul/proto-public/pbmulticluster/v2beta1"
"github.com/hashicorp/consul/proto-public/pbresource"
"github.com/hashicorp/consul/proto/private/prototest"
"google.golang.org/protobuf/proto"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
type controllerSuite struct {
suite.Suite
ctx context.Context
client *rtest.Client
rt controller.Runtime
isEnterprise bool
reconciler *reconciler
tenancies []*pbresource.Tenancy
}
func (suite *controllerSuite) SetupTest() {
suite.ctx = testutil.TestContext(suite.T())
suite.tenancies = rtest.TestTenancies()
mockTenancyBridge := &svc.MockTenancyBridge{}
for _, tenancy := range suite.tenancies {
mockTenancyBridge.On("PartitionExists", tenancy.Partition).Return(true, nil)
mockTenancyBridge.On("IsPartitionMarkedForDeletion", tenancy.Partition).Return(false, nil)
mockTenancyBridge.On("NamespaceExists", tenancy.Partition, tenancy.Namespace).Return(true, nil)
mockTenancyBridge.On("IsNamespaceMarkedForDeletion", tenancy.Partition, tenancy.Namespace).Return(false, nil)
mockTenancyBridge.On("NamespaceExists", tenancy.Partition, "app").Return(true, nil)
mockTenancyBridge.On("IsNamespaceMarkedForDeletion", tenancy.Partition, "app").Return(false, nil)
}
config := svc.Config{
TenancyBridge: mockTenancyBridge,
}
client := svctest.RunResourceServiceWithConfig(suite.T(), config, types.Register, cat.RegisterTypes)
suite.client = rtest.NewClient(client)
suite.rt = controller.Runtime{
Client: suite.client,
Logger: testutil.Logger(suite.T()),
}
suite.reconciler = &reconciler{}
suite.isEnterprise = (structs.NodeEnterpriseMetaInDefaultPartition().PartitionOrEmpty() == "default")
}
func (suite *controllerSuite) TestReconcile() {
suite.runTestCaseWithTenancies(suite.reconcileTest)
}
func TestController(t *testing.T) {
suite.Run(t, new(controllerSuite))
}
func (suite *controllerSuite) runTestCaseWithTenancies(testFunc func(*pbresource.Tenancy)) {
for _, tenancy := range suite.tenancies {
suite.Run(suite.appendTenancyInfo(tenancy), func() {
testFunc(tenancy)
})
}
}
func (suite *controllerSuite) appendTenancyInfo(tenancy *pbresource.Tenancy) string {
return fmt.Sprintf("%s_Namespace_%s_Partition", tenancy.Namespace, tenancy.Partition)
}
func removeService(consumers []*pbmulticluster.ComputedExportedService, ref *pbresource.Reference) []*pbmulticluster.ComputedExportedService {
newConsumers := []*pbmulticluster.ComputedExportedService{}
for _, consumer := range consumers {
if !proto.Equal(consumer.TargetRef, ref) {
newConsumers = append(newConsumers, consumer)
}
}
return newConsumers
}
func (suite *controllerSuite) getComputedExportedSvc(id *pbresource.ID) *pbmulticluster.ComputedExportedServices {
computedExportedService := suite.client.RequireResourceExists(suite.T(), id)
decodedComputedExportedService := rtest.MustDecode[*pbmulticluster.ComputedExportedServices](suite.T(), computedExportedService)
return decodedComputedExportedService.Data
}
var svc0, svc2, svc3, svc4, svc5 *pbresource.Resource
func (suite *controllerSuite) reconcileTest(tenancy *pbresource.Tenancy) {
id := rtest.Resource(pbmulticluster.ComputedExportedServicesType, "global").WithTenancy(&pbresource.Tenancy{
Partition: tenancy.Partition,
}).ID()
require.NotNil(suite.T(), id)
rtest.Resource(pbcatalog.ServiceType, "svc1").
WithData(suite.T(), &pbcatalog.Service{
Ports: []*pbcatalog.ServicePort{
{TargetPort: "http", Protocol: pbcatalog.Protocol_PROTOCOL_HTTP},
},
}).
WithTenancy(tenancy).
Write(suite.T(), suite.client)
exportedSvcData := &pbmulticluster.ExportedServices{
Services: []string{"svc1", "svcx"},
Consumers: []*pbmulticluster.ExportedServicesConsumer{{ConsumerTenancy: &pbmulticluster.ExportedServicesConsumer_Peer{
Peer: "peer-0",
}}},
}
if suite.isEnterprise {
exportedSvcData.Consumers = append(exportedSvcData.Consumers, &pbmulticluster.ExportedServicesConsumer{ConsumerTenancy: &pbmulticluster.ExportedServicesConsumer_Partition{
Partition: "part-0",
}})
}
expSvc := rtest.Resource(pbmulticluster.ExportedServicesType, "expsvc").WithData(suite.T(), exportedSvcData).WithTenancy(tenancy).Write(suite.T(), suite.client)
require.NotNil(suite.T(), expSvc)
err := suite.reconciler.Reconcile(suite.ctx, suite.rt, controller.Request{ID: id})
require.NoError(suite.T(), err)
actualComputedExportedService := suite.getComputedExportedSvc(id)
expectedComputedExportedService := getExpectation(tenancy, suite.isEnterprise, 0)
prototest.AssertDeepEqual(suite.T(), expectedComputedExportedService, actualComputedExportedService)
svc2 = rtest.Resource(pbcatalog.ServiceType, "svc2").
WithData(suite.T(), &pbcatalog.Service{
Ports: []*pbcatalog.ServicePort{
{TargetPort: "http", Protocol: pbcatalog.Protocol_PROTOCOL_HTTP},
},
}).
WithTenancy(tenancy).
Write(suite.T(), suite.client)
svc0 = rtest.Resource(pbcatalog.ServiceType, "svc0").
WithData(suite.T(), &pbcatalog.Service{
Ports: []*pbcatalog.ServicePort{
{TargetPort: "http", Protocol: pbcatalog.Protocol_PROTOCOL_HTTP},
},
}).
WithTenancy(&pbresource.Tenancy{
Partition: tenancy.Partition,
Namespace: "app",
}).
Write(suite.T(), suite.client)
exportedNamespaceSvcData := &pbmulticluster.NamespaceExportedServices{
Consumers: []*pbmulticluster.ExportedServicesConsumer{{ConsumerTenancy: &pbmulticluster.ExportedServicesConsumer_Peer{
Peer: "peer-1",
}}},
}
nameexpSvc := rtest.Resource(pbmulticluster.NamespaceExportedServicesType, "namesvc").WithData(suite.T(), exportedNamespaceSvcData).WithTenancy(tenancy).Write(suite.T(), suite.client)
require.NotNil(suite.T(), nameexpSvc)
err = suite.reconciler.Reconcile(suite.ctx, suite.rt, controller.Request{ID: id})
require.NoError(suite.T(), err)
actualComputedExportedService = suite.getComputedExportedSvc(id)
expectedComputedExportedService = getExpectation(tenancy, suite.isEnterprise, 1)
prototest.AssertDeepEqual(suite.T(), expectedComputedExportedService, actualComputedExportedService)
svc3 = rtest.Resource(pbcatalog.ServiceType, "svc3").
WithData(suite.T(), &pbcatalog.Service{
Ports: []*pbcatalog.ServicePort{
{TargetPort: "http", Protocol: pbcatalog.Protocol_PROTOCOL_HTTP},
},
}).
WithTenancy(tenancy).
Write(suite.T(), suite.client)
err = suite.reconciler.Reconcile(suite.ctx, suite.rt, controller.Request{ID: id})
require.NoError(suite.T(), err)
actualComputedExportedService = suite.getComputedExportedSvc(id)
expectedComputedExportedService = getExpectation(tenancy, suite.isEnterprise, 2)
prototest.AssertDeepEqual(suite.T(), expectedComputedExportedService, actualComputedExportedService)
suite.client.MustDelete(suite.T(), svc3.Id)
err = suite.reconciler.Reconcile(suite.ctx, suite.rt, controller.Request{ID: id})
require.NoError(suite.T(), err)
actualComputedExportedService = suite.getComputedExportedSvc(id)
expectedComputedExportedService = getExpectation(tenancy, suite.isEnterprise, 3)
prototest.AssertDeepEqual(suite.T(), expectedComputedExportedService, actualComputedExportedService)
partitionedExportedSvcData := &pbmulticluster.PartitionExportedServices{
Consumers: []*pbmulticluster.ExportedServicesConsumer{{ConsumerTenancy: &pbmulticluster.ExportedServicesConsumer_Peer{
Peer: "peer-1",
}}, {ConsumerTenancy: &pbmulticluster.ExportedServicesConsumer_Peer{
Peer: "peer-2",
}}},
}
partexpSvc := rtest.Resource(pbmulticluster.PartitionExportedServicesType, "partsvc").WithData(suite.T(), partitionedExportedSvcData).WithTenancy(&pbresource.Tenancy{Partition: tenancy.Partition}).Write(suite.T(), suite.client)
require.NotNil(suite.T(), partexpSvc)
err = suite.reconciler.Reconcile(suite.ctx, suite.rt, controller.Request{ID: id})
require.NoError(suite.T(), err)
actualComputedExportedService = suite.getComputedExportedSvc(id)
expectedComputedExportedService = getExpectation(tenancy, suite.isEnterprise, 4)
prototest.AssertDeepEqual(suite.T(), expectedComputedExportedService, actualComputedExportedService)
svc4 = rtest.Resource(pbcatalog.ServiceType, "svc4").
WithData(suite.T(), &pbcatalog.Service{
Ports: []*pbcatalog.ServicePort{
{TargetPort: "http", Protocol: pbcatalog.Protocol_PROTOCOL_HTTP},
},
}).
WithTenancy(&pbresource.Tenancy{
Partition: tenancy.Partition,
Namespace: "app",
}).
Write(suite.T(), suite.client)
err = suite.reconciler.Reconcile(suite.ctx, suite.rt, controller.Request{ID: id})
require.NoError(suite.T(), err)
actualComputedExportedService = suite.getComputedExportedSvc(id)
expectedComputedExportedService = getExpectation(tenancy, suite.isEnterprise, 5)
prototest.AssertDeepEqual(suite.T(), expectedComputedExportedService, actualComputedExportedService)
suite.client.MustDelete(suite.T(), svc4.Id)
err = suite.reconciler.Reconcile(suite.ctx, suite.rt, controller.Request{ID: id})
require.NoError(suite.T(), err)
actualComputedExportedService = suite.getComputedExportedSvc(id)
expectedComputedExportedService = getExpectation(tenancy, suite.isEnterprise, 6)
prototest.AssertDeepEqual(suite.T(), expectedComputedExportedService, actualComputedExportedService)
svc5 = rtest.Resource(pbcatalog.ServiceType, "svc5").
WithData(suite.T(), &pbcatalog.Service{
Ports: []*pbcatalog.ServicePort{
{TargetPort: "http", Protocol: pbcatalog.Protocol_PROTOCOL_HTTP},
},
}).
WithTenancy(tenancy).
Write(suite.T(), suite.client)
err = suite.reconciler.Reconcile(suite.ctx, suite.rt, controller.Request{ID: id})
require.NoError(suite.T(), err)
actualComputedExportedService = suite.getComputedExportedSvc(id)
expectedComputedExportedService = getExpectation(tenancy, suite.isEnterprise, 7)
prototest.AssertDeepEqual(suite.T(), expectedComputedExportedService, actualComputedExportedService)
suite.client.MustDelete(suite.T(), partexpSvc.Id)
err = suite.reconciler.Reconcile(suite.ctx, suite.rt, controller.Request{ID: id})
require.NoError(suite.T(), err)
actualComputedExportedService = suite.getComputedExportedSvc(id)
expectedComputedExportedService = getExpectation(tenancy, suite.isEnterprise, 8)
prototest.AssertDeepEqual(suite.T(), expectedComputedExportedService, actualComputedExportedService)
suite.client.MustDelete(suite.T(), nameexpSvc.Id)
err = suite.reconciler.Reconcile(suite.ctx, suite.rt, controller.Request{ID: id})
require.NoError(suite.T(), err)
actualComputedExportedService = suite.getComputedExportedSvc(id)
expectedComputedExportedService = getExpectation(tenancy, suite.isEnterprise, 9)
prototest.AssertDeepEqual(suite.T(), expectedComputedExportedService, actualComputedExportedService)
suite.client.MustDelete(suite.T(), expSvc.Id)
err = suite.reconciler.Reconcile(suite.ctx, suite.rt, controller.Request{ID: id})
require.NoError(suite.T(), err)
suite.client.RequireResourceNotFound(suite.T(), id)
}
func getExpectation(tenancy *pbresource.Tenancy, isEnterprise bool, testCase int) *pbmulticluster.ComputedExportedServices {
makeCES := func(consumers ...*pbmulticluster.ComputedExportedService) *pbmulticluster.ComputedExportedServices {
return &pbmulticluster.ComputedExportedServices{
Consumers: consumers,
}
}
makeConsumer := func(ref *pbresource.Reference, consumers ...*pbmulticluster.ComputedExportedServicesConsumer) *pbmulticluster.ComputedExportedService {
var actual []*pbmulticluster.ComputedExportedServicesConsumer
for _, c := range consumers {
_, ok := c.ConsumerTenancy.(*pbmulticluster.ComputedExportedServicesConsumer_Partition)
if (isEnterprise && ok) || !ok {
actual = append(actual, c)
}
}
return &pbmulticluster.ComputedExportedService{
TargetRef: ref,
Consumers: actual,
}
}
svc0Ref := &pbresource.Reference{
Type: pbcatalog.ServiceType,
Tenancy: &pbresource.Tenancy{
Partition: tenancy.Partition,
Namespace: "app",
PeerName: resource.DefaultPeerName,
},
Name: "svc0",
}
svc1Ref := &pbresource.Reference{
Type: pbcatalog.ServiceType,
Tenancy: tenancy,
Name: "svc1",
}
svc2Ref := &pbresource.Reference{
Type: pbcatalog.ServiceType,
Tenancy: tenancy,
Name: "svc2",
}
svc3Ref := &pbresource.Reference{
Type: pbcatalog.ServiceType,
Tenancy: tenancy,
Name: "svc3",
}
svc4Ref := &pbresource.Reference{
Type: pbcatalog.ServiceType,
Tenancy: &pbresource.Tenancy{
Partition: tenancy.Partition,
Namespace: "app",
PeerName: resource.DefaultPeerName,
},
Name: "svc4",
}
svc5Ref := &pbresource.Reference{
Type: pbcatalog.ServiceType,
Tenancy: tenancy,
Name: "svc5",
}
peer0Consumer := &pbmulticluster.ComputedExportedServicesConsumer{
ConsumerTenancy: &pbmulticluster.ComputedExportedServicesConsumer_Peer{
Peer: "peer-0",
},
}
peer1Consumer := &pbmulticluster.ComputedExportedServicesConsumer{
ConsumerTenancy: &pbmulticluster.ComputedExportedServicesConsumer_Peer{
Peer: "peer-1",
},
}
peer2Consumer := &pbmulticluster.ComputedExportedServicesConsumer{
ConsumerTenancy: &pbmulticluster.ComputedExportedServicesConsumer_Peer{
Peer: "peer-2",
},
}
part0Consumer := &pbmulticluster.ComputedExportedServicesConsumer{
ConsumerTenancy: &pbmulticluster.ComputedExportedServicesConsumer_Partition{
Partition: "part-0",
},
}
switch testCase {
case 0:
return makeCES(makeConsumer(svc1Ref, peer0Consumer, part0Consumer))
case 1:
return makeCES(
makeConsumer(svc1Ref, peer0Consumer, peer1Consumer, part0Consumer),
makeConsumer(svc2Ref, peer1Consumer),
)
case 2:
return makeCES(
makeConsumer(svc1Ref, peer0Consumer, peer1Consumer, part0Consumer),
makeConsumer(svc2Ref, peer1Consumer),
makeConsumer(svc3Ref, peer1Consumer),
)
case 3:
return makeCES(
makeConsumer(svc1Ref, peer0Consumer, peer1Consumer, part0Consumer),
makeConsumer(svc2Ref, peer1Consumer),
)
case 4:
return makeCES(
makeConsumer(svc0Ref, peer1Consumer, peer2Consumer),
makeConsumer(svc1Ref, peer0Consumer, peer1Consumer, peer2Consumer, part0Consumer),
makeConsumer(svc2Ref, peer1Consumer, peer2Consumer),
)
case 5:
return makeCES(
makeConsumer(svc0Ref, peer1Consumer, peer2Consumer),
makeConsumer(svc4Ref, peer1Consumer, peer2Consumer),
makeConsumer(svc1Ref, peer0Consumer, peer1Consumer, peer2Consumer, part0Consumer),
makeConsumer(svc2Ref, peer1Consumer, peer2Consumer),
)
case 6:
return makeCES(
makeConsumer(svc0Ref, peer1Consumer, peer2Consumer),
makeConsumer(svc1Ref, peer0Consumer, peer1Consumer, peer2Consumer, part0Consumer),
makeConsumer(svc2Ref, peer1Consumer, peer2Consumer),
)
case 7:
return makeCES(
makeConsumer(svc0Ref, peer1Consumer, peer2Consumer),
makeConsumer(svc1Ref, peer0Consumer, peer1Consumer, peer2Consumer, part0Consumer),
makeConsumer(svc2Ref, peer1Consumer, peer2Consumer),
makeConsumer(svc5Ref, peer1Consumer, peer2Consumer),
)
case 8:
return makeCES(
makeConsumer(svc1Ref, peer0Consumer, peer1Consumer, part0Consumer),
makeConsumer(svc2Ref, peer1Consumer),
makeConsumer(svc5Ref, peer1Consumer),
)
case 9:
return makeCES(
makeConsumer(svc1Ref, peer0Consumer, part0Consumer),
)
}
return nil
}

View File

@ -0,0 +1,13 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package controllers
import (
"github.com/hashicorp/consul/internal/controller"
"github.com/hashicorp/consul/internal/multicluster/internal/controllers/exportedservices"
)
func Register(mgr *controller.Manager) {
mgr.Register(exportedservices.Controller())
}

View File

@ -70,3 +70,20 @@ func GetDecodedResource[T proto.Message](ctx context.Context, client pbresource.
}
return Decode[T](rsp.Resource)
}
func ListDecodedResource[T proto.Message](ctx context.Context, client pbresource.ResourceServiceClient, req *pbresource.ListRequest) ([]*DecodedResource[T], error) {
rsp, err := client.List(ctx, req)
if err != nil {
return nil, err
}
results := make([]*DecodedResource[T], len(rsp.Resources))
for idx, rsc := range rsp.Resources {
d, err := Decode[T](rsc)
if err != nil {
return nil, err
}
results[idx] = d
}
return results, nil
}