Skip to content

Commit

Permalink
Initial jira integration
Browse files Browse the repository at this point in the history
Signed-off-by: Jan-Otto Kröpke <[email protected]>
  • Loading branch information
jkroepke committed Nov 1, 2023
1 parent 7cdecbf commit ab5c92c
Show file tree
Hide file tree
Showing 10 changed files with 711 additions and 2 deletions.
4 changes: 2 additions & 2 deletions asset/assets_vfsdata.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 35 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,9 @@ func resolveFilepaths(baseDir string, cfg *Config) {
for _, cfg := range receiver.MSTeamsConfigs {
cfg.HTTPConfig.SetDirectory(baseDir)
}
for _, cfg := range receiver.JiraConfigs {
cfg.HTTPConfig.SetDirectory(baseDir)
}
}
}

Expand Down Expand Up @@ -539,6 +542,33 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
return fmt.Errorf("no msteams webhook URL provided")
}
}
for _, jira := range rcv.JiraConfigs {
if jira.HTTPConfig == nil {
jira.HTTPConfig = c.Global.HTTPConfig
}
if jira.APIURL == nil {
if c.Global.JiraAPIURL == nil {
return fmt.Errorf("no global Jira Cloud URL set")
}
jira.APIURL = c.Global.JiraAPIURL
}
if !strings.HasSuffix(jira.APIURL.Path, "/") {
jira.APIURL.Path += "/"
}
if jira.APIUsername == "" {
if c.Global.JiraAPIUsername == "" {
return fmt.Errorf("no global Jira Cloud username set")
}
jira.APIUsername = c.Global.JiraAPIUsername
}
if jira.APIToken == "" && len(jira.APITokenFile) == 0 {
if c.Global.JiraAPIToken == "" && len(c.Global.JiraAPITokenFile) == 0 {
return fmt.Errorf("no global Jira Cloud API Token set either inline or in a file")
}
jira.APIToken = c.Global.JiraAPIToken
jira.APITokenFile = c.Global.JiraAPITokenFile
}
}

names[rcv.Name] = struct{}{}
}
Expand Down Expand Up @@ -741,6 +771,10 @@ type GlobalConfig struct {

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

JiraAPIURL *URL `yaml:"jira_api_url,omitempty" json:"jira_api_url,omitempty"`
JiraAPIUsername string `yaml:"jira_api_username,omitempty" json:"jira_api_username,omitempty"`
JiraAPIToken Secret `yaml:"jira_api_token,omitempty" json:"jira_api_token,omitempty"`
JiraAPITokenFile string `yaml:"jira_api_token_file,omitempty" json:"jira_api_token_file,omitempty"`
SMTPFrom string `yaml:"smtp_from,omitempty" json:"smtp_from,omitempty"`
SMTPHello string `yaml:"smtp_hello,omitempty" json:"smtp_hello,omitempty"`
SMTPSmarthost HostPort `yaml:"smtp_smarthost,omitempty" json:"smtp_smarthost,omitempty"`
Expand Down Expand Up @@ -908,6 +942,7 @@ type Receiver struct {
TelegramConfigs []*TelegramConfig `yaml:"telegram_configs,omitempty" json:"telegram_configs,omitempty"`
WebexConfigs []*WebexConfig `yaml:"webex_configs,omitempty" json:"webex_configs,omitempty"`
MSTeamsConfigs []*MSTeamsConfig `yaml:"msteams_configs,omitempty" json:"msteams_configs,omitempty"`
JiraConfigs []*JiraConfig `yaml:"jira_configs,omitempty" json:"jira_configs,omitempty"`
}

// UnmarshalYAML implements the yaml.Unmarshaler interface for Receiver.
Expand Down
58 changes: 58 additions & 0 deletions config/notifiers.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,14 @@ var (
Title: `{{ template "msteams.default.title" . }}`,
Text: `{{ template "msteams.default.text" . }}`,
}

DefaultJiraConfig = JiraConfig{
NotifierConfig: NotifierConfig{
VSendResolved: true,
},
Summary: `{{ template "jira.default.summary" . }}`,
Description: `{{ template "jira.default.description" . }}`,
}
)

