Skip to content

Commit 626cc19

Browse files
authored
Add thing create command (#20)
things can be created in two ways: - starting from a template. templates can be extracted from existing things - cloning another thing, already available on arduino iot cloud 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 commits history: * Add thing create command * Check clone params before copying * 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 * Add optional * fix thing create - improve flags * Update readme
1 parent a3d0421 commit 626cc19

File tree

8 files changed

+249
-11
lines changed

8 files changed

+249
-11
lines changed

README.md

+12
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,15 @@ Once a device has been created thorugh the provisioning procedure, it can be del
4040

4141
Devices currently present on Arduino IoT Cloud can be retrieved by using this command:
4242
`$ iot-cloud-cli device list`
43+
44+
## Thing commands
45+
46+
Things can be created starting from a template or by cloning another thing. Additionally, a thing name should be specified.
47+
48+
Create a thing from a template:
49+
50+
`$ iot-cloud-cli thing create --name <thingName> --template <template.json>`
51+
52+
Create a thing by cloning another thing:
53+
54+
`$ iot-cloud-cli thing create --name <thingName> --clone-id <thingToCloneID>`

cli/root.go

+2
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ import (
66

77
"github.com/arduino/iot-cloud-cli/cli/config"
88
"github.com/arduino/iot-cloud-cli/cli/device"
9+
"github.com/arduino/iot-cloud-cli/cli/thing"
910
"github.com/spf13/cobra"
1011
)
1112

1213
func Execute() {
1314
rootCmd := &cobra.Command{}
1415
rootCmd.AddCommand(config.NewCommand())
1516
rootCmd.AddCommand(device.NewCommand())
17+
rootCmd.AddCommand(thing.NewCommand())
1618

1719
if err := rootCmd.Execute(); err != nil {
1820
fmt.Fprintln(os.Stderr, err)

cli/thing/create.go

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package thing
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/arduino/iot-cloud-cli/command/thing"
7+
"github.com/spf13/cobra"
8+
)
9+
10+
var createFlags struct {
11+
name string
12+
deviceID string
13+
template string
14+
cloneID string
15+
}
16+
17+
func initCreateCommand() *cobra.Command {
18+
createCommand := &cobra.Command{
19+
Use: "create",
20+
Short: "Create a thing",
21+
Long: "Create a thing for Arduino IoT Cloud",
22+
RunE: runCreateCommand,
23+
}
24+
createCommand.Flags().StringVarP(&createFlags.name, "name", "n", "", "Thing name")
25+
createCommand.Flags().StringVarP(&createFlags.deviceID, "device-id", "d", "", "ID of Device to bind to the new thing")
26+
createCommand.Flags().StringVarP(&createFlags.cloneID, "clone-id", "c", "", "ID of Thing to be cloned")
27+
createCommand.Flags().StringVarP(&createFlags.template, "template", "t", "", "File containing a thing template")
28+
createCommand.MarkFlagRequired("name")
29+
return createCommand
30+
}
31+
32+
func runCreateCommand(cmd *cobra.Command, args []string) error {
33+
fmt.Printf("Creating thing with name %s\n", createFlags.name)
34+
35+
params := &thing.CreateParams{
36+
Name: createFlags.name,
37+
DeviceID: createFlags.deviceID,
38+
Template: createFlags.template,
39+
CloneID: createFlags.cloneID,
40+
}
41+
42+
thingID, err := thing.Create(params)
43+
if err != nil {
44+
return err
45+
}
46+
47+
fmt.Printf("IoT Cloud thing created with ID: %s\n", thingID)
48+
return nil
49+
}

cli/thing/thing.go

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package thing
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
)
6+
7+
func NewCommand() *cobra.Command {
8+
thingCommand := &cobra.Command{
9+
Use: "thing",
10+
Short: "Thing commands.",
11+
Long: "Thing commands.",
12+
}
13+
14+
thingCommand.AddCommand(initCreateCommand())
15+
16+
return thingCommand
17+
}

command/thing/create.go

+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package thing
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io/ioutil"
7+
"os"
8+
9+
"errors"
10+
11+
iotclient "github.com/arduino/iot-client-go"
12+
"github.com/arduino/iot-cloud-cli/internal/config"
13+
"github.com/arduino/iot-cloud-cli/internal/iot"
14+
)
15+
16+
// CreateParams contains the parameters needed to create a new thing.
17+
type CreateParams struct {
18+
// Mandatory - contains the name of the thing
19+
Name string
20+
// Optional - contains the ID of the device to be bound to the thing
21+
DeviceID string
22+
// Mandatory if device is empty - contains the path of the template file
23+
Template string
24+
// Mandatory if template is empty- name of things to be cloned
25+
CloneID string
26+
}
27+
28+
// Create allows to create a new thing
29+
func Create(params *CreateParams) (string, error) {
30+
if params.Template == "" && params.CloneID == "" {
31+
return "", fmt.Errorf("%s", "provide either a thing(ID) to clone (--clone) or a thing template file (--template)\n")
32+
}
33+
34+
conf, err := config.Retrieve()
35+
if err != nil {
36+
return "", err
37+
}
38+
iotClient, err := iot.NewClient(conf.Client, conf.Secret)
39+
if err != nil {
40+
return "", err
41+
}
42+
43+
var thing *iotclient.Thing
44+
45+
if params.CloneID != "" {
46+
thing, err = clone(iotClient, params.CloneID)
47+
if err != nil {
48+
return "", err
49+
}
50+
51+
} else if params.Template != "" {
52+
thing, err = loadTemplate(params.Template)
53+
if err != nil {
54+
return "", err
55+
}
56+
57+
} else {
58+
return "", errors.New("provide either a thing(ID) to clone (--clone) or a thing template file (--template)")
59+
}
60+
61+
thing.Name = params.Name
62+
force := true
63+
if params.DeviceID != "" {
64+
thing.DeviceId = params.DeviceID
65+
}
66+
thingID, err := iotClient.AddThing(thing, force)
67+
if err != nil {
68+
return "", err
69+
}
70+
71+
return thingID, nil
72+
}
73+
74+
func clone(client iot.Client, thingID string) (*iotclient.Thing, error) {
75+
clone, err := client.GetThing(thingID)
76+
if err != nil {
77+
return nil, fmt.Errorf("%s: %w", "retrieving the thing to be cloned", err)
78+
}
79+
80+
thing := &iotclient.Thing{}
81+
82+
// Copy device id
83+
if clone.DeviceId != "" {
84+
thing.DeviceId = clone.DeviceId
85+
}
86+
87+
// Copy properties
88+
for _, p := range clone.Properties {
89+
thing.Properties = append(thing.Properties, iotclient.Property{
90+
Name: p.Name,
91+
MinValue: p.MinValue,
92+
MaxValue: p.MaxValue,
93+
Permission: p.Permission,
94+
UpdateParameter: p.UpdateParameter,
95+
UpdateStrategy: p.UpdateStrategy,
96+
Type: p.Type,
97+
VariableName: p.VariableName,
98+
Persist: p.Persist,
99+
Tag: p.Tag,
100+
})
101+
}
102+
103+
return thing, nil
104+
}
105+
106+
func loadTemplate(file string) (*iotclient.Thing, error) {
107+
templateFile, err := os.Open(file)
108+
if err != nil {
109+
return nil, err
110+
}
111+
defer templateFile.Close()
112+
113+
templateBytes, err := ioutil.ReadAll(templateFile)
114+
if err != nil {
115+
return nil, err
116+
}
117+
118+
thing := &iotclient.Thing{}
119+
err = json.Unmarshal([]byte(templateBytes), thing)
120+
if err != nil {
121+
return nil, fmt.Errorf("%s: %w", "reading template file: template not valid: ", err)
122+
}
123+
124+
return thing, nil
125+
}

