mirror of https://github.com/status-im/consul.git
added exported svc controller (#19589)
* added exported svc controller * added license headers
This commit is contained in:
parent
4d64ef0961
commit
005e1b9926
|
@ -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
|
||||
|
|
|
@ -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
|
||||
```
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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())
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue