• 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

88.51
/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
        "net/http"
23
        "strings"
24
        "time"
25

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

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

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

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

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

68
type client struct{}
69

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

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

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

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

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

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

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

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

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

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

152
        return device, err
56✔
153
}
154

155
func (c *client) UpsertDevice(ctx context.Context,
156
        creds model.AWSCredentials,
157
        deviceID string,
158
        device *Device,
159
        policy string,
160
) (*Device, error) {
12✔
161

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

190
                return awsDevice, err
4✔
191
        } else if err == ErrDeviceNotFound {
16✔
192
                err = nil
8✔
193
        }
8✔
194

195
        var resp *iot.CreateThingOutput
8✔
196
        if err == nil {
16✔
197
                resp, err = svc.CreateThing(ctx,
8✔
198
                        &iot.CreateThingInput{
8✔
199
                                ThingName: aws.String(deviceID),
8✔
200
                        })
8✔
201
        }
8✔
202

203
        var privKey *ecdsa.PrivateKey
8✔
204
        if err == nil {
16✔
205
                privKey, err = crypto.NewPrivateKey()
8✔
206
        }
8✔
207

208
        var csr []byte
8✔
209
        if err == nil {
16✔
210
                csr, err = crypto.NewCertificateSigningRequest(deviceID, privKey)
8✔
211
        }
8✔
212

213
        var respCert *iot.CreateCertificateFromCsrOutput
8✔
214
        if err == nil {
16✔
215
                respCert, err = svc.CreateCertificateFromCsr(ctx,
8✔
216
                        &iot.CreateCertificateFromCsrInput{
8✔
217
                                CertificateSigningRequest: aws.String(string(csr)),
8✔
218
                                SetAsActive:               *aws.Bool(device.Status == StatusEnabled),
8✔
219
                        })
8✔
220
                if err != nil {
8✔
221
                        return nil, err
×
222
                }
×
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
        if err == nil {
16✔
233
                _, err = svc.AttachPolicy(ctx,
8✔
234
                        &iot.AttachPolicyInput{
8✔
235
                                PolicyName: aws.String(policy),
8✔
236
                                Target:     respCert.CertificateArn,
8✔
237
                        })
8✔
238
        }
8✔
239

240
        if err == nil {
16✔
241
                _, err = svc.AttachThingPrincipal(ctx,
8✔
242
                        &iot.AttachThingPrincipalInput{
8✔
243
                                Principal: respCert.CertificateArn,
8✔
244
                                ThingName: aws.String(deviceID),
8✔
245
                        })
8✔
246
        }
8✔
247

248
        var deviceResp *Device
8✔
249
        if err == nil {
16✔
250
                deviceResp = &Device{
8✔
251
                        ID:          *resp.ThingId,
8✔
252
                        Name:        *resp.ThingName,
8✔
253
                        Status:      device.Status,
8✔
254
                        PrivateKey:  string(crypto.PrivateKeyToPem(privKey)),
8✔
255
                        Certificate: *respCert.CertificatePem,
8✔
256
                        Endpoint:    endpointOutput.EndpointAddress,
8✔
257
                }
8✔
258
        }
8✔
259
        return deviceResp, err
8✔
260
}
261

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

12✔
273
        respDescribe, err := svc.DescribeThing(ctx,
12✔
274
                &iot.DescribeThingInput{
12✔
275
                        ThingName: aws.String(deviceID),
12✔
276
                })
12✔
277

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

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

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

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

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

346
        if err != nil {
14✔
347
                var notFoundErr *types.ResourceNotFoundException
3✔
348
                if errors.As(err, &notFoundErr) {
5✔
349
                        err = ErrDeviceNotFound
2✔
350
                }
2✔
351
                return err
3✔
352
        }
353

354
        return err
8✔
355
}
356

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

399
}
400

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

439
}
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