bosun.org@v0.0.0-20210513094433-e25bc3e69a1f/cmd/bosun/sched/template.go (about) 1 package sched 2 3 import ( 4 "bytes" 5 "encoding/base64" 6 "encoding/json" 7 "fmt" 8 htemplate "html/template" 9 "io" 10 "io/ioutil" 11 "math" 12 "net/http" 13 "net/url" 14 "strings" 15 "time" 16 17 "bosun.org/collect" 18 19 "bosun.org/cmd/bosun/conf" 20 "bosun.org/cmd/bosun/conf/template" 21 "bosun.org/cmd/bosun/expr" 22 "bosun.org/cmd/bosun/sched/slack" 23 "bosun.org/models" 24 "bosun.org/opentsdb" 25 "bosun.org/slog" 26 27 "github.com/jmoiron/jsonq" 28 ) 29 30 type Context struct { 31 *models.IncidentState 32 Alert *conf.Alert 33 IsEmail bool 34 Errors []string 35 36 schedule *Schedule 37 runHistory *RunHistory 38 Attachments []*models.Attachment 39 ElasticHost string 40 41 vars map[string]interface{} 42 } 43 44 func (s *Schedule) Data(rh *RunHistory, st *models.IncidentState, a *conf.Alert, isEmail bool) *Context { 45 c := Context{ 46 IncidentState: st, 47 Alert: a, 48 IsEmail: isEmail, 49 schedule: s, 50 runHistory: rh, 51 ElasticHost: "default", 52 vars: map[string]interface{}{}, 53 } 54 return &c 55 } 56 57 func (c *Context) Set(name string, value interface{}) string { 58 c.vars[name] = value 59 return "" // have to return something 60 } 61 62 func (c *Context) Get(name string) interface{} { 63 return c.vars[name] 64 } 65 66 // Note: All Context methods that can return nil must return literal nils 67 // and not typed nils when returning errors to ensure that our global template 68 // function notNil behaves correctly. Context Functions that return an object 69 // that users can dereference return nils on errors. Ones that return images or 70 // string just return the error message. 71 72 // Ack returns the URL to acknowledge an alert. 73 func (c *Context) Ack() string { 74 return c.schedule.SystemConf.MakeLink("/action", &url.Values{ 75 "type": []string{"ack"}, 76 "key": []string{c.Alert.Name + c.AlertKey.Group().String()}, 77 }) 78 } 79 80 // HostView returns the URL to the host view page. 81 func (c *Context) HostView(host string) string { 82 return c.schedule.SystemConf.MakeLink("/host", &url.Values{ 83 "time": []string{"1d-ago"}, 84 "host": []string{host}, 85 }) 86 } 87 88 // Hack so template can read IncidentId off of event. 89 func (c *Context) Last() interface{} { 90 return struct { 91 models.Event 92 IncidentId int64 93 }{c.IncidentState.Last(), c.Id} 94 } 95 96 // GetIncidentState returns an IncidentState so users can 97 // include information about previous or other Incidents in alert notifications 98 func (c *Context) GetIncidentState(id int64) *models.IncidentState { 99 is, err := c.schedule.DataAccess.State().GetIncidentState(id) 100 if err != nil { 101 c.addError(err) 102 return nil 103 } 104 return is 105 } 106 107 // Expr takes an expression in the form of a string, changes the tags to 108 // match the context of the alert, and returns a link to the expression page. 109 func (c *Context) Expr(v string) string { 110 p := url.Values{} 111 p.Add("date", c.runHistory.Start.Format(`2006-01-02`)) 112 p.Add("time", c.runHistory.Start.Format(`15:04:05`)) 113 p.Add("expr", base64.StdEncoding.EncodeToString([]byte(opentsdb.ReplaceTags(v, c.AlertKey.Group())))) 114 return c.schedule.SystemConf.MakeLink("/expr", &p) 115 } 116 117 // GraphLink takes an expression in the form of a string, and returns a link to 118 // the expression page's graph tab with the time set. 119 func (c *Context) GraphLink(v string) string { 120 p := url.Values{} 121 p.Add("expr", base64.StdEncoding.EncodeToString([]byte(v))) 122 p.Add("tab", "graph") 123 p.Add("date", c.runHistory.Start.Format(`2006-01-02`)) 124 p.Add("time", c.runHistory.Start.Format(`15:04:05`)) 125 return c.schedule.SystemConf.MakeLink("/expr", &p) 126 } 127 128 // Shorten uses Bosun's url shortner service to create a shortlink for 129 // the given url 130 func (c *Context) Shorten(link string) string { 131 id, err := c.schedule.DataAccess.Configs().ShortenLink(link) 132 if err != nil { 133 c.addError(err) 134 return "" 135 } 136 return c.schedule.SystemConf.MakeLink(fmt.Sprintf("/s/%d", id), nil) 137 } 138 139 func (c *Context) Rule() string { 140 p := url.Values{} 141 time := c.runHistory.Start 142 p.Add("alert", c.Alert.Name) 143 p.Add("fromDate", time.Format("2006-01-02")) 144 p.Add("fromTime", time.Format("15:04")) 145 p.Add("template_group", c.Tags) 146 return c.schedule.SystemConf.MakeLink("/config", &p) 147 } 148 149 func (c *Context) Incident() string { 150 return c.schedule.SystemConf.MakeLink("/incident", &url.Values{ 151 "id": []string{fmt.Sprint(c.Id)}, 152 }) 153 } 154 155 func (c *Context) UseElastic(host string) interface{} { 156 c.ElasticHost = host 157 return nil 158 } 159 160 func (s *Schedule) ExecuteBody(rh *RunHistory, a *conf.Alert, st *models.IncidentState, isEmail bool) (string, []*models.Attachment, error) { 161 t := a.Template 162 if t == nil { 163 return "", nil, nil 164 } 165 tp := t.Body 166 if isEmail && t.CustomTemplates["emailBody"] != nil { 167 tp = t.CustomTemplates["emailBody"] 168 } 169 if tp == nil { 170 return "", nil, nil 171 } 172 c := s.Data(rh, st, a, isEmail) 173 return s.executeTpl(tp, c) 174 } 175 176 func (s *Schedule) executeTpl(t *template.Template, c *Context) (string, []*models.Attachment, error) { 177 buf := new(bytes.Buffer) 178 if err := t.Execute(buf, c); err != nil { 179 return "", nil, err 180 } 181 return buf.String(), c.Attachments, nil 182 } 183 184 func (s *Schedule) ExecuteSubject(rh *RunHistory, a *conf.Alert, st *models.IncidentState, isEmail bool) (string, error) { 185 t := a.Template 186 if t == nil { 187 return "", nil 188 } 189 tp := t.Subject 190 if isEmail && t.CustomTemplates["emailSubject"] != nil { 191 tp = t.CustomTemplates["emailSubject"] 192 } 193 if tp == nil { 194 return "", nil 195 } 196 c := s.Data(rh, st, a, isEmail) 197 d, _, err := s.executeTpl(tp, c) 198 if err != nil { 199 return "", err 200 } 201 // remove extra whitespace 202 d = strings.Join(strings.Fields(d), " ") 203 return d, nil 204 } 205 206 func (s *Schedule) ExecuteAll(rh *RunHistory, a *conf.Alert, st *models.IncidentState, recordTimes bool) (*models.RenderedTemplates, []error) { 207 ctx := func() *Context { return s.Data(rh, st, a, false) } 208 var errs []error 209 var timer func() 210 var category string 211 e := func(err error) { 212 if timer != nil { 213 timer() 214 } 215 if err != nil { 216 errs = append(errs, fmt.Errorf("%s: %s", category, err)) 217 } 218 } 219 t := a.Template 220 rt := &models.RenderedTemplates{} 221 222 if t == nil { 223 return rt, nil 224 } 225 var err error 226 227 start := func(t string) func() { 228 category = t 229 if !recordTimes { 230 return nil 231 } 232 return collect.StartTimer("template.render", opentsdb.TagSet{"alert": a.Name, "type": t}) 233 } 234 235 // subject 236 timer = start("subject") 237 subject, err := s.ExecuteSubject(rh, a, st, false) 238 e(err) 239 st.Subject = subject 240 rt.Subject = subject 241 // body 242 timer = start("body") 243 body, atts, err := s.ExecuteBody(rh, a, st, false) 244 e(err) 245 rt.Body = body 246 rt.Attachments = atts 247 248 timer = start("emailsubject") 249 emailSubject, err := s.ExecuteSubject(rh, a, st, true) 250 e(err) 251 rt.EmailSubject = []byte(emailSubject) 252 253 timer = start("emailbody") 254 emailBody, atts, err := s.ExecuteBody(rh, a, st, true) 255 e(err) 256 rt.EmailBody = []byte(emailBody) 257 rt.Attachments = atts 258 259 rt.Custom = map[string]string{} 260 for k, v := range a.AlertTemplateKeys { 261 // emailsubject/body get handled specially above 262 if k == "emailBody" || k == "emailSubject" || k == "body" || k == "subject" { 263 continue 264 } 265 c := ctx() 266 timer = start(k) 267 rendered, _, err := s.executeTpl(v, c) 268 e(err) 269 rt.Custom[k] = rendered 270 } 271 return rt, errs 272 } 273 274 var error_body = template.Must(template.New("body_error_template").Parse(` 275 <p>There was a runtime error processing alert {{.State.AlertKey}} using the {{.Alert.Template.Name}} template. The following errors occurred:</p> 276 <ul> 277 {{range .Errors}} 278 <li>{{.}}</li> 279 {{end}} 280 </ul> 281 <p>Use <a href="{{.Rule}}">this link</a> to the rule page to correct this.</p> 282 <h2>Generic Alert Information</h2> 283 <p>Status: {{.Last.Status}}</p> 284 <p>Alert: {{.State.AlertKey}}</p> 285 <h3>Computations</h3> 286 <table> 287 <tr> 288 <th style="text-align:left">Expression</th> 289 <th style="text-align:left">Value</th> 290 </tr> 291 {{range .Computations}} 292 <tr> 293 <td style="text-align:left">{{.Text}}</td> 294 <td style="text-align:left">{{.Value}}</td> 295 </tr> 296 {{end}}</table>`)) 297 298 func (s *Schedule) ExecuteBadTemplate(errs []error, rh *RunHistory, a *conf.Alert, st *models.IncidentState) (subject, body string, err error) { 299 sub := fmt.Sprintf("error: template rendering error for alert %v", st.AlertKey) 300 c := struct { 301 Errors []error 302 *Context 303 }{ 304 Errors: errs, 305 Context: s.Data(rh, st, a, true), 306 } 307 buf := new(bytes.Buffer) 308 error_body.Execute(buf, c) 309 return sub, buf.String(), nil 310 } 311 312 func (c *Context) evalExpr(e *expr.Expr, filter bool, series bool, autods int) (expr.ResultSlice, string, error) { 313 var err error 314 if filter { 315 e, err = expr.New(opentsdb.ReplaceTags(e.Text, c.AlertKey.Group()), c.schedule.RuleConf.GetFuncs(c.schedule.SystemConf.EnabledBackends())) 316 if err != nil { 317 return nil, "", err 318 } 319 } 320 if series && e.Root.Return() != models.TypeSeriesSet { 321 return nil, "", fmt.Errorf("need a series, got %T (%v)", e, e) 322 } 323 providers := &expr.BosunProviders{ 324 Cache: c.runHistory.Cache, 325 Search: c.schedule.Search, 326 Squelched: c.schedule.RuleConf.AlertSquelched(c.Alert), 327 History: c.schedule, 328 } 329 origin := fmt.Sprintf("Template: Alert Key: %v", c.AlertKey) 330 res, _, err := e.Execute(c.runHistory.Backends, providers, nil, c.runHistory.Start, autods, c.Alert.UnjoinedOK, origin) 331 if err != nil { 332 return nil, "", fmt.Errorf("%s: %v", e, err) 333 } 334 return res.Results, e.String(), nil 335 } 336 337 // eval takes an expression or string (which it turns into an expression), executes it and returns the result. 338 // It can also takes a ResultSlice so callers can transparantly handle different inputs. 339 // The filter argument constrains the result to matching tags in the current context. 340 // The series argument asserts that the result is a time series. 341 func (c *Context) eval(v interface{}, filter bool, series bool, autods int) (res expr.ResultSlice, title string, err error) { 342 switch v := v.(type) { 343 case string: 344 var e *expr.Expr 345 e, err = expr.New(v, c.schedule.RuleConf.GetFuncs(c.schedule.SystemConf.EnabledBackends())) 346 if err != nil { 347 return nil, "", fmt.Errorf("%s: %v", v, err) 348 } 349 res, title, err = c.evalExpr(e, filter, series, autods) 350 if err != nil { 351 return 352 } 353 case *expr.Expr: 354 res, title, err = c.evalExpr(v, filter, series, autods) 355 if err != nil { 356 return 357 } 358 case expr.ResultSlice: 359 res = v 360 default: 361 return nil, "", fmt.Errorf("expected string, expression or resultslice, got %T (%v)", v, v) 362 } 363 if filter { 364 res = res.Filter(c.AlertKey.Group()) 365 } 366 if series { 367 for _, k := range res { 368 if k.Type() != models.TypeSeriesSet { 369 return nil, "", fmt.Errorf("need a series, got %v (%v)", k.Type(), k) 370 } 371 } 372 } 373 return res, title, err 374 } 375 376 // Lookup returns the value for a key in the lookup table for the context's tagset. 377 // the returned string may be the representation of an error 378 func (c *Context) Lookup(table, key string) string { 379 return c.LookupAll(table, key, c.AlertKey.Group()) 380 } 381 382 func (c *Context) LookupAll(table, key string, group interface{}) string { 383 var t opentsdb.TagSet 384 switch v := group.(type) { 385 case string: 386 var err error 387 t, err = opentsdb.ParseTags(v) 388 if err != nil { 389 c.addError(err) 390 return err.Error() 391 } 392 case opentsdb.TagSet: 393 t = v 394 } 395 l := c.schedule.RuleConf.GetLookup(table) 396 if l == nil { 397 err := fmt.Errorf("unknown lookup table %v", table) 398 c.addError(err) 399 return err.Error() 400 } 401 if v, ok := l.ToExpr().Get(key, t); ok { 402 return v 403 } 404 err := fmt.Errorf("no entry for key %v in table %v for tagset %v", key, table, c.AlertKey.Group()) 405 c.addError(err) 406 return err.Error() 407 } 408 409 func (c *Context) addError(e error) { 410 c.Errors = append(c.Errors, e.Error()) 411 } 412 413 // LastError gets the most recent error string for the context's 414 // Error slice or returns an empty string if the error slice is 415 // empty 416 func (c *Context) LastError() string { 417 if len(c.Errors) > 0 { 418 return c.Errors[len(c.Errors)-1] 419 } 420 return "" 421 } 422 423 // Eval takes a result or an expression which it evaluates to a result. 424 // It returns a value with tags corresponding to the context's tags. 425 // If no such result is found, the first result with 426 // nil tags is returned. If no such result is found, nil is returned. 427 func (c *Context) Eval(v interface{}) interface{} { 428 res, _, err := c.eval(v, true, false, 0) 429 if err != nil { 430 c.addError(err) 431 return nil 432 } 433 if len(res) == 0 { 434 return math.NaN() 435 } 436 // TODO: don't choose a random result, make sure there's exactly 1 437 return res[0].Value 438 } 439 440 // EvalAll returns the executed expression (or the given result as is). 441 func (c *Context) EvalAll(v interface{}) interface{} { 442 res, _, err := c.eval(v, false, false, 0) 443 if err != nil { 444 c.addError(err) 445 return nil 446 } 447 return res 448 } 449 450 func (c *Context) graph(v interface{}, unit string, filter bool) (val interface{}) { 451 defer func() { 452 if p := recover(); p != nil { 453 err := fmt.Errorf("panic rendering graph %v", p) 454 c.addError(err) 455 slog.Error(err) 456 val = err.Error() 457 } 458 }() 459 res, exprText, err := c.eval(v, filter, true, 1000) 460 if err != nil { 461 c.addError(err) 462 return err.Error() 463 } 464 var buf bytes.Buffer 465 const width = 800 466 const height = 600 467 footerHTML := fmt.Sprintf(`<p><small>Query: %s<br>Time: %s</small></p>`, 468 htemplate.HTMLEscapeString(exprText), 469 c.runHistory.Start.Format(time.RFC3339)) 470 if c.IsEmail { 471 err := c.schedule.ExprPNG(nil, &buf, width, height, unit, res) 472 if err != nil { 473 c.addError(err) 474 return err.Error() 475 } 476 name := fmt.Sprintf("%d.png", len(c.Attachments)+1) 477 c.Attachments = append(c.Attachments, &models.Attachment{ 478 Data: buf.Bytes(), 479 Filename: name, 480 ContentType: "image/png", 481 }) 482 return htemplate.HTML(fmt.Sprintf(`<a href="%s" style="text-decoration: none"><img alt="%s" src="cid:%s" /></a>%s`, 483 c.GraphLink(exprText), 484 htemplate.HTMLEscapeString(fmt.Sprint(v)), 485 name, 486 footerHTML, 487 )) 488 } 489 buf.WriteString(fmt.Sprintf(`<a href="%s" style="text-decoration: none">`, c.GraphLink(exprText))) 490 if err := c.schedule.ExprSVG(nil, &buf, width, height, unit, res); err != nil { 491 c.addError(err) 492 return err.Error() 493 } 494 buf.WriteString(`</a>`) 495 buf.WriteString(footerHTML) 496 return htemplate.HTML(buf.String()) 497 } 498 499 // Graph returns an SVG for the given result (or expression, for which it gets the result) 500 // with same tags as the context's tags. 501 func (c *Context) Graph(v interface{}, args ...string) interface{} { 502 var unit string 503 if len(args) > 0 { 504 unit = args[0] 505 } 506 return c.graph(v, unit, true) 507 } 508 509 // GraphAll returns an SVG for the given result (or expression, for which it gets the result). 510 func (c *Context) GraphAll(v interface{}, args ...string) interface{} { 511 var unit string 512 if len(args) > 0 { 513 unit = args[0] 514 } 515 return c.graph(v, unit, false) 516 } 517 518 // GetMeta fetches either metric metadata (if a metric name is provided) 519 // or metadata about a tagset key by name 520 func (c *Context) GetMeta(metric, name string, v interface{}) interface{} { 521 var t opentsdb.TagSet 522 switch v := v.(type) { 523 case string: 524 if v == "" { 525 t = make(opentsdb.TagSet) 526 } else { 527 var err error 528 t, err = opentsdb.ParseTags(v) 529 if err != nil { 530 c.addError(err) 531 return nil 532 } 533 } 534 case opentsdb.TagSet: 535 t = v 536 } 537 meta, err := c.schedule.GetMetadata(metric, t) 538 if err != nil && name == "" { 539 c.addError(err) 540 return nil 541 } 542 if err != nil { 543 return err.Error() 544 } 545 if name == "" { 546 return meta 547 } 548 for _, m := range meta { 549 if m.Name == name { 550 return m.Value 551 } 552 } 553 return "metadata not found" 554 } 555 556 // LeftJoin takes slices of results and expressions for which it gets the slices of results. 557 // Then it joins the 2nd and higher slice of results onto the first slice of results. 558 // Joining is performed by group: a group that includes all tags (with same values) of the first group is a match. 559 func (c *Context) LeftJoin(v ...interface{}) (interface{}, error) { 560 if len(v) < 2 { 561 // A template error is thrown here since this should be caught when defining at testing the template 562 return nil, fmt.Errorf("need at least two values (each can be an expression or result slice), got %v", len(v)) 563 } 564 // temporarily store the results in a results[M][Ni] Result matrix: 565 // for M queries, tracks Ni results per each i'th query 566 results := make([][]*expr.Result, len(v)) 567 for col, val := range v { 568 queryResults, _, err := c.eval(val, false, false, 0) 569 if err != nil { 570 c.addError(err) 571 return nil, nil 572 } 573 results[col] = queryResults 574 } 575 576 // perform the joining by storing all results in a joined[N0][M] Result matrix: 577 // for N tagsets (based on first query results), tracks all M Results (results with matching group, from all other queries) 578 joined := make([][]*expr.Result, 0) 579 for row, firstQueryResult := range results[0] { 580 joined = append(joined, make([]*expr.Result, len(v))) 581 joined[row][0] = firstQueryResult 582 // join results of 2nd to M queries 583 for col, queryResults := range results[1:] { 584 for _, laterQueryResult := range queryResults { 585 if firstQueryResult.Group.Subset(laterQueryResult.Group) { 586 joined[row][col+1] = laterQueryResult 587 break 588 } 589 // Fill emtpy cells with NaN Value, so calling .Value is not a nil pointer dereference 590 joined[row][col+1] = &expr.Result{Value: expr.Number(math.NaN())} 591 } 592 } 593 } 594 return joined, nil 595 } 596 597 func (c *Context) HTTPGet(url string) string { 598 resp, err := DefaultClient.Get(url) 599 if err != nil { 600 c.addError(err) 601 return err.Error() 602 } 603 defer resp.Body.Close() 604 if resp.StatusCode >= 300 { 605 // Drain up to 512 bytes and close the body to let the Transport reuse the connection 606 io.CopyN(ioutil.Discard, resp.Body, 512) 607 err := fmt.Errorf("%v: returned %v", url, resp.Status) 608 c.addError(err) 609 return err.Error() 610 } 611 body, err := ioutil.ReadAll(resp.Body) 612 if err != nil { 613 c.addError(err) 614 return err.Error() 615 } 616 return string(body) 617 } 618 619 func (c *Context) HTTPGetJSON(url string) *jsonq.JsonQuery { 620 req, err := http.NewRequest("GET", url, nil) 621 if err != nil { 622 c.addError(err) 623 return nil 624 } 625 req.Header.Set("Accept", "application/json") 626 resp, err := DefaultClient.Do(req) 627 if err != nil { 628 c.addError(err) 629 return nil 630 } 631 defer resp.Body.Close() 632 if resp.StatusCode >= 300 { 633 c.addError(fmt.Errorf("%v: returned %v", url, resp.Status)) 634 } 635 body, err := ioutil.ReadAll(resp.Body) 636 if err != nil { 637 c.addError(err) 638 return nil 639 } 640 data := make(map[string]interface{}) 641 err = json.Unmarshal(body, &data) 642 if err != nil { 643 c.addError(err) 644 return nil 645 } 646 return jsonq.NewQuery(data) 647 } 648 649 func (c *Context) HTTPPost(url, bodyType, data string) string { 650 resp, err := DefaultClient.Post(url, bodyType, bytes.NewBufferString(data)) 651 if err != nil { 652 c.addError(err) 653 return err.Error() 654 } 655 defer resp.Body.Close() 656 if resp.StatusCode >= 300 { 657 // Drain up to 512 bytes and close the body to let the Transport reuse the connection 658 io.CopyN(ioutil.Discard, resp.Body, 512) 659 return fmt.Sprintf("%v: returned %v", url, resp.Status) 660 } 661 body, err := ioutil.ReadAll(resp.Body) 662 if err != nil { 663 c.addError(err) 664 return err.Error() 665 } 666 return string(body) 667 } 668 669 func (c *Context) ESQuery(indexRoot expr.ESIndexer, filter expr.ESQuery, sduration, eduration string, size int) interface{} { 670 cfg, ok := c.runHistory.Backends.ElasticHosts.Hosts[c.ElasticHost] 671 if !ok { 672 return nil 673 } 674 675 switch cfg.Version { 676 case expr.ESV2: 677 return c.esQuery2(indexRoot, filter, sduration, eduration, size) 678 case expr.ESV5: 679 return c.esQuery5(indexRoot, filter, sduration, eduration, size) 680 case expr.ESV6: 681 return c.esQuery6(indexRoot, filter, sduration, eduration, size) 682 case expr.ESV7: 683 return c.esQuery7(indexRoot, filter, sduration, eduration, size) 684 } 685 686 return nil 687 } 688 689 func (c *Context) ESQueryAll(indexRoot expr.ESIndexer, filter expr.ESQuery, sduration, eduration string, size int) interface{} { 690 cfg, ok := c.runHistory.Backends.ElasticHosts.Hosts[c.ElasticHost] 691 if !ok { 692 return nil 693 } 694 695 switch cfg.Version { 696 case expr.ESV2: 697 return c.esQueryAll2(indexRoot, filter, sduration, eduration, size) 698 case expr.ESV5: 699 return c.esQueryAll5(indexRoot, filter, sduration, eduration, size) 700 case expr.ESV6: 701 return c.esQueryAll6(indexRoot, filter, sduration, eduration, size) 702 case expr.ESV7: 703 return c.esQueryAll7(indexRoot, filter, sduration, eduration, size) 704 } 705 706 return nil 707 } 708 709 // AzureResourceLink create a link to Azure's Portal for the resource (https://portal.azure.com) 710 // given the subscription identifer (bosun expression prefix), as well as the resource type, group, 711 // and name. It uses the azrt expression function under the hood 712 func (c *Context) AzureResourceLink(prefix, rType, rsg, name string) (link string) { 713 if prefix == "" { 714 prefix = "default" 715 } 716 // Get clients so we can get the TenantId 717 clients := c.schedule.SystemConf.GetAzureMonitorContext() 718 client, ok := clients[prefix] 719 if !ok { 720 c.addError(fmt.Errorf("client/subscription %s not found", prefix)) 721 return 722 } 723 selectedResource, err := c.azureSelectResource(prefix, rType, rsg, name) 724 if err != nil { 725 c.addError(err) 726 return 727 } 728 link = fmt.Sprintf("https://portal.azure.com/#@%s/resource%s", client.TenantId, selectedResource.ID) 729 return 730 } 731 732 // AzureResourceTags returns the Azure tags associated with the resource as a map 733 func (c *Context) AzureResourceTags(prefix, rType, rsg, name string) map[string]string { 734 selectedResource, err := c.azureSelectResource(prefix, rType, rsg, name) 735 if err != nil { 736 c.addError(err) 737 return nil 738 } 739 return selectedResource.Tags 740 } 741 742 func (c *Context) azureSelectResource(prefix, rType, rsg, name string) (expr.AzureResource, error) { 743 if prefix == "" { 744 prefix = "default" 745 } 746 az := expr.AzureResource{} 747 resList, _, err := c.eval(fmt.Sprintf(`["%s"]azrt("%s")`, prefix, rType), false, false, 0) 748 if err != nil { 749 return az, err 750 } 751 if len(resList) == 0 { 752 return az, fmt.Errorf("no azure resources found for subscription %s and type %s", prefix, rType) 753 } 754 resources, ok := resList[0].Value.(expr.AzureResources) 755 if !ok { 756 return az, fmt.Errorf("failed type assertion on azure resource list") 757 } 758 if selectedResource, found := resources.Get(rType, rsg, name); found { 759 return selectedResource, nil 760 } 761 return az, fmt.Errorf("resource with type %s, group %s, and name %s not found", rType, rsg, name) 762 } 763 764 // SlackAttachment creates a new SlackAttachment with fields initalized 765 // from the IncidentState. 766 func (c *Context) SlackAttachment() *slack.Attachment { 767 return slack.NewAttachment(c.IncidentState) 768 }