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

mendersoftware / useradm / 1565757771

29 Nov 2024 07:56AM UTC coverage: 87.019%. Remained the same
1565757771

push

gitlab-ci

web-flow
Merge pull request #434 from alfrunes/1.22.x

chore(deps): Upgrade golang to latest

2869 of 3297 relevant lines covered (87.02%)

131.0 hits per line

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

85.87
/client/tenant/client_tenantadm.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
package tenant
15

16
import (
17
        "bytes"
18
        "context"
19
        "encoding/json"
20
        "net/http"
21
        "net/url"
22
        "strings"
23
        "time"
24

25
        "github.com/mendersoftware/go-lib-micro/apiclient"
26
        "github.com/mendersoftware/go-lib-micro/rest_utils"
27
        "github.com/pkg/errors"
28
)
29

30
const (
31
        // devices endpoint
32
        UriBase         = "/api/internal/v1/tenantadm"
33
        GetTenantsUri   = UriBase + "/tenants"
34
        UsersUri        = UriBase + "/users"
35
        TenantsUsersUri = UriBase + "/tenants/#tid/users/#uid"
36
        URIHealth       = UriBase + "/health"
37
        // default request timeout, 10s
38
        defaultReqTimeout = time.Duration(10) * time.Second
39

40
        redacted = "REDACTED"
41
)
42

43
var (
44
        ErrDuplicateUser = errors.New("user with the same name already exists")
45
        ErrUserNotFound  = errors.New("user not found")
46
)
47

48
// ClientConfig conveys client configuration
49
type Config struct {
50
        // tenantadm  service address
51
        TenantAdmAddr string
52
        // request timeout
53
        Timeout time.Duration
54
}
55

56
// ClientRunner is an interface of tenantadm api client
57
//
58
//go:generate ../../utils/mockgen.sh
59
type ClientRunner interface {
60
        CheckHealth(ctx context.Context) error
61
        GetTenant(ctx context.Context, username string, client apiclient.HttpRunner) (*Tenant, error)
62
        CreateUser(ctx context.Context, user *User, client apiclient.HttpRunner) error
63
        UpdateUser(
64
                ctx context.Context,
65
                tenantId,
66
                userId string,
67
                u *UserUpdate,
68
                client apiclient.HttpRunner,
69
        ) error
70
        DeleteUser(ctx context.Context, tenantId, clientId string, client apiclient.HttpRunner) error
71
}
72

73
// Client is an opaque implementation of tenantadm api client.
74
// Implements ClientRunner interface
75
type Client struct {
76
        conf Config
77
}
78

79
// Tenant is the tenantadm's api struct
80
type Tenant struct {
81
        ID     string `json:"id"`
82
        Name   string `json:"name"`
83
        Status string `json:"status"`
84
}
85

86
// User is the tenantadm's api struct
87
type User struct {
88
        ID       string `json:"id"`
89
        Name     string `json:"name"`
90
        TenantID string `json:"tenant_id"`
91
}
92

93
// UserUpdate is the tenantadm's api struct
94
type UserUpdate struct {
95
        Name string `json:"name"`
96
}
97

98
func NewClient(conf Config) *Client {
370✔
99
        if conf.Timeout == 0 {
740✔
100
                conf.Timeout = defaultReqTimeout
370✔
101
        }
370✔
102

103
        return &Client{
370✔
104
                conf: conf,
370✔
105
        }
370✔
106
}
107

108
func (c *Client) CheckHealth(ctx context.Context) error {
10✔
109
        var (
10✔
110
                client http.Client
10✔
111
                apiErr rest_utils.ApiError
10✔
112
                cancel context.CancelFunc
10✔
113
        )
10✔
114

10✔
115
        if ctx == nil {
12✔
116
                ctx = context.Background()
2✔
117
        }
2✔
118
        if _, ok := ctx.Deadline(); !ok {
16✔
119
                ctx, cancel = context.WithTimeout(ctx, c.conf.Timeout)
6✔
120
                defer cancel()
6✔
121
        }
6✔
122

123
        req, _ := http.NewRequestWithContext(
10✔
124
                ctx, "GET",
10✔
125
                JoinURL(c.conf.TenantAdmAddr, URIHealth), nil,
10✔
126
        )
10✔
127

10✔
128
        rsp, err := client.Do(req)
10✔
129
        if err != nil {
12✔
130
                return err
2✔
131
        }
2✔
132
        if rsp.StatusCode >= 200 && rsp.StatusCode < 300 {
12✔
133
                return nil
4✔
134
        }
4✔
135
        defer rsp.Body.Close()
4✔
136
        decoder := json.NewDecoder(rsp.Body)
4✔
137
        err = decoder.Decode(&apiErr)
4✔
138
        if err != nil {
6✔
139
                return errors.Errorf("service unhealthy: HTTP %s", rsp.Status)
2✔
140
        }
2✔
141
        return &apiErr
2✔
142
}
143

