From a0d920fc19cf7d1ff79dee9d5dbc7d48b54078ab Mon Sep 17 00:00:00 2001 From: gotjosh Date: Wed, 29 Jun 2022 13:29:16 +0100 Subject: [PATCH 1/3] Deprecate and remove api/v1/ Signed-off-by: gotjosh --- api/api.go | 18 +- api/v1/api.go | 808 -------------------- test/with_api_v1/acceptance/inhibit_test.go | 152 ---- test/with_api_v1/acceptance/send_test.go | 443 ----------- test/with_api_v1/acceptance/silence_test.go | 120 --- test/with_api_v1/acceptance/web_test.go | 42 - test/with_api_v1/collector.go | 172 ----- test/with_api_v1/helper.go | 349 --------- test/with_api_v1/helper_test.go | 467 ----------- test/with_api_v1/mock.go | 315 -------- 10 files changed, 1 insertion(+), 2885 deletions(-) delete mode 100644 api/v1/api.go delete mode 100644 test/with_api_v1/acceptance/inhibit_test.go delete mode 100644 test/with_api_v1/acceptance/send_test.go delete mode 100644 test/with_api_v1/acceptance/silence_test.go delete mode 100644 test/with_api_v1/acceptance/web_test.go delete mode 100644 test/with_api_v1/collector.go delete mode 100644 test/with_api_v1/helper.go delete mode 100644 test/with_api_v1/helper_test.go delete mode 100644 test/with_api_v1/mock.go diff --git a/api/api.go b/api/api.go index 59bfb76da6..d679f8e38e 100644 --- a/api/api.go +++ b/api/api.go @@ -25,7 +25,6 @@ import ( "github.com/prometheus/common/model" "github.com/prometheus/common/route" - apiv1 "github.com/prometheus/alertmanager/api/v1" apiv2 "github.com/prometheus/alertmanager/api/v2" "github.com/prometheus/alertmanager/cluster" "github.com/prometheus/alertmanager/config" @@ -37,7 +36,6 @@ import ( // API represents all APIs of Alertmanager. type API struct { - v1 *apiv1.API v2 *apiv2.API requestsInFlight prometheus.Gauge concurrencyLimitExceeded prometheus.Counter @@ -110,15 +108,6 @@ func New(opts Options) (*API, error) { } } - v1 := apiv1.New( - opts.Alerts, - opts.Silences, - opts.StatusFunc, - opts.Peer, - log.With(l, "version", "v1"), - opts.Registry, - ) - v2, err := apiv2.NewAPI( opts.Alerts, opts.GroupFunc, @@ -154,7 +143,6 @@ func New(opts Options) (*API, error) { } return &API{ - v1: v1, v2: v2, requestsInFlight: requestsInFlight, concurrencyLimitExceeded: concurrencyLimitExceeded, @@ -163,8 +151,7 @@ func New(opts Options) (*API, error) { }, nil } -// Register all APIs. It registers APIv1 with the provided router directly. As -// APIv2 works on the http.Handler level, this method also creates a new +// Register all APIs. 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 @@ -172,8 +159,6 @@ 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 { - api.v1.Register(r.WithPrefix("/api/v1")) - mux := http.NewServeMux() mux.Handle("/", api.limitHandler(r)) @@ -196,7 +181,6 @@ func (api *API) Register(r *route.Router, routePrefix string) *http.ServeMux { // Update config and resolve timeout of each API. APIv2 also needs // setAlertStatus to be updated. func (api *API) Update(cfg *config.Config, setAlertStatus func(model.LabelSet)) { - api.v1.Update(cfg) api.v2.Update(cfg, setAlertStatus) } diff --git a/api/v1/api.go b/api/v1/api.go deleted file mode 100644 index 39018a7589..0000000000 --- a/api/v1/api.go +++ /dev/null @@ -1,808 +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 v1 - -import ( - "encoding/json" - "errors" - "fmt" - "net/http" - "regexp" - "sort" - "sync" - "time" - - "github.com/go-kit/log" - "github.com/go-kit/log/level" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/common/model" - "github.com/prometheus/common/route" - "github.com/prometheus/common/version" - - "github.com/prometheus/alertmanager/api/metrics" - "github.com/prometheus/alertmanager/cluster" - "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/silence" - "github.com/prometheus/alertmanager/silence/silencepb" - "github.com/prometheus/alertmanager/types" -) - -var corsHeaders = map[string]string{ - "Access-Control-Allow-Headers": "Accept, Authorization, Content-Type, Origin", - "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS", - "Access-Control-Allow-Origin": "*", - "Access-Control-Expose-Headers": "Date", - "Cache-Control": "no-cache, no-store, must-revalidate", -} - -// Alert is the API representation of an alert, which is a regular alert -// annotated with silencing and inhibition info. -type Alert struct { - *model.Alert - Status types.AlertStatus `json:"status"` - Receivers []string `json:"receivers"` - Fingerprint string `json:"fingerprint"` -} - -// Enables cross-site script calls. -func setCORS(w http.ResponseWriter) { - for h, v := range corsHeaders { - w.Header().Set(h, v) - } -} - -// API provides registration of handlers for API routes. -type API struct { - alerts provider.Alerts - silences *silence.Silences - config *config.Config - route *dispatch.Route - uptime time.Time - peer cluster.ClusterPeer - logger log.Logger - m *metrics.Alerts - - getAlertStatus getAlertStatusFn - - mtx sync.RWMutex -} - -type getAlertStatusFn func(model.Fingerprint) types.AlertStatus - -// New returns a new API. -func New( - alerts provider.Alerts, - silences *silence.Silences, - sf getAlertStatusFn, - peer cluster.ClusterPeer, - l log.Logger, - r prometheus.Registerer, -) *API { - if l == nil { - l = log.NewNopLogger() - } - - return &API{ - alerts: alerts, - silences: silences, - getAlertStatus: sf, - uptime: time.Now(), - peer: peer, - logger: l, - m: metrics.NewAlerts("v1", r), - } -} - -// Register registers the API handlers under their correct routes -// in the given router. -func (api *API) Register(r *route.Router) { - wrap := func(f http.HandlerFunc) http.HandlerFunc { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - setCORS(w) - f(w, r) - }) - } - - r.Options("/*path", wrap(func(w http.ResponseWriter, r *http.Request) {})) - - r.Get("/status", wrap(api.status)) - r.Get("/receivers", wrap(api.receivers)) - - r.Get("/alerts", wrap(api.listAlerts)) - r.Post("/alerts", wrap(api.addAlerts)) - - r.Get("/silences", wrap(api.listSilences)) - r.Post("/silences", wrap(api.setSilence)) - r.Get("/silence/:sid", wrap(api.getSilence)) - r.Del("/silence/:sid", wrap(api.delSilence)) -} - -// Update sets the configuration string to a new value. -func (api *API) Update(cfg *config.Config) { - api.mtx.Lock() - defer api.mtx.Unlock() - - api.config = cfg - api.route = dispatch.NewRoute(cfg.Route, nil) -} - -type errorType string - -const ( - errorInternal errorType = "server_error" - errorBadData errorType = "bad_data" -) - -type apiError struct { - typ errorType - err error -} - -func (e *apiError) Error() string { - return fmt.Sprintf("%s: %s", e.typ, e.err) -} - -func (api *API) receivers(w http.ResponseWriter, req *http.Request) { - api.mtx.RLock() - defer api.mtx.RUnlock() - - receivers := make([]string, 0, len(api.config.Receivers)) - for _, r := range api.config.Receivers { - receivers = append(receivers, r.Name) - } - - api.respond(w, receivers) -} - -func (api *API) status(w http.ResponseWriter, req *http.Request) { - api.mtx.RLock() - - status := struct { - ConfigYAML string `json:"configYAML"` - ConfigJSON *config.Config `json:"configJSON"` - VersionInfo map[string]string `json:"versionInfo"` - Uptime time.Time `json:"uptime"` - ClusterStatus *clusterStatus `json:"clusterStatus"` - }{ - ConfigYAML: api.config.String(), - ConfigJSON: api.config, - VersionInfo: map[string]string{ - "version": version.Version, - "revision": version.Revision, - "branch": version.Branch, - "buildUser": version.BuildUser, - "buildDate": version.BuildDate, - "goVersion": version.GoVersion, - }, - Uptime: api.uptime, - ClusterStatus: getClusterStatus(api.peer), - } - - api.mtx.RUnlock() - - api.respond(w, status) -} - -type peerStatus struct { - Name string `json:"name"` - Address string `json:"address"` -} - -type clusterStatus struct { - Name string `json:"name"` - Status string `json:"status"` - Peers []peerStatus `json:"peers"` -} - -func getClusterStatus(p cluster.ClusterPeer) *clusterStatus { - if p == nil { - return nil - } - s := &clusterStatus{Name: p.Name(), Status: p.Status()} - - for _, n := range p.Peers() { - s.Peers = append(s.Peers, peerStatus{ - Name: n.Name(), - Address: n.Address(), - }) - } - return s -} - -func (api *API) listAlerts(w http.ResponseWriter, r *http.Request) { - var ( - err error - receiverFilter *regexp.Regexp - // Initialize result slice to prevent api returning `null` when there - // are no alerts present - res = []*Alert{} - matchers = []*labels.Matcher{} - ctx = r.Context() - - showActive, showInhibited bool - showSilenced, showUnprocessed bool - ) - - getBoolParam := func(name string) (bool, error) { - v := r.FormValue(name) - if v == "" { - return true, nil - } - if v == "false" { - return false, nil - } - if v != "true" { - err := fmt.Errorf("parameter %q can either be 'true' or 'false', not %q", name, v) - api.respondError(w, apiError{ - typ: errorBadData, - err: err, - }, nil) - return false, err - } - return true, nil - } - - if filter := r.FormValue("filter"); filter != "" { - matchers, err = labels.ParseMatchers(filter) - if err != nil { - api.respondError(w, apiError{ - typ: errorBadData, - err: err, - }, nil) - return - } - } - - showActive, err = getBoolParam("active") - if err != nil { - return - } - - showSilenced, err = getBoolParam("silenced") - if err != nil { - return - } - - showInhibited, err = getBoolParam("inhibited") - if err != nil { - return - } - - showUnprocessed, err = getBoolParam("unprocessed") - if err != nil { - return - } - - if receiverParam := r.FormValue("receiver"); receiverParam != "" { - receiverFilter, err = regexp.Compile("^(?:" + receiverParam + ")$") - if err != nil { - api.respondError(w, apiError{ - typ: errorBadData, - err: fmt.Errorf( - "failed to parse receiver param: %s", - receiverParam, - ), - }, nil) - return - } - } - - alerts := api.alerts.GetPending() - defer alerts.Close() - - api.mtx.RLock() - for a := range alerts.Next() { - if err = alerts.Err(); err != nil { - break - } - if err = ctx.Err(); err != nil { - break - } - - routes := api.route.Match(a.Labels) - receivers := make([]string, 0, len(routes)) - for _, r := range routes { - receivers = append(receivers, r.RouteOpts.Receiver) - } - - if receiverFilter != nil && !receiversMatchFilter(receivers, receiverFilter) { - continue - } - - if !alertMatchesFilterLabels(&a.Alert, matchers) { - continue - } - - // Continue if the alert is resolved. - if !a.Alert.EndsAt.IsZero() && a.Alert.EndsAt.Before(time.Now()) { - continue - } - - status := api.getAlertStatus(a.Fingerprint()) - - if !showActive && status.State == types.AlertStateActive { - continue - } - - if !showUnprocessed && status.State == types.AlertStateUnprocessed { - continue - } - - if !showSilenced && len(status.SilencedBy) != 0 { - continue - } - - if !showInhibited && len(status.InhibitedBy) != 0 { - continue - } - - alert := &Alert{ - Alert: &a.Alert, - Status: status, - Receivers: receivers, - Fingerprint: a.Fingerprint().String(), - } - - res = append(res, alert) - } - api.mtx.RUnlock() - - if err != nil { - api.respondError(w, apiError{ - typ: errorInternal, - err: err, - }, nil) - return - } - sort.Slice(res, func(i, j int) bool { - return res[i].Fingerprint < res[j].Fingerprint - }) - api.respond(w, res) -} - -func receiversMatchFilter(receivers []string, filter *regexp.Regexp) bool { - for _, r := range receivers { - if filter.MatchString(r) { - return true - } - } - - return false -} - -func alertMatchesFilterLabels(a *model.Alert, matchers []*labels.Matcher) bool { - sms := make(map[string]string) - for name, value := range a.Labels { - sms[string(name)] = string(value) - } - return matchFilterLabels(matchers, sms) -} - -func (api *API) addAlerts(w http.ResponseWriter, r *http.Request) { - var alerts []*types.Alert - if err := api.receive(r, &alerts); err != nil { - api.respondError(w, apiError{ - typ: errorBadData, - err: err, - }, nil) - return - } - - api.insertAlerts(w, r, alerts...) -} - -func (api *API) insertAlerts(w http.ResponseWriter, r *http.Request, alerts ...*types.Alert) { - now := time.Now() - - api.mtx.RLock() - resolveTimeout := time.Duration(api.config.Global.ResolveTimeout) - api.mtx.RUnlock() - - for _, alert := range alerts { - alert.UpdatedAt = now - - // Ensure StartsAt is set. - if alert.StartsAt.IsZero() { - if alert.EndsAt.IsZero() { - alert.StartsAt = now - } else { - alert.StartsAt = alert.EndsAt - } - } - // If no end time is defined, set a timeout after which an alert - // is marked resolved if it is not updated. - if alert.EndsAt.IsZero() { - alert.Timeout = true - alert.EndsAt = now.Add(resolveTimeout) - } - if alert.EndsAt.After(time.Now()) { - api.m.Firing().Inc() - } else { - api.m.Resolved().Inc() - } - } - - // Make a best effort to insert all alerts that are valid. - var ( - validAlerts = make([]*types.Alert, 0, len(alerts)) - validationErrs = &types.MultiError{} - ) - for _, a := range alerts { - removeEmptyLabels(a.Labels) - - if err := a.Validate(); err != nil { - validationErrs.Add(err) - api.m.Invalid().Inc() - continue - } - validAlerts = append(validAlerts, a) - } - if err := api.alerts.Put(validAlerts...); err != nil { - api.respondError(w, apiError{ - typ: errorInternal, - err: err, - }, nil) - return - } - - if validationErrs.Len() > 0 { - api.respondError(w, apiError{ - typ: errorBadData, - err: validationErrs, - }, nil) - return - } - - api.respond(w, nil) -} - -func removeEmptyLabels(ls model.LabelSet) { - for k, v := range ls { - if string(v) == "" { - delete(ls, k) - } - } -} - -func (api *API) setSilence(w http.ResponseWriter, r *http.Request) { - var sil types.Silence - if err := api.receive(r, &sil); err != nil { - api.respondError(w, apiError{ - typ: errorBadData, - err: err, - }, nil) - return - } - - // This is an API only validation, it cannot be done internally - // because the expired silence is semantically important. - // But one should not be able to create expired silences, that - // won't have any use. - if sil.Expired() { - api.respondError(w, apiError{ - typ: errorBadData, - err: errors.New("start time must not be equal to end time"), - }, nil) - return - } - - if sil.EndsAt.Before(time.Now()) { - api.respondError(w, apiError{ - typ: errorBadData, - err: errors.New("end time can't be in the past"), - }, nil) - return - } - - psil, err := silenceToProto(&sil) - if err != nil { - api.respondError(w, apiError{ - typ: errorBadData, - err: err, - }, nil) - return - } - - sid, err := api.silences.Set(psil) - if err != nil { - api.respondError(w, apiError{ - typ: errorBadData, - err: err, - }, nil) - return - } - - api.respond(w, struct { - SilenceID string `json:"silenceId"` - }{ - SilenceID: sid, - }) -} - -func (api *API) getSilence(w http.ResponseWriter, r *http.Request) { - sid := route.Param(r.Context(), "sid") - - sils, _, err := api.silences.Query(silence.QIDs(sid)) - if err != nil || len(sils) == 0 { - http.Error(w, fmt.Sprint("Error getting silence: ", err), http.StatusNotFound) - return - } - sil, err := silenceFromProto(sils[0]) - if err != nil { - api.respondError(w, apiError{ - typ: errorInternal, - err: err, - }, nil) - return - } - - api.respond(w, sil) -} - -func (api *API) delSilence(w http.ResponseWriter, r *http.Request) { - sid := route.Param(r.Context(), "sid") - - if err := api.silences.Expire(sid); err != nil { - api.respondError(w, apiError{ - typ: errorBadData, - err: err, - }, nil) - return - } - api.respond(w, nil) -} - -func (api *API) listSilences(w http.ResponseWriter, r *http.Request) { - psils, _, err := api.silences.Query() - if err != nil { - api.respondError(w, apiError{ - typ: errorInternal, - err: err, - }, nil) - return - } - - matchers := []*labels.Matcher{} - if filter := r.FormValue("filter"); filter != "" { - matchers, err = labels.ParseMatchers(filter) - if err != nil { - api.respondError(w, apiError{ - typ: errorBadData, - err: err, - }, nil) - return - } - } - - sils := []*types.Silence{} - for _, ps := range psils { - s, err := silenceFromProto(ps) - if err != nil { - api.respondError(w, apiError{ - typ: errorInternal, - err: err, - }, nil) - return - } - - if !silenceMatchesFilterLabels(s, matchers) { - continue - } - sils = append(sils, s) - } - - var active, pending, expired []*types.Silence - - for _, s := range sils { - switch s.Status.State { - case types.SilenceStateActive: - active = append(active, s) - case types.SilenceStatePending: - pending = append(pending, s) - case types.SilenceStateExpired: - expired = append(expired, s) - } - } - - sort.Slice(active, func(i, j int) bool { - return active[i].EndsAt.Before(active[j].EndsAt) - }) - sort.Slice(pending, func(i, j int) bool { - return pending[i].StartsAt.Before(pending[j].EndsAt) - }) - sort.Slice(expired, func(i, j int) bool { - return expired[i].EndsAt.After(expired[j].EndsAt) - }) - - // Initialize silences explicitly to an empty list (instead of nil) - // So that it does not get converted to "null" in JSON. - silences := []*types.Silence{} - silences = append(silences, active...) - silences = append(silences, pending...) - silences = append(silences, expired...) - - api.respond(w, silences) -} - -func silenceMatchesFilterLabels(s *types.Silence, matchers []*labels.Matcher) bool { - sms := make(map[string]string) - for _, m := range s.Matchers { - sms[m.Name] = m.Value - } - - return matchFilterLabels(matchers, sms) -} - -func matchFilterLabels(matchers []*labels.Matcher, sms map[string]string) bool { - for _, m := range matchers { - v, prs := sms[m.Name] - switch m.Type { - case labels.MatchNotRegexp, labels.MatchNotEqual: - if string(m.Value) == "" && prs { - continue - } - if !m.Matches(string(v)) { - return false - } - default: - if string(m.Value) == "" && !prs { - continue - } - if !m.Matches(string(v)) { - return false - } - } - } - - return true -} - -func silenceToProto(s *types.Silence) (*silencepb.Silence, error) { - sil := &silencepb.Silence{ - Id: s.ID, - StartsAt: s.StartsAt, - EndsAt: s.EndsAt, - UpdatedAt: s.UpdatedAt, - Comment: s.Comment, - CreatedBy: s.CreatedBy, - } - for _, m := range s.Matchers { - matcher := &silencepb.Matcher{ - Name: m.Name, - Pattern: m.Value, - } - switch m.Type { - case labels.MatchEqual: - matcher.Type = silencepb.Matcher_EQUAL - case labels.MatchNotEqual: - matcher.Type = silencepb.Matcher_NOT_EQUAL - case labels.MatchRegexp: - matcher.Type = silencepb.Matcher_REGEXP - case labels.MatchNotRegexp: - matcher.Type = silencepb.Matcher_NOT_REGEXP - } - sil.Matchers = append(sil.Matchers, matcher) - } - return sil, nil -} - -func silenceFromProto(s *silencepb.Silence) (*types.Silence, error) { - sil := &types.Silence{ - ID: s.Id, - StartsAt: s.StartsAt, - EndsAt: s.EndsAt, - UpdatedAt: s.UpdatedAt, - Status: types.SilenceStatus{ - State: types.CalcSilenceState(s.StartsAt, s.EndsAt), - }, - Comment: s.Comment, - CreatedBy: s.CreatedBy, - } - for _, m := range s.Matchers { - var t labels.MatchType - switch m.Type { - case silencepb.Matcher_EQUAL: - t = labels.MatchEqual - case silencepb.Matcher_NOT_EQUAL: - t = labels.MatchNotEqual - case silencepb.Matcher_REGEXP: - t = labels.MatchRegexp - case silencepb.Matcher_NOT_REGEXP: - t = labels.MatchNotRegexp - } - matcher, err := labels.NewMatcher(t, m.Name, m.Pattern) - if err != nil { - return nil, err - } - - sil.Matchers = append(sil.Matchers, matcher) - } - - return sil, nil -} - -type status string - -const ( - statusSuccess status = "success" - statusError status = "error" -) - -type response struct { - Status status `json:"status"` - Data interface{} `json:"data,omitempty"` - ErrorType errorType `json:"errorType,omitempty"` - Error string `json:"error,omitempty"` -} - -func (api *API) respond(w http.ResponseWriter, data interface{}) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - - b, err := json.Marshal(&response{ - Status: statusSuccess, - Data: data, - }) - if err != nil { - level.Error(api.logger).Log("msg", "Error marshaling JSON", "err", err) - return - } - - if _, err := w.Write(b); err != nil { - level.Error(api.logger).Log("msg", "failed to write data to connection", "err", err) - } -} - -func (api *API) respondError(w http.ResponseWriter, apiErr apiError, data interface{}) { - w.Header().Set("Content-Type", "application/json") - - switch apiErr.typ { - case errorBadData: - w.WriteHeader(http.StatusBadRequest) - case errorInternal: - w.WriteHeader(http.StatusInternalServerError) - default: - panic(fmt.Sprintf("unknown error type %q", apiErr.Error())) - } - - b, err := json.Marshal(&response{ - Status: statusError, - ErrorType: apiErr.typ, - Error: apiErr.err.Error(), - Data: data, - }) - if err != nil { - return - } - level.Error(api.logger).Log("msg", "API error", "err", apiErr.Error()) - - if _, err := w.Write(b); err != nil { - level.Error(api.logger).Log("msg", "failed to write data to connection", "err", err) - } -} - -func (api *API) receive(r *http.Request, v interface{}) error { - dec := json.NewDecoder(r.Body) - defer r.Body.Close() - - err := dec.Decode(v) - if err != nil { - level.Debug(api.logger).Log("msg", "Decoding request failed", "err", err) - return err - } - return nil -} diff --git a/test/with_api_v1/acceptance/inhibit_test.go b/test/with_api_v1/acceptance/inhibit_test.go deleted file mode 100644 index 3ee1fe1478..0000000000 --- a/test/with_api_v1/acceptance/inhibit_test.go +++ /dev/null @@ -1,152 +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 ( - "fmt" - "testing" - "time" - - . "github.com/prometheus/alertmanager/test/with_api_v1" -) - -func TestInhibiting(t *testing.T) { - t.Parallel() - - // This integration test checks that alerts can be inhibited and that an - // inhibited alert will be notified again as soon as the inhibiting alert - // gets resolved. - - conf := ` -route: - receiver: "default" - group_by: [] - group_wait: 1s - group_interval: 1s - repeat_interval: 1s - -receivers: -- name: "default" - webhook_configs: - - url: 'http://%s' - -inhibit_rules: -- source_match: - alertname: JobDown - target_match: - alertname: InstanceDown - equal: - - job - - zone -` - - at := NewAcceptanceTest(t, &AcceptanceOpts{ - Tolerance: 150 * time.Millisecond, - }) - - co := at.Collector("webhook") - wh := NewWebhook(co) - - am := at.Alertmanager(fmt.Sprintf(conf, wh.Address())) - - am.Push(At(1), Alert("alertname", "test1", "job", "testjob", "zone", "aa")) - am.Push(At(1), Alert("alertname", "InstanceDown", "job", "testjob", "zone", "aa")) - am.Push(At(1), Alert("alertname", "InstanceDown", "job", "testjob", "zone", "ab")) - - // This JobDown in zone aa should inhibit InstanceDown in zone aa in the - // second batch of notifications. - am.Push(At(2.2), Alert("alertname", "JobDown", "job", "testjob", "zone", "aa")) - - // InstanceDown in zone aa should fire again in the third batch of - // notifications once JobDown in zone aa gets resolved. - am.Push(At(3.6), Alert("alertname", "JobDown", "job", "testjob", "zone", "aa").Active(2.2, 3.6)) - - co.Want(Between(2, 2.5), - Alert("alertname", "test1", "job", "testjob", "zone", "aa").Active(1), - Alert("alertname", "InstanceDown", "job", "testjob", "zone", "aa").Active(1), - Alert("alertname", "InstanceDown", "job", "testjob", "zone", "ab").Active(1), - ) - - co.Want(Between(3, 3.5), - Alert("alertname", "test1", "job", "testjob", "zone", "aa").Active(1), - Alert("alertname", "InstanceDown", "job", "testjob", "zone", "ab").Active(1), - Alert("alertname", "JobDown", "job", "testjob", "zone", "aa").Active(2.2), - ) - - co.Want(Between(4, 4.5), - Alert("alertname", "test1", "job", "testjob", "zone", "aa").Active(1), - Alert("alertname", "InstanceDown", "job", "testjob", "zone", "aa").Active(1), - Alert("alertname", "InstanceDown", "job", "testjob", "zone", "ab").Active(1), - Alert("alertname", "JobDown", "job", "testjob", "zone", "aa").Active(2.2, 3.6), - ) - - at.Run() -} - -func TestAlwaysInhibiting(t *testing.T) { - t.Parallel() - - // This integration test checks that when inhibited and inhibiting alerts - // gets resolved at the same time, the final notification contains both - // alerts. - - conf := ` -route: - receiver: "default" - group_by: [] - group_wait: 1s - group_interval: 1s - repeat_interval: 1s - -receivers: -- name: "default" - webhook_configs: - - url: 'http://%s' - -inhibit_rules: -- source_match: - alertname: JobDown - target_match: - alertname: InstanceDown - equal: - - job - - zone -` - - at := NewAcceptanceTest(t, &AcceptanceOpts{ - Tolerance: 150 * time.Millisecond, - }) - - co := at.Collector("webhook") - wh := NewWebhook(co) - - am := at.Alertmanager(fmt.Sprintf(conf, wh.Address())) - - am.Push(At(1), Alert("alertname", "InstanceDown", "job", "testjob", "zone", "aa")) - am.Push(At(1), Alert("alertname", "JobDown", "job", "testjob", "zone", "aa")) - - am.Push(At(2.6), Alert("alertname", "JobDown", "job", "testjob", "zone", "aa").Active(1, 2.6)) - am.Push(At(2.6), Alert("alertname", "InstanceDown", "job", "testjob", "zone", "aa").Active(1, 2.6)) - - co.Want(Between(2, 2.5), - Alert("alertname", "JobDown", "job", "testjob", "zone", "aa").Active(1), - ) - - co.Want(Between(3, 3.5), - Alert("alertname", "InstanceDown", "job", "testjob", "zone", "aa").Active(1, 2.6), - Alert("alertname", "JobDown", "job", "testjob", "zone", "aa").Active(1, 2.6), - ) - - at.Run() -} diff --git a/test/with_api_v1/acceptance/send_test.go b/test/with_api_v1/acceptance/send_test.go deleted file mode 100644 index 060c1da505..0000000000 --- a/test/with_api_v1/acceptance/send_test.go +++ /dev/null @@ -1,443 +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 ( - "fmt" - "testing" - "time" - - . "github.com/prometheus/alertmanager/test/with_api_v1" -) - -// This file contains acceptance tests around the basic sending logic -// for notifications, which includes batching and ensuring that each -// notification is eventually sent at least once and ideally exactly -// once. - -func TestMergeAlerts(t *testing.T) { - t.Parallel() - - conf := ` -route: - receiver: "default" - group_by: [alertname] - group_wait: 1s - group_interval: 1s - repeat_interval: 1ms - -receivers: -- name: "default" - webhook_configs: - - url: 'http://%s' - send_resolved: true -` - - at := NewAcceptanceTest(t, &AcceptanceOpts{ - Tolerance: 150 * time.Millisecond, - }) - - co := at.Collector("webhook") - wh := NewWebhook(co) - - am := at.Alertmanager(fmt.Sprintf(conf, wh.Address())) - - // Refresh an alert several times. The starting time must remain at the earliest - // point in time. - am.Push(At(1), Alert("alertname", "test").Active(1.1)) - // Another Prometheus server might be sending later but with an earlier start time. - am.Push(At(1.2), Alert("alertname", "test").Active(1)) - - co.Want(Between(2, 2.5), Alert("alertname", "test").Active(1)) - - am.Push(At(2.1), Alert("alertname", "test").Annotate("ann", "v1").Active(2)) - - co.Want(Between(3, 3.5), Alert("alertname", "test").Annotate("ann", "v1").Active(1)) - - // Annotations are always overwritten by the alert that arrived most recently. - am.Push(At(3.6), Alert("alertname", "test").Annotate("ann", "v2").Active(1.5)) - - co.Want(Between(4, 4.5), Alert("alertname", "test").Annotate("ann", "v2").Active(1)) - - // If an alert is marked resolved twice, the latest point in time must be - // set as the eventual resolve time. - am.Push(At(4.6), Alert("alertname", "test").Annotate("ann", "v2").Active(3, 4.5)) - am.Push(At(4.8), Alert("alertname", "test").Annotate("ann", "v3").Active(2.9, 4.8)) - am.Push(At(4.8), Alert("alertname", "test").Annotate("ann", "v3").Active(2.9, 4.1)) - - co.Want(Between(5, 5.5), Alert("alertname", "test").Annotate("ann", "v3").Active(1, 4.8)) - - // Reactivate an alert after a previous occurrence has been resolved. - // No overlap, no merge must occur. - am.Push(At(5.3), Alert("alertname", "test")) - - co.Want(Between(6, 6.5), Alert("alertname", "test").Active(5.3)) - - // Test against a bug which occurred after a restart. The previous occurrence of - // the alert was sent rather than the most recent one. - // - // XXX(fabxc) disabled as notification info won't be persisted. Thus, with a mesh - // notifier we lose the state in this single-node setup. - //at.Do(At(6.7), func() { - // am.Terminate() - // am.Start() - //}) - - // On restart the alert is flushed right away as the group_wait has already passed. - // However, it must be caught in the deduplication stage. - // The next attempt will be 1s later and won't be filtered in deduping. - // co.Want(Between(7.7, 8), Alert("alertname", "test").Active(5.3)) - - at.Run() -} - -func TestRepeat(t *testing.T) { - t.Parallel() - - conf := ` -route: - receiver: "default" - group_by: [alertname] - group_wait: 1s - group_interval: 1s - repeat_interval: 1ms - -receivers: -- name: "default" - webhook_configs: - - url: 'http://%s' -` - - // Create a new acceptance test that instantiates new Alertmanagers - // with the given configuration and verifies times with the given - // tolerance. - at := NewAcceptanceTest(t, &AcceptanceOpts{ - Tolerance: 150 * time.Millisecond, - }) - - // Create a collector to which alerts can be written and verified - // against a set of expected alert notifications. - co := at.Collector("webhook") - // Run something that satisfies the webhook interface to which the - // Alertmanager pushes as defined by its configuration. - wh := NewWebhook(co) - - // Create a new Alertmanager process listening to a random port - am := at.Alertmanager(fmt.Sprintf(conf, wh.Address())) - - // Declare pushes to be made to the Alertmanager at the given time. - // Times are provided in fractions of seconds. - am.Push(At(1), Alert("alertname", "test").Active(1)) - - // XXX(fabxc): disabled as long as alerts are not persisted. - // at.Do(At(1.2), func() { - // am.Terminate() - // am.Start() - // }) - am.Push(At(3.5), Alert("alertname", "test").Active(1, 3)) - - // Declare which alerts are expected to arrive at the collector within - // the defined time intervals. - co.Want(Between(2, 2.5), Alert("alertname", "test").Active(1)) - co.Want(Between(3, 3.5), Alert("alertname", "test").Active(1)) - co.Want(Between(4, 4.5), Alert("alertname", "test").Active(1, 3)) - - // Start the flow as defined above and run the checks afterwards. - at.Run() -} - -func TestRetry(t *testing.T) { - t.Parallel() - - // We create a notification config that fans out into two different - // webhooks. - // The succeeding one must still only receive the first successful - // notifications. Sending to the succeeding one must eventually succeed. - conf := ` -route: - receiver: "default" - group_by: [alertname] - group_wait: 1s - group_interval: 1s - repeat_interval: 3s - -receivers: -- name: "default" - webhook_configs: - - url: 'http://%s' - - url: 'http://%s' -` - - at := NewAcceptanceTest(t, &AcceptanceOpts{ - Tolerance: 150 * time.Millisecond, - }) - - co1 := at.Collector("webhook") - wh1 := NewWebhook(co1) - - co2 := at.Collector("webhook_failing") - wh2 := NewWebhook(co2) - - wh2.Func = func(ts float64) bool { - // Fail the first two interval periods but eventually - // succeed in the third interval after a few failed attempts. - return ts < 4.5 - } - - am := at.Alertmanager(fmt.Sprintf(conf, wh1.Address(), wh2.Address())) - - am.Push(At(1), Alert("alertname", "test1")) - - co1.Want(Between(2, 2.5), Alert("alertname", "test1").Active(1)) - co1.Want(Between(5, 5.5), Alert("alertname", "test1").Active(1)) - - co2.Want(Between(4.5, 5), Alert("alertname", "test1").Active(1)) -} - -func TestBatching(t *testing.T) { - t.Parallel() - - conf := ` -route: - receiver: "default" - group_by: [] - group_wait: 1s - group_interval: 1s - # use a value slightly below the 5s interval to avoid timing issues - repeat_interval: 4900ms - -receivers: -- name: "default" - webhook_configs: - - url: 'http://%s' -` - - at := NewAcceptanceTest(t, &AcceptanceOpts{ - Tolerance: 150 * time.Millisecond, - }) - - co := at.Collector("webhook") - wh := NewWebhook(co) - - am := at.Alertmanager(fmt.Sprintf(conf, wh.Address())) - - am.Push(At(1.1), Alert("alertname", "test1").Active(1)) - am.Push(At(1.7), Alert("alertname", "test5").Active(1)) - - co.Want(Between(2.0, 2.5), - Alert("alertname", "test1").Active(1), - Alert("alertname", "test5").Active(1), - ) - - am.Push(At(3.3), - Alert("alertname", "test2").Active(1.5), - Alert("alertname", "test3").Active(1.5), - Alert("alertname", "test4").Active(1.6), - ) - - co.Want(Between(4.1, 4.5), - Alert("alertname", "test1").Active(1), - Alert("alertname", "test5").Active(1), - Alert("alertname", "test2").Active(1.5), - Alert("alertname", "test3").Active(1.5), - Alert("alertname", "test4").Active(1.6), - ) - - // While no changes happen expect no additional notifications - // until the 5s repeat interval has ended. - - co.Want(Between(9.1, 9.5), - Alert("alertname", "test1").Active(1), - Alert("alertname", "test5").Active(1), - Alert("alertname", "test2").Active(1.5), - Alert("alertname", "test3").Active(1.5), - Alert("alertname", "test4").Active(1.6), - ) - - at.Run() -} - -func TestResolved(t *testing.T) { - t.Parallel() - - for i := 0; i < 2; i++ { - conf := ` -global: - resolve_timeout: 10s - -route: - receiver: "default" - group_by: [alertname] - group_wait: 1s - group_interval: 5s - -receivers: -- name: "default" - webhook_configs: - - url: 'http://%s' -` - - at := NewAcceptanceTest(t, &AcceptanceOpts{ - Tolerance: 150 * time.Millisecond, - }) - - co := at.Collector("webhook") - wh := NewWebhook(co) - - am := at.Alertmanager(fmt.Sprintf(conf, wh.Address())) - - am.Push(At(1), - Alert("alertname", "test", "lbl", "v1"), - Alert("alertname", "test", "lbl", "v2"), - Alert("alertname", "test", "lbl", "v3"), - ) - - co.Want(Between(2, 2.5), - Alert("alertname", "test", "lbl", "v1").Active(1), - Alert("alertname", "test", "lbl", "v2").Active(1), - Alert("alertname", "test", "lbl", "v3").Active(1), - ) - co.Want(Between(12, 13), - Alert("alertname", "test", "lbl", "v1").Active(1, 11), - Alert("alertname", "test", "lbl", "v2").Active(1, 11), - Alert("alertname", "test", "lbl", "v3").Active(1, 11), - ) - - at.Run() - } -} - -func TestResolvedFilter(t *testing.T) { - t.Parallel() - - // This integration test ensures that even though resolved alerts may not be - // notified about, they must be set as notified. Resolved alerts, even when - // filtered, have to end up in the SetNotifiesStage, otherwise when an alert - // fires again it is ambiguous whether it was resolved in between or not. - - conf := ` -global: - resolve_timeout: 10s - -route: - receiver: "default" - group_by: [alertname] - group_wait: 1s - group_interval: 5s - -receivers: -- name: "default" - webhook_configs: - - url: 'http://%s' - send_resolved: true - - url: 'http://%s' - send_resolved: false -` - - at := NewAcceptanceTest(t, &AcceptanceOpts{ - Tolerance: 150 * time.Millisecond, - }) - - co1 := at.Collector("webhook1") - wh1 := NewWebhook(co1) - - co2 := at.Collector("webhook2") - wh2 := NewWebhook(co2) - - am := at.Alertmanager(fmt.Sprintf(conf, wh1.Address(), wh2.Address())) - - am.Push(At(1), - Alert("alertname", "test", "lbl", "v1"), - Alert("alertname", "test", "lbl", "v2"), - ) - am.Push(At(3), - Alert("alertname", "test", "lbl", "v1").Active(1, 4), - Alert("alertname", "test", "lbl", "v3"), - ) - am.Push(At(8), - Alert("alertname", "test", "lbl", "v3").Active(3), - ) - - co1.Want(Between(2, 2.5), - Alert("alertname", "test", "lbl", "v1").Active(1), - Alert("alertname", "test", "lbl", "v2").Active(1), - ) - co1.Want(Between(7, 7.5), - Alert("alertname", "test", "lbl", "v1").Active(1, 4), - Alert("alertname", "test", "lbl", "v2").Active(1), - Alert("alertname", "test", "lbl", "v3").Active(3), - ) - // Notification should be sent because the v2 alert is resolved due to the time-out. - co1.Want(Between(12, 12.5), - Alert("alertname", "test", "lbl", "v2").Active(1, 11), - Alert("alertname", "test", "lbl", "v3").Active(3), - ) - - co2.Want(Between(2, 2.5), - Alert("alertname", "test", "lbl", "v1").Active(1), - Alert("alertname", "test", "lbl", "v2").Active(1), - ) - co2.Want(Between(7, 7.5), - Alert("alertname", "test", "lbl", "v2").Active(1), - Alert("alertname", "test", "lbl", "v3").Active(3), - ) - // No notification should be sent after group_interval because no new alert has been fired. - co2.Want(Between(12, 12.5)) - - at.Run() -} - -func TestReload(t *testing.T) { - t.Parallel() - - // This integration test ensures that the first alert isn't notified twice - // and repeat_interval applies after the AlertManager process has been - // reloaded. - conf := ` -route: - receiver: "default" - group_by: [] - group_wait: 1s - group_interval: 6s - repeat_interval: 10m - -receivers: -- name: "default" - webhook_configs: - - url: 'http://%s' -` - - at := NewAcceptanceTest(t, &AcceptanceOpts{ - Tolerance: 150 * time.Millisecond, - }) - - co := at.Collector("webhook") - wh := NewWebhook(co) - - am := at.Alertmanager(fmt.Sprintf(conf, wh.Address())) - - am.Push(At(1), Alert("alertname", "test1")) - at.Do(At(3), am.Reload) - am.Push(At(4), Alert("alertname", "test2")) - - co.Want(Between(2, 2.5), Alert("alertname", "test1").Active(1)) - // Timers are reset on reload regardless, so we count the 6 second group - // interval from 3 onwards. - co.Want(Between(9, 9.5), - Alert("alertname", "test1").Active(1), - Alert("alertname", "test2").Active(4), - ) - - at.Run() -} diff --git a/test/with_api_v1/acceptance/silence_test.go b/test/with_api_v1/acceptance/silence_test.go deleted file mode 100644 index df75000cad..0000000000 --- a/test/with_api_v1/acceptance/silence_test.go +++ /dev/null @@ -1,120 +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 ( - "fmt" - "testing" - "time" - - . "github.com/prometheus/alertmanager/test/with_api_v1" -) - -func TestSilencing(t *testing.T) { - t.Parallel() - - conf := ` -route: - receiver: "default" - group_by: [] - group_wait: 1s - group_interval: 1s - repeat_interval: 1ms - -receivers: -- name: "default" - webhook_configs: - - url: 'http://%s' -` - - at := NewAcceptanceTest(t, &AcceptanceOpts{ - Tolerance: 150 * time.Millisecond, - }) - - co := at.Collector("webhook") - wh := NewWebhook(co) - - am := at.Alertmanager(fmt.Sprintf(conf, wh.Address())) - - // No repeat interval is configured. Thus, we receive an alert - // notification every second. - am.Push(At(1), Alert("alertname", "test1").Active(1)) - am.Push(At(1), Alert("alertname", "test2").Active(1)) - - co.Want(Between(2, 2.5), - Alert("alertname", "test1").Active(1), - Alert("alertname", "test2").Active(1), - ) - - // Add a silence that affects the first alert. - am.SetSilence(At(2.3), Silence(2.5, 4.5).Match("alertname", "test1")) - - co.Want(Between(3, 3.5), Alert("alertname", "test2").Active(1)) - co.Want(Between(4, 4.5), Alert("alertname", "test2").Active(1)) - - // Silence should be over now and we receive both alerts again. - - co.Want(Between(5, 5.5), - Alert("alertname", "test1").Active(1), - Alert("alertname", "test2").Active(1), - ) - - at.Run() -} - -func TestSilenceDelete(t *testing.T) { - t.Parallel() - - conf := ` -route: - receiver: "default" - group_by: [] - group_wait: 1s - group_interval: 1s - repeat_interval: 1ms - -receivers: -- name: "default" - webhook_configs: - - url: 'http://%s' -` - - at := NewAcceptanceTest(t, &AcceptanceOpts{ - Tolerance: 150 * time.Millisecond, - }) - - co := at.Collector("webhook") - wh := NewWebhook(co) - - am := at.Alertmanager(fmt.Sprintf(conf, wh.Address())) - - // No repeat interval is configured. Thus, we receive an alert - // notification every second. - am.Push(At(1), Alert("alertname", "test1").Active(1)) - am.Push(At(1), Alert("alertname", "test2").Active(1)) - - // Silence everything for a long time and delete the silence after - // two iterations. - sil := Silence(1.5, 100).MatchRE("alertname", ".+") - - am.SetSilence(At(1.3), sil) - am.DelSilence(At(3.5), sil) - - co.Want(Between(3.5, 4.5), - Alert("alertname", "test1").Active(1), - Alert("alertname", "test2").Active(1), - ) - - at.Run() -} diff --git a/test/with_api_v1/acceptance/web_test.go b/test/with_api_v1/acceptance/web_test.go deleted file mode 100644 index ff6cdbc3ed..0000000000 --- a/test/with_api_v1/acceptance/web_test.go +++ /dev/null @@ -1,42 +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 test - -import ( - "testing" - - a "github.com/prometheus/alertmanager/test/with_api_v1" -) - -func TestWebWithPrefix(t *testing.T) { - t.Parallel() - - conf := ` -route: - receiver: "default" - group_by: [] - group_wait: 1s - group_interval: 1s - repeat_interval: 1h - -receivers: -- name: "default" -` - - // The test framework polls the API with the given prefix during - // Alertmanager startup and thereby ensures proper configuration. - at := a.NewAcceptanceTest(t, &a.AcceptanceOpts{RoutePrefix: "/foo"}) - at.Alertmanager(conf) - at.Run() -} diff --git a/test/with_api_v1/collector.go b/test/with_api_v1/collector.go deleted file mode 100644 index 9f94cb07df..0000000000 --- a/test/with_api_v1/collector.go +++ /dev/null @@ -1,172 +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 ( - "fmt" - "sync" - "testing" - "time" - - "github.com/prometheus/common/model" -) - -// Collector gathers alerts received by a notification receiver -// and verifies whether all arrived and within the correct time boundaries. -type Collector struct { - t *testing.T - name string - opts *AcceptanceOpts - - collected map[float64][]model.Alerts - expected map[Interval][]model.Alerts - - mtx sync.RWMutex -} - -func (c *Collector) String() string { - return c.name -} - -func batchesEqual(as, bs model.Alerts, opts *AcceptanceOpts) bool { - if len(as) != len(bs) { - return false - } - - for _, a := range as { - found := false - for _, b := range bs { - if equalAlerts(a, b, opts) { - found = true - break - } - } - if !found { - return false - } - } - return true -} - -// latest returns the latest relative point in time where a notification is -// expected. -func (c *Collector) latest() float64 { - c.mtx.RLock() - defer c.mtx.RUnlock() - var latest float64 - for iv := range c.expected { - if iv.end > latest { - latest = iv.end - } - } - return latest -} - -// Want declares that the Collector expects to receive the given alerts -// within the given time boundaries. -func (c *Collector) Want(iv Interval, alerts ...*TestAlert) { - c.mtx.Lock() - defer c.mtx.Unlock() - var nas model.Alerts - for _, a := range alerts { - nas = append(nas, a.nativeAlert(c.opts)) - } - - c.expected[iv] = append(c.expected[iv], nas) -} - -// add the given alerts to the collected alerts. -func (c *Collector) add(alerts ...*model.Alert) { - c.mtx.Lock() - defer c.mtx.Unlock() - arrival := c.opts.relativeTime(time.Now()) - - c.collected[arrival] = append(c.collected[arrival], model.Alerts(alerts)) -} - -func (c *Collector) check() string { - report := fmt.Sprintf("\ncollector %q:\n\n", c) - - c.mtx.RLock() - defer c.mtx.RUnlock() - for iv, expected := range c.expected { - report += fmt.Sprintf("interval %v\n", iv) - - var alerts []model.Alerts - for at, got := range c.collected { - if iv.contains(at) { - alerts = append(alerts, got...) - } - } - - for _, exp := range expected { - found := len(exp) == 0 && len(alerts) == 0 - - report += "---\n" - - for _, e := range exp { - report += fmt.Sprintf("- %v\n", c.opts.alertString(e)) - } - - for _, a := range alerts { - if batchesEqual(exp, a, c.opts) { - found = true - break - } - } - - if found { - report += " [ ✓ ]\n" - } else { - c.t.Fail() - report += " [ ✗ ]\n" - } - } - } - - // Detect unexpected notifications. - var totalExp, totalAct int - for _, exp := range c.expected { - for _, e := range exp { - totalExp += len(e) - } - } - for _, act := range c.collected { - for _, a := range act { - if len(a) == 0 { - c.t.Error("received empty notifications") - } - totalAct += len(a) - } - } - if totalExp != totalAct { - c.t.Fail() - report += fmt.Sprintf("\nExpected total of %d alerts, got %d", totalExp, totalAct) - } - - if c.t.Failed() { - report += "\nreceived:\n" - - for at, col := range c.collected { - for _, alerts := range col { - report += fmt.Sprintf("@ %v\n", at) - for _, a := range alerts { - report += fmt.Sprintf("- %v\n", c.opts.alertString(a)) - } - } - } - } - - return report -} diff --git a/test/with_api_v1/helper.go b/test/with_api_v1/helper.go deleted file mode 100644 index cf05e12973..0000000000 --- a/test/with_api_v1/helper.go +++ /dev/null @@ -1,349 +0,0 @@ -// Copyright 2018 The Prometheus Authors -// 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" - "net/http" - "net/url" - "time" - - "github.com/prometheus/client_golang/api" - - "github.com/prometheus/alertmanager/config" - "github.com/prometheus/alertmanager/types" -) - -const ( - apiPrefix = "/api/v1" - - epStatus = apiPrefix + "/status" - epSilence = apiPrefix + "/silence/:id" - epSilences = apiPrefix + "/silences" - epAlerts = apiPrefix + "/alerts" - - statusSuccess = "success" - statusError = "error" -) - -// ServerStatus represents the status of the AlertManager endpoint. -type ServerStatus struct { - ConfigYAML string `json:"configYAML"` - ConfigJSON *config.Config `json:"configJSON"` - VersionInfo map[string]string `json:"versionInfo"` - Uptime time.Time `json:"uptime"` - ClusterStatus *ClusterStatus `json:"clusterStatus"` -} - -// PeerStatus represents the status of a peer in the cluster. -type PeerStatus struct { - Name string `json:"name"` - Address string `json:"address"` -} - -// ClusterStatus represents the status of the cluster. -type ClusterStatus struct { - Name string `json:"name"` - Status string `json:"status"` - Peers []PeerStatus `json:"peers"` -} - -// apiClient wraps a regular client and processes successful API responses. -// Successful also includes responses that errored at the API level. -type apiClient struct { - api.Client -} - -type apiResponse struct { - Status string `json:"status"` - Data json.RawMessage `json:"data,omitempty"` - ErrorType string `json:"errorType,omitempty"` - Error string `json:"error,omitempty"` -} - -type clientError struct { - code int - msg string -} - -func (e *clientError) Error() string { - return fmt.Sprintf("%s (code: %d)", e.msg, e.code) -} - -func (c apiClient) Do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) { - resp, body, err := c.Client.Do(ctx, req) - if err != nil { - return resp, body, err - } - - code := resp.StatusCode - - var result apiResponse - if err = json.Unmarshal(body, &result); err != nil { - // Pass the returned body rather than the JSON error because some API - // endpoints return plain text instead of JSON payload. - return resp, body, &clientError{ - code: code, - msg: string(body), - } - } - - if (code/100 == 2) && (result.Status != statusSuccess) { - return resp, body, &clientError{ - code: code, - msg: "inconsistent body for response code", - } - } - - if result.Status == statusError { - err = &clientError{ - code: code, - msg: result.Error, - } - } - - return resp, []byte(result.Data), err -} - -// StatusAPI provides bindings for the Alertmanager's status API. -type StatusAPI interface { - // Get returns the server's configuration, version, uptime and cluster information. - Get(ctx context.Context) (*ServerStatus, error) -} - -// NewStatusAPI returns a status API client. -func NewStatusAPI(c api.Client) StatusAPI { - return &httpStatusAPI{client: apiClient{c}} -} - -type httpStatusAPI struct { - client api.Client -} - -func (h *httpStatusAPI) Get(ctx context.Context) (*ServerStatus, error) { - u := h.client.URL(epStatus, nil) - - req, err := http.NewRequest(http.MethodGet, u.String(), nil) - if err != nil { - return nil, fmt.Errorf("error creating request: %v", err) - } - - _, body, err := h.client.Do(ctx, req) - if err != nil { - return nil, err - } - - var ss *ServerStatus - err = json.Unmarshal(body, &ss) - - return ss, err -} - -// AlertAPI provides bindings for the Alertmanager's alert API. -type AlertAPI interface { - // List returns all the active alerts. - List(ctx context.Context, filter, receiver string, silenced, inhibited, active, unprocessed bool) ([]*ExtendedAlert, error) - // Push sends a list of alerts to the Alertmanager. - Push(ctx context.Context, alerts ...APIV1Alert) error -} - -// APIV1Alert represents an alert as expected by the AlertManager's push alert API. -type APIV1Alert struct { - Labels LabelSet `json:"labels"` - Annotations LabelSet `json:"annotations"` - StartsAt time.Time `json:"startsAt,omitempty"` - EndsAt time.Time `json:"endsAt,omitempty"` - GeneratorURL string `json:"generatorURL"` -} - -// ExtendedAlert represents an alert as returned by the AlertManager's list alert API. -type ExtendedAlert struct { - APIV1Alert - Status types.AlertStatus `json:"status"` - Receivers []string `json:"receivers"` - Fingerprint string `json:"fingerprint"` -} - -// LabelSet represents a collection of label names and values as a map. -type LabelSet map[LabelName]LabelValue - -// LabelName represents the name of a label. -type LabelName string - -// LabelValue represents the value of a label. -type LabelValue string - -// NewAlertAPI returns a new AlertAPI for the client. -func NewAlertAPI(c api.Client) AlertAPI { - return &httpAlertAPI{client: apiClient{c}} -} - -type httpAlertAPI struct { - client api.Client -} - -func (h *httpAlertAPI) List(ctx context.Context, filter, receiver string, silenced, inhibited, active, unprocessed bool) ([]*ExtendedAlert, error) { - u := h.client.URL(epAlerts, nil) - params := url.Values{} - if filter != "" { - params.Add("filter", filter) - } - params.Add("silenced", fmt.Sprintf("%t", silenced)) - params.Add("inhibited", fmt.Sprintf("%t", inhibited)) - params.Add("active", fmt.Sprintf("%t", active)) - params.Add("unprocessed", fmt.Sprintf("%t", unprocessed)) - params.Add("receiver", receiver) - u.RawQuery = params.Encode() - - req, err := http.NewRequest(http.MethodGet, u.String(), nil) - if err != nil { - return nil, fmt.Errorf("error creating request: %v", err) - } - - _, body, err := h.client.Do(ctx, req) // ignoring warnings. - if err != nil { - return nil, err - } - - var alts []*ExtendedAlert - err = json.Unmarshal(body, &alts) - - return alts, err -} - -func (h *httpAlertAPI) Push(ctx context.Context, alerts ...APIV1Alert) error { - u := h.client.URL(epAlerts, nil) - - var buf bytes.Buffer - if err := json.NewEncoder(&buf).Encode(&alerts); err != nil { - return err - } - - req, err := http.NewRequest(http.MethodPost, u.String(), &buf) - if err != nil { - return fmt.Errorf("error creating request: %v", err) - } - - _, _, err = h.client.Do(ctx, req) - return err -} - -// SilenceAPI provides bindings for the Alertmanager's silence API. -type SilenceAPI interface { - // Get returns the silence associated with the given ID. - Get(ctx context.Context, id string) (*types.Silence, error) - // Set updates or creates the given silence and returns its ID. - Set(ctx context.Context, sil types.Silence) (string, error) - // Expire expires the silence with the given ID. - Expire(ctx context.Context, id string) error - // List returns silences matching the given filter. - List(ctx context.Context, filter string) ([]*types.Silence, error) -} - -// NewSilenceAPI returns a new SilenceAPI for the client. -func NewSilenceAPI(c api.Client) SilenceAPI { - return &httpSilenceAPI{client: apiClient{c}} -} - -type httpSilenceAPI struct { - client api.Client -} - -func (h *httpSilenceAPI) Get(ctx context.Context, id string) (*types.Silence, error) { - u := h.client.URL(epSilence, map[string]string{ - "id": id, - }) - - req, err := http.NewRequest(http.MethodGet, u.String(), nil) - if err != nil { - return nil, fmt.Errorf("error creating request: %v", err) - } - - _, body, err := h.client.Do(ctx, req) - if err != nil { - return nil, err - } - - var sil types.Silence - err = json.Unmarshal(body, &sil) - - return &sil, err -} - -func (h *httpSilenceAPI) Expire(ctx context.Context, id string) error { - u := h.client.URL(epSilence, map[string]string{ - "id": id, - }) - - req, err := http.NewRequest(http.MethodDelete, u.String(), nil) - if err != nil { - return fmt.Errorf("error creating request: %v", err) - } - - _, _, err = h.client.Do(ctx, req) - return err -} - -func (h *httpSilenceAPI) Set(ctx context.Context, sil types.Silence) (string, error) { - u := h.client.URL(epSilences, nil) - - var buf bytes.Buffer - if err := json.NewEncoder(&buf).Encode(&sil); err != nil { - return "", err - } - - req, err := http.NewRequest(http.MethodPost, u.String(), &buf) - if err != nil { - return "", fmt.Errorf("error creating request: %v", err) - } - - _, body, err := h.client.Do(ctx, req) - if err != nil { - return "", err - } - - var res struct { - SilenceID string `json:"silenceId"` - } - err = json.Unmarshal(body, &res) - - return res.SilenceID, err -} - -func (h *httpSilenceAPI) List(ctx context.Context, filter string) ([]*types.Silence, error) { - u := h.client.URL(epSilences, nil) - params := url.Values{} - if filter != "" { - params.Add("filter", filter) - } - u.RawQuery = params.Encode() - - req, err := http.NewRequest(http.MethodGet, u.String(), nil) - if err != nil { - return nil, fmt.Errorf("error creating request: %v", err) - } - - _, body, err := h.client.Do(ctx, req) - if err != nil { - return nil, err - } - - var sils []*types.Silence - err = json.Unmarshal(body, &sils) - - return sils, err -} diff --git a/test/with_api_v1/helper_test.go b/test/with_api_v1/helper_test.go deleted file mode 100644 index aa12bc6de5..0000000000 --- a/test/with_api_v1/helper_test.go +++ /dev/null @@ -1,467 +0,0 @@ -// Copyright 2018 The Prometheus Authors -// 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" - "net/http" - "net/url" - "strings" - "testing" - "time" - - "github.com/prometheus/alertmanager/config" - "github.com/prometheus/alertmanager/pkg/labels" - "github.com/prometheus/alertmanager/types" -) - -type apiTest struct { - // Wrapper around the tested function. - do func() (interface{}, error) - - apiRes fakeAPIResponse - - // Expected values returned by the tested function. - res interface{} - err error -} - -// Fake HTTP client for TestAPI. -type fakeAPIClient struct { - *testing.T - ch chan fakeAPIResponse -} - -type fakeAPIResponse struct { - // Expected input values. - path string - method string - - // Values to be returned by fakeAPIClient.Do(). - err error - res interface{} -} - -func (c *fakeAPIClient) URL(ep string, args map[string]string) *url.URL { - path := ep - for k, v := range args { - path = strings.Replace(path, ":"+k, v, -1) - } - - return &url.URL{ - Host: "test:9093", - Path: path, - } -} - -func (c *fakeAPIClient) Do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) { - test := <-c.ch - - if req.URL.Path != test.path { - c.Errorf("unexpected request path: want %s, got %s", test.path, req.URL.Path) - } - if req.Method != test.method { - c.Errorf("unexpected request method: want %s, got %s", test.method, req.Method) - } - - b, err := json.Marshal(test.res) - if err != nil { - c.Fatal(err) - } - - return &http.Response{}, b, test.err -} - -func TestAPI(t *testing.T) { - client := &fakeAPIClient{T: t, ch: make(chan fakeAPIResponse, 1)} - now := time.Now() - - u, err := url.Parse("http://example.com") - if err != nil { - t.Errorf("unexpected error: %v", err) - } - statusData := &ServerStatus{ - ConfigYAML: "{}", - ConfigJSON: &config.Config{ - Global: &config.GlobalConfig{ - PagerdutyURL: &config.URL{URL: u}, - SMTPSmarthost: config.HostPort{Host: "localhost", Port: "25"}, - }, - }, - VersionInfo: map[string]string{"version": "v1"}, - Uptime: now, - ClusterStatus: &ClusterStatus{Peers: []PeerStatus{}}, - } - doStatus := func() (interface{}, error) { - api := httpStatusAPI{client: client} - return api.Get(context.Background()) - } - - alertOne := APIV1Alert{ - StartsAt: now, - EndsAt: now.Add(time.Duration(5 * time.Minute)), - Labels: LabelSet{"label1": "test1"}, - Annotations: LabelSet{"annotation1": "some text"}, - } - alerts := []*ExtendedAlert{ - { - APIV1Alert: alertOne, - Fingerprint: "1c93eec3511dc156", - Status: types.AlertStatus{ - State: types.AlertStateActive, - }, - }, - } - doAlertList := func() (interface{}, error) { - api := httpAlertAPI{client: client} - return api.List(context.Background(), "", "", false, false, false, false) - } - doAlertPush := func() (interface{}, error) { - api := httpAlertAPI{client: client} - return nil, api.Push(context.Background(), []APIV1Alert{alertOne}...) - } - - silOne := &types.Silence{ - ID: "abc", - Matchers: []*labels.Matcher{ - { - Name: "label1", - Value: "test1", - Type: labels.MatchEqual, - }, - }, - StartsAt: now, - EndsAt: now.Add(time.Duration(2 * time.Hour)), - UpdatedAt: now, - CreatedBy: "alice", - Comment: "some comment", - Status: types.SilenceStatus{ - State: "active", - }, - } - doSilenceGet := func(id string) func() (interface{}, error) { - return func() (interface{}, error) { - api := httpSilenceAPI{client: client} - return api.Get(context.Background(), id) - } - } - doSilenceSet := func(sil types.Silence) func() (interface{}, error) { - return func() (interface{}, error) { - api := httpSilenceAPI{client: client} - return api.Set(context.Background(), sil) - } - } - doSilenceExpire := func(id string) func() (interface{}, error) { - return func() (interface{}, error) { - api := httpSilenceAPI{client: client} - return nil, api.Expire(context.Background(), id) - } - } - doSilenceList := func() (interface{}, error) { - api := httpSilenceAPI{client: client} - return api.List(context.Background(), "") - } - - tests := []apiTest{ - { - do: doStatus, - apiRes: fakeAPIResponse{ - res: statusData, - path: "/api/v1/status", - method: http.MethodGet, - }, - res: statusData, - }, - { - do: doStatus, - apiRes: fakeAPIResponse{ - err: fmt.Errorf("some error"), - path: "/api/v1/status", - method: http.MethodGet, - }, - err: fmt.Errorf("some error"), - }, - { - do: doAlertList, - apiRes: fakeAPIResponse{ - res: alerts, - path: "/api/v1/alerts", - method: http.MethodGet, - }, - res: alerts, - }, - { - do: doAlertList, - apiRes: fakeAPIResponse{ - err: fmt.Errorf("some error"), - path: "/api/v1/alerts", - method: http.MethodGet, - }, - err: fmt.Errorf("some error"), - }, - { - do: doAlertPush, - apiRes: fakeAPIResponse{ - res: nil, - path: "/api/v1/alerts", - method: http.MethodPost, - }, - res: nil, - }, - { - do: doAlertPush, - apiRes: fakeAPIResponse{ - err: fmt.Errorf("some error"), - path: "/api/v1/alerts", - method: http.MethodPost, - }, - err: fmt.Errorf("some error"), - }, - { - do: doSilenceGet("abc"), - apiRes: fakeAPIResponse{ - res: silOne, - path: "/api/v1/silence/abc", - method: http.MethodGet, - }, - res: silOne, - }, - { - do: doSilenceGet("abc"), - apiRes: fakeAPIResponse{ - err: fmt.Errorf("some error"), - path: "/api/v1/silence/abc", - method: http.MethodGet, - }, - err: fmt.Errorf("some error"), - }, - { - do: doSilenceSet(*silOne), - apiRes: fakeAPIResponse{ - res: map[string]string{"SilenceId": "abc"}, - path: "/api/v1/silences", - method: http.MethodPost, - }, - res: "abc", - }, - { - do: doSilenceSet(*silOne), - apiRes: fakeAPIResponse{ - err: fmt.Errorf("some error"), - path: "/api/v1/silences", - method: http.MethodPost, - }, - err: fmt.Errorf("some error"), - }, - { - do: doSilenceExpire("abc"), - apiRes: fakeAPIResponse{ - path: "/api/v1/silence/abc", - method: http.MethodDelete, - }, - }, - { - do: doSilenceExpire("abc"), - apiRes: fakeAPIResponse{ - err: fmt.Errorf("some error"), - path: "/api/v1/silence/abc", - method: http.MethodDelete, - }, - err: fmt.Errorf("some error"), - }, - { - do: doSilenceList, - apiRes: fakeAPIResponse{ - res: []*types.Silence{silOne}, - path: "/api/v1/silences", - method: http.MethodGet, - }, - res: []*types.Silence{silOne}, - }, - { - do: doSilenceList, - apiRes: fakeAPIResponse{ - err: fmt.Errorf("some error"), - path: "/api/v1/silences", - method: http.MethodGet, - }, - err: fmt.Errorf("some error"), - }, - } - for _, test := range tests { - test := test - client.ch <- test.apiRes - t.Run(fmt.Sprintf("%s %s", test.apiRes.method, test.apiRes.path), func(t *testing.T) { - res, err := test.do() - if test.err != nil { - if err == nil { - t.Errorf("unexpected error: want: %s but got none", test.err) - return - } - if err.Error() != test.err.Error() { - t.Errorf("unexpected error: want: %s, got: %s", test.err, err) - } - return - } - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - want, err := json.Marshal(test.res) - if err != nil { - t.Fatal(err) - } - got, err := json.Marshal(res) - if err != nil { - t.Fatal(err) - } - if !bytes.Equal(want, got) { - t.Errorf("unexpected result: want: %s, got: %s", string(want), string(got)) - } - }) - } -} - -// Fake HTTP client for TestAPIClientDo. -type fakeClient struct { - *testing.T - ch chan fakeResponse -} - -type fakeResponse struct { - code int - res interface{} - err error -} - -func (c fakeClient) URL(string, map[string]string) *url.URL { - return nil -} - -func (c fakeClient) Do(context.Context, *http.Request) (*http.Response, []byte, error) { - fakeRes := <-c.ch - - if fakeRes.err != nil { - return nil, nil, fakeRes.err - } - - var b []byte - var err error - switch v := fakeRes.res.(type) { - case string: - b = []byte(v) - default: - b, err = json.Marshal(v) - if err != nil { - c.Fatal(err) - } - } - - return &http.Response{StatusCode: fakeRes.code}, b, nil -} - -type apiClientTest struct { - response fakeResponse - - expected string - err error -} - -func TestAPIClientDo(t *testing.T) { - tests := []apiClientTest{ - { - response: fakeResponse{ - code: http.StatusOK, - res: &apiResponse{ - Status: statusSuccess, - Data: json.RawMessage(`"test"`), - }, - err: nil, - }, - expected: `"test"`, - err: nil, - }, - { - response: fakeResponse{ - code: http.StatusBadRequest, - res: &apiResponse{ - Status: statusError, - Error: "some error", - }, - err: nil, - }, - err: fmt.Errorf("some error (code: 400)"), - }, - { - response: fakeResponse{ - code: http.StatusOK, - res: &apiResponse{ - Status: statusError, - Error: "some error", - }, - err: nil, - }, - err: fmt.Errorf("inconsistent body for response code (code: 200)"), - }, - { - response: fakeResponse{ - code: http.StatusNotFound, - res: "not found", - err: nil, - }, - err: fmt.Errorf("not found (code: 404)"), - }, - { - response: fakeResponse{ - err: fmt.Errorf("some error"), - }, - err: fmt.Errorf("some error"), - }, - } - - fake := fakeClient{T: t, ch: make(chan fakeResponse, 1)} - client := apiClient{fake} - - for _, test := range tests { - t.Run("", func(t *testing.T) { - fake.ch <- test.response - - _, body, err := client.Do(context.Background(), &http.Request{}) - if test.err != nil { - if err == nil { - t.Errorf("expected error %q but got none", test.err) - return - } - if test.err.Error() != err.Error() { - t.Errorf("unexpected error: want %q, got %q", test.err, err) - return - } - return - } - - if err != nil { - t.Errorf("unexpected error %q", err) - return - } - - want, got := test.expected, string(body) - if want != got { - t.Errorf("unexpected body: want %q, got %q", want, got) - } - }) - } -} diff --git a/test/with_api_v1/mock.go b/test/with_api_v1/mock.go deleted file mode 100644 index d299db9ef6..0000000000 --- a/test/with_api_v1/mock.go +++ /dev/null @@ -1,315 +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 ( - "encoding/json" - "fmt" - "net" - "net/http" - "reflect" - "sync" - "time" - - "github.com/prometheus/common/model" - - "github.com/prometheus/alertmanager/notify/webhook" - "github.com/prometheus/alertmanager/pkg/labels" - "github.com/prometheus/alertmanager/types" -) - -// At is a convenience method to allow for declarative syntax of Acceptance -// test definitions. -func At(ts float64) float64 { - return ts -} - -type Interval struct { - start, end float64 -} - -func (iv Interval) String() string { - return fmt.Sprintf("[%v,%v]", iv.start, iv.end) -} - -func (iv Interval) contains(f float64) bool { - return f >= iv.start && f <= iv.end -} - -// Between is a convenience constructor for an interval for declarative syntax -// of Acceptance test definitions. -func Between(start, end float64) Interval { - return Interval{start: start, end: end} -} - -// TestSilence models a model.Silence with relative times. -type TestSilence struct { - id string - match []string - matchRE []string - startsAt, endsAt float64 - - mtx sync.RWMutex -} - -// Silence creates a new TestSilence active for the relative interval given -// by start and end. -func Silence(start, end float64) *TestSilence { - return &TestSilence{ - startsAt: start, - endsAt: end, - } -} - -// Match adds a new plain matcher to the silence. -func (s *TestSilence) Match(v ...string) *TestSilence { - s.match = append(s.match, v...) - return s -} - -// MatchRE adds a new regex matcher to the silence -func (s *TestSilence) MatchRE(v ...string) *TestSilence { - if len(v)%2 == 1 { - panic("bad key/values") - } - s.matchRE = append(s.matchRE, v...) - return s -} - -// SetID sets the silence ID. -func (s *TestSilence) SetID(ID string) { - s.mtx.Lock() - defer s.mtx.Unlock() - s.id = ID -} - -// ID gets the silence ID. -func (s *TestSilence) ID() string { - s.mtx.RLock() - defer s.mtx.RUnlock() - return s.id -} - -// nativeSilence converts the declared test silence into a regular -// silence with resolved times. -func (s *TestSilence) nativeSilence(opts *AcceptanceOpts) *types.Silence { - nsil := &types.Silence{} - - for i := 0; i < len(s.match); i += 2 { - nsil.Matchers = append(nsil.Matchers, &labels.Matcher{ - Type: labels.MatchEqual, - Name: s.match[i], - Value: s.match[i+1], - }) - } - for i := 0; i < len(s.matchRE); i += 2 { - m, err := labels.NewMatcher(labels.MatchRegexp, s.matchRE[i], s.matchRE[i+1]) - if err != nil { - panic(err) - } - nsil.Matchers = append(nsil.Matchers, m) - } - - if s.startsAt > 0 { - nsil.StartsAt = opts.expandTime(s.startsAt) - } - if s.endsAt > 0 { - nsil.EndsAt = opts.expandTime(s.endsAt) - } - nsil.Comment = "some comment" - nsil.CreatedBy = "admin@example.com" - - return nsil -} - -// TestAlert models a model.Alert with relative times. -type TestAlert struct { - labels model.LabelSet - annotations model.LabelSet - startsAt, endsAt float64 -} - -// Alert creates a new alert declaration with the given key/value pairs -// as identifying labels. -func Alert(keyval ...interface{}) *TestAlert { - if len(keyval)%2 == 1 { - panic("bad key/values") - } - a := &TestAlert{ - labels: model.LabelSet{}, - annotations: model.LabelSet{}, - } - - for i := 0; i < len(keyval); i += 2 { - ln := model.LabelName(keyval[i].(string)) - lv := model.LabelValue(keyval[i+1].(string)) - - a.labels[ln] = lv - } - - return a -} - -// nativeAlert converts the declared test alert into a full alert based -// on the given parameters. -func (a *TestAlert) nativeAlert(opts *AcceptanceOpts) *model.Alert { - na := &model.Alert{ - Labels: a.labels, - Annotations: a.annotations, - } - - if a.startsAt > 0 { - na.StartsAt = opts.expandTime(a.startsAt) - } - if a.endsAt > 0 { - na.EndsAt = opts.expandTime(a.endsAt) - } - return na -} - -// Annotate the alert with the given key/value pairs. -func (a *TestAlert) Annotate(keyval ...interface{}) *TestAlert { - if len(keyval)%2 == 1 { - panic("bad key/values") - } - - for i := 0; i < len(keyval); i += 2 { - ln := model.LabelName(keyval[i].(string)) - lv := model.LabelValue(keyval[i+1].(string)) - - a.annotations[ln] = lv - } - - return a -} - -// Active declares the relative activity time for this alert. It -// must be a single starting value or two values where the second value -// declares the resolved time. -func (a *TestAlert) Active(tss ...float64) *TestAlert { - if len(tss) > 2 || len(tss) == 0 { - panic("only one or two timestamps allowed") - } - if len(tss) == 2 { - a.endsAt = tss[1] - } - a.startsAt = tss[0] - - return a -} - -func equalAlerts(a, b *model.Alert, opts *AcceptanceOpts) bool { - if !reflect.DeepEqual(a.Labels, b.Labels) { - return false - } - if !reflect.DeepEqual(a.Annotations, b.Annotations) { - return false - } - - if !equalTime(a.StartsAt, b.StartsAt, opts) { - return false - } - if !equalTime(a.EndsAt, b.EndsAt, opts) { - return false - } - return true -} - -func equalTime(a, b time.Time, opts *AcceptanceOpts) bool { - if a.IsZero() != b.IsZero() { - return false - } - - diff := a.Sub(b) - if diff < 0 { - diff = -diff - } - return diff <= opts.Tolerance -} - -type MockWebhook struct { - opts *AcceptanceOpts - collector *Collector - listener net.Listener - - Func func(timestamp float64) bool -} - -func NewWebhook(c *Collector) *MockWebhook { - l, err := net.Listen("tcp4", "localhost:0") - if err != nil { - // TODO(fabxc): if shutdown of mock destinations ever becomes a concern - // we want to shut them down after test completion. Then we might want to - // log the error properly, too. - panic(err) - } - wh := &MockWebhook{ - listener: l, - collector: c, - opts: c.opts, - } - go func() { - if err := http.Serve(l, wh); err != nil { - panic(err) - } - }() - - return wh -} - -func (ws *MockWebhook) ServeHTTP(w http.ResponseWriter, req *http.Request) { - // Inject Func if it exists. - if ws.Func != nil { - if ws.Func(ws.opts.relativeTime(time.Now())) { - return - } - } - - dec := json.NewDecoder(req.Body) - defer req.Body.Close() - - var v webhook.Message - if err := dec.Decode(&v); err != nil { - panic(err) - } - - // Transform the webhook message alerts back into model.Alerts. - var alerts model.Alerts - for _, a := range v.Alerts { - var ( - labels = model.LabelSet{} - annotations = model.LabelSet{} - ) - for k, v := range a.Labels { - labels[model.LabelName(k)] = model.LabelValue(v) - } - for k, v := range a.Annotations { - annotations[model.LabelName(k)] = model.LabelValue(v) - } - - alerts = append(alerts, &model.Alert{ - Labels: labels, - Annotations: annotations, - StartsAt: a.StartsAt, - EndsAt: a.EndsAt, - GeneratorURL: a.GeneratorURL, - }) - } - - ws.collector.add(alerts...) -} - -func (ws *MockWebhook) Address() string { - return ws.listener.Addr().String() -} From 119e1c46166a1f526a218fc473df7d84566f74e7 Mon Sep 17 00:00:00 2001 From: gotjosh Date: Tue, 21 Nov 2023 18:51:50 +0000 Subject: [PATCH 2/3] 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) -} From 0cf6797683ec486ae12de2314e88fcca1df8285c Mon Sep 17 00:00:00 2001 From: gotjosh Date: Wed, 22 Nov 2023 10:12:55 +0000 Subject: [PATCH 3/3] address review feedback Signed-off-by: gotjosh --- api/v1_deprecation_router.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/api/v1_deprecation_router.go b/api/v1_deprecation_router.go index c4e690d486..3ebbbd076f 100644 --- a/api/v1_deprecation_router.go +++ b/api/v1_deprecation_router.go @@ -55,16 +55,13 @@ func (dr *V1DeprecationRouter) deprecationHandler(w http.ResponseWriter, req *ht 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", + "The Alertmanager v1 API was deprecated in version 0.16.0 and is removed as of 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) + if err := json.NewEncoder(w).Encode(resp); err != nil { + level.Error(dr.logger).Log("msg", "failed to write response", "err", err) } }