go.mod

+6-3
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,21 @@ module github.com/arduino/iot-cloud-cli
33
go 1.15
44

55
require (
6+
github.com/antihax/optional v1.0.0 // indirect
67
github.com/arduino/arduino-cli v0.0.0-20210607095659-16f41352eac3
78
github.com/arduino/go-paths-helper v1.6.0
8-
github.com/arduino/iot-client-go v1.3.3
9+
github.com/arduino/iot-client-go v1.3.4-0.20210824101852-4a44149473c1
910
github.com/howeyc/crc16 v0.0.0-20171223171357-2b2a61e366a6
1011
github.com/sirupsen/logrus v1.4.2
1112
github.com/spf13/cobra v1.1.3
1213
github.com/spf13/viper v1.7.0
1314
github.com/stretchr/testify v1.6.1
1415
go.bug.st/serial v1.3.0
15-
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125 // indirect
16-
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914
16+
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d // indirect
17+
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f
1718
golang.org/x/sys v0.0.0-20210503173754-0981d6026fa6 // indirect
19+
google.golang.org/appengine v1.6.7 // indirect
1820
google.golang.org/genproto v0.0.0-20210504143626-3b2ad6ccc450 // indirect
1921
google.golang.org/grpc v1.39.0
22+
google.golang.org/protobuf v1.27.1 // indirect
2023
)

go.sum

