diff --git a/cli/app.go b/cli/app.go index 1946536ef65..8302b348e54 100644 --- a/cli/app.go +++ b/cli/app.go @@ -53,6 +53,7 @@ const ( generalFlagPart = "part" generalFlagPartName = "part-name" generalFlagPartID = "part-id" + generalFlagID = "id" generalFlagName = "name" generalFlagMethod = "method" generalFlagDestination = "destination" @@ -74,7 +75,6 @@ const ( moduleFlagLocal = "local" moduleFlagHomeDir = "home" moduleCreateLocalOnly = "local-only" - moduleFlagID = "id" moduleFlagIsPublic = "public" moduleFlagResourceType = "resource-type" moduleFlagModelName = "model-name" @@ -117,6 +117,10 @@ const ( dataFlagFilterTags = "filter-tags" dataFlagTimeout = "timeout" + datapipelineFlagSchedule = "schedule" + datapipelineFlagMQL = "mql" + datapipelineFlagMQLFile = "mql-path" + packageFlagFramework = "model-framework" oauthAppFlagClientID = "client-id" @@ -1508,6 +1512,150 @@ var app = &cli.App{ }, }, }, + { + Name: "datapipelines", + Usage: "manage and track data pipelines", + UsageText: createUsageText("datapipelines", nil, false, true), + HideHelpCommand: true, + Subcommands: []*cli.Command{ + { + Name: "list", + Usage: "list data pipelines for an organization ID", + UsageText: createUsageText("datapipelines list", + []string{generalFlagOrgID}, true, false), + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: generalFlagOrgID, + Usage: "organization ID for which data pipelines will be listed", + Required: true, + }, + }, + Action: createCommandWithT[datapipelineListArgs](DatapipelineListAction), + }, + { + Name: "describe", + Usage: "describe a data pipeline and its status", + UsageText: createUsageText("datapipelines describe", []string{generalFlagID}, true, false), + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: generalFlagID, + Usage: "ID of the data pipeline to describe", + Required: true, + }, + }, + Action: createCommandWithT[datapipelineDescribeArgs](DatapipelineDescribeAction), + }, + { + Name: "create", + Usage: "create a new data pipeline", + UsageText: createUsageText("datapipelines create", + []string{generalFlagOrgID, generalFlagName, datapipelineFlagSchedule}, false, false, + fmt.Sprintf("[--%s=<%s> | --%s=<%s>]", + datapipelineFlagMQL, datapipelineFlagMQL, + datapipelineFlagMQLFile, datapipelineFlagMQLFile), + ), + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: generalFlagOrgID, + Usage: "organization ID for which data pipeline will be created", + Required: true, + }, + &cli.StringFlag{ + Name: generalFlagName, + Usage: "name of the new data pipeline", + Required: true, + }, + &cli.StringFlag{ + Name: datapipelineFlagSchedule, + Usage: "schedule of the new data pipeline (cron expression)", + Required: true, + }, + &cli.StringFlag{ + Name: datapipelineFlagMQL, + Usage: "MQL query for the new data pipeline", + }, + &cli.StringFlag{ + Name: datapipelineFlagMQLFile, + Usage: "path to JSON file containing MQL query for the new data pipeline", + }, + }, + Action: createCommandWithT[datapipelineCreateArgs](DatapipelineCreateAction), + }, + { + Name: "update", + Usage: "update a data pipeline", + UsageText: createUsageText("datapipelines update", + []string{generalFlagID, generalFlagName, datapipelineFlagSchedule}, false, false, + fmt.Sprintf("[--%s=<%s> | --%s=<%s>]", + datapipelineFlagMQL, datapipelineFlagMQL, + datapipelineFlagMQLFile, datapipelineFlagMQLFile), + ), + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: generalFlagID, + Usage: "ID of the data pipeline to update", + Required: true, + }, + &cli.StringFlag{ + Name: generalFlagName, + Usage: "name of the data pipeline to update", + }, + &cli.StringFlag{ + Name: datapipelineFlagSchedule, + Usage: "schedule of the data pipeline to update (cron expression)", + }, + &cli.StringFlag{ + Name: datapipelineFlagMQL, + Usage: "MQL query for the data pipeline to update", + }, + &cli.StringFlag{ + Name: datapipelineFlagMQLFile, + Usage: "path to JSON file containing MQL query for the data pipeline to update", + }, + }, + Action: createCommandWithT[datapipelineUpdateArgs](DatapipelineUpdateAction), + }, + { + Name: "delete", + Usage: "delete a data pipeline", + UsageText: createUsageText("datapipelines delete", []string{generalFlagID}, true, false), + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: generalFlagID, + Usage: "ID of the data pipeline to delete", + Required: true, + }, + }, + Action: createCommandWithT[datapipelineDeleteArgs](DatapipelineDeleteAction), + }, + { + Name: "enable", + Usage: "enable a data pipeline", + UsageText: createUsageText("datapipelines enable", []string{generalFlagID}, true, false), + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: generalFlagID, + Usage: "ID of the data pipeline to enable", + Required: true, + }, + }, + Action: createCommandWithT[datapipelineEnableArgs](DatapipelineEnableAction), + }, + { + Name: "disable", + Usage: "disable a data pipeline", + UsageText: createUsageText("datapipelines disable", []string{generalFlagID}, true, false), + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: generalFlagID, + Usage: "ID of the data pipeline to disable", + Required: true, + }, + }, + Action: createCommandWithT[datapipelineDisableArgs](DatapipelineDisableAction), + }, + }, + }, { Name: "train", Usage: "train on data", @@ -2550,7 +2698,7 @@ Example: DefaultText: "all", }, &cli.StringFlag{ - Name: moduleFlagID, + Name: generalFlagID, Usage: "restrict output to just return builds that match this id", }, }, @@ -2560,10 +2708,10 @@ Example: Name: "logs", Aliases: []string{"log"}, Usage: "get the logs from one of your cloud builds", - UsageText: createUsageText("module build logs", []string{moduleFlagID}, true, false), + UsageText: createUsageText("module build logs", []string{generalFlagID}, true, false), Flags: []cli.Flag{ &cli.StringFlag{ - Name: moduleFlagID, + Name: generalFlagID, Usage: "build that you want to get the logs for", Required: true, }, @@ -2645,7 +2793,7 @@ This won't work unless you have an existing installation of our GitHub app on yo Usage: "name of module to restart. pass at most one of --name, --id", }, &cli.StringFlag{ - Name: moduleFlagID, + Name: generalFlagID, Usage: "ID of module to restart, for example viam:wifi-sensor. pass at most one of --name, --id", }, &cli.BoolFlag{ @@ -2679,7 +2827,7 @@ This won't work unless you have an existing installation of our GitHub app on yo Value: ".", }, &cli.StringFlag{ - Name: moduleFlagID, + Name: generalFlagID, Usage: "module ID as org-id:name or namespace:name", DefaultText: "will try to read from meta.json", }, diff --git a/cli/auth.go b/cli/auth.go index 8bc31f38e33..01dfa282798 100644 --- a/cli/auth.go +++ b/cli/auth.go @@ -20,6 +20,7 @@ import ( "go.uber.org/multierr" buildpb "go.viam.com/api/app/build/v1" datapb "go.viam.com/api/app/data/v1" + datapipelinespb "go.viam.com/api/app/datapipelines/v1" datasetpb "go.viam.com/api/app/dataset/v1" mlinferencepb "go.viam.com/api/app/mlinference/v1" mltrainingpb "go.viam.com/api/app/mltraining/v1" @@ -528,6 +529,7 @@ func (c *viamClient) ensureLoggedInInner() error { c.dataClient = datapb.NewDataServiceClient(conn) c.packageClient = packagepb.NewPackageServiceClient(conn) c.datasetClient = datasetpb.NewDatasetServiceClient(conn) + c.datapipelinesClient = datapipelinespb.NewDataPipelinesServiceClient(conn) c.mlTrainingClient = mltrainingpb.NewMLTrainingServiceClient(conn) c.mlInferenceClient = mlinferencepb.NewMLInferenceServiceClient(conn) c.buildClient = buildpb.NewBuildServiceClient(conn) diff --git a/cli/client.go b/cli/client.go index 5777538c5ef..2853aec18bd 100644 --- a/cli/client.go +++ b/cli/client.go @@ -31,6 +31,7 @@ import ( "go.uber.org/zap" buildpb "go.viam.com/api/app/build/v1" datapb "go.viam.com/api/app/data/v1" + datapipelinespb "go.viam.com/api/app/datapipelines/v1" datasetpb "go.viam.com/api/app/dataset/v1" mlinferencepb "go.viam.com/api/app/mlinference/v1" mltrainingpb "go.viam.com/api/app/mltraining/v1" @@ -79,17 +80,18 @@ var errNoShellService = errors.New("shell service is not enabled on this machine // viamClient wraps a cli.Context and provides all the CLI command functionality // needed to talk to the app and data services but not directly to robot parts. type viamClient struct { - c *cli.Context - conf *Config - client apppb.AppServiceClient - dataClient datapb.DataServiceClient - packageClient packagepb.PackageServiceClient - datasetClient datasetpb.DatasetServiceClient - mlTrainingClient mltrainingpb.MLTrainingServiceClient - mlInferenceClient mlinferencepb.MLInferenceServiceClient - buildClient buildpb.BuildServiceClient - baseURL *url.URL - authFlow *authFlow + c *cli.Context + conf *Config + client apppb.AppServiceClient + dataClient datapb.DataServiceClient + packageClient packagepb.PackageServiceClient + datasetClient datasetpb.DatasetServiceClient + datapipelinesClient datapipelinespb.DataPipelinesServiceClient + mlTrainingClient mltrainingpb.MLTrainingServiceClient + mlInferenceClient mlinferencepb.MLInferenceServiceClient + buildClient buildpb.BuildServiceClient + baseURL *url.URL + authFlow *authFlow selectedOrg *apppb.Organization selectedLoc *apppb.Location diff --git a/cli/datapipelines.go b/cli/datapipelines.go new file mode 100644 index 00000000000..95224c707e8 --- /dev/null +++ b/cli/datapipelines.go @@ -0,0 +1,322 @@ +package cli + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "time" + + "github.com/urfave/cli/v2" + "github.com/yosuke-furukawa/json5/encoding/json5" + "go.mongodb.org/mongo-driver/bson" + datapipelinespb "go.viam.com/api/app/datapipelines/v1" +) + +// pipelineRunStatusMap maps pipeline run statuses to human-readable strings. +var pipelineRunStatusMap = map[datapipelinespb.DataPipelineRunStatus]string{ + datapipelinespb.DataPipelineRunStatus_DATA_PIPELINE_RUN_STATUS_UNSPECIFIED: "Unknown", + datapipelinespb.DataPipelineRunStatus_DATA_PIPELINE_RUN_STATUS_SCHEDULED: "Scheduled", + datapipelinespb.DataPipelineRunStatus_DATA_PIPELINE_RUN_STATUS_STARTED: "Running", + datapipelinespb.DataPipelineRunStatus_DATA_PIPELINE_RUN_STATUS_COMPLETED: "Success", + datapipelinespb.DataPipelineRunStatus_DATA_PIPELINE_RUN_STATUS_FAILED: "Failed", +} + +type datapipelineListArgs struct { + OrgID string +} + +// DatapipelineListAction lists all data pipelines for an organization. +func DatapipelineListAction(c *cli.Context, args datapipelineListArgs) error { + client, err := newViamClient(c) + if err != nil { + return err + } + + resp, err := client.datapipelinesClient.ListDataPipelines(context.Background(), &datapipelinespb.ListDataPipelinesRequest{ + OrganizationId: args.OrgID, + }) + if err != nil { + return err + } + + for _, pipeline := range resp.GetDataPipelines() { + enabled := "Enabled" + if !pipeline.Enabled { + enabled = "Disabled" + } + printf(c.App.Writer, "\t%s (ID: %s) [%s]", pipeline.Name, pipeline.Id, enabled) + } + + return nil +} + +type datapipelineCreateArgs struct { + OrgID string + Name string + Schedule string + MQL string + MqlPath string +} + +// DatapipelineCreateAction creates a new data pipeline. +func DatapipelineCreateAction(c *cli.Context, args datapipelineCreateArgs) error { + client, err := newViamClient(c) + if err != nil { + return err + } + + mqlBinary, err := parseMQL(args.MQL, args.MqlPath) + if err != nil { + return err + } + + resp, err := client.datapipelinesClient.CreateDataPipeline(context.Background(), &datapipelinespb.CreateDataPipelineRequest{ + OrganizationId: args.OrgID, + Name: args.Name, + Schedule: args.Schedule, + MqlBinary: mqlBinary, + }) + if err != nil { + return fmt.Errorf("error creating data pipeline: %w", err) + } + + printf(c.App.Writer, "%s (ID: %s) created.", args.Name, resp.GetId()) + + return nil +} + +type datapipelineUpdateArgs struct { + ID string + Name string + Schedule string + MQL string + MqlPath string +} + +// DatapipelineUpdateAction updates an existing data pipeline. +func DatapipelineUpdateAction(c *cli.Context, args datapipelineUpdateArgs) error { + client, err := newViamClient(c) + if err != nil { + return err + } + + resp, err := client.datapipelinesClient.GetDataPipeline(context.Background(), &datapipelinespb.GetDataPipelineRequest{ + Id: args.ID, + }) + if err != nil { + return fmt.Errorf("error getting data pipeline: %w", err) + } + current := resp.GetDataPipeline() + + name := args.Name + if name == "" { + name = current.GetName() + } + + schedule := args.Schedule + if schedule == "" { + schedule = current.GetSchedule() + } + + mqlBinary := current.GetMqlBinary() + if args.MQL != "" || args.MqlPath != "" { + mqlBinary, err = parseMQL(args.MQL, args.MqlPath) + if err != nil { + return err + } + } + + _, err = client.datapipelinesClient.UpdateDataPipeline(context.Background(), &datapipelinespb.UpdateDataPipelineRequest{ + Id: args.ID, + Name: name, + Schedule: schedule, + MqlBinary: mqlBinary, + }) + if err != nil { + return fmt.Errorf("error updating data pipeline: %w", err) + } + + printf(c.App.Writer, "%s (id: %s) updated.", name, args.ID) + return nil +} + +type datapipelineDeleteArgs struct { + ID string +} + +// DatapipelineDeleteAction deletes a data pipeline. +func DatapipelineDeleteAction(c *cli.Context, args datapipelineDeleteArgs) error { + client, err := newViamClient(c) + if err != nil { + return err + } + + _, err = client.datapipelinesClient.DeleteDataPipeline(context.Background(), &datapipelinespb.DeleteDataPipelineRequest{ + Id: args.ID, + }) + if err != nil { + return fmt.Errorf("error deleting data pipeline: %w", err) + } + + printf(c.App.Writer, "data pipeline (id: %s) deleted.", args.ID) + return nil +} + +type datapipelineDescribeArgs struct { + ID string +} + +// DatapipelineDescribeAction describes a data pipeline and its status. +func DatapipelineDescribeAction(c *cli.Context, args datapipelineDescribeArgs) error { + client, err := newViamClient(c) + if err != nil { + return err + } + + resp, err := client.datapipelinesClient.GetDataPipeline(context.Background(), &datapipelinespb.GetDataPipelineRequest{ + Id: args.ID, + }) + if err != nil { + return fmt.Errorf("error getting data pipeline: %w", err) + } + pipeline := resp.GetDataPipeline() + + runsResp, err := client.datapipelinesClient.ListDataPipelineRuns(context.Background(), &datapipelinespb.ListDataPipelineRunsRequest{ + Id: args.ID, + PageSize: 1, + }) + if err != nil { + return fmt.Errorf("error getting list of pipeline runs: %w", err) + } + runs := runsResp.Runs + + mql, err := mqlJSON(pipeline.GetMqlBinary()) + if err != nil { + warningf(c.App.Writer, "error parsing MQL query: %s", err) + mql = "(error parsing MQL query)" + } + + printf(c.App.Writer, "ID: %s", pipeline.GetId()) + printf(c.App.Writer, "Name: %s", pipeline.GetName()) + printf(c.App.Writer, "Enabled: %t", pipeline.GetEnabled()) + printf(c.App.Writer, "Schedule: %s", pipeline.GetSchedule()) + printf(c.App.Writer, "MQL query: %s", mql) + + if len(runs) > 0 { + r := runs[0] + + printf(c.App.Writer, "Last run:") + printf(c.App.Writer, " Status: %s", pipelineRunStatusMap[r.GetStatus()]) + printf(c.App.Writer, " Started: %s", r.GetStartTime().AsTime().Format(time.RFC3339)) + printf(c.App.Writer, " Data range: [%s, %s]", + r.GetDataStartTime().AsTime().Format(time.RFC3339), + r.GetDataEndTime().AsTime().Format(time.RFC3339)) + if r.GetEndTime() != nil { + printf(c.App.Writer, " Ended: %s", r.GetEndTime().AsTime().Format(time.RFC3339)) + } + } else { + printf(c.App.Writer, "Has not run yet.") + } + + return nil +} + +type datapipelineEnableArgs struct { + ID string +} + +// DatapipelineEnableAction enables a data pipeline. +func DatapipelineEnableAction(c *cli.Context, args datapipelineEnableArgs) error { + client, err := newViamClient(c) + if err != nil { + return err + } + + _, err = client.datapipelinesClient.EnableDataPipeline(context.Background(), &datapipelinespb.EnableDataPipelineRequest{ + Id: args.ID, + }) + if err != nil { + return fmt.Errorf("error enabling data pipeline: %w", err) + } + + printf(c.App.Writer, "data pipeline (id: %s) enabled.", args.ID) + return nil +} + +type datapipelineDisableArgs struct { + ID string +} + +// DatapipelineDisableAction disables a data pipeline. +func DatapipelineDisableAction(c *cli.Context, args datapipelineDisableArgs) error { + client, err := newViamClient(c) + if err != nil { + return err + } + + _, err = client.datapipelinesClient.DisableDataPipeline(context.Background(), &datapipelinespb.DisableDataPipelineRequest{ + Id: args.ID, + }) + if err != nil { + return fmt.Errorf("error disabling data pipeline: %w", err) + } + + printf(c.App.Writer, "data pipeline (id: %s) disabled.", args.ID) + return nil +} + +func parseMQL(mql, mqlFile string) ([][]byte, error) { + if mqlFile != "" && mql != "" { + return nil, errors.New("data pipeline MQL and MQL file cannot both be provided") + } + + if mqlFile != "" { + //nolint:gosec // mqlFile is a user-provided path for reading MQL query files + content, err := os.ReadFile(mqlFile) + if err != nil { + return nil, fmt.Errorf("error reading MQL file: %w", err) + } + mql = string(content) + } + + if mql == "" { + return nil, errors.New("missing data pipeline MQL") + } + + // Parse the MQL stages JSON (using JSON5 for unquoted keys + comments). + var mqlArray []bson.M + if err := json5.Unmarshal([]byte(mql), &mqlArray); err != nil { + return nil, fmt.Errorf("unable to parse MQL argument: %w", err) + } + + var mqlBinary [][]byte + for _, stage := range mqlArray { + bytes, err := bson.Marshal(stage) + if err != nil { + return nil, fmt.Errorf("error converting MQL stage to BSON: %w", err) + } + mqlBinary = append(mqlBinary, bytes) + } + + return mqlBinary, nil +} + +func mqlJSON(mql [][]byte) (string, error) { + var stages []bson.M + for _, bsonBytes := range mql { + var stage bson.M + if err := bson.Unmarshal(bsonBytes, &stage); err != nil { + return "", fmt.Errorf("error unmarshaling BSON stage: %w", err) + } + stages = append(stages, stage) + } + + jsonBytes, err := json.MarshalIndent(stages, "", " ") + if err != nil { + return "", fmt.Errorf("error marshaling stages to JSON: %w", err) + } + + return string(jsonBytes), nil +} diff --git a/cli/datapipelines_test.go b/cli/datapipelines_test.go new file mode 100644 index 00000000000..2c9eb05f751 --- /dev/null +++ b/cli/datapipelines_test.go @@ -0,0 +1,147 @@ +package cli + +import ( + "encoding/json" + "os" + "testing" + + "go.mongodb.org/mongo-driver/bson" + "go.viam.com/test" +) + +var ( + mqlString = `[ + {"$match": { "component_name": "dragino" }}, + {"$group": { + "_id": "$part_id", + "count": { "$sum": 1 }, // a comment just for fun + "avgTemp": { "$avg": "$data.readings.TempC_SHT" }, + "avgHum": { "$avg": "$data.readings.Hum_SHT" } + }}, + ]` + mqlBSON = []bson.M{ + {"$match": bson.M{"component_name": "dragino"}}, + {"$group": bson.M{ + "_id": "$part_id", + "count": bson.M{"$sum": 1}, + "avgTemp": bson.M{"$avg": "$data.readings.TempC_SHT"}, + "avgHum": bson.M{"$avg": "$data.readings.Hum_SHT"}, + }}, + } +) + +func TestParseMQL(t *testing.T) { + testCases := map[string]struct { + mqlString string + mqlFile string + expectedError bool + expectedBSON []bson.M + }{ + "valid MQL string": { + mqlString: mqlString, + mqlFile: "", + expectedError: false, + expectedBSON: mqlBSON, + }, + "valid MQL file": { + mqlString: "", + mqlFile: createTempMQLFile(t, mqlString), + expectedError: false, + expectedBSON: mqlBSON, + }, + "empty string and file": { + mqlString: "", + mqlFile: "", + expectedError: true, + }, + "both string and file provided": { + mqlString: mqlString, + mqlFile: createTempMQLFile(t, mqlString), + expectedError: true, + }, + "invalid MQL JSON string": { + mqlString: `[{"$match": {"component_name": "dragino"`, // missing closing brackets + mqlFile: "", + expectedError: true, + }, + "invalid MQL JSON file": { + mqlString: "", + mqlFile: createTempMQLFile(t, `[{"$match": {"component_name": "dragino"`), // missing closing brackets + expectedError: true, + }, + "invalid MQL file path": { + mqlString: "", + mqlFile: "invalid/path/to/mql.json", + expectedError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + if tc.mqlFile != "" { + defer os.Remove(tc.mqlFile) + } + + mqlBytes, err := parseMQL(tc.mqlString, tc.mqlFile) + if tc.expectedError { + test.That(t, err, test.ShouldNotBeNil) + return + } + test.That(t, err, test.ShouldBeNil) + + for i, bsonBytes := range mqlBytes { + var bsonM bson.M + err = bson.Unmarshal(bsonBytes, &bsonM) + test.That(t, err, test.ShouldBeNil) + testBSONResemble(t, bsonM, tc.expectedBSON[i]) + } + }) + } +} + +// testBSONResemble compares two bson.M objects and asserts that they are equal. +func testBSONResemble(t *testing.T, actual, expected bson.M) { + t.Helper() + + actualJSON, err := json.Marshal(actual) + test.That(t, err, test.ShouldBeNil) + + expectedJSON, err := json.Marshal(expected) + test.That(t, err, test.ShouldBeNil) + + test.That(t, string(actualJSON), test.ShouldEqualJSON, string(expectedJSON)) +} + +func createTempMQLFile(t *testing.T, mql string) string { + t.Helper() + + f, err := os.CreateTemp("", "mql.json") + test.That(t, err, test.ShouldBeNil) + + _, err = f.WriteString(mql) + test.That(t, err, test.ShouldBeNil) + err = f.Close() + test.That(t, err, test.ShouldBeNil) + + return f.Name() +} + +func TestMQLJSON(t *testing.T) { + // expectedJSON is a vanilla JSON representation of the MQL string. + expectedJSON := `[{"$match":{"component_name":"dragino"}}, + {"$group":{"_id":"$part_id","count":{"$sum":1}, + "avgTemp":{"$avg":"$data.readings.TempC_SHT"}, + "avgHum":{"$avg":"$data.readings.Hum_SHT"}}}]` + + bsonBytes := make([][]byte, len(mqlBSON)) + var err error + for i, bsonDoc := range mqlBSON { + bsonBytes[i], err = bson.Marshal(bsonDoc) + if err != nil { + break + } + } + json, err := mqlJSON(bsonBytes) + test.That(t, err, test.ShouldBeNil) + test.That(t, json, test.ShouldEqualJSON, expectedJSON) +} diff --git a/cli/module_build.go b/cli/module_build.go index bb50386c353..9286cd5c900 100644 --- a/cli/module_build.go +++ b/cli/module_build.go @@ -660,7 +660,7 @@ func resolveTargetModule(c *cli.Context, manifest *moduleManifest) (*robot.Resta modID := args.ID // todo: use MutuallyExclusiveFlags for this when urfave/cli 3.x is stable if (len(modName) > 0) && (len(modID) > 0) { - return nil, fmt.Errorf("provide at most one of --%s and --%s", generalFlagName, moduleFlagID) + return nil, fmt.Errorf("provide at most one of --%s and --%s", generalFlagName, generalFlagID) } request := &robot.RestartModuleRequest{} //nolint:gocritic @@ -671,7 +671,7 @@ func resolveTargetModule(c *cli.Context, manifest *moduleManifest) (*robot.Resta } else if manifest != nil { request.ModuleID = manifest.ModuleID } else { - return nil, fmt.Errorf("if there is no meta.json, provide one of --%s or --%s", generalFlagName, moduleFlagID) + return nil, fmt.Errorf("if there is no meta.json, provide one of --%s or --%s", generalFlagName, generalFlagID) } return request, nil } diff --git a/doc/dependency_decisions.yml b/doc/dependency_decisions.yml index 2dfb3940093..495bf1e2918 100644 --- a/doc/dependency_decisions.yml +++ b/doc/dependency_decisions.yml @@ -217,3 +217,11 @@ :why: :versions: [] :when: 2023-01-30 21:31:27.476042000 Z +- - :license + - github.com/yosuke-furukawa/json5 + - Simplified BSD + - :who: sagie + :why: + :license_links: https://github.com/yosuke-furukawa/json5/blob/cf7bb3f354ffe5d5ad4c9b714895eab7e0498b5f/LICENSE.md + :versions: [] + :when: 2025-04-11 20:04:09.564107000 Z diff --git a/go.mod b/go.mod index 051a8a1ef42..6503c642a6a 100644 --- a/go.mod +++ b/go.mod @@ -77,7 +77,7 @@ require ( go.uber.org/atomic v1.11.0 go.uber.org/multierr v1.11.0 go.uber.org/zap v1.27.0 - go.viam.com/api v0.1.430 + go.viam.com/api v0.1.432 go.viam.com/test v1.2.4 go.viam.com/utils v0.1.141 goji.io v2.0.2+incompatible @@ -433,6 +433,7 @@ require ( github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 // indirect github.com/kylelemons/go-gypsy v1.0.0 // indirect github.com/pkg/errors v0.9.1 + github.com/yosuke-furukawa/json5 v0.1.1 github.com/ziutek/mymysql v1.5.4 // indirect golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e ) diff --git a/go.sum b/go.sum index b8f0829406e..4eb791e5759 100644 --- a/go.sum +++ b/go.sum @@ -1443,6 +1443,8 @@ github.com/yeya24/promlinter v0.3.0 h1:JVDbMp08lVCP7Y6NP3qHroGAO6z2yGKQtS5Jsjqto github.com/yeya24/promlinter v0.3.0/go.mod h1:cDfJQQYv9uYciW60QT0eeHlFodotkYZlL+YcPQN+mW4= github.com/ykadowak/zerologlint v0.1.5 h1:Gy/fMz1dFQN9JZTPjv1hxEk+sRWm05row04Yoolgdiw= github.com/ykadowak/zerologlint v0.1.5/go.mod h1:KaUskqF3e/v59oPmdq1U1DnKcuHokl2/K1U4pmIELKg= +github.com/yosuke-furukawa/json5 v0.1.1 h1:0F9mNwTvOuDNH243hoPqvf+dxa5QsKnZzU20uNsh3ZI= +github.com/yosuke-furukawa/json5 v0.1.1/go.mod h1:sw49aWDqNdRJ6DYUtIQiaA3xyj2IL9tjeNYmX2ixwcU= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -1530,8 +1532,8 @@ go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -go.viam.com/api v0.1.430 h1:6CF3xIA5CGJb3qkQeKYG3FWNinEy6S3XtQBfcjaPUbk= -go.viam.com/api v0.1.430/go.mod h1:drvlBWaiHFxPziz5jayHvibez1qG7lylcNCC1LF8onU= +go.viam.com/api v0.1.432 h1:XT2HfUC/nO/1otRbYat25E4dhk+9KiTU/yj2jmy/YMM= +go.viam.com/api v0.1.432/go.mod h1:drvlBWaiHFxPziz5jayHvibez1qG7lylcNCC1LF8onU= 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.141 h1:rTyofmhC8lDthNA9wwSyYfCEOAccVZpKXefRdh2pXWU=