Skip to content

Commit 9da06dc

Browse files
feat(jira): enhance Jira field configuration with disable update option
Signed-off-by: Holger Waschke <[email protected]>
1 parent fd259d3 commit 9da06dc

File tree

4 files changed

+180
-45
lines changed

4 files changed

+180
-45
lines changed

config/notifiers.go

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -206,10 +206,14 @@ var (
206206
NotifierConfig: NotifierConfig{
207207
VSendResolved: true,
208208
},
209-
APIType: "auto",
210-
Summary: `{{ template "jira.default.summary" . }}`,
211-
Description: `{{ template "jira.default.description" . }}`,
212-
Priority: `{{ template "jira.default.priority" . }}`,
209+
APIType: "auto",
210+
Summary: JiraFieldConfig{
211+
Template: `{{ template "jira.default.summary" . }}`,
212+
},
213+
Description: JiraFieldConfig{
214+
Template: `{{ template "jira.default.description" . }}`,
215+
},
216+
Priority: `{{ template "jira.default.priority" . }}`,
213217
}
214218

215219
DefaultMattermostConfig = MattermostConfig{
@@ -969,19 +973,26 @@ func (c *MSTeamsV2Config) UnmarshalYAML(unmarshal func(any) error) error {
969973
return nil
970974
}
971975

976+
type JiraFieldConfig struct {
977+
// Template is the template string used to render the field.
978+
Template string `yaml:"template,omitempty" json:"template,omitempty"`
979+
// DisableUpdate indicates whether this field should be omitted when updating an existing issue.
980+
DisableUpdate bool `yaml:"disable_update,omitempty" json:"disable_update,omitempty"`
981+
}
982+
972983
type JiraConfig struct {
973984
NotifierConfig `yaml:",inline" json:",inline"`
974985
HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"`
975986

976987
APIURL *URL `yaml:"api_url,omitempty" json:"api_url,omitempty"`
977988
APIType string `yaml:"api_type,omitempty" json:"api_type,omitempty"`
978989

979-
Project string `yaml:"project,omitempty" json:"project,omitempty"`
980-
Summary string `yaml:"summary,omitempty" json:"summary,omitempty"`
981-
Description string `yaml:"description,omitempty" json:"description,omitempty"`
982-
Labels []string `yaml:"labels,omitempty" json:"labels,omitempty"`
983-
Priority string `yaml:"priority,omitempty" json:"priority,omitempty"`
984-
IssueType string `yaml:"issue_type,omitempty" json:"issue_type,omitempty"`
990+
Project string `yaml:"project,omitempty" json:"project,omitempty"`
991+
Summary JiraFieldConfig `yaml:"summary,omitempty" json:"summary,omitempty"`
992+
Description JiraFieldConfig `yaml:"description,omitempty" json:"description,omitempty"`
993+
Labels []string `yaml:"labels,omitempty" json:"labels,omitempty"`
994+
Priority string `yaml:"priority,omitempty" json:"priority,omitempty"`
995+
IssueType string `yaml:"issue_type,omitempty" json:"issue_type,omitempty"`
985996

986997
ReopenTransition string `yaml:"reopen_transition,omitempty" json:"reopen_transition,omitempty"`
987998
ResolveTransition string `yaml:"resolve_transition,omitempty" json:"resolve_transition,omitempty"`
@@ -991,6 +1002,27 @@ type JiraConfig struct {
9911002
Fields map[string]any `yaml:"fields,omitempty" json:"custom_fields,omitempty"`
9921003
}
9931004

1005+
// Supports both the legacy string and the new object form.
1006+
func (f *JiraFieldConfig) UnmarshalYAML(unmarshal func(any) error) error {
1007+
// Try simple string first (backward compatibility).
1008+
var s string
1009+
if err := unmarshal(&s); err == nil {
1010+
f.Template = s
1011+
// DisableUpdate stays false by default.
1012+
return nil
1013+
}
1014+
1015+
// Fallback to full object form.
1016+
type plain JiraFieldConfig
1017+
var cfg plain
1018+
if err := unmarshal(&cfg); err != nil {
1019+
return err
1020+
}
1021+
1022+
*f = JiraFieldConfig(cfg)
1023+
return nil
1024+
}
1025+
9941026
func (c *JiraConfig) UnmarshalYAML(unmarshal func(any) error) error {
9951027
*c = DefaultJiraConfig
9961028
type plain JiraConfig

notify/jira/jira.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,15 @@ func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error)
112112
return false, err
113113
}
114114

115+
if method == http.MethodPut && requestBody.Fields != nil {
116+
if n.conf.Description.DisableUpdate {
117+
requestBody.Fields.Description = nil
118+
}
119+
if n.conf.Summary.DisableUpdate {
120+
requestBody.Fields.Summary = nil
121+
}
122+
}
123+
115124
_, shouldRetry, err = n.doAPIRequest(ctx, method, path, requestBody)
116125
if err != nil {
117126
return shouldRetry, fmt.Errorf("failed to %s request to %q: %w", method, path, err)
@@ -121,10 +130,11 @@ func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error)
121130
}
122131