// NotifierConfig contains base options common across all notifier configurations.
Expand Down Expand Up @@ -797,3 +805,53 @@ func (c *MSTeamsConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
type plain MSTeamsConfig
return unmarshal((*plain)(c))
}

type JiraConfig struct {
NotifierConfig `yaml:",inline" json:",inline"`
HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"`

APIURL *URL `yaml:"api_url,omitempty" json:"api_url,omitempty"`
APIUsername string `yaml:"api_username,omitempty" json:"api_username,omitempty"`
APIToken Secret `yaml:"api_token,omitempty" json:"api_token,omitempty"`
APITokenFile string `yaml:"api_token_file,omitempty" json:"api_token_file,omitempty"`

Project string `yaml:"project,omitempty" json:"project,omitempty"`
Summary string `yaml:"summary,omitempty" json:"summary,omitempty"`
Description string `yaml:"description,omitempty" json:"description,omitempty"`
StaticLabels []string `yaml:"static_labels,omitempty" json:"static_labels,omitempty"`
GroupLabels []string `yaml:"group_labels,omitempty" json:"group_labels,omitempty"`
Components []string `yaml:"components,omitempty" json:"components,omitempty"`
Priority string `yaml:"priority,omitempty" json:"priority,omitempty"`
IssueType string `yaml:"issue_type,omitempty" json:"issue_type,omitempty"`

ReopenTransition string `yaml:"reopen_transition,omitempty" json:"reopen_transition,omitempty"`
ResolveTransition string `yaml:"resolve_transition,omitempty" json:"resolve_transition,omitempty"`
WontFixResolution string `yaml:"wont_fix_resolution,omitempty" json:"wont_fix_resolution,omitempty"`
ReopenDuration duration `yaml:"reopen_duration,omitempty" json:"reopen_duration,omitempty"`

CustomFields map[string]any `yaml:"custom_fields,omitempty" json:"custom_fields,omitempty"`
}

