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

mendersoftware / iot-manager / 1422858057

22 Aug 2024 08:03AM UTC coverage: 87.172% (-0.4%) from 87.577%
1422858057

Pull #298

gitlab-ci

alfrunes
refac(iotcore): Break on errors instead of falling through

Using long chains of fallthrough error conditions makes it very
difficult to read and error prone to extend. Refactoring to use common
coding patterns instead.

Signed-off-by: Alf-Rune Siqveland <alf.rune@northern.tech>
Pull Request #298: fix(iot-core): Incosistent serialization format for device private key

42 of 54 new or added lines in 2 files covered. (77.78%)

6 existing lines in 1 file now uncovered.

3255 of 3734 relevant lines covered (87.17%)

11.38 hits per line

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

84.05
/client/iotcore/client.go
1
// Copyright 2024 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 iotcore
16

17
import (
18
        "context"
19
        "crypto/ecdsa"
20
        "encoding/json"
21
        "errors"
22
        "fmt"
23
        "net/http"
24
        "strings"
25
        "time"
26

27
        "github.com/aws/aws-sdk-go-v2/aws"
28
        awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http"
29
        "github.com/aws/aws-sdk-go-v2/config"
30
        "github.com/aws/aws-sdk-go-v2/credentials"
31
        "github.com/aws/aws-sdk-go-v2/service/iot"
32
        "github.com/aws/aws-sdk-go-v2/service/iot/types"
33
        "github.com/aws/aws-sdk-go-v2/service/iotdataplane"
34

35
        "github.com/mendersoftware/iot-manager/crypto"
36
        "github.com/mendersoftware/iot-manager/model"
37
)
38

39
var (
40
        ErrDeviceNotFound            = errors.New("device not found")
41
        ErrDeviceIncosistent         = errors.New("device is not consistent")
42
        ErrThingPrincipalNotDetached = errors.New(
43
                "giving up on waiting for Thing principal being detached")
44
)
45

46
const (
47
        endpointType = "iot:Data-ATS"
48
        // wait for Detach Thing Principal Operation
49
        // check 5 times every 2 seconds, which will give us 10s wait
50
        detachThingPrincipalWaitSleep      = 2 * time.Second
51
        detachThingPrincipalWaitMaxRetries = 4 // looks like 4, but it is 5, we're counting from 0
52
)
53

54
//nolint:lll
55
//go:generate ../../utils/mockgen.sh
56
type Client interface {
57
        GetDeviceShadow(ctx context.Context, creds model.AWSCredentials, id string) (*DeviceShadow, error)
58
        UpdateDeviceShadow(
59
                ctx context.Context,
60
                creds model.AWSCredentials,
61
                deviceID string,
62
                update DeviceShadowUpdate,
63
        ) (*DeviceShadow, error)
64
        GetDevice(ctx context.Context, creds model.AWSCredentials, deviceID string) (*Device, error)
65
        UpsertDevice(ctx context.Context, creds model.AWSCredentials, deviceID string, device *Device, policy string) (*Device, error)
66
        DeleteDevice(ctx context.Context, creds model.AWSCredentials, deviceID string) error
67
}
68

69
type client struct{}
70

71
func NewClient() Client {
11✔
72
        return &client{}
11✔
73
}
11✔
74

75
func getAWSConfig(creds model.AWSCredentials) (*aws.Config, error) {
88✔
76
        err := creds.Validate()
88✔
77
        if err != nil {
88✔
78
                return nil, err
×
79
        }
×
80

81
        appCreds := credentials.NewStaticCredentialsProvider(
88✔
82
                *creds.AccessKeyID,
88✔
83
                string(*creds.SecretAccessKey),
88✔
84
                "",
88✔
85
        )
88✔
86
        cfg, err := config.LoadDefaultConfig(context.TODO(),
88✔
87
                config.WithRegion(*creds.Region),
88✔
88
                config.WithCredentialsProvider(appCreds),
88✔
89
        )
88✔
90
        return &cfg, err
88✔
91
}
92

