Skip to content

Commit

Permalink
[RSDK-9620] implement discover service (#4665)
Browse files Browse the repository at this point in the history
  • Loading branch information
JohnN193 authored Jan 8, 2025
1 parent 8f2b817 commit f982115
Show file tree
Hide file tree
Showing 10 changed files with 607 additions and 4 deletions.
2 changes: 1 addition & 1 deletion app/billing_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ func (c *BillingClient) SendPaymentRequiredEmail(ctx context.Context, customerOr
}

func usageCostTypeFromProto(costType pb.UsageCostType) UsageCostType {
//nolint:exhaustive
//nolint:exhaustive,deprecated,staticcheck
switch costType {
case pb.UsageCostType_USAGE_COST_TYPE_UNSPECIFIED:
return UsageCostTypeUnspecified
Expand Down
4 changes: 4 additions & 0 deletions app/billing_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,18 +101,22 @@ func usageCostTypeToProto(costType UsageCostType) pb.UsageCostType {
case UsageCostTypeUnspecified:
return pb.UsageCostType_USAGE_COST_TYPE_UNSPECIFIED
case UsageCostTypeDataUpload:
//nolint:deprecated,staticcheck
return pb.UsageCostType_USAGE_COST_TYPE_DATA_UPLOAD
case UsageCostTypeDataEgress:
//nolint:deprecated,staticcheck
return pb.UsageCostType_USAGE_COST_TYPE_DATA_EGRESS
case UsageCostTypeRemoteControl:
return pb.UsageCostType_USAGE_COST_TYPE_REMOTE_CONTROL
case UsageCostTypeStandardCompute:
return pb.UsageCostType_USAGE_COST_TYPE_STANDARD_COMPUTE
case UsageCostTypeCloudStorage:
//nolint:deprecated,staticcheck
return pb.UsageCostType_USAGE_COST_TYPE_CLOUD_STORAGE
case UsageCostTypeBinaryDataCloudStorage:
return pb.UsageCostType_USAGE_COST_TYPE_BINARY_DATA_CLOUD_STORAGE
case UsageCostTypeOtherCloudStorage:
//nolint:deprecated,staticcheck
return pb.UsageCostType_USAGE_COST_TYPE_OTHER_CLOUD_STORAGE
case UsageCostTypePerMachine:
return pb.UsageCostType_USAGE_COST_TYPE_PER_MACHINE
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ require (
go.uber.org/atomic v1.11.0
go.uber.org/multierr v1.11.0
go.uber.org/zap v1.27.0
go.viam.com/api v0.1.378
go.viam.com/api v0.1.380
go.viam.com/test v1.2.4
go.viam.com/utils v0.1.118
goji.io v2.0.2+incompatible
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1513,8 +1513,8 @@ go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.viam.com/api v0.1.378 h1:PW/4tYKHg4emEs8U+zxurtMAK2MVN6fMGKYlaoosBeU=
go.viam.com/api v0.1.378/go.mod h1:g5eipXHNm0rQmW7DWya6avKcmzoypLmxnMlAaIsE5Ls=
go.viam.com/api v0.1.380 h1:VgRHDlPBku+kjIp4omxmPNmRVZezytFUUOFJ2xpRFR8=
go.viam.com/api v0.1.380/go.mod h1:g5eipXHNm0rQmW7DWya6avKcmzoypLmxnMlAaIsE5Ls=
go.viam.com/test v1.2.4 h1:JYgZhsuGAQ8sL9jWkziAXN9VJJiKbjoi9BsO33TW3ug=
go.viam.com/test v1.2.4/go.mod h1:zI2xzosHdqXAJ/kFqcN+OIF78kQuTV2nIhGZ8EzvaJI=
go.viam.com/utils v0.1.118 h1:Kp6ebrCBiYReeSC1XnWPTjtBJoTUsQ6YWAomQkQF/mE=
Expand Down
79 changes: 79 additions & 0 deletions services/discovery/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package discovery

import (
"context"

"go.opencensus.io/trace"
pb "go.viam.com/api/service/discovery/v1"
"go.viam.com/utils/protoutils"
"go.viam.com/utils/rpc"

"go.viam.com/rdk/config"
"go.viam.com/rdk/logging"
rprotoutils "go.viam.com/rdk/protoutils"
"go.viam.com/rdk/resource"
)

// client implements DiscoveryServiceClient.
type client struct {
resource.Named
resource.TriviallyReconfigurable
resource.TriviallyCloseable
name string
client pb.DiscoveryServiceClient
logger logging.Logger
}

// NewClientFromConn constructs a new Client from the connection passed in.
func NewClientFromConn(
ctx context.Context,
conn rpc.ClientConn,
remoteName string,
name resource.Name,
logger logging.Logger,
) (Service, error) {
grpcClient := pb.NewDiscoveryServiceClient(conn)
c := &client{
Named: name.PrependRemote(remoteName).AsNamed(),
name: name.ShortName(),
client: grpcClient,
logger: logger,
}
return c, nil
}

func (c *client) DiscoverResources(ctx context.Context, extra map[string]any) ([]resource.Config, error) {
ctx, span := trace.StartSpan(ctx, "discovery::client::DoCommand")
defer span.End()
ext, err := protoutils.StructToStructPb(extra)
if err != nil {
return nil, err
}

req := &pb.DiscoverResourcesRequest{Name: c.name, Extra: ext}
resp, err := c.client.DiscoverResources(ctx, req)
if err != nil {
return nil, err
}
protoConfigs := resp.GetDiscoveries()
if protoConfigs == nil {
return nil, ErrNilResponse
}

discoveredConfigs := []resource.Config{}
for _, proto := range protoConfigs {
config, err := config.ComponentConfigFromProto(proto)
if err != nil {
return nil, err
}
discoveredConfigs = append(discoveredConfigs, *config)
}
return discoveredConfigs, nil
}

func (c *client) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) {
ctx, span := trace.StartSpan(ctx, "discovery::client::DoCommand")
defer span.End()

return rprotoutils.DoFromResourceClient(ctx, c.client, c.name, cmd)
}
141 changes: 141 additions & 0 deletions services/discovery/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package discovery_test

import (
"context"
"net"
"testing"

"go.viam.com/test"
"go.viam.com/utils/rpc"

viamgrpc "go.viam.com/rdk/grpc"
"go.viam.com/rdk/logging"
"go.viam.com/rdk/resource"
"go.viam.com/rdk/services/discovery"
"go.viam.com/rdk/testutils"
"go.viam.com/rdk/testutils/inject"
)

func TestClient(t *testing.T) {
logger := logging.NewTestLogger(t)
listener1, err := net.Listen("tcp", "localhost:0")
test.That(t, err, test.ShouldBeNil)
rpcServer, err := rpc.NewServer(logger, rpc.WithUnauthenticated())
test.That(t, err, test.ShouldBeNil)

testComponents := []resource.Config{createTestComponent("component-1"), createTestComponent("component-2")}

workingDiscovery := inject.NewDiscoveryService(testDiscoveryName)
workingDiscovery.DiscoverResourcesFunc = func(ctx context.Context, extra map[string]any) ([]resource.Config, error) {
return testComponents, nil
}
workingDiscovery.DoFunc = testutils.EchoFunc

failingDiscovery := inject.NewDiscoveryService(failDiscoveryName)
failingDiscovery.DiscoverResourcesFunc = func(ctx context.Context, extra map[string]any) ([]resource.Config, error) {
return nil, errDiscoverFailed
}
failingDiscovery.DoFunc = func(
ctx context.Context,
cmd map[string]interface{},
) (
map[string]interface{},
error,
) {
return nil, errDoFailed
}

resourceMap := map[resource.Name]discovery.Service{
discovery.Named(testDiscoveryName): workingDiscovery,
discovery.Named(failDiscoveryName): failingDiscovery,
}
discoverySvc, err := resource.NewAPIResourceCollection(discovery.API, resourceMap)
test.That(t, err, test.ShouldBeNil)
resourceAPI, ok, err := resource.LookupAPIRegistration[discovery.Service](discovery.API)
test.That(t, err, test.ShouldBeNil)
test.That(t, ok, test.ShouldBeTrue)
test.That(t, resourceAPI.RegisterRPCService(context.Background(), rpcServer, discoverySvc), test.ShouldBeNil)

go rpcServer.Serve(listener1)
defer rpcServer.Stop()

t.Run("Failing client", func(t *testing.T) {
cancelCtx, cancel := context.WithCancel(context.Background())
cancel()
_, err = viamgrpc.Dial(cancelCtx, listener1.Addr().String(), logger)
test.That(t, err, test.ShouldNotBeNil)
test.That(t, err, test.ShouldBeError, context.Canceled)
})

t.Run("client tests for working discovery", func(t *testing.T) {
conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger)
test.That(t, err, test.ShouldBeNil)
workingDiscoveryClient, err := discovery.NewClientFromConn(context.Background(), conn, "", discovery.Named(testDiscoveryName), logger)
test.That(t, err, test.ShouldBeNil)

respDis, err := workingDiscoveryClient.DiscoverResources(context.Background(), nil)
test.That(t, err, test.ShouldBeNil)
test.That(t, len(respDis), test.ShouldEqual, len(testComponents))
for index, actual := range respDis {
expected := testComponents[index]
validateComponent(t, actual, expected)
}

resp, err := workingDiscoveryClient.DoCommand(context.Background(), testutils.TestCommand)
test.That(t, err, test.ShouldBeNil)
test.That(t, resp["cmd"], test.ShouldEqual, testutils.TestCommand["cmd"])
test.That(t, resp["data"], test.ShouldEqual, testutils.TestCommand["data"])

test.That(t, workingDiscoveryClient.Close(context.Background()), test.ShouldBeNil)
test.That(t, conn.Close(), test.ShouldBeNil)
})

t.Run("client tests for failing discovery", func(t *testing.T) {
conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger)
test.That(t, err, test.ShouldBeNil)
failingDiscoveryClient, err := discovery.NewClientFromConn(context.Background(), conn, "", discovery.Named(failDiscoveryName), logger)
test.That(t, err, test.ShouldBeNil)

_, err = failingDiscoveryClient.DiscoverResources(context.Background(), nil)
test.That(t, err, test.ShouldNotBeNil)
test.That(t, err.Error(), test.ShouldContainSubstring, errDiscoverFailed.Error())

_, err = failingDiscoveryClient.DoCommand(context.Background(), testutils.TestCommand)
test.That(t, err, test.ShouldNotBeNil)
test.That(t, err.Error(), test.ShouldContainSubstring, errDoFailed.Error())

test.That(t, failingDiscoveryClient.Close(context.Background()), test.ShouldBeNil)
test.That(t, conn.Close(), test.ShouldBeNil)
})

t.Run("client tests for failing discovery due to nil response", func(t *testing.T) {
conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger)
test.That(t, err, test.ShouldBeNil)
failingDiscoveryClient, err := discovery.NewClientFromConn(context.Background(), conn, "", discovery.Named(failDiscoveryName), logger)
test.That(t, err, test.ShouldBeNil)

failingDiscovery.DiscoverResourcesFunc = func(ctx context.Context, extra map[string]any) ([]resource.Config, error) {
return nil, nil
}
_, err = failingDiscoveryClient.DiscoverResources(context.Background(), nil)
test.That(t, err, test.ShouldNotBeNil)
test.That(t, err.Error(), test.ShouldContainSubstring, discovery.ErrNilResponse.Error())

test.That(t, failingDiscoveryClient.Close(context.Background()), test.ShouldBeNil)
test.That(t, conn.Close(), test.ShouldBeNil)
})

t.Run("dialed client tests for working discovery", func(t *testing.T) {
conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger)
test.That(t, err, test.ShouldBeNil)
client, err := resourceAPI.RPCClient(context.Background(), conn, "", discovery.Named(testDiscoveryName), logger)
test.That(t, err, test.ShouldBeNil)

resp, err := client.DoCommand(context.Background(), testutils.TestCommand)
test.That(t, err, test.ShouldBeNil)
test.That(t, resp["cmd"], test.ShouldEqual, testutils.TestCommand["cmd"])
test.That(t, resp["data"], test.ShouldEqual, testutils.TestCommand["data"])

test.That(t, conn.Close(), test.ShouldBeNil)
})
}
54 changes: 54 additions & 0 deletions services/discovery/discovery.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Package discovery implements the discovery service, which lets users surface resource configs for their machines to use.
package discovery

