Skip to content

Commit 47846a8

Browse files
committed
Initial jira integration
Signed-off-by: Jan-Otto Kröpke <[email protected]>
1 parent 7cdecbf commit 47846a8

File tree

10 files changed

+692
-2
lines changed

10 files changed

+692
-2
lines changed

asset/assets_vfsdata.go

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/config.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,9 @@ func resolveFilepaths(baseDir string, cfg *Config) {
257257
for _, cfg := range receiver.MSTeamsConfigs {
258258
cfg.HTTPConfig.SetDirectory(baseDir)
259259
}
260+
for _, cfg := range receiver.JiraConfigs {
261+
cfg.HTTPConfig.SetDirectory(baseDir)
262+
}
260263
}
261264
}
262265

@@ -539,6 +542,33 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
539542
return fmt.Errorf("no msteams webhook URL provided")
540543
}
541544
}
545+
for _, jira := range rcv.JiraConfigs {
546+
if jira.HTTPConfig == nil {
547+
jira.HTTPConfig = c.Global.HTTPConfig
548+
}
549+
if jira.APIURL == nil {
550+
if c.Global.JiraAPIURL == nil {
551+
return fmt.Errorf("no global Jira Cloud URL set")
552+
}
553+
jira.APIURL = c.Global.JiraAPIURL
554+
}
555+
if !strings.HasSuffix(jira.APIURL.Path, "/") {
556+
jira.APIURL.Path += "/"
557+
}
558+
if jira.APIUsername == "" {
559+
if c.Global.JiraAPIUsername == "" {
560+
return fmt.Errorf("no global Jira Cloud username set")
561+
}
562+
jira.APIUsername = c.Global.JiraAPIUsername
563+
}
564+
if jira.APIToken == "" && len(jira.APITokenFile) == 0 {
565+
if c.Global.JiraAPIToken == "" && len(c.Global.JiraAPITokenFile) == 0 {
566+
return fmt.Errorf("no global Jira Cloud API Token set either inline or in a file")
567+
}
568+
jira.APIToken = c.Global.JiraAPIToken
569+
jira.APITokenFile = c.Global.JiraAPITokenFile
570+
}
571+
}
542572

543573
names[rcv.Name] = struct{}{}
544574
}
@@ -741,6 +771,10 @@ type GlobalConfig struct {
741771

742772
HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"`
743773

774+
JiraAPIURL *URL `yaml:"jira_api_url,omitempty" json:"jira_api_url,omitempty"`
775+
JiraAPIUsername string `yaml:"jira_api_username,omitempty" json:"jira_api_username,omitempty"`
776+
JiraAPIToken Secret `yaml:"jira_api_token,omitempty" json:"jira_api_token,omitempty"`
777+
JiraAPITokenFile string `yaml:"jira_api_token_file,omitempty" json:"jira_api_token_file,omitempty"`
744778
SMTPFrom string `yaml:"smtp_from,omitempty" json:"smtp_from,omitempty"`
745779
SMTPHello string `yaml:"smtp_hello,omitempty" json:"smtp_hello,omitempty"`
746780
SMTPSmarthost HostPort `yaml:"smtp_smarthost,omitempty" json:"smtp_smarthost,omitempty"`
@@ -908,6 +942,7 @@ type Receiver struct {
908942
TelegramConfigs []*TelegramConfig `yaml:"telegram_configs,omitempty" json:"telegram_configs,omitempty"`
909943
WebexConfigs []*WebexConfig `yaml:"webex_configs,omitempty" json:"webex_configs,omitempty"`
910944
MSTeamsConfigs []*MSTeamsConfig `yaml:"msteams_configs,omitempty" json:"msteams_configs,omitempty"`
945+
JiraConfigs []*JiraConfig `yaml:"jira_configs,omitempty" json:"jira_configs,omitempty"`
911946
}
912947

913948
// UnmarshalYAML implements the yaml.Unmarshaler interface for Receiver.

config/notifiers.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,14 @@ var (
172172
Title: `{{ template "msteams.default.title" . }}`,
173173
Text: `{{ template "msteams.default.text" . }}`,
174174
}
175+
176+
DefaultJiraConfig = JiraConfig{
177+
NotifierConfig: NotifierConfig{
178+
VSendResolved: true,
179+
},
180+
Summary: `{{ template "jira.default.summary" . }}`,
181+
Description: `{{ template "jira.default.description" . }}`,
182+
}
175183
)
176184