93
func (c *client) GetDevice(
94
        ctx context.Context,
95
        creds model.AWSCredentials,
96
        deviceID string,
97
) (*Device, error) {
56✔
98
        cfg, err := getAWSConfig(creds)
56✔
99
        if err != nil {
56✔
100
                return nil, err
×
101
        }
×
102
        svc := iot.NewFromConfig(*cfg)
56✔
103

56✔
104
        resp, err := svc.DescribeThing(ctx,
56✔
105
                &iot.DescribeThingInput{
56✔
106
                        ThingName: aws.String(deviceID),
56✔
107
                })
56✔
108

56✔
109
        var device *Device
56✔
110
        var respListThingPrincipals *iot.ListThingPrincipalsOutput
56✔
111
        if err == nil {
93✔
112
                device = &Device{
37✔
113
                        ID:      *resp.ThingId,
37✔
114
                        Name:    *resp.ThingName,
37✔
115
                        Version: resp.Version,
37✔
116
                        Status:  StatusDisabled,
37✔
117
                }
37✔
118
                respListThingPrincipals, err = svc.ListThingPrincipals(ctx,
37✔
119
                        &iot.ListThingPrincipalsInput{
37✔
120
                                ThingName: aws.String(deviceID),
37✔
121
                        })
37✔
122
        }
37✔
123

124
        if err == nil {
93✔
125
                if len(respListThingPrincipals.Principals) > 1 {
37✔
126
                        err = ErrDeviceIncosistent
×
127
                }
×
128
        }
129

130
        if err == nil {
93✔
131
                for _, principal := range respListThingPrincipals.Principals {
74✔
132
                        parts := strings.Split(principal, "/")
37✔
133
                        certificateID := parts[len(parts)-1]
37✔
134

37✔
135
                        cert, err := svc.DescribeCertificate(ctx, &iot.DescribeCertificateInput{
37✔
136
                                CertificateId: aws.String(certificateID),
37✔
137
                        })
37✔
138
                        if err != nil {
37✔
139
                                return nil, err
×
140
                        }
×
141
                        device.CertificateID = certificateID
37✔
142
                        if cert.CertificateDescription.Status == types.CertificateStatusActive {
53✔
143
                                device.Status = StatusEnabled
16✔
144
                        }
16✔
145
                }
146
        }
147

148
        var notFoundErr *types.ResourceNotFoundException
56✔
149
        if errors.As(err, &notFoundErr) {
74✔
150
                err = ErrDeviceNotFound
18✔
151
        }
18✔
152

153
        return device, err
56✔
154
}
155

