Skip to content

Commit f982115

Browse files
authored
[RSDK-9620] implement discover service (#4665)
1 parent 8f2b817 commit f982115

10 files changed

+607
-4
lines changed

app/billing_client.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ func (c *BillingClient) SendPaymentRequiredEmail(ctx context.Context, customerOr
192192
}
193193

194194
func usageCostTypeFromProto(costType pb.UsageCostType) UsageCostType {
195-
//nolint:exhaustive
195+
//nolint:exhaustive,deprecated,staticcheck
196196
switch costType {
197197
case pb.UsageCostType_USAGE_COST_TYPE_UNSPECIFIED:
198198
return UsageCostTypeUnspecified

app/billing_client_test.go

+4
Original file line numberDiff line numberDiff line change
@@ -101,18 +101,22 @@ func usageCostTypeToProto(costType UsageCostType) pb.UsageCostType {
101101
case UsageCostTypeUnspecified:
102102
return pb.UsageCostType_USAGE_COST_TYPE_UNSPECIFIED
103103
case UsageCostTypeDataUpload:
104+
//nolint:deprecated,staticcheck
104105
return pb.UsageCostType_USAGE_COST_TYPE_DATA_UPLOAD
105106
case UsageCostTypeDataEgress:
107+
//nolint:deprecated,staticcheck
106108
return pb.UsageCostType_USAGE_COST_TYPE_DATA_EGRESS
107109
case UsageCostTypeRemoteControl:
108110
return pb.UsageCostType_USAGE_COST_TYPE_REMOTE_CONTROL
109111
case UsageCostTypeStandardCompute:
110112
return pb.UsageCostType_USAGE_COST_TYPE_STANDARD_COMPUTE
111113
case UsageCostTypeCloudStorage:
114+
//nolint:deprecated,staticcheck
112115
return pb.UsageCostType_USAGE_COST_TYPE_CLOUD_STORAGE
113116
case UsageCostTypeBinaryDataCloudStorage:
114117
return pb.UsageCostType_USAGE_COST_TYPE_BINARY_DATA_CLOUD_STORAGE
115118
case UsageCostTypeOtherCloudStorage:
119+
//nolint:deprecated,staticcheck
116120
return pb.UsageCostType_USAGE_COST_TYPE_OTHER_CLOUD_STORAGE
117121
case UsageCostTypePerMachine:
118122
return pb.UsageCostType_USAGE_COST_TYPE_PER_MACHINE

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ require (
7575
go.uber.org/atomic v1.11.0
7676
go.uber.org/multierr v1.11.0
7777
go.uber.org/zap v1.27.0
78-
go.viam.com/api v0.1.378
78+
go.viam.com/api v0.1.380
7979
go.viam.com/test v1.2.4
8080
go.viam.com/utils v0.1.118
8181
goji.io v2.0.2+incompatible

go.sum

+2-2
Original file line numberDiff line numberDiff line change
@@ -1513,8 +1513,8 @@ go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
15131513
go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY=
15141514
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
15151515
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
1516-
go.viam.com/api v0.1.378 h1:PW/4tYKHg4emEs8U+zxurtMAK2MVN6fMGKYlaoosBeU=
1517-
go.viam.com/api v0.1.378/go.mod h1:g5eipXHNm0rQmW7DWya6avKcmzoypLmxnMlAaIsE5Ls=
1516+
go.viam.com/api v0.1.380 h1:VgRHDlPBku+kjIp4omxmPNmRVZezytFUUOFJ2xpRFR8=
1517+
go.viam.com/api v0.1.380/go.mod h1:g5eipXHNm0rQmW7DWya6avKcmzoypLmxnMlAaIsE5Ls=
15181518
go.viam.com/test v1.2.4 h1:JYgZhsuGAQ8sL9jWkziAXN9VJJiKbjoi9BsO33TW3ug=
15191519
go.viam.com/test v1.2.4/go.mod h1:zI2xzosHdqXAJ/kFqcN+OIF78kQuTV2nIhGZ8EzvaJI=
15201520
go.viam.com/utils v0.1.118 h1:Kp6ebrCBiYReeSC1XnWPTjtBJoTUsQ6YWAomQkQF/mE=

services/discovery/client.go

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package discovery
2+
3+
import (
4+
"context"
5+
6+
"go.opencensus.io/trace"
7+
pb "go.viam.com/api/service/discovery/v1"
8+
"go.viam.com/utils/protoutils"
9+
"go.viam.com/utils/rpc"
10+
11+
"go.viam.com/rdk/config"
12+
"go.viam.com/rdk/logging"
13+
rprotoutils "go.viam.com/rdk/protoutils"
14+
"go.viam.com/rdk/resource"
15+
)
16+
17+
// client implements DiscoveryServiceClient.
18+
type client struct {
19+
resource.Named
20+
resource.TriviallyReconfigurable
21+
resource.TriviallyCloseable
22+
name string
23+
client pb.DiscoveryServiceClient
24+
logger logging.Logger
25+
}
26+
27+
// NewClientFromConn constructs a new Client from the connection passed in.
28+
func NewClientFromConn(
29+
ctx context.Context,
30+
conn rpc.ClientConn,
31+
remoteName string,
32+
name resource.Name,
33+
logger logging.Logger,
34+
) (Service, error) {
35+
grpcClient := pb.NewDiscoveryServiceClient(conn)
36+
c := &client{
37+
Named: name.PrependRemote(remoteName).AsNamed(),
38+
name: name.ShortName(),
39+
client: grpcClient,
40+
logger: logger,
41+
}
42+
return c, nil
43+
}
44+
45+
func (c *client) DiscoverResources(ctx context.Context, extra map[string]any) ([]resource.Config, error) {
46+
ctx, span := trace.StartSpan(ctx, "discovery::client::DoCommand")
47+
defer span.End()
48+
ext, err := protoutils.StructToStructPb(extra)
49+
if err != nil {
50+
return nil, err
51+
}
52+
53+
req := &pb.DiscoverResourcesRequest{Name: c.name, Extra: ext}
54+
resp, err := c.client.DiscoverResources(ctx, req)
55+
if err != nil {
56+
return nil, err
57+
}
58+
protoConfigs := resp.GetDiscoveries()
59+
if protoConfigs == nil {
60+
return nil, ErrNilResponse
61+
}
62+
63+
discoveredConfigs := []resource.Config{}
64+
for _, proto := range protoConfigs {
65+
config, err := config.ComponentConfigFromProto(proto)
66+
if err != nil {
67+
return nil, err
68+
}
69+
discoveredConfigs = append(discoveredConfigs, *config)
70+
}
71+
return discoveredConfigs, nil
72+
}
73+
74+
func (c *client) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) {
75+
ctx, span := trace.StartSpan(ctx, "discovery::client::DoCommand")
76+
defer span.End()
77+
78+
return rprotoutils.DoFromResourceClient(ctx, c.client, c.name, cmd)
79+
}

services/discovery/client_test.go

+141
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package discovery_test
2+
3+
import (
4+
"context"
5+
"net"
6+
"testing"
7+
8+
"go.viam.com/test"
9+
"go.viam.com/utils/rpc"
10+
11+
viamgrpc "go.viam.com/rdk/grpc"
12+
"go.viam.com/rdk/logging"
13+
"go.viam.com/rdk/resource"
14+
"go.viam.com/rdk/services/discovery"
15+
"go.viam.com/rdk/testutils"
16+
"go.viam.com/rdk/testutils/inject"
17+
)
18+
19+
func TestClient(t *testing.T) {
20+
logger := logging.NewTestLogger(t)
21+
listener1, err := net.Listen("tcp", "localhost:0")
22+
test.That(t, err, test.ShouldBeNil)
23+
rpcServer, err := rpc.NewServer(logger, rpc.WithUnauthenticated())
24+
test.That(t, err, test.ShouldBeNil)
25+
26+
testComponents := []resource.Config{createTestComponent("component-1"), createTestComponent("component-2")}
27+
28+
workingDiscovery := inject.NewDiscoveryService(testDiscoveryName)
29+
workingDiscovery.DiscoverResourcesFunc = func(ctx context.Context, extra map[string]any) ([]resource.Config, error) {
30+
return testComponents, nil
31+
}
32+
workingDiscovery.DoFunc = testutils.EchoFunc
33+
34+
failingDiscovery := inject.NewDiscoveryService(failDiscoveryName)
35+
failingDiscovery.DiscoverResourcesFunc = func(ctx context.Context, extra map[string]any) ([]resource.Config, error) {
36+
return nil, errDiscoverFailed
37+
}
38+
failingDiscovery.DoFunc = func(
39+
ctx context.Context,
40+
cmd map[string]interface{},
41+
) (
42+
map[string]interface{},
43+
error,
44+
) {
45+
return nil, errDoFailed
46+
}
47+
48+
resourceMap := map[resource.Name]discovery.Service{
49+
discovery.Named(testDiscoveryName): workingDiscovery,
50+
discovery.Named(failDiscoveryName): failingDiscovery,
51+
}
52+
discoverySvc, err := resource.NewAPIResourceCollection(discovery.API, resourceMap)
53+
test.That(t, err, test.ShouldBeNil)
54+
resourceAPI, ok, err := resource.LookupAPIRegistration[discovery.Service](discovery.API)
55+
test.That(t, err, test.ShouldBeNil)
56+
test.That(t, ok, test.ShouldBeTrue)
57+
test.That(t, resourceAPI.RegisterRPCService(context.Background(), rpcServer, discoverySvc), test.ShouldBeNil)
58+
59+
go rpcServer.Serve(listener1)
60+
defer rpcServer.Stop()
61+
62+
t.Run("Failing client", func(t *testing.T) {
63+
cancelCtx, cancel := context.WithCancel(context.Background())
64+
cancel()
65+
_, err = viamgrpc.Dial(cancelCtx, listener1.Addr().String(), logger)
66+
test.That(t, err, test.ShouldNotBeNil)
67+
test.That(t, err, test.ShouldBeError, context.Canceled)
68+
})
69+
70+
t.Run("client tests for working discovery", func(t *testing.T) {
71+
conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger)
72+
test.That(t, err, test.ShouldBeNil)
73+
workingDiscoveryClient, err := discovery.NewClientFromConn(context.Background(), conn, "", discovery.Named(testDiscoveryName), logger)
74+
test.That(t, err, test.ShouldBeNil)
75+
76+
respDis, err := workingDiscoveryClient.DiscoverResources(context.Background(), nil)
77+
test.That(t, err, test.ShouldBeNil)
78+
test.That(t, len(respDis), test.ShouldEqual, len(testComponents))
79+
for index, actual := range respDis {
80+
expected := testComponents[index]
81+
validateComponent(t, actual, expected)
82+
}
83+
84+
resp, err := workingDiscoveryClient.DoCommand(context.Background(), testutils.TestCommand)
85+
test.That(t, err, test.ShouldBeNil)
86+
test.That(t, resp["cmd"], test.ShouldEqual, testutils.TestCommand["cmd"])
87+
test.That(t, resp["data"], test.ShouldEqual, testutils.TestCommand["data"])
88+
89+
test.That(t, workingDiscoveryClient.Close(context.Background()), test.ShouldBeNil)
90+
test.That(t, conn.Close(), test.ShouldBeNil)
91+
})
92+
93+
t.Run("client tests for failing discovery", func(t *testing.T) {
94+
conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger)
95+
test.That(t, err, test.ShouldBeNil)
96+
failingDiscoveryClient, err := discovery.NewClientFromConn(context.Background(), conn, "", discovery.Named(failDiscoveryName), logger)
97+
test.That(t, err, test.ShouldBeNil)
98+
99+
_, err = failingDiscoveryClient.DiscoverResources(context.Background(), nil)
100+
test.That(t, err, test.ShouldNotBeNil)
101+
test.That(t, err.Error(), test.ShouldContainSubstring, errDiscoverFailed.Error())
102+
103+
_, err = failingDiscoveryClient.DoCommand(context.Background(), testutils.TestCommand)
104+
test.That(t, err, test.ShouldNotBeNil)
105+
test.That(t, err.Error(), test.ShouldContainSubstring, errDoFailed.Error())
106+
107+
test.That(t, failingDiscoveryClient.Close(context.Background()), test.ShouldBeNil)
108+
test.That(t, conn.Close(), test.ShouldBeNil)
109+
})
110+
111+
t.Run("client tests for failing discovery due to nil response", func(t *testing.T) {
112+
conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger)
113+
test.That(t, err, test.ShouldBeNil)
114+
failingDiscoveryClient, err := discovery.NewClientFromConn(context.Background(), conn, "", discovery.Named(failDiscoveryName), logger)
115+
test.That(t, err, test.ShouldBeNil)
116+
117+
failingDiscovery.DiscoverResourcesFunc = func(ctx context.Context, extra map[string]any) ([]resource.Config, error) {
118+
return nil, nil
119+
}
120+
_, err = failingDiscoveryClient.DiscoverResources(context.Background(), nil)
121+
test.That(t, err, test.ShouldNotBeNil)
122+
test.That(t, err.Error(), test.ShouldContainSubstring, discovery.ErrNilResponse.Error())
123+
124+
test.That(t, failingDiscoveryClient.Close(context.Background()), test.ShouldBeNil)
125+
test.That(t, conn.Close(), test.ShouldBeNil)
126+
})
127+
128+
t.Run("dialed client tests for working discovery", func(t *testing.T) {
129+
conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger)
130+
test.That(t, err, test.ShouldBeNil)
131+
client, err := resourceAPI.RPCClient(context.Background(), conn, "", discovery.Named(testDiscoveryName), logger)
132+
test.That(t, err, test.ShouldBeNil)
133+
134+
resp, err := client.DoCommand(context.Background(), testutils.TestCommand)
135+
test.That(t, err, test.ShouldBeNil)
136+
test.That(t, resp["cmd"], test.ShouldEqual, testutils.TestCommand["cmd"])
137+
test.That(t, resp["data"], test.ShouldEqual, testutils.TestCommand["data"])
138+
139+
test.That(t, conn.Close(), test.ShouldBeNil)
140+
})
141+
}

