diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml new file mode 100644 index 0000000..14b8e5b --- /dev/null +++ b/.github/workflows/docker-image.yml @@ -0,0 +1,30 @@ +name: Docker Image CI + +on: + workflow_dispatch: + inputs: + version: + required: true +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Docker Login + # You may pin to the exact commit or the version. + # uses: docker/login-action@49ed152c8eca782a232dede0303416e8f356c37b + uses: docker/login-action@v2.0.0 + with: + username: ${{secrets.DOCKERHUB_USERNAME}} + password: ${{secrets.DOCKERHUB_PASSWORD}} + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: 1.19 + - name: Build the Docker image + run: docker build . --file Dockerfile --tag crikke95/jiralert:${{github.event.inputs.version}} + - name: Docker push + run: docker push crikke95/jiralert:${{github.event.inputs.version}} + diff --git a/pkg/config/config.go b/pkg/config/config.go index f72809c..48a8b36 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -146,7 +146,8 @@ type ReceiverConfig struct { Components []string `yaml:"components" json:"components"` // Label copy settings - AddGroupLabels bool `yaml:"add_group_labels" json:"add_group_labels"` + AddGroupLabels bool `yaml:"add_group_labels" json:"add_group_labels"` + AddCommonLabels bool `yaml:"add_common_labels" json:"add_common_labels"` // Flag to auto-resolve opened issue when the alert is resolved. AutoResolve *AutoResolve `yaml:"auto_resolve" json:"auto_resolve"` diff --git a/pkg/notify/notify.go b/pkg/notify/notify.go index 00a2d9e..a813c24 100644 --- a/pkg/notify/notify.go +++ b/pkg/notify/notify.go @@ -15,14 +15,16 @@ package notify import ( "bytes" + "crypto/sha1" "crypto/sha512" "fmt" - "github.com/andygrunwald/go-jira" "io" "reflect" "strings" "time" + "github.com/andygrunwald/go-jira" + "github.com/go-kit/log" "github.com/go-kit/log/level" "github.com/pkg/errors" @@ -32,6 +34,10 @@ import ( "github.com/trivago/tgo/tcontainer" ) +const ( + jiraHashLabel = "Jiralert-checksum" +) + // TODO(bwplotka): Consider renaming this package to ticketer. type jiraIssueService interface { @@ -59,6 +65,17 @@ func NewReceiver(logger log.Logger, c *config.ReceiverConfig, t *template.Templa return &Receiver{logger: logger, conf: c, tmpl: t, client: client, timeNow: time.Now} } +func toChecksumTicketLabel(labels []string) string { + + h := sha1.New() + + for _, label := range labels { + _, _ = h.Write([]byte(label)) + } + + return fmt.Sprintf("%s=%x", jiraHashLabel, h.Sum(nil)) +} + // Notify manages JIRA issues based on alertmanager webhook notify message. func (r *Receiver) Notify(data *alertmanager.Data, hashJiraLabel bool) (bool, error) { project, err := r.tmpl.Execute(r.conf.Project, data) @@ -66,9 +83,18 @@ func (r *Receiver) Notify(data *alertmanager.Data, hashJiraLabel bool) (bool, er return false, errors.Wrap(err, "generate project from template") } - issueGroupLabel := toGroupTicketLabel(data.GroupLabels, hashJiraLabel) + labels := make([]string, 0) + + if r.conf.AddCommonLabels { + for k, v := range data.CommonLabels { + labels = append(labels, fmt.Sprintf("%s=%q", k, v)) + } + labels = append(labels, toChecksumTicketLabel(labels)) + } else { + labels = append(labels, toGroupTicketLabel(data.GroupLabels, hashJiraLabel)) + } - issue, retry, err := r.findIssueToReuse(project, issueGroupLabel) + issue, retry, err := r.findIssueToReuse(project, labels[len(labels)-1]) if err != nil { return retry, err } @@ -103,7 +129,7 @@ func (r *Receiver) Notify(data *alertmanager.Data, hashJiraLabel bool) (bool, er if len(data.Alerts.Firing()) == 0 { if r.conf.AutoResolve != nil { - level.Debug(r.logger).Log("msg", "no firing alert; resolving issue", "key", issue.Key, "label", issueGroupLabel) + level.Debug(r.logger).Log("msg", "no firing alert; resolving issue", "key", issue.Key, "label", labels) retry, err := r.resolveIssue(issue.Key) if err != nil { return retry, err @@ -111,32 +137,32 @@ func (r *Receiver) Notify(data *alertmanager.Data, hashJiraLabel bool) (bool, er return false, nil } - level.Debug(r.logger).Log("msg", "no firing alert; summary checked, nothing else to do.", "key", issue.Key, "label", issueGroupLabel) + level.Debug(r.logger).Log("msg", "no firing alert; summary checked, nothing else to do.", "key", issue.Key, "label", labels) return false, nil } // The set of JIRA status categories is fixed, this is a safe check to make. if issue.Fields.Status.StatusCategory.Key != "done" { - level.Debug(r.logger).Log("msg", "issue is unresolved, all is done", "key", issue.Key, "label", issueGroupLabel) + level.Debug(r.logger).Log("msg", "issue is unresolved, all is done", "key", issue.Key, "label", labels) return false, nil } if r.conf.WontFixResolution != "" && issue.Fields.Resolution != nil && issue.Fields.Resolution.Name == r.conf.WontFixResolution { - level.Info(r.logger).Log("msg", "issue was resolved as won't fix, not reopening", "key", issue.Key, "label", issueGroupLabel, "resolution", issue.Fields.Resolution.Name) + level.Info(r.logger).Log("msg", "issue was resolved as won't fix, not reopening", "key", issue.Key, "label", labels, "resolution", issue.Fields.Resolution.Name) return false, nil } - level.Info(r.logger).Log("msg", "issue was recently resolved, reopening", "key", issue.Key, "label", issueGroupLabel) + level.Info(r.logger).Log("msg", "issue was recently resolved, reopening", "key", issue.Key, "label", labels) return r.reopen(issue.Key) } if len(data.Alerts.Firing()) == 0 { - level.Debug(r.logger).Log("msg", "no firing alert; nothing to do.", "label", issueGroupLabel) + level.Debug(r.logger).Log("msg", "no firing alert; nothing to do.", "label", labels) return false, nil } - level.Info(r.logger).Log("msg", "no recent matching issue found, creating new issue", "label", issueGroupLabel) + level.Info(r.logger).Log("msg", "no recent matching issue found, creating new issue", "label", labels) issueType, err := r.tmpl.Execute(r.conf.IssueType, data) if err != nil { @@ -149,7 +175,7 @@ func (r *Receiver) Notify(data *alertmanager.Data, hashJiraLabel bool) (bool, er Type: jira.IssueType{Name: issueType}, Description: issueDesc, Summary: issueSummary, - Labels: []string{issueGroupLabel}, + Labels: labels, Unknowns: tcontainer.NewMarshalMap(), }, } @@ -249,6 +275,7 @@ func deepCopyWithTemplate(value interface{}, tmpl *template.Template, data inter // if the combined length of all groupLabel key-value pairs would be // longer than 255 chars func toGroupTicketLabel(groupLabels alertmanager.KV, hashJiraLabel bool) string { + // new opt in behavior if hashJiraLabel { hash := sha512.New() diff --git a/pkg/notify/notify_test.go b/pkg/notify/notify_test.go index 78ef554..cdd5571 100644 --- a/pkg/notify/notify_test.go +++ b/pkg/notify/notify_test.go @@ -173,6 +173,18 @@ func testReceiverConfig2() *config.ReceiverConfig { WontFixResolution: "won't-fix", } } +func testReceiverConfig3() *config.ReceiverConfig { + reopen := config.Duration(1 * time.Hour) + return &config.ReceiverConfig{ + Project: "abc", + Summary: `[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .GroupLabels.SortedPairs.Values | join " " }} {{ if gt (len .CommonLabels) (len .GroupLabels) }}({{ with .CommonLabels.Remove .GroupLabels.Names }}{{ .Values | join " " }}{{ end }}){{ end }}`, + ReopenDuration: &reopen, + ReopenState: "reopened", + Description: `{{ .Alerts.Firing | len }}`, + WontFixResolution: "won't-fix", + AddCommonLabels: true, + } +} func testReceiverConfigAutoResolve() *config.ReceiverConfig { reopen := config.Duration(1 * time.Hour) @@ -197,6 +209,40 @@ func TestNotify_JIRAInteraction(t *testing.T) { initJira func(t *testing.T) *fakeJira expectedJiraIssues map[string]*jira.Issue }{ + { + name: "empty jira, add common labels", + inputConfig: testReceiverConfig3(), + initJira: func(t *testing.T) *fakeJira { return newTestFakeJira() }, + inputAlert: &alertmanager.Data{ + CommonLabels: alertmanager.KV{"foo": "bar"}, + Alerts: alertmanager.Alerts{ + {Status: alertmanager.AlertFiring}, + {Status: "not firing"}, + {Status: alertmanager.AlertFiring}, + }, + Status: alertmanager.AlertFiring, + GroupLabels: alertmanager.KV{"a": "b", "c": "d"}, + }, + expectedJiraIssues: map[string]*jira.Issue{ + "1": { + ID: "1", + Key: "1", + Fields: &jira.IssueFields{ + Project: jira.Project{Key: testReceiverConfig1().Project}, + Labels: []string{ + `foo="bar"`, + "Jiralert-checksum=ab03a89430228717b59a73fe39f7b6a44aa79408", + }, + Description: "2", + Status: &jira.Status{ + StatusCategory: jira.StatusCategory{Key: "NotDone"}, + }, + Unknowns: tcontainer.MarshalMap{}, + Summary: "[FIRING:2] b d ", + }, + }, + }, + }, { name: "empty jira, new alert group", inputConfig: testReceiverConfig1(),