• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mendersoftware / iot-manager / 1336988003

18 Jun 2024 09:38AM UTC coverage: 87.602%. Remained the same
1336988003

Pull #288

gitlab-ci

alfrunes
test(accpetance): Infer Docker compose service name from host

Remove hard-coded host name from config and actually use the `--host`
pytest config.

Signed-off-by: Alf-Rune Siqveland <alf.rune@northern.tech>
Pull Request #288: test(accpetance): Infer Docker compose service name from host

3229 of 3686 relevant lines covered (87.6%)

11.46 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.08
/client/iothub/client.go
1
// Copyright 2022 Northern.tech AS
2
//
3
//    Licensed under the Apache License, Version 2.0 (the "License");
4
//    you may not use this file except in compliance with the License.
5
//    You may obtain a copy of the License at
6
//
7
//        http://www.apache.org/licenses/LICENSE-2.0
8
//
9
//    Unless required by applicable law or agreed to in writing, software
10
//    distributed under the License is distributed on an "AS IS" BASIS,
11
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
//    See the License for the specific language governing permissions and
13
//    limitations under the License.
14

15
package iothub
16

17
import (
18
        "bytes"
19
        "context"
20
        "encoding/json"
21
        "fmt"
22
        "io"
23
        "net/http"
24
        "net/url"
25
        "strconv"
26
        "strings"
27
        "time"
28

29
        common "github.com/mendersoftware/iot-manager/client"
30
        "github.com/mendersoftware/iot-manager/model"
31

32
        "github.com/pkg/errors"
33
)
34

35
var (
36
        ErrNoCredentials = errors.New("no connection string configured for tenant")
37
)
38

39
const (
40
        uriTwin      = "/twins"
41
        uriDevices   = "/devices"
42
        uriQueryTwin = uriDevices + "/query"
43

44
        hdrKeyCount = "X-Ms-Max-Item-Count"
45

46
        // https://docs.microsoft.com/en-us/rest/api/iothub/service/devices
47
        APIVersion = "2021-04-12"
48
)
49

50
func uriDevice(id string) string {
35✔
51
        return uriDevices + "/" + url.QueryEscape(id)
35✔
52
}
35✔
53

54
const (
55
        defaultTTL = time.Minute
56
)
57

58
const (
59
        hdrKeyAuthorization = "Authorization"
60
)
61

62
//nolint:lll
63
//go:generate ../../utils/mockgen.sh
64
type Client interface {
65
        GetDeviceTwins(ctx context.Context, cs *model.ConnectionString, deviceIDs []string) ([]DeviceTwin, error)
66
        GetDeviceTwin(ctx context.Context, cs *model.ConnectionString, id string) (*DeviceTwin, error)
67
        UpdateDeviceTwin(ctx context.Context, cs *model.ConnectionString, id string, r *DeviceTwinUpdate) error
68

69
        GetDevice(ctx context.Context, cs *model.ConnectionString, id string) (*Device, error)
70
        // UpsertDevice create or update a device with the given ID. If a device
71
        // is created, the IoT Hub will generate a new 256-bit primary and
72
        // secondary key used to construct the device connection string:
73
        // primaryCS := &model.ConnectionString{
74
        //         HostName: cs.HostName,
75
        //         DeviceID: Device.DeviceID,
76
        //         Key:      Device.Auth.SymmetricKey.Primary,
77
        // }.String()
78
        // secondary := &model.ConnectionString{
79
        //         HostName: cs.HostName,
80
        //         DeviceID: Device.DeviceID,
81
        //         Key:      Device.Auth.SymmetricKey.Secondary,
82
        // }.String()
83
        UpsertDevice(ctx context.Context, cs *model.ConnectionString, id string, deviceUpdate ...*Device) (*Device, error)
84
        DeleteDevice(ctx context.Context, cs *model.ConnectionString, id string) error
85
}
86

87
type client struct {
88
        *http.Client
89
}
90

91
type Options struct {
92
        Client *http.Client
93
}
94

95
func NewOptions(opts ...*Options) *Options {
92✔
96
        opt := new(Options)
92✔
97
        for _, o := range opts {
156✔
98
                if o == nil {
82✔
99
                        continue
18✔
100
                }
101
                if o.Client != nil {
92✔
102
                        opt.Client = o.Client
46✔
103
                }
46✔
104
        }
105
        return opt
92✔
106
}
107

108
func (opt *Options) SetClient(client *http.Client) *Options {
46✔
109
        opt.Client = client
46✔
110
        return opt
46✔
111
}
46✔
112

113
func NewClient(options ...*Options) Client {
46✔
114
        opts := NewOptions(options...)
46✔
115
        if opts.Client == nil {
46✔
116
                opts.Client = new(http.Client)
×
117
        }
×
118
        // Make sure that we never follow redirects
119
        opts.Client.CheckRedirect = func(*http.Request, []*http.Request) error {
46✔
120
                return http.ErrUseLastResponse
×
121
        }
×
122
        return &client{
46✔
123
                Client: opts.Client,
46✔
124
        }
46✔
125
}
126

