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

mendersoftware / iot-manager / 1445709825

09 Sep 2024 11:23AM UTC coverage: 86.917% (-0.3%) from 87.172%
1445709825

Pull #303

gitlab-ci

alfrunes
ci: update gitlab runner

Moving to deprecated docker gitlab public runner, to a self-hosted runner

Ticket: SEC-1133
Changelog: none

Signed-off-by: Roberto Giovanardi <roberto.giovanardi@northern.tech>
(cherry picked from commit bb026f77c)
Pull Request #303: Cherry-pick MEN-7478 to 1.3.x (3.7.x)

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

128 existing lines in 10 files now uncovered.

3169 of 3646 relevant lines covered (86.92%)

9.75 hits per line

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

82.06
/client/iotcore/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 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 {
10✔
72
        return &client{}
10✔
73
}
10✔
74

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

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

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

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

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

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

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

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

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

153
        return device, err
26✔
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) {
10✔
162
        cfg, err := getAWSConfig(creds)
10✔
163
        if err != nil {
10✔
164
                return nil, err
×
165
        }
×
166
        svc := iot.NewFromConfig(*cfg)
10✔
167

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

181
                        if cert.CertificateDescription.Status != newStatus {
4✔
182
                                paramsUpdateCertificate := &iot.UpdateCertificateInput{
2✔
183
                                        CertificateId: aws.String(awsDevice.CertificateID),
2✔
184
                                        NewStatus:     types.CertificateStatus(newStatus),
2✔
185
                                }
2✔
186
                                _, err = svc.UpdateCertificate(ctx, paramsUpdateCertificate)
2✔
187
                        }
2✔
188
                }
189
                return awsDevice, err
2✔
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)
×
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)
×
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)
×
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 {
10✔
271
        cfg, err := getAWSConfig(creds)
10✔
272
        if err != nil {
10✔
UNCOV
273
                return err
×
UNCOV
274
        }
×
275
        svc := iot.NewFromConfig(*cfg)
10✔
276

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

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

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

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

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

342
        if err == nil {
18✔
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 {
12✔
351
                var notFoundErr *types.ResourceNotFoundException
2✔
352
                if errors.As(err, &notFoundErr) {
4✔
353
                        err = ErrDeviceNotFound
2✔
354
                }
2✔
355
                return err
2✔
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✔
UNCOV
368
                return nil, err
×
UNCOV
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✔
UNCOV
399
                return nil, err
×
UNCOV
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✔
UNCOV
412
                return nil, err
×
UNCOV
413
        }
×
414
        svc := iotdataplane.NewFromConfig(*cfg)
2✔
415
        payloadUpdate, err := json.Marshal(update)
2✔
416
        if err != nil {
2✔
UNCOV
417
                return nil, err
×
UNCOV
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✔
UNCOV
427
                var httpResponseErr *awshttp.ResponseError
×
UNCOV
428
                if errors.As(err, &httpResponseErr) {
×
UNCOV
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✔
UNCOV
438
                return nil, err
×
UNCOV
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