diff --git a/README.md b/README.md index 25367cb6..994e0bae 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,14 @@ Once a device has been created thorugh the provisioning procedure, it can be del Devices currently present on Arduino IoT Cloud can be retrieved by using this command: `$ arduino-cloud-cli device list` +Add tags to a device. Tags should be passed as a comma-separated list of `=` items: + +`$ arduino-cloud-cli device create-tags --id --tags =,=` + +Delete specific tags of a device. The keys of the tags to delete should be passed in a comma-separated list of strings: + +`$ arduino-cloud-cli device delete-tags --id --keys ,` + ## Thing commands Things can be created starting from a template or by cloning another thing. @@ -110,6 +118,15 @@ Bind a thing to an existing device: `$ arduino-cloud-cli thing bind --id --device-id ` +Add tags to a thing. Tags should be passed as a comma-separated list of `=` items: + +`$ arduino-cloud-cli thing create-tags --id --tags =,=` + +Delete specific tags of a thing. The keys of the tags to delete should be passed in a comma-separated list of strings: + +`$ arduino-cloud-cli thing delete-tags --id --keys ,` + + ## Ota commands Perform an OTA firmware update. Note that the binary file (`.bin`) should be compiled using an arduino core that supports the specified device. diff --git a/cli/device/device.go b/cli/device/device.go index 7163c6ce..51a894ec 100644 --- a/cli/device/device.go +++ b/cli/device/device.go @@ -18,6 +18,7 @@ package device import ( + "github.com/arduino/arduino-cloud-cli/cli/device/tag" "github.com/spf13/cobra" ) @@ -31,6 +32,8 @@ func NewCommand() *cobra.Command { deviceCommand.AddCommand(initCreateCommand()) deviceCommand.AddCommand(initListCommand()) deviceCommand.AddCommand(initDeleteCommand()) + deviceCommand.AddCommand(tag.InitCreateTagsCommand()) + deviceCommand.AddCommand(tag.InitDeleteTagsCommand()) return deviceCommand } diff --git a/cli/device/tag/create.go b/cli/device/tag/create.go new file mode 100644 index 00000000..e43345e4 --- /dev/null +++ b/cli/device/tag/create.go @@ -0,0 +1,70 @@ +// This file is part of arduino-cloud-cli. +// +// Copyright (C) 2021 ARDUINO SA (http://www.arduino.cc/) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package tag + +import ( + "os" + + "github.com/arduino/arduino-cli/cli/errorcodes" + "github.com/arduino/arduino-cli/cli/feedback" + "github.com/arduino/arduino-cloud-cli/command/tag" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var createTagsFlags struct { + id string + tags map[string]string +} + +func InitCreateTagsCommand() *cobra.Command { + createTagsCommand := &cobra.Command{ + Use: "create-tags", + Short: "Create or overwrite tags on a device", + Long: "Create or overwrite tags on a device of Arduino IoT Cloud", + Run: runCreateTagsCommand, + } + createTagsCommand.Flags().StringVarP(&createTagsFlags.id, "id", "i", "", "Device ID") + createTagsCommand.Flags().StringToStringVar( + &createTagsFlags.tags, + "tags", + nil, + "Comma-separated list of tags with format =.", + ) + createTagsCommand.MarkFlagRequired("id") + createTagsCommand.MarkFlagRequired("tags") + return createTagsCommand +} + +func runCreateTagsCommand(cmd *cobra.Command, args []string) { + logrus.Infof("Creating tags on device %s\n", createTagsFlags.id) + + params := &tag.CreateTagsParams{ + ID: createTagsFlags.id, + Tags: createTagsFlags.tags, + Resource: tag.Device, + } + + err := tag.CreateTags(params) + if err != nil { + feedback.Errorf("Error during device create-tags: %v", err) + os.Exit(errorcodes.ErrGeneric) + } + + logrus.Info("Tags successfully created") +} diff --git a/cli/device/tag/delete.go b/cli/device/tag/delete.go new file mode 100644 index 00000000..e5b38dd3 --- /dev/null +++ b/cli/device/tag/delete.go @@ -0,0 +1,67 @@ +// This file is part of arduino-cloud-cli. +// +// Copyright (C) 2021 ARDUINO SA (http://www.arduino.cc/) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package tag + +import ( + "os" + + "github.com/arduino/arduino-cli/cli/errorcodes" + "github.com/arduino/arduino-cli/cli/feedback" + "github.com/arduino/arduino-cloud-cli/command/tag" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var deleteTagsFlags struct { + id string + keys []string +} + +func InitDeleteTagsCommand() *cobra.Command { + deleteTagsCommand := &cobra.Command{ + Use: "delete-tags", + Short: "Delete tags of a device", + Long: "Delete tags of a device of Arduino IoT Cloud", + Run: runDeleteTagsCommand, + } + + deleteTagsCommand.Flags().StringVarP(&deleteTagsFlags.id, "id", "i", "", "Device ID") + deleteTagsCommand.Flags().StringSliceVarP(&deleteTagsFlags.keys, "keys", "k", nil, "Comma-separated list of keys of tags to delete") + + deleteTagsCommand.MarkFlagRequired("id") + deleteTagsCommand.MarkFlagRequired("keys") + return deleteTagsCommand +} + +func runDeleteTagsCommand(cmd *cobra.Command, args []string) { + logrus.Infof("Deleting tags with keys %s\n", deleteTagsFlags.keys) + + params := &tag.DeleteTagsParams{ + ID: deleteTagsFlags.id, + Keys: deleteTagsFlags.keys, + Resource: tag.Device, + } + + err := tag.DeleteTags(params) + if err != nil { + feedback.Errorf("Error during device delete-tags: %v", err) + os.Exit(errorcodes.ErrGeneric) + } + + logrus.Info("Tags successfully deleted") +} diff --git a/cli/thing/tag/create.go b/cli/thing/tag/create.go new file mode 100644 index 00000000..18b7e5a1 --- /dev/null +++ b/cli/thing/tag/create.go @@ -0,0 +1,70 @@ +// This file is part of arduino-cloud-cli. +// +// Copyright (C) 2021 ARDUINO SA (http://www.arduino.cc/) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package tag + +import ( + "os" + + "github.com/arduino/arduino-cli/cli/errorcodes" + "github.com/arduino/arduino-cli/cli/feedback" + "github.com/arduino/arduino-cloud-cli/command/tag" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var createTagsFlags struct { + id string + tags map[string]string +} + +func InitCreateTagsCommand() *cobra.Command { + createTagsCommand := &cobra.Command{ + Use: "create-tags", + Short: "Create or overwrite tags on a thing", + Long: "Create or overwrite tags on a thing of Arduino IoT Cloud", + Run: runCreateTagsCommand, + } + createTagsCommand.Flags().StringVarP(&createTagsFlags.id, "id", "i", "", "Thing ID") + createTagsCommand.Flags().StringToStringVar( + &createTagsFlags.tags, + "tags", + nil, + "Comma-separated list of tags with format =.", + ) + createTagsCommand.MarkFlagRequired("id") + createTagsCommand.MarkFlagRequired("tags") + return createTagsCommand +} + +func runCreateTagsCommand(cmd *cobra.Command, args []string) { + logrus.Infof("Creating tags on thing %s\n", createTagsFlags.id) + + params := &tag.CreateTagsParams{ + ID: createTagsFlags.id, + Tags: createTagsFlags.tags, + Resource: tag.Thing, + } + + err := tag.CreateTags(params) + if err != nil { + feedback.Errorf("Error during thing create-tags: %v", err) + os.Exit(errorcodes.ErrGeneric) + } + + logrus.Info("Tags successfully created") +} diff --git a/cli/thing/tag/delete.go b/cli/thing/tag/delete.go new file mode 100644 index 00000000..fd5ae9bb --- /dev/null +++ b/cli/thing/tag/delete.go @@ -0,0 +1,67 @@ +// This file is part of arduino-cloud-cli. +// +// Copyright (C) 2021 ARDUINO SA (http://www.arduino.cc/) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package tag + +import ( + "os" + + "github.com/arduino/arduino-cli/cli/errorcodes" + "github.com/arduino/arduino-cli/cli/feedback" + "github.com/arduino/arduino-cloud-cli/command/tag" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var deleteTagsFlags struct { + id string + keys []string +} + +func InitDeleteTagsCommand() *cobra.Command { + deleteTagsCommand := &cobra.Command{ + Use: "delete-tags", + Short: "Delete tags of a thing", + Long: "Delete tags of a thing of Arduino IoT Cloud", + Run: runDeleteTagsCommand, + } + + deleteTagsCommand.Flags().StringVarP(&deleteTagsFlags.id, "id", "i", "", "Thing ID") + deleteTagsCommand.Flags().StringSliceVarP(&deleteTagsFlags.keys, "keys", "k", nil, "Comma-separated list of keys of tags to delete") + + deleteTagsCommand.MarkFlagRequired("id") + deleteTagsCommand.MarkFlagRequired("keys") + return deleteTagsCommand +} + +func runDeleteTagsCommand(cmd *cobra.Command, args []string) { + logrus.Infof("Deleting tags with keys %s\n", deleteTagsFlags.keys) + + params := &tag.DeleteTagsParams{ + ID: deleteTagsFlags.id, + Keys: deleteTagsFlags.keys, + Resource: tag.Thing, + } + + err := tag.DeleteTags(params) + if err != nil { + feedback.Errorf("Error during thing delete-tags: %v", err) + os.Exit(errorcodes.ErrGeneric) + } + + logrus.Info("Tags successfully deleted") +} diff --git a/cli/thing/thing.go b/cli/thing/thing.go index f33bffa9..cf5b3d7f 100644 --- a/cli/thing/thing.go +++ b/cli/thing/thing.go @@ -18,6 +18,7 @@ package thing import ( + "github.com/arduino/arduino-cloud-cli/cli/thing/tag" "github.com/spf13/cobra" ) @@ -34,6 +35,8 @@ func NewCommand() *cobra.Command { thingCommand.AddCommand(initDeleteCommand()) thingCommand.AddCommand(initExtractCommand()) thingCommand.AddCommand(initBindCommand()) + thingCommand.AddCommand(tag.InitCreateTagsCommand()) + thingCommand.AddCommand(tag.InitDeleteTagsCommand()) return thingCommand } diff --git a/command/tag/create.go b/command/tag/create.go new file mode 100644 index 00000000..a0970906 --- /dev/null +++ b/command/tag/create.go @@ -0,0 +1,56 @@ +// This file is part of arduino-cloud-cli. +// +// Copyright (C) 2021 ARDUINO SA (http://www.arduino.cc/) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package tag + +import ( + "errors" + + "github.com/arduino/arduino-cloud-cli/internal/config" + "github.com/arduino/arduino-cloud-cli/internal/iot" +) + +// CreateTagsParams contains the parameters needed to create or overwrite +// tags on a resource of Arduino IoT Cloud. +type CreateTagsParams struct { + ID string // Resource ID + Tags map[string]string // Map of tags to create + Resource ResourceType +} + +// CreateTags allows to create or overwrite tags +// on a resource of Arduino IoT Cloud +func CreateTags(params *CreateTagsParams) error { + conf, err := config.Retrieve() + if err != nil { + return err + } + iotClient, err := iot.NewClient(conf.Client, conf.Secret) + if err != nil { + return err + } + + switch params.Resource { + case Thing: + err = iotClient.ThingTagsCreate(params.ID, params.Tags) + case Device: + err = iotClient.DeviceTagsCreate(params.ID, params.Tags) + default: + err = errors.New("passed Resource parameter is not valid") + } + return err +} diff --git a/command/tag/delete.go b/command/tag/delete.go new file mode 100644 index 00000000..cb8fce63 --- /dev/null +++ b/command/tag/delete.go @@ -0,0 +1,56 @@ +// This file is part of arduino-cloud-cli. +// +// Copyright (C) 2021 ARDUINO SA (http://www.arduino.cc/) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package tag + +import ( + "errors" + + "github.com/arduino/arduino-cloud-cli/internal/config" + "github.com/arduino/arduino-cloud-cli/internal/iot" +) + +// DeleteTagsParams contains the parameters needed to +// delete tags of a device from Arduino IoT Cloud. +type DeleteTagsParams struct { + ID string + Keys []string // Keys of tags to delete + Resource ResourceType +} + +// DeleteTags command is used to delete tags of a device +// from Arduino IoT Cloud. +func DeleteTags(params *DeleteTagsParams) error { + conf, err := config.Retrieve() + if err != nil { + return err + } + iotClient, err := iot.NewClient(conf.Client, conf.Secret) + if err != nil { + return err + } + + switch params.Resource { + case Thing: + err = iotClient.ThingTagsDelete(params.ID, params.Keys) + case Device: + err = iotClient.DeviceTagsDelete(params.ID, params.Keys) + default: + err = errors.New("passed Resource parameter is not valid") + } + return err +} diff --git a/command/tag/resource.go b/command/tag/resource.go new file mode 100644 index 00000000..3912e120 --- /dev/null +++ b/command/tag/resource.go @@ -0,0 +1,13 @@ +package tag + +// ResourceType specifies which resource the +// tag command refers to. +// Valid resources are the entities of the +// cloud that have a 'tags' field. +type ResourceType int + +const ( + None ResourceType = iota + Device + Thing +) diff --git a/internal/iot/client.go b/internal/iot/client.go index 0b42326a..335bac36 100644 --- a/internal/iot/client.go +++ b/internal/iot/client.go @@ -33,12 +33,16 @@ type Client interface { DeviceList() ([]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 + DeviceTagsDelete(id string, keys []string) error CertificateCreate(id, csr string) (*iotclient.ArduinoCompressedv2, error) ThingCreate(thing *iotclient.Thing, force bool) (*iotclient.ArduinoThing, error) 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) + ThingTagsCreate(id string, tags map[string]string) error + ThingTagsDelete(id string, keys []string) error DashboardCreate(dashboard *iotclient.Dashboardv2) (*iotclient.ArduinoDashboardv2, error) DashboardShow(id string) (*iotclient.ArduinoDashboardv2, error) DashboardDelete(id string) error @@ -126,6 +130,32 @@ func (cl *client) DeviceOTA(id string, file *os.File, expireMins int) error { return nil } +// DeviceTagsCreate allows to create or overwrite tags on a device of Arduino IoT Cloud. +func (cl *client) DeviceTagsCreate(id string, tags map[string]string) error { + for key, val := range tags { + t := iotclient.Tag{Key: key, Value: val} + _, err := cl.api.DevicesV2TagsApi.DevicesV2TagsUpsert(cl.ctx, id, t) + if err != nil { + err = fmt.Errorf("cannot create tag %s: %w", key, errorDetail(err)) + return err + } + } + return nil +} + +// DeviceTagsDelete deletes the tags of a device of Arduino IoT Cloud, +// given the device id and the keys of the tags. +func (cl *client) DeviceTagsDelete(id string, keys []string) error { + for _, key := range keys { + _, err := cl.api.DevicesV2TagsApi.DevicesV2TagsDelete(cl.ctx, id, key) + if err != nil { + err = fmt.Errorf("cannot delete tag %s: %w", key, errorDetail(err)) + return err + } + } + return nil +} + // CertificateCreate allows to upload a certificate on Arduino IoT Cloud. // It returns the certificate parameters populated by the cloud. func (cl *client) CertificateCreate(id, csr string) (*iotclient.ArduinoCompressedv2, error) { @@ -206,6 +236,32 @@ func (cl *client) ThingList(ids []string, device *string, props bool) ([]iotclie return things, nil } +// ThingTagsCreate allows to create or overwrite tags on a thing of Arduino IoT Cloud. +func (cl *client) ThingTagsCreate(id string, tags map[string]string) error { + for key, val := range tags { + t := iotclient.Tag{Key: key, Value: val} + _, err := cl.api.ThingsV2TagsApi.ThingsV2TagsUpsert(cl.ctx, id, t) + if err != nil { + err = fmt.Errorf("cannot create tag %s: %w", key, errorDetail(err)) + return err + } + } + return nil +} + +// ThingTagsDelete deletes the tags of a thing of Arduino IoT Cloud, +// given the thing id and the keys of the tags. +func (cl *client) ThingTagsDelete(id string, keys []string) error { + for _, key := range keys { + _, err := cl.api.ThingsV2TagsApi.ThingsV2TagsDelete(cl.ctx, id, key) + if err != nil { + err = fmt.Errorf("cannot delete tag %s: %w", key, errorDetail(err)) + return err + } + } + return nil +} + // DashboardCreate adds a new dashboard on Arduino IoT Cloud. func (cl *client) DashboardCreate(dashboard *iotclient.Dashboardv2) (*iotclient.ArduinoDashboardv2, error) { newDashboard, _, err := cl.api.DashboardsV2Api.DashboardsV2Create(cl.ctx, *dashboard) diff --git a/internal/iot/mocks/Client.go b/internal/iot/mocks/Client.go index 09baa3a9..96920346 100644 --- a/internal/iot/mocks/Client.go +++ b/internal/iot/mocks/Client.go @@ -217,6 +217,34 @@ func (_m *Client) DeviceShow(id string) (*iot.ArduinoDevicev2, error) { return r0, r1 } +// DeviceTagsCreate provides a mock function with given fields: id, tags +func (_m *Client) DeviceTagsCreate(id string, tags map[string]string) error { + ret := _m.Called(id, tags) + + var r0 error + if rf, ok := ret.Get(0).(func(string, map[string]string) error); ok { + r0 = rf(id, tags) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeviceTagsDelete provides a mock function with given fields: id, keys +func (_m *Client) DeviceTagsDelete(id string, keys []string) error { + ret := _m.Called(id, keys) + + var r0 error + if rf, ok := ret.Get(0).(func(string, []string) error); ok { + r0 = rf(id, keys) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // ThingCreate provides a mock function with given fields: thing, force func (_m *Client) ThingCreate(thing *iot.Thing, force bool) (*iot.ArduinoThing, error) { ret := _m.Called(thing, force) @@ -300,6 +328,34 @@ func (_m *Client) ThingShow(id string) (*iot.ArduinoThing, error) { return r0, r1 } +// ThingTagsCreate provides a mock function with given fields: id, tags +func (_m *Client) ThingTagsCreate(id string, tags map[string]string) error { + ret := _m.Called(id, tags) + + var r0 error + if rf, ok := ret.Get(0).(func(string, map[string]string) error); ok { + r0 = rf(id, tags) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ThingTagsDelete provides a mock function with given fields: id, keys +func (_m *Client) ThingTagsDelete(id string, keys []string) error { + ret := _m.Called(id, keys) + + var r0 error + if rf, ok := ret.Get(0).(func(string, []string) error); ok { + r0 = rf(id, keys) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // ThingUpdate provides a mock function with given fields: id, thing, force func (_m *Client) ThingUpdate(id string, thing *iot.Thing, force bool) error { ret := _m.Called(id, thing, force)