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

mendersoftware / useradm / 1337903039

04 Jun 2024 10:02AM UTC coverage: 85.226%. Remained the same
1337903039

push

gitlab-ci

web-flow
Merge pull request #425 from mendersoftware/dependabot/go_modules/golang-dependencies-8c34a824dd

chore: bump the golang-dependencies group with 2 updates

2792 of 3276 relevant lines covered (85.23%)

49.04 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 {
36✔
99
        if conf.Timeout == 0 {
72✔
100
                conf.Timeout = defaultReqTimeout
36✔
101
        }
36✔
102

103
        return &Client{
36✔
104
                conf: conf,
36✔
105
        }
36✔
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) {
6✔
149
        usernameQ := url.QueryEscape(username)
6✔
150
        req, err := http.NewRequest(http.MethodGet,
6✔
151
                JoinURL(c.conf.TenantAdmAddr, GetTenantsUri+"?username="+url.QueryEscape(username)),
6✔
152
                nil)
6✔
153
        if err != nil {
6✔
154
                return nil, errors.New("failed to prepare request to tenantadm")
×
155
        }
×
156

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

6✔
160
        rsp, err := client.Do(req.WithContext(ctx))
6✔
161
        if err != nil {
6✔
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()
6✔
167

6✔
168
        if rsp.StatusCode != http.StatusOK {
8✔
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{}
4✔
176
        if err := json.NewDecoder(rsp.Body).Decode(&tenants); err != nil {
4✔
177
                return nil, errors.Wrap(err, "error parsing GET /tenants response")
×
178
        }
×
179

180
        switch len(tenants) {
4✔
181
        case 1:
2✔
182
                return &tenants[0], nil
2✔
183
        case 0:
2✔
184
                return nil, nil
2✔
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 {
6✔
191
        // prepare request body
6✔
192
        userJson, err := json.Marshal(user)
6✔
193
        if err != nil {
6✔
194
                return errors.Wrap(err, "failed to prepare body for POST /users")
×
195
        }
×
196

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

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

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

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

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

6✔
218
        switch rsp.StatusCode {
6✔
219
        case http.StatusCreated:
2✔
220
                return nil
2✔
221
        case http.StatusUnprocessableEntity:
2✔
222
                return ErrDuplicateUser
2✔
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 {
8✔
235
        // prepare request body
8✔
236
        json, err := json.Marshal(u)
8✔
237
        if err != nil {
8✔
238
                return errors.Wrap(err, "failed to prepare body for PUT /tenants/:id/users/:id")
×
239
        }
×
240

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

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

8✔
246
        req, err := http.NewRequest(http.MethodPut,
8✔
247
                JoinURL(c.conf.TenantAdmAddr, uri),
8✔
248
                reader)
8✔
249
        if err != nil {
8✔
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")
8✔
254

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

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

8✔
265
        switch rsp.StatusCode {
8✔
266
        case http.StatusNoContent:
2✔
267
                return nil
2✔
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 {
4✔
286

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

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

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

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

4✔
306
        if rsp.StatusCode != http.StatusNoContent {
6✔
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
2✔
314
}
315

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