NET-5824 Exported services api (#20015)

* Exported services api implemented

* Tests added, refactored code

* Adding server tests

* changelog added

* Proto gen added

* Adding codegen changes

* changing url, response object

* Fixing lint error by having namespace and partition directly

* Tests changes

* refactoring tests

* Simplified uniqueness logic for exported services, sorted the response in order of service name

* Fix lint errors, refactored code
This commit is contained in:
Tauhid Anjum 2024-01-23 10:06:59 +05:30 committed by GitHub
parent 528147e5ad
commit 5d294b26d3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 4185 additions and 2289 deletions

3
.changelog/20015.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
api: add a new api(/v1/exported-services) to list all the exported service and their consumers.
```

View File

@ -78,6 +78,7 @@ import (
"github.com/hashicorp/consul/lib/routine"
"github.com/hashicorp/consul/logging"
"github.com/hashicorp/consul/proto-public/pbresource"
"github.com/hashicorp/consul/proto/private/pbconfigentry"
"github.com/hashicorp/consul/proto/private/pboperator"
"github.com/hashicorp/consul/proto/private/pbpeering"
"github.com/hashicorp/consul/tlsutil"
@ -419,6 +420,7 @@ type Agent struct {
// into Agent, which will allow us to remove this field.
rpcClientHealth *health.Client
rpcClientConfigEntry *configentry.Client
grpcClientConfigEntry pbconfigentry.ConfigEntryServiceClient
rpcClientPeering pbpeering.PeeringServiceClient
@ -521,6 +523,7 @@ func New(bd BaseDeps) (*Agent, error) {
a.rpcClientPeering = pbpeering.NewPeeringServiceClient(conn)
a.rpcClientOperator = pboperator.NewOperatorServiceClient(conn)
a.grpcClientConfigEntry = pbconfigentry.NewConfigEntryServiceClient(conn)
a.serviceManager = NewServiceManager(&a)
a.rpcClientConfigEntry = &configentry.Client{

View File

@ -10,7 +10,12 @@ import (
"strings"
"github.com/hashicorp/consul/acl"
external "github.com/hashicorp/consul/agent/grpc-external"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/proto/private/pbconfigentry"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
const ConfigEntryNotFoundErr string = "Config entry not found"
@ -176,3 +181,45 @@ func (s *HTTPHandlers) parseEntMetaForConfigEntryKind(kind string, req *http.Req
}
return s.parseEntMetaNoWildcard(req, entMeta)
}
// ExportedServices returns all the exported services by resolving wildcards and sameness groups
// in the exported services configuration entry
func (s *HTTPHandlers) ExportedServices(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
var entMeta acl.EnterpriseMeta
if err := s.parseEntMetaPartition(req, &entMeta); err != nil {
return nil, err
}
args := pbconfigentry.GetResolvedExportedServicesRequest{
Partition: entMeta.PartitionOrEmpty(),
}
var dc string
options := structs.QueryOptions{}
s.parse(resp, req, &dc, &options)
ctx, err := external.ContextWithQueryOptions(req.Context(), options)
if err != nil {
return nil, err
}
var header metadata.MD
result, err := s.agent.grpcClientConfigEntry.GetResolvedExportedServices(ctx, &args, grpc.Header(&header))
if err != nil {
return nil, err
}
meta, err := external.QueryMetaFromGRPCMeta(header)
if err != nil {
return result.Services, fmt.Errorf("could not convert gRPC metadata to query meta: %w", err)
}
if err := setMeta(resp, &meta); err != nil {
return nil, err
}
svcs := make([]api.ResolvedExportedService, len(result.Services))
for idx, svc := range result.Services {
svcs[idx] = *svc.ToAPI()
}
return svcs, nil
}

View File

@ -16,6 +16,7 @@ import (
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/testrpc"
)
@ -736,3 +737,84 @@ func TestConfig_Apply_ProxyDefaultsExpose(t *testing.T) {
require.Equal(t, expose, entry.Expose)
}
}
func TestConfig_Exported_Services(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")
}
t.Parallel()
a := NewTestAgent(t, "")
testrpc.WaitForTestAgent(t, a.RPC, "dc1")
defer a.Shutdown()
{
// Register exported services
args := &structs.ExportedServicesConfigEntry{
Name: "default",
Services: []structs.ExportedService{
{
Name: "api",
Consumers: []structs.ServiceConsumer{
{
Peer: "east",
},
{
Peer: "west",
},
},
},
{
Name: "db",
Consumers: []structs.ServiceConsumer{
{
Peer: "east",
},
},
},
},
}
req := structs.ConfigEntryRequest{
Datacenter: "dc1",
Entry: args,
}
var configOutput bool
require.NoError(t, a.RPC(context.Background(), "ConfigEntry.Apply", &req, &configOutput))
require.True(t, configOutput)
}
t.Run("exported services", func(t *testing.T) {
req, _ := http.NewRequest("GET", "/v1/exported-services", nil)
resp := httptest.NewRecorder()
raw, err := a.srv.ExportedServices(resp, req)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.Code)
services, ok := raw.([]api.ResolvedExportedService)
require.True(t, ok)
require.Len(t, services, 2)
assertIndex(t, resp)
entMeta := acl.DefaultEnterpriseMeta()
expected := []api.ResolvedExportedService{
{
Service: "api",
Partition: entMeta.PartitionOrEmpty(),
Namespace: entMeta.NamespaceOrEmpty(),
Consumers: api.ResolvedConsumers{
Peers: []string{"east", "west"},
},
},
{
Service: "db",
Partition: entMeta.PartitionOrEmpty(),
Namespace: entMeta.NamespaceOrEmpty(),
Consumers: api.ResolvedConsumers{
Peers: []string{"east"},
},
},
}
require.Equal(t, expected, services)
})
}

View File

@ -0,0 +1,31 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package consul
import (
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/acl/resolver"
"github.com/hashicorp/consul/agent/grpc-external/services/configentry"
)
type ConfigEntryBackend struct {
srv *Server
}
var _ configentry.Backend = (*ConfigEntryBackend)(nil)
// NewConfigEntryBackend returns a configentry.Backend implementation that is bound to the given server.
func NewConfigEntryBackend(srv *Server) *ConfigEntryBackend {
return &ConfigEntryBackend{
srv: srv,
}
}
func (b *ConfigEntryBackend) EnterpriseCheckPartitions(partition string) error {
return b.enterpriseCheckPartitions(partition)
}
func (b *ConfigEntryBackend) ResolveTokenAndDefaultMeta(token string, entMeta *acl.EnterpriseMeta, authzCtx *acl.AuthorizerContext) (resolver.Result, error) {
return b.srv.ResolveTokenAndDefaultMeta(token, entMeta, authzCtx)
}

View File

@ -0,0 +1,18 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
//go:build !consulent
package consul
import (
"fmt"
"strings"
)
func (b *ConfigEntryBackend) enterpriseCheckPartitions(partition string) error {
if partition == "" || strings.EqualFold(partition, "default") {
return nil
}
return fmt.Errorf("Partitions are a Consul Enterprise feature")
}

View File

@ -0,0 +1,87 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
//go:build !consulent
package consul
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/require"
gogrpc "google.golang.org/grpc"
"github.com/hashicorp/consul/proto/private/pbconfigentry"
"github.com/hashicorp/consul/sdk/freeport"
"github.com/hashicorp/consul/testrpc"
)
func TestConfigEntryBackend_RejectsPartition(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")
}
t.Parallel()
_, s1 := testServerWithConfig(t, func(c *Config) {
c.GRPCTLSPort = freeport.GetOne(t)
})
testrpc.WaitForLeader(t, s1.RPC, "dc1")
// make a grpc client to dial s1 directly
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
t.Cleanup(cancel)
conn, err := gogrpc.DialContext(ctx, s1.config.RPCAddr.String(),
gogrpc.WithContextDialer(newServerDialer(s1.config.RPCAddr.String())),
//nolint:staticcheck
gogrpc.WithInsecure(),
gogrpc.WithBlock())
require.NoError(t, err)
t.Cleanup(func() { conn.Close() })
configEntryClient := pbconfigentry.NewConfigEntryServiceClient(conn)
req := pbconfigentry.GetResolvedExportedServicesRequest{
Partition: "test",
}
_, err = configEntryClient.GetResolvedExportedServices(ctx, &req)
require.Error(t, err)
require.Contains(t, err.Error(), "Partitions are a Consul Enterprise feature")
}
func TestConfigEntryBackend_IgnoresDefaultPartition(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")
}
t.Parallel()
_, s1 := testServerWithConfig(t, func(c *Config) {
c.GRPCTLSPort = freeport.GetOne(t)
})
testrpc.WaitForLeader(t, s1.RPC, "dc1")
// make a grpc client to dial s1 directly
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
t.Cleanup(cancel)
conn, err := gogrpc.DialContext(ctx, s1.config.RPCAddr.String(),
gogrpc.WithContextDialer(newServerDialer(s1.config.RPCAddr.String())),
//nolint:staticcheck
gogrpc.WithInsecure(),
gogrpc.WithBlock())
require.NoError(t, err)
t.Cleanup(func() { conn.Close() })
configEntryClient := pbconfigentry.NewConfigEntryServiceClient(conn)
req := pbconfigentry.GetResolvedExportedServicesRequest{
Partition: "DeFaUlT",
}
_, err = configEntryClient.GetResolvedExportedServices(ctx, &req)
require.NoError(t, err)
}

View File

@ -0,0 +1,49 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package consul
import (
"context"
"testing"
"time"
"github.com/hashicorp/consul/proto/private/pbconfigentry"
"github.com/hashicorp/consul/sdk/freeport"
"github.com/hashicorp/consul/testrpc"
"github.com/stretchr/testify/require"
gogrpc "google.golang.org/grpc"
)
func TestConfigEntryBackend_EmptyPartition(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")
}
t.Parallel()
_, s1 := testServerWithConfig(t, func(c *Config) {
c.GRPCTLSPort = freeport.GetOne(t)
})
testrpc.WaitForLeader(t, s1.RPC, "dc1")
// make a grpc client to dial s1 directly
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
t.Cleanup(cancel)
conn, err := gogrpc.DialContext(ctx, s1.config.RPCAddr.String(),
gogrpc.WithContextDialer(newServerDialer(s1.config.RPCAddr.String())),
//nolint:staticcheck
gogrpc.WithInsecure(),
gogrpc.WithBlock())
require.NoError(t, err)
t.Cleanup(func() { conn.Close() })
configEntryClient := pbconfigentry.NewConfigEntryServiceClient(conn)
req := pbconfigentry.GetResolvedExportedServicesRequest{
Partition: "",
}
_, err = configEntryClient.GetResolvedExportedServices(ctx, &req)
require.NoError(t, err)
}

View File

@ -16,6 +16,7 @@ import (
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/acl/resolver"
aclgrpc "github.com/hashicorp/consul/agent/grpc-external/services/acl"
"github.com/hashicorp/consul/agent/grpc-external/services/configentry"
"github.com/hashicorp/consul/agent/grpc-external/services/connectca"
"github.com/hashicorp/consul/agent/grpc-external/services/dataplane"
"github.com/hashicorp/consul/agent/grpc-external/services/peerstream"
@ -322,6 +323,16 @@ func (s *Server) setupGRPCServices(config *Config, deps Deps) error {
return err
}
// register the configEntry service on the internal interface only. As
// it is only accessed via the internalGRPCHandler with an actual network
// conn managed by the Agents GRPCConnPool.
err = s.registerConfigEntryServer(
s.internalGRPCHandler,
)
if err != nil {
return err
}
// enable grpc server reflection for the external gRPC interface only
reflection.Register(s.externalGRPCServer)
@ -536,3 +547,21 @@ func (s *Server) registerServerDiscoveryServer(resolver serverdiscovery.ACLResol
return nil
}
func (s *Server) registerConfigEntryServer(registrars ...grpc.ServiceRegistrar) error {
srv := configentry.NewServer(configentry.Config{
Backend: NewConfigEntryBackend(s),
Logger: s.loggers.Named(logging.GRPCAPI).Named(logging.ConfigEntry),
ForwardRPC: func(info structs.RPCInfo, fn func(*grpc.ClientConn) error) (bool, error) {
return s.ForwardGRPC(s.grpcConnPool, info, fn)
},
FSMServer: s,
})
for _, reg := range registrars {
srv.Register(reg)
}
return nil
}

View File

@ -5,10 +5,13 @@ package state
import (
"fmt"
"sort"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/configentry"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/lib"
"github.com/hashicorp/consul/proto/private/pbconfigentry"
"github.com/hashicorp/go-memdb"
)
@ -64,3 +67,108 @@ func getExportedServicesConfigEntryTxn(
}
return idx, export, nil
}
// ResolvedExportedServices returns the list of exported services along with consumers.
// Sameness Groups and wild card entries are resolved.
func (s *Store) ResolvedExportedServices(ws memdb.WatchSet, entMeta *acl.EnterpriseMeta) (uint64, []*pbconfigentry.ResolvedExportedService, error) {
tx := s.db.ReadTxn()
defer tx.Abort()
return resolvedExportedServicesTxn(tx, ws, entMeta)
}
func resolvedExportedServicesTxn(tx ReadTxn, ws memdb.WatchSet, entMeta *acl.EnterpriseMeta) (uint64, []*pbconfigentry.ResolvedExportedService, error) {
var resp []*pbconfigentry.ResolvedExportedService
// getSimplifiedExportedServices resolves the sameness group information to partitions and peers.
maxIdx, exports, err := getSimplifiedExportedServices(tx, ws, nil, *entMeta)
if err != nil {
return 0, nil, err
}
if exports == nil {
return maxIdx, nil, nil
}
var exportedServices []structs.ExportedService
for _, svc := range exports.Services {
// Prevent exporting the "consul" service.
if svc.Name == structs.ConsulServiceName {
continue
}
// If this isn't a wildcard, we can simply add it to the list of exportedServices and move to the next entry.
if svc.Name != structs.WildcardSpecifier {
exportedServices = append(exportedServices, svc)
continue
}
svcEntMeta := acl.NewEnterpriseMetaWithPartition(entMeta.PartitionOrDefault(), svc.Namespace)
// If all services in the namespace are exported by the wildcard, query those service names.
idx, typicalServices, err := serviceNamesOfKindTxn(tx, ws, structs.ServiceKindTypical, svcEntMeta)
if err != nil {
return 0, nil, fmt.Errorf("failed to get typical service names: %w", err)
}
maxIdx = lib.MaxUint64(maxIdx, idx)
for _, sn := range typicalServices {
// Prevent exporting the "consul" service.
if sn.Service.Name != structs.ConsulServiceName {
exportedServices = append(exportedServices, structs.ExportedService{
Name: sn.Service.Name,
Namespace: sn.Service.NamespaceOrDefault(),
Consumers: svc.Consumers,
})
}
}
}
uniqueExportedServices := getUniqueExportedServices(exportedServices, entMeta)
resp = prepareExportedServicesResponse(uniqueExportedServices, entMeta)
return maxIdx, resp, nil
}
// getUniqueExportedServices removes duplicate services and consumers. Services are also sorted in ascending order
func getUniqueExportedServices(exportedServices []structs.ExportedService, entMeta *acl.EnterpriseMeta) []structs.ExportedService {
// Services -> ServiceConsumers
var exportedServicesMapper = make(map[structs.ServiceName]map[structs.ServiceConsumer]struct{})
for _, svc := range exportedServices {
svcEntMeta := acl.NewEnterpriseMetaWithPartition(entMeta.PartitionOrDefault(), svc.Namespace)
svcName := structs.NewServiceName(svc.Name, &svcEntMeta)
for _, c := range svc.Consumers {
cons, ok := exportedServicesMapper[svcName]
if !ok {
cons = make(map[structs.ServiceConsumer]struct{})
exportedServicesMapper[svcName] = cons
}
cons[c] = struct{}{}
}
}
uniqueExportedServices := make([]structs.ExportedService, 0, len(exportedServicesMapper))
for svc, cons := range exportedServicesMapper {
consumers := make([]structs.ServiceConsumer, 0, len(cons))
for con := range cons {
consumers = append(consumers, con)
}
uniqueExportedServices = append(uniqueExportedServices, structs.ExportedService{
Name: svc.Name,
Namespace: svc.NamespaceOrDefault(),
Consumers: consumers,
})
}
sort.Slice(uniqueExportedServices, func(i, j int) bool {
return (uniqueExportedServices[i].Name < uniqueExportedServices[j].Name) ||
(uniqueExportedServices[i].Name == uniqueExportedServices[j].Name && uniqueExportedServices[i].Namespace < uniqueExportedServices[j].Namespace)
})
return uniqueExportedServices
}

View File

@ -6,9 +6,12 @@
package state
import (
"sort"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/configentry"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/proto/private/pbconfigentry"
"github.com/hashicorp/go-memdb"
)
@ -31,3 +34,29 @@ func (s *Store) GetSimplifiedExportedServices(ws memdb.WatchSet, entMeta acl.Ent
defer tx.Abort()
return getSimplifiedExportedServices(tx, ws, nil, entMeta)
}
func prepareExportedServicesResponse(exportedServices []structs.ExportedService, entMeta *acl.EnterpriseMeta) []*pbconfigentry.ResolvedExportedService {
resp := make([]*pbconfigentry.ResolvedExportedService, len(exportedServices))
for idx, exportedService := range exportedServices {
consumerPeers := []string{}
for _, consumer := range exportedService.Consumers {
if consumer.Peer != "" {
consumerPeers = append(consumerPeers, consumer.Peer)
}
}
sort.Strings(consumerPeers)
resp[idx] = &pbconfigentry.ResolvedExportedService{
Service: exportedService.Name,
Consumers: &pbconfigentry.Consumers{
Peers: consumerPeers,
},
}
}
return resp
}

View File

@ -0,0 +1,317 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
//go:build !consulent
package state
import (
"testing"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/proto/private/pbconfigentry"
"github.com/hashicorp/go-memdb"
"github.com/stretchr/testify/require"
)
func TestStore_prepareExportedServicesResponse(t *testing.T) {
exportedServices := []structs.ExportedService{
{
Name: "db",
Consumers: []structs.ServiceConsumer{
{
Peer: "west",
},
{
Peer: "east",
},
{
Partition: "part",
},
},
},
{
Name: "web",
Consumers: []structs.ServiceConsumer{
{
Peer: "peer-a",
},
{
Peer: "peer-b",
},
},
},
}
resp := prepareExportedServicesResponse(exportedServices, nil)
expected := []*pbconfigentry.ResolvedExportedService{
{
Service: "db",
Consumers: &pbconfigentry.Consumers{
Peers: []string{"east", "west"},
},
},
{
Service: "web",
Consumers: &pbconfigentry.Consumers{
Peers: []string{"peer-a", "peer-b"},
},
},
}
require.Equal(t, expected, resp)
}
func TestStore_ResolvedExportingServices(t *testing.T) {
s := NewStateStore(nil)
var c indexCounter
{
require.NoError(t, s.EnsureNode(c.Next(), &structs.Node{
Node: "foo", Address: "127.0.0.1",
}))
require.NoError(t, s.EnsureService(c.Next(), "foo", &structs.NodeService{
ID: "db", Service: "db", Port: 5000,
}))
require.NoError(t, s.EnsureService(c.Next(), "foo", &structs.NodeService{
ID: "cache", Service: "cache", Port: 5000,
}))
entry := &structs.ExportedServicesConfigEntry{
Name: "default",
Services: []structs.ExportedService{
{
Name: "db",
Consumers: []structs.ServiceConsumer{
{
Peer: "east",
},
{
Peer: "west",
},
},
},
{
Name: "cache",
Consumers: []structs.ServiceConsumer{
{
Peer: "east",
},
},
},
},
}
err := s.EnsureConfigEntry(c.Next(), entry)
require.NoError(t, err)
// Adding services to check wildcard config later on
require.NoError(t, s.EnsureService(c.Next(), "foo", &structs.NodeService{
ID: "frontend", Service: "frontend", Port: 5000,
}))
require.NoError(t, s.EnsureService(c.Next(), "foo", &structs.NodeService{
ID: "backend", Service: "backend", Port: 5000,
}))
// The consul service should never be exported.
require.NoError(t, s.EnsureService(c.Next(), "foo", &structs.NodeService{
ID: structs.ConsulServiceID, Service: structs.ConsulServiceName, Port: 8000,
}))
}
type testCase struct {
expect []*pbconfigentry.ResolvedExportedService
idx uint64
}
run := func(t *testing.T, tc testCase) {
ws := memdb.NewWatchSet()
defaultMeta := structs.DefaultEnterpriseMetaInDefaultPartition()
idx, services, err := s.ResolvedExportedServices(ws, defaultMeta)
require.NoError(t, err)
require.Equal(t, tc.idx, idx)
require.Equal(t, tc.expect, services)
}
t.Run("only exported services are included", func(t *testing.T) {
tc := testCase{
expect: []*pbconfigentry.ResolvedExportedService{
{
Service: "cache",
Consumers: &pbconfigentry.Consumers{
Peers: []string{"east"},
},
},
{
Service: "db",
Consumers: &pbconfigentry.Consumers{
Peers: []string{"east", "west"},
},
},
},
idx: 4,
}
run(t, tc)
})
t.Run("wild card includes all services", func(t *testing.T) {
entry := &structs.ExportedServicesConfigEntry{
Name: "default",
Services: []structs.ExportedService{
{
Name: "*",
Consumers: []structs.ServiceConsumer{
{Peer: "west"},
},
},
},
}
err := s.EnsureConfigEntry(c.Next(), entry)
require.NoError(t, err)
tc := testCase{
expect: []*pbconfigentry.ResolvedExportedService{
{
Service: "backend",
Consumers: &pbconfigentry.Consumers{
Peers: []string{"west"},
},
},
{
Service: "cache",
Consumers: &pbconfigentry.Consumers{
Peers: []string{"west"},
},
},
{
Service: "db",
Consumers: &pbconfigentry.Consumers{
Peers: []string{"west"},
},
},
{
Service: "frontend",
Consumers: &pbconfigentry.Consumers{
Peers: []string{"west"},
},
},
},
idx: c.Last(),
}
run(t, tc)
})
t.Run("deleting the config entry clears the services", func(t *testing.T) {
defaultMeta := structs.DefaultEnterpriseMetaInDefaultPartition()
err := s.DeleteConfigEntry(c.Next(), structs.ExportedServices, "default", nil)
require.NoError(t, err)
idx, result, err := s.ResolvedExportedServices(nil, defaultMeta)
require.NoError(t, err)
require.Equal(t, c.Last(), idx)
require.Nil(t, result)
})
}
func TestStore_getUniqueExportedServices(t *testing.T) {
exportedServices := []structs.ExportedService{
{
Name: "db",
Consumers: []structs.ServiceConsumer{
{
Peer: "west",
},
{
Peer: "east",
},
{
Partition: "part",
},
},
},
{
Name: "web",
Consumers: []structs.ServiceConsumer{
{
Peer: "peer-a",
},
{
Peer: "peer-b",
},
},
},
{
Name: "db",
Consumers: []structs.ServiceConsumer{
{
Peer: "west",
},
{
Peer: "west-2",
},
},
},
{
Name: "db",
Consumers: []structs.ServiceConsumer{
{
Peer: "west",
},
{
Peer: "west-2",
},
},
},
}
resp := getUniqueExportedServices(exportedServices, nil)
expected := []structs.ExportedService{
{
Name: "db",
Consumers: []structs.ServiceConsumer{
{
Peer: "west",
},
{
Peer: "east",
},
{
Partition: "part",
},
{
Peer: "west-2",
},
},
},
{
Name: "web",
Consumers: []structs.ServiceConsumer{
{
Peer: "peer-a",
},
{
Peer: "peer-b",
},
},
},
}
require.Equal(t, 2, len(resp))
for idx, expSvc := range expected {
require.Equal(t, expSvc.Name, resp[idx].Name)
require.ElementsMatch(t, expSvc.Consumers, resp[idx].Consumers)
}
}

View File

@ -0,0 +1,133 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package configentry
import (
"context"
"fmt"
"time"
"github.com/armon/go-metrics"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-memdb"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
grpcstatus "google.golang.org/grpc/status"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/acl/resolver"
"github.com/hashicorp/consul/agent/blockingquery"
"github.com/hashicorp/consul/agent/consul/state"
external "github.com/hashicorp/consul/agent/grpc-external"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/proto/private/pbconfigentry"
)
// Server implements pbconfigentry.ConfigEntryService to provide RPC operations related to
// configentries
type Server struct {
Config
}
type Config struct {
Backend Backend
Logger hclog.Logger
ForwardRPC func(structs.RPCInfo, func(*grpc.ClientConn) error) (bool, error)
FSMServer blockingquery.FSMServer
}
type Backend interface {
EnterpriseCheckPartitions(partition string) error
ResolveTokenAndDefaultMeta(token string, entMeta *acl.EnterpriseMeta, authzCtx *acl.AuthorizerContext) (resolver.Result, error)
}
func NewServer(cfg Config) *Server {
external.RequireNotNil(cfg.Backend, "Backend")
external.RequireNotNil(cfg.Logger, "Logger")
external.RequireNotNil(cfg.FSMServer, "FSMServer")
return &Server{
Config: cfg,
}
}
var _ pbconfigentry.ConfigEntryServiceServer = (*Server)(nil)
type readRequest struct {
structs.QueryOptions
structs.DCSpecificRequest
}
func (s *Server) Register(grpcServer grpc.ServiceRegistrar) {
pbconfigentry.RegisterConfigEntryServiceServer(grpcServer, s)
}
func (s *Server) GetResolvedExportedServices(
ctx context.Context,
req *pbconfigentry.GetResolvedExportedServicesRequest,
) (*pbconfigentry.GetResolvedExportedServicesResponse, error) {
if err := s.Backend.EnterpriseCheckPartitions(req.Partition); err != nil {
return nil, grpcstatus.Error(codes.InvalidArgument, err.Error())
}
options, err := external.QueryOptionsFromContext(ctx)
if err != nil {
return nil, err
}
var resp *pbconfigentry.GetResolvedExportedServicesResponse
var emptyDCSpecificRequest structs.DCSpecificRequest
handled, err := s.ForwardRPC(&readRequest{options, emptyDCSpecificRequest}, func(conn *grpc.ClientConn) error {
var err error
resp, err = pbconfigentry.NewConfigEntryServiceClient(conn).GetResolvedExportedServices(ctx, req)
return err
})
if handled || err != nil {
return resp, err
}
defer metrics.MeasureSince([]string{"configentry", "get_resolved_exported_services"}, time.Now())
var authzCtx acl.AuthorizerContext
entMeta := structs.DefaultEnterpriseMetaInPartition(req.Partition)
authz, err := s.Backend.ResolveTokenAndDefaultMeta(options.Token, entMeta, &authzCtx)
if err != nil {
return nil, err
}
if err := authz.ToAllowAuthorizer().MeshReadAllowed(&authzCtx); err != nil {
return nil, err
}
res := &pbconfigentry.GetResolvedExportedServicesResponse{}
meta := structs.QueryMeta{}
err = blockingquery.Query(s.FSMServer, &options, &meta, func(ws memdb.WatchSet, store *state.Store) error {
idx, exportedSvcs, err := store.ResolvedExportedServices(ws, entMeta)
if err != nil {
return err
}
meta.SetIndex(idx)
res.Services = exportedSvcs
return nil
})
if err != nil {
return nil, fmt.Errorf("error executing exported services blocking query: %w", err)
}
header, err := external.GRPCMetadataFromQueryMeta(meta)
if err != nil {
return nil, fmt.Errorf("could not convert query metadata to gRPC header")
}
if err := grpc.SendHeader(ctx, header); err != nil {
return nil, fmt.Errorf("could not send gRPC header")
}
return res, nil
}

View File

@ -0,0 +1,83 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package configentry
import (
"context"
"testing"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/grpc-external/testutils"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/proto/private/pbconfigentry"
"github.com/hashicorp/go-hclog"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
)
func TestGetResolvedExportedServices(t *testing.T) {
authorizer := acl.MockAuthorizer{}
authorizer.On("MeshRead", mock.Anything).Return(acl.Allow)
backend := &MockBackend{authorizer: &authorizer}
backend.On("EnterpriseCheckPartitions", mock.Anything).Return(nil)
fakeFSM := testutils.NewFakeBlockingFSM(t)
c := Config{
Backend: backend,
Logger: hclog.New(nil),
ForwardRPC: doForwardRPC,
FSMServer: fakeFSM,
}
server := NewServer(c)
// Add config entry
entry := &structs.ExportedServicesConfigEntry{
Name: "default",
Services: []structs.ExportedService{
{
Name: "db",
Consumers: []structs.ServiceConsumer{
{
Peer: "east",
},
{
Peer: "west",
},
},
},
{
Name: "cache",
Consumers: []structs.ServiceConsumer{
{
Peer: "east",
},
},
},
},
}
fakeFSM.GetState().EnsureConfigEntry(1, entry)
expected := []*pbconfigentry.ResolvedExportedService{
{
Service: "cache",
Consumers: &pbconfigentry.Consumers{
Peers: []string{"east"},
},
},
{
Service: "db",
Consumers: &pbconfigentry.Consumers{
Peers: []string{"east", "west"},
},
},
}
ctx := grpc.NewContextWithServerTransportStream(context.Background(), &testutils.MockServerTransportStream{})
resp, err := server.GetResolvedExportedServices(ctx, &pbconfigentry.GetResolvedExportedServicesRequest{})
require.NoError(t, err)
require.Equal(t, expected, resp.Services)
}

View File

@ -0,0 +1,236 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package configentry
import (
"context"
"fmt"
"testing"
"time"
"github.com/armon/go-metrics"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/acl/resolver"
"github.com/hashicorp/consul/agent/grpc-external/testutils"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/proto/private/pbconfigentry"
"github.com/hashicorp/go-hclog"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
)
type MockBackend struct {
mock.Mock
authorizer acl.Authorizer
}
func (m *MockBackend) ResolveTokenAndDefaultMeta(string, *acl.EnterpriseMeta, *acl.AuthorizerContext) (resolver.Result, error) {
return resolver.Result{Authorizer: m.authorizer}, nil
}
func (m *MockBackend) EnterpriseCheckPartitions(partition string) error {
called := m.Called(partition)
ret := called.Get(0)
if ret == nil {
return nil
} else {
return ret.(error)
}
}
func TestGetResolvedExportedServices_ACL_Deny(t *testing.T) {
authorizer := acl.MockAuthorizer{}
authorizer.On("MeshRead", mock.Anything).Return(acl.Deny)
backend := &MockBackend{authorizer: &authorizer}
backend.On("EnterpriseCheckPartitions", mock.Anything).Return(nil)
fakeFSM := testutils.NewFakeBlockingFSM(t)
c := Config{
Backend: backend,
Logger: hclog.New(nil),
ForwardRPC: doForwardRPC,
FSMServer: fakeFSM,
}
server := NewServer(c)
_, err := server.GetResolvedExportedServices(context.Background(), &pbconfigentry.GetResolvedExportedServicesRequest{})
require.Error(t, err)
}
func TestGetResolvedExportedServices_AC_Allow(t *testing.T) {
authorizer := acl.MockAuthorizer{}
authorizer.On("MeshRead", mock.Anything).Return(acl.Allow)
backend := &MockBackend{authorizer: &authorizer}
backend.On("EnterpriseCheckPartitions", mock.Anything).Return(nil)
fakeFSM := testutils.NewFakeBlockingFSM(t)
c := Config{
Backend: backend,
Logger: hclog.New(nil),
ForwardRPC: doForwardRPC,
FSMServer: fakeFSM,
}
server := NewServer(c)
ctx := grpc.NewContextWithServerTransportStream(context.Background(), &testutils.MockServerTransportStream{})
_, err := server.GetResolvedExportedServices(ctx, &pbconfigentry.GetResolvedExportedServicesRequest{})
require.NoError(t, err)
}
func TestGetResolvedExportedServices_PartitionCheck(t *testing.T) {
authorizer := acl.MockAuthorizer{}
authorizer.On("MeshRead", mock.Anything).Return(acl.Allow)
backend := &MockBackend{authorizer: &authorizer}
backend.On("EnterpriseCheckPartitions", mock.Anything).Return(fmt.Errorf("partition not supported"))
fakeFSM := testutils.NewFakeBlockingFSM(t)
c := Config{
Backend: backend,
Logger: hclog.New(nil),
ForwardRPC: doForwardRPC,
FSMServer: fakeFSM,
}
server := NewServer(c)
ctx := grpc.NewContextWithServerTransportStream(context.Background(), &testutils.MockServerTransportStream{})
resp, err := server.GetResolvedExportedServices(ctx, &pbconfigentry.GetResolvedExportedServicesRequest{})
require.EqualError(t, err, "rpc error: code = InvalidArgument desc = partition not supported")
require.Nil(t, resp)
}
func TestGetResolvedExportedServices_Index(t *testing.T) {
authorizer := acl.MockAuthorizer{}
authorizer.On("MeshRead", mock.Anything).Return(acl.Allow)
backend := &MockBackend{authorizer: &authorizer}
backend.On("EnterpriseCheckPartitions", mock.Anything).Return(nil)
fakeFSM := testutils.NewFakeBlockingFSM(t)
c := Config{
Backend: backend,
Logger: hclog.New(nil),
ForwardRPC: doForwardRPC,
FSMServer: fakeFSM,
}
server := NewServer(c)
// Add config entry
entry := &structs.ExportedServicesConfigEntry{
Name: "default",
Services: []structs.ExportedService{
{
Name: "db",
Consumers: []structs.ServiceConsumer{
{
Peer: "east",
},
{
Peer: "west",
},
},
},
{
Name: "cache",
Consumers: []structs.ServiceConsumer{
{
Peer: "east",
},
},
},
},
}
fakeFSM.GetState().EnsureConfigEntry(1, entry)
headerStream := &testutils.MockServerTransportStream{}
ctx := grpc.NewContextWithServerTransportStream(context.Background(), headerStream)
resp, err := server.GetResolvedExportedServices(ctx, &pbconfigentry.GetResolvedExportedServicesRequest{})
require.NoError(t, err)
require.Equal(t, 2, len(resp.Services))
require.Equal(t, []string{"1"}, headerStream.MD.Get("index"))
// Updating the index
fakeFSM.GetState().EnsureConfigEntry(2, entry)
headerStream = &testutils.MockServerTransportStream{}
ctx = grpc.NewContextWithServerTransportStream(context.Background(), headerStream)
resp, err = server.GetResolvedExportedServices(ctx, &pbconfigentry.GetResolvedExportedServicesRequest{})
require.NoError(t, err)
require.Equal(t, 2, len(resp.Services))
require.Equal(t, []string{"2"}, headerStream.MD.Get("index"))
}
func TestGetResolvedExportedServices_Metrics(t *testing.T) {
sink := metrics.NewInmemSink(5*time.Second, time.Minute)
cfg := metrics.DefaultConfig("consul")
metrics.NewGlobal(cfg, sink)
authorizer := acl.MockAuthorizer{}
authorizer.On("MeshRead", mock.Anything).Return(acl.Allow)
backend := &MockBackend{authorizer: &authorizer}
backend.On("EnterpriseCheckPartitions", mock.Anything).Return(nil)
fakeFSM := testutils.NewFakeBlockingFSM(t)
c := Config{
Backend: backend,
Logger: hclog.New(nil),
ForwardRPC: doForwardRPC,
FSMServer: fakeFSM,
}
server := NewServer(c)
// Add config entry
entry := &structs.ExportedServicesConfigEntry{
Name: "default",
Services: []structs.ExportedService{
{
Name: "db",
Consumers: []structs.ServiceConsumer{
{
Peer: "east",
},
{
Peer: "west",
},
},
},
{
Name: "cache",
Consumers: []structs.ServiceConsumer{
{
Peer: "east",
},
},
},
},
}
fakeFSM.GetState().EnsureConfigEntry(1, entry)
ctx := grpc.NewContextWithServerTransportStream(context.Background(), &testutils.MockServerTransportStream{})
resp, err := server.GetResolvedExportedServices(ctx, &pbconfigentry.GetResolvedExportedServicesRequest{})
require.NoError(t, err)
require.Equal(t, 2, len(resp.Services))
// Checking if metrics were added
require.NotNil(t, sink.Data()[0].Samples[`consul.configentry.get_resolved_exported_services`])
}
func doForwardRPC(structs.RPCInfo, func(*grpc.ClientConn) error) (bool, error) {
return false, nil
}

View File

@ -9,6 +9,7 @@ import (
"testing"
"time"
"github.com/hashicorp/consul/agent/blockingquery"
"github.com/hashicorp/consul/agent/consul/state"
"github.com/hashicorp/consul/agent/consul/stream"
"github.com/stretchr/testify/require"
@ -70,6 +71,47 @@ func (f *FakeFSM) ReplaceStore(store *state.Store) {
}
}
type FakeBlockingFSM struct {
store *state.Store
}
func NewFakeBlockingFSM(t *testing.T) *FakeBlockingFSM {
t.Helper()
store := TestStateStore(t, nil)
fsm := &FakeBlockingFSM{store: store}
return fsm
}
func (f *FakeBlockingFSM) GetState() *state.Store {
return f.store
}
func (f *FakeBlockingFSM) ConsistentRead() error {
return nil
}
func (f *FakeBlockingFSM) DecrementBlockingQueries() uint64 {
return 0
}
func (f *FakeBlockingFSM) IncrementBlockingQueries() uint64 {
return 0
}
func (f *FakeBlockingFSM) GetShutdownChannel() chan struct{} {
return nil
}
func (f *FakeBlockingFSM) RPCQueryTimeout(queryTimeout time.Duration) time.Duration {
return queryTimeout
}
func (f *FakeBlockingFSM) SetQueryMeta(blockingquery.ResponseMeta, string) {
}
func SetupFSMAndPublisher(t *testing.T, config FakeFSMConfig) (*FakeFSM, state.EventPublisher) {
t.Helper()
config.publisher = stream.NewEventPublisher(10 * time.Second)

View File

@ -0,0 +1,27 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package testutils
import "google.golang.org/grpc/metadata"
type MockServerTransportStream struct {
MD metadata.MD
}
func (m *MockServerTransportStream) Method() string {
return ""
}
func (m *MockServerTransportStream) SetHeader(md metadata.MD) error {
return nil
}
func (m *MockServerTransportStream) SendHeader(md metadata.MD) error {
m.MD = metadata.Join(m.MD, md)
return nil
}
func (m *MockServerTransportStream) SetTrailer(md metadata.MD) error {
return nil
}

View File

@ -44,3 +44,9 @@ func RequireAnyValidACLToken(resolver ACLResolver, token string) error {
return nil
}
func RequireNotNil(v interface{}, name string) {
if v == nil {
panic(name + " is required")
}
}

View File

@ -11,6 +11,7 @@ var rpcRateLimitSpecs = map[string]rate.OperationSpec{
"/hashicorp.consul.dataplane.DataplaneService/GetEnvoyBootstrapParams": {Type: rate.OperationTypeRead, Category: rate.OperationCategoryDataPlane},
"/hashicorp.consul.dataplane.DataplaneService/GetSupportedDataplaneFeatures": {Type: rate.OperationTypeRead, Category: rate.OperationCategoryDataPlane},
"/hashicorp.consul.dns.DNSService/Query": {Type: rate.OperationTypeRead, Category: rate.OperationCategoryDNS},
"/hashicorp.consul.internal.configentry.ConfigEntryService/GetResolvedExportedServices": {Type: rate.OperationTypeRead, Category: rate.OperationCategoryConfigEntry},
"/hashicorp.consul.internal.operator.OperatorService/TransferLeader": {Type: rate.OperationTypeExempt, Category: rate.OperationCategoryOperator},
"/hashicorp.consul.internal.peering.PeeringService/Establish": {Type: rate.OperationTypeWrite, Category: rate.OperationCategoryPeering},
"/hashicorp.consul.internal.peering.PeeringService/GenerateToken": {Type: rate.OperationTypeWrite, Category: rate.OperationCategoryPeering},

View File

@ -86,6 +86,7 @@ func init() {
registerEndpoint("/v1/internal/federation-states/mesh-gateways", []string{"GET"}, (*HTTPHandlers).FederationStateListMeshGateways)
registerEndpoint("/v1/internal/federation-state/", []string{"GET"}, (*HTTPHandlers).FederationStateGet)
registerEndpoint("/v1/discovery-chain/", []string{"GET", "POST"}, (*HTTPHandlers).DiscoveryChainRead)
registerEndpoint("/v1/exported-services", []string{"GET"}, (*HTTPHandlers).ExportedServices)
registerEndpoint("/v1/event/fire/", []string{"PUT"}, (*HTTPHandlers).EventFire)
registerEndpoint("/v1/event/list", []string{"GET"}, (*HTTPHandlers).EventList)
registerEndpoint("/v1/health/node/", []string{"GET"}, (*HTTPHandlers).HealthNodeChecks)

View File

@ -3,7 +3,9 @@
package api
import "encoding/json"
import (
"encoding/json"
)
// ExportedServicesConfigEntry manages the exported services for a single admin partition.
// Admin Partitions are a Consul Enterprise feature.

49
api/exported_services.go Normal file
View File

@ -0,0 +1,49 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package api
type ResolvedExportedService struct {
// Service is the name of the service which is exported.
Service string
// Partition of the service
Partition string `json:",omitempty"`
// Namespace of the service
Namespace string `json:",omitempty"`
// Consumers is a list of downstream consumers of the service.
Consumers ResolvedConsumers
}
type ResolvedConsumers struct {
Peers []string `json:",omitempty"`
Partitions []string `json:",omitempty"`
}
func (c *Client) ExportedServices(q *QueryOptions) ([]ResolvedExportedService, *QueryMeta, error) {
r := c.newRequest("GET", "/v1/exported-services")
r.setQueryOptions(q)
rtt, resp, err := c.doRequest(r)
if err != nil {
return nil, nil, err
}
defer closeResponseBody(resp)
if err := requireOK(resp); err != nil {
return nil, nil, err
}
qm := &QueryMeta{}
parseQueryMeta(resp, qm)
qm.RequestTime = rtt
var expSvcs []ResolvedExportedService
if err := decodeBody(resp, &expSvcs); err != nil {
return nil, nil, err
}
return expSvcs, qm, nil
}

View File

@ -62,7 +62,7 @@ const (
rpc %s(...) returns (...) {
option (hashicorp.consul.internal.ratelimit.spec) = {
operation_type: OPERATION_TYPE_READ | OPERATION_TYPE_WRITE | OPERATION_TYPE_EXEMPT,
operation_category: OPERATION_CATEGORY_ACL | OPERATION_CATEGORY_PEER_STREAM | OPERATION_CATEGORY_CONNECT_CA | OPERATION_CATEGORY_PARTITION | OPERATION_CATEGORY_PEERING | OPERATION_CATEGORY_SERVER_DISCOVERY | OPERATION_CATEGORY_DATAPLANE | OPERATION_CATEGORY_DNS | OPERATION_CATEGORY_SUBSCRIBE | OPERATION_CATEGORY_OPERATOR | OPERATION_CATEGORY_RESOURCE,
operation_category: OPERATION_CATEGORY_ACL | OPERATION_CATEGORY_PEER_STREAM | OPERATION_CATEGORY_CONNECT_CA | OPERATION_CATEGORY_PARTITION | OPERATION_CATEGORY_PEERING | OPERATION_CATEGORY_SERVER_DISCOVERY | OPERATION_CATEGORY_DATAPLANE | OPERATION_CATEGORY_DNS | OPERATION_CATEGORY_SUBSCRIBE | OPERATION_CATEGORY_OPERATOR | OPERATION_CATEGORY_RESOURCE | OPERATION_CATEGORY_CONFIGENTRY,
};
}
}

View File

@ -128,6 +128,8 @@ func (s spec) GoOperationCategory() string {
return "rate.OperationCategoryPartition"
case "OPERATION_CATEGORY_PEERING":
return "rate.OperationCategoryPeering"
case "OPERATION_CATEGORY_CONFIGENTRY":
return "rate.OperationCategoryConfigEntry"
case "OPERATION_CATEGORY_SERVER_DISCOVERY":
return "rate.OperationCategoryServerDiscovery"
case "OPERATION_CATEGORY_DATAPLANE":

View File

@ -93,6 +93,7 @@ const (
OperationCategory_OPERATION_CATEGORY_SUBSCRIBE OperationCategory = 9
OperationCategory_OPERATION_CATEGORY_OPERATOR OperationCategory = 10
OperationCategory_OPERATION_CATEGORY_RESOURCE OperationCategory = 11
OperationCategory_OPERATION_CATEGORY_CONFIGENTRY OperationCategory = 12
)
// Enum value maps for OperationCategory.
@ -110,6 +111,7 @@ var (
9: "OPERATION_CATEGORY_SUBSCRIBE",
10: "OPERATION_CATEGORY_OPERATOR",
11: "OPERATION_CATEGORY_RESOURCE",
12: "OPERATION_CATEGORY_CONFIGENTRY",
}
OperationCategory_value = map[string]int32{
"OPERATION_CATEGORY_UNSPECIFIED": 0,
@ -124,6 +126,7 @@ var (
"OPERATION_CATEGORY_SUBSCRIBE": 9,
"OPERATION_CATEGORY_OPERATOR": 10,
"OPERATION_CATEGORY_RESOURCE": 11,
"OPERATION_CATEGORY_CONFIGENTRY": 12,
}
)
@ -257,7 +260,7 @@ var file_annotations_ratelimit_ratelimit_proto_rawDesc = []byte{
0x4d, 0x50, 0x54, 0x10, 0x01, 0x12, 0x17, 0x0a, 0x13, 0x4f, 0x50, 0x45, 0x52, 0x41, 0x54, 0x49,
0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x44, 0x10, 0x02, 0x12, 0x18,
0x0a, 0x14, 0x4f, 0x50, 0x45, 0x52, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45,
0x5f, 0x57, 0x52, 0x49, 0x54, 0x45, 0x10, 0x03, 0x2a, 0xa7, 0x03, 0x0a, 0x11, 0x4f, 0x70, 0x65,
0x5f, 0x57, 0x52, 0x49, 0x54, 0x45, 0x10, 0x03, 0x2a, 0xcb, 0x03, 0x0a, 0x11, 0x4f, 0x70, 0x65,
0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, 0x22,
0x0a, 0x1e, 0x4f, 0x50, 0x45, 0x52, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x43, 0x41, 0x54, 0x45,
0x47, 0x4f, 0x52, 0x59, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44,
@ -284,32 +287,34 @@ var file_annotations_ratelimit_ratelimit_proto_rawDesc = []byte{
0x54, 0x45, 0x47, 0x4f, 0x52, 0x59, 0x5f, 0x4f, 0x50, 0x45, 0x52, 0x41, 0x54, 0x4f, 0x52, 0x10,
0x0a, 0x12, 0x1f, 0x0a, 0x1b, 0x4f, 0x50, 0x45, 0x52, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x43,
0x41, 0x54, 0x45, 0x47, 0x4f, 0x52, 0x59, 0x5f, 0x52, 0x45, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45,
0x10, 0x0b, 0x3a, 0x5e, 0x0a, 0x04, 0x73, 0x70, 0x65, 0x63, 0x12, 0x1e, 0x2e, 0x67, 0x6f, 0x6f,
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4d, 0x65, 0x74,
0x68, 0x6f, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xec, 0x40, 0x20, 0x01, 0x28,
0x0b, 0x32, 0x29, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f,
0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x72, 0x61,
0x74, 0x65, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x2e, 0x53, 0x70, 0x65, 0x63, 0x52, 0x04, 0x73, 0x70,
0x65, 0x63, 0x42, 0xa9, 0x02, 0x0a, 0x27, 0x63, 0x6f, 0x6d, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69,
0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65,
0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x72, 0x61, 0x74, 0x65, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x42, 0x0e,
0x52, 0x61, 0x74, 0x65, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01,
0x5a, 0x3e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73,
0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2f, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x2d, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74,
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x72, 0x61, 0x74, 0x65, 0x6c, 0x69, 0x6d, 0x69, 0x74,
0xa2, 0x02, 0x04, 0x48, 0x43, 0x49, 0x52, 0xaa, 0x02, 0x23, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63,
0x6f, 0x72, 0x70, 0x2e, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72,
0x6e, 0x61, 0x6c, 0x2e, 0x52, 0x61, 0x74, 0x65, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0xca, 0x02, 0x23,
0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x5c, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c,
0x5c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5c, 0x52, 0x61, 0x74, 0x65, 0x6c, 0x69,
0x6d, 0x69, 0x74, 0xe2, 0x02, 0x2f, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x5c,
0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x5c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5c,
0x52, 0x61, 0x74, 0x65, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74,
0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x26, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72,
0x70, 0x3a, 0x3a, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x3a, 0x3a, 0x49, 0x6e, 0x74, 0x65, 0x72,
0x6e, 0x61, 0x6c, 0x3a, 0x3a, 0x52, 0x61, 0x74, 0x65, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x62, 0x06,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x10, 0x0b, 0x12, 0x22, 0x0a, 0x1e, 0x4f, 0x50, 0x45, 0x52, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f,
0x43, 0x41, 0x54, 0x45, 0x47, 0x4f, 0x52, 0x59, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x45,
0x4e, 0x54, 0x52, 0x59, 0x10, 0x0c, 0x3a, 0x5e, 0x0a, 0x04, 0x73, 0x70, 0x65, 0x63, 0x12, 0x1e,
0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
0x2e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xec,
0x40, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72,
0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61,
0x6c, 0x2e, 0x72, 0x61, 0x74, 0x65, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x2e, 0x53, 0x70, 0x65, 0x63,
0x52, 0x04, 0x73, 0x70, 0x65, 0x63, 0x42, 0xa9, 0x02, 0x0a, 0x27, 0x63, 0x6f, 0x6d, 0x2e, 0x68,
0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e,
0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x72, 0x61, 0x74, 0x65, 0x6c, 0x69, 0x6d,
0x69, 0x74, 0x42, 0x0e, 0x52, 0x61, 0x74, 0x65, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x50, 0x72, 0x6f,
0x74, 0x6f, 0x50, 0x01, 0x5a, 0x3e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x75,
0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2d, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x2f, 0x61,
0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x72, 0x61, 0x74, 0x65, 0x6c,
0x69, 0x6d, 0x69, 0x74, 0xa2, 0x02, 0x04, 0x48, 0x43, 0x49, 0x52, 0xaa, 0x02, 0x23, 0x48, 0x61,
0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x49,
0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x52, 0x61, 0x74, 0x65, 0x6c, 0x69, 0x6d, 0x69,
0x74, 0xca, 0x02, 0x23, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x5c, 0x43, 0x6f,
0x6e, 0x73, 0x75, 0x6c, 0x5c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5c, 0x52, 0x61,
0x74, 0x65, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0xe2, 0x02, 0x2f, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63,
0x6f, 0x72, 0x70, 0x5c, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x5c, 0x49, 0x6e, 0x74, 0x65, 0x72,
0x6e, 0x61, 0x6c, 0x5c, 0x52, 0x61, 0x74, 0x65, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x5c, 0x47, 0x50,
0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x26, 0x48, 0x61, 0x73, 0x68,
0x69, 0x63, 0x6f, 0x72, 0x70, 0x3a, 0x3a, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x3a, 0x3a, 0x49,
0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x3a, 0x3a, 0x52, 0x61, 0x74, 0x65, 0x6c, 0x69, 0x6d,
0x69, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (

View File

@ -29,6 +29,7 @@ enum OperationCategory {
OPERATION_CATEGORY_SUBSCRIBE = 9;
OPERATION_CATEGORY_OPERATOR = 10;
OPERATION_CATEGORY_RESOURCE = 11;
OPERATION_CATEGORY_CONFIGENTRY = 12;
}
// Spec describes the kind of rate limit that will be applied to this RPC.

View File

@ -11,6 +11,7 @@ import (
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/proto/private/pbcommon"
"github.com/hashicorp/consul/types"
)
@ -603,3 +604,18 @@ func serviceRefFromStructs(a structs.ServiceRouteReferences) map[string]*ListOfR
}
return m
}
func (r *ResolvedExportedService) ToAPI() *api.ResolvedExportedService {
var t api.ResolvedExportedService
t.Service = r.Service
if r.EnterpriseMeta != nil {
t.Namespace = r.EnterpriseMeta.Namespace
t.Partition = r.EnterpriseMeta.Partition
}
t.Consumers.Peers = r.Consumers.Peers
t.Consumers.Partitions = r.Consumers.Partitions
return &t
}

View File

@ -7,6 +7,46 @@ import (
"google.golang.org/protobuf/proto"
)
// MarshalBinary implements encoding.BinaryMarshaler
func (msg *GetResolvedExportedServicesRequest) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg)
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (msg *GetResolvedExportedServicesRequest) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg)
}
// MarshalBinary implements encoding.BinaryMarshaler
func (msg *GetResolvedExportedServicesResponse) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg)
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (msg *GetResolvedExportedServicesResponse) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg)
}
// MarshalBinary implements encoding.BinaryMarshaler
func (msg *ResolvedExportedService) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg)
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (msg *ResolvedExportedService) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg)
}
// MarshalBinary implements encoding.BinaryMarshaler
func (msg *Consumers) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg)
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (msg *Consumers) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg)
}
// MarshalBinary implements encoding.BinaryMarshaler
func (msg *ConfigEntry) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg)

File diff suppressed because it is too large Load Diff

View File

@ -5,10 +5,40 @@ syntax = "proto3";
package hashicorp.consul.internal.configentry;
import "annotations/ratelimit/ratelimit.proto";
import "google/protobuf/duration.proto";
import "google/protobuf/timestamp.proto";
import "private/pbcommon/common.proto";
// ConfigEntryService handles operations related to config entries
service ConfigEntryService {
rpc GetResolvedExportedServices(GetResolvedExportedServicesRequest) returns (GetResolvedExportedServicesResponse) {
option (hashicorp.consul.internal.ratelimit.spec) = {
operation_type: OPERATION_TYPE_READ,
operation_category: OPERATION_CATEGORY_CONFIGENTRY
};
}
}
message GetResolvedExportedServicesRequest {
string Partition = 1;
}
message GetResolvedExportedServicesResponse {
repeated ResolvedExportedService services = 1;
}
message ResolvedExportedService {
string Service = 1;
common.EnterpriseMeta EnterpriseMeta = 2;
Consumers Consumers = 3;
}
message Consumers {
repeated string Peers = 1;
repeated string Partitions = 2;
}
enum Kind {
KindUnknown = 0;
KindMeshConfig = 1;

View File

@ -0,0 +1,103 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.2.0
// - protoc (unknown)
// source: private/pbconfigentry/config_entry.proto
package pbconfigentry
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7
// ConfigEntryServiceClient is the client API for ConfigEntryService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type ConfigEntryServiceClient interface {
GetResolvedExportedServices(ctx context.Context, in *GetResolvedExportedServicesRequest, opts ...grpc.CallOption) (*GetResolvedExportedServicesResponse, error)
}
type configEntryServiceClient struct {
cc grpc.ClientConnInterface
}
func NewConfigEntryServiceClient(cc grpc.ClientConnInterface) ConfigEntryServiceClient {
return &configEntryServiceClient{cc}
}
func (c *configEntryServiceClient) GetResolvedExportedServices(ctx context.Context, in *GetResolvedExportedServicesRequest, opts ...grpc.CallOption) (*GetResolvedExportedServicesResponse, error) {
out := new(GetResolvedExportedServicesResponse)
err := c.cc.Invoke(ctx, "/hashicorp.consul.internal.configentry.ConfigEntryService/GetResolvedExportedServices", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// ConfigEntryServiceServer is the server API for ConfigEntryService service.
// All implementations should embed UnimplementedConfigEntryServiceServer
// for forward compatibility
type ConfigEntryServiceServer interface {
GetResolvedExportedServices(context.Context, *GetResolvedExportedServicesRequest) (*GetResolvedExportedServicesResponse, error)
}
// UnimplementedConfigEntryServiceServer should be embedded to have forward compatible implementations.
type UnimplementedConfigEntryServiceServer struct {
}
func (UnimplementedConfigEntryServiceServer) GetResolvedExportedServices(context.Context, *GetResolvedExportedServicesRequest) (*GetResolvedExportedServicesResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetResolvedExportedServices not implemented")
}
// UnsafeConfigEntryServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to ConfigEntryServiceServer will
// result in compilation errors.
type UnsafeConfigEntryServiceServer interface {
mustEmbedUnimplementedConfigEntryServiceServer()
}
func RegisterConfigEntryServiceServer(s grpc.ServiceRegistrar, srv ConfigEntryServiceServer) {
s.RegisterService(&ConfigEntryService_ServiceDesc, srv)
}
func _ConfigEntryService_GetResolvedExportedServices_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetResolvedExportedServicesRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ConfigEntryServiceServer).GetResolvedExportedServices(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/hashicorp.consul.internal.configentry.ConfigEntryService/GetResolvedExportedServices",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ConfigEntryServiceServer).GetResolvedExportedServices(ctx, req.(*GetResolvedExportedServicesRequest))
}
return interceptor(ctx, in, info, handler)
}
// ConfigEntryService_ServiceDesc is the grpc.ServiceDesc for ConfigEntryService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var ConfigEntryService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "hashicorp.consul.internal.configentry.ConfigEntryService",
HandlerType: (*ConfigEntryServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "GetResolvedExportedServices",
Handler: _ConfigEntryService_GetResolvedExportedServices_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "private/pbconfigentry/config_entry.proto",
}