127
func (c *client) NewRequestWithContext(
128
        ctx context.Context,
129
        cs *model.ConnectionString,
130
        method, urlPath string,
131
        body io.Reader,
132
) (*http.Request, error) {
49✔
133
        if cs == nil {
49✔
134
                return nil, ErrNoCredentials
×
135
        } else if err := cs.Validate(); err != nil {
55✔
136
                return nil, errors.Wrap(err, "invalid connection string")
6✔
137
        }
6✔
138
        hostname := cs.HostName
43✔
139
        if cs.GatewayHostName != "" {
43✔
140
                hostname = cs.GatewayHostName
×
141
        }
×
142
        uri := "https://" + hostname + "/" +
43✔
143
                strings.TrimPrefix(urlPath, "/")
43✔
144
        if idx := strings.IndexRune(uri, '?'); idx < 0 {
86✔
145
                uri += "?"
43✔
146
        }
43✔
147
        uri += "api-version=" + APIVersion
43✔
148
        req, err := http.NewRequestWithContext(ctx, method, uri, body)
43✔
149
        if err != nil {
45✔
150
                return req, err
2✔
151
        }
2✔
152
        if body != nil {
64✔
153
                req.Header.Set(common.HdrKeyContentType, "application/json")
23✔
154
        }
23✔
155
        // Ensure that we set the correct Host header (in case GatewayHostName is set)
156
        req.Host = cs.HostName
41✔
157

41✔
158
        var expireAt time.Time
41✔
159
        if dl, ok := ctx.Deadline(); ok {
41✔
160
                expireAt = dl
×
161
        } else {
41✔
162
                expireAt = time.Now().Add(defaultTTL)
41✔
163
        }
41✔
164
        req.Header.Set(hdrKeyAuthorization, cs.Authorization(expireAt))
41✔
165

41✔
166
        return req, err
41✔
167
}
168

169
// GET /devices/{id}
170
func (c *client) GetDevice(
171
        ctx context.Context,
172
        cs *model.ConnectionString,
173
        id string,
174
) (*Device, error) {
12✔
175
        var dev = new(Device)
12✔
176
        req, err := c.NewRequestWithContext(
12✔
177
                ctx,
12✔
178
                cs,
12✔
179
                http.MethodGet,
12✔
180
                uriDevice(id),
12✔
181
                nil,
12✔
182
        )
12✔
183
        if err != nil {
14✔
184
                return nil, errors.Wrap(err, "iothub: failed to prepare request")
2✔
185
        }
2✔
186
        rsp, err := c.Do(req)
10✔
187
        if err != nil {
12✔
188
                return nil, errors.Wrap(err, "iothub: failed to execute request")
2✔
189
        }
2✔
190
        defer rsp.Body.Close()
8✔
191
        if rsp.StatusCode >= 400 {
10✔
192
                return nil, common.NewHTTPError(rsp.StatusCode)
2✔
193
        }
2✔
194
        dec := json.NewDecoder(rsp.Body)
6✔
195
        if err = dec.Decode(dev); err != nil {
8✔
196
                return nil, errors.Wrap(err, "iothub: failed to decode device")
2✔
197
        }
2✔
198
        return dev, nil
4✔
199
}
200

201
func (c *client) UpsertDevice(ctx context.Context,
202
        cs *model.ConnectionString,
203
        deviceID string,
204
        deviceUpdate ...*Device,
205
) (*Device, error) {
13✔
206
        dev := mergeDevices(deviceUpdate...)
13✔
207
        dev.DeviceID = deviceID
13✔
208
        etag := dev.ETag
13✔
209
        dev.ETag = ""
13✔
210
        b, _ := json.Marshal(*dev)
13✔
211
        req, err := c.NewRequestWithContext(
13✔
212
                ctx,
13✔
213
                cs,
13✔
214
                http.MethodPut,
13✔
215
                uriDevice(deviceID),
13✔
216
                bytes.NewReader(b),
13✔
217
        )
13✔
218
        if err != nil {
15✔
219
                return nil, errors.Wrap(err, "iothub: failed to prepare request")
2✔
220
        }
2✔
221
        if etag != "" {
13✔
222
                req.Header.Set("If-Match", `"`+etag+`"`)
2✔
223
        }
2✔
224
        rsp, err := c.Do(req)
11✔
225
        if err != nil {
13✔
226
                return nil, errors.Wrap(err, "iothub: failed to execute request")
2✔
227
        }
2✔
228
        defer rsp.Body.Close()
9✔
229
        if rsp.StatusCode >= 400 {
12✔
230
                return nil, common.NewHTTPError(rsp.StatusCode)
3✔
231
        }
3✔
232
        dec := json.NewDecoder(rsp.Body)
6✔
233
        if err = dec.Decode(dev); err != nil {
8✔
234
                return nil, errors.Wrap(err, "iothub: failed to decode updated device")
2✔
235
        }
2✔
236
        return dev, nil
4✔
237
}
238

