1// Copyright 2019 Prometheus Team 2// Licensed under the Apache License, Version 2.0 (the "License"); 3// you may not use this file except in compliance with the License. 4// You may obtain a copy of the License at 5// 6// http://www.apache.org/licenses/LICENSE-2.0 7// 8// Unless required by applicable law or agreed to in writing, software 9// distributed under the License is distributed on an "AS IS" BASIS, 10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11// See the License for the specific language governing permissions and 12// limitations under the License. 13 14package test 15 16import ( 17 "encoding/json" 18 "fmt" 19 "net" 20 "net/http" 21 "reflect" 22 "time" 23 24 "github.com/go-openapi/strfmt" 25 26 "github.com/prometheus/alertmanager/api/v2/models" 27 "github.com/prometheus/alertmanager/notify/webhook" 28) 29 30// At is a convenience method to allow for declarative syntax of Acceptance 31// test definitions. 32func At(ts float64) float64 { 33 return ts 34} 35 36type Interval struct { 37 start, end float64 38} 39 40func (iv Interval) String() string { 41 return fmt.Sprintf("[%v,%v]", iv.start, iv.end) 42} 43 44func (iv Interval) contains(f float64) bool { 45 return f >= iv.start && f <= iv.end 46} 47 48// Between is a convenience constructor for an interval for declarative syntax 49// of Acceptance test definitions. 50func Between(start, end float64) Interval { 51 return Interval{start: start, end: end} 52} 53 54// TestSilence models a model.Silence with relative times. 55type TestSilence struct { 56 id string 57 createdBy string 58 match []string 59 matchRE []string 60 startsAt, endsAt float64 61 comment string 62} 63 64// Silence creates a new TestSilence active for the relative interval given 65// by start and end. 66func Silence(start, end float64) *TestSilence { 67 return &TestSilence{ 68 startsAt: start, 69 endsAt: end, 70 } 71} 72 73// Match adds a new plain matcher to the silence. 74func (s *TestSilence) Match(v ...string) *TestSilence { 75 s.match = append(s.match, v...) 76 return s 77} 78 79// MatchRE adds a new regex matcher to the silence 80func (s *TestSilence) MatchRE(v ...string) *TestSilence { 81 if len(v)%2 == 1 { 82 panic("bad key/values") 83 } 84 s.matchRE = append(s.matchRE, v...) 85 return s 86} 87 88// Comment sets the comment to the silence. 89func (s *TestSilence) Comment(c string) *TestSilence { 90 s.comment = c 91 return s 92} 93 94// SetID sets the silence ID. 95func (s *TestSilence) SetID(ID string) { 96 s.id = ID 97} 98 99// ID gets the silence ID. 100func (s *TestSilence) ID() string { 101 return s.id 102} 103 104// TestAlert models a model.Alert with relative times. 105type TestAlert struct { 106 labels models.LabelSet 107 annotations models.LabelSet 108 startsAt, endsAt float64 109 summary string 110} 111 112// Alert creates a new alert declaration with the given key/value pairs 113// as identifying labels. 114func Alert(keyval ...interface{}) *TestAlert { 115 if len(keyval)%2 == 1 { 116 panic("bad key/values") 117 } 118 a := &TestAlert{ 119 labels: models.LabelSet{}, 120 annotations: models.LabelSet{}, 121 } 122 123 for i := 0; i < len(keyval); i += 2 { 124 ln := keyval[i].(string) 125 lv := keyval[i+1].(string) 126 127 a.labels[ln] = lv 128 } 129 130 return a 131} 132 133// nativeAlert converts the declared test alert into a full alert based 134// on the given parameters. 135func (a *TestAlert) nativeAlert(opts *AcceptanceOpts) *models.GettableAlert { 136 na := &models.GettableAlert{ 137 Alert: models.Alert{ 138 Labels: a.labels, 139 }, 140 Annotations: a.annotations, 141 StartsAt: &strfmt.DateTime{}, 142 EndsAt: &strfmt.DateTime{}, 143 } 144 145 if a.startsAt > 0 { 146 start := strfmt.DateTime(opts.expandTime(a.startsAt)) 147 na.StartsAt = &start 148 } 149 if a.endsAt > 0 { 150 end := strfmt.DateTime(opts.expandTime(a.endsAt)) 151 na.EndsAt = &end 152 } 153 154 return na 155} 156 157// Annotate the alert with the given key/value pairs. 158func (a *TestAlert) Annotate(keyval ...interface{}) *TestAlert { 159 if len(keyval)%2 == 1 { 160 panic("bad key/values") 161 } 162 163 for i := 0; i < len(keyval); i += 2 { 164 ln := keyval[i].(string) 165 lv := keyval[i+1].(string) 166 167 a.annotations[ln] = lv 168 } 169 170 return a 171} 172 173// Active declares the relative activity time for this alert. It 174// must be a single starting value or two values where the second value 175// declares the resolved time. 176func (a *TestAlert) Active(tss ...float64) *TestAlert { 177 178 if len(tss) > 2 || len(tss) == 0 { 179 panic("only one or two timestamps allowed") 180 } 181 if len(tss) == 2 { 182 a.endsAt = tss[1] 183 } 184 a.startsAt = tss[0] 185 186 return a 187} 188 189func equalAlerts(a, b *models.GettableAlert, opts *AcceptanceOpts) bool { 190 if !reflect.DeepEqual(a.Labels, b.Labels) { 191 return false 192 } 193 if !reflect.DeepEqual(a.Annotations, b.Annotations) { 194 return false 195 } 196 197 if !equalTime(time.Time(*a.StartsAt), time.Time(*b.StartsAt), opts) { 198 return false 199 } 200 if (a.EndsAt == nil) != (b.EndsAt == nil) { 201 return false 202 } 203 if !(a.EndsAt == nil) && !(b.EndsAt == nil) && !equalTime(time.Time(*a.EndsAt), time.Time(*b.EndsAt), opts) { 204 return false 205 } 206 return true 207} 208 209func equalTime(a, b time.Time, opts *AcceptanceOpts) bool { 210 if a.IsZero() != b.IsZero() { 211 return false 212 } 213 214 diff := a.Sub(b) 215 if diff < 0 { 216 diff = -diff 217 } 218 return diff <= opts.Tolerance 219} 220 221type MockWebhook struct { 222 opts *AcceptanceOpts 223 collector *Collector 224 listener net.Listener 225 226 // Func is called early on when retrieving a notification by an 227 // Alertmanager. If Func returns true, the given notification is dropped. 228 // See sample usage in `send_test.go/TestRetry()`. 229 Func func(timestamp float64) bool 230} 231 232func NewWebhook(c *Collector) *MockWebhook { 233 l, err := net.Listen("tcp4", "localhost:0") 234 if err != nil { 235 // TODO(fabxc): if shutdown of mock destinations ever becomes a concern 236 // we want to shut them down after test completion. Then we might want to 237 // log the error properly, too. 238 panic(err) 239 } 240 wh := &MockWebhook{ 241 listener: l, 242 collector: c, 243 opts: c.opts, 244 } 245 go func() { 246 if err := http.Serve(l, wh); err != nil { 247 panic(err) 248 } 249 }() 250 251 return wh 252} 253 254func (ws *MockWebhook) ServeHTTP(w http.ResponseWriter, req *http.Request) { 255 // Inject Func if it exists. 256 if ws.Func != nil { 257 if ws.Func(ws.opts.relativeTime(time.Now())) { 258 return 259 } 260 } 261 262 dec := json.NewDecoder(req.Body) 263 defer req.Body.Close() 264 265 var v webhook.Message 266 if err := dec.Decode(&v); err != nil { 267 panic(err) 268 } 269 270 // Transform the webhook message alerts back into model.Alerts. 271 var alerts models.GettableAlerts 272 for _, a := range v.Alerts { 273 var ( 274 labels = models.LabelSet{} 275 annotations = models.LabelSet{} 276 ) 277 for k, v := range a.Labels { 278 labels[k] = v 279 } 280 for k, v := range a.Annotations { 281 annotations[k] = v 282 } 283 284 start := strfmt.DateTime(a.StartsAt) 285 end := strfmt.DateTime(a.EndsAt) 286 287 alerts = append(alerts, &models.GettableAlert{ 288 Alert: models.Alert{ 289 Labels: labels, 290 GeneratorURL: strfmt.URI(a.GeneratorURL), 291 }, 292 Annotations: annotations, 293 StartsAt: &start, 294 EndsAt: &end, 295 }) 296 } 297 298 ws.collector.add(alerts...) 299} 300 301func (ws *MockWebhook) Address() string { 302 return ws.listener.Addr().String() 303} 304