+10-8
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ github.com/arduino/go-properties-orderedmap v1.3.0/go.mod h1:DKjD2VXY/NZmlingh4l
5959
github.com/arduino/go-timeutils v0.0.0-20171220113728-d1dd9e313b1b/go.mod h1:uwGy5PpN4lqW97FiLnbcx+xx8jly5YuPMJWfVwwjJiQ=
6060
github.com/arduino/go-win32-utils v0.0.0-20180330194947-ed041402e83b h1:3PjgYG5gVPA7cipp7vIR2lF96KkEJIFBJ+ANnuv6J20=
6161
github.com/arduino/go-win32-utils v0.0.0-20180330194947-ed041402e83b/go.mod h1:iIPnclBMYm1g32Q5kXoqng4jLhMStReIP7ZxaoUC2y8=
62-
github.com/arduino/iot-client-go v1.3.3 h1:W+92osS+WcdVpePdPmj/BtupM+xV6DOJlI0HGpKrTX4=
63-
github.com/arduino/iot-client-go v1.3.3/go.mod h1:gYvpMt7Qw+OSScTLyIlCnpbvy9y96ey/2zhB4w6FoK0=
62+
github.com/arduino/iot-client-go v1.3.4-0.20210824101852-4a44149473c1 h1:tgVUBPbqkyd3KHTs+gweP5t9KAnkLbAsAMrHvu9jZSg=
63+
github.com/arduino/iot-client-go v1.3.4-0.20210824101852-4a44149473c1/go.mod h1:gYvpMt7Qw+OSScTLyIlCnpbvy9y96ey/2zhB4w6FoK0=
6464
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
6565
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
6666
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
494494
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
495495
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
496496
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
497-
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125 h1:Ugb8sMTWuWRC3+sz5WeN/4kejDx9BvIwnPUiJBjJE+8=
498-
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
497+
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d h1:LO7XpTYMwTqxjLcGWPijK3vRXg1aWdlNOVOHRq45d7c=
498+
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
499499
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
500500
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
501501
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
502502
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
503503
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
504-
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914 h1:3B43BWw0xEBsLZ/NO1VALz6fppU3481pik+2Ksv45z8=
505-
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
504+
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f h1:Qmd2pbz05z7z6lm0DrgQVVPuBm92jqujBKMHMOlOQEw=
505+
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
506506
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
507507
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
508508
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
638638
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
639639
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
640640
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
641-
google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
642641
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
642+
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
643+
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
643644
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
644645
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
645646
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
701702
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
702703
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
703704
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
704-
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
705705
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
706+
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
707+
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
706708
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
707709
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
708710
gopkg.in/check.v1 v1.0.0-20160105164936-4f90aeace3a2/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

internal/iot/client.go

+28
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package iot
22

33
import (
44
"context"
5+
"encoding/json"
56
"fmt"
67

8+
"github.com/antihax/optional"
79
iotclient "github.com/arduino/iot-client-go"
810
)
911

@@ -13,6 +15,8 @@ type Client interface {
1315
DeleteDevice(id string) error
1416
ListDevices() ([]iotclient.ArduinoDevicev2, error)
1517
AddCertificate(id, csr string) (*iotclient.ArduinoCompressedv2, error)
18+
AddThing(thing *iotclient.Thing, force bool) (string, error)
19+
GetThing(id string) (*iotclient.ArduinoThing, error)
1620
}
1721

1822
type client struct {
@@ -89,6 +93,30 @@ func (cl *client) AddCertificate(id, csr string) (*iotclient.ArduinoCompressedv2
8993
return &newCert.Compressed, nil
9094
}
9195

96+
// AddThing adds a new thing on Arduino IoT Cloud.
97+
func (cl *client) AddThing(thing *iotclient.Thing, force bool) (string, error) {
98+
opt := &iotclient.ThingsV2CreateOpts{Force: optional.NewBool(force)}
99+
newThing, resp, err := cl.api.ThingsV2Api.ThingsV2Create(cl.ctx, *thing, opt)
100+
if err != nil {
101+
var respObj map[string]interface{}
102+
json.NewDecoder(resp.Body).Decode(&respObj)
103+
resp.Body.Close()
104+
return "", fmt.Errorf("%s: %s: %v", "adding new thing", err, respObj)
105+
}
106+
return newThing.Id, nil
107+
}
108+
109+
// GetThing allows to retrieve a specific thing, given its id,
110+
// from Arduino IoT Cloud.
111+
func (cl *client) GetThing(id string) (*iotclient.ArduinoThing, error) {
112+
thing, _, err := cl.api.ThingsV2Api.ThingsV2Show(cl.ctx, id, nil)
113+
if err != nil {
114+
err = fmt.Errorf("retrieving thing, %w", err)
115+
return nil, err
116+
}
117+
return &thing, nil
118+
}
119+
92120
func (cl *client) setup(client, secret string) error {
93121
// Get the access token in exchange of client_id and client_secret
94122
tok, err := token(client, secret)

0 commit comments

Comments
 (0)