diff --git a/README.md b/README.md index 994e0bae..c2422bb6 100644 --- a/README.md +++ b/README.md @@ -65,12 +65,20 @@ Use this command to provision a device: ## Device commands -Once a device has been created thorugh the provisioning procedure, it can be deleted by using the following command: +Devices can be deleted using the device delete command. This command accepts two mutually exclusive flags: `--id` and `--tags`. Only one of them must be passed. When the `--id` is passed, the device having such ID gets deleted: + `$ arduino-cloud-cli device delete --id ` -Devices currently present on Arduino IoT Cloud can be retrieved by using this command: +When `--tags` is passed, the devices having all the specified tags get deleted: + +`$ arduino-cloud-cli device delete --tags =,=` + +Devices currently present on Arduino IoT Cloud can be retrieved with: `$ arduino-cloud-cli device list` +It has an optional `--tags` flag that allows to list only the devices having all the provided tags: +`$ arduino-cloud-cli device list --tags =,=` + Add tags to a device. Tags should be passed as a comma-separated list of `=` items: `$ arduino-cloud-cli device create-tags --id --tags =,=` @@ -106,6 +114,18 @@ Print only the thing associated to the passed device: `$ arduino-cloud-cli thing list --device-id ` +Print only the things that have all the passed tags: + +`$ arduino-cloud-cli thing list --tags =,=` + +Things can be deleted using the thing delete command. This command accepts two mutually exclusive flags: `--id` and `--tags`. Only one of them must be passed. When the `--id` is passed, the thing having such ID gets deleted: + +`$ arduino-cloud-cli thing delete --id ` + +When `--tags` is passed, the things having all the specified tags get deleted: + +`$ arduino-cloud-cli thing delete --tags =,=` + Delete a thing with the following command: `$ arduino-cloud-cli thing delete --id ` diff --git a/cli/device/delete.go b/cli/device/delete.go index 6d0c3154..b5fd74ee 100644 --- a/cli/device/delete.go +++ b/cli/device/delete.go @@ -28,7 +28,8 @@ import ( ) var deleteFlags struct { - id string + id string + tags map[string]string } func initDeleteCommand() *cobra.Command { @@ -39,14 +40,25 @@ func initDeleteCommand() *cobra.Command { Run: runDeleteCommand, } deleteCommand.Flags().StringVarP(&deleteFlags.id, "id", "i", "", "Device ID") - deleteCommand.MarkFlagRequired("id") + deleteCommand.Flags().StringToStringVar( + &deleteFlags.tags, + "tags", + nil, + "Comma-separated list of tags with format =.\n"+ + "Delete all devices that match the provided tags.\n"+ + "Mutually exclusive with `--id`.", + ) return deleteCommand } func runDeleteCommand(cmd *cobra.Command, args []string) { logrus.Infof("Deleting device %s\n", deleteFlags.id) - params := &device.DeleteParams{ID: deleteFlags.id} + params := &device.DeleteParams{Tags: deleteFlags.tags} + if deleteFlags.id != "" { + params.ID = &deleteFlags.id + } + err := device.Delete(params) if err != nil { feedback.Errorf("Error during device delete: %v", err) diff --git a/cli/device/list.go b/cli/device/list.go index ff289dba..9e7a1036 100644 --- a/cli/device/list.go +++ b/cli/device/list.go @@ -28,6 +28,10 @@ import ( "github.com/spf13/cobra" ) +var listFlags struct { + tags map[string]string +} + func initListCommand() *cobra.Command { listCommand := &cobra.Command{ Use: "list", @@ -35,13 +39,21 @@ func initListCommand() *cobra.Command { Long: "List devices on Arduino IoT Cloud", Run: runListCommand, } + listCommand.Flags().StringToStringVar( + &listFlags.tags, + "tags", + nil, + "Comma-separated list of tags with format =.\n"+ + "List only devices that match the provided tags.", + ) return listCommand } func runListCommand(cmd *cobra.Command, args []string) { logrus.Info("Listing devices") - devs, err := device.List() + params := &device.ListParams{Tags: listFlags.tags} + devs, err := device.List(params) if err != nil { feedback.Errorf("Error during device list: %v", err) os.Exit(errorcodes.ErrGeneric) diff --git a/cli/thing/delete.go b/cli/thing/delete.go index 6ab5f737..8d9ab7ca 100644 --- a/cli/thing/delete.go +++ b/cli/thing/delete.go @@ -28,7 +28,8 @@ import ( ) var deleteFlags struct { - id string + id string + tags map[string]string } func initDeleteCommand() *cobra.Command { @@ -39,14 +40,25 @@ func initDeleteCommand() *cobra.Command { Run: runDeleteCommand, } deleteCommand.Flags().StringVarP(&deleteFlags.id, "id", "i", "", "Thing ID") - deleteCommand.MarkFlagRequired("id") + deleteCommand.Flags().StringToStringVar( + &deleteFlags.tags, + "tags", + nil, + "Comma-separated list of tags with format =.\n"+ + "Delete all things that match the provided tags.\n"+ + "Mutually exclusive with `--id`.", + ) return deleteCommand } func runDeleteCommand(cmd *cobra.Command, args []string) { logrus.Infof("Deleting thing %s\n", deleteFlags.id) - params := &thing.DeleteParams{ID: deleteFlags.id} + params := &thing.DeleteParams{Tags: deleteFlags.tags} + if deleteFlags.id != "" { + params.ID = &deleteFlags.id + } + err := thing.Delete(params) if err != nil { feedback.Errorf("Error during thing delete: %v", err) diff --git a/cli/thing/list.go b/cli/thing/list.go index 713870d2..ad149c60 100644 --- a/cli/thing/list.go +++ b/cli/thing/list.go @@ -33,6 +33,7 @@ var listFlags struct { ids []string deviceID string variables bool + tags map[string]string } func initListCommand() *cobra.Command { @@ -47,6 +48,13 @@ func initListCommand() *cobra.Command { // list only the thing associated to the passed device id listCommand.Flags().StringVarP(&listFlags.deviceID, "device-id", "d", "", "ID of Device associated to the thing to be retrieved") listCommand.Flags().BoolVarP(&listFlags.variables, "show-variables", "s", false, "Show thing variables") + listCommand.Flags().StringToStringVar( + &listFlags.tags, + "tags", + nil, + "Comma-separated list of tags with format =.\n"+ + "List only things that match the provided tags.", + ) return listCommand } @@ -56,6 +64,7 @@ func runListCommand(cmd *cobra.Command, args []string) { params := &thing.ListParams{ IDs: listFlags.ids, Variables: listFlags.variables, + Tags: listFlags.tags, } if listFlags.deviceID != "" { params.DeviceID = &listFlags.deviceID diff --git a/command/device/delete.go b/command/device/delete.go index 26a1a856..0abae209 100644 --- a/command/device/delete.go +++ b/command/device/delete.go @@ -18,19 +18,31 @@ package device import ( + "errors" + "github.com/arduino/arduino-cloud-cli/internal/config" "github.com/arduino/arduino-cloud-cli/internal/iot" ) // DeleteParams contains the parameters needed to // delete a device from Arduino IoT Cloud. +// ID and Tags parameters are mutually exclusive +// and one among them is required: An error is returned +// if they are both nil or if they are both not nil. type DeleteParams struct { - ID string + ID *string + Tags map[string]string } // Delete command is used to delete a device // from Arduino IoT Cloud. func Delete(params *DeleteParams) error { + if params.ID == nil && params.Tags == nil { + return errors.New("provide either ID or Tags") + } else if params.ID != nil && params.Tags != nil { + return errors.New("cannot use both ID and Tags. only one of them should be not nil") + } + conf, err := config.Retrieve() if err != nil { return err @@ -40,5 +52,27 @@ func Delete(params *DeleteParams) error { return err } - return iotClient.DeviceDelete(params.ID) + if params.ID != nil { + // Delete by id + return iotClient.DeviceDelete(*params.ID) + + } else if params.Tags != nil { + // Delete by tags + dev, err := iotClient.DeviceList(params.Tags) + if err != nil { + return err + } + for _, d := range dev { + err = iotClient.DeviceDelete(d.Id) + if err != nil { + return err + } + } + + } else { + // should not be reachable + return errors.New("provide either '--id' or '--tags' flag") + } + + return nil } diff --git a/command/device/list.go b/command/device/list.go index 26666f1b..cd707521 100644 --- a/command/device/list.go +++ b/command/device/list.go @@ -32,9 +32,15 @@ type DeviceInfo struct { FQBN string `json:"fqbn"` } +// ListParams contains the optional parameters needed +// to filter the devices to be listed. +type ListParams struct { + Tags map[string]string // If tags are provided, only devices that have all these tags are listed. +} + // List command is used to list // the devices of Arduino IoT Cloud. -func List() ([]DeviceInfo, error) { +func List(params *ListParams) ([]DeviceInfo, error) { conf, err := config.Retrieve() if err != nil { return nil, err @@ -44,7 +50,7 @@ func List() ([]DeviceInfo, error) { return nil, err } - foundDevices, err := iotClient.DeviceList() + foundDevices, err := iotClient.DeviceList(params.Tags) if err != nil { return nil, err } diff --git a/command/thing/delete.go b/command/thing/delete.go index 2a7b1f33..83ec4495 100644 --- a/command/thing/delete.go +++ b/command/thing/delete.go @@ -18,19 +18,31 @@ package thing import ( + "errors" + "github.com/arduino/arduino-cloud-cli/internal/config" "github.com/arduino/arduino-cloud-cli/internal/iot" ) // DeleteParams contains the parameters needed to // delete a thing from Arduino IoT Cloud. +// ID and Tags parameters are mutually exclusive +// and one among them is required: An error is returned +// if they are both nil or if they are both not nil. type DeleteParams struct { - ID string + ID *string + Tags map[string]string } // Delete command is used to delete a thing // from Arduino IoT Cloud. func Delete(params *DeleteParams) error { + if params.ID == nil && params.Tags == nil { + return errors.New("provide either ID or Tags") + } else if params.ID != nil && params.Tags != nil { + return errors.New("cannot use both ID and Tags. only one of them should be not nil") + } + conf, err := config.Retrieve() if err != nil { return err @@ -40,5 +52,26 @@ func Delete(params *DeleteParams) error { return err } - return iotClient.ThingDelete(params.ID) + if params.ID != nil { + // Delete by ID + return iotClient.ThingDelete(*params.ID) + + } else if params.Tags != nil { + things, err := iotClient.ThingList(nil, nil, false, params.Tags) + if err != nil { + return err + } + for _, t := range things { + err = iotClient.ThingDelete(t.Id) + if err != nil { + return err + } + } + + } else { + // should not be reachable + return errors.New("provide either '--id' or '--tags' flag") + } + + return nil } diff --git a/command/thing/list.go b/command/thing/list.go index 691089e4..e0d647a7 100644 --- a/command/thing/list.go +++ b/command/thing/list.go @@ -25,9 +25,10 @@ import ( // ListParams contains the optional parameters needed // to filter the things to be listed. type ListParams struct { - IDs []string // If IDs is not nil, only things belonging to that list are returned - DeviceID *string // If DeviceID is provided, only the thing associated to that device is listed. - Variables bool // If Variables is true, variable names are retrieved. + IDs []string // If IDs is not nil, only things belonging to that list are returned + DeviceID *string // If DeviceID is provided, only the thing associated to that device is listed. + Variables bool // If Variables is true, variable names are retrieved. + Tags map[string]string // If tags are provided, only things that have all these tags are listed. } // List command is used to list @@ -42,7 +43,7 @@ func List(params *ListParams) ([]ThingInfo, error) { return nil, err } - foundThings, err := iotClient.ThingList(params.IDs, params.DeviceID, params.Variables) + foundThings, err := iotClient.ThingList(params.IDs, params.DeviceID, params.Variables, params.Tags) if err != nil { return nil, err } diff --git a/internal/iot/client.go b/internal/iot/client.go index 335bac36..5091ae47 100644 --- a/internal/iot/client.go +++ b/internal/iot/client.go @@ -30,7 +30,7 @@ import ( type Client interface { DeviceCreate(fqbn, name, serial, devType string) (*iotclient.ArduinoDevicev2, error) DeviceDelete(id string) error - DeviceList() ([]iotclient.ArduinoDevicev2, error) + DeviceList(tags map[string]string) ([]iotclient.ArduinoDevicev2, error) DeviceShow(id string) (*iotclient.ArduinoDevicev2, error) DeviceOTA(id string, file *os.File, expireMins int) error DeviceTagsCreate(id string, tags map[string]string) error @@ -40,7 +40,7 @@ type Client interface { ThingUpdate(id string, thing *iotclient.Thing, force bool) error ThingDelete(id string) error ThingShow(id string) (*iotclient.ArduinoThing, error) - ThingList(ids []string, device *string, props bool) ([]iotclient.ArduinoThing, error) + ThingList(ids []string, device *string, props bool, tags map[string]string) ([]iotclient.ArduinoThing, error) ThingTagsCreate(id string, tags map[string]string) error ThingTagsDelete(id string, keys []string) error DashboardCreate(dashboard *iotclient.Dashboardv2) (*iotclient.ArduinoDashboardv2, error) @@ -96,8 +96,18 @@ func (cl *client) DeviceDelete(id string) error { // DeviceList retrieves and returns a list of all Arduino IoT Cloud devices // belonging to the user performing the request. -func (cl *client) DeviceList() ([]iotclient.ArduinoDevicev2, error) { - devices, _, err := cl.api.DevicesV2Api.DevicesV2List(cl.ctx, nil) +func (cl *client) DeviceList(tags map[string]string) ([]iotclient.ArduinoDevicev2, error) { + opts := &iotclient.DevicesV2ListOpts{} + if tags != nil { + t := make([]string, 0, len(tags)) + for key, val := range tags { + // Use the 'key:value' format required from the backend + t = append(t, key+":"+val) + } + opts.Tags = optional.NewInterface(t) + } + + devices, _, err := cl.api.DevicesV2Api.DevicesV2List(cl.ctx, opts) if err != nil { err = fmt.Errorf("listing devices: %w", errorDetail(err)) return nil, err @@ -216,7 +226,7 @@ func (cl *client) ThingShow(id string) (*iotclient.ArduinoThing, error) { } // ThingList returns a list of things on Arduino IoT Cloud. -func (cl *client) ThingList(ids []string, device *string, props bool) ([]iotclient.ArduinoThing, error) { +func (cl *client) ThingList(ids []string, device *string, props bool, tags map[string]string) ([]iotclient.ArduinoThing, error) { opts := &iotclient.ThingsV2ListOpts{} opts.ShowProperties = optional.NewBool(props) @@ -228,6 +238,15 @@ func (cl *client) ThingList(ids []string, device *string, props bool) ([]iotclie opts.DeviceId = optional.NewString(*device) } + if tags != nil { + t := make([]string, 0, len(tags)) + for key, val := range tags { + // Use the 'key:value' format required from the backend + t = append(t, key+":"+val) + } + opts.Tags = optional.NewInterface(t) + } + things, _, err := cl.api.ThingsV2Api.ThingsV2List(cl.ctx, opts) if err != nil { err = fmt.Errorf("retrieving things, %w", errorDetail(err)) diff --git a/internal/iot/mocks/Client.go b/internal/iot/mocks/Client.go index 96920346..e812a7dd 100644 --- a/internal/iot/mocks/Client.go +++ b/internal/iot/mocks/Client.go @@ -157,13 +157,13 @@ func (_m *Client) DeviceDelete(id string) error { return r0 } -// DeviceList provides a mock function with given fields: -func (_m *Client) DeviceList() ([]iot.ArduinoDevicev2, error) { - ret := _m.Called() +// DeviceList provides a mock function with given fields: tags +func (_m *Client) DeviceList(tags map[string]string) ([]iot.ArduinoDevicev2, error) { + ret := _m.Called(tags) var r0 []iot.ArduinoDevicev2 - if rf, ok := ret.Get(0).(func() []iot.ArduinoDevicev2); ok { - r0 = rf() + if rf, ok := ret.Get(0).(func(map[string]string) []iot.ArduinoDevicev2); ok { + r0 = rf(tags) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]iot.ArduinoDevicev2) @@ -171,8 +171,8 @@ func (_m *Client) DeviceList() ([]iot.ArduinoDevicev2, error) { } var r1 error - if rf, ok := ret.Get(1).(func() error); ok { - r1 = rf() + if rf, ok := ret.Get(1).(func(map[string]string) error); ok { + r1 = rf(tags) } else { r1 = ret.Error(1) } @@ -282,13 +282,13 @@ func (_m *Client) ThingDelete(id string) error { return r0 } -// ThingList provides a mock function with given fields: ids, device, props -func (_m *Client) ThingList(ids []string, device *string, props bool) ([]iot.ArduinoThing, error) { - ret := _m.Called(ids, device, props) +// ThingList provides a mock function with given fields: ids, device, props, tags +func (_m *Client) ThingList(ids []string, device *string, props bool, tags map[string]string) ([]iot.ArduinoThing, error) { + ret := _m.Called(ids, device, props, tags) var r0 []iot.ArduinoThing - if rf, ok := ret.Get(0).(func([]string, *string, bool) []iot.ArduinoThing); ok { - r0 = rf(ids, device, props) + if rf, ok := ret.Get(0).(func([]string, *string, bool, map[string]string) []iot.ArduinoThing); ok { + r0 = rf(ids, device, props, tags) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]iot.ArduinoThing) @@ -296,8 +296,8 @@ func (_m *Client) ThingList(ids []string, device *string, props bool) ([]iot.Ard } var r1 error - if rf, ok := ret.Get(1).(func([]string, *string, bool) error); ok { - r1 = rf(ids, device, props) + if rf, ok := ret.Get(1).(func([]string, *string, bool, map[string]string) error); ok { + r1 = rf(ids, device, props, tags) } else { r1 = ret.Error(1) }