123132
func (n *Notifier) prepareIssueRequestBody(_ context.Context, logger *slog.Logger, groupID string, tmplTextFunc template.TemplateFunc) (issue, error) {
124-
summary, err := tmplTextFunc(n.conf.Summary)
133+
summary, err := tmplTextFunc(n.conf.Summary.Template)
125134
if err != nil {
126135
return issue{}, fmt.Errorf("summary template: %w", err)
127136
}
137+
128138
project, err := tmplTextFunc(n.conf.Project)
129139
if err != nil {
130140
return issue{}, fmt.Errorf("project template: %w", err)
@@ -161,7 +171,7 @@ func (n *Notifier) prepareIssueRequestBody(_ context.Context, logger *slog.Logge
161171
Fields: fieldsWithStringKeys,
162172
}}
163173

164-
issueDescriptionString, err := tmplTextFunc(n.conf.Description)
174+
issueDescriptionString, err := tmplTextFunc(n.conf.Description.Template)
165175
if err != nil {
166176
return issue{}, fmt.Errorf("description template: %w", err)
167177
}

notify/jira/jira_test.go

Lines changed: 118 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,8 @@ func TestSearchExistingIssue(t *testing.T) {
109109
{
110110
title: "search existing issue with project template for firing alert",
111111
cfg: &config.JiraConfig{
112-
Summary: `{{ template "jira.default.summary" . }}`,
113-
Description: `{{ template "jira.default.description" . }}`,
112+
Summary: config.JiraFieldConfig{Template: `{{ template "jira.default.summary" . }}`},
113+
Description: config.JiraFieldConfig{Template: `{{ template "jira.default.description" . }}`},
114114
Project: `{{ .CommonLabels.project }}`,
115115
},
116116
groupKey: "1",
@@ -120,8 +120,8 @@ func TestSearchExistingIssue(t *testing.T) {
120120
{
121121
title: "search existing issue with reopen duration for firing alert",
122122
cfg: &config.JiraConfig{
123-
Summary: `{{ template "jira.default.summary" . }}`,
124-
Description: `{{ template "jira.default.description" . }}`,
123+
Summary: config.JiraFieldConfig{Template: `{{ template "jira.default.summary" . }}`},
124+
Description: config.JiraFieldConfig{Template: `{{ template "jira.default.description" . }}`},
125125
Project: `{{ .CommonLabels.project }}`,
126126
ReopenDuration: model.Duration(60 * time.Minute),
127127
ReopenTransition: "REOPEN",
@@ -133,8 +133,8 @@ func TestSearchExistingIssue(t *testing.T) {
133133
{
134134
title: "search existing issue for resolved alert",
135135
cfg: &config.JiraConfig{
136-
Summary: `{{ template "jira.default.summary" . }}`,
137-
Description: `{{ template "jira.default.description" . }}`,
136+
Summary: config.JiraFieldConfig{Template: `{{ template "jira.default.summary" . }}`},
137+
Description: config.JiraFieldConfig{Template: `{{ template "jira.default.description" . }}`},
138138
Project: `{{ .CommonLabels.project }}`,
139139
},
140140
groupKey: "1",
@@ -349,8 +349,8 @@ func TestJiraTemplating(t *testing.T) {
349349
{
350350
title: "full-blown message with templated custom field",
351351
cfg: &config.JiraConfig{
352-
Summary: `{{ template "jira.default.summary" . }}`,
353-
Description: `{{ template "jira.default.description" . }}`,
352+
Summary: config.JiraFieldConfig{Template: `{{ template "jira.default.summary" . }}`},
353+
Description: config.JiraFieldConfig{Template: `{{ template "jira.default.description" . }}`},
354354
Fields: map[string]any{
355355
"customfield_14400": `{{ template "jira.host" . }}`,
356356
},
@@ -363,40 +363,40 @@ func TestJiraTemplating(t *testing.T) {
363363
title: "template project",
364364
cfg: &config.JiraConfig{
365365
Project: `{{ .CommonLabels.lbl1 }}`,
366-
Summary: `{{ template "jira.default.summary" . }}`,
367-
Description: `{{ template "jira.default.description" . }}`,
366+
Summary: config.JiraFieldConfig{Template: `{{ template "jira.default.summary" . }}`},
367+
Description: config.JiraFieldConfig{Template: `{{ template "jira.default.description" . }}`},
368368
},
369369
retry: false,
370370
},
371371
{
372372
title: "template issue type",
373373
cfg: &config.JiraConfig{
374374
IssueType: `{{ .CommonLabels.lbl1 }}`,
375-
Summary: `{{ template "jira.default.summary" . }}`,
376-
Description: `{{ template "jira.default.description" . }}`,
375+
Summary: config.JiraFieldConfig{Template: `{{ template "jira.default.summary" . }}`},
376+
Description: config.JiraFieldConfig{Template: `{{ template "jira.default.description" . }}`},
377377
},
378378
retry: false,
379379
},
380380
{
381381
title: "summary with templating errors",
382382
cfg: &config.JiraConfig{
383-
Summary: "{{ ",
383+
Summary: config.JiraFieldConfig{Template: "{{ "},
384384
},
385385
errMsg: "template: :1: unclosed action",
386386
},
387387
{
388388
title: "description with templating errors",
389389
cfg: &config.JiraConfig{
390-
Summary: `{{ template "jira.default.summary" . }}`,
391-
Description: "{{ ",
390+
Summary: config.JiraFieldConfig{Template: `{{ template "jira.default.summary" . }}`},
391+
Description: config.JiraFieldConfig{Template: "{{ "},
392392
},
393393
errMsg: "template: :1: unclosed action",
394394
},
395395
{
396396
title: "priority with templating errors",
397397
cfg: &config.JiraConfig{
398-
Summary: `{{ template "jira.default.summary" . }}`,
399-
Description: `{{ template "jira.default.description" . }}`,
398+
Summary: config.JiraFieldConfig{Template: `{{ template "jira.default.summary" . }}`},
399+
Description: config.JiraFieldConfig{Template: `{{ template "jira.default.description" . }}`},
400400
Priority: "{{ ",
401401
},
402402
errMsg: "template: :1: unclosed action",
@@ -471,8 +471,8 @@ func TestJiraNotify(t *testing.T) {
471471
{
472472
title: "create new issue",
473473
cfg: &config.JiraConfig{
474-
Summary: `{{ template "jira.default.summary" . }}`,
475-
Description: `{{ template "jira.default.description" . }}`,
474+
Summary: config.JiraFieldConfig{Template: `{{ template "jira.default.summary" . }}`},
475+
Description: config.JiraFieldConfig{Template: `{{ template "jira.default.description" . }}`},
476476
IssueType: "Incident",
477477
Project: "OPS",
478478
Priority: `{{ template "jira.default.priority" . }}`,
@@ -510,11 +510,82 @@ func TestJiraNotify(t *testing.T) {
510510
customFieldAssetFn: func(t *testing.T, issue map[string]any) {},
511511
errMsg: "",
512512
},
513+
{
514+
title: "update existing issue with disabled summary and description",
515+
cfg: &config.JiraConfig{
516+
Summary: config.JiraFieldConfig{
517+
Template: `{{ template "jira.default.summary" . }}`,
518+
DisableUpdate: true,
519+
},
520+
Description: config.JiraFieldConfig{
521+
Template: `{{ template "jira.default.description" . }}`,
522+
DisableUpdate: true,
523+
},
524+
IssueType: "{{ .CommonLabels.issue_type }}",
525+
Project: "{{ .CommonLabels.project }}",
526+
Priority: `{{ template "jira.default.priority" . }}`,
527+
Labels: []string{"alertmanager", "{{ .GroupLabels.alertname }}"},
528+
ReopenDuration: model.Duration(1 * time.Hour),
529+
ReopenTransition: "REOPEN",
530+
ResolveTransition: "CLOSE",
531+
WontFixResolution: "WONTFIX",
532+
},
533+
alert: &types.Alert{
534+
Alert: model.Alert{
535+
Labels: model.LabelSet{
536+
"alertname": "test",
537+
"instance": "vm1",
538+
"severity": "critical",
539+
"project": "MONITORING",
540+
"issue_type": "MINOR",
541+
},
542+
StartsAt: time.Now(),
543+
EndsAt: time.Now().Add(time.Hour),
544+
},
545+
},
546+
searchResponse: issueSearchResult{
547+
Issues: []issue{
548+
{
549+
Key: "MONITORING-1",
550+
Fields: &issueFields{
551+
Summary: "Original Summary",
552+
Description: "Original Description",
553+
Status: &issueStatus{
554+
Name: "Open",
555+
StatusCategory: struct {
556+
Key string `json:"key"`
557+
}{
558+
Key: "open",
559+
},
560+
},
561+
},
562+
},
563+
},
564+
},
565+
issue: issue{
566+
Key: "MONITORING-1",
567+
Fields: &issueFields{
568+
// Summary and Description should NOT be present in the update request
569+
Issuetype: &idNameValue{Name: "MINOR"},
570+
Labels: []string{"ALERT{6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b}", "alertmanager", "test"},
571+
Project: &issueProject{Key: "MONITORING"},
572+
Priority: &idNameValue{Name: "High"},
573+
},
574+
},
575+
customFieldAssetFn: func(t *testing.T, issue map[string]any) {
576+
// Verify that summary and description are NOT in the update request
577+
_, hasSummary := issue["summary"]
578+
_, hasDescription := issue["description"]
579+
require.False(t, hasSummary, "summary should not be present in update request")
580+
require.False(t, hasDescription, "description should not be present in update request")
581+
},
582+
errMsg: "",
583+
},
513584
{
514585
title: "create new issue with template project and issue type",
515586
cfg: &config.JiraConfig{
516-
Summary: `{{ template "jira.default.summary" . }}`,
517-
Description: `{{ template "jira.default.description" . }}`,
587+
Summary: config.JiraFieldConfig{Template: `{{ template "jira.default.summary" . }}`},
588+
Description: config.JiraFieldConfig{Template: `{{ template "jira.default.description" . }}`},
518589
IssueType: "{{ .CommonLabels.issue_type }}",
519590
Project: "{{ .CommonLabels.project }}",
520591
Priority: `{{ template "jira.default.priority" . }}`,
@@ -557,8 +628,8 @@ func TestJiraNotify(t *testing.T) {
557628
{
558629
title: "create new issue with custom field and too long summary",
559630
cfg: &config.JiraConfig{
560-
Summary: strings.Repeat("A", maxSummaryLenRunes+10),
561-
Description: `{{ template "jira.default.description" . }}`,
631+
Summary: config.JiraFieldConfig{Template: strings.Repeat("A", maxSummaryLenRunes+10)},
632+
Description: config.JiraFieldConfig{Template: `{{ template "jira.default.description" . }}`},
562633
IssueType: "Incident",
563634
Project: "OPS",
564635
Priority: `{{ template "jira.default.priority" . }}`,
@@ -617,8 +688,8 @@ func TestJiraNotify(t *testing.T) {
617688
{
618689
title: "reopen issue",
619690
cfg: &config.JiraConfig{
620-
Summary: `{{ template "jira.default.summary" . }}`,
621-
Description: `{{ template "jira.default.description" . }}`,
691+
Summary: config.JiraFieldConfig{Template: `{{ template "jira.default.summary" . }}`},
692+
Description: config.JiraFieldConfig{Template: `{{ template "jira.default.description" . }}`},
622693
IssueType: "Incident",
623694
Project: "OPS",
624695
Priority: `{{ template "jira.default.priority" . }}`,
@@ -672,8 +743,8 @@ func TestJiraNotify(t *testing.T) {
672743
{
673744
title: "error resolve transition not found",
674745
cfg: &config.JiraConfig{
675-
Summary: `{{ template "jira.default.summary" . }}`,
676-
Description: `{{ template "jira.default.description" . }}`,
746+
Summary: config.JiraFieldConfig{Template: `{{ template "jira.default.summary" . }}`},
747+
Description: config.JiraFieldConfig{Template: `{{ template "jira.default.description" . }}`},
677748
IssueType: "Incident",
678749
Project: "OPS",
679750
Priority: `{{ template "jira.default.priority" . }}`,
@@ -726,8 +797,8 @@ func TestJiraNotify(t *testing.T) {
726797
{
727798
title: "error reopen transition not found",
728799
cfg: &config.JiraConfig{
729-
Summary: `{{ template "jira.default.summary" . }}`,
730-
Description: `{{ template "jira.default.description" . }}`,
800+
Summary: config.JiraFieldConfig{Template: `{{ template "jira.default.summary" . }}`},
801+
Description: config.JiraFieldConfig{Template: `{{ template "jira.default.description" . }}`},
731802
IssueType: "Incident",
732803
Project: "OPS",
733804
Priority: `{{ template "jira.default.priority" . }}`,
@@ -867,10 +938,28 @@ func TestJiraNotify(t *testing.T) {
867938
t.Fatalf("unexpected method %s", r.Method)
868939
}
869940

941+
return
942+
case "/issue/MONITORING-1":
943+
body, err := io.ReadAll(r.Body)
944+
if err != nil {
945+
panic(err)
946+
}
947+
948+
var raw map[string]any
949+
if err := json.Unmarshal(body, &raw); err != nil {
950+
panic(err)
951+
}
952+
953+
if fields, ok := raw["fields"].(map[string]any); ok {
954+
tc.customFieldAssetFn(t, fields)
955+
}
956+
957+
w.WriteHeader(http.StatusNoContent)
870958
return
871959
case "/issue/OPS-1":
872960
case "/issue/OPS-2":
873961
case "/issue/OPS-3":
962+
case "/issue/OPS-4":
874963
fallthrough
875964
case "/issue":
876965
body, err := io.ReadAll(r.Body)

0 commit comments

Comments
 (0)