Skip to content

Commit

Permalink
NET-5824 Exported services api (#20015)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
tauhid621 committed Jan 23, 2024
1 parent 571aac1 commit 6e0b491
Show file tree
Hide file tree
Showing 31 changed files with 4,145 additions and 2,262 deletions.
3 changes: 3 additions & 0 deletions .changelog/20015.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
api: add a new api(/v1/exported-services) to list all the exported service and their consumers.
```
7 changes: 5 additions & 2 deletions agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,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"
Expand Down Expand Up @@ -403,8 +404,9 @@ type Agent struct {

// TODO: pass directly to HTTPHandlers and DNSServer once those are passed
// into Agent, which will allow us to remove this field.
rpcClientHealth *health.Client
rpcClientConfigEntry *configentry.Client
rpcClientHealth *health.Client
rpcClientConfigEntry *configentry.Client
grpcClientConfigEntry pbconfigentry.ConfigEntryServiceClient

rpcClientPeering pbpeering.PeeringServiceClient

Expand Down Expand Up @@ -507,6 +509,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{
Expand Down
47 changes: 47 additions & 0 deletions agent/config_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
82 changes: 82 additions & 0 deletions agent/config_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -739,3 +740,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)
})
}
31 changes: 31 additions & 0 deletions agent/consul/configentry_backend.go
Original file line number Diff line number Diff line change
@@ -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)
}
18 changes: 18 additions & 0 deletions agent/consul/configentry_backend_ce.go
Original file line number Diff line number Diff line change
@@ -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")
}
87 changes: 87 additions & 0 deletions agent/consul/configentry_backend_ce_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
49 changes: 49 additions & 0 deletions agent/consul/configentry_backend_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading

0 comments on commit 6e0b491

Please sign in to comment.