From 85711194d83d6a5bcf81932308c8ff2b171a7fa7 Mon Sep 17 00:00:00 2001 From: MytkoEnko Date: Sun, 30 Apr 2023 15:54:13 +0200 Subject: [PATCH] Allowing selected (custom/standard) fields to be updated Signed-off-by: MytkoEnko --- examples/jiralert.yml | 5 +++ pkg/config/config.go | 16 +++++---- pkg/config/config_test.go | 15 +++++--- pkg/notify/notify.go | 47 ++++++++++++++++++++++++- pkg/notify/notify_test.go | 73 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 144 insertions(+), 12 deletions(-) diff --git a/examples/jiralert.yml b/examples/jiralert.yml index da9b0b8..3a71578 100644 --- a/examples/jiralert.yml +++ b/examples/jiralert.yml @@ -54,6 +54,11 @@ receivers: # MultiSelect customfield_10003: [{"value": "red"}, {"value": "blue"}, {"value": "green"}] # + # List of standard or custom field names which values will be updated. Optional. + update_always_fields: + - customfield_10001 + - customfield_10003 + # # Automatically resolve jira issues when alert is resolved. Optional. If declared, ensure state is not an empty string. auto_resolve: state: 'Done' diff --git a/pkg/config/config.go b/pkg/config/config.go index 71ba2b8..718b0d2 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -139,12 +139,13 @@ type ReceiverConfig struct { ReopenDuration *Duration `yaml:"reopen_duration" json:"reopen_duration"` // Optional issue fields - Priority string `yaml:"priority" json:"priority"` - Description string `yaml:"description" json:"description"` - WontFixResolution string `yaml:"wont_fix_resolution" json:"wont_fix_resolution"` - Fields map[string]interface{} `yaml:"fields" json:"fields"` - Components []string `yaml:"components" json:"components"` - StaticLabels []string `yaml:"static_labels" json:"static_labels"` + Priority string `yaml:"priority" json:"priority"` + Description string `yaml:"description" json:"description"` + WontFixResolution string `yaml:"wont_fix_resolution" json:"wont_fix_resolution"` + Fields map[string]interface{} `yaml:"fields" json:"fields"` + Components []string `yaml:"components" json:"components"` + StaticLabels []string `yaml:"static_labels" json:"static_labels"` + CustomFieldsToUpdate []string `yaml:"update_always_fields" json:"update_always_fields"` // Label copy settings AddGroupLabels bool `yaml:"add_group_labels" json:"add_group_labels"` @@ -308,6 +309,9 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { } } } + if len(c.Defaults.CustomFieldsToUpdate) > 0 { + rc.CustomFieldsToUpdate = c.Defaults.CustomFieldsToUpdate + } if len(c.Defaults.StaticLabels) > 0 { rc.StaticLabels = append(rc.StaticLabels, c.Defaults.StaticLabels...) } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index d851f72..644a7d6 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -74,6 +74,10 @@ receivers: customfield_10002: { "value": "red" } # MultiSelect customfield_10003: [{"value": "red" }, {"value": "blue" }, {"value": "green" }] + # List of standard or custom field names which values will be updated. Optional. + update_always_fields: + - customfield_10001 + - customfield_10003 # File containing template definitions. Required. template: jiralert.tmpl @@ -124,11 +128,12 @@ type receiverTestConfig struct { ReopenState string `yaml:"reopen_state,omitempty"` ReopenDuration string `yaml:"reopen_duration,omitempty"` - Priority string `yaml:"priority,omitempty"` - Description string `yaml:"description,omitempty"` - WontFixResolution string `yaml:"wont_fix_resolution,omitempty"` - AddGroupLabels bool `yaml:"add_group_labels,omitempty"` - StaticLabels []string `yaml:"static_labels" json:"static_labels"` + Priority string `yaml:"priority,omitempty"` + Description string `yaml:"description,omitempty"` + WontFixResolution string `yaml:"wont_fix_resolution,omitempty"` + AddGroupLabels bool `yaml:"add_group_labels,omitempty"` + StaticLabels []string `yaml:"static_labels" json:"static_labels"` + CustomFieldsToUpdate []string `yaml:"update_always_fields,omitempty"` AutoResolve *AutoResolve `yaml:"auto_resolve" json:"auto_resolve"` diff --git a/pkg/notify/notify.go b/pkg/notify/notify.go index ffcc6d8..e6b8a7c 100644 --- a/pkg/notify/notify.go +++ b/pkg/notify/notify.go @@ -85,6 +85,18 @@ func (r *Receiver) Notify(data *alertmanager.Data, hashJiraLabel bool, updateSum return false, errors.Wrap(err, "render issue description") } + issueCustomFields := tcontainer.NewMarshalMap() + + for _, field := range r.conf.CustomFieldsToUpdate { + _, ok := r.conf.Fields[field] + if ok { + issueCustomFields[field], err = deepCopyWithTemplate(r.conf.Fields[field], r.tmpl, data) + if err != nil { + return false, errors.Wrap(err, "render issue fields") + } + } + } + if issue != nil { // Update summary if needed. @@ -107,6 +119,23 @@ func (r *Receiver) Notify(data *alertmanager.Data, hashJiraLabel bool, updateSum } } + + for CustomField := range issueCustomFields { + if _, ok := issue.Fields.Unknowns[CustomField]; ok { + if issue.Fields.Unknowns[CustomField] != issueCustomFields[CustomField] { + retry, err = r.updateUnknownFields(issue.Key, tcontainer.MarshalMap(map[string]interface{}{ + CustomField: issueCustomFields[CustomField], + })) + if err != nil { + return retry, err + } + } + if err != nil { + return false, err + } + } + } + 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) @@ -286,7 +315,7 @@ func toGroupTicketLabel(groupLabels alertmanager.KV, hashJiraLabel bool) string func (r *Receiver) search(project, issueLabel string) (*jira.Issue, bool, error) { query := fmt.Sprintf("project=\"%s\" and labels=%q order by resolutiondate desc", project, issueLabel) options := &jira.SearchOptions{ - Fields: []string{"summary", "status", "resolution", "resolutiondate"}, + Fields: append([]string{"summary", "status", "resolution", "resolutiondate"}, r.conf.CustomFieldsToUpdate...), MaxResults: 2, } @@ -365,6 +394,22 @@ func (r *Receiver) updateDescription(issueKey string, description string) (bool, return false, nil } +func (r *Receiver) updateUnknownFields(issueKey string, unknowns tcontainer.MarshalMap) (bool, error) { + level.Debug(r.logger).Log("msg", "updating issue with unknown fields", "key", issueKey, "unknowns", unknowns) + + issueUpdate := &jira.Issue{ + Key: issueKey, + Fields: &jira.IssueFields{ + Unknowns: unknowns, + }, + } + _, resp, err := r.client.UpdateWithOptions(issueUpdate, nil) + if err != nil { + return handleJiraErrResponse("Issue.UpdateWithOptions", resp, err, r.logger) + } + return false, nil +} + func (r *Receiver) reopen(issueKey string) (bool, error) { return r.doTransition(issueKey, r.conf.ReopenState) } diff --git a/pkg/notify/notify_test.go b/pkg/notify/notify_test.go index 8e73f4a..5807be5 100644 --- a/pkg/notify/notify_test.go +++ b/pkg/notify/notify_test.go @@ -56,6 +56,7 @@ func (f *fakeJira) Search(jql string, options *jira.SearchOptions) ([]jira.Issue var issues []jira.Issue for _, key := range f.keysByQuery[jql] { issue := jira.Issue{Key: key, Fields: &jira.IssueFields{}} + issue.Fields.Unknowns = f.issuesByKey[key].Fields.Unknowns for _, field := range options.Fields { switch field { case "summary": @@ -130,6 +131,10 @@ func (f *fakeJira) UpdateWithOptions(old *jira.Issue, _ *jira.UpdateQueryOptions issue.Fields.Description = old.Fields.Description } + if old.Fields.Unknowns != nil { + issue.Fields.Unknowns = old.Fields.Unknowns + } + f.issuesByKey[issue.Key] = issue return issue, nil, nil } @@ -175,6 +180,7 @@ func testReceiverConfig2() *config.ReceiverConfig { } } + func testReceiverConfigAutoResolve() *config.ReceiverConfig { reopen := config.Duration(1 * time.Hour) autoResolve := config.AutoResolve{State: "Done"} @@ -200,6 +206,21 @@ func testReceiverConfigWithStaticLabels() *config.ReceiverConfig { } } +func testReceiverConfigWithCustomFields() *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 }}`, + Fields: tcontainer.MarshalMap(map[string]interface{}{ + "customfield_12345": `{{ (index .Alerts 0).Annotations.AlertValue }}`, + }), + CustomFieldsToUpdate: []string{"customfield_12345","non_existant_field"}, + } +} + func TestNotify_JIRAInteraction(t *testing.T) { testNowTime := time.Now() @@ -325,6 +346,58 @@ func TestNotify_JIRAInteraction(t *testing.T) { }, }, }, + { + name: "opened ticket, update specified custom field value", + inputConfig: testReceiverConfigWithCustomFields(), + initJira: func(t *testing.T) *fakeJira { + f := newTestFakeJira() + _, _, err := f.Create(&jira.Issue{ + ID: "1", + Key: "1", + Fields: &jira.IssueFields{ + Project: jira.Project{Key: testReceiverConfigWithCustomFields().Project}, + Labels: []string{"JIRALERT{819ba5ecba4ea5946a8d17d285cb23f3bb6862e08bb602ab08fd231cd8e1a83a1d095b0208a661787e9035f0541817634df5a994d1b5d4200d6c68a7663c97f5}"}, + Unknowns: tcontainer.MarshalMap(map[string]interface{}{ + "customfield_12345": "90", + }), + Summary: "[FIRING:1] b d ", + Description: "1", + }, + }) + require.NoError(t, err) + return f + }, + inputAlert: &alertmanager.Data{ + Alerts: alertmanager.Alerts{ + {Status: alertmanager.AlertFiring, + Annotations: alertmanager.KV{ + "AlertValue": "95", + }, + }, + //{Status: "not firing"}, + }, + 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: testReceiverConfigWithCustomFields().Project}, + Labels: []string{"JIRALERT{819ba5ecba4ea5946a8d17d285cb23f3bb6862e08bb602ab08fd231cd8e1a83a1d095b0208a661787e9035f0541817634df5a994d1b5d4200d6c68a7663c97f5}"}, + Status: &jira.Status{ + StatusCategory: jira.StatusCategory{Key: "NotDone"}, + }, + Unknowns: tcontainer.MarshalMap{ + "customfield_12345": "95", + }, + Summary: "[FIRING:1] b d ", + Description: "1", + }, + }, + }, + }, { name: "closed ticket, reopen and update summary", inputConfig: testReceiverConfig1(),