import (
"context"
"errors"

pb "go.viam.com/api/service/discovery/v1"

"go.viam.com/rdk/resource"
"go.viam.com/rdk/robot"
)

func init() {
resource.RegisterAPI(API, resource.APIRegistration[Service]{
RPCServiceServerConstructor: NewRPCServiceServer,
RPCServiceHandler: pb.RegisterDiscoveryServiceHandlerFromEndpoint,
RPCServiceDesc: &pb.DiscoveryService_ServiceDesc,
RPCClient: NewClientFromConn,
})
}

// SubtypeName is the name of the type of service.
const (
SubtypeName = "discovery"
)

// API is a variable that identifies the discovery resource API.
var API = resource.APINamespaceRDK.WithServiceType(SubtypeName)

// ErrNilResponse is the error for when a nil response is returned from a discovery service.
var ErrNilResponse = errors.New("discovery service returned a nil response")

// Named is a helper for getting the named service's typed resource name.
func Named(name string) resource.Name {
return resource.NewName(API, name)
}

// FromRobot is a helper for getting the named discovery service from the given Robot.
func FromRobot(r robot.Robot, name string) (Service, error) {
return robot.ResourceFromRobot[Service](r, Named(name))
}

// FromDependencies is a helper for getting the named discovery service from a collection of
// dependencies.
func FromDependencies(deps resource.Dependencies, name string) (Service, error) {
return resource.FromDependencies[Service](deps, Named(name))
}

// Service describes the functions that are available to the service.
type Service interface {
resource.Resource
DiscoverResources(ctx context.Context, extra map[string]any) ([]resource.Config, error)
}
Loading

0 comments on commit f982115

Please sign in to comment.