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

mendersoftware / reporting / 815512710

pending completion
815512710

Pull #121

gitlab-ci

Alf-Rune Siqveland
chore: Gracefully shut down indexer on SIGINT/SIGTERM
Pull Request #121: fix: Prevent overflowing the per_page parameter on device deployment requests

63 of 73 new or added lines in 2 files covered. (86.3%)

7 existing lines in 2 files now uncovered.

2808 of 3294 relevant lines covered (85.25%)

17.49 hits per line

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

98.25
/client/deployments/client.go
1
// Copyright 2023 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 deployments
16

17
import (
18
        "context"
19
        "encoding/json"
20
        "io"
21
        "net/http"
22
        "strconv"
23
        "strings"
24
        "time"
25

26
        "github.com/pkg/errors"
27

28
        "github.com/mendersoftware/go-lib-micro/log"
29

30
        "github.com/mendersoftware/reporting/utils"
31
)
32

33
const (
34
        urlDeviceDeployments   = "/api/internal/v1/deployments/tenants/:tid/deployments/devices"
35
        urlDeviceDeploymentsID = urlDeviceDeployments + "/:id"
36
        defaultTimeout         = 10 * time.Second
37
)
38

39
//go:generate ../../x/mockgen.sh
40
type Client interface {
41
        // GetDeployments retrieves a list of deployments by ID
42
        GetDeployments(
43
                ctx context.Context,
44
                tenantID string,
45
                IDs []string,
46
        ) ([]*DeviceDeployment, error)
47
        // GetLatestDeployment retrieves the latest deployment for a given device
48
        GetLatestFinishedDeployment(
49
                ctx context.Context,
50
                tenantID string,
51
                deviceID string,
52
        ) (*DeviceDeployment, error)
53
}
54

55
type client struct {
56
        client  *http.Client
57
        urlBase string
58
}
59

60
func NewClient(urlBase string) Client {
31✔
61
        return &client{
31✔
62
                client:  &http.Client{},
31✔
63
                urlBase: urlBase,
31✔
64
        }
31✔
65
}
31✔
66

67
func (c *client) GetDeployments(
68
        ctx context.Context,
69
        tenantID string,
70
        IDs []string,
71
) ([]*DeviceDeployment, error) {
18✔
72
        const maxDeploymentIDs = 20 // API constraint
18✔
73
        l := log.FromContext(ctx)
18✔
74

18✔
75
        url := utils.JoinURL(c.urlBase, urlDeviceDeployments)
18✔
76
        url = strings.Replace(url, ":tid", tenantID, 1)
18✔
77

18✔
78
        ctx, cancel := context.WithTimeout(ctx, defaultTimeout)
18✔
79
        defer cancel()
18✔
80

18✔
81
        req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
18✔
82
        if err != nil {
20✔
83
                return nil, errors.Wrapf(err, "failed to create request")
2✔
84
        }
2✔
85

86
        var (
16✔
87
                i, j    int
16✔
88
                body    io.ReadCloser
16✔
89
                devDevs []*DeviceDeployment
16✔
90
                rsp     *http.Response
16✔
91
        )
16✔
92
        defer func() {
32✔
93
                if body != nil {
16✔
NEW
94
                        body.Close()
×
NEW
95
                }
×
96
        }()
97

98
        for i < len(IDs) && err == nil {
34✔
99
                j += maxDeploymentIDs
18✔
100
                if j > len(IDs) {
34✔
101
                        j = len(IDs)
16✔
102
                }
16✔
103
                q := req.URL.Query()
18✔
104
                q.Set("page", "1")
18✔
105
                q.Set("per_page", strconv.Itoa(j-i))
18✔
106
                q.Del("id")
18✔
107
                for k := i; k < j; k++ {
76✔
108
                        q.Add("id", IDs[k])
58✔
109
                }
58✔
110
                req.URL.RawQuery = q.Encode()
18✔
111
                rsp, err = c.client.Do(req) //nolint:bodyclose
18✔
112
                if err != nil {
20✔
113
                        err = errors.Wrapf(err, "failed to submit %s %s", req.Method, req.URL)
2✔
114
                        break
2✔
115
                }
116
                body = rsp.Body
16✔
117

16✔
118
                switch rsp.StatusCode {
16✔
119
                case http.StatusNotFound:
2✔
120
                        // pass
121
                case http.StatusOK:
12✔
122
                        dec := json.NewDecoder(rsp.Body)
12✔
123
                        var batch []*DeviceDeployment
12✔
124
                        if err = dec.Decode(&batch); err != nil {
14✔
125
                                err = errors.Wrap(err, "failed to parse request body")
2✔
126
                                break
2✔
127
                        }
128
                        if devDevs == nil {
18✔
129
                                devDevs = batch
8✔
130
                        } else {
10✔
131
                                devDevs = append(devDevs, batch...)
2✔
132
                        }
2✔
133
                default:
2✔
134
                        err = errors.Errorf("%s %s request failed with status %v",
2✔
135
                                req.Method, req.URL, rsp.Status)
2✔
136
                        l.Errorf(err.Error())
2✔
137
                }
138
                body.Close()
16✔
139
                body = nil
16✔
140
                i = j
16✔
141
        }
142
        if len(devDevs) == 0 {
26✔
143
                return nil, err
10✔
144
        }
10✔
145

146
        return devDevs, err
6✔
147
}
148

149
func (c *client) GetLatestFinishedDeployment(
150
        ctx context.Context,
151
        tenantID string,
152
        deviceID string,
153
) (*DeviceDeployment, error) {
29✔
154
        l := log.FromContext(ctx)
29✔
155

29✔
156
        url := utils.JoinURL(c.urlBase, urlDeviceDeploymentsID)
29✔
157
        url = strings.Replace(url, ":tid", tenantID, 1)
29✔
158
        url = strings.Replace(url, ":id", deviceID, 1)
29✔
159

29✔
160
        ctx, cancel := context.WithTimeout(ctx, defaultTimeout)
29✔
161
        defer cancel()
29✔
162

29✔
163
        req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
29✔
164
        if err != nil {
31✔
165
                return nil, errors.Wrapf(err, "failed to create request")
2✔
166
        }
2✔
167

168
        q := req.URL.Query()
27✔
169
        q.Add("page", "1")
27✔
170
        q.Add("per_page", "1")
27✔
171
        req.URL.RawQuery = q.Encode()
27✔
172

27✔
173
        rsp, err := c.client.Do(req)
27✔
174
        if err != nil {
29✔
175
                return nil, errors.Wrapf(err, "failed to submit %s %s", req.Method, req.URL)
2✔
176
        }
2✔
177
        defer rsp.Body.Close()
25✔
178

25✔
179
        if rsp.StatusCode == http.StatusNotFound {
27✔
180
                return nil, nil
2✔
181
        } else if rsp.StatusCode != http.StatusOK {
27✔
182
                err := errors.Errorf("%s %s request failed with status %v",
2✔
183
                        req.Method, req.URL, rsp.Status)
2✔
184
                l.Errorf(err.Error())
2✔
185
                return nil, err
2✔
186
        }
2✔
187

188
        dec := json.NewDecoder(rsp.Body)
21✔
189
        var devDevs []*DeviceDeployment
21✔
190
        if err = dec.Decode(&devDevs); err != nil {
23✔
191
                return nil, errors.Wrap(err, "failed to parse request body")
2✔
192
        } else if len(devDevs) == 0 {
23✔
193
                return nil, nil
2✔
194
        }
2✔
195
        return devDevs[0], nil
17✔
196
}
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