From e01463c9ab46d80b132abccd37b3e7a9836f9096 Mon Sep 17 00:00:00 2001 From: Rufina Talalaeva Date: Mon, 13 May 2024 17:09:06 +0300 Subject: [PATCH] feat: cherry-pick upstream commits (#3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Better Jira error handling (#140) * Better Jira error handling * Return HTTP 400 Bad Request for non-retriable errors. It is inaccurate, but will prevent alertmanager from retrying. * Turns out go-jira does actually produce useful error messages (and it consumes the response body in the process). Log that instead of the empty body. Signed-off-by: Alin Sinpalean * Also include HTTP response status 429 among retriable errors. Signed-off-by: Alin Sinpalean * Include both the go-jira error and the response body in errors. Sometimes go-jira consumes the body and includes it in its error, sometimes it doesn't. Signed-off-by: Alin Sinpalean --------- Signed-off-by: Alin Sinpalean Co-authored-by: Alin Sinpalean * disable update existing jira issues with parameter (#150) * Bump all dependencies (#133) Signed-off-by: Jan-Otto Kröpke Signed-off-by: Jan-Otto Kröpke Signed-off-by: Holger Waschke * parameter to disable update jira issues Signed-off-by: Holger Waschke Signed-off-by: Holger Waschke * rename parameter to make it more clear and avoid double negation. fix bug with missing return value. Signed-off-by: Holger Waschke * Update main.go Signed-off-by: dvag-holger-waschke <85643002+dvag-holger-waschke@users.noreply.github.com> * Update notify.go Signed-off-by: dvag-holger-waschke <85643002+dvag-holger-waschke@users.noreply.github.com> * Update main.go Signed-off-by: dvag-holger-waschke <85643002+dvag-holger-waschke@users.noreply.github.com> * Update main.go Signed-off-by: dvag-holger-waschke <85643002+dvag-holger-waschke@users.noreply.github.com> * Update main.go Signed-off-by: dvag-holger-waschke <85643002+dvag-holger-waschke@users.noreply.github.com> * Update notify.go Signed-off-by: dvag-holger-waschke <85643002+dvag-holger-waschke@users.noreply.github.com> * fix for notify test Signed-off-by: Holger Waschke --------- Signed-off-by: Jan-Otto Kröpke Signed-off-by: Holger Waschke Signed-off-by: Holger Waschke Signed-off-by: dvag-holger-waschke <85643002+dvag-holger-waschke@users.noreply.github.com> Co-authored-by: Jan-Otto Kröpke Co-authored-by: Holger Waschke * Adding getEnv templating function (#153) Signed-off-by: Jiri Tyr * feat: add support for static jira labels (#154) Signed-off-by: Herman Ewert Co-authored-by: Herman Ewert * Fix #146 (safe limit of 200 characters from group label value) (#147) Signed-off-by: jzajic * doc(PAT): Adds doc for PAT usage (#155) Signed-off-by: Julian Beck * truncate descriptions that exceed -max-description-length (default 32KB) (#165) * truncate descriptions that exceed -max-description-length (default 32,768) Signed-off-by: Jason Wells * Update main.go size was off by 1 Signed-off-by: Jason Wells --------- Signed-off-by: Jason Wells * fix: :bug: Fixes error message for doTransition to display the proper transition state (#176) Signed-off-by: Nathan Gotz * search for existing issue in multiple projects (#162) * search for existing issue in multiple projects Signed-off-by: Jason Wells * Apply suggestions from code review Co-authored-by: Bartlomiej Plotka Signed-off-by: Jason Wells --------- Signed-off-by: Jason Wells Co-authored-by: Bartlomiej Plotka * add Fingerprint field to Alert so that it may be used in templates (#152) (#163) Signed-off-by: Jason Wells --------- Signed-off-by: Alin Sinpalean Signed-off-by: Jan-Otto Kröpke Signed-off-by: Holger Waschke Signed-off-by: Holger Waschke Signed-off-by: dvag-holger-waschke <85643002+dvag-holger-waschke@users.noreply.github.com> Signed-off-by: Jiri Tyr Signed-off-by: Herman Ewert Signed-off-by: jzajic Signed-off-by: Julian Beck Signed-off-by: Jason Wells Signed-off-by: Nathan Gotz Co-authored-by: Alin Sinpalean <58422065+alin-at-dfinity@users.noreply.github.com> Co-authored-by: Alin Sinpalean Co-authored-by: dvag-holger-waschke <85643002+dvag-holger-waschke@users.noreply.github.com> Co-authored-by: Jan-Otto Kröpke Co-authored-by: Holger Waschke Co-authored-by: Jiri Tyr Co-authored-by: Herman Co-authored-by: Herman Ewert Co-authored-by: Jan Zajic Co-authored-by: Julian Beck Co-authored-by: Jason Wells Co-authored-by: Nathan Gotz <775979+nlgotz@users.noreply.github.com> Co-authored-by: Bartlomiej Plotka --- cmd/jiralert/main.go | 18 +++++--- examples/jiralert.yml | 14 +++++- pkg/alertmanager/alertmanager.go | 1 + pkg/config/config.go | 8 ++++ pkg/config/config_test.go | 53 +++++++++++++++++++--- pkg/notify/notify.go | 77 +++++++++++++++++++++----------- pkg/notify/notify_test.go | 48 ++++++++++++++++++-- pkg/template/template.go | 4 ++ 8 files changed, 181 insertions(+), 42 deletions(-) diff --git a/cmd/jiralert/main.go b/cmd/jiralert/main.go index 2351203..6f8ddec 100644 --- a/cmd/jiralert/main.go +++ b/cmd/jiralert/main.go @@ -17,12 +17,12 @@ import ( "encoding/json" "flag" "fmt" - "github.com/andygrunwald/go-jira" "net/http" "os" "runtime" "strconv" + "github.com/andygrunwald/go-jira" "github.com/go-kit/log" "github.com/go-kit/log/level" "github.com/prometheus-community/jiralert/pkg/alertmanager" @@ -36,9 +36,10 @@ import ( ) const ( - unknownReceiver = "" - logFormatLogfmt = "logfmt" - logFormatJSON = "json" + unknownReceiver = "" + logFormatLogfmt = "logfmt" + logFormatJSON = "json" + defaultMaxDescriptionLength = 32767 // https://jira.atlassian.com/browse/JRASERVER-64351 ) var ( @@ -48,6 +49,10 @@ var ( logFormat = flag.String("log.format", logFormatLogfmt, "Log format to use ("+logFormatLogfmt+", "+logFormatJSON+")") hashJiraLabel = flag.Bool("hash-jira-label", false, "if enabled: renames ALERT{...} to JIRALERT{...}; also hashes the key-value pairs inside of JIRALERT{...} in the created jira issue labels"+ "- this ensures that the label text does not overflow the allowed length in jira (255)") + updateSummary = flag.Bool("update-summary", true, "When false, jiralert does not update the summary of the existing jira issue, even when changes are spotted.") + updateDescription = flag.Bool("update-description", true, "When false, jiralert does not update the description of the existing jira issue, even when changes are spotted.") + reopenTickets = flag.Bool("reopen-tickets", true, "When false, jiralert does not reopen tickets.") + maxDescriptionLength = flag.Int("max-description-length", defaultMaxDescriptionLength, "Maximum length of Descriptions. Truncate to this size avoid server errors.") // Version is the build version, set by make to latest git tag/hash via `-ldflags "-X main.Version=$(VERSION)"`. Version = "" @@ -121,13 +126,14 @@ func main() { return } - if retry, err := notify.NewReceiver(logger, conf, tmpl, client.Issue).Notify(&data, *hashJiraLabel); err != nil { + if retry, err := notify.NewReceiver(logger, conf, tmpl, client.Issue).Notify(&data, *hashJiraLabel, *updateSummary, *updateDescription, *reopenTickets, *maxDescriptionLength); err != nil { var status int if retry { // Instruct Alertmanager to retry. status = http.StatusServiceUnavailable } else { - status = http.StatusInternalServerError + // Inaccurate, just letting Alertmanager know that it should not retry. + status = http.StatusBadRequest } errorHandler(w, status, err, conf.Name, &data, logger) return diff --git a/examples/jiralert.yml b/examples/jiralert.yml index a6713de..2955703 100644 --- a/examples/jiralert.yml +++ b/examples/jiralert.yml @@ -5,8 +5,10 @@ defaults: api_url: https://jiralert.atlassian.net user: jiralert password: 'JIRAlert' + # Alternatively to user and password use a Personal Access Token + # personal_access_token: "Your Personal Access Token". See https://confluence.atlassian.com/enterprise/using-personal-access-tokens-1026032365.html - # Exclude labels in JIRA issue. + # Exclude labels in JIRA issue. exclude_keys: [] # The type of JIRA issue to create. Required. @@ -26,6 +28,14 @@ defaults: # Amount of time after being closed that an issue should be reopened, after which, a new issue is created. # Optional (default: always reopen) reopen_duration: 0h + # Static label that will be added to the JIRA ticket alongisde the JIRALERT{...} or ALERT{...} label + static_labels: ["custom"] + # Other projects are the projects to search for existing issues for the given alerts if + # the main project does not have it. If no issue was found in, the main projects will + # be used to create a new one. If the old issue is found in one of the other projects + # (first found is used in case of duplicates) that old project's issue will be used for + # alert updates instead of creating on in the main project. + other_projects: ["OTHER1", "OTHER2"] # Receiver definitions. At least one must be defined. receivers: @@ -37,6 +47,8 @@ receivers: add_group_labels: false exclude_keys: - pod + # Will be merged with the static_labels from the default map + static_labels: ["anotherLabel"] - name: 'jira-xy' project: XY diff --git a/pkg/alertmanager/alertmanager.go b/pkg/alertmanager/alertmanager.go index 55217f9..0860764 100644 --- a/pkg/alertmanager/alertmanager.go +++ b/pkg/alertmanager/alertmanager.go @@ -138,6 +138,7 @@ type Alert struct { StartsAt time.Time `json:"startsAt"` EndsAt time.Time `json:"endsAt"` GeneratorURL string `json:"generatorURL"` + Fingerprint string `json:"fingerprint"` } // Alerts is a list of Alert objects. diff --git a/pkg/config/config.go b/pkg/config/config.go index fc0adac..f21f61c 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -133,6 +133,7 @@ type ReceiverConfig struct { // Required issue fields Project string `yaml:"project" json:"project"` + OtherProjects []string `yaml:"other_projects" json:"other_projects"` IssueType string `yaml:"issue_type" json:"issue_type"` Summary string `yaml:"summary" json:"summary"` ReopenEnabled *bool `yaml:"reopen_enabled" json:"reopen_enabled"` @@ -145,6 +146,7 @@ type ReceiverConfig struct { 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"` // ExcludeKeys settings ExcludeKeys []string `yaml:"exclude_keys" json:"exclude_keys"` @@ -326,6 +328,12 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { } } } + if len(c.Defaults.StaticLabels) > 0 { + rc.StaticLabels = append(rc.StaticLabels, c.Defaults.StaticLabels...) + } + if len(c.Defaults.OtherProjects) > 0 { + rc.OtherProjects = append(rc.OtherProjects, c.Defaults.OtherProjects...) + } } if len(c.Receivers) == 0 { diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 3bc7de7..d851f72 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -46,6 +46,7 @@ defaults: # Amount of time after being closed that an issue should be reopened, after which, a new issue is created. # Optional (default: always reopen) reopen_duration: 0h + static_labels: ["defaultlabel"] # Receiver definitions. At least one must be defined. receivers: @@ -55,6 +56,7 @@ receivers: project: AB # Copy all Prometheus labels into separate JIRA labels. Optional (default: false). add_group_labels: false + static_labels: ["somelabel"] - name: 'jira-xy' project: XY @@ -122,10 +124,11 @@ 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"` + 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"` AutoResolve *AutoResolve `yaml:"auto_resolve" json:"auto_resolve"` @@ -328,8 +331,9 @@ func TestReceiverOverrides(t *testing.T) { {"WontFixResolution", "Won't Fix", "Won't Fix"}, {"AddGroupLabels", false, false}, {"AutoResolve", &AutoResolve{State: "Done"}, &autoResolve}, + {"StaticLabels", []string{"somelabel"}, []string{"somelabel"}}, } { - optionalFields := []string{"Priority", "Description", "WontFixResolution", "AddGroupLabels", "AutoResolve"} + optionalFields := []string{"Priority", "Description", "WontFixResolution", "AddGroupLabels", "AutoResolve", "StaticLabels"} defaultsConfig := newReceiverTestConfig(mandatoryReceiverFields(), optionalFields) receiverConfig := newReceiverTestConfig([]string{"Name"}, optionalFields) @@ -383,6 +387,8 @@ func newReceiverTestConfig(mandatory []string, optional []string) *receiverTestC value = reflect.ValueOf(true) } else if name == "AutoResolve" { value = reflect.ValueOf(&AutoResolve{State: "Done"}) + } else if name == "StaticLabels" { + value = reflect.ValueOf([]string{}) } else { value = reflect.ValueOf(name) } @@ -459,3 +465,40 @@ func TestAutoResolveConfigDefault(t *testing.T) { configErrorTestRunner(t, config, "bad config in defaults section: state cannot be empty") } + +func TestStaticLabelsConfigMerge(t *testing.T) { + + for i, test := range []struct { + defaultValue []string + receiverValue []string + expectedElements []string + }{ + {[]string{"defaultlabel"}, []string{"receiverlabel"}, []string{"defaultlabel", "receiverlabel"}}, + {[]string{}, []string{"receiverlabel"}, []string{"receiverlabel"}}, + {[]string{"defaultlabel"}, []string{}, []string{"defaultlabel"}}, + {[]string{}, []string{}, []string{}}, + } { + mandatory := mandatoryReceiverFields() + + defaultsConfig := newReceiverTestConfig(mandatory, []string{}) + defaultsConfig.StaticLabels = test.defaultValue + + receiverConfig := newReceiverTestConfig([]string{"Name"}, []string{"StaticLabels"}) + receiverConfig.StaticLabels = test.receiverValue + + config := testConfig{ + Defaults: defaultsConfig, + Receivers: []*receiverTestConfig{receiverConfig}, + Template: "jiralert.tmpl", + } + + yamlConfig, err := yaml.Marshal(&config) + require.NoError(t, err) + + cfg, err := Load(string(yamlConfig)) + require.NoError(t, err) + + receiver := cfg.Receivers[0] + require.ElementsMatch(t, receiver.StaticLabels, test.expectedElements, "Elements should match (failing index: %v)", i) + } +} diff --git a/pkg/notify/notify.go b/pkg/notify/notify.go index 94c3fbe..7574300 100644 --- a/pkg/notify/notify.go +++ b/pkg/notify/notify.go @@ -23,7 +23,6 @@ import ( "time" "github.com/andygrunwald/go-jira" - "github.com/go-kit/log" "github.com/go-kit/log/level" "github.com/pkg/errors" @@ -61,7 +60,7 @@ func NewReceiver(logger log.Logger, c *config.ReceiverConfig, t *template.Templa } // Notify manages JIRA issues based on alertmanager webhook notify message. -func (r *Receiver) Notify(data *alertmanager.Data, hashJiraLabel bool) (bool, error) { +func (r *Receiver) Notify(data *alertmanager.Data, hashJiraLabel bool, updateSummary bool, updateDescription bool, reopenTickets bool, maxDescriptionLength int) (bool, error) { project, err := r.tmpl.Execute(r.conf.Project, data) if err != nil { return false, errors.Wrap(err, "generate project from template") @@ -87,19 +86,30 @@ func (r *Receiver) Notify(data *alertmanager.Data, hashJiraLabel bool) (bool, er return false, errors.Wrap(err, "render issue description") } + if len(issueDesc) > maxDescriptionLength { + level.Warn(r.logger).Log("msg", "truncating description", "original", len(issueDesc), "limit", maxDescriptionLength) + issueDesc = issueDesc[:maxDescriptionLength] + } + if issue != nil { + // Update summary if needed. - if issue.Fields.Summary != issueSummary { - retry, err := r.updateSummary(issue.Key, issueSummary) - if err != nil { - return retry, err + if updateSummary { + if issue.Fields.Summary != issueSummary { + level.Debug(r.logger).Log("updateSummaryDisabled executing") + retry, err := r.updateSummary(issue.Key, issueSummary) + if err != nil { + return retry, err + } } } - if issue.Fields.Description != issueDesc { - retry, err := r.updateDescription(issue.Key, issueDesc) - if err != nil { - return retry, err + if updateDescription { + if issue.Fields.Description != issueDesc { + retry, err := r.updateDescription(issue.Key, issueDesc) + if err != nil { + return retry, err + } } } @@ -123,18 +133,19 @@ func (r *Receiver) Notify(data *alertmanager.Data, hashJiraLabel bool) (bool, er 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) - return false, nil - } + if reopenTickets { + 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) + return false, nil + } - if r.conf.ReopenEnabled != nil && !*r.conf.ReopenEnabled { - level.Debug(r.logger).Log("msg", "reopening disabled, skipping search for existing issue") - } else { level.Info(r.logger).Log("msg", "issue was recently resolved, reopening", "key", issue.Key, "label", issueGroupLabel) return r.reopen(issue.Key) } + + level.Debug(r.logger).Log("Did not update anything") + return false, nil } if len(data.Alerts.Firing()) == 0 { @@ -149,13 +160,15 @@ func (r *Receiver) Notify(data *alertmanager.Data, hashJiraLabel bool) (bool, er return false, errors.Wrap(err, "render issue type") } + staticLabels := r.conf.StaticLabels + issue = &jira.Issue{ Fields: &jira.IssueFields{ Project: jira.Project{Key: project}, Type: jira.IssueType{Name: issueType}, Description: issueDesc, Summary: issueSummary, - Labels: []string{issueGroupLabel}, + Labels: append(staticLabels, issueGroupLabel), Unknowns: tcontainer.NewMarshalMap(), }, } @@ -182,7 +195,7 @@ func (r *Receiver) Notify(data *alertmanager.Data, hashJiraLabel bool) (bool, er if r.conf.AddGroupLabels { for k, v := range data.GroupLabels { - issue.Fields.Labels = append(issue.Fields.Labels, fmt.Sprintf("%s=%q", k, v)) + issue.Fields.Labels = append(issue.Fields.Labels, fmt.Sprintf("%s=%.200q", k, v)) } } @@ -276,8 +289,10 @@ func toGroupTicketLabel(groupLabels alertmanager.KV, hashJiraLabel bool, exclude return strings.Replace(buf.String(), " ", "", -1) } -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) +func (r *Receiver) search(projects []string, issueLabel string) (*jira.Issue, bool, error) { + // Search multiple projects in case issue was moved and further alert firings are desired in existing JIRA. + 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"}, MaxResults: 2, @@ -305,8 +320,15 @@ func (r *Receiver) search(project, issueLabel string) (*jira.Issue, bool, error) } func (r *Receiver) findIssueToReuse(project string, issueGroupLabel string) (*jira.Issue, bool, error) { + projectsToSearch := []string{project} + // In case issue was moved to a different project, include the other configured projects in search (if any). + for _, other := range r.conf.OtherProjects { + if other != project { + projectsToSearch = append(projectsToSearch, other) + } + } - issue, retry, err := r.search(project, issueGroupLabel) + issue, retry, err := r.search(projectsToSearch, issueGroupLabel) if err != nil { return nil, retry, err } @@ -383,10 +405,11 @@ func handleJiraErrResponse(api string, resp *jira.Response, err error, logger lo } if resp != nil && resp.StatusCode/100 != 2 { - retry := resp.StatusCode == 500 || resp.StatusCode == 503 + retry := resp.StatusCode == 500 || resp.StatusCode == 503 || resp.StatusCode == 429 + // Sometimes go-jira consumes the body (e.g. in `Search`) and includes it in the error message; + // sometimes (e.g. in `Create`) it doesn't. Include both the error and the body, just in case. body, _ := io.ReadAll(resp.Body) - // go-jira error message is not particularly helpful, replace it - return retry, errors.Errorf("JIRA request %s returned status %s, body %q", resp.Request.URL, resp.Status, string(body)) + return retry, errors.Errorf("JIRA request %s returned status %s, error %q, body %q", resp.Request.URL, resp.Status, err, body) } return false, errors.Wrapf(err, "JIRA request %s failed", api) } @@ -413,6 +436,6 @@ func (r *Receiver) doTransition(issueKey string, transitionState string) (bool, return false, nil } } - return false, errors.Errorf("JIRA state %q does not exist or no transition possible for %s", r.conf.ReopenState, issueKey) + return false, errors.Errorf("JIRA state %q does not exist or no transition possible for %s", transitionState, issueKey) } diff --git a/pkg/notify/notify_test.go b/pkg/notify/notify_test.go index 78ef554..15ea758 100644 --- a/pkg/notify/notify_test.go +++ b/pkg/notify/notify_test.go @@ -14,12 +14,13 @@ package notify import ( "fmt" - "github.com/andygrunwald/go-jira" "os" "sort" "testing" "time" + "github.com/andygrunwald/go-jira" + "github.com/trivago/tgo/tcontainer" "github.com/go-kit/log" @@ -106,7 +107,7 @@ func (f *fakeJira) Create(issue *jira.Issue) (*jira.Issue, *jira.Response, error // Assuming single label. query := fmt.Sprintf( - "project=\"%s\" and labels=%q order by resolutiondate desc", + "project in('%s') and labels=%q order by resolutiondate desc", issue.Fields.Project.Key, issue.Fields.Labels[0], ) @@ -187,6 +188,18 @@ func testReceiverConfigAutoResolve() *config.ReceiverConfig { } } +func testReceiverConfigWithStaticLabels() *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", + WontFixResolution: "won't-fix", + StaticLabels: []string{"somelabel"}, + } +} + func TestNotify_JIRAInteraction(t *testing.T) { testNowTime := time.Now() @@ -535,6 +548,35 @@ func TestNotify_JIRAInteraction(t *testing.T) { }, }, }, + { + name: "empty jira, new alert group with StaticLabels", + inputConfig: testReceiverConfigWithStaticLabels(), + initJira: func(t *testing.T) *fakeJira { return newTestFakeJira() }, + inputAlert: &alertmanager.Data{ + 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{"somelabel", "JIRALERT{819ba5ecba4ea5946a8d17d285cb23f3bb6862e08bb602ab08fd231cd8e1a83a1d095b0208a661787e9035f0541817634df5a994d1b5d4200d6c68a7663c97f5}"}, + Status: &jira.Status{ + StatusCategory: jira.StatusCategory{Key: "NotDone"}, + }, + Unknowns: tcontainer.MarshalMap{}, + Summary: "[FIRING:2] b d ", + }, + }, + }, + }, } { if ok := t.Run(tcase.name, func(t *testing.T) { fakeJira := tcase.initJira(t) @@ -550,7 +592,7 @@ func TestNotify_JIRAInteraction(t *testing.T) { return testNowTime } - _, err := receiver.Notify(tcase.inputAlert, true) + _, err := receiver.Notify(tcase.inputAlert, true, true, true, true, 32768) require.NoError(t, err) require.Equal(t, tcase.expectedJiraIssues, fakeJira.issuesByKey) }); !ok { diff --git a/pkg/template/template.go b/pkg/template/template.go index a180cf1..648ae7d 100644 --- a/pkg/template/template.go +++ b/pkg/template/template.go @@ -15,6 +15,7 @@ package template import ( "bytes" + "os" "regexp" "strings" "text/template" @@ -47,6 +48,9 @@ var funcs = template.FuncMap{ "stringSlice": func(s ...string) []string { return s }, + "getEnv": func(name string) string { + return os.Getenv(name) + }, } // LoadTemplate reads and parses all templates defined in the given file and constructs a jiralert.Template.