Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aos
GitHub Repository: aos/grafana-agent
Path: blob/main/pkg/metrics/instance/configstore/api_test.go
5317 views
1
package configstore
2
3
import (
4
"bytes"
5
"context"
6
"encoding/json"
7
"fmt"
8
"io"
9
"net/http"
10
"net/http/httptest"
11
"strings"
12
"testing"
13
"time"
14
15
"github.com/go-kit/log"
16
"github.com/gorilla/mux"
17
"github.com/grafana/agent/pkg/client"
18
"github.com/grafana/agent/pkg/metrics/cluster/configapi"
19
"github.com/grafana/agent/pkg/metrics/instance"
20
"github.com/stretchr/testify/assert"
21
"github.com/stretchr/testify/require"
22
)
23
24
func TestAPI_ListConfigurations(t *testing.T) {
25
s := &Mock{
26
ListFunc: func(ctx context.Context) ([]string, error) {
27
return []string{"a", "b", "c"}, nil
28
},
29
}
30
31
api := NewAPI(log.NewNopLogger(), s, nil, true)
32
env := newAPITestEnvironment(t, api)
33
34
resp, err := http.Get(env.srv.URL + "/agent/api/v1/configs")
35
require.NoError(t, err)
36
require.Equal(t, http.StatusOK, resp.StatusCode)
37
38
expect := `{
39
"status": "success",
40
"data": {
41
"configs": ["a", "b", "c"]
42
}
43
}`
44
body, err := io.ReadAll(resp.Body)
45
require.NoError(t, err)
46
require.JSONEq(t, expect, string(body))
47
48
t.Run("With Client", func(t *testing.T) {
49
cli := client.New(env.srv.URL)
50
apiResp, err := cli.ListConfigs(context.Background())
51
require.NoError(t, err)
52
53
expect := &configapi.ListConfigurationsResponse{Configs: []string{"a", "b", "c"}}
54
require.Equal(t, expect, apiResp)
55
})
56
}
57
58
func TestAPI_GetConfiguration_Invalid(t *testing.T) {
59
s := &Mock{
60
GetFunc: func(ctx context.Context, key string) (instance.Config, error) {
61
return instance.Config{}, NotExistError{Key: key}
62
},
63
}
64
65
api := NewAPI(log.NewNopLogger(), s, nil, true)
66
env := newAPITestEnvironment(t, api)
67
68
resp, err := http.Get(env.srv.URL + "/agent/api/v1/configs/does-not-exist")
69
require.NoError(t, err)
70
require.Equal(t, http.StatusNotFound, resp.StatusCode)
71
72
expect := `{
73
"status": "error",
74
"data": {
75
"error": "configuration does-not-exist does not exist"
76
}
77
}`
78
body, err := io.ReadAll(resp.Body)
79
require.NoError(t, err)
80
require.JSONEq(t, expect, string(body))
81
82
t.Run("With Client", func(t *testing.T) {
83
cli := client.New(env.srv.URL)
84
_, err := cli.GetConfiguration(context.Background(), "does-not-exist")
85
require.NotNil(t, err)
86
require.Equal(t, "configuration does-not-exist does not exist", err.Error())
87
})
88
}
89
90
func TestAPI_GetConfiguration(t *testing.T) {
91
s := &Mock{
92
GetFunc: func(ctx context.Context, key string) (instance.Config, error) {
93
return instance.Config{
94
Name: key,
95
HostFilter: true,
96
RemoteFlushDeadline: 10 * time.Minute,
97
}, nil
98
},
99
}
100
101
api := NewAPI(log.NewNopLogger(), s, nil, true)
102
env := newAPITestEnvironment(t, api)
103
104
resp, err := http.Get(env.srv.URL + "/agent/api/v1/configs/exists")
105
require.NoError(t, err)
106
require.Equal(t, http.StatusOK, resp.StatusCode)
107
108
expect := `{
109
"status": "success",
110
"data": {
111
"value": "name: exists\nhost_filter: true\nremote_flush_deadline: 10m0s\n"
112
}
113
}`
114
body, err := io.ReadAll(resp.Body)
115
require.NoError(t, err)
116
require.JSONEq(t, expect, string(body))
117
118
t.Run("With Client", func(t *testing.T) {
119
cli := client.New(env.srv.URL)
120
actual, err := cli.GetConfiguration(context.Background(), "exists")
121
require.NoError(t, err)
122
123
// The client will apply defaults, so we need to start with the DefaultConfig
124
// as a base here.
125
expect := instance.DefaultConfig
126
expect.Name = "exists"
127
expect.HostFilter = true
128
expect.RemoteFlushDeadline = 10 * time.Minute
129
require.Equal(t, &expect, actual)
130
})
131
}
132
133
func TestAPI_GetConfiguration_ScrubSecrets(t *testing.T) {
134
rawConfig := `name: exists
135
scrape_configs:
136
- job_name: local_scrape
137
follow_redirects: true
138
enable_http2: true
139
honor_timestamps: true
140
metrics_path: /metrics
141
scheme: http
142
static_configs:
143
- targets:
144
- 127.0.0.1:12345
145
labels:
146
cluster: localhost
147
basic_auth:
148
username: admin
149
password: SCRUBME
150
remote_write:
151
- url: http://localhost:9009/api/prom/push
152
remote_timeout: 30s
153
name: test-d0f32c
154
send_exemplars: true
155
basic_auth:
156
username: admin
157
password: SCRUBME
158
queue_config:
159
capacity: 500
160
max_shards: 1000
161
min_shards: 1
162
max_samples_per_send: 100
163
batch_send_deadline: 5s
164
min_backoff: 30ms
165
max_backoff: 100ms
166
follow_redirects: true
167
enable_http2: true
168
metadata_config:
169
send: true
170
send_interval: 1m
171
max_samples_per_send: 500
172
wal_truncate_frequency: 1m0s
173
min_wal_time: 5m0s
174
max_wal_time: 4h0m0s
175
remote_flush_deadline: 1m0s
176
`
177
scrubbedConfig := strings.ReplaceAll(rawConfig, "SCRUBME", "<secret>")
178
179
s := &Mock{
180
GetFunc: func(ctx context.Context, key string) (instance.Config, error) {
181
c, err := instance.UnmarshalConfig(strings.NewReader(rawConfig))
182
if err != nil {
183
return instance.Config{}, err
184
}
185
return *c, nil
186
},
187
}
188
189
api := NewAPI(log.NewNopLogger(), s, nil, true)
190
env := newAPITestEnvironment(t, api)
191
192
resp, err := http.Get(env.srv.URL + "/agent/api/v1/configs/exists")
193
require.NoError(t, err)
194
require.Equal(t, http.StatusOK, resp.StatusCode)
195
respBytes, err := io.ReadAll(resp.Body)
196
require.NoError(t, err)
197
198
var apiResp struct {
199
Status string `json:"status"`
200
Data struct {
201
Value string `json:"value"`
202
} `json:"data"`
203
}
204
err = json.Unmarshal(respBytes, &apiResp)
205
require.NoError(t, err)
206
require.Equal(t, "success", apiResp.Status)
207
require.YAMLEq(t, scrubbedConfig, apiResp.Data.Value)
208
209
t.Run("With Client", func(t *testing.T) {
210
cli := client.New(env.srv.URL)
211
actual, err := cli.GetConfiguration(context.Background(), "exists")
212
require.NoError(t, err)
213
214
// Marshal the retrieved config _without_ scrubbing. This means
215
// that if the secrets weren't scrubbed from GetConfiguration, something
216
// bad happened at the API level.
217
actualBytes, err := instance.MarshalConfig(actual, false)
218
require.NoError(t, err)
219
require.YAMLEq(t, scrubbedConfig, string(actualBytes))
220
})
221
}
222
223
func TestServer_GetConfiguration_Disabled(t *testing.T) {
224
api := NewAPI(log.NewNopLogger(), nil, nil, false)
225
env := newAPITestEnvironment(t, api)
226
resp, err := http.Get(env.srv.URL + "/agent/api/v1/configs/exists")
227
require.NoError(t, err)
228
require.Equal(t, http.StatusNotFound, resp.StatusCode)
229
body, err := io.ReadAll(resp.Body)
230
require.NoError(t, err)
231
require.Equal(t, []byte("404 - config endpoint is disabled"), body)
232
}
233
234
func TestServer_PutConfiguration(t *testing.T) {
235
var s Mock
236
237
api := NewAPI(log.NewNopLogger(), &s, nil, true)
238
env := newAPITestEnvironment(t, api)
239
240
cfg := instance.Config{Name: "newconfig"}
241
bb, err := instance.MarshalConfig(&cfg, false)
242
require.NoError(t, err)
243
244
t.Run("Created", func(t *testing.T) {
245
// Created configs should return http.StatusCreated
246
s.PutFunc = func(ctx context.Context, c instance.Config) (created bool, err error) {
247
return true, nil
248
}
249
250
resp, err := http.Post(env.srv.URL+"/agent/api/v1/config/newconfig", "", bytes.NewReader(bb))
251
require.NoError(t, err)
252
require.Equal(t, http.StatusCreated, resp.StatusCode)
253
})
254
255
t.Run("Updated", func(t *testing.T) {
256
// Updated configs should return http.StatusOK
257
s.PutFunc = func(ctx context.Context, c instance.Config) (created bool, err error) {
258
return false, nil
259
}
260
261
resp, err := http.Post(env.srv.URL+"/agent/api/v1/config/newconfig", "", bytes.NewReader(bb))
262
require.NoError(t, err)
263
require.Equal(t, http.StatusOK, resp.StatusCode)
264
})
265
}
266
267
func TestServer_PutConfiguration_Invalid(t *testing.T) {
268
var s Mock
269
270
api := NewAPI(log.NewNopLogger(), &s, func(c *instance.Config) error {
271
return fmt.Errorf("custom validation error")
272
}, true)
273
env := newAPITestEnvironment(t, api)
274
275
cfg := instance.Config{Name: "newconfig"}
276
bb, err := instance.MarshalConfig(&cfg, false)
277
require.NoError(t, err)
278
279
resp, err := http.Post(env.srv.URL+"/agent/api/v1/config/newconfig", "", bytes.NewReader(bb))
280
require.NoError(t, err)
281
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
282
283
expect := `{
284
"status": "error",
285
"data": {
286
"error": "failed to validate config: custom validation error"
287
}
288
}`
289
body, err := io.ReadAll(resp.Body)
290
require.NoError(t, err)
291
require.JSONEq(t, expect, string(body))
292
}
293
294
func TestServer_PutConfiguration_WithClient(t *testing.T) {
295
var s Mock
296
api := NewAPI(log.NewNopLogger(), &s, nil, true)
297
env := newAPITestEnvironment(t, api)
298
299
cfg := instance.DefaultConfig
300
cfg.Name = "newconfig-withclient"
301
cfg.HostFilter = true
302
cfg.RemoteFlushDeadline = 10 * time.Minute
303
304
s.PutFunc = func(ctx context.Context, c instance.Config) (created bool, err error) {
305
assert.Equal(t, cfg, c)
306
return true, nil
307
}
308
309
cli := client.New(env.srv.URL)
310
err := cli.PutConfiguration(context.Background(), "newconfig-withclient", &cfg)
311
require.NoError(t, err)
312
}
313
314
func TestServer_DeleteConfiguration(t *testing.T) {
315
s := &Mock{
316
DeleteFunc: func(ctx context.Context, key string) error {
317
assert.Equal(t, "deleteme", key)
318
return nil
319
},
320
}
321
322
api := NewAPI(log.NewNopLogger(), s, nil, true)
323
env := newAPITestEnvironment(t, api)
324
325
req, err := http.NewRequest(http.MethodDelete, env.srv.URL+"/agent/api/v1/config/deleteme", nil)
326
require.NoError(t, err)
327
resp, err := http.DefaultClient.Do(req)
328
require.NoError(t, err)
329
require.Equal(t, http.StatusOK, resp.StatusCode)
330
331
t.Run("With Client", func(t *testing.T) {
332
cli := client.New(env.srv.URL)
333
err := cli.DeleteConfiguration(context.Background(), "deleteme")
334
require.NoError(t, err)
335
})
336
}
337
338
func TestServer_DeleteConfiguration_Invalid(t *testing.T) {
339
s := &Mock{
340
DeleteFunc: func(ctx context.Context, key string) error {
341
assert.Equal(t, "deleteme", key)
342
return NotExistError{Key: key}
343
},
344
}
345
346
api := NewAPI(log.NewNopLogger(), s, nil, true)
347
env := newAPITestEnvironment(t, api)
348
349
req, err := http.NewRequest(http.MethodDelete, env.srv.URL+"/agent/api/v1/config/deleteme", nil)
350
require.NoError(t, err)
351
resp, err := http.DefaultClient.Do(req)
352
require.NoError(t, err)
353
require.Equal(t, http.StatusNotFound, resp.StatusCode)
354
355
t.Run("With Client", func(t *testing.T) {
356
cli := client.New(env.srv.URL)
357
err := cli.DeleteConfiguration(context.Background(), "deleteme")
358
require.Error(t, err)
359
})
360
}
361
362
func TestServer_URLEncoded(t *testing.T) {
363
var s Mock
364
365
api := NewAPI(log.NewNopLogger(), &s, nil, true)
366
env := newAPITestEnvironment(t, api)
367
368
var cfg instance.Config
369
bb, err := instance.MarshalConfig(&cfg, false)
370
require.NoError(t, err)
371
372
s.PutFunc = func(ctx context.Context, c instance.Config) (created bool, err error) {
373
assert.Equal(t, "url/encoded", c.Name)
374
return true, nil
375
}
376
377
resp, err := http.Post(env.srv.URL+"/agent/api/v1/config/url%2Fencoded", "", bytes.NewReader(bb))
378
require.NoError(t, err)
379
require.Equal(t, http.StatusCreated, resp.StatusCode)
380
381
s.GetFunc = func(ctx context.Context, key string) (instance.Config, error) {
382
assert.Equal(t, "url/encoded", key)
383
return instance.Config{Name: "url/encoded"}, nil
384
}
385
386
resp, err = http.Get(env.srv.URL + "/agent/api/v1/configs/url%2Fencoded")
387
require.NoError(t, err)
388
require.Equal(t, http.StatusOK, resp.StatusCode)
389
}
390
391
type apiTestEnvironment struct {
392
srv *httptest.Server
393
router *mux.Router
394
}
395
396
func newAPITestEnvironment(t *testing.T, api *API) apiTestEnvironment {
397
t.Helper()
398
399
router := mux.NewRouter()
400
srv := httptest.NewServer(router)
401
t.Cleanup(srv.Close)
402
403
api.WireAPI(router)
404
405
return apiTestEnvironment{srv: srv, router: router}
406
}
407
408