Skip to content

Commit c12e8c0

Browse files
[RSDK-8885] Allow DiscoverComponents API to play well with modules (viamrobotics#4410)
Co-authored-by: Maxim Pertsov <[email protected]>
1 parent e1875d6 commit c12e8c0

File tree

4 files changed

+163
-3
lines changed

4 files changed

+163
-3
lines changed

module/modmanager/manager.go

+26-2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"go.uber.org/multierr"
1919
"go.uber.org/zap/zapcore"
2020
pb "go.viam.com/api/module/v1"
21+
robotpb "go.viam.com/api/robot/v1"
2122
"go.viam.com/utils"
2223
"go.viam.com/utils/pexec"
2324
"go.viam.com/utils/rpc"
@@ -75,8 +76,11 @@ type module struct {
7576
handles modlib.HandlerMap
7677
sharedConn rdkgrpc.SharedConn
7778
client pb.ModuleServiceClient
78-
addr string
79-
resources map[resource.Name]*addedResource
79+
// robotClient supplements the ModuleServiceClient client to serve select robot level methods from the module server
80+
// such as the DiscoverComponents API
81+
robotClient robotpb.RobotServiceClient
82+
addr string
83+
resources map[resource.Name]*addedResource
8084
// resourcesMu must be held if the `resources` field is accessed without
8185
// write-locking the module manager.
8286
resourcesMu sync.Mutex
@@ -991,6 +995,7 @@ func (m *module) dial() error {
991995
// out.
992996
m.sharedConn.ResetConn(rpc.GrpcOverHTTPClientConn{ClientConn: conn}, m.logger)
993997
m.client = pb.NewModuleServiceClient(m.sharedConn.GrpcConn())
998+
m.robotClient = robotpb.NewRobotServiceClient(m.sharedConn.GrpcConn())
994999
return nil
9951000
}
9961001

@@ -1163,6 +1168,10 @@ func (m *module) registerResources(mgr modmaninterface.ModuleManager, logger log
11631168
case api.API.IsComponent():
11641169
for _, model := range models {
11651170
logger.Infow("Registering component API and model from module", "module", m.cfg.Name, "API", api.API, "model", model)
1171+
// We must copy because the Discover closure func relies on api and model, but they are iterators and mutate.
1172+
// Copying prevents mutation.
1173+
modelCopy := model
1174+
apiCopy := api
11661175
resource.RegisterComponent(api.API, model, resource.Registration[resource.Resource, resource.NoNativeConfig]{
11671176
Constructor: func(
11681177
ctx context.Context,
@@ -1172,6 +1181,21 @@ func (m *module) registerResources(mgr modmaninterface.ModuleManager, logger log
11721181
) (resource.Resource, error) {
11731182
return mgr.AddResource(ctx, conf, DepsToNames(deps))
11741183
},
1184+
Discover: func(ctx context.Context, logger logging.Logger) (interface{}, error) {
1185+
req := &robotpb.DiscoverComponentsRequest{
1186+
Queries: []*robotpb.DiscoveryQuery{
1187+
{Subtype: apiCopy.API.String(), Model: modelCopy.String()},
1188+
},
1189+
}
1190+
1191+
res, err := m.robotClient.DiscoverComponents(ctx, req)
1192+
if err != nil {
1193+
m.logger.Errorf("error in modular DiscoverComponents: %s", err)
1194+
return nil, err
1195+
}
1196+
1197+
return res, nil
1198+
},
11751199
})
11761200
}
11771201
case api.API.IsService():

module/modmanager/manager_test.go

+64
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package modmanager
22

33
import (
44
"context"
5+
"encoding/json"
56
"fmt"
67
"os"
78
"path/filepath"
@@ -32,6 +33,20 @@ import (
3233
rutils "go.viam.com/rdk/utils"
3334
)
3435

36+
type testDiscoveryResult struct {
37+
Discovery []testDiscoveryItem `json:"discovery"`
38+
}
39+
40+
type testDiscoveryItem struct {
41+
Query testDiscoveryQuery `json:"query"`
42+
Results map[string]string `json:"results"`
43+
}
44+
45+
type testDiscoveryQuery struct {
46+
Subtype string `json:"subtype"`
47+
Model string `json:"model"`
48+
}
49+
3550
func setupSocketWithRobot(t *testing.T) string {
3651
t.Helper()
3752

@@ -1322,3 +1337,52 @@ func TestBadModuleFailsFast(t *testing.T) {
13221337

13231338
test.That(t, err.Error(), test.ShouldContainSubstring, "module test-module exited too quickly after attempted startup")
13241339
}
1340+
1341+
func TestModularDiscovery(t *testing.T) {
1342+
ctx := context.Background()
1343+
logger := logging.NewTestLogger(t)
1344+
1345+
modPath := rtestutils.BuildTempModule(t, "module/testmodule")
1346+
1347+
modCfg := config.Module{
1348+
Name: "test-module",
1349+
ExePath: modPath,
1350+
}
1351+
1352+
parentAddr := setupSocketWithRobot(t)
1353+
1354+
mgr := setupModManager(t, ctx, parentAddr, logger, modmanageroptions.Options{UntrustedEnv: false})
1355+
1356+
err := mgr.Add(ctx, modCfg)
1357+
test.That(t, err, test.ShouldBeNil)
1358+
1359+
// The helper model implements actual (foobar) discovery
1360+
reg, ok := resource.LookupRegistration(generic.API, resource.NewModel("rdk", "test", "helper"))
1361+
test.That(t, ok, test.ShouldBeTrue)
1362+
test.That(t, reg, test.ShouldNotBeNil)
1363+
1364+
// Check that the Discover function is registered and make call
1365+
test.That(t, reg.Discover, test.ShouldNotBeNil)
1366+
result, err := reg.Discover(ctx, logger)
1367+
test.That(t, err, test.ShouldBeNil)
1368+
t.Log("Discovery result: ", result)
1369+
1370+
// Format result
1371+
jsonData, err := json.Marshal(result)
1372+
test.That(t, err, test.ShouldBeNil)
1373+
// Debug: print the JSON data
1374+
t.Logf("Raw JSON: %s", string(jsonData))
1375+
1376+
var discoveryResult testDiscoveryResult
1377+
err = json.Unmarshal(jsonData, &discoveryResult)
1378+
test.That(t, err, test.ShouldBeNil)
1379+
// Debug: print the casted struct
1380+
t.Logf("Casted struct: %+v", discoveryResult)
1381+
1382+
// Test fields
1383+
test.That(t, len(discoveryResult.Discovery), test.ShouldEqual, 1)
1384+
discovery := discoveryResult.Discovery[0]
1385+
test.That(t, discovery.Query.Subtype, test.ShouldEqual, "rdk:component:generic")
1386+
test.That(t, discovery.Query.Model, test.ShouldEqual, "rdk:test:helper")
1387+
test.That(t, discovery.Results["foo"], test.ShouldEqual, "bar")
1388+
}

module/module.go

+65
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"net"
99
"os"
1010
"path/filepath"
11+
"strings"
1112
"sync"
1213
"time"
1314

@@ -24,6 +25,7 @@ import (
2425
robotpb "go.viam.com/api/robot/v1"
2526
streampb "go.viam.com/api/stream/v1"
2627
"go.viam.com/utils"
28+
vprotoutils "go.viam.com/utils/protoutils"
2729
"go.viam.com/utils/rpc"
2830
"golang.org/x/exp/maps"
2931
"google.golang.org/grpc"
@@ -191,6 +193,7 @@ type Module struct {
191193
pcFailed <-chan struct{}
192194
pb.UnimplementedModuleServiceServer
193195
streampb.UnimplementedStreamServiceServer
196+
robotpb.UnimplementedRobotServiceServer
194197
}
195198

196199
// NewModule returns the basic module framework/structure.
@@ -230,6 +233,11 @@ func NewModule(ctx context.Context, address string, logger logging.Logger) (*Mod
230233
if err := m.server.RegisterServiceServer(ctx, &streampb.StreamService_ServiceDesc, m); err != nil {
231234
return nil, err
232235
}
236+
// We register the RobotService API to supplement the ModuleService in order to serve select robot level methods from the module server
237+
// such as the DiscoverComponents API
238+
if err := m.server.RegisterServiceServer(ctx, &robotpb.RobotService_ServiceDesc, m); err != nil {
239+
return nil, err
240+
}
233241

234242
// attempt to construct a PeerConnection
235243
pc, err := rgrpc.NewLocalPeerConnection(logger)
@@ -506,6 +514,63 @@ func (m *Module) AddResource(ctx context.Context, req *pb.AddResourceRequest) (*
506514
return &pb.AddResourceResponse{}, nil
507515
}
508516

517+
// DiscoverComponents takes a list of discovery queries and returns corresponding
518+
// component configurations.
519+
func (m *Module) DiscoverComponents(
520+
ctx context.Context,
521+
req *robotpb.DiscoverComponentsRequest,
522+
) (*robotpb.DiscoverComponentsResponse, error) {
523+
var discoveries []*robotpb.Discovery
524+
525+
for _, q := range req.Queries {
526+
// Handle triplet edge case i.e. if the subtype doesn't contain ':', add the "rdk:component:" prefix
527+
if !strings.ContainsRune(q.Subtype, ':') {
528+
q.Subtype = "rdk:component:" + q.Subtype
529+
}
530+
531+
api, err := resource.NewAPIFromString(q.Subtype)
532+
if err != nil {
533+
return nil, fmt.Errorf("invalid subtype: %s: %w", q.Subtype, err)
534+
}
535+
model, err := resource.NewModelFromString(q.Model)
536+
if err != nil {
537+
return nil, fmt.Errorf("invalid model: %s: %w", q.Model, err)
538+
}
539+
540+
resInfo, ok := resource.LookupRegistration(api, model)
541+
if !ok {
542+
return nil, fmt.Errorf("no registration found for API %s and model %s", api, model)
543+
}
544+
545+
if resInfo.Discover == nil {
546+
return nil, fmt.Errorf("discovery not supported for API %s and model %s", api, model)
547+
}
548+
549+
results, err := resInfo.Discover(ctx, m.logger)
550+
if err != nil {
551+
return nil, fmt.Errorf("error discovering components for API %s and model %s: %w", api, model, err)
552+
}
553+
if results == nil {
554+
return nil, fmt.Errorf("error discovering components for API %s and model %s: results was nil", api, model)
555+
}
556+
557+
pbResults, err := vprotoutils.StructToStructPb(results)
558+
if err != nil {
559+
return nil, fmt.Errorf("unable to convert discovery results to pb struct for query %v: %w", q, err)
560+
}
561+
562+
pbDiscovery := &robotpb.Discovery{
563+
Query: q,
564+
Results: pbResults,
565+
}
566+
discoveries = append(discoveries, pbDiscovery)
567+
}
568+
569+
return &robotpb.DiscoverComponentsResponse{
570+
Discovery: discoveries,
571+
}, nil
572+
}
573+
509574
// ReconfigureResource receives the component/service configuration from the parent.
510575
func (m *Module) ReconfigureResource(ctx context.Context, req *pb.ReconfigureResourceRequest) (*pb.ReconfigureResourceResponse, error) {
511576
var res resource.Resource

module/testmodule/main.go

+8-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,14 @@ func mainWithArgs(ctx context.Context, args []string, logger logging.Logger) err
4646
resource.RegisterComponent(
4747
generic.API,
4848
helperModel,
49-
resource.Registration[resource.Resource, resource.NoNativeConfig]{Constructor: newHelper})
49+
resource.Registration[resource.Resource, resource.NoNativeConfig]{
50+
Constructor: newHelper,
51+
Discover: func(ctx context.Context, logger logging.Logger) (interface{}, error) {
52+
return map[string]string{
53+
"foo": "bar",
54+
}, nil
55+
},
56+
})
5057
err = myMod.AddModelFromRegistry(ctx, generic.API, helperModel)
5158
if err != nil {
5259
return err

0 commit comments

Comments
 (0)