156
func (c *client) UpsertDevice(ctx context.Context,
157
        creds model.AWSCredentials,
158
        deviceID string,
159
        device *Device,
160
        policy string,
161
) (*Device, error) {
12✔
162
        cfg, err := getAWSConfig(creds)
12✔
163
        if err != nil {
12✔
164
                return nil, err
×
165
        }
×
166
        svc := iot.NewFromConfig(*cfg)
12✔
167

12✔
168
        awsDevice, err := c.GetDevice(ctx, creds, deviceID)
12✔
169
        if err == nil && awsDevice != nil {
16✔
170
                cert, err := svc.DescribeCertificate(ctx, &iot.DescribeCertificateInput{
4✔
171
                        CertificateId: aws.String(awsDevice.CertificateID),
4✔
172
                })
4✔
173
                if err == nil {
8✔
174
                        newStatus := types.CertificateStatusInactive
4✔
175
                        awsDevice.Status = StatusDisabled
4✔
176
                        if device.Status == StatusEnabled {
7✔
177
                                newStatus = types.CertificateStatusActive
3✔
178
                                awsDevice.Status = StatusEnabled
3✔
179
                        }
3✔
180

181
                        if cert.CertificateDescription.Status != newStatus {
8✔
182
                                paramsUpdateCertificate := &iot.UpdateCertificateInput{
4✔
183
                                        CertificateId: aws.String(awsDevice.CertificateID),
4✔
184
                                        NewStatus:     types.CertificateStatus(newStatus),
4✔
185
                                }
4✔
186
                                _, err = svc.UpdateCertificate(ctx, paramsUpdateCertificate)
4✔
187
                        }
4✔
188
                }
189
                return awsDevice, err
4✔
190
        } else if !errors.Is(err, ErrDeviceNotFound) {
8✔
NEW
191
                return nil, fmt.Errorf("unexpected error getting the device: %w", err)
×
UNCOV
192
        }
×
193

194
        var privKey *ecdsa.PrivateKey
8✔
195
        privKey, err = crypto.NewPrivateKey()
8✔
196
        if err != nil {
8✔
NEW
197
                return nil, fmt.Errorf("failed to generate key for device: %w", err)
×
UNCOV
198
        }
×
199

200
        var csr []byte
8✔
201
        csr, err = crypto.NewCertificateSigningRequest(deviceID, privKey)
8✔
202
        if err != nil {
8✔
NEW
203
                return nil, fmt.Errorf("error creating certificate signing request: %w", err)
×
NEW
204
        }
×
205

206
        var resp *iot.CreateThingOutput
8✔
207
        resp, err = svc.CreateThing(ctx,
8✔
208
                &iot.CreateThingInput{
8✔
209
                        ThingName: aws.String(deviceID),
8✔
210
                })
8✔
211
        if err != nil {
8✔
NEW
212
                return nil, fmt.Errorf("failed to create Thing: %w", err)
×
UNCOV
213
        }
×
214

215
        var respCert *iot.CreateCertificateFromCsrOutput
8✔
216
        respCert, err = svc.CreateCertificateFromCsr(ctx,
8✔
217
                &iot.CreateCertificateFromCsrInput{
8✔
218
                        CertificateSigningRequest: aws.String(string(csr)),
8✔
219
                        SetAsActive:               *aws.Bool(device.Status == StatusEnabled),
8✔
220
                })
8✔
221
        if err != nil {
8✔
NEW
222
                return nil, err
×
UNCOV
223
        }
×
224

225
        endpointOutput, err := svc.DescribeEndpoint(ctx, &iot.DescribeEndpointInput{
8✔
226
                EndpointType: aws.String(endpointType),
8✔
227
        })
8✔
228
        if err != nil {
8✔
229
                return nil, err
×
230
        }
×
231

232
        _, err = svc.AttachPolicy(ctx,
8✔
233
                &iot.AttachPolicyInput{
8✔
234
                        PolicyName: aws.String(policy),
8✔
235
                        Target:     respCert.CertificateArn,
8✔
236
                })
8✔
237
        if err != nil {
8✔
NEW
238
                return nil, fmt.Errorf("failed to attach device certificate policy: %w", err)
×
UNCOV
239
        }
×
240

241
        _, err = svc.AttachThingPrincipal(ctx,
8✔
242
                &iot.AttachThingPrincipalInput{
8✔
243
                        Principal: respCert.CertificateArn,
8✔
244
                        ThingName: aws.String(deviceID),
8✔
245
                })
8✔
246
        if err != nil {
8✔
NEW
247
                return nil, fmt.Errorf("failed to attach thing principal: %w", err)
×
UNCOV
248
        }
×
249

250
        var deviceResp *Device
8✔
251
        pkeyPEM, err := crypto.PrivateKeyToPem(privKey)
8✔
252
        if err != nil {
8✔
NEW
253
                return nil, fmt.Errorf("failed to serialize private key: %w", err)
×
NEW
254
        }
×
255
        deviceResp = &Device{
8✔
256
                ID:          *resp.ThingId,
8✔
257
                Name:        *resp.ThingName,
8✔
258
                Status:      device.Status,
8✔
259
                PrivateKey:  string(pkeyPEM),
8✔
260
                Certificate: *respCert.CertificatePem,
8✔
261
                Endpoint:    endpointOutput.EndpointAddress,
8✔
262
        }
8✔
263
        return deviceResp, err
8✔
264
}
265

