notifications/webhook.go
Rama main · réplica de lectura del repositorio original.
package notifications
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net/http"
"reflect"
"strings"
"text/template"
"github.com/coroot/coroot/utils"
"github.com/coroot/coroot/db"
"github.com/coroot/coroot/model"
)
type Webhook struct {
cfg *db.IntegrationWebhook
}
type IncidentTemplateValues struct {
Status string `json:"status"`
Application model.ApplicationId `json:"application"`
Reports []db.IncidentNotificationDetailsReport `json:"reports"`
URL string `json:"url"`
RCASummary string `json:"rca_summary,omitempty"`
RCARemediations string `json:"rca_remediations,omitempty"`
}
type DeploymentTemplateValues struct {
Status string `json:"status"`
Application model.ApplicationId `json:"application"`
Version string `json:"version"`
Summary []string `json:"summary"`
URL string `json:"url"`
}
type AlertTemplateValues struct {
Status string `json:"status"`
ProjectName string `json:"project_name"`
Application model.ApplicationId `json:"application"`
RuleName string `json:"rule_name"`
Severity string `json:"severity"`
Summary string `json:"summary"`
Details []model.AlertDetail `json:"details,omitempty"`
Duration string `json:"duration,omitempty"`
ResolvedBy string `json:"resolved_by,omitempty"`
URL string `json:"url"`
}
func NewWebhook(cfg *db.IntegrationWebhook) *Webhook {
return &Webhook{cfg: cfg}
}
func mergeCustomFields(values any, customFields map[string]string) any {
if len(customFields) == 0 {
return values
}
v := reflect.ValueOf(values)
t := v.Type()
existingFields := make(map[string]bool)
var fields []reflect.StructField
for i := range t.NumField() {
f := t.Field(i)
fields = append(fields, reflect.StructField{
Name: f.Name,
Type: f.Type,
Tag: f.Tag,
})
existingFields[f.Name] = true
}
var customKeys []string
for k := range customFields {
if k == "" {
continue
}
exported := strings.ToUpper(k[:1]) + k[1:]
if existingFields[exported] {
continue
}
customKeys = append(customKeys, k)
fields = append(fields, reflect.StructField{
Name: exported,
Type: reflect.TypeFor[string](),
Tag: reflect.StructTag(`json:"` + k + `"`),
})
}
newType := reflect.StructOf(fields)
newValue := reflect.New(newType).Elem()
for i := range t.NumField() {
newValue.Field(i).Set(v.Field(i))
}
for i, k := range customKeys {
newValue.Field(t.NumField() + i).SetString(customFields[k])
}
return newValue.Interface()
}
func (wh *Webhook) SendIncident(ctx context.Context, baseUrl string, n *db.IncidentNotification) error {
tmpl, err := template.New("incidentTemplate").Funcs(templateFunctions).Parse(wh.cfg.IncidentTemplate)
if err != nil {
return fmt.Errorf("invalid incident template: %s", err)
}
var data bytes.Buffer
values := IncidentTemplateValues{
Status: strings.ToUpper(n.Status.String()),
Application: n.ApplicationId,
URL: incidentUrl(baseUrl, n),
}
if n.Details != nil {
values.Reports = n.Details.Reports
values.RCASummary = n.Details.RCASummary
values.RCARemediations = n.Details.RCARemediations
}
err = tmpl.Execute(&data, mergeCustomFields(values, wh.cfg.CustomFields))
if err != nil {
return fmt.Errorf("invalid incident template: %s", err)
}
return wh.send(ctx, data.Bytes())
}
func (wh *Webhook) SendAlert(ctx context.Context, baseUrl string, n *db.AlertNotification) error {
if wh.cfg.AlertTemplate == "" {
return nil
}
tmpl, err := template.New("alertTemplate").Funcs(templateFunctions).Parse(wh.cfg.AlertTemplate)
if err != nil {
return fmt.Errorf("invalid alert template: %s", err)
}
var data bytes.Buffer
values := AlertTemplateValues{
Status: strings.ToUpper(n.Status.String()),
Application: n.ApplicationId,
URL: alertUrl(baseUrl, n),
}
if n.Details != nil {
values.ProjectName = n.Details.ProjectName
values.RuleName = n.Details.RuleName
values.Severity = n.Details.Severity
values.Summary = n.Details.Summary
values.Details = n.Details.Details
values.Duration = n.Details.Duration
values.ResolvedBy = n.Details.ResolvedBy
}
err = tmpl.Execute(&data, mergeCustomFields(values, wh.cfg.CustomFields))
if err != nil {
return fmt.Errorf("invalid alert template: %s", err)
}
return wh.send(ctx, data.Bytes())
}
func (wh *Webhook) SendDeployment(ctx context.Context, project *db.Project, ds model.ApplicationDeploymentStatus) error {
tmpl, err := template.New("deploymentTemplate").Funcs(templateFunctions).Parse(wh.cfg.DeploymentTemplate)
if err != nil {
return fmt.Errorf("invalid deployment template: %s", err)
}
status := "Deployed"
var summary []string
switch ds.State {
case model.ApplicationDeploymentStateInProgress:
status = "In-progress"
case model.ApplicationDeploymentStateStuck:
status = "Stuck"
case model.ApplicationDeploymentStateCancelled:
status = "Cancelled"
case model.ApplicationDeploymentStateSummary:
for _, s := range ds.Summary {
summary = append(summary, fmt.Sprintf("%s %s", s.Emoji(), s.Message))
}
if len(summary) == 0 {
summary = append(summary, "No notable changes")
}
}
var data bytes.Buffer
err = tmpl.Execute(&data, mergeCustomFields(DeploymentTemplateValues{
Application: ds.Deployment.ApplicationId,
Status: status,
Version: ds.Deployment.Version(),
Summary: summary,
URL: deploymentUrl(project.Settings.Integrations.BaseUrl, project.Id, ds.Deployment),
}, wh.cfg.CustomFields))
if err != nil {
return fmt.Errorf("invalid deployment template: %s", err)
}
return wh.send(ctx, data.Bytes())
}
func (wh *Webhook) send(ctx context.Context, data []byte) error {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, wh.cfg.Url, bytes.NewReader(utils.EscapeJsonMultilineStrings(data)))
if err != nil {
return err
}
if wh.cfg.BasicAuth != nil && wh.cfg.BasicAuth.User != "" && wh.cfg.BasicAuth.Password != "" {
req.SetBasicAuth(wh.cfg.BasicAuth.User, wh.cfg.BasicAuth.Password)
}
req.Header.Set("Content-Type", "application/json")
for _, h := range wh.cfg.CustomHeaders {
req.Header.Add(h.Key, h.Value)
}
httpClient := &http.Client{}
if wh.cfg.TlsSkipVerify {
httpClient.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
}
resp, err := httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= http.StatusBadRequest {
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("response status: %s", resp.Status)
}
return fmt.Errorf("%s: %s", resp.Status, string(body))
}
return nil
}
var (
templateFunctions = template.FuncMap{
"json": func(arg any) (string, error) {
data, err := json.Marshal(arg)
return string(data), err
},
}
)