239
func (c *client) DeleteDevice(ctx context.Context, cs *model.ConnectionString, id string) error {
10✔
240
        req, err := c.NewRequestWithContext(ctx,
10✔
241
                cs,
10✔
242
                http.MethodDelete,
10✔
243
                uriDevice(id),
10✔
244
                nil,
10✔
245
        )
10✔
246
        if err != nil {
12✔
247
                return errors.Wrap(err, "iothub: failed to prepare request")
2✔
248
        }
2✔
249
        req.Header.Set("If-Match", "*")
8✔
250
        rsp, err := c.Do(req)
8✔
251
        if err != nil {
10✔
252
                return errors.Wrap(err, "iothub: failed to execute request")
2✔
253
        }
2✔
254
        defer rsp.Body.Close()
6✔
255
        if rsp.StatusCode >= 400 {
8✔
256
                return common.NewHTTPError(rsp.StatusCode)
2✔
257
        }
2✔
258
        return nil
4✔
259
}
260

261
func (c *client) GetDeviceTwins(
262
        ctx context.Context, cs *model.ConnectionString, deviceIDs []string,
263
) ([]DeviceTwin, error) {
15✔
264
        if len(deviceIDs) == 0 {
17✔
265
                return []DeviceTwin{}, nil
2✔
266
        }
2✔
267
        SQLQuery := fmt.Sprintf(
13✔
268
                `{"query":"SELECT * FROM devices WHERE devices.deviceid IN ['%s']"}`,
13✔
269
                strings.Join(deviceIDs, "','"),
13✔
270
        )
13✔
271
        q := bytes.NewReader([]byte(SQLQuery))
13✔
272
        req, err := c.NewRequestWithContext(ctx, cs, http.MethodPost, uriQueryTwin, q)
13✔
273
        if err != nil {
15✔
274
                return nil, errors.Wrap(err, "iothub: failed to prepare request")
2✔
275
        }
2✔
276
        req.Header.Set(hdrKeyCount, strconv.Itoa(len(deviceIDs)))
11✔
277

11✔
278
        rsp, err := c.Do(req)
11✔
279
        if err != nil {
13✔
280
                return nil, errors.Wrap(err, "iothub: failed to fetch device twins")
2✔
281
        }
2✔
282
        defer rsp.Body.Close()
9✔
283
        if rsp.StatusCode >= 400 {
11✔
284
                return nil, common.NewHTTPError(rsp.StatusCode)
2✔
285
        }
2✔
286
        twins := make([]DeviceTwin, 0, len(deviceIDs))
7✔
287
        dec := json.NewDecoder(rsp.Body)
7✔
288
        if err = dec.Decode(&twins); err != nil {
9✔
289
                return nil, errors.Wrap(err, "iothub: failed to decode API response")
2✔
290
        }
2✔
291
        return twins, nil
5✔
292
}
293

294
func (c *client) GetDeviceTwin(
295
        ctx context.Context,
296
        cs *model.ConnectionString,
297
        id string,
298
) (*DeviceTwin, error) {
×
299
        uri := uriTwin + "/" + id
×
300
        req, err := c.NewRequestWithContext(ctx, cs, http.MethodGet, uri, nil)
×
301
        if err != nil {
×
302
                return nil, errors.Wrap(err, "iothub: failed to prepare request")
×
303
        }
×
304

305
        rsp, err := c.Do(req)
×
306
        if err != nil {
×
307
                return nil, errors.Wrap(err, "iothub: failed to fetch device twin")
×
308
        }
×
309
        defer rsp.Body.Close()
×
310
        if rsp.StatusCode >= 400 {
×
311
                return nil, common.NewHTTPError(rsp.StatusCode)
×
312
        }
×
313
        twin := new(DeviceTwin)
×
314
        dec := json.NewDecoder(rsp.Body)
×
315
        if err = dec.Decode(twin); err != nil {
×
316
                return nil, errors.Wrap(err, "iothub: failed to decode API response")
×
317
        }
×
318
        return twin, nil
×
319
}
320

321
func (c *client) UpdateDeviceTwin(
322
        ctx context.Context,
323
        cs *model.ConnectionString,
324
        id string,
325
        r *DeviceTwinUpdate,
326
) error {
1✔
327
        method := http.MethodPatch
1✔
328
        if r.Replace {
1✔
329
                method = http.MethodPut
×
330
        }
×
331

332
        b, _ := json.Marshal(r)
1✔
333

1✔
334
        req, err := c.NewRequestWithContext(ctx, cs, method, uriTwin+"/"+id, bytes.NewReader(b))
1✔
335
        if err != nil {
1✔
336
                return errors.Wrap(err, "iothub: failed to prepare request")
×
337
        }
×
338
        etag := r.ETag
1✔
339
        if etag != "" {
1✔
340
                req.Header.Set("If-Match", `"`+etag+`"`)
×
341
        }
×
342
        rsp, err := c.Do(req)
1✔
343
        if err != nil {
1✔
344
                return errors.Wrap(err, "iothub: failed to submit device twin update")
×
345
        }
×
346
        defer rsp.Body.Close()
1✔
347

1✔
348
        if rsp.StatusCode >= 400 {
1✔
349
                return common.NewHTTPError(rsp.StatusCode)
×
350
        }
×
351
        return nil
1✔
352
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc