alertmanager/test/cli/mock.go

318 lines
7.2 KiB
Go
Raw Normal View History

// Copyright 2019 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"
"time"
"github.com/go-openapi/strfmt"
"github.com/prometheus/alertmanager/api/v2/models"
"github.com/prometheus/alertmanager/notify/webhook"
)
// 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
createdBy string
match []string
matchRE []string
startsAt, endsAt float64
comment string
}
// 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
}
// GetMatches returns the plain matchers for the silence.
func (s TestSilence) GetMatches() []string {
return s.match
}
// 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
}
// GetMatchREs returns the regex matchers for the silence.
func (s *TestSilence) GetMatchREs() []string {
return s.matchRE
}
// Comment sets the comment to the silence.
func (s *TestSilence) Comment(c string) *TestSilence {
s.comment = c
return s
}
// SetID sets the silence ID.
func (s *TestSilence) SetID(ID string) {
s.id = ID
}
// ID gets the silence ID.
func (s *TestSilence) ID() string {
return s.id
}
// TestAlert models a model.Alert with relative times.
type TestAlert struct {
labels models.LabelSet
annotations models.LabelSet
startsAt, endsAt float64
summary string
}
// 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: models.LabelSet{},
annotations: models.LabelSet{},
}
for i := 0; i < len(keyval); i += 2 {
ln := keyval[i].(string)
lv := 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) *models.GettableAlert {
na := &models.GettableAlert{
Alert: models.Alert{
Labels: a.labels,
},
Annotations: a.annotations,
StartsAt: &strfmt.DateTime{},
EndsAt: &strfmt.DateTime{},
}
if a.startsAt > 0 {
start := strfmt.DateTime(opts.expandTime(a.startsAt))
na.StartsAt = &start
}
if a.endsAt > 0 {
end := strfmt.DateTime(opts.expandTime(a.endsAt))
na.EndsAt = &end
}
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 := keyval[i].(string)
lv := 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
}
// HasLabels returns true if the two label sets are equivalent, otherwise false.
func (a *TestAlert) HasLabels(labels models.LabelSet) bool {
return reflect.DeepEqual(a.labels, labels)
}
func equalAlerts(a, b *models.GettableAlert, opts *AcceptanceOpts) bool {
if !reflect.DeepEqual(a.Labels, b.Labels) {
return false
}
if !reflect.DeepEqual(a.Annotations, b.Annotations) {
return false
}
if !equalTime(time.Time(*a.StartsAt), time.Time(*b.StartsAt), opts) {
return false
}
if (a.EndsAt == nil) != (b.EndsAt == nil) {
return false
}
if !(a.EndsAt == nil) && !(b.EndsAt == nil) && !equalTime(time.Time(*a.EndsAt), time.Time(*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 is called early on when retrieving a notification by an
// Alertmanager. If Func returns true, the given notification is dropped.
// See sample usage in `send_test.go/TestRetry()`.
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 models.GettableAlerts
for _, a := range v.Alerts {
var (
labels = models.LabelSet{}
annotations = models.LabelSet{}
)
for k, v := range a.Labels {
labels[k] = v
}
for k, v := range a.Annotations {
annotations[k] = v
}
start := strfmt.DateTime(a.StartsAt)
end := strfmt.DateTime(a.EndsAt)
alerts = append(alerts, &models.GettableAlert{
Alert: models.Alert{
Labels: labels,
GeneratorURL: strfmt.URI(a.GeneratorURL),
},
Annotations: annotations,
StartsAt: &start,
EndsAt: &end,
})
}
ws.collector.add(alerts...)
}
func (ws *MockWebhook) Address() string {
return ws.listener.Addr().String()
}