266
func (c *client) DeleteDevice(
267
        ctx context.Context,
268
        creds model.AWSCredentials,
269
        deviceID string,
270
) error {
12✔
271
        cfg, err := getAWSConfig(creds)
12✔
272
        if err != nil {
12✔
273
                return err
×
274
        }
×
275
        svc := iot.NewFromConfig(*cfg)
12✔
276

12✔
277
        respDescribe, err := svc.DescribeThing(ctx,
12✔
278
                &iot.DescribeThingInput{
12✔
279
                        ThingName: aws.String(deviceID),
12✔
280
                })
12✔
281

12✔
282
        var respListThingPrincipals *iot.ListThingPrincipalsOutput
12✔
283
        if err == nil {
21✔
284
                respListThingPrincipals, err = svc.ListThingPrincipals(ctx,
9✔
285
                        &iot.ListThingPrincipalsInput{
9✔
286
                                ThingName: aws.String(deviceID),
9✔
287
                        })
9✔
288
        }
9✔
289

290
        if err == nil {
21✔
291
                for _, principal := range respListThingPrincipals.Principals {
18✔
292
                        _, err := svc.DetachThingPrincipal(ctx,
9✔
293
                                &iot.DetachThingPrincipalInput{
9✔
294
                                        Principal: aws.String(principal),
9✔
295
                                        ThingName: aws.String(deviceID),
9✔
296
                                })
9✔
297
                        var certificateID string
9✔
298
                        if err == nil {
18✔
299
                                parts := strings.SplitAfter(principal, "/")
9✔
300
                                certificateID = parts[len(parts)-1]
9✔
301

9✔
302
                                _, err = svc.UpdateCertificate(ctx,
9✔
303
                                        &iot.UpdateCertificateInput{
9✔
304
                                                CertificateId: aws.String(certificateID),
9✔
305
                                                NewStatus:     types.CertificateStatusInactive,
9✔
306
                                        })
9✔
307
                        }
9✔
308
                        if err == nil {
18✔
309
                                _, err = svc.DeleteCertificate(ctx,
9✔
310
                                        &iot.DeleteCertificateInput{
9✔
311
                                                CertificateId: aws.String(certificateID),
9✔
312
                                                ForceDelete:   *aws.Bool(true),
9✔
313
                                        })
9✔
314
                        }
9✔
315
                        if err != nil {
9✔
316
                                break
×
317
                        }
318
                }
319
        }
320

321
        if err == nil && len(respListThingPrincipals.Principals) > 0 {
21✔
322
                // wait for DetachThingPrincipal operation to complete
9✔
323
                // this operation is asynchronous, so wait couple of seconds
9✔
324
                for retries := 0; retries <= detachThingPrincipalWaitMaxRetries; retries++ {
54✔
325
                        respListThingPrincipals, err = svc.ListThingPrincipals(ctx,
45✔
326
                                &iot.ListThingPrincipalsInput{
45✔
327
                                        ThingName: aws.String(deviceID),
45✔
328
                                })
45✔
329
                        if err != nil {
45✔
330
                                break
×
331
                        }
332
                        if len(respListThingPrincipals.Principals) > 0 {
50✔
333
                                time.Sleep(detachThingPrincipalWaitSleep)
5✔
334
                        }
5✔
335
                }
336
                // Thing Principle still not detached; return error
337
                if respListThingPrincipals != nil && len(respListThingPrincipals.Principals) > 0 {
10✔
338
                        return ErrThingPrincipalNotDetached
1✔
339
                }
1✔
340
        }
341

342
        if err == nil {
19✔
343
                _, err = svc.DeleteThing(ctx,
8✔
344
                        &iot.DeleteThingInput{
8✔
345
                                ThingName:       aws.String(deviceID),
8✔
346
                                ExpectedVersion: aws.Int64(respDescribe.Version),
8✔
347
                        })
8✔
348
        }
8✔
349

350
        if err != nil {
14✔
351
                var notFoundErr *types.ResourceNotFoundException
3✔
352
                if errors.As(err, &notFoundErr) {
5✔
353
                        err = ErrDeviceNotFound
2✔
354
                }
2✔
355
                return err
3✔
356
        }
357

358
        return err
8✔
359
}
360

