Alerting: Support Jira Integration (#100480)

This commit is contained in:
Yuri Tseretyan
2025-02-21 12:51:38 -05:00
committed by GitHub
parent 2d2e595555
commit d1dfa0576b
13 changed files with 226 additions and 12 deletions
+1 -1
View File
@@ -71,7 +71,7 @@ require (
github.com/googleapis/gax-go/v2 v2.14.1 // @grafana/grafana-backend-group
github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group
github.com/gorilla/websocket v1.5.3 // @grafana/grafana-app-platform-squad
github.com/grafana/alerting v0.0.0-20250219153626-c475b1a572b0 // @grafana/alerting-backend
github.com/grafana/alerting v0.0.0-20250220212119-4baca04e46bb // @grafana/alerting-backend
github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7 // @grafana/identity-access-team
github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31 // @grafana/identity-access-team
github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics
+2 -2
View File
@@ -1511,8 +1511,8 @@ github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7Fsg
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grafana/alerting v0.0.0-20250219153626-c475b1a572b0 h1:LGH+tVzHCDrR9hsltmkP4jmNRg5IreQw5CNFbJKlnts=
github.com/grafana/alerting v0.0.0-20250219153626-c475b1a572b0/go.mod h1:hdGB3dSl8Ma9Rjo2YiAEAjMkZ5HiNJbNDqRKDefRZrM=
github.com/grafana/alerting v0.0.0-20250220212119-4baca04e46bb h1:WfCsiuZXhGXIdzImQ9/Kjfn9M4e6f7z5mddcSKCRxmI=
github.com/grafana/alerting v0.0.0-20250220212119-4baca04e46bb/go.mod h1:hdGB3dSl8Ma9Rjo2YiAEAjMkZ5HiNJbNDqRKDefRZrM=
github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7 h1:NTMmow+74I3Jb033xhbRgWQS7A//5TDhiM4tl7bsVP4=
github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7/go.mod h1:T3X4z0ejGfJOiOmZLFeKCRT/yxWJq/RtclAc/PHj/w4=
github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31 h1:EokLC5grHwLPs4tXW8T6E8187H1e5G9AP0QQ5B60HbA=
@@ -91,6 +91,13 @@ func ContactPointToContactPointExport(cp definitions.ContactPoint) (notify.APIRe
}
integration = append(integration, el)
}
for _, i := range cp.Jira {
el, err := marshallIntegration(j, "jira", i, i.DisableResolveMessage)
if err != nil {
errs = append(errs, err)
}
integration = append(integration, el)
}
for _, i := range cp.Kafka {
el, err := marshallIntegration(j, "kafka", i, i.DisableResolveMessage)
if err != nil {
@@ -271,6 +278,11 @@ func parseIntegration(json jsoniter.API, result *definitions.ContactPoint, recei
if err = json.Unmarshal(data, &integration); err == nil {
result.Googlechat = append(result.Googlechat, integration)
}
case "jira":
integration := definitions.JiraIntegration{DisableResolveMessage: disable}
if err = json.Unmarshal(data, &integration); err == nil {
result.Jira = append(result.Jira, integration)
}
case "kafka":
integration := definitions.KafkaIntegration{DisableResolveMessage: disable}
if err = json.Unmarshal(data, &integration); err == nil {
@@ -62,6 +62,29 @@ type GooglechatIntegration struct {
Message *string `json:"message,omitempty" yaml:"message,omitempty" hcl:"message"`
}
type JiraIntegration struct {
DisableResolveMessage *bool `json:"-" yaml:"-" hcl:"disable_resolve_message"`
URL string `yaml:"api_url,omitempty" json:"api_url,omitempty" hcl:"api_url"`
Project string `yaml:"project,omitempty" json:"project,omitempty" hcl:"project"`
IssueType string `yaml:"issue_type,omitempty" json:"issue_type,omitempty" hcl:"issue_type"`
Summary *string `yaml:"summary,omitempty" json:"summary,omitempty" hcl:"summary"`
Description *string `yaml:"description,omitempty" json:"description,omitempty" hcl:"description"`
Labels *[]string `yaml:"labels,omitempty" json:"labels,omitempty" hcl:"labels"`
Priority *string `yaml:"priority,omitempty" json:"priority,omitempty" hcl:"priority"`
ReopenTransition *string `yaml:"reopen_transition,omitempty" json:"reopen_transition,omitempty" hcl:"reopen_transition"`
ResolveTransition *string `yaml:"resolve_transition,omitempty" json:"resolve_transition,omitempty" hcl:"resolve_transition"`
WontFixResolution *string `yaml:"wont_fix_resolution,omitempty" json:"wont_fix_resolution,omitempty" hcl:"wont_fix_resolution"`
ReopenDuration *string `yaml:"reopen_duration,omitempty" json:"reopen_duration,omitempty" hcl:"reopen_duration"`
DedupKeyFieldName *string `yaml:"dedup_key_field,omitempty" json:"dedup_key_field,omitempty" hcl:"dedup_key_field"`
Fields *map[string]any `yaml:"fields,omitempty" json:"fields,omitempty" hcl:"fields"`
User *Secret `yaml:"user,omitempty" json:"user,omitempty" hcl:"user"`
Password *Secret `yaml:"password,omitempty" json:"password,omitempty" hcl:"password"`
Token *Secret `yaml:"api_token,omitempty" json:"api_token,omitempty" hcl:"api_token"`
}
type KafkaIntegration struct {
DisableResolveMessage *bool `json:"-" yaml:"-" hcl:"disable_resolve_message"`
@@ -321,6 +344,7 @@ type ContactPoint struct {
Discord []DiscordIntegration `json:"discord" yaml:"discord" hcl:"discord,block"`
Email []EmailIntegration `json:"email" yaml:"email" hcl:"email,block"`
Googlechat []GooglechatIntegration `json:"googlechat" yaml:"googlechat" hcl:"googlechat,block"`
Jira []JiraIntegration `json:"jira" yaml:"jira" hcl:"jira,block"`
Kafka []KafkaIntegration `json:"kafka" yaml:"kafka" hcl:"kafka,block"`
Line []LineIntegration `json:"line" yaml:"line" hcl:"line,block"`
Mqtt []MqttIntegration `json:"mqtt" yaml:"mqtt" hcl:"mqtt,block"`
@@ -5,6 +5,7 @@ import (
"os"
"strings"
"github.com/grafana/alerting/receivers/jira"
alertingMqtt "github.com/grafana/alerting/receivers/mqtt"
alertingOpsgenie "github.com/grafana/alerting/receivers/opsgenie"
alertingPagerduty "github.com/grafana/alerting/receivers/pagerduty"
@@ -365,7 +366,7 @@ func GetAvailableNotifiers() []*NotifierPlugin {
InputType: InputTypeText,
PropertyName: "details",
},
{ //New in 11.1
{ // New in 11.1
Label: "URL",
Description: "The URL to send API requests to",
Element: ElementTypeInput,
@@ -1708,6 +1709,154 @@ func GetAvailableNotifiers() []*NotifierPlugin {
},
},
},
{ // Since Grafana 11.6
Type: "jira",
Name: "Jira",
Description: "Creates Jira issues from alerts",
Heading: "Jira settings",
Options: []NotifierOption{
{
Label: "API URL of Jira instance, including version of API",
Element: ElementTypeInput,
InputType: InputTypeText,
Placeholder: "https://grafana.atlassian.net/rest/api/3",
PropertyName: "api_url",
Description: "Supported v2 or v3 APIs",
Required: true,
},
{
Label: "HTTP Basic Authentication - Username",
Element: ElementTypeInput,
InputType: InputTypeText,
PropertyName: "user",
Description: "Username to use for Jira authentication.",
Secure: true,
Required: false,
},
{
Label: "HTTP Basic Authentication - Password",
Element: ElementTypeInput,
InputType: InputTypePassword,
PropertyName: "password",
// Go to https://id.atlassian.com/manage-profile/security/api-tokens to obtain a token.
Description: "Password to use for Jira authentication.",
Secure: true,
Required: false,
},
{
Label: "Authorization Header - Personal Access Token",
Element: ElementTypeInput,
InputType: InputTypePassword,
PropertyName: "api_token",
// Go to https://confluence.atlassian.com/enterprise/using-personal-access-tokens-1026032365.html for how to obtain a token.
Description: "Personal Access Token that is used as a bearer authorization header.",
Secure: true,
Required: false,
},
{
Label: "Project Key",
Description: "The project key associated with the relevant Jira project",
Element: ElementTypeInput,
InputType: InputTypeText,
Placeholder: "Grafana",
PropertyName: "project",
Required: true,
},
{
Label: "Issue Type",
Description: "The type of the Jira issue (e.g., Bug, Task, Story). You can use templates to customize this field.",
Element: ElementTypeInput,
InputType: InputTypeText,
Placeholder: "Task",
Required: true,
PropertyName: "issue_type",
},
{
Label: "Summary",
Description: fmt.Sprintf("The summary of the Jira issue. You can use templates to customize this field. Maximum length is %d characters.", jira.MaxSummaryLenRunes),
Element: ElementTypeInput,
InputType: InputTypeText,
Placeholder: jira.DefaultSummary,
PropertyName: "summary",
},
{
Label: "Description",
Description: fmt.Sprintf("The description of the Jira issue. You can use templates to customize this field. Maximum length is %d characters.", jira.MaxDescriptionLenRunes),
Element: ElementTypeTextArea,
InputType: InputTypeText,
Placeholder: jira.DefaultDescription,
PropertyName: "description",
},
{
Label: "Labels",
Description: "Labels to assign to the Jira issue. You can use templates to customize this field.",
Element: ElementStringArray,
Placeholder: "",
PropertyName: "labels",
},
{
Label: "Priority",
Description: "The priority of the Jira issue (e.g., High, Medium, Low). You can use templates to customize this field.",
Element: ElementTypeInput,
InputType: InputTypeText,
Placeholder: jira.DefaultPriority,
PropertyName: "priority",
Required: false,
},
{
Label: "Resolve Transition",
Description: `Name of the workflow transition to resolve an issue. The target status must have the category "done". If not set, the issue will not be resolved.`,
Element: ElementTypeInput,
InputType: InputTypeText,
Placeholder: "",
PropertyName: "resolve_transition",
Required: false,
},
{
Label: "Reopen Transition",
Description: `Name of the workflow transition to resolve an issue. The target status must not have the category "done". If not set, the issue will not be reopened.`,
Element: ElementTypeInput,
InputType: InputTypeText,
Placeholder: "",
PropertyName: "reopen_transition",
Required: false,
},
{
Label: "Reopen Duration",
Description: "Reopen the issue when it is not older than this value in minutes. Otherwise, create a new issue.",
Element: ElementTypeInput,
InputType: InputTypeText,
Placeholder: "10m",
PropertyName: "reopen_duration",
},
{
Label: "\"Won't fix\" Transition",
Description: `If reopen transition is defined, ignore issues with that resolution.`,
Element: ElementTypeInput,
InputType: InputTypeText,
Placeholder: "",
PropertyName: "wont_fix_resolution",
Required: false,
},
{
Label: "Custom field ID for deduplication",
Description: "Id of the custom field where the deduplication key should be stored. Otherwise, it is added to labels in format 'ALERT($KEY).'",
Element: ElementTypeInput,
InputType: InputTypeText,
Placeholder: "10000",
ValidationRule: "^[0-9]+$",
PropertyName: "dedup_key_field",
},
{
Label: "Custom Field Data",
Description: "Custom field data to set on the Jira issue.",
Element: ElementTypeKeyValueMap,
InputType: InputTypeText,
Placeholder: "",
PropertyName: "fields",
},
},
},
}
}
@@ -27,20 +27,38 @@ func TestGetSecretKeysForContactPointType(t *testing.T) {
{receiverType: "prometheus-alertmanager", expectedSecretFields: []string{"basicAuthPassword"}},
{receiverType: "discord", expectedSecretFields: []string{"url"}},
{receiverType: "googlechat", expectedSecretFields: []string{"url"}},
{receiverType: "line", expectedSecretFields: []string{"token"}},
{receiverType: "LINE", expectedSecretFields: []string{"token"}},
{receiverType: "threema", expectedSecretFields: []string{"api_secret"}},
{receiverType: "opsgenie", expectedSecretFields: []string{"apiKey"}},
{receiverType: "webex", expectedSecretFields: []string{"bot_token"}},
{receiverType: "sns", expectedSecretFields: []string{"sigv4.access_key", "sigv4.secret_key"}},
{receiverType: "mqtt", expectedSecretFields: []string{"password", "tlsConfig.caCertificate", "tlsConfig.clientCertificate", "tlsConfig.clientKey"}},
{receiverType: "jira", expectedSecretFields: []string{"user", "password", "api_token"}},
}
n := GetAvailableNotifiers()
allTypes := make(map[string]struct{}, len(n))
for _, plugin := range n {
allTypes[plugin.Type] = struct{}{}
}
for _, testCase := range testCases {
delete(allTypes, testCase.receiverType)
t.Run(testCase.receiverType, func(t *testing.T) {
got, err := GetSecretKeysForContactPointType(testCase.receiverType)
require.NoError(t, err)
t.Logf("got secret fields: %#v", got)
require.ElementsMatch(t, testCase.expectedSecretFields, got)
})
}
for integrationType := range allTypes {
t.Run(integrationType, func(t *testing.T) {
got, err := GetSecretKeysForContactPointType(integrationType)
require.NoError(t, err)
require.Emptyf(t, got, "secret keys for %s should be empty", integrationType)
})
}
require.Emptyf(t, allTypes, "not all types are covered: %s", allTypes)
}
func Test_getSecretFields(t *testing.T) {
@@ -45,6 +45,8 @@ const (
ElementTypeSubform = "subform"
// ElementSubformArray will render a multiple sub-forms with schema defined in SubformOptions
ElementSubformArray = "subform_array"
// ElementStringArray will render a set of fields to manage an array of strings.
ElementStringArray = "string_array"
)
// InputType is the type of input that can be rendered in the frontend.
@@ -423,6 +423,9 @@ func TestRemoveSecretsForContactPoint(t *testing.T) {
"webhook": func(settings map[string]any) { // add additional field to the settings because valid config does not allow it to be specified along with password
settings["authorization_credentials"] = "test-authz-creds"
},
"jira": func(settings map[string]any) { // add additional field to the settings because valid config does not allow it to be specified along with password
settings["api_token"] = "test-token"
},
}
configs := notify.AllKnownConfigsForTesting
+1 -1
View File
@@ -192,7 +192,7 @@ require (
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/grafana/alerting v0.0.0-20250219153626-c475b1a572b0 // indirect
github.com/grafana/alerting v0.0.0-20250220212119-4baca04e46bb // indirect
github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7 // indirect
github.com/grafana/dataplane/sdata v0.0.9 // indirect
github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 // indirect
+2 -2
View File
@@ -566,8 +566,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grafana/alerting v0.0.0-20250219153626-c475b1a572b0 h1:LGH+tVzHCDrR9hsltmkP4jmNRg5IreQw5CNFbJKlnts=
github.com/grafana/alerting v0.0.0-20250219153626-c475b1a572b0/go.mod h1:hdGB3dSl8Ma9Rjo2YiAEAjMkZ5HiNJbNDqRKDefRZrM=
github.com/grafana/alerting v0.0.0-20250220212119-4baca04e46bb h1:WfCsiuZXhGXIdzImQ9/Kjfn9M4e6f7z5mddcSKCRxmI=
github.com/grafana/alerting v0.0.0-20250220212119-4baca04e46bb/go.mod h1:hdGB3dSl8Ma9Rjo2YiAEAjMkZ5HiNJbNDqRKDefRZrM=
github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7 h1:NTMmow+74I3Jb033xhbRgWQS7A//5TDhiM4tl7bsVP4=
github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7/go.mod h1:T3X4z0ejGfJOiOmZLFeKCRT/yxWJq/RtclAc/PHj/w4=
github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31 h1:EokLC5grHwLPs4tXW8T6E8187H1e5G9AP0QQ5B60HbA=
+1 -1
View File
@@ -117,7 +117,7 @@ require (
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/grafana/alerting v0.0.0-20250219153626-c475b1a572b0 // indirect
github.com/grafana/alerting v0.0.0-20250220212119-4baca04e46bb // indirect
github.com/grafana/dataplane/sdata v0.0.9 // indirect
github.com/grafana/grafana-app-sdk/logging v0.30.0 // indirect
github.com/grafana/grafana-aws-sdk v0.31.5 // indirect
+2 -2
View File
@@ -397,8 +397,8 @@ github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z
github.com/gorilla/mux v1.7.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/grafana/alerting v0.0.0-20250219153626-c475b1a572b0 h1:LGH+tVzHCDrR9hsltmkP4jmNRg5IreQw5CNFbJKlnts=
github.com/grafana/alerting v0.0.0-20250219153626-c475b1a572b0/go.mod h1:hdGB3dSl8Ma9Rjo2YiAEAjMkZ5HiNJbNDqRKDefRZrM=
github.com/grafana/alerting v0.0.0-20250220212119-4baca04e46bb h1:WfCsiuZXhGXIdzImQ9/Kjfn9M4e6f7z5mddcSKCRxmI=
github.com/grafana/alerting v0.0.0-20250220212119-4baca04e46bb/go.mod h1:hdGB3dSl8Ma9Rjo2YiAEAjMkZ5HiNJbNDqRKDefRZrM=
github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7 h1:NTMmow+74I3Jb033xhbRgWQS7A//5TDhiM4tl7bsVP4=
github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7/go.mod h1:T3X4z0ejGfJOiOmZLFeKCRT/yxWJq/RtclAc/PHj/w4=
github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31 h1:EokLC5grHwLPs4tXW8T6E8187H1e5G9AP0QQ5B60HbA=
@@ -1392,9 +1392,15 @@ func TestIntegrationCRUD(t *testing.T) {
t.Run("should return secrets in secureFields but not settings", func(t *testing.T) {
for _, integration := range get.Spec.Integrations {
t.Run(integration.Type, func(t *testing.T) {
expected := notify.AllKnownConfigsForTesting[strings.ToLower(integration.Type)]
var fields map[string]any
require.NoError(t, json.Unmarshal([]byte(expected.Config), &fields))
secretFields, err := channels_config.GetSecretKeysForContactPointType(integration.Type)
require.NoError(t, err)
for _, field := range secretFields {
if _, ok := fields[field]; !ok { // skip field that is not in the original setting
continue
}
assert.Contains(t, integration.SecureFields, field)
assert.Truef(t, integration.SecureFields[field], "secure field should be always true")