From 697ce85668055cc45ca93645000967675bd2e4ad Mon Sep 17 00:00:00 2001 From: Paolo Calao Date: Tue, 3 Aug 2021 17:52:32 +0200 Subject: [PATCH 1/6] Add thing create command The parameters to create a new thing are: - thing name - mandatory - device (id) to bind the thing to - optional - thing template - mandatory if no thing to clone is passed - thing to clone (id) - mandatory if no template note that: - iot client is really ugly and should be refactored -> the problem is that iot-client-go doesn't accept the 'properties' field when creating a new thing. For this reason a standard http request has been performed. todos and questions: - improve naming of create flags and create params (maybe it's not clear that IDs should be used) - is template or thing to be cloned a real requirement? An empty thing could also make sense - refactor iot client AddThing -> prerequisite: add the properties parameter in iot-client-go - thing Create function stores the thing parameters into an empty interface -> this will be replaced into a proper structure as soon as iot-client-go can handle properties --- cli/root.go | 2 + cli/thing/create.go | 49 ++++++++++++++++++++ cli/thing/thing.go | 17 +++++++ command/thing/create.go | 100 ++++++++++++++++++++++++++++++++++++++++ internal/iot/client.go | 78 +++++++++++++++++++++++++++++++ 5 files changed, 246 insertions(+) create mode 100644 cli/thing/create.go create mode 100644 cli/thing/thing.go create mode 100644 command/thing/create.go diff --git a/cli/root.go b/cli/root.go index a999a1b8..ccc75821 100644 --- a/cli/root.go +++ b/cli/root.go @@ -6,6 +6,7 @@ import ( "github.com/arduino/iot-cloud-cli/cli/config" "github.com/arduino/iot-cloud-cli/cli/device" + "github.com/arduino/iot-cloud-cli/cli/thing" "github.com/spf13/cobra" ) @@ -13,6 +14,7 @@ func Execute() { rootCmd := &cobra.Command{} rootCmd.AddCommand(config.NewCommand()) rootCmd.AddCommand(device.NewCommand()) + rootCmd.AddCommand(thing.NewCommand()) if err := rootCmd.Execute(); err != nil { fmt.Fprintln(os.Stderr, err) diff --git a/cli/thing/create.go b/cli/thing/create.go new file mode 100644 index 00000000..f64f6888 --- /dev/null +++ b/cli/thing/create.go @@ -0,0 +1,49 @@ +package thing + +import ( + "fmt" + + "github.com/arduino/iot-cloud-cli/command/thing" + "github.com/spf13/cobra" +) + +var createFlags struct { + name string + device string + template string + clone string +} + +func initCreateCommand() *cobra.Command { + createCommand := &cobra.Command{ + Use: "create", + Short: "Create a thing", + Long: "Create a thing for Arduino IoT Cloud", + RunE: runCreateCommand, + } + createCommand.Flags().StringVarP(&createFlags.name, "name", "n", "", "Thing name") + createCommand.Flags().StringVarP(&createFlags.device, "device", "d", "", "ID of Device to bind to the new thing") + createCommand.Flags().StringVarP(&createFlags.clone, "clone", "c", "", "ID of Thing to be cloned") + createCommand.Flags().StringVarP(&createFlags.template, "template", "t", "", "File containing a thing template") + createCommand.MarkFlagRequired("name") + return createCommand +} + +func runCreateCommand(cmd *cobra.Command, args []string) error { + fmt.Printf("Creating thing with name %s\n", createFlags.name) + + params := &thing.CreateParams{ + Name: createFlags.name, + Device: createFlags.device, + Template: createFlags.template, + Clone: createFlags.clone, + } + + thingID, err := thing.Create(params) + if err != nil { + return err + } + + fmt.Printf("IoT Cloud thing created with ID: %s\n", thingID) + return nil +} diff --git a/cli/thing/thing.go b/cli/thing/thing.go new file mode 100644 index 00000000..a1ccce9e --- /dev/null +++ b/cli/thing/thing.go @@ -0,0 +1,17 @@ +package thing + +import ( + "github.com/spf13/cobra" +) + +func NewCommand() *cobra.Command { + thingCommand := &cobra.Command{ + Use: "thing", + Short: "Thing commands.", + Long: "Thing commands.", + } + + thingCommand.AddCommand(initCreateCommand()) + + return thingCommand +} diff --git a/command/thing/create.go b/command/thing/create.go new file mode 100644 index 00000000..d0f5c9a1 --- /dev/null +++ b/command/thing/create.go @@ -0,0 +1,100 @@ +package thing + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + + "github.com/arduino/iot-cloud-cli/internal/config" + "github.com/arduino/iot-cloud-cli/internal/iot" +) + +// CreateParams contains the parameters needed to create a new thing. +type CreateParams struct { + // Mandatory - contains the name of the thing + Name string + // Optional - contains the ID of the device to be bound to the thing + Device string + // Mandatory if device is empty - contains the path of the template file + Template string + // Mandatory if template is empty- name of things to be cloned + Clone string +} + +// Create allows to create a new thing +func Create(params *CreateParams) (string, error) { + if params.Template == "" && params.Clone == "" { + return "", fmt.Errorf("%s", "provide either a thing(ID) to clone (--clone) or a thing template file (--template)\n") + } + + conf, err := config.Retrieve() + if err != nil { + return "", err + } + iotClient, err := iot.NewClient(conf.Client, conf.Secret) + if err != nil { + return "", err + } + + var thing map[string]interface{} + + if params.Clone != "" { + thing, err = cloneThing(iotClient, params.Clone) + if err != nil { + return "", err + } + + } else if params.Template != "" { + thing, err = loadTemplate(params.Template) + if err != nil { + return "", err + } + } + + thing["name"] = params.Name + force := true + if params.Device != "" { + thing["device_id"] = params.Device + } + thingID, err := iotClient.AddThing(thing, force) + if err != nil { + return "", err + } + + return thingID, nil +} + +func cloneThing(client iot.Client, thingID string) (map[string]interface{}, error) { + clone, err := client.GetThing(thingID) + if err != nil { + return nil, fmt.Errorf("%s: %w", "retrieving the thing to be cloned", err) + } + + thing := make(map[string]interface{}) + thing["device_id"] = clone.DeviceId + thing["properties"] = clone.Properties + + return thing, nil +} + +func loadTemplate(file string) (map[string]interface{}, error) { + templateFile, err := os.Open(file) + if err != nil { + return nil, err + } + defer templateFile.Close() + + templateBytes, err := ioutil.ReadAll(templateFile) + if err != nil { + return nil, err + } + + var template map[string]interface{} + err = json.Unmarshal([]byte(templateBytes), &template) + if err != nil { + return nil, fmt.Errorf("%s: %w", "reading template file: template not valid: ", err) + } + + return template, nil +} diff --git a/internal/iot/client.go b/internal/iot/client.go index a48a308f..4d55cfd5 100644 --- a/internal/iot/client.go +++ b/internal/iot/client.go @@ -1,8 +1,13 @@ package iot import ( + "bytes" "context" + "encoding/json" + "errors" "fmt" + "net/http" + "strconv" iotclient "github.com/arduino/iot-client-go" ) @@ -13,6 +18,8 @@ type Client interface { DeleteDevice(id string) error ListDevices() ([]iotclient.ArduinoDevicev2, error) AddCertificate(id, csr string) (*iotclient.ArduinoCompressedv2, error) + AddThing(thing interface{}, force bool) (string, error) + GetThing(id string) (*iotclient.ArduinoThing, error) } type client struct { @@ -89,6 +96,77 @@ func (cl *client) AddCertificate(id, csr string) (*iotclient.ArduinoCompressedv2 return &newCert.Compressed, nil } +func (cl *client) AddThing(thing interface{}, force bool) (string, error) { + // Request + url := "https://api2.arduino.cc/iot/v2/things" + bodyBuf := &bytes.Buffer{} + err := json.NewEncoder(bodyBuf).Encode(thing) + if err != nil { + return "", err + } + if bodyBuf.Len() == 0 { + err = errors.New("invalid body type") + return "", err + } + + req, err := http.NewRequest(http.MethodPut, url, bodyBuf) + if err != nil { + return "", err + } + req.Header.Set("Content-Type", "application/json") + + var bearer = "Bearer " + cl.ctx.Value(iotclient.ContextAccessToken).(string) + req.Header.Add("Authorization", bearer) + + q := req.URL.Query() + q.Add("force", strconv.FormatBool(force)) + req.URL.RawQuery = q.Encode() + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + err = fmt.Errorf("%s: %w", "adding new thing", err) + return "", err + } + + // Response + if resp.StatusCode != 201 { + // Get response error detail + var respObj map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&respObj) + if err != nil { + return "", fmt.Errorf("%s: %s: %s", "cannot get response body", err, resp.Status) + } + return "", fmt.Errorf("%s: %s", "adding new thing", respObj["detail"].(string)) + } + + if resp.Body == nil { + return "", errors.New("response body is empty") + } + defer resp.Body.Close() + + var newThing map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&newThing) + if err != nil { + err = fmt.Errorf("%s: %w", "cannot parse body response", err) + return "", err + } + newID, ok := newThing["id"].(string) + if !ok { + return "", errors.New("adding new thing: new thing created: returned id is not available") + } + return newID, nil +} + +func (cl *client) GetThing(id string) (*iotclient.ArduinoThing, error) { + thing, _, err := cl.api.ThingsV2Api.ThingsV2Show(cl.ctx, id, nil) + if err != nil { + err = fmt.Errorf("retrieving thing, %w", err) + return nil, err + } + return &thing, nil +} + func (cl *client) setup(client, secret string) error { // Get the access token in exchange of client_id and client_secret tok, err := token(client, secret) From 5365a3d930ec9426c6d4126e11acc159ca604af4 Mon Sep 17 00:00:00 2001 From: Paolo Calao Date: Fri, 6 Aug 2021 11:41:11 +0200 Subject: [PATCH 2/6] Check clone params before copying --- command/thing/create.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/command/thing/create.go b/command/thing/create.go index d0f5c9a1..a1db48c0 100644 --- a/command/thing/create.go +++ b/command/thing/create.go @@ -72,8 +72,12 @@ func cloneThing(client iot.Client, thingID string) (map[string]interface{}, erro } thing := make(map[string]interface{}) - thing["device_id"] = clone.DeviceId - thing["properties"] = clone.Properties + if clone.DeviceId != "" { + thing["device_id"] = clone.DeviceId + } + if clone.Properties != nil { + thing["properties"] = clone.Properties + } return thing, nil } From 18dfa27f9b978073b68c47349dc5823ff24fbf39 Mon Sep 17 00:00:00 2001 From: Paolo Calao Date: Wed, 25 Aug 2021 12:25:30 +0200 Subject: [PATCH 3/6] Thing create uses updated iot-client-go Given that iot-client-go has been updated and now supports the creation of things with a slice of properties, it's been here employed to create a thing. Issue: response's error details, coming from arduino iot cloud, are masked by iot-client-go --- command/thing/create.go | 45 ++++++++++++++++++------- go.mod | 8 +++-- go.sum | 18 +++++----- internal/iot/client.go | 73 +++++++---------------------------------- 4 files changed, 59 insertions(+), 85 deletions(-) diff --git a/command/thing/create.go b/command/thing/create.go index a1db48c0..0d536293 100644 --- a/command/thing/create.go +++ b/command/thing/create.go @@ -6,6 +6,9 @@ import ( "io/ioutil" "os" + "errors" + + iotclient "github.com/arduino/iot-client-go" "github.com/arduino/iot-cloud-cli/internal/config" "github.com/arduino/iot-cloud-cli/internal/iot" ) @@ -37,7 +40,7 @@ func Create(params *CreateParams) (string, error) { return "", err } - var thing map[string]interface{} + var thing *iotclient.Thing if params.Clone != "" { thing, err = cloneThing(iotClient, params.Clone) @@ -50,12 +53,15 @@ func Create(params *CreateParams) (string, error) { if err != nil { return "", err } + + } else { + return "", errors.New("provide either a thing(ID) to clone (--clone) or a thing template file (--template)") } - thing["name"] = params.Name + thing.Name = params.Name force := true if params.Device != "" { - thing["device_id"] = params.Device + thing.DeviceId = params.Device } thingID, err := iotClient.AddThing(thing, force) if err != nil { @@ -65,24 +71,39 @@ func Create(params *CreateParams) (string, error) { return thingID, nil } -func cloneThing(client iot.Client, thingID string) (map[string]interface{}, error) { +func cloneThing(client iot.Client, thingID string) (*iotclient.Thing, error) { clone, err := client.GetThing(thingID) if err != nil { return nil, fmt.Errorf("%s: %w", "retrieving the thing to be cloned", err) } - thing := make(map[string]interface{}) + thing := &iotclient.Thing{} + + // Copy device id if clone.DeviceId != "" { - thing["device_id"] = clone.DeviceId + thing.DeviceId = clone.DeviceId } - if clone.Properties != nil { - thing["properties"] = clone.Properties + + // Copy properties + for _, p := range clone.Properties { + thing.Properties = append(thing.Properties, iotclient.Property{ + Name: p.Name, + MinValue: p.MinValue, + MaxValue: p.MaxValue, + Permission: p.Permission, + UpdateParameter: p.UpdateParameter, + UpdateStrategy: p.UpdateStrategy, + Type: p.Type, + VariableName: p.VariableName, + Persist: p.Persist, + Tag: p.Tag, + }) } return thing, nil } -func loadTemplate(file string) (map[string]interface{}, error) { +func loadTemplate(file string) (*iotclient.Thing, error) { templateFile, err := os.Open(file) if err != nil { return nil, err @@ -94,11 +115,11 @@ func loadTemplate(file string) (map[string]interface{}, error) { return nil, err } - var template map[string]interface{} - err = json.Unmarshal([]byte(templateBytes), &template) + thing := &iotclient.Thing{} + err = json.Unmarshal([]byte(templateBytes), thing) if err != nil { return nil, fmt.Errorf("%s: %w", "reading template file: template not valid: ", err) } - return template, nil + return thing, nil } diff --git a/go.mod b/go.mod index 2c0205c1..34c856d6 100644 --- a/go.mod +++ b/go.mod @@ -5,16 +5,18 @@ go 1.15 require ( github.com/arduino/arduino-cli v0.0.0-20210607095659-16f41352eac3 github.com/arduino/go-paths-helper v1.6.0 - github.com/arduino/iot-client-go v1.3.3 + github.com/arduino/iot-client-go v1.3.4-0.20210824101852-4a44149473c1 github.com/howeyc/crc16 v0.0.0-20171223171357-2b2a61e366a6 github.com/sirupsen/logrus v1.4.2 github.com/spf13/cobra v1.1.3 github.com/spf13/viper v1.7.0 github.com/stretchr/testify v1.6.1 go.bug.st/serial v1.3.0 - golang.org/x/net v0.0.0-20210505024714-0287a6fb4125 // indirect - golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914 + golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d // indirect + golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f golang.org/x/sys v0.0.0-20210503173754-0981d6026fa6 // indirect + google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20210504143626-3b2ad6ccc450 // indirect google.golang.org/grpc v1.39.0 + google.golang.org/protobuf v1.27.1 // indirect ) diff --git a/go.sum b/go.sum index 320ae417..89529be4 100644 --- a/go.sum +++ b/go.sum @@ -59,8 +59,8 @@ github.com/arduino/go-properties-orderedmap v1.3.0/go.mod h1:DKjD2VXY/NZmlingh4l github.com/arduino/go-timeutils v0.0.0-20171220113728-d1dd9e313b1b/go.mod h1:uwGy5PpN4lqW97FiLnbcx+xx8jly5YuPMJWfVwwjJiQ= github.com/arduino/go-win32-utils v0.0.0-20180330194947-ed041402e83b h1:3PjgYG5gVPA7cipp7vIR2lF96KkEJIFBJ+ANnuv6J20= github.com/arduino/go-win32-utils v0.0.0-20180330194947-ed041402e83b/go.mod h1:iIPnclBMYm1g32Q5kXoqng4jLhMStReIP7ZxaoUC2y8= -github.com/arduino/iot-client-go v1.3.3 h1:W+92osS+WcdVpePdPmj/BtupM+xV6DOJlI0HGpKrTX4= -github.com/arduino/iot-client-go v1.3.3/go.mod h1:gYvpMt7Qw+OSScTLyIlCnpbvy9y96ey/2zhB4w6FoK0= +github.com/arduino/iot-client-go v1.3.4-0.20210824101852-4a44149473c1 h1:tgVUBPbqkyd3KHTs+gweP5t9KAnkLbAsAMrHvu9jZSg= +github.com/arduino/iot-client-go v1.3.4-0.20210824101852-4a44149473c1/go.mod h1:gYvpMt7Qw+OSScTLyIlCnpbvy9y96ey/2zhB4w6FoK0= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= @@ -494,15 +494,15 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= -golang.org/x/net v0.0.0-20210505024714-0287a6fb4125 h1:Ugb8sMTWuWRC3+sz5WeN/4kejDx9BvIwnPUiJBjJE+8= -golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d h1:LO7XpTYMwTqxjLcGWPijK3vRXg1aWdlNOVOHRq45d7c= +golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914 h1:3B43BWw0xEBsLZ/NO1VALz6fppU3481pik+2Ksv45z8= -golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f h1:Qmd2pbz05z7z6lm0DrgQVVPuBm92jqujBKMHMOlOQEw= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -638,8 +638,9 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -701,8 +702,9 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20160105164936-4f90aeace3a2/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/iot/client.go b/internal/iot/client.go index 4d55cfd5..02796fd2 100644 --- a/internal/iot/client.go +++ b/internal/iot/client.go @@ -1,13 +1,9 @@ package iot import ( - "bytes" "context" "encoding/json" - "errors" "fmt" - "net/http" - "strconv" iotclient "github.com/arduino/iot-client-go" ) @@ -18,7 +14,7 @@ type Client interface { DeleteDevice(id string) error ListDevices() ([]iotclient.ArduinoDevicev2, error) AddCertificate(id, csr string) (*iotclient.ArduinoCompressedv2, error) - AddThing(thing interface{}, force bool) (string, error) + AddThing(thing *iotclient.Thing, force bool) (string, error) GetThing(id string) (*iotclient.ArduinoThing, error) } @@ -96,68 +92,21 @@ func (cl *client) AddCertificate(id, csr string) (*iotclient.ArduinoCompressedv2 return &newCert.Compressed, nil } -func (cl *client) AddThing(thing interface{}, force bool) (string, error) { - // Request - url := "https://api2.arduino.cc/iot/v2/things" - bodyBuf := &bytes.Buffer{} - err := json.NewEncoder(bodyBuf).Encode(thing) +// AddThing adds a new thing on Arduino IoT Cloud. +func (cl *client) AddThing(thing *iotclient.Thing, force bool) (string, error) { + opt := &iotclient.ThingsV2CreateOpts{Force: optional.NewBool(force)} + newThing, resp, err := cl.api.ThingsV2Api.ThingsV2Create(cl.ctx, *thing, opt) if err != nil { - return "", err - } - if bodyBuf.Len() == 0 { - err = errors.New("invalid body type") - return "", err - } - - req, err := http.NewRequest(http.MethodPut, url, bodyBuf) - if err != nil { - return "", err - } - req.Header.Set("Content-Type", "application/json") - - var bearer = "Bearer " + cl.ctx.Value(iotclient.ContextAccessToken).(string) - req.Header.Add("Authorization", bearer) - - q := req.URL.Query() - q.Add("force", strconv.FormatBool(force)) - req.URL.RawQuery = q.Encode() - - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - err = fmt.Errorf("%s: %w", "adding new thing", err) - return "", err - } - - // Response - if resp.StatusCode != 201 { - // Get response error detail var respObj map[string]interface{} - err = json.NewDecoder(resp.Body).Decode(&respObj) - if err != nil { - return "", fmt.Errorf("%s: %s: %s", "cannot get response body", err, resp.Status) - } - return "", fmt.Errorf("%s: %s", "adding new thing", respObj["detail"].(string)) + json.NewDecoder(resp.Body).Decode(&respObj) + resp.Body.Close() + return "", fmt.Errorf("%s: %s: %v", "adding new thing", err, respObj) } - - if resp.Body == nil { - return "", errors.New("response body is empty") - } - defer resp.Body.Close() - - var newThing map[string]interface{} - err = json.NewDecoder(resp.Body).Decode(&newThing) - if err != nil { - err = fmt.Errorf("%s: %w", "cannot parse body response", err) - return "", err - } - newID, ok := newThing["id"].(string) - if !ok { - return "", errors.New("adding new thing: new thing created: returned id is not available") - } - return newID, nil + return newThing.Id, nil } +// GetThing allows to retrieve a specific thing, given its id, +// from Arduino IoT Cloud. func (cl *client) GetThing(id string) (*iotclient.ArduinoThing, error) { thing, _, err := cl.api.ThingsV2Api.ThingsV2Show(cl.ctx, id, nil) if err != nil { From 545d16561b6010593ea903b658abcc793b5be2ba Mon Sep 17 00:00:00 2001 From: Paolo Calao Date: Thu, 26 Aug 2021 13:59:27 +0200 Subject: [PATCH 4/6] Add optional --- go.mod | 1 + internal/iot/client.go | 1 + 2 files changed, 2 insertions(+) diff --git a/go.mod b/go.mod index 34c856d6..87b09bd8 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/arduino/iot-cloud-cli go 1.15 require ( + github.com/antihax/optional v1.0.0 // indirect github.com/arduino/arduino-cli v0.0.0-20210607095659-16f41352eac3 github.com/arduino/go-paths-helper v1.6.0 github.com/arduino/iot-client-go v1.3.4-0.20210824101852-4a44149473c1 diff --git a/internal/iot/client.go b/internal/iot/client.go index 02796fd2..c8ee311c 100644 --- a/internal/iot/client.go +++ b/internal/iot/client.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/antihax/optional" iotclient "github.com/arduino/iot-client-go" ) From a7f59f97c4b22d40e6bc90c2e66ae5333b3982e5 Mon Sep 17 00:00:00 2001 From: Paolo Calao Date: Thu, 26 Aug 2021 11:36:04 +0200 Subject: [PATCH 5/6] fix thing create - improve flags --- cli/thing/create.go | 12 ++++++------ command/thing/create.go | 16 ++++++++-------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/cli/thing/create.go b/cli/thing/create.go index f64f6888..1f48c44f 100644 --- a/cli/thing/create.go +++ b/cli/thing/create.go @@ -9,9 +9,9 @@ import ( var createFlags struct { name string - device string + deviceID string template string - clone string + cloneID string } func initCreateCommand() *cobra.Command { @@ -22,8 +22,8 @@ func initCreateCommand() *cobra.Command { RunE: runCreateCommand, } createCommand.Flags().StringVarP(&createFlags.name, "name", "n", "", "Thing name") - createCommand.Flags().StringVarP(&createFlags.device, "device", "d", "", "ID of Device to bind to the new thing") - createCommand.Flags().StringVarP(&createFlags.clone, "clone", "c", "", "ID of Thing to be cloned") + createCommand.Flags().StringVarP(&createFlags.deviceID, "device-id", "d", "", "ID of Device to bind to the new thing") + createCommand.Flags().StringVarP(&createFlags.cloneID, "clone-id", "c", "", "ID of Thing to be cloned") createCommand.Flags().StringVarP(&createFlags.template, "template", "t", "", "File containing a thing template") createCommand.MarkFlagRequired("name") return createCommand @@ -34,9 +34,9 @@ func runCreateCommand(cmd *cobra.Command, args []string) error { params := &thing.CreateParams{ Name: createFlags.name, - Device: createFlags.device, + DeviceID: createFlags.deviceID, Template: createFlags.template, - Clone: createFlags.clone, + CloneID: createFlags.cloneID, } thingID, err := thing.Create(params) diff --git a/command/thing/create.go b/command/thing/create.go index 0d536293..b4537154 100644 --- a/command/thing/create.go +++ b/command/thing/create.go @@ -18,16 +18,16 @@ type CreateParams struct { // Mandatory - contains the name of the thing Name string // Optional - contains the ID of the device to be bound to the thing - Device string + DeviceID string // Mandatory if device is empty - contains the path of the template file Template string // Mandatory if template is empty- name of things to be cloned - Clone string + CloneID string } // Create allows to create a new thing func Create(params *CreateParams) (string, error) { - if params.Template == "" && params.Clone == "" { + if params.Template == "" && params.CloneID == "" { return "", fmt.Errorf("%s", "provide either a thing(ID) to clone (--clone) or a thing template file (--template)\n") } @@ -42,8 +42,8 @@ func Create(params *CreateParams) (string, error) { var thing *iotclient.Thing - if params.Clone != "" { - thing, err = cloneThing(iotClient, params.Clone) + if params.CloneID != "" { + thing, err = clone(iotClient, params.CloneID) if err != nil { return "", err } @@ -60,8 +60,8 @@ func Create(params *CreateParams) (string, error) { thing.Name = params.Name force := true - if params.Device != "" { - thing.DeviceId = params.Device + if params.DeviceID != "" { + thing.DeviceId = params.DeviceID } thingID, err := iotClient.AddThing(thing, force) if err != nil { @@ -71,7 +71,7 @@ func Create(params *CreateParams) (string, error) { return thingID, nil } -func cloneThing(client iot.Client, thingID string) (*iotclient.Thing, error) { +func clone(client iot.Client, thingID string) (*iotclient.Thing, error) { clone, err := client.GetThing(thingID) if err != nil { return nil, fmt.Errorf("%s: %w", "retrieving the thing to be cloned", err) From 4e7348c8ff60e927d1c8b4335be868f2bc1fabe6 Mon Sep 17 00:00:00 2001 From: Paolo Calao Date: Thu, 26 Aug 2021 13:49:51 +0200 Subject: [PATCH 6/6] Update readme --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index fe9748e5..a412f297 100644 --- a/README.md +++ b/README.md @@ -40,3 +40,15 @@ 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: `$ iot-cloud-cli device list` + +## Thing commands + +Things can be created starting from a template or by cloning another thing. Additionally, a thing name should be specified. + +Create a thing from a template: + +`$ iot-cloud-cli thing create --name --template ` + +Create a thing by cloning another thing: + +`$ iot-cloud-cli thing create --name --clone-id `