Skip to content

Commit c792c12

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

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

@@ -962,19 +966,26 @@ func (c *MSTeamsV2Config) UnmarshalYAML(unmarshal func(any) error) error {
962966
return nil
963967
}
964968

969+
type JiraFieldConfig struct {
970+
// Template is the template string used to render the field.
971+
Template string `yaml:"template,omitempty" json:"template,omitempty"`
972+
// DisableUpdate indicates whether this field should be omitted when updating an existing issue.
973+
DisableUpdate bool `yaml:"disable_update,omitempty" json:"disable_update,omitempty"`
974+
}
975+
965976
type JiraConfig struct {
966977
NotifierConfig `yaml:",inline" json:",inline"`
967978
HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"`
968979

969980
APIURL *URL `yaml:"api_url,omitempty" json:"api_url,omitempty"`
970981
APIType string `yaml:"api_type,omitempty" json:"api_type,omitempty"`
971982

972-
Project string `yaml:"project,omitempty" json:"project,omitempty"`
973-
Summary string `yaml:"summary,omitempty" json:"summary,omitempty"`
974-
Description string `yaml:"description,omitempty" json:"description,omitempty"`
975-
Labels []string `yaml:"labels,omitempty" json:"labels,omitempty"`
976-
Priority string `yaml:"priority,omitempty" json:"priority,omitempty"`
977-
IssueType string `yaml:"issue_type,omitempty" json:"issue_type,omitempty"`
983+
Project string `yaml:"project,omitempty" json:"project,omitempty"`
984+
Summary JiraFieldConfig `yaml:"summary,omitempty" json:"summary,omitempty"`
985+
Description JiraFieldConfig `yaml:"description,omitempty" json:"description,omitempty"`
986+
Labels []string `yaml:"labels,omitempty" json:"labels,omitempty"`
987+
Priority string `yaml:"priority,omitempty" json:"priority,omitempty"`
988+
IssueType string `yaml:"issue_type,omitempty" json:"issue_type,omitempty"`
978989

979990
ReopenTransition string `yaml:"reopen_transition,omitempty" json:"reopen_transition,omitempty"`
980991
ResolveTransition string `yaml:"resolve_transition,omitempty" json:"resolve_transition,omitempty"`
@@ -984,6 +995,27 @@ type JiraConfig struct {
984995
Fields map[string]any `yaml:"fields,omitempty" json:"custom_fields,omitempty"`
985996
}
986997

998+
// Supports both the legacy string and the new object form.
999+
func (f *JiraFieldConfig) UnmarshalYAML(unmarshal func(any) error) error {
1000+
// Try simple string first (backward compatibility).
1001+
var s string
1002+
if err := unmarshal(&s); err == nil {
1003+
f.Template = s
1004+
// DisableUpdate stays false by default.
1005+
return nil
1006+
}
1007+
1008+
// Fallback to full object form.
1009+
type plain JiraFieldConfig
1010+
var cfg plain
1011+
if err := unmarshal(&cfg); err != nil {
1012+
return err
1013+
}
1014+
1015+
*f = JiraFieldConfig(cfg)
1016+
return nil
1017+
}
1018+
9871019
func (c *JiraConfig) UnmarshalYAML(unmarshal func(any) error) error {
9881020
*c = DefaultJiraConfig
9891021
type plain JiraConfig

notify/jira/jira.go

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

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

122131
func (n *Notifier) prepareIssueRequestBody(_ context.Context, logger *slog.Logger, groupID string, tmplTextFunc templateFunc) (issue, error) {
123-
summary, err := tmplTextFunc(n.conf.Summary)
132+
summary, err := tmplTextFunc(n.conf.Summary.Template)
124133
if err != nil {
125134
return issue{}, fmt.Errorf("summary template: %w", err)
126135
}
136+
127137
project, err := tmplTextFunc(n.conf.Project)
128138
if err != nil {
129139
return issue{}, fmt.Errorf("project template: %w", err)
@@ -153,7 +163,7 @@ func (n *Notifier) prepareIssueRequestBody(_ context.Context, logger *slog.Logge
153163
Fields: fieldsWithStringKeys,
154164
}}
155165

156-
issueDescriptionString, err := tmplTextFunc(n.conf.Description)
166+
issueDescriptionString, err := tmplTextFunc(n.conf.Description.Template)
157167
if err != nil {
158168
return issue{}, fmt.Errorf("description template: %w", err)
159169
}

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",
@@ -345,49 +345,49 @@ func TestJiraTemplating(t *testing.T) {
345345
{
346346
title: "full-blown message",
347347
cfg: &config.JiraConfig{
348-
Summary: `{{ template "jira.default.summary" . }}`,
349-
Description: `{{ template "jira.default.description" . }}`,
348+
Summary: config.JiraFieldConfig{Template: `{{ template "jira.default.summary" . }}`},
349+
Description: config.JiraFieldConfig{Template: `{{ template "jira.default.description" . }}`},
350350
},
351351
retry: false,
352352
},
353353
{
354354
title: "template project",
355355
cfg: &config.JiraConfig{
356356
Project: `{{ .CommonLabels.lbl1 }}`,
357-
Summary: `{{ template "jira.default.summary" . }}`,
358-
Description: `{{ template "jira.default.description" . }}`,
357+
Summary: config.JiraFieldConfig{Template: `{{ template "jira.default.summary" . }}`},
358+
Description: config.JiraFieldConfig{Template: `{{ template "jira.default.description" . }}`},
359359
},
360360
retry: false,
361361
},
362362
{
363363
title: "template issue type",
364364
cfg: &config.JiraConfig{
365365
IssueType: `{{ .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: "summary with templating errors",
373373
cfg: &config.JiraConfig{
374-
Summary: "{{ ",
374+
Summary: config.JiraFieldConfig{Template: "{{ "},
375375
},
376376
errMsg: "template: :1: unclosed action",
377377
},
378378
{
379379
title: "description with templating errors",
380380
cfg: &config.JiraConfig{
381-
Summary: `{{ template "jira.default.summary" . }}`,
382-
Description: "{{ ",
381+
Summary: config.JiraFieldConfig{Template: `{{ template "jira.default.summary" . }}`},
382+
Description: config.JiraFieldConfig{Template: "{{ "},
383383
},
384384
errMsg: "template: :1: unclosed action",
385385
},
386386
{
387387
title: "priority with templating errors",
388388
cfg: &config.JiraConfig{
389-
Summary: `{{ template "jira.default.summary" . }}`,
390-
Description: `{{ template "jira.default.description" . }}`,
389+
Summary: config.JiraFieldConfig{Template: `{{ template "jira.default.summary" . }}`},
390+
Description: config.JiraFieldConfig{Template: `{{ template "jira.default.description" . }}`},
391391
Priority: "{{ ",
392392
},
393393
errMsg: "template: :1: unclosed action",
@@ -441,8 +441,8 @@ func TestJiraNotify(t *testing.T) {
441441
{
442442
title: "create new issue",
443443
cfg: &config.JiraConfig{
444-
Summary: `{{ template "jira.default.summary" . }}`,
445-
Description: `{{ template "jira.default.description" . }}`,
444+
Summary: config.JiraFieldConfig{Template: `{{ template "jira.default.summary" . }}`},
445+
Description: config.JiraFieldConfig{Template: `{{ template "jira.default.description" . }}`},
446446
IssueType: "Incident",
447447
Project: "OPS",
448448
Priority: `{{ template "jira.default.priority" . }}`,
@@ -480,11 +480,82 @@ func TestJiraNotify(t *testing.T) {
480480
customFieldAssetFn: func(t *testing.T, issue map[string]any) {},
481481
errMsg: "",
482482
},
483+
{
484+
title: "update existing issue with disabled summary and description",
485+
cfg: &config.JiraConfig{
486+
Summary: config.JiraFieldConfig{
487+
Template: `{{ template "jira.default.summary" . }}`,
488+
DisableUpdate: true,
489+
},
490+
Description: config.JiraFieldConfig{
491+
Template: `{{ template "jira.default.description" . }}`,
492+
DisableUpdate: true,
493+
},
494+
IssueType: "{{ .CommonLabels.issue_type }}",
495+
Project: "{{ .CommonLabels.project }}",
496+
Priority: `{{ template "jira.default.priority" . }}`,
497+
Labels: []string{"alertmanager", "{{ .GroupLabels.alertname }}"},
498+
ReopenDuration: model.Duration(1 * time.Hour),
499+
ReopenTransition: "REOPEN",
500+
ResolveTransition: "CLOSE",
501+
WontFixResolution: "WONTFIX",
502+
},
503+
alert: &types.Alert{
504+
Alert: model.Alert{
505+
Labels: model.LabelSet{
506+
"alertname": "test",
507+
"instance": "vm1",
508+
"severity": "critical",
509+
"project": "MONITORING",
510+
"issue_type": "MINOR",
511+
},
512+
StartsAt: time.Now(),
513+
EndsAt: time.Now().Add(time.Hour),
514+
},
515+
},
516+
searchResponse: issueSearchResult{
517+
Issues: []issue{
518+
{
519+
Key: "MONITORING-1",
520+
Fields: &issueFields{
521+
Summary: "Original Summary",
522+
Description: "Original Description",
523+
Status: &issueStatus{
524+
Name: "Open",
525+
StatusCategory: struct {
526+
Key string `json:"key"`
527+
}{
528+
Key: "open",
529+
},
530+
},
531+
},
532+
},
533+
},
534+
},
535+
issue: issue{
536+
Key: "MONITORING-1",
537+
Fields: &issueFields{
538+
// Summary and Description should NOT be present in the update request
539+
Issuetype: &idNameValue{Name: "MINOR"},
540+
Labels: []string{"ALERT{6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b}", "alertmanager", "test"},
541+
Project: &issueProject{Key: "MONITORING"},
542+
Priority: &idNameValue{Name: "High"},
543+
},
544+
},
545+
customFieldAssetFn: func(t *testing.T, issue map[string]any) {
546+
// Verify that summary and description are NOT in the update request
547+
_, hasSummary := issue["summary"]
548+
_, hasDescription := issue["description"]
549+
require.False(t, hasSummary, "summary should not be present in update request")
550+
require.False(t, hasDescription, "description should not be present in update request")
551+
},
552+
errMsg: "",
553+
},
483554
{
484555
title: "create new issue with template project and issue type",
485556
cfg: &config.JiraConfig{
486-
Summary: `{{ template "jira.default.summary" . }}`,
487-
Description: `{{ template "jira.default.description" . }}`,
557+
Summary: config.JiraFieldConfig{Template: `{{ template "jira.default.summary" . }}`},
558+
Description: config.JiraFieldConfig{Template: `{{ template "jira.default.description" . }}`},
488559
IssueType: "{{ .CommonLabels.issue_type }}",
489560
Project: "{{ .CommonLabels.project }}",
490561
Priority: `{{ template "jira.default.priority" . }}`,
@@ -527,8 +598,8 @@ func TestJiraNotify(t *testing.T) {
527598
{
528599
title: "create new issue with custom field and too long summary",
529600
cfg: &config.JiraConfig{
530-
Summary: strings.Repeat("A", maxSummaryLenRunes+10),
531-
Description: `{{ template "jira.default.description" . }}`,
601+
Summary: config.JiraFieldConfig{Template: strings.Repeat("A", maxSummaryLenRunes+10)},
602+
Description: config.JiraFieldConfig{Template: `{{ template "jira.default.description" . }}`},
532603
IssueType: "Incident",
533604
Project: "OPS",
534605
Priority: `{{ template "jira.default.priority" . }}`,
@@ -587,8 +658,8 @@ func TestJiraNotify(t *testing.T) {
587658
{
588659
title: "reopen issue",
589660
cfg: &config.JiraConfig{
590-
Summary: `{{ template "jira.default.summary" . }}`,
591-
Description: `{{ template "jira.default.description" . }}`,
661+
Summary: config.JiraFieldConfig{Template: `{{ template "jira.default.summary" . }}`},
662+
Description: config.JiraFieldConfig{Template: `{{ template "jira.default.description" . }}`},
592663
IssueType: "Incident",
593664
Project: "OPS",
594665
Priority: `{{ template "jira.default.priority" . }}`,
@@ -642,8 +713,8 @@ func TestJiraNotify(t *testing.T) {
642713
{
643714
title: "error resolve transition not found",
644715
cfg: &config.JiraConfig{
645-
Summary: `{{ template "jira.default.summary" . }}`,
646-
Description: `{{ template "jira.default.description" . }}`,
716+
Summary: config.JiraFieldConfig{Template: `{{ template "jira.default.summary" . }}`},
717+
Description: config.JiraFieldConfig{Template: `{{ template "jira.default.description" . }}`},
647718
IssueType: "Incident",
648719
Project: "OPS",
649720
Priority: `{{ template "jira.default.priority" . }}`,
@@ -696,8 +767,8 @@ func TestJiraNotify(t *testing.T) {
696767
{
697768
title: "error reopen transition not found",
698769
cfg: &config.JiraConfig{
699-
Summary: `{{ template "jira.default.summary" . }}`,
700-
Description: `{{ template "jira.default.description" . }}`,
770+
Summary: config.JiraFieldConfig{Template: `{{ template "jira.default.summary" . }}`},
771+
Description: config.JiraFieldConfig{Template: `{{ template "jira.default.description" . }}`},
701772
IssueType: "Incident",
702773
Project: "OPS",
703774
Priority: `{{ template "jira.default.priority" . }}`,
@@ -837,10 +908,28 @@ func TestJiraNotify(t *testing.T) {
837908
t.Fatalf("unexpected method %s", r.Method)
838909
}
839910

911+
return
912+
case "/issue/MONITORING-1":
913+
body, err := io.ReadAll(r.Body)
914+
if err != nil {
915+
panic(err)
916+
}
917+
918+
var raw map[string]any
919+
if err := json.Unmarshal(body, &raw); err != nil {
920+
panic(err)
921+
}
922+
923+
if fields, ok := raw["fields"].(map[string]any); ok {
924+
tc.customFieldAssetFn(t, fields)
925+
}
926+
927+
w.WriteHeader(http.StatusNoContent)
840928
return
841929
case "/issue/OPS-1":
842930
case "/issue/OPS-2":
843931
case "/issue/OPS-3":
932+
case "/issue/OPS-4":
844933
fallthrough
845934
case "/issue":
846935
body, err := io.ReadAll(r.Body)

0 commit comments

Comments
 (0)