Skip to content

Commit ea7efcd

Browse files
author
allenzhli
committed
add test
Signed-off-by: allenzhli <[email protected]>
1 parent 4c9a2af commit ea7efcd

File tree

7 files changed

+220
-0
lines changed

7 files changed

+220
-0
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
* [ENHANCEMENT] Memberlist: add status page (/memberlist) with available details about memberlist-based KV store and memberlist cluster. It's also possible to view KV values in Go struct or JSON format, or download for inspection. #3575
1515
* [ENHANCEMENT] Memberlist: client can now keep a size-bounded buffer with sent and received messages and display them in the admin UI (/memberlist) for troubleshooting. #3581
1616
* [BUGFIX] Query-Frontend: `cortex_query_seconds_total` now return seconds not nanoseconds. #3589
17+
* [ENHANCEMENT] Add api to list all tenant alertmanager configs and ruler rules. #3259
18+
- `GET /multitenant_alertmanager/configs`
19+
- `GET /ruler/rules`
1720

1821
## 1.6.0-rc.0 in progress
1922

docs/api/_index.md

+18
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ For the sake of clarity, in this document we have grouped API endpoints by servi
4040
| [Get tenant ingestion stats](#get-tenant-ingestion-stats) | Querier | `GET /api/v1/user_stats` |
4141
| [Get tenant chunks](#get-tenant-chunks) | Querier | `GET /api/v1/chunks` |
4242
| [Ruler ring status](#ruler-ring-status) | Ruler | `GET /ruler/ring` |
43+
| [Ruler rules ](#ruler-rules) | Ruler | `GET /ruler/rules` |
4344
| [List rules](#list-rules) | Ruler | `GET <prometheus-http-prefix>/api/v1/rules` |
4445
| [List alerts](#list-alerts) | Ruler | `GET <prometheus-http-prefix>/api/v1/alerts` |
4546
| [List rule groups](#list-rule-groups) | Ruler | `GET /api/v1/rules` |
@@ -49,6 +50,7 @@ For the sake of clarity, in this document we have grouped API endpoints by servi
4950
| [Delete rule group](#delete-rule-group) | Ruler | `DELETE /api/v1/rules/{namespace}/{groupName}` |
5051
| [Delete namespace](#delete-namespace) | Ruler | `DELETE /api/v1/rules/{namespace}` |
5152
| [Alertmanager status](#alertmanager-status) | Alertmanager | `GET /multitenant_alertmanager/status` |
53+
| [Alertmanager configs](#alertmanager-configs) | Alertmanager | `GET /multitenant_alertmanager/configs` |
5254
| [Alertmanager UI](#alertmanager-ui) | Alertmanager | `GET /<alertmanager-http-prefix>` |
5355
| [Get Alertmanager configuration](#get-alertmanager-configuration) | Alertmanager | `GET /api/v1/alerts` |
5456
| [Set Alertmanager configuration](#set-alertmanager-configuration) | Alertmanager | `POST /api/v1/alerts` |
@@ -397,6 +399,14 @@ GET /ruler_ring
397399

398400
Displays a web page with the ruler hash ring status, including the state, healthy and last heartbeat time of each ruler.
399401

402+
### Ruler rules
403+
404+
```
405+
GET /ruler/rules
406+
```
407+
408+
List all tenant rules that are currently loaded. This endpoint returns a YAML dictionary with all the rule groups for each tenant and `200` status code on success.
409+
400410
### List rules
401411

402412
```
@@ -617,6 +627,14 @@ GET /status
617627

618628
Displays a web page with the current status of the Alertmanager, including the Alertmanager cluster members.
619629

630+
### Alertmanager configs
631+
632+
```
633+
GET /multitenant_alertmanager/configs
634+
```
635+
636+
List all Alertmanager configurations that are currently loaded. This endpoint returns a YAML dictionary with all the Alertmanager configurations and `200` status code on success.
637+
620638
### Alertmanager UI
621639

622640
```

pkg/alertmanager/api.go

+35
Original file line numberDiff line numberDiff line change
@@ -182,3 +182,38 @@ func validateUserConfig(logger log.Logger, cfg alerts.AlertConfigDesc) error {
182182

183183
return nil
184184
}
185+
186+
func (am *MultitenantAlertmanager) ListUserConfig(w http.ResponseWriter, r *http.Request) {
187+
logger := util.WithContext(r.Context(), am.logger)
188+
189+
cfgMap, err := am.store.ListAlertConfigs(r.Context())
190+
if err != nil {
191+
if err == alerts.ErrNotFound {
192+
http.Error(w, err.Error(), http.StatusNotFound)
193+
} else {
194+
http.Error(w, err.Error(), http.StatusInternalServerError)
195+
}
196+
return
197+
}
198+
userConfigMap := make(map[string]*UserConfig, len(cfgMap))
199+
for userID, cfg := range cfgMap {
200+
userConfigMap[userID] = &UserConfig{
201+
TemplateFiles: alerts.ParseTemplates(cfg),
202+
AlertmanagerConfig: cfg.RawConfig,
203+
}
204+
}
205+
206+
d, err := yaml.Marshal(userConfigMap)
207+
208+
if err != nil {
209+
level.Error(logger).Log("msg", errMarshallingYAML, "err", err)
210+
http.Error(w, fmt.Sprintf("%s: %s", errMarshallingYAML, err.Error()), http.StatusInternalServerError)
211+
return
212+
}
213+
214+
w.Header().Set("Content-Type", "application/yaml")
215+
if _, err := w.Write(d); err != nil {
216+
http.Error(w, err.Error(), http.StatusInternalServerError)
217+
return
218+
}
219+
}

pkg/alertmanager/api_test.go

+101
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,17 @@ import (
77
"io/ioutil"
88
"net/http"
99
"net/http/httptest"
10+
"os"
1011
"testing"
1112

13+
"github.com/go-kit/kit/log"
14+
"github.com/gorilla/mux"
15+
"gopkg.in/yaml.v2"
16+
1217
"github.com/cortexproject/cortex/pkg/alertmanager/alerts"
1318
"github.com/cortexproject/cortex/pkg/util"
19+
"github.com/cortexproject/cortex/pkg/util/flagext"
20+
"github.com/cortexproject/cortex/pkg/util/services"
1421

1522
"github.com/stretchr/testify/require"
1623
"github.com/weaveworks/common/user"
@@ -165,3 +172,97 @@ func (noopAlertStore) SetAlertConfig(ctx context.Context, cfg alerts.AlertConfig
165172
func (noopAlertStore) DeleteAlertConfig(ctx context.Context, user string) error {
166173
return nil
167174
}
175+
176+
func TestAMConfigListUserConfig(t *testing.T) {
177+
testCases := map[string]*UserConfig{
178+
"user1": {
179+
AlertmanagerConfig: `
180+
global:
181+
resolve_timeout: 5m
182+
route:
183+
receiver: route1
184+
group_by:
185+
- '...'
186+
continue: false
187+
receivers:
188+
- name: route1
189+
webhook_configs:
190+
- send_resolved: true
191+
http_config: {}
192+
url: http://alertmanager/api/notifications?orgId=1&rrid=7
193+
max_alerts: 0
194+
`,
195+
},
196+
"user2": {
197+
AlertmanagerConfig: `
198+
global:
199+
resolve_timeout: 5m
200+
route:
201+
receiver: route1
202+
group_by:
203+
- '...'
204+
continue: false
205+
receivers:
206+
- name: route1
207+
webhook_configs:
208+
- send_resolved: true
209+
http_config: {}
210+
url: http://alertmanager/api/notifications?orgId=2&rrid=7
211+
max_alerts: 0
212+
`,
213+
},
214+
}
215+
216+
mockStore := &mockAlertStore{
217+
configs: map[string]alerts.AlertConfigDesc{},
218+
}
219+
220+
for u, cfg := range testCases {
221+
err := mockStore.SetAlertConfig(context.Background(), alerts.AlertConfigDesc{
222+
User: u,
223+
RawConfig: cfg.AlertmanagerConfig,
224+
})
225+
require.NoError(t, err)
226+
}
227+
228+
externalURL := flagext.URLValue{}
229+
err := externalURL.Set("http://localhost:8080/alertmanager")
230+
require.NoError(t, err)
231+
232+
tempDir, err := ioutil.TempDir(os.TempDir(), "alertmanager")
233+
require.NoError(t, err)
234+
defer os.RemoveAll(tempDir)
235+
236+
// Create the Multitenant Alertmanager.
237+
am := createMultitenantAlertmanager(&MultitenantAlertmanagerConfig{
238+
ExternalURL: externalURL,
239+
DataDir: tempDir,
240+
}, nil, nil, mockStore, log.NewNopLogger(), nil)
241+
defer services.StopAndAwaitTerminated(context.Background(), am) //nolint:errcheck
242+
243+
err = am.updateConfigs()
244+
require.NoError(t, err)
245+
246+
router := mux.NewRouter()
247+
router.Path("/multitenant_alertmanager/configs").Methods(http.MethodGet).HandlerFunc(am.ListUserConfig)
248+
// Request when no user configuration is present.
249+
req := httptest.NewRequest("GET", "https://localhost:8080/multitenant_alertmanager/configs", nil)
250+
w := httptest.NewRecorder()
251+
router.ServeHTTP(w, req)
252+
253+
resp := w.Result()
254+
require.Equal(t, http.StatusOK, resp.StatusCode)
255+
require.Equal(t, "application/yaml", resp.Header.Get("Content-Type"))
256+
body, _ := ioutil.ReadAll(resp.Body)
257+
old, _ := yaml.Marshal(testCases)
258+
require.Equal(t, string(old), string(body))
259+
260+
// It succeeds and the Alertmanager is started
261+
require.Len(t, am.alertmanagers, 2)
262+
require.True(t, am.alertmanagers["user1"].IsActive())
263+
require.True(t, am.alertmanagers["user2"].IsActive())
264+
265+
// Pause the alertmanager
266+
am.alertmanagers["user1"].Stop()
267+
am.alertmanagers["user2"].Stop()
268+
}

pkg/api/api.go

+2
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ func (a *API) RegisterAlertmanager(am *alertmanager.MultitenantAlertmanager, tar
145145
a.indexPage.AddLink(SectionAdminEndpoints, "/multitenant_alertmanager/status", "Alertmanager Status")
146146
// Ensure this route is registered before the prefixed AM route
147147
a.RegisterRoute("/multitenant_alertmanager/status", am.GetStatusHandler(), false, "GET")
148+
a.RegisterRoute("/multitenant_alertmanager/configs", http.HandlerFunc(am.ListUserConfig), false, "GET")
148149

149150
// UI components lead to a large number of routes to support, utilize a path prefix instead
150151
a.RegisterRoutesWithPrefix(a.cfg.AlertmanagerHTTPPrefix, am, true)
@@ -254,6 +255,7 @@ func (a *API) RegisterRulerAPI(r *ruler.API) {
254255
a.RegisterRoute(a.cfg.PrometheusHTTPPrefix+"/api/v1/alerts", http.HandlerFunc(r.PrometheusAlerts), true, "GET")
255256

256257
// Ruler API Routes
258+
a.RegisterRoute("/ruler/rules", http.HandlerFunc(r.ListAllRules), false, "GET")
257259
a.RegisterRoute("/api/v1/rules", http.HandlerFunc(r.ListRules), true, "GET")
258260
a.RegisterRoute("/api/v1/rules/{namespace}", http.HandlerFunc(r.ListRules), true, "GET")
259261
a.RegisterRoute("/api/v1/rules/{namespace}/{groupName}", http.HandlerFunc(r.GetRuleGroup), true, "GET")

pkg/ruler/api.go

+25
Original file line numberDiff line numberDiff line change
@@ -548,3 +548,28 @@ func (a *API) DeleteRuleGroup(w http.ResponseWriter, req *http.Request) {
548548

549549
respondAccepted(w, logger)
550550
}
551+
552+
func (a *API) ListAllRules(w http.ResponseWriter, req *http.Request) {
553+
logger := util.WithContext(req.Context(), util.Logger)
554+
555+
level.Debug(logger).Log("msg", "retrieving all rule groups")
556+
rgs, err := a.store.ListAllRuleGroups(req.Context())
557+
if err != nil {
558+
http.Error(w, err.Error(), http.StatusBadRequest)
559+
return
560+
}
561+
562+
level.Debug(logger).Log("msg", "retrieved all rule groups from rule store", len(rgs))
563+
564+
if len(rgs) == 0 {
565+
level.Info(logger).Log("msg", "no rule groups found")
566+
http.Error(w, ErrNoRuleGroups.Error(), http.StatusNotFound)
567+
return
568+
}
569+
570+
gs := make(map[string]map[string][]rulefmt.RuleGroup, len(rgs)) // user:namespace:[]rulefmt.RuleGroup
571+
for userID := range rgs {
572+
gs[userID] = rgs[userID].Formatted()
573+
}
574+
marshalAndSend(gs, w, logger)
575+
}

pkg/ruler/api_test.go

+36
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ import (
1212
"testing"
1313

1414
"github.com/gorilla/mux"
15+
"github.com/prometheus/prometheus/pkg/rulefmt"
1516
"github.com/stretchr/testify/require"
1617
"github.com/weaveworks/common/user"
18+
"gopkg.in/yaml.v3"
1719

1820
"github.com/cortexproject/cortex/pkg/ruler/rules"
1921
"github.com/cortexproject/cortex/pkg/util/services"
@@ -366,6 +368,40 @@ rules:
366368
}
367369
}
368370

371+
func TestRuler_ListAllRules(t *testing.T) {
372+
cfg, cleanup := defaultRulerConfig(newMockRuleStore(mockRules))
373+
defer cleanup()
374+
375+
r, rcleanup := newTestRuler(t, cfg)
376+
defer rcleanup()
377+
defer services.StopAndAwaitTerminated(context.Background(), r) //nolint:errcheck
378+
379+
a := NewAPI(r, r.store)
380+
381+
router := mux.NewRouter()
382+
router.Path("/ruler/rules").Methods(http.MethodGet).HandlerFunc(a.ListAllRules)
383+
384+
// Verify namespace1 rules are there.
385+
req := requestFor(t, http.MethodGet, "https://localhost:8080/ruler/rules", nil, "")
386+
w := httptest.NewRecorder()
387+
router.ServeHTTP(w, req)
388+
389+
resp := w.Result()
390+
body, _ := ioutil.ReadAll(resp.Body)
391+
392+
// Check status code and header
393+
require.Equal(t, http.StatusOK, resp.StatusCode)
394+
require.Equal(t, "application/yaml", resp.Header.Get("Content-Type"))
395+
396+
// Testing the running rules for user1 in the mock store
397+
gs := make(map[string]map[string][]rulefmt.RuleGroup) // user:namespace:[]rulefmt.RuleGroup
398+
for userID := range mockRules {
399+
gs[userID] = mockRules[userID].Formatted()
400+
}
401+
expectedResponse, _ := yaml.Marshal(gs)
402+
require.Equal(t, string(expectedResponse), string(body))
403+
}
404+
369405
func requestFor(t *testing.T, method string, url string, body io.Reader, userID string) *http.Request {
370406
t.Helper()
371407

0 commit comments

Comments
 (0)