177185
// NotifierConfig contains base options common across all notifier configurations.
@@ -797,3 +805,52 @@ func (c *MSTeamsConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
797805
type plain MSTeamsConfig
798806
return unmarshal((*plain)(c))
799807
}
808+
809+
type JiraConfig struct {
810+
NotifierConfig `yaml:",inline" json:",inline"`
811+
HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"`
812+
813+
APIURL *URL `yaml:"api_url,omitempty" json:"api_url,omitempty"`
814+
APIUsername string `yaml:"api_username,omitempty" json:"api_username,omitempty"`
815+
APIToken Secret `yaml:"api_token,omitempty" json:"api_token,omitempty"`
816+
APITokenFile string `yaml:"api_token_file,omitempty" json:"api_token_file,omitempty"`
817+
818+
Project string `yaml:"project,omitempty" json:"project,omitempty"`
819+
Summary string `yaml:"summary,omitempty" json:"summary,omitempty"`
820+
Description string `yaml:"description,omitempty" json:"description,omitempty"`
821+
StaticLabels []string `yaml:"static_labels,omitempty" json:"static_labels,omitempty"`
822+
GroupLabels []string `yaml:"group_labels,omitempty" json:"group_labels,omitempty"`
823+
Priority string `yaml:"priority,omitempty" json:"priority,omitempty"`
824+
IssueType string `yaml:"issue_type,omitempty" json:"issue_type,omitempty"`
825+
826+
ReopenTransition string `yaml:"reopen_transition,omitempty" json:"reopen_transition,omitempty"`
827+
ResolveTransition string `yaml:"resolve_transition,omitempty" json:"resolve_transition,omitempty"`
828+
WontFixResolution string `yaml:"wont_fix_resolution,omitempty" json:"wont_fix_resolution,omitempty"`
829+
ReopenDuration duration `yaml:"reopen_duration,omitempty" json:"reopen_duration,omitempty"`
830+
831+
CustomFields map[string]any `yaml:"custom_fields,omitempty" json:"custom_fields,omitempty"`
832+
}
833+
834+
func (c *JiraConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
835+
*c = DefaultJiraConfig
836+
type plain JiraConfig
837+
if err := unmarshal((*plain)(c)); err != nil {
838+
return err
839+
}
840+
if c.APIToken == "" && c.APITokenFile == "" {
841+
return fmt.Errorf("missing api_token or api_token_file on jira_config")
842+
}
843+
844+
if c.APIToken != "" && len(c.APITokenFile) > 0 {
845+
return fmt.Errorf("at most one of api_token & api_token_file must be configured")
846+
}
847+
848+
if c.Project == "" {
849+
return fmt.Errorf("missing project on jira_config")
850+
}
851+
if c.IssueType == "" {
852+
return fmt.Errorf("missing issue_type on jira_config")
853+
}
854+
855+
return nil
856+
}

config/receiver/receiver.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ package receiver
1515

