diff --git a/examples/jiralert.yml b/examples/jiralert.yml index 46263c3..1a8029b 100644 --- a/examples/jiralert.yml +++ b/examples/jiralert.yml @@ -64,6 +64,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 7fc63c4..3c8dc9f 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -146,6 +146,7 @@ type ReceiverConfig struct { Fields map[string]interface{} `yaml:"fields" json:"fields"` Components []string `yaml:"components" json:"components"` StaticLabels []string `yaml:"static_labels" json:"static_labels"` + FieldsToUpdate []string `yaml:"update_always_fields" json:"update_always_fields"` // Label copy settings AddGroupLabels *bool `yaml:"add_group_labels" json:"add_group_labels"` @@ -312,6 +313,9 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { } } } + if len(c.Defaults.FieldsToUpdate) > 0 { + rc.FieldsToUpdate = c.Defaults.FieldsToUpdate + } 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 e0bd0c9..ac49db8 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -76,6 +76,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 @@ -132,6 +136,7 @@ type receiverTestConfig struct { AddGroupLabels *bool `yaml:"add_group_labels,omitempty"` UpdateInComment *bool `yaml:"update_in_comment,omitempty"` StaticLabels []string `yaml:"static_labels" json:"static_labels"` + FieldsToUpdate []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 e4b96a2..5af8eac 100644 --- a/pkg/notify/notify.go +++ b/pkg/notify/notify.go @@ -91,6 +91,17 @@ func (r *Receiver) Notify(data *alertmanager.Data, hashJiraLabel bool, updateSum issueDesc = issueDesc[:maxDescriptionLength] } + issueCustomFields := tcontainer.NewMarshalMap() + + for _, field := range r.conf.FieldsToUpdate { + if _, ok := r.conf.Fields[field]; 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. @@ -135,6 +146,19 @@ func (r *Receiver) Notify(data *alertmanager.Data, hashJiraLabel bool, updateSum } } + for field := range issueCustomFields { + if _, ok := issue.Fields.Unknowns[field]; ok { + if issue.Fields.Unknowns[field] != issueCustomFields[field] { + retry, err = r.updateUnknownFields(issue.Key, tcontainer.MarshalMap(map[string]interface{}{ + field: issueCustomFields[field], + })) + if err != nil { + return retry, 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) @@ -316,7 +340,7 @@ func (r *Receiver) search(projects []string, issueLabel string) (*jira.Issue, bo projectList := "'" + strings.Join(projects, "', '") + "'" query := fmt.Sprintf("project in(%s) and labels=%q order by resolutiondate desc", projectList, issueLabel) options := &jira.SearchOptions{ - Fields: []string{"summary", "status", "resolution", "resolutiondate", "description", "comment"}, + Fields: append([]string{"summary", "status", "resolution", "resolutiondate", "description", "comment"}, r.conf.FieldsToUpdate...), MaxResults: 2, } @@ -418,6 +442,21 @@ func (r *Receiver) addComment(issueKey string, content string) (bool, error) { 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, + }, + } + if _, resp, err := r.client.UpdateWithOptions(issueUpdate, nil); err != nil { + return handleJiraErrResponse("Issue.UpdateUnknownFields", 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 49d686f..c306a5f 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": @@ -132,6 +133,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 } @@ -222,6 +227,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 }}`, + }), + FieldsToUpdate: []string{"customfield_12345", "non_existant_field"}, + } +} + func TestNotify_JIRAInteraction(t *testing.T) { testNowTime := time.Now() @@ -347,6 +367,57 @@ 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", + }, + }, // New value for the field + }, + 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(),