Skip to content

Commit d9bcd10

Browse files
committed
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
1 parent 1fbe184 commit d9bcd10

File tree

5 files changed

+246
-0
lines changed

5 files changed

+246
-0
lines changed

cli/root.go

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/arduino/iot-cloud-cli/cli/config"
88
"github.com/arduino/iot-cloud-cli/cli/device"
99
"github.com/arduino/iot-cloud-cli/cli/ping"
10+
"github.com/arduino/iot-cloud-cli/cli/thing"
1011
"github.com/spf13/cobra"
1112
)
1213

@@ -15,6 +16,7 @@ func Execute() {
1516
rootCmd.AddCommand(ping.NewCommand())
1617
rootCmd.AddCommand(config.NewCommand())
1718
rootCmd.AddCommand(device.NewCommand())
19+
rootCmd.AddCommand(thing.NewCommand())
1820

1921
if err := rootCmd.Execute(); err != nil {
2022
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+
device string
13+
template string
14+
clone 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.device, "device", "d", "", "ID of Device to bind to the new thing")
26+
createCommand.Flags().StringVarP(&createFlags.clone, "clone", "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+
Device: createFlags.device,
38+
Template: createFlags.template,
39+
Clone: createFlags.clone,
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

+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package thing
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io/ioutil"
7+
"os"
8+
9+
"github.com/arduino/iot-cloud-cli/internal/config"
10+
"github.com/arduino/iot-cloud-cli/internal/iot"
11+
)
12+
13+
// CreateParams contains the parameters needed to create a new thing.
14+
type CreateParams struct {
15+
// Mandatory - contains the name of the thing
16+
Name string
17+
// Optional - contains the ID of the device to be bound to the thing
18+
Device string
19+
// Mandatory if device is empty - contains the path of the template file
20+
Template string
21+
// Mandatory if template is empty- name of things to be cloned
22+
Clone string
23+
}
24+
25+
// Create allows to create a new thing
26+
func Create(params *CreateParams) (string, error) {
27+
if params.Template == "" && params.Clone == "" {
28+
return "", fmt.Errorf("%s", "provide either a thing(ID) to clone (--clone) or a thing template file (--template)\n")
29+
}
30+
31+
conf, err := config.Retrieve()
32+
if err != nil {
33+
return "", err
34+
}
35+
iotClient, err := iot.NewClient(conf.Client, conf.Secret)
36+
if err != nil {
37+
return "", err
38+
}
39+
40+
var thing map[string]interface{}
41+
42+
if params.Clone != "" {
43+
thing, err = cloneThing(iotClient, params.Clone)
44+
if err != nil {
45+
return "", err
46+
}
47+
48+
} else if params.Template != "" {
49+
thing, err = loadTemplate(params.Template)
50+
if err != nil {
51+
return "", err
52+
}
53+
}
54+
55+
thing["name"] = params.Name
56+
force := true
57+
if params.Device != "" {
58+
thing["device_id"] = params.Device
59+
}
60+
thingID, err := iotClient.AddThing(thing, force)
61+
if err != nil {
62+
return "", err
63+
}
64+
65+
return thingID, nil
66+
}
67+
68+
func cloneThing(client iot.Client, thingID string) (map[string]interface{}, error) {
69+
clone, err := client.GetThing(thingID)
70+
if err != nil {
71+
return nil, fmt.Errorf("%s: %w", "retrieving the thing to be cloned", err)
72+
}
73+
74+
thing := make(map[string]interface{})
75+
thing["device_id"] = clone.DeviceId
76+
thing["properties"] = clone.Properties
77+
78+
return thing, nil
79+
}
80+
81+
func loadTemplate(file string) (map[string]interface{}, error) {
82+
templateFile, err := os.Open(file)
83+
if err != nil {
84+
return nil, err
85+
}
86+
defer templateFile.Close()
87+
88+
templateBytes, err := ioutil.ReadAll(templateFile)
89+
if err != nil {
90+
return nil, err
91+
}
92+
93+
var template map[string]interface{}
94+
err = json.Unmarshal([]byte(templateBytes), &template)
95+
if err != nil {
96+
return nil, fmt.Errorf("%s: %w", "reading template file: template not valid: ", err)
97+
}
98+
99+
return template, nil
100+
}

internal/iot/client.go

+78
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
package iot
22

33
import (
4+
"bytes"
45
"context"
6+
"encoding/json"
7+
"errors"
58
"fmt"
9+
"net/http"
10+
"strconv"
611

712
iotclient "github.com/arduino/iot-client-go"
813
)
@@ -11,6 +16,8 @@ import (
1116
type Client interface {
1217
AddDevice(fqbn, name, serial, devType string) (string, error)
1318
AddCertificate(id, csr string) (*iotclient.ArduinoCompressedv2, error)
19+
AddThing(thing interface{}, force bool) (string, error)
20+
GetThing(id string) (*iotclient.ArduinoThing, error)
1421
}
1522

1623
type client struct {
@@ -65,6 +72,77 @@ func (cl *client) AddCertificate(id, csr string) (*iotclient.ArduinoCompressedv2
6572
return &newCert.Compressed, nil
6673
}
6774

75+
func (cl *client) AddThing(thing interface{}, force bool) (string, error) {
76+
// Request
77+
url := "https://api2.arduino.cc/iot/v2/things"
78+
bodyBuf := &bytes.Buffer{}
79+
err := json.NewEncoder(bodyBuf).Encode(thing)
80+
if err != nil {
81+
return "", err
82+
}
83+
if bodyBuf.Len() == 0 {
84+
err = errors.New("invalid body type")
85+
return "", err
86+
}
87+
88+
req, err := http.NewRequest(http.MethodPut, url, bodyBuf)
89+
if err != nil {
90+
return "", err
91+
}
92+
req.Header.Set("Content-Type", "application/json")
93+
94+
var bearer = "Bearer " + cl.ctx.Value(iotclient.ContextAccessToken).(string)
95+
req.Header.Add("Authorization", bearer)
96+
97+
q := req.URL.Query()
98+
q.Add("force", strconv.FormatBool(force))
99+
req.URL.RawQuery = q.Encode()
100+
101+
client := &http.Client{}
102+
resp, err := client.Do(req)
103+
if err != nil {
104+
err = fmt.Errorf("%s: %w", "adding new thing", err)
105+
return "", err
106+
}
107+
108+
// Response
109+
if resp.StatusCode != 201 {
110+
// Get response error detail
111+
var respObj map[string]interface{}
112+
err = json.NewDecoder(resp.Body).Decode(&respObj)
113+
if err != nil {
114+
return "", fmt.Errorf("%s: %s: %s", "cannot get response body", err, resp.Status)
115+
}
116+
return "", fmt.Errorf("%s: %s", "adding new thing", respObj["detail"].(string))
117+
}
118+
119+
if resp.Body == nil {
120+
return "", errors.New("response body is empty")
121+
}
122+
defer resp.Body.Close()
123+
124+
var newThing map[string]interface{}
125+
err = json.NewDecoder(resp.Body).Decode(&newThing)
126+
if err != nil {
127+
err = fmt.Errorf("%s: %w", "cannot parse body response", err)
128+
return "", err
129+
}
130+
newID, ok := newThing["id"].(string)
131+
if !ok {
132+
return "", errors.New("adding new thing: new thing created: returned id is not available")
133+
}
134+
return newID, nil
135+
}
136+
137+
func (cl *client) GetThing(id string) (*iotclient.ArduinoThing, error) {
138+
thing, _, err := cl.api.ThingsV2Api.ThingsV2Show(cl.ctx, id, nil)
139+
if err != nil {
140+
err = fmt.Errorf("retrieving thing, %w", err)
141+
return nil, err
142+
}
143+
return &thing, nil
144+
}
145+
68146
func (cl *client) setup(client, secret string) error {
69147
// Get the access token in exchange of client_id and client_secret
70148
tok, err := token(client, secret)

0 commit comments

Comments
 (0)