diff --git a/cli/app.go b/cli/app.go index 979d05e228a..aa4590f1dbc 100644 --- a/cli/app.go +++ b/cli/app.go @@ -119,13 +119,6 @@ const ( packageFlagFramework = "model-framework" - // TODO: APP-6993 remove these flags. - authApplicationFlagName = "application-name" - authApplicationFlagApplicationID = "application-id" - authApplicationFlagOriginURIs = "origin-uris" - authApplicationFlagRedirectURIs = "redirect-uris" - authApplicationFlagLogoutURI = "logout-uri" - oauthAppFlagClientID = "client-id" oauthAppFlagClientName = "client-name" oauthAppFlagClientAuthentication = "client-authentication" @@ -2755,106 +2748,6 @@ This won't work unless you have an existing installation of our GitHub app on yo }, }, }, - { - Name: "auth-app", - Usage: "manage third party auth applications", - UsageText: createUsageText("auth-app", nil, false, true), - HideHelpCommand: true, - Subcommands: []*cli.Command{ - { - Name: "register", - Usage: "register a third party auth application", - UsageText: createUsageText("auth-app register", - []string{ - generalFlagOrgID, authApplicationFlagName, authApplicationFlagOriginURIs, - authApplicationFlagRedirectURIs, authApplicationFlagLogoutURI, - }, false, false), - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: generalFlagOrgID, - Usage: "organization ID that will be tied to auth application", - Required: true, - }, - &cli.StringFlag{ - Name: authApplicationFlagName, - Usage: "name for the auth application", - Required: true, - }, - &cli.StringSliceFlag{ - Name: authApplicationFlagOriginURIs, - Usage: "origin uris for the auth application", - Required: true, - }, - &cli.StringSliceFlag{ - Name: authApplicationFlagRedirectURIs, - Usage: "redirect uris for the auth application", - Required: true, - }, - &cli.StringFlag{ - Name: authApplicationFlagLogoutURI, - Usage: "logout uri for the auth application", - Required: true, - }, - }, - Action: createCommandWithT[registerAuthApplicationArgs](RegisterAuthApplicationAction), - }, - { - Name: "update", - Usage: "update a third party auth application", - UsageText: createUsageText( - "auth-app update", []string{generalFlagOrgID, authApplicationFlagApplicationID, authApplicationFlagName}, false, false, - ), - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: generalFlagOrgID, - Required: true, - Usage: "organization ID that will be tied to auth application", - }, - &cli.StringFlag{ - Name: authApplicationFlagApplicationID, - Usage: "id for the auth application", - Required: true, - }, - &cli.StringFlag{ - Name: authApplicationFlagName, - Usage: "updated name for the auth application", - Required: true, - }, - &cli.StringSliceFlag{ - Name: authApplicationFlagOriginURIs, - Usage: "updated origin uris for the auth application", - }, - &cli.StringSliceFlag{ - Name: authApplicationFlagRedirectURIs, - Usage: "updated redirect uris for the auth application", - }, - &cli.StringFlag{ - Name: authApplicationFlagLogoutURI, - Usage: "updated logout uri for the auth application", - }, - }, - Action: createCommandWithT[updateAuthApplicationArgs](UpdateAuthApplicationAction), - }, - { - Name: "get", - Usage: "get configuration for a third party auth application", - UsageText: createUsageText("auth-app get", []string{generalFlagOrgID, authApplicationFlagApplicationID}, false, false), - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: generalFlagOrgID, - Required: true, - Usage: "organization ID that will be tied to auth application", - }, - &cli.StringFlag{ - Name: authApplicationFlagApplicationID, - Usage: "id for the auth application", - Required: true, - }, - }, - Action: createCommandWithT[getAuthApplicationArgs](GetAuthApplicationAction), - }, - }, - }, { Name: "version", Usage: "print version info for this program", diff --git a/cli/auth.go b/cli/auth.go index f26e0433b11..c35c04d7bfe 100644 --- a/cli/auth.go +++ b/cli/auth.go @@ -543,7 +543,6 @@ func (c *viamClient) ensureLoggedInInner() error { c.dataClient = datapb.NewDataServiceClient(conn) c.packageClient = packagepb.NewPackageServiceClient(conn) c.datasetClient = datasetpb.NewDatasetServiceClient(conn) - c.endUserClient = apppb.NewEndUserServiceClient(conn) c.mlTrainingClient = mltrainingpb.NewMLTrainingServiceClient(conn) c.buildClient = buildpb.NewBuildServiceClient(conn) diff --git a/cli/auth_application.go b/cli/auth_application.go deleted file mode 100644 index ebcedb8219d..00000000000 --- a/cli/auth_application.go +++ /dev/null @@ -1,155 +0,0 @@ -package cli - -import ( - "encoding/json" - - "github.com/urfave/cli/v2" - apppb "go.viam.com/api/app/v1" -) - -type registerAuthApplicationArgs struct { - OrgID string - ApplicationName string - OriginURIs []string - RedirectURIs []string - LogoutURI string -} - -// RegisterAuthApplicationAction is the corresponding action for 'auth-app register'. -func RegisterAuthApplicationAction(c *cli.Context, args registerAuthApplicationArgs) error { - client, err := newViamClient(c) - if err != nil { - return err - } - - return client.registerAuthApplicationAction(c, args) -} - -func (c *viamClient) registerAuthApplicationAction(cCtx *cli.Context, args registerAuthApplicationArgs) error { - if err := c.ensureLoggedIn(); err != nil { - return err - } - - orgID := args.OrgID - applicationName := args.ApplicationName - originURIs := args.OriginURIs - redirectURIs := args.RedirectURIs - logoutURI := args.LogoutURI - - req := &apppb.RegisterAuthApplicationRequest{ - OrgId: orgID, - ApplicationName: applicationName, - OriginUris: originURIs, - RedirectUris: redirectURIs, - LogoutUri: logoutURI, - } - resp, err := c.endUserClient.RegisterAuthApplication(c.c.Context, req) - if err != nil { - return err - } - - infof(cCtx.App.Writer, "Successfully registered auth application") - formatOutput, err := json.MarshalIndent(resp, "", "\t") - if err != nil { - return err - } - printf(cCtx.App.Writer, "%s", formatOutput) - warningf(cCtx.App.Writer, "Keep this information somewhere safe; "+ - "it contains the secret to your auth application") - return nil -} - -type updateAuthApplicationArgs struct { - OrgID string - ApplicationID string - ApplicationName string - OriginURIs []string - RedirectURIs []string - LogoutURI string -} - -// UpdateAuthApplicationAction is the corresponding action for 'auth-app update'. -func UpdateAuthApplicationAction(c *cli.Context, args updateAuthApplicationArgs) error { - client, err := newViamClient(c) - if err != nil { - return err - } - - return client.updateAuthApplicationAction(c, args) -} - -func (c *viamClient) updateAuthApplicationAction(cCtx *cli.Context, args updateAuthApplicationArgs) error { - if err := c.ensureLoggedIn(); err != nil { - return err - } - - orgID := args.OrgID - applicationID := args.ApplicationID - applicationName := args.ApplicationName - originURIs := args.OriginURIs - redirectURIs := args.RedirectURIs - logoutURI := args.LogoutURI - - req := &apppb.UpdateAuthApplicationRequest{ - OrgId: orgID, - ApplicationId: applicationID, - ApplicationName: applicationName, - OriginUris: originURIs, - RedirectUris: redirectURIs, - LogoutUri: logoutURI, - } - resp, err := c.endUserClient.UpdateAuthApplication(c.c.Context, req) - if err != nil { - return err - } - - infof(cCtx.App.Writer, "Successfully updated auth application") - formatOutput, err := json.MarshalIndent(resp, "", "\t") - if err != nil { - return err - } - printf(cCtx.App.Writer, "%s", formatOutput) - return nil -} - -type getAuthApplicationArgs struct { - OrgID string - ApplicationID string -} - -// GetAuthApplicationAction is the corresponding action for 'auth-app get'. -func GetAuthApplicationAction(c *cli.Context, args getAuthApplicationArgs) error { - client, err := newViamClient(c) - if err != nil { - return err - } - - return client.getAuthApplicationAction(c, args) -} - -func (c *viamClient) getAuthApplicationAction(cCtx *cli.Context, args getAuthApplicationArgs) error { - if err := c.ensureLoggedIn(); err != nil { - return err - } - - orgID := args.OrgID - applicationID := args.ApplicationID - - req := &apppb.GetAuthApplicationRequest{ - OrgId: orgID, - ApplicationId: applicationID, - } - resp, err := c.endUserClient.GetAuthApplication(c.c.Context, req) - if err != nil { - return err - } - - formatOutput, err := json.MarshalIndent(resp, "", "\t") - if err != nil { - return err - } - printf(cCtx.App.Writer, "%s", formatOutput) - warningf(cCtx.App.Writer, "Keep this information somewhere safe; "+ - "it contains the secret to your auth application") - return nil -} diff --git a/cli/auth_application_test.go b/cli/auth_application_test.go deleted file mode 100644 index a632623ae7e..00000000000 --- a/cli/auth_application_test.go +++ /dev/null @@ -1,114 +0,0 @@ -package cli - -import ( - "context" - "testing" - - apppb "go.viam.com/api/app/v1" - "go.viam.com/test" - "google.golang.org/grpc" - - "go.viam.com/rdk/testutils/inject" -) - -func TestRegisterAuthApplicationAction(t *testing.T) { - registerAuthApplicationFunc := func(ctx context.Context, in *apppb.RegisterAuthApplicationRequest, - opts ...grpc.CallOption, - ) (*apppb.RegisterAuthApplicationResponse, error) { - return &apppb.RegisterAuthApplicationResponse{ - ApplicationId: "c6215428-1b73-41c3-b44a-56db0631c8f1", - ApplicationName: in.ApplicationName, - ClientSecret: "reallysecretsecret", - }, nil - } - - eusc := &inject.EndUserServiceClient{ - RegisterAuthApplicationFunc: registerAuthApplicationFunc, - } - flags := make(map[string]any) - flags[generalFlagOrgID] = "a757fe30-5648-4c5b-ab74-4ecd6bf06e4c" - flags[authApplicationFlagName] = "pupper_app" - flags[authApplicationFlagOriginURIs] = []string{"https://woof.com/login", "https://arf.com/"} - flags[authApplicationFlagRedirectURIs] = []string{"https://woof.com/home", "https://arf.com/home"} - flags[authApplicationFlagLogoutURI] = "https://woof.com/logout" - - cCtx, ac, out, errOut := setup(&inject.AppServiceClient{}, nil, nil, eusc, flags, "token") - err := ac.registerAuthApplicationAction(cCtx, parseStructFromCtx[registerAuthApplicationArgs](cCtx)) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(errOut.messages), test.ShouldEqual, 0) - test.That(t, len(out.messages), test.ShouldEqual, 5) - - expectedResponseString := "{\n\t\"application_id\": \"c6215428-1b73-41c3-b44a-56db0631c8f1\"," + - "\n\t\"application_name\": \"pupper_app\",\n\t\"client_secret\": \"reallysecretsecret\"\n}\n" - test.That(t, out.messages[2], test.ShouldEqual, expectedResponseString) -} - -func TestUpdateAuthApplicationAction(t *testing.T) { - updateAuthApplication := func(ctx context.Context, in *apppb.UpdateAuthApplicationRequest, - opts ...grpc.CallOption, - ) (*apppb.UpdateAuthApplicationResponse, error) { - return &apppb.UpdateAuthApplicationResponse{ - ApplicationId: "c6215428-1b73-41c3-b44a-56db0631c8f1", - ApplicationName: in.ApplicationName, - }, nil - } - - eusc := &inject.EndUserServiceClient{ - UpdateAuthApplicationFunc: updateAuthApplication, - } - flags := make(map[string]any) - flags[generalFlagOrgID] = "a757fe30-5648-4c5b-ab74-4ecd6bf06e4c" - flags[authApplicationFlagApplicationID] = "a673022c-9916-4238-b8eb-4f7a89885909" - flags[authApplicationFlagName] = "pupper_app" - flags[authApplicationFlagOriginURIs] = []string{"https://woof.com/login", "https://arf.com/"} - flags[authApplicationFlagRedirectURIs] = []string{"https://woof.com/home", "https://arf.com/home"} - flags[authApplicationFlagLogoutURI] = "https://woof.com/logout" - - cCtx, ac, out, errOut := setup(&inject.AppServiceClient{}, nil, nil, eusc, flags, "token") - err := ac.updateAuthApplicationAction(cCtx, parseStructFromCtx[updateAuthApplicationArgs](cCtx)) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(errOut.messages), test.ShouldEqual, 0) - test.That(t, len(out.messages), test.ShouldEqual, 3) - - expectedResponseString := "{\n\t\"application_id\": \"c6215428-1b73-41c3-b44a-56db0631c8f1\"," + - "\n\t\"application_name\": \"pupper_app\"\n}\n" - test.That(t, out.messages[2], test.ShouldEqual, expectedResponseString) -} - -func TestGetAuthApplicationAction(t *testing.T) { - getAuthApplication := func(ctx context.Context, in *apppb.GetAuthApplicationRequest, - opts ...grpc.CallOption, - ) (*apppb.GetAuthApplicationResponse, error) { - return &apppb.GetAuthApplicationResponse{ - ApplicationId: "c6215428-1b73-41c3-b44a-56db0631c8f1", - ApplicationName: "my_app", - ClientSecret: "supersupersecretsecret", - OriginUris: []string{"https://woof.com/login", "https://arf.com/"}, - RedirectUris: []string{"https://woof.com/home", "https://arf.com/home"}, - LogoutUri: "https://woof.com/logout", - }, nil - } - - eusc := &inject.EndUserServiceClient{ - GetAuthApplicationFunc: getAuthApplication, - } - flags := make(map[string]any) - flags[generalFlagOrgID] = "a757fe30-5648-4c5b-ab74-4ecd6bf06e4c" - flags[authApplicationFlagApplicationID] = "a673022c-9916-4238-b8eb-4f7a89885909" - - cCtx, ac, out, errOut := setup(&inject.AppServiceClient{}, nil, nil, eusc, flags, "token") - err := ac.getAuthApplicationAction(cCtx, parseStructFromCtx[getAuthApplicationArgs](cCtx)) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(errOut.messages), test.ShouldEqual, 0) - test.That(t, len(out.messages), test.ShouldEqual, 3) - - expectedResponseString := "{\n\t\"" + - "application_id\": \"c6215428-1b73-41c3-b44a-56db0631c8f1\"," + - "\n\t\"application_name\": \"my_app\"," + - "\n\t\"client_secret\": \"supersupersecretsecret\"," + - "\n\t\"origin_uris\": [\n\t\t\"https://woof.com/login\"," + - "\n\t\t\"https://arf.com/\"\n\t]," + - "\n\t\"redirect_uris\": [\n\t\t\"https://woof.com/home\",\n\t\t\"https://arf.com/home\"\n\t]," + - "\n\t\"logout_uri\": \"https://woof.com/logout\"\n}\n" - test.That(t, out.messages[0], test.ShouldEqual, expectedResponseString) -} diff --git a/cli/auth_test.go b/cli/auth_test.go index 22b8dcd5c15..57a9cc516d9 100644 --- a/cli/auth_test.go +++ b/cli/auth_test.go @@ -16,7 +16,7 @@ import ( ) func TestLoginAction(t *testing.T) { - cCtx, ac, out, errOut := setup(nil, nil, nil, nil, nil, "token") + cCtx, ac, out, errOut := setup(nil, nil, nil, nil, "token") test.That(t, ac.loginAction(cCtx), test.ShouldBeNil) test.That(t, len(errOut.messages), test.ShouldEqual, 0) @@ -26,7 +26,7 @@ func TestLoginAction(t *testing.T) { } func TestAPIKeyAuth(t *testing.T) { - _, ac, _, errOut := setup(nil, nil, nil, nil, nil, "apiKey") + _, ac, _, errOut := setup(nil, nil, nil, nil, "apiKey") test.That(t, len(errOut.messages), test.ShouldEqual, 0) APIKey, isAPIKey := ac.conf.Auth.(*apiKey) test.That(t, isAPIKey, test.ShouldBeTrue) @@ -36,7 +36,7 @@ func TestAPIKeyAuth(t *testing.T) { func TestPrintAccessTokenAction(t *testing.T) { // AppServiceClient needed for any Action that calls ensureLoggedIn. - cCtx, ac, out, errOut := setup(&inject.AppServiceClient{}, nil, nil, nil, nil, "token") + cCtx, ac, out, errOut := setup(&inject.AppServiceClient{}, nil, nil, nil, "token") test.That(t, ac.printAccessTokenAction(cCtx), test.ShouldBeNil) test.That(t, len(errOut.messages), test.ShouldEqual, 0) @@ -53,7 +53,7 @@ func TestAPIKeyCreateAction(t *testing.T) { asc := &inject.AppServiceClient{ CreateKeyFunc: createKeyFunc, } - cCtx, ac, out, errOut := setup(asc, nil, nil, nil, nil, "token") + cCtx, ac, out, errOut := setup(asc, nil, nil, nil, "token") test.That(t, ac.organizationsAPIKeyCreateAction(cCtx, parseStructFromCtx[organizationsAPIKeyCreateArgs](cCtx)), test.ShouldBeNil) test.That(t, len(errOut.messages), test.ShouldEqual, 0) @@ -80,7 +80,7 @@ func TestRobotAPIKeyCreateAction(t *testing.T) { flags[generalFlagOrgID] = fakeOrgID flags[generalFlagMachineID] = fakeRobotID flags[generalFlagName] = "my-name" - cCtx, ac, out, errOut := setup(asc, nil, nil, nil, flags, "token") + cCtx, ac, out, errOut := setup(asc, nil, nil, flags, "token") test.That(t, ac.robotAPIKeyCreateAction(cCtx, parseStructFromCtx[robotAPIKeyCreateArgs](cCtx)), test.ShouldBeNil) test.That(t, len(errOut.messages), test.ShouldEqual, 0) @@ -135,7 +135,7 @@ func TestRobotAPIKeyCreateAction(t *testing.T) { flags[generalFlagMachineID] = fakeRobotID flags[generalFlagOrgID] = "" flags[generalFlagName] = "test-me" - cCtx, ac, out, _ = setup(asc, nil, nil, nil, flags, "token") + cCtx, ac, out, _ = setup(asc, nil, nil, flags, "token") err = ac.robotAPIKeyCreateAction(cCtx, parseStructFromCtx[robotAPIKeyCreateArgs](cCtx)) test.That(t, err, test.ShouldNotBeNil) @@ -162,7 +162,7 @@ func TestLocationAPIKeyCreateAction(t *testing.T) { flags[generalFlagOrgID] = "" flags[generalFlagName] = "" // testing no locationID - cCtx, ac, out, errOut := setup(asc, nil, nil, nil, flags, "token") + cCtx, ac, out, errOut := setup(asc, nil, nil, flags, "token") err := ac.locationAPIKeyCreateAction(cCtx, parseStructFromCtx[locationAPIKeyCreateArgs](cCtx)) test.That(t, err, test.ShouldNotBeNil) test.That(t, len(errOut.messages), test.ShouldEqual, 0) @@ -203,7 +203,7 @@ func TestLocationAPIKeyCreateAction(t *testing.T) { flags[generalFlagOrgID] = "" flags[generalFlagName] = "test-name" - cCtx, ac, _, _ = setup(asc, nil, nil, nil, flags, "token") + cCtx, ac, _, _ = setup(asc, nil, nil, flags, "token") err = ac.locationAPIKeyCreateAction(cCtx, parseStructFromCtx[locationAPIKeyCreateArgs](cCtx)) test.That(t, err, test.ShouldNotBeNil) @@ -212,7 +212,7 @@ func TestLocationAPIKeyCreateAction(t *testing.T) { } func TestLogoutAction(t *testing.T) { - cCtx, ac, out, errOut := setup(nil, nil, nil, nil, nil, "token") + cCtx, ac, out, errOut := setup(nil, nil, nil, nil, "token") test.That(t, ac.logoutAction(cCtx), test.ShouldBeNil) test.That(t, len(errOut.messages), test.ShouldEqual, 0) @@ -222,7 +222,7 @@ func TestLogoutAction(t *testing.T) { } func TestWhoAmIAction(t *testing.T) { - cCtx, ac, out, errOut := setup(nil, nil, nil, nil, nil, "token") + cCtx, ac, out, errOut := setup(nil, nil, nil, nil, "token") test.That(t, ac.whoAmIAction(cCtx), test.ShouldBeNil) test.That(t, len(errOut.messages), test.ShouldEqual, 0) diff --git a/cli/client.go b/cli/client.go index d5e24a7e1f4..aae57c46267 100644 --- a/cli/client.go +++ b/cli/client.go @@ -75,7 +75,6 @@ type viamClient struct { dataClient datapb.DataServiceClient packageClient packagepb.PackageServiceClient datasetClient datasetpb.DatasetServiceClient - endUserClient apppb.EndUserServiceClient mlTrainingClient mltrainingpb.MLTrainingServiceClient buildClient buildpb.BuildServiceClient baseURL *url.URL diff --git a/cli/client_test.go b/cli/client_test.go index 2755d9ed263..11819882327 100644 --- a/cli/client_test.go +++ b/cli/client_test.go @@ -90,14 +90,9 @@ func newTestContext(t *testing.T, flags map[string]any) *cli.Context { // setup creates a new cli.Context and viamClient with fake auth and the passed // in AppServiceClient and DataServiceClient. It also returns testWriters that capture Stdout and // Stdin. -func setup( - asc apppb.AppServiceClient, - dataClient datapb.DataServiceClient, - buildClient buildpb.BuildServiceClient, - endUserClient apppb.EndUserServiceClient, - defaultFlags map[string]any, - authMethod string, - cliArgs ...string, +func setup(asc apppb.AppServiceClient, dataClient datapb.DataServiceClient, + buildClient buildpb.BuildServiceClient, defaultFlags map[string]any, + authMethod string, cliArgs ...string, ) (*cli.Context, *viamClient, *testWriter, *testWriter) { out := &testWriter{} errOut := &testWriter{} @@ -126,14 +121,13 @@ func setup( } ac := &viamClient{ - client: asc, - conf: conf, - c: cCtx, - dataClient: dataClient, - buildClient: buildClient, - endUserClient: endUserClient, - selectedOrg: &apppb.Organization{}, - selectedLoc: &apppb.Location{}, + client: asc, + conf: conf, + c: cCtx, + dataClient: dataClient, + buildClient: buildClient, + selectedOrg: &apppb.Organization{}, + selectedLoc: &apppb.Location{}, } return cCtx, ac, out, errOut } @@ -151,7 +145,7 @@ func setupWithRunningPart( ) (*cli.Context, *viamClient, *testWriter, *testWriter) { t.Helper() - cCtx, ac, out, errOut := setup(asc, dataClient, buildClient, nil, defaultFlags, authMethod, cliArgs...) + cCtx, ac, out, errOut := setup(asc, dataClient, buildClient, defaultFlags, authMethod, cliArgs...) // this config could later become a parameter r, err := robotimpl.New(cCtx.Context, &robotconfig.Config{ @@ -192,7 +186,7 @@ func TestListOrganizationsAction(t *testing.T) { asc := &inject.AppServiceClient{ ListOrganizationsFunc: listOrganizationsFunc, } - cCtx, ac, out, errOut := setup(asc, nil, nil, nil, nil, "token") + cCtx, ac, out, errOut := setup(asc, nil, nil, nil, "token") test.That(t, ac.listOrganizationsAction(cCtx), test.ShouldBeNil) test.That(t, len(errOut.messages), test.ShouldEqual, 0) @@ -213,7 +207,7 @@ func TestSetSupportEmailAction(t *testing.T) { OrganizationSetSupportEmailFunc: setSupportEmailFunc, } - cCtx, ac, out, errOut := setup(asc, nil, nil, nil, nil, "token") + cCtx, ac, out, errOut := setup(asc, nil, nil, nil, "token") test.That(t, ac.organizationsSupportEmailSetAction(cCtx, "test-org", "test-email"), test.ShouldBeNil) test.That(t, len(errOut.messages), test.ShouldEqual, 0) @@ -230,7 +224,7 @@ func TestGetSupportEmailAction(t *testing.T) { OrganizationGetSupportEmailFunc: getSupportEmailFunc, } - cCtx, ac, out, errOut := setup(asc, nil, nil, nil, nil, "token") + cCtx, ac, out, errOut := setup(asc, nil, nil, nil, "token") test.That(t, ac.organizationsSupportEmailGetAction(cCtx, "test-org"), test.ShouldBeNil) test.That(t, len(errOut.messages), test.ShouldEqual, 0) @@ -249,7 +243,7 @@ func TestBillingServiceDisableAction(t *testing.T) { DisableBillingServiceFunc: disableBillingFunc, } - cCtx, ac, out, errOut := setup(asc, nil, nil, nil, nil, "token") + cCtx, ac, out, errOut := setup(asc, nil, nil, nil, "token") test.That(t, ac.organizationDisableBillingServiceAction(cCtx, "test-org"), test.ShouldBeNil) test.That(t, len(errOut.messages), test.ShouldEqual, 0) test.That(t, len(out.messages), test.ShouldEqual, 1) @@ -280,7 +274,7 @@ func TestGetBillingConfigAction(t *testing.T) { GetBillingServiceConfigFunc: getConfigEmailFunc, } - cCtx, ac, out, errOut := setup(asc, nil, nil, nil, nil, "token") + cCtx, ac, out, errOut := setup(asc, nil, nil, nil, "token") test.That(t, ac.getBillingConfig(cCtx, "test-org"), test.ShouldBeNil) test.That(t, len(errOut.messages), test.ShouldEqual, 0) test.That(t, len(out.messages), test.ShouldEqual, 12) @@ -309,7 +303,7 @@ func TestOrganizationSetLogoAction(t *testing.T) { OrganizationSetLogoFunc: organizationSetLogoFunc, } - cCtx, ac, out, errOut := setup(asc, nil, nil, nil, nil, "token") + cCtx, ac, out, errOut := setup(asc, nil, nil, nil, "token") // Create a temporary file for testing fileName := "test-logo-*.png" tmpFile, err := os.CreateTemp("", fileName) @@ -320,7 +314,7 @@ func TestOrganizationSetLogoAction(t *testing.T) { test.That(t, len(out.messages), test.ShouldEqual, 1) test.That(t, out.messages[0], test.ShouldContainSubstring, "Successfully set the logo for organization") - cCtx, ac, out, errOut = setup(asc, nil, nil, nil, nil, "token") + cCtx, ac, out, errOut = setup(asc, nil, nil, nil, "token") logoFileName2 := "test-logo-2-*.PNG" tmpFile2, err := os.CreateTemp("", logoFileName2) @@ -344,7 +338,7 @@ func TestGetLogoAction(t *testing.T) { OrganizationGetLogoFunc: getLogoFunc, } - cCtx, ac, out, errOut := setup(asc, nil, nil, nil, nil, "token") + cCtx, ac, out, errOut := setup(asc, nil, nil, nil, "token") test.That(t, ac.organizationsLogoGetAction(cCtx, "test-org"), test.ShouldBeNil) test.That(t, len(errOut.messages), test.ShouldEqual, 0) @@ -363,7 +357,7 @@ func TestEnableAuthServiceAction(t *testing.T) { EnableAuthServiceFunc: enableAuthServiceFunc, } - cCtx, ac, out, errOut := setup(asc, nil, nil, nil, nil, "token") + cCtx, ac, out, errOut := setup(asc, nil, nil, nil, "token") test.That(t, ac.enableAuthServiceAction(cCtx, "test-org"), test.ShouldBeNil) test.That(t, len(errOut.messages), test.ShouldEqual, 0) @@ -382,7 +376,7 @@ func TestDisableAuthServiceAction(t *testing.T) { DisableAuthServiceFunc: disableAuthServiceFunc, } - cCtx, ac, out, errOut := setup(asc, nil, nil, nil, nil, "token") + cCtx, ac, out, errOut := setup(asc, nil, nil, nil, "token") test.That(t, ac.disableAuthServiceAction(cCtx, "test-org"), test.ShouldBeNil) test.That(t, len(errOut.messages), test.ShouldEqual, 0) @@ -405,7 +399,7 @@ func TestListOAuthAppsAction(t *testing.T) { ListOAuthAppsFunc: listOAuthAppFunc, } - cCtx, ac, out, errOut := setup(asc, nil, nil, nil, nil, "token") + cCtx, ac, out, errOut := setup(asc, nil, nil, nil, "token") test.That(t, ac.listOAuthAppsAction(cCtx, "test-org"), test.ShouldBeNil) test.That(t, len(errOut.messages), test.ShouldEqual, 0) test.That(t, len(out.messages), test.ShouldEqual, 1) @@ -423,7 +417,7 @@ func TestDeleteOAuthAppAction(t *testing.T) { DeleteOAuthAppFunc: deleteOAuthAppFunc, } - cCtx, ac, out, errOut := setup(asc, nil, nil, nil, nil, "token") + cCtx, ac, out, errOut := setup(asc, nil, nil, nil, "token") test.That(t, ac.deleteOAuthAppAction(cCtx, "test-org", "client-id"), test.ShouldBeNil) test.That(t, len(errOut.messages), test.ShouldEqual, 0) test.That(t, len(out.messages), test.ShouldEqual, 1) @@ -440,7 +434,7 @@ func TestUpdateBillingServiceAction(t *testing.T) { UpdateBillingServiceFunc: updateConfigFunc, } - cCtx, ac, out, errOut := setup(asc, nil, nil, nil, nil, "token") + cCtx, ac, out, errOut := setup(asc, nil, nil, nil, "token") address := "123 Main St, Suite 100, San Francisco, CA, 94105" test.That(t, ac.updateBillingServiceAction(cCtx, "test-org", address), test.ShouldBeNil) test.That(t, len(errOut.messages), test.ShouldEqual, 0) @@ -466,7 +460,7 @@ func TestOrganizationEnableBillingServiceAction(t *testing.T) { EnableBillingServiceFunc: enableBillingFunc, } - cCtx, ac, out, errOut := setup(asc, nil, nil, nil, nil, "token") + cCtx, ac, out, errOut := setup(asc, nil, nil, nil, "token") test.That(t, ac.organizationEnableBillingServiceAction(cCtx, "test-org", "123 Main St, Suite 100, San Francisco, CA, 94105"), test.ShouldBeNil) test.That(t, len(errOut.messages), test.ShouldEqual, 0) @@ -523,7 +517,7 @@ func TestDataExportTabularAction(t *testing.T) { ExportTabularDataFunc: exportTabularDataFunc, } - cCtx, ac, out, errOut := setup(&inject.AppServiceClient{}, dsc, nil, nil, nil, "token") + cCtx, ac, out, errOut := setup(&inject.AppServiceClient{}, dsc, nil, nil, "token") test.That(t, ac.dataExportTabularAction(cCtx, parseStructFromCtx[dataExportTabularArgs](cCtx)), test.ShouldBeNil) test.That(t, len(errOut.messages), test.ShouldEqual, 0) @@ -577,7 +571,7 @@ func TestDataExportTabularAction(t *testing.T) { ExportTabularDataFunc: exportTabularDataFunc, } - cCtx, ac, out, errOut := setup(&inject.AppServiceClient{}, dsc, nil, nil, nil, "token") + cCtx, ac, out, errOut := setup(&inject.AppServiceClient{}, dsc, nil, nil, "token") err := ac.dataExportTabularAction(cCtx, parseStructFromCtx[dataExportTabularArgs](cCtx)) test.That(t, err, test.ShouldBeError, errors.New("error receiving tabular data: whoops")) @@ -747,7 +741,7 @@ func TestGetRobotPartLogs(t *testing.T) { } t.Run("no count", func(t *testing.T) { - cCtx, ac, out, errOut := setup(asc, nil, nil, nil, nil, "") + cCtx, ac, out, errOut := setup(asc, nil, nil, nil, "") test.That(t, ac.robotsPartLogsAction(cCtx, parseStructFromCtx[robotsPartLogsArgs](cCtx)), test.ShouldBeNil) @@ -768,7 +762,7 @@ func TestGetRobotPartLogs(t *testing.T) { }) t.Run("178 count", func(t *testing.T) { flags := map[string]any{"count": 178} - cCtx, ac, out, errOut := setup(asc, nil, nil, nil, flags, "") + cCtx, ac, out, errOut := setup(asc, nil, nil, flags, "") test.That(t, ac.robotsPartLogsAction(cCtx, parseStructFromCtx[robotsPartLogsArgs](cCtx)), test.ShouldBeNil) @@ -789,7 +783,7 @@ func TestGetRobotPartLogs(t *testing.T) { }) t.Run("max count", func(t *testing.T) { flags := map[string]any{generalFlagCount: maxNumLogs} - cCtx, ac, out, errOut := setup(asc, nil, nil, nil, flags, "") + cCtx, ac, out, errOut := setup(asc, nil, nil, flags, "") test.That(t, ac.robotsPartLogsAction(cCtx, parseStructFromCtx[robotsPartLogsArgs](cCtx)), test.ShouldBeNil) @@ -811,7 +805,7 @@ func TestGetRobotPartLogs(t *testing.T) { }) t.Run("negative count", func(t *testing.T) { flags := map[string]any{"count": -1} - cCtx, ac, out, errOut := setup(asc, nil, nil, nil, flags, "") + cCtx, ac, out, errOut := setup(asc, nil, nil, flags, "") test.That(t, ac.robotsPartLogsAction(cCtx, parseStructFromCtx[robotsPartLogsArgs](cCtx)), test.ShouldBeNil) @@ -834,7 +828,7 @@ func TestGetRobotPartLogs(t *testing.T) { }) t.Run("count too high", func(t *testing.T) { flags := map[string]any{"count": 1000000} - cCtx, ac, _, _ := setup(asc, nil, nil, nil, flags, "") + cCtx, ac, _, _ := setup(asc, nil, nil, flags, "") err := ac.robotsPartLogsAction(cCtx, parseStructFromCtx[robotsPartLogsArgs](cCtx)) test.That(t, err, test.ShouldNotBeNil) @@ -887,7 +881,7 @@ func TestShellFileCopy(t *testing.T) { } t.Run("no arguments or files", func(t *testing.T) { - cCtx, viamClient, _, _ := setup(asc, nil, nil, nil, partFlags, "token") + cCtx, viamClient, _, _ := setup(asc, nil, nil, partFlags, "token") test.That(t, viamClient.machinesPartCopyFilesAction(cCtx, parseStructFromCtx[machinesPartCopyFilesArgs](cCtx), logger), test.ShouldEqual, errNoFiles) @@ -895,13 +889,13 @@ func TestShellFileCopy(t *testing.T) { t.Run("one file path is insufficient", func(t *testing.T) { args := []string{"machine:path"} - cCtx, viamClient, _, _ := setup(asc, nil, nil, nil, partFlags, "token", args...) + cCtx, viamClient, _, _ := setup(asc, nil, nil, partFlags, "token", args...) test.That(t, viamClient.machinesPartCopyFilesAction(cCtx, parseStructFromCtx[machinesPartCopyFilesArgs](cCtx), logger), test.ShouldEqual, errLastArgOfFromMissing) args = []string{"path"} - cCtx, viamClient, _, _ = setup(asc, nil, nil, nil, partFlags, "token", args...) + cCtx, viamClient, _, _ = setup(asc, nil, nil, partFlags, "token", args...) test.That(t, viamClient.machinesPartCopyFilesAction(cCtx, parseStructFromCtx[machinesPartCopyFilesArgs](cCtx), logger), test.ShouldEqual, errLastArgOfToMissing) @@ -909,7 +903,7 @@ func TestShellFileCopy(t *testing.T) { t.Run("from has wrong path prefixes", func(t *testing.T) { args := []string{"machine:path", "path2", "machine:path3", "destination"} - cCtx, viamClient, _, _ := setup(asc, nil, nil, nil, partFlags, "token", args...) + cCtx, viamClient, _, _ := setup(asc, nil, nil, partFlags, "token", args...) test.That(t, viamClient.machinesPartCopyFilesAction(cCtx, parseStructFromCtx[machinesPartCopyFilesArgs](cCtx), logger), test.ShouldHaveSameTypeAs, copyFromPathInvalidError{}) @@ -1197,7 +1191,7 @@ func TestCreateOAuthAppAction(t *testing.T) { flags[oauthAppFlagRedirectURIs] = []string{"https://woof.com/home", "https://arf.com/home"} flags[oauthAppFlagLogoutURI] = "https://woof.com/logout" flags[oauthAppFlagEnabledGrants] = []string{"implicit", "password"} - cCtx, ac, out, errOut := setup(asc, nil, nil, nil, flags, "token") + cCtx, ac, out, errOut := setup(asc, nil, nil, flags, "token") test.That(t, ac.createOAuthAppAction(cCtx, parseStructFromCtx[createOAuthAppArgs](cCtx)), test.ShouldBeNil) test.That(t, len(errOut.messages), test.ShouldEqual, 0) test.That(t, out.messages[0], test.ShouldContainSubstring, @@ -1206,7 +1200,7 @@ func TestCreateOAuthAppAction(t *testing.T) { t.Run("should error if pkce is not a valid enum value", func(t *testing.T) { flags := map[string]any{oauthAppFlagClientAuthentication: unspecified, oauthAppFlagPKCE: "not_one_of_the_allowed_values"} - cCtx, ac, out, _ := setup(asc, nil, nil, nil, flags, "token") + cCtx, ac, out, _ := setup(asc, nil, nil, flags, "token") err := ac.updateOAuthAppAction(cCtx, parseStructFromCtx[updateOAuthAppArgs](cCtx)) test.That(t, err, test.ShouldNotBeNil) test.That(t, err.Error(), test.ShouldContainSubstring, "pkce must be a valid PKCE") @@ -1218,7 +1212,7 @@ func TestCreateOAuthAppAction(t *testing.T) { oauthAppFlagClientAuthentication: unspecified, oauthAppFlagPKCE: unspecified, oauthAppFlagURLValidation: "not_one_of_the_allowed_values", } - cCtx, ac, out, _ := setup(asc, nil, nil, nil, flags, "token") + cCtx, ac, out, _ := setup(asc, nil, nil, flags, "token") err := ac.updateOAuthAppAction(cCtx, parseStructFromCtx[updateOAuthAppArgs](cCtx)) test.That(t, err, test.ShouldNotBeNil) test.That(t, err.Error(), test.ShouldContainSubstring, "url-validation must be a valid UrlValidation") @@ -1249,7 +1243,7 @@ func TestReadOAuthApp(t *testing.T) { ReadOAuthAppFunc: readOAuthAppFunc, } - cCtx, ac, out, errOut := setup(asc, nil, nil, nil, nil, "token") + cCtx, ac, out, errOut := setup(asc, nil, nil, nil, "token") test.That(t, ac.readOAuthAppAction(cCtx, "test-org-id", "test-client-id"), test.ShouldBeNil) test.That(t, len(out.messages), test.ShouldEqual, 9) @@ -1286,7 +1280,7 @@ func TestUpdateOAuthAppAction(t *testing.T) { flags[oauthAppFlagRedirectURIs] = []string{"https://woof.com/home", "https://arf.com/home"} flags[oauthAppFlagLogoutURI] = "https://woof.com/logout" flags[oauthAppFlagEnabledGrants] = []string{"implicit", "password"} - cCtx, ac, out, errOut := setup(asc, nil, nil, nil, flags, "token") + cCtx, ac, out, errOut := setup(asc, nil, nil, flags, "token") test.That(t, ac.updateOAuthAppAction(cCtx, parseStructFromCtx[updateOAuthAppArgs](cCtx)), test.ShouldBeNil) test.That(t, len(errOut.messages), test.ShouldEqual, 0) test.That(t, out.messages[0], test.ShouldContainSubstring, "Successfully updated OAuth app") @@ -1297,7 +1291,7 @@ func TestUpdateOAuthAppAction(t *testing.T) { flags[generalFlagOrgID] = "org-id" flags[oauthAppFlagClientID] = "client-id" flags[oauthAppFlagClientAuthentication] = "not_one_of_the_allowed_values" - cCtx, ac, out, _ := setup(asc, nil, nil, nil, flags, "token") + cCtx, ac, out, _ := setup(asc, nil, nil, flags, "token") err := ac.updateOAuthAppAction(cCtx, parseStructFromCtx[updateOAuthAppArgs](cCtx)) test.That(t, err, test.ShouldNotBeNil) test.That(t, err.Error(), test.ShouldContainSubstring, "client-authentication must be a valid ClientAuthentication") @@ -1306,7 +1300,7 @@ func TestUpdateOAuthAppAction(t *testing.T) { t.Run("should error if pkce is not a valid enum value", func(t *testing.T) { flags := map[string]any{oauthAppFlagClientAuthentication: unspecified, oauthAppFlagPKCE: "not_one_of_the_allowed_values"} - cCtx, ac, out, _ := setup(asc, nil, nil, nil, flags, "token") + cCtx, ac, out, _ := setup(asc, nil, nil, flags, "token") err := ac.updateOAuthAppAction(cCtx, parseStructFromCtx[updateOAuthAppArgs](cCtx)) test.That(t, err, test.ShouldNotBeNil) test.That(t, err.Error(), test.ShouldContainSubstring, "pkce must be a valid PKCE") @@ -1318,7 +1312,7 @@ func TestUpdateOAuthAppAction(t *testing.T) { oauthAppFlagClientAuthentication: unspecified, oauthAppFlagPKCE: unspecified, oauthAppFlagURLValidation: "not_one_of_the_allowed_values", } - cCtx, ac, out, _ := setup(asc, nil, nil, nil, flags, "token") + cCtx, ac, out, _ := setup(asc, nil, nil, flags, "token") err := ac.updateOAuthAppAction(cCtx, parseStructFromCtx[updateOAuthAppArgs](cCtx)) test.That(t, err, test.ShouldNotBeNil) test.That(t, err.Error(), test.ShouldContainSubstring, "url-validation must be a valid UrlValidation") diff --git a/cli/module_build_test.go b/cli/module_build_test.go index a7eeb58d7c0..71f2df17da6 100644 --- a/cli/module_build_test.go +++ b/cli/module_build_test.go @@ -56,7 +56,7 @@ func TestStartBuild(t *testing.T) { StartBuildFunc: func(ctx context.Context, in *v1.StartBuildRequest, opts ...grpc.CallOption) (*v1.StartBuildResponse, error) { return &v1.StartBuildResponse{BuildId: "xyz123"}, nil }, - }, nil, map[string]any{moduleFlagPath: manifest, generalFlagVersion: "1.2.3"}, "token") + }, map[string]any{moduleFlagPath: manifest, generalFlagVersion: "1.2.3"}, "token") err := ac.moduleBuildStartAction(cCtx, parseStructFromCtx[moduleBuildStartArgs](cCtx)) test.That(t, err, test.ShouldBeNil) test.That(t, out.messages, test.ShouldHaveLength, 1) @@ -79,7 +79,7 @@ func TestListBuild(t *testing.T) { }, }}, nil }, - }, nil, map[string]any{moduleFlagPath: manifest}, "token") + }, map[string]any{moduleFlagPath: manifest}, "token") err := ac.moduleBuildListAction(cCtx, parseStructFromCtx[moduleBuildListArgs](cCtx)) test.That(t, err, test.ShouldBeNil) joinedOutput := strings.Join(out.messages, "") @@ -121,7 +121,7 @@ func TestModuleBuildWait(t *testing.T) { }, }}, nil }, - }, nil, map[string]any{}, "token") + }, map[string]any{}, "token") startWaitTime := time.Now() statuses, err := ac.waitForBuildToFinish("xyz123", "") test.That(t, err, test.ShouldBeNil) @@ -154,7 +154,7 @@ func TestModuleGetPlatformsForModule(t *testing.T) { }, }}, nil }, - }, nil, map[string]any{}, "token") + }, map[string]any{}, "token") platforms, err := ac.getPlatformsForModuleBuild("xyz123") test.That(t, err, test.ShouldBeNil) test.That(t, platforms, test.ShouldResemble, []string{"linux/amd64", "linux/arm64"}) @@ -195,7 +195,7 @@ func TestLocalBuild(t *testing.T) { // run the build local action cCtx, _, out, errOut := setup(&inject.AppServiceClient{}, nil, &inject.BuildServiceClient{}, - nil, map[string]any{moduleFlagPath: manifestPath, generalFlagVersion: "1.2.3"}, "token") + map[string]any{moduleFlagPath: manifestPath, generalFlagVersion: "1.2.3"}, "token") manifest, err := loadManifest(manifestPath) test.That(t, err, test.ShouldBeNil) err = moduleBuildLocalAction(cCtx, &manifest) diff --git a/cli/module_generate.go b/cli/module_generate.go index 218d2e826f0..e655b70a1b1 100644 --- a/cli/module_generate.go +++ b/cli/module_generate.go @@ -378,9 +378,12 @@ func populateAdditionalInfo(newModule *modulegen.ModuleInputs) { } newModule.ResourceTypePascal = spaceReplacer.Replace(titleCaser.String(replacer.Replace(newModule.ResourceType))) newModule.ModelPascal = spaceReplacer.Replace(titleCaser.String(replacer.Replace(newModule.ModelName))) - newModule.ModelTriple = fmt.Sprintf("%s:%s:%s", newModule.Namespace, newModule.ModuleName, newModule.ModelName) newModule.ModelCamel = strings.ToLower(string(newModule.ModelPascal[0])) + newModule.ModelPascal[1:] newModule.ModelLowercase = strings.ToLower(newModule.ModelPascal) + + modelTriple := fmt.Sprintf("%s:%s:%s", newModule.Namespace, newModule.ModuleName, newModule.ModelName) + newModule.ModelTriple = modelTriple + newModule.ModelReadmeLink = "README.md#" + generateAnchor(fmt.Sprintf("Model %s", modelTriple)) } // Creates a new directory with moduleName. @@ -415,6 +418,11 @@ func renderCommonFiles(c *cli.Context, module modulegen.ModuleInputs, globalArgs return errors.Wrapf(err, "failed to write generator info to %s", infoFilePath) } + // Render README.md + if err := renderReadme(module); err != nil { + return errors.Wrap(err, "failed to render README.md") + } + // Render workflows for cloud build if module.EnableCloudBuild { debugf(c.App.Writer, globalArgs.Debug, "\tCreating cloud build workflow") @@ -784,6 +792,38 @@ func createModuleAndManifest(cCtx *cli.Context, c *viamClient, module modulegen. return nil } +// Create the README.md file. +func renderReadme(module modulegen.ModuleInputs) error { + readmeTemplatePath, err := templates.Open(filepath.Join(templatesPath, defaultReadmeFilename)) + readmeDest := filepath.Join(module.ModuleName, defaultReadmeFilename) + if err != nil { + return err + } + defer utils.UncheckedErrorFunc(readmeTemplatePath.Close) + tBytes, err := io.ReadAll(readmeTemplatePath) + if err != nil { + return err + } + + tmpl, err := template.New(defaultReadmeFilename).Parse(string(tBytes)) + if err != nil { + return err + } + + //nolint:gosec + destFile, err := os.Create(readmeDest) + if err != nil { + return err + } + defer utils.UncheckedErrorFunc(destFile.Close) + + err = tmpl.Execute(destFile, module) + if err != nil { + return err + } + return nil +} + // Create the meta.json manifest. func renderManifest(c *cli.Context, moduleID string, module modulegen.ModuleInputs, globalArgs globalArgs) error { debugf(c.App.Writer, globalArgs.Debug, "Rendering module manifest") @@ -793,13 +833,14 @@ func renderManifest(c *cli.Context, moduleID string, module modulegen.ModuleInpu visibility = moduleVisibilityPublic } + modelDescription := "Provide a short (100 characters or less) description of this model here" manifest := moduleManifest{ Schema: "https://dl.viam.dev/module.schema.json", ModuleID: moduleID, Visibility: visibility, Description: fmt.Sprintf("Modular %s %s: %s", module.ResourceSubtype, module.ResourceType, module.ModelName), Models: []ModuleComponent{ - {API: module.API, Model: module.ModelTriple}, + {API: module.API, Model: module.ModelTriple, MarkdownLink: &module.ModelReadmeLink, Description: &modelDescription}, }, } switch module.Language { diff --git a/cli/module_generate/_templates/README.md b/cli/module_generate/_templates/README.md new file mode 100644 index 00000000000..2845828bc4a --- /dev/null +++ b/cli/module_generate/_templates/README.md @@ -0,0 +1,50 @@ +# Module {{.ModuleName}} + +Provide a description of the purpose of the module and any relevant information. + +## Model {{.ModelTriple}} + +Provide a description of the model and any relevant information. + +### Configuration +The following attribute template can be used to configure this model: + +```json +{ +"attribute_1": , +"attribute_2": +} +``` + +#### Attributes + +The following attributes are available for this model: + +| Name | Type | Inclusion | Description | +|---------------|--------|-----------|----------------------------| +| `attribute_1` | float | Required | Description of attribute 1 | +| `attribute_2` | string | Optional | Description of attribute 2 | + +#### Example Configuration + +```json +{ + "attribute_1": 1.0, + "attribute_2": "foo" +} +``` + +### DoCommand + +If your model implements DoCommand, provide an example payload of each command that is supported and the arguments that can be used. If your model does not implement DoCommand, remove this section. + +#### Example DoCommand + +```json +{ + "command_name": { + "arg1": "foo", + "arg2": 1 + } +} +``` diff --git a/cli/module_generate/modulegen/inputs.go b/cli/module_generate/modulegen/inputs.go index 6ac65445337..57bdb42f122 100644 --- a/cli/module_generate/modulegen/inputs.go +++ b/cli/module_generate/modulegen/inputs.go @@ -35,8 +35,8 @@ type ModuleInputs struct { ModelCamel string `json:"-"` ModelTriple string `json:"-"` ModelLowercase string `json:"-"` - - SDKVersion string `json:"-"` + ModelReadmeLink string `json:"-"` + SDKVersion string `json:"-"` } // Resources is a list of all the available resources in Viam. diff --git a/cli/module_generate_test.go b/cli/module_generate_test.go index d7e9cae4e56..61bab7b37fb 100644 --- a/cli/module_generate_test.go +++ b/cli/module_generate_test.go @@ -36,6 +36,7 @@ func TestGenerateModuleAction(t *testing.T) { ResourceSubtypePascal: "Arm", ModelPascal: "MyModel", ModelTriple: "my-org:my-module:my-model", + ModelReadmeLink: "model-readme-link", SDKVersion: "0.0.0", } @@ -74,6 +75,17 @@ func TestGenerateModuleAction(t *testing.T) { test.That(t, err, test.ShouldBeNil) test.That(t, module.ModuleName, test.ShouldEqual, testModule.ModuleName) + _, err = os.Stat(filepath.Join(modulePath, "README.md")) + test.That(t, err, test.ShouldBeNil) + + readme, err := os.Open(filepath.Join(modulePath, "README.md")) + test.That(t, err, test.ShouldBeNil) + defer readme.Close() + bytes, err = io.ReadAll(readme) + test.That(t, err, test.ShouldBeNil) + test.That(t, string(bytes), test.ShouldContainSubstring, "Module "+testModule.ModuleName) + test.That(t, string(bytes), test.ShouldContainSubstring, "Model "+testModule.ModelTriple) + // cloud build enabled _, err = os.Stat(filepath.Join(modulePath, ".github")) test.That(t, err, test.ShouldBeNil) @@ -159,7 +171,7 @@ func TestGenerateModuleAction(t *testing.T) { StartBuildFunc: func(ctx context.Context, in *v1.StartBuildRequest, opts ...grpc.CallOption) (*v1.StartBuildResponse, error) { return &v1.StartBuildResponse{BuildId: "xyz123"}, nil }, - }, nil, map[string]any{}, "token") + }, map[string]any{}, "token") err := createModuleAndManifest(cCtx, ac, testModule, globalArgs) test.That(t, err, test.ShouldBeNil) }) @@ -170,5 +182,17 @@ func TestGenerateModuleAction(t *testing.T) { test.That(t, err, test.ShouldBeNil) _, err = os.Stat(filepath.Join(testDir, testModule.ModuleName, "meta.json")) test.That(t, err, test.ShouldBeNil) + + manifestFile, err := os.Open(filepath.Join(testDir, testModule.ModuleName, "meta.json")) + test.That(t, err, test.ShouldBeNil) + defer manifestFile.Close() + bytes, err := io.ReadAll(manifestFile) + test.That(t, err, test.ShouldBeNil) + var manifest moduleManifest + err = json.Unmarshal(bytes, &manifest) + test.That(t, err, test.ShouldBeNil) + test.That(t, len(manifest.Models), test.ShouldEqual, 1) + test.That(t, manifest.Models[0].Model, test.ShouldEqual, testModule.ModelTriple) + test.That(t, *manifest.Models[0].MarkdownLink, test.ShouldEqual, testModule.ModelReadmeLink) }) } diff --git a/cli/module_registry.go b/cli/module_registry.go index e99e8bbad3a..95c17c34717 100644 --- a/cli/module_registry.go +++ b/cli/module_registry.go @@ -106,6 +106,7 @@ type moduleManifest struct { const ( defaultManifestFilename = "meta.json" + defaultReadmeFilename = "README.md" ) type createModuleActionArgs struct { diff --git a/cli/module_registry_test.go b/cli/module_registry_test.go index ada46dd8703..27528d2b950 100644 --- a/cli/module_registry_test.go +++ b/cli/module_registry_test.go @@ -34,7 +34,7 @@ func TestUpdateModelsAction(t *testing.T) { test.That(t, err, test.ShouldBeNil) flags := map[string]any{"binary": binaryPath, "module": metaPath} - cCtx, _, _, errOut := setup(&inject.AppServiceClient{}, nil, nil, nil, flags, "") + cCtx, _, _, errOut := setup(&inject.AppServiceClient{}, nil, nil, flags, "") test.That(t, UpdateModelsAction(cCtx, parseStructFromCtx[updateModelsArgs](cCtx)), test.ShouldBeNil) test.That(t, len(errOut.messages), test.ShouldEqual, 0) diff --git a/cli/module_reload_test.go b/cli/module_reload_test.go index 9a29f1b70ff..7ef60ecd2e6 100644 --- a/cli/module_reload_test.go +++ b/cli/module_reload_test.go @@ -23,7 +23,7 @@ func TestConfigureModule(t *testing.T) { StartBuildFunc: func(ctx context.Context, in *v1.StartBuildRequest, opts ...grpc.CallOption) (*v1.StartBuildResponse, error) { return &v1.StartBuildResponse{BuildId: "xyz123"}, nil }, - }, nil, map[string]any{moduleFlagPath: manifestPath, generalFlagVersion: "1.2.3"}, "token") + }, map[string]any{moduleFlagPath: manifestPath, generalFlagVersion: "1.2.3"}, "token") err := ac.moduleBuildStartAction(cCtx, parseStructFromCtx[moduleBuildStartArgs](cCtx)) test.That(t, err, test.ShouldBeNil) test.That(t, out.messages, test.ShouldHaveLength, 1) @@ -62,7 +62,7 @@ func TestFullReloadFlow(t *testing.T) { {ApiKey: &apppb.APIKey{}}, }}, nil }, - }, nil, &inject.BuildServiceClient{}, nil, + }, nil, &inject.BuildServiceClient{}, map[string]any{ moduleFlagPath: manifestPath, generalFlagPartID: "part-123", moduleBuildFlagNoBuild: true, moduleFlagLocal: true, diff --git a/components/generic/generic.go b/components/generic/generic.go index 218b6fe5158..99cb60acbac 100644 --- a/components/generic/generic.go +++ b/components/generic/generic.go @@ -31,6 +31,12 @@ func Named(name string) resource.Name { return resource.NewName(API, name) } +// FromDependencies is a helper for getting the named generic from a collection of +// dependencies. +func FromDependencies(deps resource.Dependencies, name string) (resource.Resource, error) { + return resource.FromDependencies[resource.Resource](deps, Named(name)) +} + // FromRobot is a helper for getting the named Generic from the given Robot. func FromRobot(r robot.Robot, name string) (resource.Resource, error) { return robot.ResourceFromRobot[resource.Resource](r, Named(name)) diff --git a/components/gripper/gripper.go b/components/gripper/gripper.go index d2e8450c2fd..74d373b69ff 100644 --- a/components/gripper/gripper.go +++ b/components/gripper/gripper.go @@ -79,6 +79,12 @@ func FromRobot(r robot.Robot, name string) (Gripper, error) { return robot.ResourceFromRobot[Gripper](r, Named(name)) } +// FromDependencies is a helper for getting the named gripper from a collection of +// dependencies. +func FromDependencies(deps resource.Dependencies, name string) (Gripper, error) { + return resource.FromDependencies[Gripper](deps, Named(name)) +} + // NamesFromRobot is a helper for getting all gripper names from the given Robot. func NamesFromRobot(r robot.Robot) []string { return robot.NamesByAPI(r, API) diff --git a/config/config.go b/config/config.go index 14f33cf9250..d911f6a11c4 100644 --- a/config/config.go +++ b/config/config.go @@ -965,6 +965,22 @@ func (config *AuthHandlerConfig) Validate(path string) error { return nil } +// ParseAPIKeys parses API keys from the handler config. It will return an empty map +// if the credential type is not [rpc.CredentialsTypeAPIKey]. +func ParseAPIKeys(handler AuthHandlerConfig) map[string]string { + apiKeys := map[string]string{} + if handler.Type == rpc.CredentialsTypeAPIKey { + for k := range handler.Config { + // if it is not a legacy api key indicated by "key(s)" key + // current api keys will follow format { [keyId]: [key] } + if k != "keys" && k != "key" { + apiKeys[k] = handler.Config.String(k) + } + } + } + return apiKeys +} + // CreateTLSWithCert creates a tls.Config with the TLS certificate to be returned. func CreateTLSWithCert(cfg *Config) (*tls.Config, error) { cert, err := tls.X509KeyPair([]byte(cfg.Cloud.TLSCertificate), []byte(cfg.Cloud.TLSPrivateKey)) diff --git a/config/module.go b/config/module.go index 7548ec4a3c0..8d6711664ea 100644 --- a/config/module.go +++ b/config/module.go @@ -130,6 +130,20 @@ func (m Module) Equals(other Module) bool { return reflect.DeepEqual(m, other) } +// MergeEnvVars will merge the provided environment variables with the existing Environment, with the existing Environment +// taking priority. +func (m *Module) MergeEnvVars(env map[string]string) { + if m.Environment == nil { + m.Environment = make(map[string]string) + } + for k, v := range env { + if _, ok := m.Environment[k]; ok { + continue + } + m.Environment[k] = v + } +} + var tarballExtensionsRegexp = regexp.MustCompile(`\.(tgz|tar\.gz)$`) // NeedsSyntheticPackage returns true if this is a local module pointing at a tarball. @@ -315,6 +329,7 @@ func (m *Module) FirstRun( for key, val := range env { cmd.Env = append(cmd.Env, key+"="+val) } + utils.LogViamEnvVariables("Running first run script with following Viam environment variables", env, logger) stdOut, err := cmd.StdoutPipe() if err != nil { diff --git a/config/module_test.go b/config/module_test.go index 19e40429670..d6293185b5c 100644 --- a/config/module_test.go +++ b/config/module_test.go @@ -397,6 +397,31 @@ func TestFindMetaJSONFile(t *testing.T) { }) } +func TestMergeEnvVars(t *testing.T) { + t.Run("nil", func(t *testing.T) { + m := Module{} + expected := map[string]string{"abc": "def", "hello": "world"} + + test.That(t, func() { m.MergeEnvVars(expected) }, test.ShouldNotPanic) + test.That(t, m.Environment, test.ShouldResemble, expected) + }) + t.Run("empty", func(t *testing.T) { + m := Module{Environment: map[string]string{}} + expected := map[string]string{"abc": "def", "hello": "world"} + m.MergeEnvVars(expected) + test.That(t, m.Environment, test.ShouldResemble, expected) + }) + + t.Run("existing env priority", func(t *testing.T) { + m := Module{Environment: map[string]string{"hello": "world"}} + env := map[string]string{"abc": "def", "hello": "friend"} + + expected := map[string]string{"abc": "def", "hello": "world"} + m.MergeEnvVars(env) + test.That(t, m.Environment, test.ShouldResemble, expected) + }) +} + // testWriteJSON is a t.Helper that serializes `value` to `path` as json. func testWriteJSON(t *testing.T, path string, value any) { t.Helper() diff --git a/config/reader.go b/config/reader.go index 1898e41bd6d..e80ca240e9d 100644 --- a/config/reader.go +++ b/config/reader.go @@ -11,6 +11,7 @@ import ( "os" "path/filepath" "runtime" + "sort" "time" "github.com/a8m/envsubst" @@ -437,6 +438,36 @@ func processConfigLocalConfig(unprocessedConfig *Config, logger logging.Logger) return processConfig(unprocessedConfig, false, logger) } +// additionalModuleEnvVars will get additional environment variables for modules using other parts of the config. +func additionalModuleEnvVars(cloud *Cloud, auth AuthConfig) map[string]string { + env := make(map[string]string) + if cloud != nil { + env[rutils.PrimaryOrgIDEnvVar] = cloud.PrimaryOrgID + env[rutils.LocationIDEnvVar] = cloud.LocationID + env[rutils.MachineIDEnvVar] = cloud.MachineID + env[rutils.MachinePartIDEnvVar] = cloud.ID + } + for _, handler := range auth.Handlers { + if handler.Type != rpc.CredentialsTypeAPIKey { + continue + } + apiKeys := ParseAPIKeys(handler) + if len(apiKeys) == 0 { + continue + } + // the keys come in unsorted, so sort the keys so we'll always get the same API key + // if there are no changes + keyIDs := make([]string, 0, len(apiKeys)) + for k := range apiKeys { + keyIDs = append(keyIDs, k) + } + sort.Strings(keyIDs) + env[rutils.APIKeyIDEnvVar] = keyIDs[0] + env[rutils.APIKeyEnvVar] = apiKeys[keyIDs[0]] + } + return env +} + // processConfig processes the config passed in. The config can be either JSON or gRPC derived. // If any part of this function errors, the function will exit and no part of the new config will be returned // until it is corrected. @@ -596,6 +627,15 @@ func processConfig(unprocessedConfig *Config, fromCloud bool, logger logging.Log } } + // add additional environment vars to modules + // adding them here ensures that if the parsed API key changes, the module will be restarted with the updated environment. + env := additionalModuleEnvVars(cfg.Cloud, cfg.Auth) + if len(env) > 0 { + for _, m := range cfg.Modules { + m.MergeEnvVars(env) + } + } + // now that the attribute maps are converted, validate configs and get implicit dependencies for builtin resource models if err := cfg.Ensure(fromCloud, logger); err != nil { return nil, err diff --git a/config/reader_test.go b/config/reader_test.go index 0767f5d9a0e..94c80e7a63a 100644 --- a/config/reader_test.go +++ b/config/reader_test.go @@ -13,9 +13,11 @@ import ( "github.com/pkg/errors" pb "go.viam.com/api/app/v1" "go.viam.com/test" + "go.viam.com/utils/rpc" "go.viam.com/rdk/config/testutils" "go.viam.com/rdk/logging" + "go.viam.com/rdk/utils" ) func TestFromReader(t *testing.T) { @@ -394,3 +396,97 @@ func TestReadTLSFromCache(t *testing.T) { test.That(t, err, test.ShouldBeNil) }) } + +func TestAdditionalModuleEnvVars(t *testing.T) { + t.Run("empty", func(t *testing.T) { + expected := map[string]string{} + observed := additionalModuleEnvVars(nil, AuthConfig{}) + test.That(t, observed, test.ShouldResemble, expected) + }) + + cloud1 := Cloud{ + ID: "test", + LocationID: "the-location", + PrimaryOrgID: "the-primary-org", + MachineID: "the-machine", + } + t.Run("cloud", func(t *testing.T) { + expected := map[string]string{ + utils.MachinePartIDEnvVar: cloud1.ID, + utils.MachineIDEnvVar: cloud1.MachineID, + utils.PrimaryOrgIDEnvVar: cloud1.PrimaryOrgID, + utils.LocationIDEnvVar: cloud1.LocationID, + } + observed := additionalModuleEnvVars(&cloud1, AuthConfig{}) + test.That(t, observed, test.ShouldResemble, expected) + }) + + authWithExternalCreds := AuthConfig{ + Handlers: []AuthHandlerConfig{{Type: rpc.CredentialsTypeExternal}}, + } + + t.Run("auth with external creds", func(t *testing.T) { + expected := map[string]string{} + observed := additionalModuleEnvVars(nil, authWithExternalCreds) + test.That(t, observed, test.ShouldResemble, expected) + }) + apiKeyID := "abc" + apiKey := "def" + authWithAPIKeyCreds := AuthConfig{ + Handlers: []AuthHandlerConfig{{Type: rpc.CredentialsTypeAPIKey, Config: utils.AttributeMap{ + apiKeyID: apiKey, + "keys": []string{apiKeyID}, + }}}, + } + + t.Run("auth with api key creds", func(t *testing.T) { + expected := map[string]string{ + utils.APIKeyEnvVar: apiKey, + utils.APIKeyIDEnvVar: apiKeyID, + } + observed := additionalModuleEnvVars(nil, authWithAPIKeyCreds) + test.That(t, observed, test.ShouldResemble, expected) + }) + + apiKeyID2 := "uvw" + apiKey2 := "xyz" + order1 := AuthConfig{ + Handlers: []AuthHandlerConfig{{Type: rpc.CredentialsTypeAPIKey, Config: utils.AttributeMap{ + apiKeyID: apiKey, + apiKeyID2: apiKey2, + "keys": []string{apiKeyID, apiKeyID2}, + }}}, + } + order2 := AuthConfig{ + Handlers: []AuthHandlerConfig{{Type: rpc.CredentialsTypeAPIKey, Config: utils.AttributeMap{ + apiKeyID2: apiKey2, + apiKeyID: apiKey, + "keys": []string{apiKeyID, apiKeyID2}, + }}}, + } + + t.Run("auth with keys in different order are stable", func(t *testing.T) { + expected := map[string]string{ + utils.APIKeyEnvVar: apiKey, + utils.APIKeyIDEnvVar: apiKeyID, + } + observed := additionalModuleEnvVars(nil, order1) + test.That(t, observed, test.ShouldResemble, expected) + + observed = additionalModuleEnvVars(nil, order2) + test.That(t, observed, test.ShouldResemble, expected) + }) + + t.Run("full", func(t *testing.T) { + expected := map[string]string{ + utils.MachinePartIDEnvVar: cloud1.ID, + utils.MachineIDEnvVar: cloud1.MachineID, + utils.PrimaryOrgIDEnvVar: cloud1.PrimaryOrgID, + utils.LocationIDEnvVar: cloud1.LocationID, + utils.APIKeyEnvVar: apiKey, + utils.APIKeyIDEnvVar: apiKeyID, + } + observed := additionalModuleEnvVars(&cloud1, authWithAPIKeyCreds) + test.That(t, observed, test.ShouldResemble, expected) + }) +} diff --git a/go.mod b/go.mod index f25f158cf0f..4a5ffa632c0 100644 --- a/go.mod +++ b/go.mod @@ -75,9 +75,9 @@ 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.380 + go.viam.com/api v0.1.383 go.viam.com/test v1.2.4 - go.viam.com/utils v0.1.126 + go.viam.com/utils v0.1.127 goji.io v2.0.2+incompatible golang.org/x/image v0.19.0 golang.org/x/mobile v0.0.0-20240112133503-c713f31d574b diff --git a/go.sum b/go.sum index e6369eae6d0..9c19a347b51 100644 --- a/go.sum +++ b/go.sum @@ -1513,12 +1513,12 @@ 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.380 h1:VgRHDlPBku+kjIp4omxmPNmRVZezytFUUOFJ2xpRFR8= -go.viam.com/api v0.1.380/go.mod h1:g5eipXHNm0rQmW7DWya6avKcmzoypLmxnMlAaIsE5Ls= +go.viam.com/api v0.1.383 h1:HE9EpUWoyDBYJLTVLo29f66oCjzH31V1YJ02tlzCtyo= +go.viam.com/api v0.1.383/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.126 h1:ecFlzln5/u1NqzVMOVxwgwbkg4dDWvQmcCS2fMg0ZNU= -go.viam.com/utils v0.1.126/go.mod h1:g1CaEkf7aJCrSI/Sfkx+6cBS1+Y3fPT2pvMQbp7TTBI= +go.viam.com/utils v0.1.127 h1:Ju7SKelAVzTWNMSJyoomY7svn/HN5+xdFR7gltYePjE= +go.viam.com/utils v0.1.127/go.mod h1:g1CaEkf7aJCrSI/Sfkx+6cBS1+Y3fPT2pvMQbp7TTBI= go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2 h1:WJhcL4p+YeDxmZWg141nRm7XC8IDmhz7lk5GpadO1Sg= go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= gocv.io/x/gocv v0.25.0/go.mod h1:Rar2PS6DV+T4FL+PM535EImD/h13hGVaHhnCu1xarBs= diff --git a/module/modmanager/manager.go b/module/modmanager/manager.go index f5b05478f6e..8e21558a97b 100644 --- a/module/modmanager/manager.go +++ b/module/modmanager/manager.go @@ -612,6 +612,29 @@ func (mgr *Manager) Configs() []config.Module { return configs } +// AllModels returns a slice of resource.ModuleModelDiscovery representing the available models +// from the currently managed modules. +func (mgr *Manager) AllModels() []resource.ModuleModelDiscovery { + moduleTypes := map[string]config.ModuleType{} + models := []resource.ModuleModelDiscovery{} + for _, moduleConfig := range mgr.Configs() { + moduleName := moduleConfig.Name + moduleTypes[moduleName] = moduleConfig.Type + } + for moduleName, handleMap := range mgr.Handles() { + for api, handle := range handleMap { + for _, model := range handle { + modelModel := resource.ModuleModelDiscovery{ + ModuleName: moduleName, Model: model, API: api.API, + FromLocalModule: moduleTypes[moduleName] == config.ModuleTypeLocal, + } + models = append(models, modelModel) + } + } + } + return models +} + // Provides returns true if a component/service config WOULD be handled by a module. func (mgr *Manager) Provides(conf resource.Config) bool { _, ok := mgr.getModule(conf) @@ -1198,6 +1221,8 @@ func (m *module) startProcess( defer checkTicker.Stop() m.logger.CInfow(ctx, "Starting up module", "module", m.cfg.Name) + rutils.LogViamEnvVariables("Starting module with following Viam environment variables", moduleEnvironment, m.logger) + ctxTimeout, cancel := context.WithTimeout(ctx, rutils.GetModuleStartupTimeout(m.logger)) defer cancel() for { @@ -1302,12 +1327,15 @@ func (m *module) registerResources(mgr modmaninterface.ModuleManager) { if err != nil { return nil, err } + + //nolint:deprecated,staticcheck req := &robotpb.DiscoverComponentsRequest{ Queries: []*robotpb.DiscoveryQuery{ {Subtype: apiCopy.API.String(), Model: modelCopy.String(), Extra: extraStructPb}, }, } + //nolint:deprecated,staticcheck res, err := m.robotClient.DiscoverComponents(ctx, req) if err != nil { m.logger.Errorf("error in modular DiscoverComponents: %s", err) @@ -1413,6 +1441,7 @@ func getFullEnvironment( environment["VIAM_MODULE_ID"] = cfg.ModuleID } // Overwrite the base environment variables with the module's environment variables (if specified) + // VIAM_MODULE_ROOT is filled out by app.viam.com in cloud robots. for key, value := range cfg.Environment { environment[key] = value } diff --git a/module/modmanager/manager_test.go b/module/modmanager/manager_test.go index 5ab749732f6..73029ea1846 100644 --- a/module/modmanager/manager_test.go +++ b/module/modmanager/manager_test.go @@ -206,6 +206,32 @@ func TestModManagerFunctions(t *testing.T) { test.That(t, ok, test.ShouldBeTrue) test.That(t, reg.Constructor, test.ShouldNotBeNil) + t.Log("test AllModels") + modCfg2 := config.Module{ + Name: "simple-module2", + ExePath: modPath, + Type: config.ModuleTypeLocal, + } + err = mgr.Add(ctx, modCfg2) + test.That(t, err, test.ShouldBeNil) + models := mgr.AllModels() + for _, model := range models { + test.That(t, model.Model, test.ShouldResemble, resource.NewModel("acme", "demo", "mycounter")) + test.That(t, model.API, test.ShouldResemble, resource.NewAPI("rdk", "component", "generic")) + switch model.ModuleName { + case "simple-module": + test.That(t, model.FromLocalModule, test.ShouldEqual, false) + case "simple-module2": + test.That(t, model.FromLocalModule, test.ShouldEqual, true) + default: + t.Fail() + t.Logf("test AllModels failure: unrecoginzed moduleName %v", model.ModuleName) + } + } + names, err := mgr.Remove(modCfg2.Name) + test.That(t, names, test.ShouldBeEmpty) + test.That(t, err, test.ShouldBeNil) + t.Log("test Provides") ok = mgr.Provides(cfgCounter1) test.That(t, ok, test.ShouldBeTrue) diff --git a/module/modmaninterface/interface.go b/module/modmaninterface/interface.go index 605910d2ba8..4763f923e25 100644 --- a/module/modmaninterface/interface.go +++ b/module/modmaninterface/interface.go @@ -24,6 +24,7 @@ type ModuleManager interface { CleanModuleDataDirectory() error Configs() []config.Module + AllModels() []resource.ModuleModelDiscovery Provides(cfg resource.Config) bool Handles() map[string]module.HandlerMap diff --git a/module/module.go b/module/module.go index 877741ec0da..d879da4a6cd 100644 --- a/module/module.go +++ b/module/module.go @@ -538,8 +538,11 @@ func (m *Module) AddResource(ctx context.Context, req *pb.AddResourceRequest) (* return &pb.AddResourceResponse{}, nil } +// DiscoverComponents is DEPRECATED!!! Please use the Discovery Service instead. // DiscoverComponents takes a list of discovery queries and returns corresponding // component configurations. +// +//nolint:deprecated,staticcheck func (m *Module) DiscoverComponents( ctx context.Context, req *robotpb.DiscoverComponentsRequest, diff --git a/resource/discovery.go b/resource/discovery.go index 9b2a11f6cba..f4b6c8e686b 100644 --- a/resource/discovery.go +++ b/resource/discovery.go @@ -4,6 +4,8 @@ import ( "context" "fmt" + pb "go.viam.com/api/robot/v1" + "go.viam.com/rdk/logging" ) @@ -33,8 +35,24 @@ type ( Query DiscoveryQuery Cause error } + + // ModuleModelDiscovery holds the API and Model information of models within a module. + ModuleModelDiscovery struct { + ModuleName string + API API + Model Model + FromLocalModule bool + } ) +// ToProto converts a ModuleModelDiscovery into the equivalent proto message. +func (mm *ModuleModelDiscovery) ToProto() *pb.ModuleModel { + return &pb.ModuleModel{ + Model: mm.Model.String(), Api: mm.API.String(), ModuleName: mm.ModuleName, + FromLocalModule: mm.FromLocalModule, + } +} + func (e *DiscoverError) Error() string { return fmt.Sprintf("failed to get discovery for api %q and model %q error: %v", e.Query.API, e.Query.Model, e.Cause) } diff --git a/robot/client/client.go b/robot/client/client.go index a3967f954bc..94461e0744c 100644 --- a/robot/client/client.go +++ b/robot/client/client.go @@ -860,6 +860,7 @@ func (rc *RobotClient) Logger() logging.Logger { return rc.logger } +// DiscoverComponents is DEPRECATED!!! Please use the Discovery Service instead. // DiscoverComponents takes a list of discovery queries and returns corresponding // component configurations. // @@ -871,7 +872,11 @@ func (rc *RobotClient) Logger() logging.Logger { // // // Get component configurations with these queries. // component_configs, err := machine.DiscoverComponents(ctx.Background(), qs) +// +//nolint:deprecated,staticcheck func (rc *RobotClient) DiscoverComponents(ctx context.Context, qs []resource.DiscoveryQuery) ([]resource.Discovery, error) { + rc.logger.Warn( + "DiscoverComponents is deprecated and will be removed on March 10th 2025. Please use the Discovery Service instead.") pbQueries := make([]*pb.DiscoveryQuery, 0, len(qs)) for _, q := range qs { extra, err := structpb.NewStruct(q.Extra) @@ -917,6 +922,32 @@ func (rc *RobotClient) DiscoverComponents(ctx context.Context, qs []resource.Dis return discoveries, nil } +// GetModelsFromModules returns the available models from the configured modules on a given machine. +func (rc *RobotClient) GetModelsFromModules(ctx context.Context) ([]resource.ModuleModelDiscovery, error) { + resp, err := rc.client.GetModelsFromModules(ctx, &pb.GetModelsFromModulesRequest{}) + if err != nil { + return nil, err + } + protoModels := resp.GetModels() + models := []resource.ModuleModelDiscovery{} + for _, protoModel := range protoModels { + modelTriplet, err := resource.NewModelFromString(protoModel.Model) + if err != nil { + return nil, err + } + api, err := resource.NewAPIFromString(protoModel.Api) + if err != nil { + return nil, err + } + model := resource.ModuleModelDiscovery{ + ModuleName: protoModel.ModuleName, Model: modelTriplet, API: api, + FromLocalModule: protoModel.FromLocalModule, + } + models = append(models, model) + } + return models, nil +} + // FrameSystemConfig returns the configuration of the frame system of a given machine. // // frameSystem, err := machine.FrameSystemConfig(context.Background(), nil) diff --git a/robot/client/client_test.go b/robot/client/client_test.go index 5258dcb2e7c..9e2e882cca9 100644 --- a/robot/client/client_test.go +++ b/robot/client/client_test.go @@ -1320,6 +1320,7 @@ func TestClientDiscovery(t *testing.T) { injectRobot.MachineStatusFunc = func(_ context.Context) (robot.MachineStatus, error) { return robot.MachineStatus{State: robot.StateRunning}, nil } + injectRobot.LoggerFunc = func() logging.Logger { return logging.NewTestLogger(t) } q := resource.DiscoveryQuery{ API: movementsensor.Named("foo").API, Model: resource.DefaultModelFamily.WithModel("bar"), @@ -1354,6 +1355,60 @@ func TestClientDiscovery(t *testing.T) { test.That(t, err, test.ShouldBeNil) } +func TestClientGetModelsFromModules(t *testing.T) { + injectRobot := &inject.Robot{} + injectRobot.ResourceRPCAPIsFunc = func() []resource.RPCAPI { return nil } + injectRobot.ResourceNamesFunc = func() []resource.Name { + return finalResources + } + injectRobot.MachineStatusFunc = func(_ context.Context) (robot.MachineStatus, error) { + return robot.MachineStatus{State: robot.StateRunning}, nil + } + expectedModels := []resource.ModuleModelDiscovery{ + { + ModuleName: "simple-module", + API: resource.NewAPI("rdk", "component", "generic"), + Model: resource.NewModel("acme", "demo", "mycounter"), + FromLocalModule: false, + }, + { + ModuleName: "simple-module2", + API: resource.NewAPI("rdk", "component", "generic"), + Model: resource.NewModel("acme", "demo", "mycounter"), + FromLocalModule: true, + }, + } + injectRobot.GetModelsFromModulesFunc = func(context.Context) ([]resource.ModuleModelDiscovery, error) { + return expectedModels, nil + } + + gServer := grpc.NewServer() + pb.RegisterRobotServiceServer(gServer, server.New(injectRobot)) + listener, err := net.Listen("tcp", "localhost:0") + test.That(t, err, test.ShouldBeNil) + logger := logging.NewTestLogger(t) + + go gServer.Serve(listener) + defer gServer.Stop() + + client, err := New(context.Background(), listener.Addr().String(), logger) + test.That(t, err, test.ShouldBeNil) + + resp, err := client.GetModelsFromModules(context.Background()) + test.That(t, err, test.ShouldBeNil) + test.That(t, len(resp), test.ShouldEqual, 2) + test.That(t, resp, test.ShouldResemble, expectedModels) + for index, model := range resp { + test.That(t, model.ModuleName, test.ShouldEqual, expectedModels[index].ModuleName) + test.That(t, model.Model, test.ShouldResemble, expectedModels[index].Model) + test.That(t, model.API, test.ShouldResemble, expectedModels[index].API) + test.That(t, model.FromLocalModule, test.ShouldEqual, expectedModels[index].FromLocalModule) + } + + err = client.Close(context.Background()) + test.That(t, err, test.ShouldBeNil) +} + func ensurePartsAreEqual(part, otherPart *referenceframe.FrameSystemPart) error { if part.FrameConfig.Name() != otherPart.FrameConfig.Name() { return fmt.Errorf("part had name %s while other part had name %s", part.FrameConfig.Name(), otherPart.FrameConfig.Name()) diff --git a/robot/impl/discovery_test.go b/robot/impl/discovery_test.go index 31c68be3044..f959b7104db 100644 --- a/robot/impl/discovery_test.go +++ b/robot/impl/discovery_test.go @@ -8,10 +8,19 @@ import ( modulepb "go.viam.com/api/module/v1" "go.viam.com/test" + "go.viam.com/rdk/components/base" + "go.viam.com/rdk/components/generic" "go.viam.com/rdk/config" + "go.viam.com/rdk/examples/customresources/apis/gizmoapi" + "go.viam.com/rdk/examples/customresources/apis/summationapi" + "go.viam.com/rdk/examples/customresources/models/mybase" + "go.viam.com/rdk/examples/customresources/models/mygizmo" + "go.viam.com/rdk/examples/customresources/models/mynavigation" + "go.viam.com/rdk/examples/customresources/models/mysum" "go.viam.com/rdk/logging" "go.viam.com/rdk/resource" "go.viam.com/rdk/robot" + "go.viam.com/rdk/services/navigation" rtestutils "go.viam.com/rdk/testutils" ) @@ -168,3 +177,66 @@ func TestDiscovery(t *testing.T) { test.That(t, len(complexHandles.Handlers), test.ShouldBeGreaterThan, 1) }) } + +func TestGetModelsFromModules(t *testing.T) { + t.Run("no modules configured", func(t *testing.T) { + r := setupLocalRobotWithFakeConfig(t) + models, err := r.GetModelsFromModules(context.Background()) + test.That(t, err, test.ShouldBeNil) + test.That(t, models, test.ShouldBeEmpty) + }) + t.Run("local and registry modules are configured", func(t *testing.T) { + r := setupLocalRobotWithFakeConfig(t) + ctx := context.Background() + + // add modules + complexPath := rtestutils.BuildTempModule(t, "examples/customresources/demos/complexmodule") + simplePath := rtestutils.BuildTempModule(t, "examples/customresources/demos/simplemodule") + cfg := &config.Config{ + Modules: []config.Module{ + { + Name: "simple", + ExePath: simplePath, + Type: config.ModuleTypeRegistry, + }, + { + Name: "complex", + ExePath: complexPath, + Type: config.ModuleTypeLocal, + }, + }, + } + r.Reconfigure(ctx, cfg) + models, err := r.GetModelsFromModules(context.Background()) + test.That(t, err, test.ShouldBeNil) + test.That(t, models, test.ShouldHaveLength, 5) + + for _, model := range models { + switch model.Model { + case mygizmo.Model: + test.That(t, model.FromLocalModule, test.ShouldEqual, true) + test.That(t, model.ModuleName, test.ShouldEqual, "complex") + test.That(t, model.API, test.ShouldResemble, gizmoapi.API) + case mysum.Model: + test.That(t, model.FromLocalModule, test.ShouldEqual, true) + test.That(t, model.ModuleName, test.ShouldEqual, "complex") + test.That(t, model.API, test.ShouldResemble, summationapi.API) + case mybase.Model: + test.That(t, model.FromLocalModule, test.ShouldEqual, true) + test.That(t, model.ModuleName, test.ShouldEqual, "complex") + test.That(t, model.API, test.ShouldResemble, base.API) + case mynavigation.Model: + test.That(t, model.FromLocalModule, test.ShouldEqual, true) + test.That(t, model.ModuleName, test.ShouldEqual, "complex") + test.That(t, model.API, test.ShouldResemble, navigation.API) + case resource.NewModel("acme", "demo", "mycounter"): + test.That(t, model.FromLocalModule, test.ShouldEqual, false) + test.That(t, model.ModuleName, test.ShouldEqual, "simple") + test.That(t, model.API, test.ShouldResemble, generic.API) + default: + t.Fail() + t.Logf("test GetModelsFromModules failure: unrecoginzed model %v", model.Model) + } + } + }) +} diff --git a/robot/impl/local_robot.go b/robot/impl/local_robot.go index 92269b31450..858544c542e 100644 --- a/robot/impl/local_robot.go +++ b/robot/impl/local_robot.go @@ -1077,6 +1077,10 @@ func (r *localRobot) discoverRobotInternals(query resource.DiscoveryQuery) (inte } } +func (r *localRobot) GetModelsFromModules(ctx context.Context) ([]resource.ModuleModelDiscovery, error) { + return r.manager.moduleManager.AllModels(), nil +} + func dialRobotClient( ctx context.Context, config config.Remote, diff --git a/robot/impl/resource_manager_test.go b/robot/impl/resource_manager_test.go index 6da6b4dc75d..771783a1c41 100644 --- a/robot/impl/resource_manager_test.go +++ b/robot/impl/resource_manager_test.go @@ -1886,6 +1886,15 @@ func (rr *dummyRobot) DiscoverComponents(ctx context.Context, qs []resource.Disc return rr.robot.DiscoverComponents(ctx, qs) } +func (rr *dummyRobot) GetModelsFromModules(ctx context.Context) ([]resource.ModuleModelDiscovery, error) { + rr.mu.Lock() + defer rr.mu.Unlock() + if rr.offline { + return nil, errors.New("offline") + } + return rr.robot.GetModelsFromModules(ctx) +} + func (rr *dummyRobot) RemoteNames() []string { return nil } diff --git a/robot/robot.go b/robot/robot.go index 652ff32abec..fc9e2f94b86 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -88,6 +88,10 @@ type Robot interface { // Only implemented for webcam cameras in builtin components. DiscoverComponents(ctx context.Context, qs []resource.DiscoveryQuery) ([]resource.Discovery, error) + // GetModelsFromModules returns a list of models supported by the configured modules, + // and specifies whether the models are from a local or registry module. + GetModelsFromModules(ctx context.Context) ([]resource.ModuleModelDiscovery, error) + // RemoteByName returns a remote robot by name. RemoteByName(name string) (Robot, bool) diff --git a/robot/server/server.go b/robot/server/server.go index cdcd8a7988f..fc391036e3c 100644 --- a/robot/server/server.go +++ b/robot/server/server.go @@ -164,9 +164,14 @@ func (s *Server) ResourceRPCSubtypes(ctx context.Context, _ *pb.ResourceRPCSubty return &pb.ResourceRPCSubtypesResponse{ResourceRpcSubtypes: protoTypes}, nil } +// DiscoverComponents is DEPRECATED!!! Please use the Discovery Service instead. // DiscoverComponents takes a list of discovery queries and returns corresponding // component configurations. +// +//nolint:deprecated,staticcheck func (s *Server) DiscoverComponents(ctx context.Context, req *pb.DiscoverComponentsRequest) (*pb.DiscoverComponentsResponse, error) { + s.robot.Logger().CWarn(ctx, + "DiscoverComponents is deprecated and will be removed on March 10th 2025. Please use the Discovery Service instead.") // nonTriplet indicates older syntax for type and model E.g. "camera" instead of "rdk:component:camera" // TODO(PRODUCT-344): remove triplet checking here after complete var nonTriplet bool @@ -223,6 +228,19 @@ func (s *Server) DiscoverComponents(ctx context.Context, req *pb.DiscoverCompone return &pb.DiscoverComponentsResponse{Discovery: pbDiscoveries}, nil } +// GetModelsFromModules returns all models from the currently managed modules. +func (s *Server) GetModelsFromModules(ctx context.Context, req *pb.GetModelsFromModulesRequest) (*pb.GetModelsFromModulesResponse, error) { + models, err := s.robot.GetModelsFromModules(ctx) + if err != nil { + return nil, err + } + resp := pb.GetModelsFromModulesResponse{} + for _, mm := range models { + resp.Models = append(resp.Models, mm.ToProto()) + } + return &resp, nil +} + // FrameSystemConfig returns the info of each individual part that makes up the frame system. func (s *Server) FrameSystemConfig(ctx context.Context, req *pb.FrameSystemConfigRequest) (*pb.FrameSystemConfigResponse, error) { fsCfg, err := s.robot.FrameSystemConfig(ctx) diff --git a/robot/server/server_test.go b/robot/server/server_test.go index 40425a8976b..dae6edd0f33 100644 --- a/robot/server/server_test.go +++ b/robot/server/server_test.go @@ -401,10 +401,12 @@ func TestServer(t *testing.T) { test.That(t, resp.GetMachinePartId(), test.ShouldEqual, "the-robot-part") }) + //nolint:deprecated,staticcheck t.Run("Discovery", func(t *testing.T) { injectRobot := &inject.Robot{} injectRobot.ResourceRPCAPIsFunc = func() []resource.RPCAPI { return nil } injectRobot.ResourceNamesFunc = func() []resource.Name { return []resource.Name{} } + injectRobot.LoggerFunc = func() logging.Logger { return logging.NewTestLogger(t) } server := server.New(injectRobot) q := resource.DiscoveryQuery{arm.Named("arm").API, resource.DefaultModelFamily.WithModel("some-arm"), nil} @@ -446,6 +448,49 @@ func TestServer(t *testing.T) { }) }) + t.Run("GetModelsFromModules", func(t *testing.T) { + injectRobot := &inject.Robot{} + injectRobot.ResourceRPCAPIsFunc = func() []resource.RPCAPI { return nil } + injectRobot.ResourceNamesFunc = func() []resource.Name { return []resource.Name{} } + server := server.New(injectRobot) + + expectedModels := []resource.ModuleModelDiscovery{ + { + ModuleName: "simple-module", + API: resource.NewAPI("rdk", "component", "generic"), + Model: resource.NewModel("acme", "demo", "mycounter"), + FromLocalModule: false, + }, + { + ModuleName: "simple-module2", + API: resource.NewAPI("rdk", "component", "generic"), + Model: resource.NewModel("acme", "demo", "mycounter"), + FromLocalModule: true, + }, + } + injectRobot.GetModelsFromModulesFunc = func(context.Context) ([]resource.ModuleModelDiscovery, error) { + return expectedModels, nil + } + expectedProto := []*pb.ModuleModel{expectedModels[0].ToProto(), expectedModels[1].ToProto()} + + req := &pb.GetModelsFromModulesRequest{} + resp, err := server.GetModelsFromModules(context.Background(), req) + test.That(t, err, test.ShouldBeNil) + protoModels := resp.GetModels() + test.That(t, len(protoModels), test.ShouldEqual, 2) + test.That(t, protoModels, test.ShouldResemble, expectedProto) + for index, protoModel := range protoModels { + test.That(t, protoModel.ModuleName, test.ShouldEqual, expectedProto[index].ModuleName) + test.That(t, protoModel.ModuleName, test.ShouldEqual, expectedModels[index].ModuleName) + test.That(t, protoModel.Model, test.ShouldEqual, expectedProto[index].Model) + test.That(t, protoModel.Model, test.ShouldEqual, expectedModels[index].Model.String()) + test.That(t, protoModel.Api, test.ShouldEqual, expectedProto[index].Api) + test.That(t, protoModel.Api, test.ShouldEqual, expectedModels[index].API.String()) + test.That(t, protoModel.FromLocalModule, test.ShouldEqual, expectedProto[index].FromLocalModule) + test.That(t, protoModel.FromLocalModule, test.ShouldEqual, expectedModels[index].FromLocalModule) + } + }) + t.Run("ResourceRPCSubtypes", func(t *testing.T) { injectRobot := &inject.Robot{} injectRobot.ResourceRPCAPIsFunc = func() []resource.RPCAPI { return nil } diff --git a/robot/web/stream/server.go b/robot/web/stream/server.go index dd6646dc245..f48429b2f7e 100644 --- a/robot/web/stream/server.go +++ b/robot/web/stream/server.go @@ -658,6 +658,37 @@ func (server *Server) refreshVideoSources() { } existing, ok := server.videoSources[cam.Name().SDPTrackName()] if ok { + // Check stream state for the camera to see if it is in resized mode. + // If it is in resized mode, we want to apply the resize transformation to the + // video source before swapping it. + streamState, ok := server.nameToStreamState[cam.Name().SDPTrackName()] + if ok && streamState.IsResized() { + server.logger.Debugf("stream %q is resized attempting to reapply resize transformation", cam.Name().SDPTrackName()) + mediaProps, err := existing.MediaProperties(server.closedCtx) + if err != nil { + server.logger.Errorf("error getting media properties from resize source: %v", err) + } else { + // resizeVideoSource should always have a width and height set. + height, width := mediaProps.Height, mediaProps.Width + if height != 0 && width != 0 { + server.logger.Debugf( + "resizing video source to width %d and height %d", + width, height, + ) + resizer := gostream.NewResizeVideoSource(cam, width, height) + existing.Swap(resizer) + continue + } + } + // If we can't get the media properties or the width and height are 0, we fall back to + // the original source and need to notify the stream state that the source is no longer + // resized. + server.logger.Warnf("falling back to original source for stream %q", cam.Name().SDPTrackName()) + err = streamState.Reset() + if err != nil { + server.logger.Errorf("error resetting stream %q: %v", cam.Name().SDPTrackName(), err) + } + } existing.Swap(cam) continue } diff --git a/robot/web/stream/state/state.go b/robot/web/stream/state/state.go index b885907f12a..c86ff485427 100644 --- a/robot/web/stream/state/state.go +++ b/robot/web/stream/state/state.go @@ -409,3 +409,8 @@ func (state *StreamState) unsubscribeH264Passthrough(ctx context.Context, id rtp return nil } + +// IsResized returns whether the stream is in a resized state. +func (state *StreamState) IsResized() bool { + return state.isResized +} diff --git a/robot/web/web.go b/robot/web/web.go index dbf5ae4a713..48b5f28024d 100644 --- a/robot/web/web.go +++ b/robot/web/web.go @@ -603,7 +603,7 @@ func (svc *webService) initAuthHandlers(listenerTCPAddr *net.TCPAddr, options we for _, handler := range options.Auth.Handlers { switch handler.Type { case rpc.CredentialsTypeAPIKey: - apiKeys := parseAPIKeys(handler) + apiKeys := config.ParseAPIKeys(handler) if len(apiKeys) == 0 { return nil, errors.Errorf("%q handler requires non-empty API keys", handler.Type) @@ -640,18 +640,6 @@ func (svc *webService) initAuthHandlers(listenerTCPAddr *net.TCPAddr, options we return rpcOpts, nil } -func parseAPIKeys(handler config.AuthHandlerConfig) map[string]string { - apiKeys := map[string]string{} - for k := range handler.Config { - // if it is not a legacy api key indicated by "key(s)" key - // current api keys will follow format { [keyId]: [key] } - if k != "keys" && k != "key" { - apiKeys[k] = handler.Config.String(k) - } - } - return apiKeys -} - // Register every API resource grpc service here. func (svc *webService) initAPIResourceCollections(ctx context.Context, mod bool) error { // TODO (RSDK-144): only register necessary services diff --git a/testutils/inject/enduser_service_client.go b/testutils/inject/enduser_service_client.go deleted file mode 100644 index 3f4679f0615..00000000000 --- a/testutils/inject/enduser_service_client.go +++ /dev/null @@ -1,52 +0,0 @@ -package inject - -import ( - "context" - - apppb "go.viam.com/api/app/v1" - "google.golang.org/grpc" -) - -// EndUserServiceClient represents a fake instance of an end user service client. -type EndUserServiceClient struct { - apppb.EndUserServiceClient - RegisterAuthApplicationFunc func(ctx context.Context, in *apppb.RegisterAuthApplicationRequest, - opts ...grpc.CallOption, - ) (*apppb.RegisterAuthApplicationResponse, error) - UpdateAuthApplicationFunc func(ctx context.Context, in *apppb.UpdateAuthApplicationRequest, - opts ...grpc.CallOption, - ) (*apppb.UpdateAuthApplicationResponse, error) - GetAuthApplicationFunc func(ctx context.Context, in *apppb.GetAuthApplicationRequest, - opts ...grpc.CallOption, - ) (*apppb.GetAuthApplicationResponse, error) -} - -// RegisterAuthApplication calls the injected RegisterAuthApplicationFunc or the real version. -func (eusc *EndUserServiceClient) RegisterAuthApplication(ctx context.Context, in *apppb.RegisterAuthApplicationRequest, - opts ...grpc.CallOption, -) (*apppb.RegisterAuthApplicationResponse, error) { - if eusc.RegisterAuthApplicationFunc == nil { - return eusc.EndUserServiceClient.RegisterAuthApplication(ctx, in, opts...) - } - return eusc.RegisterAuthApplicationFunc(ctx, in, opts...) -} - -// UpdateAuthApplication calls the injected UpdateAuthApplicationFunc or the real version. -func (eusc *EndUserServiceClient) UpdateAuthApplication(ctx context.Context, in *apppb.UpdateAuthApplicationRequest, - opts ...grpc.CallOption, -) (*apppb.UpdateAuthApplicationResponse, error) { - if eusc.UpdateAuthApplicationFunc == nil { - return eusc.EndUserServiceClient.UpdateAuthApplication(ctx, in, opts...) - } - return eusc.UpdateAuthApplicationFunc(ctx, in, opts...) -} - -// GetAuthApplication calls the injected GetAuthApplication or the real version. -func (eusc *EndUserServiceClient) GetAuthApplication(ctx context.Context, in *apppb.GetAuthApplicationRequest, - opts ...grpc.CallOption, -) (*apppb.GetAuthApplicationResponse, error) { - if eusc.GetAuthApplicationFunc == nil { - return eusc.EndUserServiceClient.GetAuthApplication(ctx, in, opts...) - } - return eusc.GetAuthApplicationFunc(ctx, in, opts...) -} diff --git a/testutils/inject/robot.go b/testutils/inject/robot.go index 570cc8d1c2b..8b42d50c45b 100644 --- a/testutils/inject/robot.go +++ b/testutils/inject/robot.go @@ -26,20 +26,21 @@ import ( // Robot is an injected robot. type Robot struct { robot.LocalRobot - Mu sync.RWMutex // Ugly, has to be manually locked if a test means to swap funcs on an in-use robot. - DiscoverComponentsFunc func(ctx context.Context, keys []resource.DiscoveryQuery) ([]resource.Discovery, error) - RemoteByNameFunc func(name string) (robot.Robot, bool) - ResourceByNameFunc func(name resource.Name) (resource.Resource, error) - RemoteNamesFunc func() []string - ResourceNamesFunc func() []resource.Name - ResourceRPCAPIsFunc func() []resource.RPCAPI - ProcessManagerFunc func() pexec.ProcessManager - ConfigFunc func() *config.Config - LoggerFunc func() logging.Logger - CloseFunc func(ctx context.Context) error - StopAllFunc func(ctx context.Context, extra map[resource.Name]map[string]interface{}) error - FrameSystemConfigFunc func(ctx context.Context) (*framesystem.Config, error) - TransformPoseFunc func( + Mu sync.RWMutex // Ugly, has to be manually locked if a test means to swap funcs on an in-use robot. + DiscoverComponentsFunc func(ctx context.Context, keys []resource.DiscoveryQuery) ([]resource.Discovery, error) + GetModelsFromModulesFunc func(ctx context.Context) ([]resource.ModuleModelDiscovery, error) + RemoteByNameFunc func(name string) (robot.Robot, bool) + ResourceByNameFunc func(name resource.Name) (resource.Resource, error) + RemoteNamesFunc func() []string + ResourceNamesFunc func() []resource.Name + ResourceRPCAPIsFunc func() []resource.RPCAPI + ProcessManagerFunc func() pexec.ProcessManager + ConfigFunc func() *config.Config + LoggerFunc func() logging.Logger + CloseFunc func(ctx context.Context) error + StopAllFunc func(ctx context.Context, extra map[resource.Name]map[string]interface{}) error + FrameSystemConfigFunc func(ctx context.Context) (*framesystem.Config, error) + TransformPoseFunc func( ctx context.Context, pose *referenceframe.PoseInFrame, dst string, @@ -228,6 +229,16 @@ func (r *Robot) DiscoverComponents(ctx context.Context, keys []resource.Discover return r.DiscoverComponentsFunc(ctx, keys) } +// GetModelsFromModules calls the injected GetModelsFromModules or the real one. +func (r *Robot) GetModelsFromModules(ctx context.Context) ([]resource.ModuleModelDiscovery, error) { + r.Mu.RLock() + defer r.Mu.RUnlock() + if r.GetModelsFromModulesFunc == nil { + return r.LocalRobot.GetModelsFromModules(ctx) + } + return r.GetModelsFromModulesFunc(ctx) +} + // FrameSystemConfig calls the injected FrameSystemConfig or the real version. func (r *Robot) FrameSystemConfig(ctx context.Context) (*framesystem.Config, error) { r.Mu.RLock() diff --git a/utils/env.go b/utils/env.go index 0d872ad9287..005bae7a755 100644 --- a/utils/env.go +++ b/utils/env.go @@ -5,6 +5,7 @@ import ( "regexp" "runtime" "slices" + "strings" "time" "go.viam.com/rdk/logging" @@ -30,6 +31,31 @@ const ( // AndroidFilesDir is hardcoded because golang inits before our android code can override HOME var. AndroidFilesDir = "/data/user/0/com.viam.rdk.fgservice/cache" + + // ViamEnvVarPrefix is the prefix for all Viam-related environment variables. + ViamEnvVarPrefix = "VIAM_" + + // APIKeyEnvVar is the environment variable which contains an API key that can be used for + // communications to app.viam.com. + //nolint:gosec + APIKeyEnvVar = "VIAM_API_KEY" + + // APIKeyIDEnvVar is the environment variable which contains an API key ID that can be used for + // communications to app.viam.com. + //nolint:gosec + APIKeyIDEnvVar = "VIAM_API_KEY_ID" + + // MachineIDEnvVar is the environment variable that contains the machine ID of the machine. + MachineIDEnvVar = "VIAM_MACHINE_ID" + + // MachinePartIDEnvVar is the environment variable that contains the machine part ID of the machine. + MachinePartIDEnvVar = "VIAM_MACHINE_PART_ID" + + // LocationIDEnvVar is the environment variable that contains the location ID of the machine. + LocationIDEnvVar = "VIAM_LOCATION_ID" + + // PrimaryOrgIDEnvVar is the environment variable that contains the primary org ID of the machine. + PrimaryOrgIDEnvVar = "VIAM_PRIMARY_ORG_ID" ) // EnvTrueValues contains strings that we interpret as boolean true in env vars. @@ -95,3 +121,24 @@ func ViamTCPSockets() bool { return runtime.GOOS == "windows" || slices.Contains(EnvTrueValues, os.Getenv("VIAM_TCP_SOCKETS")) } + +// LogViamEnvVariables logs the list of viam environment variables in [os.Environ] along with the env passed in. +func LogViamEnvVariables(msg string, envVars map[string]string, logger logging.Logger) { + var env []string + for _, v := range os.Environ() { + if !strings.HasPrefix(v, ViamEnvVarPrefix) { + continue + } + env = append(env, v) + } + for key, val := range envVars { + // mask the secret + if key == APIKeyEnvVar { + val = "XXXXXXXXXX" + } + env = append(env, key+"="+val) + } + if len(env) != 0 { + logger.Infow(msg, "environment", env) + } +}