144
func (c *Client) GetTenant(
145
        ctx context.Context,
146
        username string,
147
        client apiclient.HttpRunner,
148
) (*Tenant, error) {
113✔
149
        usernameQ := url.QueryEscape(username)
113✔
150
        req, err := http.NewRequest(http.MethodGet,
113✔
151
                JoinURL(c.conf.TenantAdmAddr, GetTenantsUri+"?username="+url.QueryEscape(username)),
113✔
152
                nil)
113✔
153
        if err != nil {
113✔
154
                return nil, errors.New("failed to prepare request to tenantadm")
×
155
        }
×
156

157
        ctx, cancel := context.WithTimeout(ctx, c.conf.Timeout)
113✔
158
        defer cancel()
113✔
159

113✔
160
        rsp, err := client.Do(req.WithContext(ctx))
113✔
161
        if err != nil {
113✔
162
                repl := strings.NewReplacer(username, redacted, usernameQ, redacted)
×
163
                err = errors.New(repl.Replace(err.Error()))
×
164
                return nil, errors.Wrap(err, "GET /tenants request failed")
×
165
        }
×
166
        defer rsp.Body.Close()
113✔
167

113✔
168
        if rsp.StatusCode != http.StatusOK {
115✔
169
                return nil, errors.Errorf(
2✔
170
                        "GET /tenants request failed with unexpected status %v",
2✔
171
                        rsp.StatusCode,
2✔
172
                )
2✔
173
        }
2✔
174

175
        tenants := []Tenant{}
111✔
176
        if err := json.NewDecoder(rsp.Body).Decode(&tenants); err != nil {
111✔
177
                return nil, errors.Wrap(err, "error parsing GET /tenants response")
×
178
        }
×
179

180
        switch len(tenants) {
111✔
181
        case 1:
106✔
182
                return &tenants[0], nil
106✔
183
        case 0:
5✔
184
                return nil, nil
5✔
185
        default:
×
186
                return nil, errors.Errorf("got unexpected number of tenants: %v", len(tenants))
×
187
        }
188
}
189

190
func (c *Client) CreateUser(ctx context.Context, user *User, client apiclient.HttpRunner) error {
344✔
191
        // prepare request body
344✔
192
        userJson, err := json.Marshal(user)
344✔
193
        if err != nil {
344✔
194
                return errors.Wrap(err, "failed to prepare body for POST /users")
×
195
        }
×
196

197
        reader := bytes.NewReader(userJson)
344✔
198

344✔
199
        req, err := http.NewRequest(http.MethodPost,
344✔
200
                JoinURL(c.conf.TenantAdmAddr, UsersUri),
344✔
201
                reader)
344✔
202
        if err != nil {
344✔
203
                return errors.Wrap(err, "failed to create request for POST /users")
×
204
        }
×
205

206
        req.Header.Set("Content-Type", "application/json")
344✔
207

344✔
208
        ctx, cancel := context.WithTimeout(ctx, c.conf.Timeout)
344✔
209
        defer cancel()
344✔
210

344✔
211
        // send
344✔
212
        rsp, err := client.Do(req.WithContext(ctx))
344✔
213
        if err != nil {
344✔
214
                return errors.Wrap(err, "POST /users request failed")
×
215
        }
×
216
        defer rsp.Body.Close()
344✔
217

344✔
218
        switch rsp.StatusCode {
344✔
219
        case http.StatusCreated:
338✔
220
                return nil
338✔
221
        case http.StatusUnprocessableEntity:
4✔
222
                return ErrDuplicateUser
4✔
223
        default:
2✔
224
                return errors.Errorf("POST /users request failed with unexpected status %v", rsp.StatusCode)
2✔
225
        }
226
}
227

