Skip to content

Commit

Permalink
Add alert credentials api to sync to alertmanager
Browse files Browse the repository at this point in the history
working yaml file

add route to update alerting credentials

call alertmanager to sync config

end to end alertmanager sync

add request validations for alert credentials
  • Loading branch information
kevinbhedag committed Mar 19, 2021
1 parent 08e2d13 commit b34accf
Show file tree
Hide file tree
Showing 24 changed files with 2,697 additions and 29 deletions.
120 changes: 120 additions & 0 deletions alertCredentials/alertmanager/configSync.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package alertmanager

import (
"bytes"
"context"
"github.com/grafana/cortex-tools/pkg/client"
"github.com/markbates/pkger"
"github.com/odpf/siren/domain"
"github.com/prometheus/alertmanager/config"
"text/template"
)

type SlackCredential struct {
Webhook string
Channel string
Username string
}

type SlackConfig struct {
Critical SlackCredential
Warning SlackCredential
}

type TeamCredentials struct {
PagerdutyCredential string
Slackcredentials SlackConfig
Name string
}

type EntityCredentials struct {
Entity string
Teams map[string]TeamCredentials
}

type Client interface {
SyncConfig(credentials EntityCredentials) error
}

type AlertmanagerClient struct {
CortextClient client.CortexClient
vartmplStr string
detmplStr string
}

func NewClient(c domain.AlertmanagerConfig) (AlertmanagerClient, error) {
config := client.Config{
Address: c.Address,
}
amClient, err := client.New(config)
if err != nil {
return AlertmanagerClient{}, err
}

deTemplatePath := "/alertCredentials/alertmanagerde.tmpl"
deTmplateString, err := readTemplateString(err, deTemplatePath)
if err != nil {
return AlertmanagerClient{}, err
}
varTmplPath := "/alertCredentials/alertmanagervar.tmpl"
varTmplateString, err := readTemplateString(err, varTmplPath)
if err != nil {
return AlertmanagerClient{}, err
}
return AlertmanagerClient{
CortextClient: *amClient,
detmplStr: deTmplateString,
vartmplStr: varTmplateString,
}, nil
}

func (am AlertmanagerClient) SyncConfig(credentials EntityCredentials) error {
cfg, err := generateAlertmanagerConfig(credentials)
if err != nil {
return err
}
templates := map[string]string{
"var.tmpl": am.vartmplStr,
"de.tmpl": am.detmplStr,
}

ctx := client.NewContextWithTenantId(context.Background(), credentials.Entity)
err = am.CortextClient.CreateAlertmanagerConfig(ctx, cfg, templates)
if err != nil {
return err
}
return nil
}

func generateAlertmanagerConfig(credentials EntityCredentials) (string, error) {
delims := template.New("alertmanagerConfigTemplate").Delims("[[", "]]")
parse, err := delims.Parse(alertmanagerConfigTemplate)
if err != nil {
return "", err
}
var tpl bytes.Buffer
err = parse.Execute(&tpl, credentials)
if err != nil {
return "", err
}
configStr := tpl.String()
_, err = config.Load(configStr)
if err != nil {
return "", err
}
return configStr, nil
}

func readTemplateString(err error, templatePath string) (string, error) {
tmpl, err := pkger.Open(templatePath)
if err != nil {
return "", err
}
defer tmpl.Close()
detmplBuf := new(bytes.Buffer)
_, err = detmplBuf.ReadFrom(tmpl)
if err != nil {
return "", err
}
return detmplBuf.String(), nil
}
249 changes: 249 additions & 0 deletions alertCredentials/alertmanager/configSync_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
package alertmanager

import (
"bytes"
"github.com/odpf/siren/domain"
"github.com/stretchr/testify/assert"
"gopkg.in/yaml.v3"
"net/http"
"net/http/httptest"
"testing"
)