services/discovery/discovery.go

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Package discovery implements the discovery service, which lets users surface resource configs for their machines to use.
2+
package discovery
3+
4+
import (
5+
"context"
6+
"errors"
7+
8+
pb "go.viam.com/api/service/discovery/v1"
9+
10+
"go.viam.com/rdk/resource"
11+
"go.viam.com/rdk/robot"
12+
)
13+
14+
func init() {
15+
resource.RegisterAPI(API, resource.APIRegistration[Service]{
16+
RPCServiceServerConstructor: NewRPCServiceServer,
17+
RPCServiceHandler: pb.RegisterDiscoveryServiceHandlerFromEndpoint,
18+
RPCServiceDesc: &pb.DiscoveryService_ServiceDesc,
19+
RPCClient: NewClientFromConn,
20+
})
21+
}
22+
23+
// SubtypeName is the name of the type of service.
24+
const (
25+
SubtypeName = "discovery"
26+
)
27+
28+
// API is a variable that identifies the discovery resource API.
29+
var API = resource.APINamespaceRDK.WithServiceType(SubtypeName)
30+
31+
// ErrNilResponse is the error for when a nil response is returned from a discovery service.
32+
var ErrNilResponse = errors.New("discovery service returned a nil response")
33+
34+
// Named is a helper for getting the named service's typed resource name.
35+
func Named(name string) resource.Name {
36+
return resource.NewName(API, name)
37+
}
38+
39+
// FromRobot is a helper for getting the named discovery service from the given Robot.
40+
func FromRobot(r robot.Robot, name string) (Service, error) {
41+
return robot.ResourceFromRobot[Service](r, Named(name))
42+
}
43+
44+
// FromDependencies is a helper for getting the named discovery service from a collection of
45+
// dependencies.
46+
func FromDependencies(deps resource.Dependencies, name string) (Service, error) {
47+
return resource.FromDependencies[Service](deps, Named(name))
48+
}
49+
50+
// Service describes the functions that are available to the service.
51+
type Service interface {
52+
resource.Resource
53+
DiscoverResources(ctx context.Context, extra map[string]any) ([]resource.Config, error)
54+
}

0 commit comments

Comments
 (0)