From 119e1c46166a1f526a218fc473df7d84566f74e7 Mon Sep 17 00:00:00 2001 From: gotjosh Date: Tue, 21 Nov 2023 18:51:50 +0000 Subject: [PATCH] more changes to the API v1 removal Signed-off-by: gotjosh --- api/api.go | 10 +- api/metrics/metrics.go | 9 +- api/v1/api_test.go | 586 --------------------------------- api/v1_deprecation_router.go | 70 ++++ api/v2/api.go | 2 +- test/with_api_v1/acceptance.go | 432 ------------------------ 6 files changed, 84 insertions(+), 1025 deletions(-) delete mode 100644 api/v1/api_test.go create mode 100644 api/v1_deprecation_router.go delete mode 100644 test/with_api_v1/acceptance.go diff --git a/api/api.go b/api/api.go index d679f8e38e..823a9f6f46 100644 --- a/api/api.go +++ b/api/api.go @@ -36,7 +36,9 @@ import ( // API represents all APIs of Alertmanager. type API struct { - v2 *apiv2.API + v2 *apiv2.API + deprecationRouter *V1DeprecationRouter + requestsInFlight prometheus.Gauge concurrencyLimitExceeded prometheus.Counter timeout time.Duration @@ -143,6 +145,7 @@ func New(opts Options) (*API, error) { } return &API{ + deprecationRouter: NewV1DeprecationRouter(log.With(l, "version", "v1")), v2: v2, requestsInFlight: requestsInFlight, concurrencyLimitExceeded: concurrencyLimitExceeded, @@ -151,7 +154,7 @@ func New(opts Options) (*API, error) { }, nil } -// Register all APIs. As APIv2 works on the http.Handler level, this method also creates a new +// Register API. As APIv2 works on the http.Handler level, this method also creates a new // http.ServeMux and then uses it to register both the provided router (to // handle "/") and APIv2 (to handle "/api/v2"). The method returns // the newly created http.ServeMux. If a timeout has been set on construction of @@ -159,6 +162,9 @@ func New(opts Options) (*API, error) { // true for the concurrency limit, with the exception that it is only applied to // GET requests. func (api *API) Register(r *route.Router, routePrefix string) *http.ServeMux { + // TODO(gotjosh) API V1 was removed as of version 0.28, when we reach 1.0.0 we should removed these deprecation warnings. + api.deprecationRouter.Register(r.WithPrefix("/api/v1")) + mux := http.NewServeMux() mux.Handle("/", api.limitHandler(r)) diff --git a/api/metrics/metrics.go b/api/metrics/metrics.go index 483569ab9d..ea45acc2ee 100644 --- a/api/metrics/metrics.go +++ b/api/metrics/metrics.go @@ -15,7 +15,7 @@ package metrics import "github.com/prometheus/client_golang/prometheus" -// Alerts stores metrics for alerts which are common across all API versions. +// Alerts stores metrics for alerts. type Alerts struct { firing prometheus.Counter resolved prometheus.Counter @@ -23,16 +23,17 @@ type Alerts struct { } // NewAlerts returns an *Alerts struct for the given API version. -func NewAlerts(version string, r prometheus.Registerer) *Alerts { +// Since v1 was deprecated in 0.28, v2 is now hardcoded. +func NewAlerts(r prometheus.Registerer) *Alerts { numReceivedAlerts := prometheus.NewCounterVec(prometheus.CounterOpts{ Name: "alertmanager_alerts_received_total", Help: "The total number of received alerts.", - ConstLabels: prometheus.Labels{"version": version}, + ConstLabels: prometheus.Labels{"version": "v2"}, }, []string{"status"}) numInvalidAlerts := prometheus.NewCounter(prometheus.CounterOpts{ Name: "alertmanager_alerts_invalid_total", Help: "The total number of received alerts that were invalid.", - ConstLabels: prometheus.Labels{"version": version}, + ConstLabels: prometheus.Labels{"version": "v2"}, }) if r != nil { r.MustRegister(numReceivedAlerts, numInvalidAlerts) diff --git a/api/v1/api_test.go b/api/v1/api_test.go deleted file mode 100644 index 84315ef394..0000000000 --- a/api/v1/api_test.go +++ /dev/null @@ -1,586 +0,0 @@ -// Copyright 2018 Prometheus Team -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package v1 - -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/http/httptest" - "regexp" - "testing" - "time" - - "github.com/prometheus/common/model" - "github.com/stretchr/testify/require" - - "github.com/prometheus/alertmanager/config" - "github.com/prometheus/alertmanager/dispatch" - "github.com/prometheus/alertmanager/pkg/labels" - "github.com/prometheus/alertmanager/provider" - "github.com/prometheus/alertmanager/types" -) - -// fakeAlerts is a struct implementing the provider.Alerts interface for tests. -type fakeAlerts struct { - fps map[model.Fingerprint]int - alerts []*types.Alert - err error -} - -func newFakeAlerts(alerts []*types.Alert, withErr bool) *fakeAlerts { - fps := make(map[model.Fingerprint]int) - for i, a := range alerts { - fps[a.Fingerprint()] = i - } - f := &fakeAlerts{ - alerts: alerts, - fps: fps, - } - if withErr { - f.err = errors.New("error occurred") - } - return f -} - -func (f *fakeAlerts) Subscribe() provider.AlertIterator { return nil } -func (f *fakeAlerts) Get(model.Fingerprint) (*types.Alert, error) { return nil, nil } -func (f *fakeAlerts) Put(alerts ...*types.Alert) error { - return f.err -} - -func (f *fakeAlerts) GetPending() provider.AlertIterator { - ch := make(chan *types.Alert) - done := make(chan struct{}) - go func() { - defer close(ch) - for _, a := range f.alerts { - ch <- a - } - }() - return provider.NewAlertIterator(ch, done, f.err) -} - -func newGetAlertStatus(f *fakeAlerts) func(model.Fingerprint) types.AlertStatus { - return func(fp model.Fingerprint) types.AlertStatus { - status := types.AlertStatus{SilencedBy: []string{}, InhibitedBy: []string{}} - - i, ok := f.fps[fp] - if !ok { - return status - } - alert := f.alerts[i] - switch alert.Labels["state"] { - case "active": - status.State = types.AlertStateActive - case "unprocessed": - status.State = types.AlertStateUnprocessed - case "suppressed": - status.State = types.AlertStateSuppressed - } - if alert.Labels["silenced_by"] != "" { - status.SilencedBy = append(status.SilencedBy, string(alert.Labels["silenced_by"])) - } - if alert.Labels["inhibited_by"] != "" { - status.InhibitedBy = append(status.InhibitedBy, string(alert.Labels["inhibited_by"])) - } - return status - } -} - -func TestAddAlerts(t *testing.T) { - now := func(offset int) time.Time { - return time.Now().Add(time.Duration(offset) * time.Second) - } - - for i, tc := range []struct { - start, end time.Time - err bool - code int - }{ - {time.Time{}, time.Time{}, false, 200}, - {now(0), time.Time{}, false, 200}, - {time.Time{}, now(-1), false, 200}, - {time.Time{}, now(0), false, 200}, - {time.Time{}, now(1), false, 200}, - {now(-2), now(-1), false, 200}, - {now(1), now(2), false, 200}, - {now(1), now(0), false, 400}, - {now(0), time.Time{}, true, 500}, - } { - alerts := []model.Alert{{ - StartsAt: tc.start, - EndsAt: tc.end, - Labels: model.LabelSet{"label1": "test1"}, - Annotations: model.LabelSet{"annotation1": "some text"}, - }} - b, err := json.Marshal(&alerts) - if err != nil { - t.Errorf("Unexpected error %v", err) - } - - alertsProvider := newFakeAlerts([]*types.Alert{}, tc.err) - api := New(alertsProvider, nil, newGetAlertStatus(alertsProvider), nil, nil, nil) - defaultGlobalConfig := config.DefaultGlobalConfig() - route := config.Route{} - api.Update(&config.Config{ - Global: &defaultGlobalConfig, - Route: &route, - }) - - r, err := http.NewRequest("POST", "/api/v1/alerts", bytes.NewReader(b)) - w := httptest.NewRecorder() - if err != nil { - t.Errorf("Unexpected error %v", err) - } - - api.addAlerts(w, r) - res := w.Result() - body, _ := io.ReadAll(res.Body) - - require.Equal(t, tc.code, w.Code, fmt.Sprintf("test case: %d, StartsAt %v, EndsAt %v, Response: %s", i, tc.start, tc.end, string(body))) - } -} - -func TestListAlerts(t *testing.T) { - now := time.Now() - alerts := []*types.Alert{ - { - Alert: model.Alert{ - Labels: model.LabelSet{"state": "active", "alertname": "alert1"}, - StartsAt: now.Add(-time.Minute), - }, - }, - { - Alert: model.Alert{ - Labels: model.LabelSet{"state": "unprocessed", "alertname": "alert2"}, - StartsAt: now.Add(-time.Minute), - }, - }, - { - Alert: model.Alert{ - Labels: model.LabelSet{"state": "suppressed", "silenced_by": "abc", "alertname": "alert3"}, - StartsAt: now.Add(-time.Minute), - }, - }, - { - Alert: model.Alert{ - Labels: model.LabelSet{"state": "suppressed", "inhibited_by": "abc", "alertname": "alert4"}, - StartsAt: now.Add(-time.Minute), - }, - }, - { - Alert: model.Alert{ - Labels: model.LabelSet{"alertname": "alert5"}, - StartsAt: now.Add(-2 * time.Minute), - EndsAt: now.Add(-time.Minute), - }, - }, - } - - for i, tc := range []struct { - err bool - params map[string]string - - code int - anames []string - }{ - { - false, - map[string]string{}, - 200, - []string{"alert1", "alert2", "alert3", "alert4"}, - }, - { - false, - map[string]string{"active": "true", "unprocessed": "true", "silenced": "true", "inhibited": "true"}, - 200, - []string{"alert1", "alert2", "alert3", "alert4"}, - }, - { - false, - map[string]string{"active": "false", "unprocessed": "true", "silenced": "true", "inhibited": "true"}, - 200, - []string{"alert2", "alert3", "alert4"}, - }, - { - false, - map[string]string{"active": "true", "unprocessed": "false", "silenced": "true", "inhibited": "true"}, - 200, - []string{"alert1", "alert3", "alert4"}, - }, - { - false, - map[string]string{"active": "true", "unprocessed": "true", "silenced": "false", "inhibited": "true"}, - 200, - []string{"alert1", "alert2", "alert4"}, - }, - { - false, - map[string]string{"active": "true", "unprocessed": "true", "silenced": "true", "inhibited": "false"}, - 200, - []string{"alert1", "alert2", "alert3"}, - }, - { - false, - map[string]string{"filter": "{alertname=\"alert3\""}, - 200, - []string{"alert3"}, - }, - { - false, - map[string]string{"filter": "{alertname"}, - 400, - []string{}, - }, - { - false, - map[string]string{"receiver": "other"}, - 200, - []string{}, - }, - { - false, - map[string]string{"active": "invalid"}, - 400, - []string{}, - }, - { - true, - map[string]string{}, - 500, - []string{}, - }, - } { - alertsProvider := newFakeAlerts(alerts, tc.err) - api := New(alertsProvider, nil, newGetAlertStatus(alertsProvider), nil, nil, nil) - api.route = dispatch.NewRoute(&config.Route{Receiver: "def-receiver"}, nil) - - r, err := http.NewRequest("GET", "/api/v1/alerts", nil) - if err != nil { - t.Fatalf("Unexpected error %v", err) - } - q := r.URL.Query() - for k, v := range tc.params { - q.Add(k, v) - } - r.URL.RawQuery = q.Encode() - w := httptest.NewRecorder() - - api.listAlerts(w, r) - body, _ := io.ReadAll(w.Result().Body) - - var res response - err = json.Unmarshal(body, &res) - if err != nil { - t.Fatalf("Unexpected error %v", err) - } - - require.Equal(t, tc.code, w.Code, fmt.Sprintf("test case: %d, response: %s", i, string(body))) - if w.Code != 200 { - continue - } - - // Data needs to be serialized/deserialized to be converted to the real type. - b, err := json.Marshal(res.Data) - if err != nil { - t.Fatalf("Unexpected error %v", err) - } - retAlerts := []*Alert{} - err = json.Unmarshal(b, &retAlerts) - if err != nil { - t.Fatalf("Unexpected error %v", err) - } - - anames := []string{} - for _, a := range retAlerts { - name, ok := a.Labels["alertname"] - if ok { - anames = append(anames, string(name)) - } - } - require.Equal(t, tc.anames, anames, fmt.Sprintf("test case: %d, alert names are not equal", i)) - } -} - -func TestAlertFiltering(t *testing.T) { - type test struct { - alert *model.Alert - msg string - expected bool - } - - // Equal - equal, err := labels.NewMatcher(labels.MatchEqual, "label1", "test1") - if err != nil { - t.Errorf("Unexpected error %v", err) - } - - tests := []test{ - {&model.Alert{Labels: model.LabelSet{"label1": "test1"}}, "label1=test1", true}, - {&model.Alert{Labels: model.LabelSet{"label1": "test2"}}, "label1=test2", false}, - {&model.Alert{Labels: model.LabelSet{"label2": "test2"}}, "label2=test2", false}, - } - - for _, test := range tests { - actual := alertMatchesFilterLabels(test.alert, []*labels.Matcher{equal}) - msg := fmt.Sprintf("Expected %t for %s", test.expected, test.msg) - require.Equal(t, test.expected, actual, msg) - } - - // Not Equal - notEqual, err := labels.NewMatcher(labels.MatchNotEqual, "label1", "test1") - if err != nil { - t.Errorf("Unexpected error %v", err) - } - - tests = []test{ - {&model.Alert{Labels: model.LabelSet{"label1": "test1"}}, "label1!=test1", false}, - {&model.Alert{Labels: model.LabelSet{"label1": "test2"}}, "label1!=test2", true}, - {&model.Alert{Labels: model.LabelSet{"label2": "test2"}}, "label2!=test2", true}, - } - - for _, test := range tests { - actual := alertMatchesFilterLabels(test.alert, []*labels.Matcher{notEqual}) - msg := fmt.Sprintf("Expected %t for %s", test.expected, test.msg) - require.Equal(t, test.expected, actual, msg) - } - - // Regexp Equal - regexpEqual, err := labels.NewMatcher(labels.MatchRegexp, "label1", "tes.*") - if err != nil { - t.Errorf("Unexpected error %v", err) - } - - tests = []test{ - {&model.Alert{Labels: model.LabelSet{"label1": "test1"}}, "label1=~test1", true}, - {&model.Alert{Labels: model.LabelSet{"label1": "test2"}}, "label1=~test2", true}, - {&model.Alert{Labels: model.LabelSet{"label2": "test2"}}, "label2=~test2", false}, - } - - for _, test := range tests { - actual := alertMatchesFilterLabels(test.alert, []*labels.Matcher{regexpEqual}) - msg := fmt.Sprintf("Expected %t for %s", test.expected, test.msg) - require.Equal(t, test.expected, actual, msg) - } - - // Regexp Not Equal - regexpNotEqual, err := labels.NewMatcher(labels.MatchNotRegexp, "label1", "tes.*") - if err != nil { - t.Errorf("Unexpected error %v", err) - } - - tests = []test{ - {&model.Alert{Labels: model.LabelSet{"label1": "test1"}}, "label1!~test1", false}, - {&model.Alert{Labels: model.LabelSet{"label1": "test2"}}, "label1!~test2", false}, - {&model.Alert{Labels: model.LabelSet{"label2": "test2"}}, "label2!~test2", true}, - } - - for _, test := range tests { - actual := alertMatchesFilterLabels(test.alert, []*labels.Matcher{regexpNotEqual}) - msg := fmt.Sprintf("Expected %t for %s", test.expected, test.msg) - require.Equal(t, test.expected, actual, msg) - } -} - -func TestSilenceFiltering(t *testing.T) { - type test struct { - silence *types.Silence - msg string - expected bool - } - - // Equal - equal, err := labels.NewMatcher(labels.MatchEqual, "label1", "test1") - if err != nil { - t.Errorf("Unexpected error %v", err) - } - - tests := []test{ - { - &types.Silence{Matchers: newMatcher(model.LabelSet{"label1": "test1"})}, - "label1=test1", - true, - }, - { - &types.Silence{Matchers: newMatcher(model.LabelSet{"label1": "test2"})}, - "label1=test2", - false, - }, - { - &types.Silence{Matchers: newMatcher(model.LabelSet{"label2": "test2"})}, - "label2=test2", - false, - }, - } - - for _, test := range tests { - actual := silenceMatchesFilterLabels(test.silence, []*labels.Matcher{equal}) - msg := fmt.Sprintf("Expected %t for %s", test.expected, test.msg) - require.Equal(t, test.expected, actual, msg) - } - - // Not Equal - notEqual, err := labels.NewMatcher(labels.MatchNotEqual, "label1", "test1") - if err != nil { - t.Errorf("Unexpected error %v", err) - } - - tests = []test{ - { - &types.Silence{Matchers: newMatcher(model.LabelSet{"label1": "test1"})}, - "label1!=test1", - false, - }, - { - &types.Silence{Matchers: newMatcher(model.LabelSet{"label1": "test2"})}, - "label1!=test2", - true, - }, - { - &types.Silence{Matchers: newMatcher(model.LabelSet{"label2": "test2"})}, - "label2!=test2", - true, - }, - } - - for _, test := range tests { - actual := silenceMatchesFilterLabels(test.silence, []*labels.Matcher{notEqual}) - msg := fmt.Sprintf("Expected %t for %s", test.expected, test.msg) - require.Equal(t, test.expected, actual, msg) - } - - // Regexp Equal - regexpEqual, err := labels.NewMatcher(labels.MatchRegexp, "label1", "tes.*") - if err != nil { - t.Errorf("Unexpected error %v", err) - } - - tests = []test{ - { - &types.Silence{Matchers: newMatcher(model.LabelSet{"label1": "test1"})}, - "label1=~test1", - true, - }, - { - &types.Silence{Matchers: newMatcher(model.LabelSet{"label1": "test2"})}, - "label1=~test2", - true, - }, - { - &types.Silence{Matchers: newMatcher(model.LabelSet{"label2": "test2"})}, - "label2=~test2", - false, - }, - } - - for _, test := range tests { - actual := silenceMatchesFilterLabels(test.silence, []*labels.Matcher{regexpEqual}) - msg := fmt.Sprintf("Expected %t for %s", test.expected, test.msg) - require.Equal(t, test.expected, actual, msg) - } - - // Regexp Not Equal - regexpNotEqual, err := labels.NewMatcher(labels.MatchNotRegexp, "label1", "tes.*") - if err != nil { - t.Errorf("Unexpected error %v", err) - } - - tests = []test{ - { - &types.Silence{Matchers: newMatcher(model.LabelSet{"label1": "test1"})}, - "label1!~test1", - false, - }, - { - &types.Silence{Matchers: newMatcher(model.LabelSet{"label1": "test2"})}, - "label1!~test2", - false, - }, - { - &types.Silence{Matchers: newMatcher(model.LabelSet{"label2": "test2"})}, - "label2!~test2", - true, - }, - } - - for _, test := range tests { - actual := silenceMatchesFilterLabels(test.silence, []*labels.Matcher{regexpNotEqual}) - msg := fmt.Sprintf("Expected %t for %s", test.expected, test.msg) - require.Equal(t, test.expected, actual, msg) - } -} - -func TestReceiversMatchFilter(t *testing.T) { - receivers := []string{"pagerduty", "slack", "pushover"} - - filter, err := regexp.Compile(fmt.Sprintf("^(?:%s)$", "push.*")) - if err != nil { - t.Errorf("Unexpected error %v", err) - } - require.True(t, receiversMatchFilter(receivers, filter)) - - filter, err = regexp.Compile(fmt.Sprintf("^(?:%s)$", "push")) - if err != nil { - t.Errorf("Unexpected error %v", err) - } - require.False(t, receiversMatchFilter(receivers, filter)) -} - -func TestMatchFilterLabels(t *testing.T) { - testCases := []struct { - matcher labels.MatchType - expected bool - }{ - {labels.MatchEqual, true}, - {labels.MatchRegexp, true}, - {labels.MatchNotEqual, false}, - {labels.MatchNotRegexp, false}, - } - - for _, tc := range testCases { - l, err := labels.NewMatcher(tc.matcher, "foo", "") - require.NoError(t, err) - sms := map[string]string{ - "baz": "bar", - } - ls := []*labels.Matcher{l} - - require.Equal(t, tc.expected, matchFilterLabels(ls, sms)) - - l, err = labels.NewMatcher(tc.matcher, "foo", "") - require.NoError(t, err) - sms = map[string]string{ - "baz": "bar", - "foo": "quux", - } - ls = []*labels.Matcher{l} - require.NotEqual(t, tc.expected, matchFilterLabels(ls, sms)) - } -} - -func newMatcher(labelSet model.LabelSet) labels.Matchers { - matchers := make([]*labels.Matcher, 0, len(labelSet)) - for key, val := range labelSet { - matchers = append(matchers, &labels.Matcher{ - Type: labels.MatchEqual, - Name: string(key), - Value: string(val), - }) - } - return matchers -} diff --git a/api/v1_deprecation_router.go b/api/v1_deprecation_router.go new file mode 100644 index 0000000000..c4e690d486 --- /dev/null +++ b/api/v1_deprecation_router.go @@ -0,0 +1,70 @@ +// Copyright 2023 Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific l + +package api + +import ( + "encoding/json" + "net/http" + + "github.com/go-kit/log" + "github.com/go-kit/log/level" + "github.com/prometheus/common/route" +) + +// V1DeprecationRouter is the router to signal v1 users that the API v1 is now removed. +type V1DeprecationRouter struct { + logger log.Logger +} + +// NewV1DeprecationRouter returns a new V1DeprecationRouter. +func NewV1DeprecationRouter(l log.Logger) *V1DeprecationRouter { + return &V1DeprecationRouter{ + logger: l, + } +} + +// Register registers all the API v1 routes with an endpoint that returns a JSON deprecation notice and a logs a warning. +func (dr *V1DeprecationRouter) Register(r *route.Router) { + r.Get("/status", dr.deprecationHandler) + r.Get("/receivers", dr.deprecationHandler) + + r.Get("/alerts", dr.deprecationHandler) + r.Post("/alerts", dr.deprecationHandler) + + r.Get("/silences", dr.deprecationHandler) + r.Post("/silences", dr.deprecationHandler) + r.Get("/silence/:sid", dr.deprecationHandler) + r.Del("/silence/:sid", dr.deprecationHandler) +} + +func (dr *V1DeprecationRouter) deprecationHandler(w http.ResponseWriter, req *http.Request) { + level.Warn(dr.logger).Log("msg", "v1 API received a request on a removed endpoint", "path", req.URL.Path, "method", req.Method) + + resp := struct { + Status string `json:"status"` + Error string `json:"error"` + }{ + "deprecated", + "The Alertmanager v1 API was deprecated in version 0.16.0 and entirely removed since version 0.28.0 - please use the equivalent route in the v2 API", + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(410) + + // We don't care about errors for this route. + b, _ := json.Marshal(resp) + + if _, err := w.Write(b); err != nil { + level.Error(dr.logger).Log("msg", "failed to write data to connection", "err", err) + } +} diff --git a/api/v2/api.go b/api/v2/api.go index 1ddb2bcbae..74dd25a27a 100644 --- a/api/v2/api.go +++ b/api/v2/api.go @@ -97,7 +97,7 @@ func NewAPI( peer: peer, silences: silences, logger: l, - m: metrics.NewAlerts("v2", r), + m: metrics.NewAlerts(r), uptime: time.Now(), } diff --git a/test/with_api_v1/acceptance.go b/test/with_api_v1/acceptance.go deleted file mode 100644 index 4f5ecd156f..0000000000 --- a/test/with_api_v1/acceptance.go +++ /dev/null @@ -1,432 +0,0 @@ -// Copyright 2015 Prometheus Team -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package test - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net" - "net/http" - "os" - "os/exec" - "path/filepath" - "sync" - "syscall" - "testing" - "time" - - "github.com/prometheus/client_golang/api" - "github.com/prometheus/common/model" -) - -// AcceptanceTest provides declarative definition of given inputs and expected -// output of an Alertmanager setup. -type AcceptanceTest struct { - *testing.T - - opts *AcceptanceOpts - - ams []*Alertmanager - collectors []*Collector - - actions map[float64][]func() -} - -// AcceptanceOpts defines configuration parameters for an acceptance test. -type AcceptanceOpts struct { - RoutePrefix string - Tolerance time.Duration - baseTime time.Time -} - -func (opts *AcceptanceOpts) alertString(a *model.Alert) string { - if a.EndsAt.IsZero() { - return fmt.Sprintf("%s[%v:]", a, opts.relativeTime(a.StartsAt)) - } - return fmt.Sprintf("%s[%v:%v]", a, opts.relativeTime(a.StartsAt), opts.relativeTime(a.EndsAt)) -} - -// expandTime returns the absolute time for the relative time -// calculated from the test's base time. -func (opts *AcceptanceOpts) expandTime(rel float64) time.Time { - return opts.baseTime.Add(time.Duration(rel * float64(time.Second))) -} - -// expandTime returns the relative time for the given time -// calculated from the test's base time. -func (opts *AcceptanceOpts) relativeTime(act time.Time) float64 { - return float64(act.Sub(opts.baseTime)) / float64(time.Second) -} - -// NewAcceptanceTest returns a new acceptance test with the base time -// set to the current time. -func NewAcceptanceTest(t *testing.T, opts *AcceptanceOpts) *AcceptanceTest { - test := &AcceptanceTest{ - T: t, - opts: opts, - actions: map[float64][]func(){}, - } - - return test -} - -// freeAddress returns a new listen address not currently in use. -func freeAddress() string { - // Let the OS allocate a free address, close it and hope - // it is still free when starting Alertmanager. - l, err := net.Listen("tcp4", "localhost:0") - if err != nil { - panic(err) - } - defer func() { - if err := l.Close(); err != nil { - panic(err) - } - }() - - return l.Addr().String() -} - -// Do sets the given function to be executed at the given time. -func (t *AcceptanceTest) Do(at float64, f func()) { - t.actions[at] = append(t.actions[at], f) -} - -// Alertmanager returns a new structure that allows starting an instance -// of Alertmanager on a random port. -func (t *AcceptanceTest) Alertmanager(conf string) *Alertmanager { - am := &Alertmanager{ - t: t, - opts: t.opts, - } - - dir, err := os.MkdirTemp("", "am_test") - if err != nil { - t.Fatal(err) - } - am.dir = dir - - cf, err := os.Create(filepath.Join(dir, "config.yml")) - if err != nil { - t.Fatal(err) - } - am.confFile = cf - am.UpdateConfig(conf) - - am.apiAddr = freeAddress() - am.clusterAddr = freeAddress() - - t.Logf("AM on %s", am.apiAddr) - - c, err := api.NewClient(api.Config{ - Address: am.getURL(""), - }) - if err != nil { - t.Fatal(err) - } - am.client = c - - t.ams = append(t.ams, am) - - return am -} - -// Collector returns a new collector bound to the test instance. -func (t *AcceptanceTest) Collector(name string) *Collector { - co := &Collector{ - t: t.T, - name: name, - opts: t.opts, - collected: map[float64][]model.Alerts{}, - expected: map[Interval][]model.Alerts{}, - } - t.collectors = append(t.collectors, co) - - return co -} - -// Run starts all Alertmanagers and runs queries against them. It then checks -// whether all expected notifications have arrived at the expected receiver. -func (t *AcceptanceTest) Run() { - errc := make(chan error) - - for _, am := range t.ams { - am.errc = errc - - am.Start() - defer func(am *Alertmanager) { - am.Terminate() - am.cleanup() - t.Logf("stdout:\n%v", am.cmd.Stdout) - t.Logf("stderr:\n%v", am.cmd.Stderr) - }(am) - } - - // Set the reference time right before running the test actions to avoid - // test failures due to slow setup of the test environment. - t.opts.baseTime = time.Now() - - go t.runActions() - - var latest float64 - for _, coll := range t.collectors { - if l := coll.latest(); l > latest { - latest = l - } - } - - deadline := t.opts.expandTime(latest) - - select { - case <-time.After(time.Until(deadline)): - // continue - case err := <-errc: - t.Error(err) - } - - for _, coll := range t.collectors { - report := coll.check() - t.Log(report) - } -} - -// runActions performs the stored actions at the defined times. -func (t *AcceptanceTest) runActions() { - var wg sync.WaitGroup - - for at, fs := range t.actions { - ts := t.opts.expandTime(at) - wg.Add(len(fs)) - - for _, f := range fs { - go func(f func()) { - time.Sleep(time.Until(ts)) - f() - wg.Done() - }(f) - } - } - - wg.Wait() -} - -type buffer struct { - b bytes.Buffer - mtx sync.Mutex -} - -func (b *buffer) Write(p []byte) (int, error) { - b.mtx.Lock() - defer b.mtx.Unlock() - return b.b.Write(p) -} - -func (b *buffer) String() string { - b.mtx.Lock() - defer b.mtx.Unlock() - return b.b.String() -} - -// Alertmanager encapsulates an Alertmanager process and allows -// declaring alerts being pushed to it at fixed points in time. -type Alertmanager struct { - t *AcceptanceTest - opts *AcceptanceOpts - - apiAddr string - clusterAddr string - client api.Client - cmd *exec.Cmd - confFile *os.File - dir string - - errc chan<- error -} - -// Start the alertmanager and wait until it is ready to receive. -func (am *Alertmanager) Start() { - args := []string{ - "--config.file", am.confFile.Name(), - "--log.level", "debug", - "--web.listen-address", am.apiAddr, - "--storage.path", am.dir, - "--cluster.listen-address", am.clusterAddr, - "--cluster.settle-timeout", "0s", - } - if am.opts.RoutePrefix != "" { - args = append(args, "--web.route-prefix", am.opts.RoutePrefix) - } - cmd := exec.Command("../../../alertmanager", args...) - - if am.cmd == nil { - var outb, errb buffer - cmd.Stdout = &outb - cmd.Stderr = &errb - } else { - cmd.Stdout = am.cmd.Stdout - cmd.Stderr = am.cmd.Stderr - } - am.cmd = cmd - - if err := am.cmd.Start(); err != nil { - am.t.Fatalf("Starting alertmanager failed: %s", err) - } - - go func() { - if err := am.cmd.Wait(); err != nil { - am.errc <- err - } - }() - - time.Sleep(50 * time.Millisecond) - for i := 0; i < 10; i++ { - resp, err := http.Get(am.getURL("/")) - if err != nil { - time.Sleep(500 * time.Millisecond) - continue - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - am.t.Fatalf("Starting alertmanager failed: expected HTTP status '200', got '%d'", resp.StatusCode) - } - _, err = io.ReadAll(resp.Body) - if err != nil { - am.t.Fatalf("Starting alertmanager failed: %s", err) - } - return - } - am.t.Fatalf("Starting alertmanager failed: timeout") -} - -// Terminate kills the underlying Alertmanager process and remove intermediate -// data. -func (am *Alertmanager) Terminate() { - if err := syscall.Kill(am.cmd.Process.Pid, syscall.SIGTERM); err != nil { - am.t.Fatalf("error sending SIGTERM to Alertmanager process: %v", err) - } -} - -// Reload sends the reloading signal to the Alertmanager process. -func (am *Alertmanager) Reload() { - if err := syscall.Kill(am.cmd.Process.Pid, syscall.SIGHUP); err != nil { - am.t.Fatalf("error sending SIGHUP to Alertmanager process: %v", err) - } -} - -func (am *Alertmanager) cleanup() { - if err := os.RemoveAll(am.confFile.Name()); err != nil { - am.t.Errorf("error removing test config file %q: %v", am.confFile.Name(), err) - } -} - -// Push declares alerts that are to be pushed to the Alertmanager -// server at a relative point in time. -func (am *Alertmanager) Push(at float64, alerts ...*TestAlert) { - am.t.Do(at, func() { - var cas []APIV1Alert - for i := range alerts { - a := alerts[i].nativeAlert(am.opts) - al := APIV1Alert{ - Labels: LabelSet{}, - Annotations: LabelSet{}, - StartsAt: a.StartsAt, - EndsAt: a.EndsAt, - GeneratorURL: a.GeneratorURL, - } - for n, v := range a.Labels { - al.Labels[LabelName(n)] = LabelValue(v) - } - for n, v := range a.Annotations { - al.Annotations[LabelName(n)] = LabelValue(v) - } - cas = append(cas, al) - } - - alertAPI := NewAlertAPI(am.client) - - if err := alertAPI.Push(context.Background(), cas...); err != nil { - am.t.Errorf("Error pushing %v: %s", cas, err) - } - }) -} - -// SetSilence updates or creates the given Silence. -func (am *Alertmanager) SetSilence(at float64, sil *TestSilence) { - am.t.Do(at, func() { - var buf bytes.Buffer - if err := json.NewEncoder(&buf).Encode(sil.nativeSilence(am.opts)); err != nil { - am.t.Errorf("Error setting silence %v: %s", sil, err) - return - } - - resp, err := http.Post(am.getURL("/api/v1/silences"), "application/json", &buf) - if err != nil { - am.t.Errorf("Error setting silence %v: %s", sil, err) - return - } - defer resp.Body.Close() - - b, err := io.ReadAll(resp.Body) - if err != nil { - panic(err) - } - - var v struct { - Status string `json:"status"` - Data struct { - SilenceID string `json:"silenceId"` - } `json:"data"` - } - if err := json.Unmarshal(b, &v); err != nil || resp.StatusCode/100 != 2 { - am.t.Errorf("error setting silence %v: %s", sil, err) - return - } - sil.SetID(v.Data.SilenceID) - }) -} - -// DelSilence deletes the silence with the sid at the given time. -func (am *Alertmanager) DelSilence(at float64, sil *TestSilence) { - am.t.Do(at, func() { - req, err := http.NewRequest("DELETE", am.getURL(fmt.Sprintf("/api/v1/silence/%s", sil.ID())), nil) - if err != nil { - am.t.Errorf("Error deleting silence %v: %s", sil, err) - return - } - - resp, err := http.DefaultClient.Do(req) - if err != nil || resp.StatusCode/100 != 2 { - am.t.Errorf("Error deleting silence %v: %s", sil, err) - return - } - }) -} - -// UpdateConfig rewrites the configuration file for the Alertmanager. It does not -// initiate config reloading. -func (am *Alertmanager) UpdateConfig(conf string) { - if _, err := am.confFile.WriteString(conf); err != nil { - am.t.Fatal(err) - } - if err := am.confFile.Sync(); err != nil { - am.t.Fatal(err) - } -} - -func (am *Alertmanager) getURL(path string) string { - return fmt.Sprintf("http://%s%s%s", am.apiAddr, am.opts.RoutePrefix, path) -}