361
func (c *client) GetDeviceShadow(
362
        ctx context.Context,
363
        creds model.AWSCredentials,
364
        deviceID string,
365
) (*DeviceShadow, error) {
6✔
366
        cfg, err := getAWSConfig(creds)
6✔
367
        if err != nil {
6✔
368
                return nil, err
×
369
        }
×
370
        svc := iotdataplane.NewFromConfig(*cfg)
6✔
371
        shadow, err := svc.GetThingShadow(
6✔
372
                ctx,
6✔
373
                &iotdataplane.GetThingShadowInput{
6✔
374
                        ThingName: aws.String(deviceID),
6✔
375
                },
6✔
376
        )
6✔
377
        if err != nil {
10✔
378
                var httpResponseErr *awshttp.ResponseError
4✔
379
                if errors.As(err, &httpResponseErr) {
8✔
380
                        if httpResponseErr.HTTPStatusCode() == http.StatusNotFound {
8✔
381
                                _, errDevice := c.GetDevice(ctx, creds, deviceID)
4✔
382
                                if errDevice == ErrDeviceNotFound {
6✔
383
                                        err = ErrDeviceNotFound
2✔
384
                                } else {
4✔
385
                                        return &DeviceShadow{
2✔
386
                                                Payload: model.DeviceState{
2✔
387
                                                        Desired:  map[string]interface{}{},
2✔
388
                                                        Reported: make(map[string]interface{}),
2✔
389
                                                },
2✔
390
                                        }, nil
2✔
391
                                }
2✔
392
                        }
393
                }
394
                return nil, err
2✔
395
        }
396
        var devShadow DeviceShadow
2✔
397
        err = json.Unmarshal(shadow.Payload, &devShadow)
2✔
398
        if err != nil {
2✔
399
                return nil, err
×
400
        }
×
401
        return &devShadow, nil
2✔
402
}
403

404
func (c *client) UpdateDeviceShadow(
405
        ctx context.Context,
406
        creds model.AWSCredentials,
407
        deviceID string,
408
        update DeviceShadowUpdate,
409
) (*DeviceShadow, error) {
2✔
410
        cfg, err := getAWSConfig(creds)
2✔
411
        if err != nil {
2✔
412
                return nil, err
×
413
        }
×
414
        svc := iotdataplane.NewFromConfig(*cfg)
2✔
415
        payloadUpdate, err := json.Marshal(update)
2✔
416
        if err != nil {
2✔
417
                return nil, err
×
418
        }
×
419
        updated, err := svc.UpdateThingShadow(
2✔
420
                ctx,
2✔
421
                &iotdataplane.UpdateThingShadowInput{
2✔
422
                        Payload:   payloadUpdate,
2✔
423
                        ThingName: aws.String(deviceID),
2✔
424
                },
2✔
425
        )
2✔
426
        if err != nil {
2✔
427
                var httpResponseErr *awshttp.ResponseError
×
428
                if errors.As(err, &httpResponseErr) {
×
429
                        if httpResponseErr.HTTPStatusCode() == http.StatusNotFound {
×
430
                                err = ErrDeviceNotFound
×
431
                        }
×
432
                }
433
                return nil, err
×
434
        }
435
        var shadow DeviceShadow
2✔
436
        err = json.Unmarshal(updated.Payload, &shadow)
2✔
437
        if err != nil {
2✔
438
                return nil, err
×
439
        }
×
440
        return &shadow, nil
2✔
441
}
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