func (c *JiraConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
*c = DefaultJiraConfig
type plain JiraConfig
if err := unmarshal((*plain)(c)); err != nil {
return err
}
if c.APIToken == "" && c.APITokenFile == "" {
return fmt.Errorf("missing api_token or api_token_file on jira_config")
}

if c.APIToken != "" && len(c.APITokenFile) > 0 {
return fmt.Errorf("at most one of api_token & api_token_file must be configured")
}

if c.Project == "" {
return fmt.Errorf("missing project on jira_config")
}
if c.IssueType == "" {
return fmt.Errorf("missing issue_type on jira_config")
}

return nil
}
4 changes: 4 additions & 0 deletions config/receiver/receiver.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/prometheus/alertmanager/notify"
"github.com/prometheus/alertmanager/notify/discord"
"github.com/prometheus/alertmanager/notify/email"
"github.com/prometheus/alertmanager/notify/jira"
"github.com/prometheus/alertmanager/notify/msteams"
"github.com/prometheus/alertmanager/notify/opsgenie"
"github.com/prometheus/alertmanager/notify/pagerduty"
Expand Down Expand Up @@ -92,6 +93,9 @@ func BuildReceiverIntegrations(nc config.Receiver, tmpl *template.Template, logg
for i, c := range nc.MSTeamsConfigs {
add("msteams", i, c, func(l log.Logger) (notify.Notifier, error) { return msteams.New(c, tmpl, l, httpOpts...) })
}
for i, c := range nc.JiraConfigs {
add("jira", i, c, func(l log.Logger) (notify.Notifier, error) { return jira.New(c, tmpl, l, httpOpts...) })
}

if errs.Len() > 0 {
return nil, &errs
Expand Down
93 changes: 93 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ global:
# Note that Go does not support unencrypted connections to remote SMTP endpoints.
[ smtp_require_tls: <bool> | default = true ]

[ jira_api_url: <string> ]
[ jira_api_username: <string> ]
[ jira_api_token: <secret> ]
[ jira_api_token_file: <filepath> ]

# The API URL to use for Slack notifications.
[ slack_api_url: <secret> ]
[ slack_api_url_file: <filepath> ]
Expand Down Expand Up @@ -504,6 +509,8 @@ email_configs:
[ - <email_config>, ... ]
msteams_configs:
[ - <msteams_config>, ... ]
jira_configs:
[ - <jira_config>, ... ]
opsgenie_configs:
[ - <opsgenie_config>, ... ]
pagerduty_configs:
Expand Down Expand Up @@ -743,6 +750,92 @@ Microsoft Teams notifications are sent via the [Incoming Webhooks](https://learn
[ http_config: <http_config> | default = global.http_config ]
```

### `<jira_config>`

JIRA notification are sent via [JIRA Rest API v2](https://developer.atlassian.com/cloud/jira/platform/rest/v2/intro/)
or [JIRA REST API v3](https://developer.atlassian.com/cloud/jira/platform/rest/v3/intro/#version).

Both APIs have the same feature set. The difference is that V2 uses [Wiki Markup](https://jira.atlassian.com/secure/WikiRendererHelpAction.jspa?section=all)
for format the issue description and V3 uses [Atlassian Document Format (ADF)](https://developer.atlassian.com/cloud/jira/platform/apis/document/structure/).
The default `jira.default.description` template only works with V2.

```yaml
# Whether to notify about resolved alerts.
[ send_resolved: <boolean> | default = true ]
# The incoming webhook URL.
[ webhook_url: <secret> ]
# The Atlassian Side to send Jira API requests to. API path must be included.
# Example: https://company.atlassian.net/rest/api/2/
[ api_url: <string> | default = global.jira_api_url ]
[ api_username: <string> | default = global.jira_api_username ]
[ api_token: <string> | default = global.jira_api_token ]
[ api_token_file: <string> | default = global.jira_api_token_file ]
# The project key where issues are created.
project: <string>
# Issue summary template.
[ summary: <tmpl_string> | default = '{{ template "jira.default.summary" . }}' ]
# Issue description template.
[ description: <tmpl_string> | default = '{{ template "jira.default.description" . }}' ]
# Add labels to issues
static_labels:
[ - <string> ... ]
# Add specific group labels to issue
group_labels:
[ - <string> ... ]
# JIRA components
components:
[ - <string> ... ]
# Priority of issue
[ priority: <tmpl_string> ]
# Type of issue, e.g. Bug
[ issue_type: <string> ]
# Name of the workflow transition to resolve an issue. The target status must have the category "done"
[ resolve_transition: <string> ]
# Name of the workflow transition to reopen an issue. The target status should not have the category "done"
[ reopen_transition: <string> ]
# If reopen_transition is defined, ignore issues with that resolution
[ wont_fix_resolution: <string> ]
# If reopen_transition is defined, reopen issue not older than ...
[ reopen_duration: <duration> ]
# Custom fields
custom_fields:
[ <string>: <custom_fields> ... ]
# The HTTP client's configuration.
[ http_config: <http_config> | default = global.http_config ]
```

#### `<custom_fields>`

Jira custom field can have multiple types. Depends on the filed type, the values must be provided differently.
See https://developer.atlassian.com/server/jira/platform/jira-rest-api-examples/#setting-custom-field-data-for-other-field-types for further examples.

```yaml
fields:
# TextField
customfield_10001: "Random text"
# SelectList
customfield_10002: {"value": "red"}
# MultiSelect
customfield_10003: [{"value": "red"}, {"value": "blue"}, {"value": "green"}]
```

### `<opsgenie_config>`

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

0 comments on commit ab5c92c

Please sign in to comment.