228
func (c *Client) UpdateUser(
229
        ctx context.Context,
230
        tenantId,
231
        userId string,
232
        u *UserUpdate,
233
        client apiclient.HttpRunner,
234
) error {
14✔
235
        // prepare request body
14✔
236
        json, err := json.Marshal(u)
14✔
237
        if err != nil {
14✔
238
                return errors.Wrap(err, "failed to prepare body for PUT /tenants/:id/users/:id")
×
239
        }
×
240

241
        reader := bytes.NewReader(json)
14✔
242

14✔
243
        repl := strings.NewReplacer("#tid", tenantId, "#uid", userId)
14✔
244
        uri := repl.Replace(TenantsUsersUri)
14✔
245

14✔
246
        req, err := http.NewRequest(http.MethodPut,
14✔
247
                JoinURL(c.conf.TenantAdmAddr, uri),
14✔
248
                reader)
14✔
249
        if err != nil {
14✔
250
                return errors.Wrap(err, "failed to create request for PUT /tenants/:id/users/:id")
×
251
        }
×
252

253
        req.Header.Set("Content-Type", "application/json")
14✔
254

14✔
255
        ctx, cancel := context.WithTimeout(ctx, c.conf.Timeout)
14✔
256
        defer cancel()
14✔
257

14✔
258
        // send
14✔
259
        rsp, err := client.Do(req.WithContext(ctx))
14✔
260
        if err != nil {
14✔
261
                return errors.Wrap(err, "PUT /tenants/:id/users/:id request failed")
×
262
        }
×
263
        defer rsp.Body.Close()
14✔
264

14✔
265
        switch rsp.StatusCode {
14✔
266
        case http.StatusNoContent:
8✔
267
                return nil
8✔
268
        case http.StatusUnprocessableEntity:
2✔
269
                return ErrDuplicateUser
2✔
270
        case http.StatusNotFound:
2✔
271
                return ErrUserNotFound
2✔
272
        default:
2✔
273
                return errors.Errorf(
2✔
274
                        "PUT /tenants/:id/users/:id request failed with unexpected status %v",
2✔
275
                        rsp.StatusCode,
2✔
276
                )
2✔
277
        }
278
}
279

280
func (c *Client) DeleteUser(
281
        ctx context.Context,
282
        tenantId,
283
        userId string,
284
        client apiclient.HttpRunner,
285
) error {
8✔
286

8✔
287
        repl := strings.NewReplacer("#tid", tenantId, "#uid", userId)
8✔
288
        uri := repl.Replace(TenantsUsersUri)
8✔
289

8✔
290
        req, err := http.NewRequest(http.MethodDelete,
8✔
291
                JoinURL(c.conf.TenantAdmAddr, uri), nil)
8✔
292
        if err != nil {
8✔
293
                return errors.Wrapf(err, "failed to create request for DELETE %s", uri)
×
294
        }
×
295

296
        ctx, cancel := context.WithTimeout(ctx, c.conf.Timeout)
8✔
297
        defer cancel()
8✔
298

8✔
299
        // send
8✔
300
        rsp, err := client.Do(req.WithContext(ctx))
8✔
301
        if err != nil {
8✔
302
                return errors.Wrapf(err, "DELETE %s request failed", uri)
×
303
        }
×
304
        defer rsp.Body.Close()
8✔
305

8✔
306
        if rsp.StatusCode != http.StatusNoContent {
10✔
307
                return errors.Errorf(
2✔
308
                        "DELETE %s request failed with unexpected status %v",
2✔
309
                        uri,
2✔
310
                        rsp.StatusCode,
2✔
311
                )
2✔
312
        }
2✔
313
        return nil
6✔
314
}
315

316
func JoinURL(base, url string) string {
489✔
317
        url = strings.TrimPrefix(url, "/")
489✔
318
        if !strings.HasSuffix(base, "/") {
978✔
319
                base = base + "/"
489✔
320
        }
489✔
321
        return base + url
489✔
322
}
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