1616
import (
1717
"github.com/go-kit/log"
18+
"github.com/prometheus/alertmanager/notify/jira"
1819

1920
commoncfg "github.com/prometheus/common/config"
2021

@@ -92,6 +93,9 @@ func BuildReceiverIntegrations(nc config.Receiver, tmpl *template.Template, logg
9293
for i, c := range nc.MSTeamsConfigs {
9394
add("msteams", i, c, func(l log.Logger) (notify.Notifier, error) { return msteams.New(c, tmpl, l, httpOpts...) })
9495
}
96+
for i, c := range nc.JiraConfigs {
97+
add("jira", i, c, func(l log.Logger) (notify.Notifier, error) { return jira.New(c, tmpl, l, httpOpts...) })
98+
}
9599

96100
if errs.Len() > 0 {
97101
return nil, &errs

docs/configuration.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ global:
8484
# Note that Go does not support unencrypted connections to remote SMTP endpoints.
8585
[ smtp_require_tls: <bool> | default = true ]
8686

87+
[ jira_api_url: <string> ]
88+
[ jira_api_username: <string> ]
89+
[ jira_api_token: <secret> ]
90+
[ jira_api_token_file: <filepath> ]
91+
8792
# The API URL to use for Slack notifications.
8893
[ slack_api_url: <secret> ]
8994
[ slack_api_url_file: <filepath> ]
@@ -504,6 +509,8 @@ email_configs:
504509
[ - <email_config>, ... ]
505510
msteams_configs:
506511
[ - <msteams_config>, ... ]
512+
jira_configs:
513+
[ - <jira_config>, ... ]
507514
opsgenie_configs:
508515
[ - <opsgenie_config>, ... ]
509516
pagerduty_configs:
@@ -743,6 +750,87 @@ Microsoft Teams notifications are sent via the [Incoming Webhooks](https://learn
743750
[ http_config: <http_config> | default = global.http_config ]
744751
```
745752

753+
### `<jira_config>`
754+
755+
JIRA notification are sent via [JIRA Rest API v2](https://developer.atlassian.com/cloud/jira/platform/rest/v2/intro/)
756+
or [JIRA REST API v3](https://developer.atlassian.com/cloud/jira/platform/rest/v3/intro/#version).
757+
758+
Both APIs have the same feature set. The difference is that V2 uses [Wiki Markup](https://jira.atlassian.com/secure/WikiRendererHelpAction.jspa?section=all)
759+
for format the issue description and V3 uses [Atlassian Document Format (ADF)](https://developer.atlassian.com/cloud/jira/platform/apis/document/structure/).
760+
The default `jira.default.description` template only works with V2.
761+
762+
```yaml
763+
# Whether to notify about resolved alerts.
764+
[ send_resolved: <boolean> | default = true ]
765+
766+
# The incoming webhook URL.
767+
[ webhook_url: <secret> ]
768+
769+
# The Atlassian Side to send Jira API requests to. API path must be included.
770+
# Example: https://company.atlassian.net/rest/api/2/
771+
[ api_url: <string> | default = global.jira_api_url ]
772+
[ api_username: <string> | default = global.jira_api_username ]
773+
[ api_token: <string> | default = global.jira_api_token ]
774+
[ api_token_file: <string> | default = global.jira_api_token_file ]
775+
776+
# The project key where issues are created.
777+
project: <string>
778+
779+
# Issue summary template.
780+
[ summary: <tmpl_string> | default = '{{ template "jira.default.summary" . }}' ]
781+
782+
# Issue description template.
783+
[ description: <tmpl_string> | default = '{{ template "jira.default.description" . }}' ]
784+
785+
# Add labels to issues
786+
static_labels:
787+
[ - <string> ... ]
788+
789+
# Add specific group labels to issue
790+
group_labels:
791+
[ - <string> ... ]
792+
793+
# Priority of issue
794+
[ priority: <tmpl_string> ]
795+
796+
# Type of issue, e.g. Bug
797+
[ issue_type: <string> ]
798+
799+
# Name of the workflow transition to resolve an issue. The target status must have the category "done"
800+
[ resolve_transition: <string> ]
801+
802+
# Name of the workflow transition to reopen an issue. The target status should not have the category "done"
803+
[ reopen_transition: <string> ]
804+
805+
# If reopen_transition is defined, ignore issues with that resolution
806+
[ wont_fix_resolution: <string> ]
807+
808+
# If reopen_transition is defined, reopen issue not older than ...
809+
[ reopen_duration: <duration> ]
810+
811+
# Custom fields
812+
custom_fields:
813+
[ <string>: <custom_fields> ... ]
814+
815+
816+
# The HTTP client's configuration.
817+
[ http_config: <http_config> | default = global.http_config ]
818+
```
819+
820+
#### `<custom_fields>`
821+
822+
Jira custom field can have multiple types. Depends on the filed type, the values must be provided differently.
823+
824+
```yaml
825+
fields:
826+
# TextField
827+
customfield_10001: "Random text"
828+
# SelectList
829+
customfield_10002: {"value": "red"}
830+
# MultiSelect
831+
customfield_10003: [{"value": "red"}, {"value": "blue"}, {"value": "green"}]
832+
```
833+
746834
### `<opsgenie_config>`
747835

748836
OpsGenie notifications are sent via the [OpsGenie API](https://docs.opsgenie.com/docs/alert-api).

0 commit comments

Comments
 (0)