func TestGenerateAlertmanagerConfig(t *testing.T) {
credentials := EntityCredentials{
Entity: "de-infra",
Teams: map[string]TeamCredentials{
"eureka": {
Name: "eureka",
Slackcredentials: SlackConfig{
Critical: SlackCredential{
Webhook: "http://eurcritical.com",
Channel: "dss",
Username: "ss",
},
},
},
"wonder-woman": {
Name: "wonder-woman",
Slackcredentials: SlackConfig{
Warning: SlackCredential{
Webhook: "http://eurcritical.com",
Channel: "dss",
Username: "ss",
},
},
PagerdutyCredential: "abc",
},
},
}
expectedConfigStr :=
` templates:
- 'de.tmpl'
- 'var.tmpl'
global:
pagerduty_url: https://events.pagerduty.com/v2/enqueue
resolve_timeout: 5m
receivers:
- name: default
- name: slack-critical-eureka
slack_configs:
- channel: 'dss'
api_url: 'http://eurcritical.com'
username: 'ss'
icon_emoji: ':eagle:'
link_names: false
send_resolved: true
color: '{{ template "slack.color" . }}'
title: ''
pretext: '{{template "slack.pretext" . }}'
text: '{{ template "slack.body" . }}'
actions:
- type: button
text: 'Runbook :books:'
url: '{{template "slack.runbook" . }}'
- type: button
text: 'Dashboard :bar_chart:'
url: '{{template "slack.dashboard" . }}'
- name: slack-warning-wonder-woman
slack_configs:
- channel: 'dss'
api_url: 'http://eurcritical.com'
username: 'ss'
icon_emoji: ':eagle:'
link_names: false
send_resolved: true
color: '{{ template "slack.color" . }}'
title: ''
pretext: '{{template "slack.pretext" . }}'
text: '{{ template "slack.body" . }}'
actions:
- type: button
text: 'Runbook :books:'
url: '{{template "slack.runbook" . }}'
- type: button
text: 'Dashboard :bar_chart:'
url: '{{template "slack.dashboard" . }}'
- name: pagerduty-wonder-woman
pagerduty_configs:
- service_key: 'abc'
route:
group_by:
- alertname
- severity
- owner
- service_name
- time_stamp
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
receiver: default
routes:
- match:
team: 'eureka'
routes:
- match:
severity: "CRITICAL"
environment: "production"
receiver: default
continue: true
- match:
severity: "CRITICAL"
receiver: slack-critical-eureka
- match:
severity: "WARNING"
receiver: default
- match:
team: 'wonder-woman'
routes:
- match:
severity: "CRITICAL"
environment: "production"
receiver: pagerduty-wonder-woman
continue: true
- match:
severity: "CRITICAL"
receiver: default
- match:
severity: "WARNING"
receiver: slack-warning-wonder-woman
`
configStr, err := generateAlertmanagerConfig(credentials)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, expectedConfigStr, configStr)

}

type ConfigCompat struct {
TemplateFiles map[string]string `yaml:"template_files"`
AlertmanagerConfig string `yaml:"alertmanager_config"`
}

func TestSyncConfig(t *testing.T) {
credentials := EntityCredentials{
Entity: "greek",
Teams: map[string]TeamCredentials{
"eureka": {
Name: "eureka",
Slackcredentials: SlackConfig{
Critical: SlackCredential{
Webhook: "http://eurcritical.com",
Channel: "dss",
Username: "ss",
},
},
},
"wonder": {
Name: "wonder",
Slackcredentials: SlackConfig{
Warning: SlackCredential{
Webhook: "http://eurcritical.com",
Channel: "dss",
Username: "ss",
},
},
PagerdutyCredential: "abc",
},
},
}
t.Run("should return error if alertmanager response code is non-2xx", func(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(500)
}))
defer ts.Close()
client, err := NewClient(domain.AlertmanagerConfig{
Address: ts.URL,
})
if err != nil {
t.Fatal(err)
}
err = client.SyncConfig(credentials,
)
assert.Error(t, err)

})
t.Run("should return nil on successful sync", func(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tenant := r.Header.Get("X-Scope-Orgid")
assert.Equal(t, "greek", tenant)
requestBody := ConfigCompat{}
buf := new(bytes.Buffer)
buf.ReadFrom(r.Body)
err := yaml.Unmarshal(buf.Bytes(), &requestBody)
if err != nil {
t.Fatal(err)
}
assert.NotEmpty(t, requestBody.AlertmanagerConfig)
vartmpl := requestBody.TemplateFiles["var.tmpl"]
detmpl := requestBody.TemplateFiles["de.tmpl"]
assert.NotEmpty(t, vartmpl)
assert.NotEmpty(t, detmpl)
}))
defer ts.Close()
client, err := NewClient(domain.AlertmanagerConfig{
Address: ts.URL,
})
if err != nil {
t.Fatal(err)
}
err = client.SyncConfig(credentials,
)
if err != nil {
t.Fatal(err)
}
})
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tenant := r.Header.Get("X-Scope-Orgid")
assert.Equal(t, "greek", tenant)
requestBody := ConfigCompat{}
buf := new(bytes.Buffer)
buf.ReadFrom(r.Body)
err := yaml.Unmarshal(buf.Bytes(), &requestBody)
if err != nil {
t.Fatal(err)
}
assert.NotEmpty(t, requestBody.AlertmanagerConfig)
vartmpl := requestBody.TemplateFiles["var.tmpl"]
detmpl := requestBody.TemplateFiles["de.tmpl"]
assert.NotEmpty(t, vartmpl)
assert.NotEmpty(t, detmpl)
}))
defer ts.Close()
client, err := NewClient(domain.AlertmanagerConfig{
Address: "http://localhost:8080",
})
if err != nil {
t.Fatal(err)
}
err = client.SyncConfig(credentials,
)
if err != nil {
t.Fatal(err)
}
}

Loading

0 comments on commit b34accf

Please sign in to comment.