mirror of
https://github.com/prymitive/karma
synced 2026-02-13 20:59:53 +00:00
feat(ui): replace jira link detection with a generic link finder
Fixes #1140
This commit is contained in:
@@ -932,8 +932,8 @@ func testAlert(version string, t *testing.T, expectedAlert, gotAlert models.Aler
|
||||
for _, gs := range gotAM.Silences {
|
||||
if es.Comment == gs.Comment &&
|
||||
es.CreatedBy == gs.CreatedBy &&
|
||||
es.JiraID == gs.JiraID &&
|
||||
es.JiraURL == gs.JiraURL {
|
||||
es.TicketID == gs.TicketID &&
|
||||
es.TicketURL == gs.TicketURL {
|
||||
foundSilence = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
@@ -197,11 +198,18 @@ func main() {
|
||||
config.Config.LogValues()
|
||||
}
|
||||
|
||||
jiraRules := []models.JiraRule{}
|
||||
for _, rule := range config.Config.JIRA {
|
||||
jiraRules = append(jiraRules, models.JiraRule{Regex: rule.Regex, URI: rule.URI})
|
||||
linkDetectRules := []models.LinkDetectRule{}
|
||||
for _, rule := range config.Config.Silences.Comments.LinkDetect.Rules {
|
||||
if rule.Regex == "" || rule.URITemplate == "" {
|
||||
log.Fatalf("Invalid link detect rule, regex '%s' uriTemplate '%s'", rule.Regex, rule.URITemplate)
|
||||
}
|
||||
re, err := regexp.Compile(rule.Regex)
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid link detect rule '%s': %s", rule.Regex, err)
|
||||
}
|
||||
linkDetectRules = append(linkDetectRules, models.LinkDetectRule{Regex: re, URITemplate: rule.URITemplate})
|
||||
}
|
||||
transform.ParseRules(jiraRules)
|
||||
transform.SetLinkRules(linkDetectRules)
|
||||
|
||||
apiCache = cache.New(cache.NoExpiration, 10*time.Second)
|
||||
|
||||
|
||||
@@ -64,9 +64,12 @@ log:
|
||||
sentry:
|
||||
private: https://84a9ef37a6ed4fdb80e9ea2310d1ed26:8c6ee6f0ab02406482ff4b4e824e2c27@sentry.io/1279017
|
||||
public: https://84a9ef37a6ed4fdb80e9ea2310d1ed26@sentry.io/1279017
|
||||
jira:
|
||||
- regex: DEVOPS-[0-9]+
|
||||
uri: https://jira.example.com
|
||||
silences:
|
||||
comments:
|
||||
linkDetect:
|
||||
rules:
|
||||
- regex: "(DEVOPS-[0-9]+)"
|
||||
uriTemplate: https://jira.example.com/browse/$1
|
||||
silenceForm:
|
||||
author:
|
||||
populate_from_header:
|
||||
|
||||
@@ -684,30 +684,38 @@ log:
|
||||
format: text
|
||||
```
|
||||
|
||||
### JIRA
|
||||
### Silences
|
||||
|
||||
`jira` section allows specifying a list of regex rules for finding links to Jira
|
||||
issues in silence comments. If a string inside a comment matches one of the
|
||||
rules it will be rendered as a link.
|
||||
`silences` section allows specifying to configure silence post post-processing.
|
||||
Syntax:
|
||||
|
||||
```YAML
|
||||
jira:
|
||||
- regex: string
|
||||
- uri: string
|
||||
silences:
|
||||
comments:
|
||||
linkDetect:
|
||||
rules: list of link detection rules
|
||||
```
|
||||
|
||||
- `regex` - regular expression for matching Jira issue ID.
|
||||
- `uri` - base URL for Jira instance, `/browse/FOO-1` will be appended to it
|
||||
(where `FOO-1` is example issue ID).
|
||||
- `comments:linkDetect:rules` allows to specify a list of rules to detect links
|
||||
inside silence comments. It's intended to find ticket system ID strings and
|
||||
turn them into links.
|
||||
Each rule must specify:
|
||||
- `regex` - regular expression that matches ticket system IDs. Each regex must
|
||||
contain at least one capture group `(regex)`.
|
||||
- `uriTemplate` - template string that will be used to generate a link.
|
||||
Each template must include `$1` which will be replaced with text matched
|
||||
by the `regex`.
|
||||
|
||||
Example where a string `DEVOPS-123` inside a comment would be rendered as a link
|
||||
to `https://jira.example.com/browse/DEVOPS-123`.
|
||||
to a JIRA ticket `https://jira.example.com/browse/DEVOPS-123`.
|
||||
|
||||
```YAML
|
||||
jira:
|
||||
- regex: DEVOPS-[0-9]+
|
||||
uri: https://jira.example.com
|
||||
silences:
|
||||
comments:
|
||||
linkDetect:
|
||||
rules:
|
||||
- regex: "(DEVOPS-[0-9]+)"
|
||||
uriTemplate: https://jira.example.com/browse/$1
|
||||
```
|
||||
|
||||
Defaults:
|
||||
|
||||
@@ -46,9 +46,12 @@ listen:
|
||||
log:
|
||||
config: false
|
||||
level: info
|
||||
jira:
|
||||
- regex: DEVOPS-[0-9]+
|
||||
uri: https://jira.example.com
|
||||
silences:
|
||||
comments:
|
||||
linkDetect:
|
||||
rules:
|
||||
- regex: "(DEVOPS-[0-9]+)"
|
||||
uriTemplate: https://jira.example.com/browse/$1
|
||||
receivers:
|
||||
keep: []
|
||||
strip: []
|
||||
|
||||
@@ -184,11 +184,11 @@ func (am *Alertmanager) pullSilences(version string) error {
|
||||
}
|
||||
log.Infof("[%s] Got %d silences(s) in %s", am.Name, len(silences), time.Since(start))
|
||||
|
||||
log.Infof("[%s] Detecting JIRA links in silences (%d)", am.Name, len(silences))
|
||||
log.Infof("[%s] Detecting ticket links in silences (%d)", am.Name, len(silences))
|
||||
silenceMap := map[string]models.Silence{}
|
||||
for _, silence := range silences {
|
||||
silence := silence // scopelint pin
|
||||
silence.JiraID, silence.JiraURL = transform.DetectJIRAs(&silence)
|
||||
silence.TicketID, silence.TicketURL = transform.DetectLinks(&silence)
|
||||
silenceMap[silence.ID] = silence
|
||||
}
|
||||
|
||||
|
||||
@@ -218,7 +218,7 @@ func (config *configSchema) Read() {
|
||||
}
|
||||
}
|
||||
|
||||
err = v.UnmarshalKey("jira", &config.JIRA)
|
||||
err = v.UnmarshalKey("silences.comments.linkDetect.rules", &config.Silences.Comments.LinkDetect.Rules)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -123,13 +123,16 @@ log:
|
||||
config: true
|
||||
level: info
|
||||
format: text
|
||||
jira: []
|
||||
receivers:
|
||||
keep: []
|
||||
strip: []
|
||||
sentry:
|
||||
private: secret key
|
||||
public: public key
|
||||
silences:
|
||||
comments:
|
||||
linkDetect:
|
||||
rules: []
|
||||
silenceForm:
|
||||
author:
|
||||
populate_from_header:
|
||||
|
||||
@@ -20,9 +20,9 @@ type alertmanagerConfig struct {
|
||||
Headers map[string]string
|
||||
}
|
||||
|
||||
type jiraRule struct {
|
||||
Regex string
|
||||
URI string
|
||||
type LinkDetectRules struct {
|
||||
Regex string `yaml:"regex" mapstructure:"regex"`
|
||||
URITemplate string `yaml:"uriTemplate" mapstructure:"uriTemplate"`
|
||||
}
|
||||
|
||||
type CustomLabelColor struct {
|
||||
@@ -94,7 +94,6 @@ type configSchema struct {
|
||||
Level string
|
||||
Format string
|
||||
}
|
||||
JIRA []jiraRule
|
||||
Receivers struct {
|
||||
Keep []string
|
||||
Strip []string
|
||||
@@ -103,6 +102,13 @@ type configSchema struct {
|
||||
Private string
|
||||
Public string
|
||||
}
|
||||
Silences struct {
|
||||
Comments struct {
|
||||
LinkDetect struct {
|
||||
Rules []LinkDetectRules `yaml:"rules" mapstructure:"rules"`
|
||||
} `yaml:"linkDetect" mapstructure:"linkDetect"`
|
||||
} `yaml:"comments" mapstructure:"comments"`
|
||||
} `yaml:"silences" mapstructure:"silences"`
|
||||
SilenceForm struct {
|
||||
Author struct {
|
||||
PopulateFromHeader struct {
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"github.com/prymitive/karma/internal/filters"
|
||||
"github.com/prymitive/karma/internal/models"
|
||||
|
||||
"github.com/pmezard/go-difflib/difflib"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
type acTest struct {
|
||||
@@ -57,7 +57,7 @@ var acTests = []acTest{
|
||||
"1234567890": {
|
||||
ID: "1234567890",
|
||||
CreatedBy: "me@example.com",
|
||||
JiraID: "JIRA-1",
|
||||
TicketID: "JIRA-1",
|
||||
},
|
||||
}},
|
||||
},
|
||||
@@ -89,10 +89,10 @@ var acTests = []acTest{
|
||||
"@silence_author=~me@example.com",
|
||||
"@silence_id!=1234567890",
|
||||
"@silence_id=1234567890",
|
||||
"@silence_jira!=JIRA-1",
|
||||
"@silence_jira!~JIRA-1",
|
||||
"@silence_jira=JIRA-1",
|
||||
"@silence_jira=~JIRA-1",
|
||||
"@silence_ticket!=JIRA-1",
|
||||
"@silence_ticket!~JIRA-1",
|
||||
"@silence_ticket=JIRA-1",
|
||||
"@silence_ticket=~JIRA-1",
|
||||
"@state!=active",
|
||||
"@state!=suppressed",
|
||||
"@state=active",
|
||||
@@ -131,18 +131,9 @@ func TestBuildAutocomplete(t *testing.T) {
|
||||
expectedJSON, _ := json.Marshal(acTest.Expected)
|
||||
|
||||
if string(resultJSON) != string(expectedJSON) {
|
||||
diff := difflib.UnifiedDiff{
|
||||
A: difflib.SplitLines(string(expectedJSON)),
|
||||
B: difflib.SplitLines(string(resultJSON)),
|
||||
FromFile: "Expected",
|
||||
ToFile: "Returned",
|
||||
Context: 3,
|
||||
if diff := cmp.Diff(expectedJSON, resultJSON); diff != "" {
|
||||
t.Errorf("Wrong autocomplete data returned (-want +got):\n%s", diff)
|
||||
}
|
||||
text, err := difflib.GetUnifiedDiffString(diff)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
t.Errorf("Autocomplete mismatch:\n%s", text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,11 @@ import (
|
||||
"github.com/prymitive/karma/internal/models"
|
||||
)
|
||||
|
||||
type silenceJiraFilter struct {
|
||||
type silenceTicketFilter struct {
|
||||
alertFilter
|
||||
}
|
||||
|
||||
func (filter *silenceJiraFilter) Match(alert *models.Alert, matches int) bool {
|
||||
func (filter *silenceTicketFilter) Match(alert *models.Alert, matches int) bool {
|
||||
if filter.IsValid {
|
||||
var isMatch bool
|
||||
if alert.IsSilenced() {
|
||||
@@ -19,7 +19,7 @@ func (filter *silenceJiraFilter) Match(alert *models.Alert, matches int) bool {
|
||||
for _, am := range alert.Alertmanager {
|
||||
silence, found := am.Silences[silenceID]
|
||||
if found {
|
||||
m := filter.Matcher.Compare(silence.JiraID, filter.Value)
|
||||
m := filter.Matcher.Compare(silence.TicketID, filter.Value)
|
||||
if m {
|
||||
isMatch = m
|
||||
}
|
||||
@@ -38,26 +38,26 @@ func (filter *silenceJiraFilter) Match(alert *models.Alert, matches int) bool {
|
||||
panic(e)
|
||||
}
|
||||
|
||||
func newSilenceJiraFilter() FilterT {
|
||||
f := silenceJiraFilter{}
|
||||
func newSilenceTicketFilter() FilterT {
|
||||
f := silenceTicketFilter{}
|
||||
return &f
|
||||
}
|
||||
|
||||
func silenceJiraIDAutocomplete(name string, operators []string, alerts []models.Alert) []models.Autocomplete {
|
||||
func silenceTicketIDAutocomplete(name string, operators []string, alerts []models.Alert) []models.Autocomplete {
|
||||
tokens := map[string]models.Autocomplete{}
|
||||
for _, alert := range alerts {
|
||||
if alert.IsSilenced() {
|
||||
for _, silenceID := range alert.SilencedBy {
|
||||
for _, am := range alert.Alertmanager {
|
||||
silence, found := am.Silences[silenceID]
|
||||
if found && silence.JiraID != "" {
|
||||
if found && silence.TicketID != "" {
|
||||
for _, operator := range operators {
|
||||
token := fmt.Sprintf("%s%s%s", name, operator, silence.JiraID)
|
||||
token := fmt.Sprintf("%s%s%s", name, operator, silence.TicketID)
|
||||
tokens[token] = makeAC(token, []string{
|
||||
name,
|
||||
strings.TrimPrefix(name, "@"),
|
||||
fmt.Sprintf("%s%s", name, operator),
|
||||
silence.JiraID,
|
||||
silence.TicketID,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -146,73 +146,73 @@ var tests = []filterTest{
|
||||
},
|
||||
|
||||
{
|
||||
Expression: "@silence_jira=1",
|
||||
Expression: "@silence_ticket=1",
|
||||
IsValid: true,
|
||||
Alert: models.Alert{State: "suppressed", SilencedBy: []string{"1"}},
|
||||
Silence: models.Silence{ID: "1", JiraID: "1"},
|
||||
Silence: models.Silence{ID: "1", TicketID: "1"},
|
||||
IsMatch: true,
|
||||
},
|
||||
{
|
||||
Expression: "@silence_jira=2",
|
||||
Expression: "@silence_ticket=2",
|
||||
IsValid: true,
|
||||
Alert: models.Alert{State: "suppressed", SilencedBy: []string{"1"}},
|
||||
Silence: models.Silence{ID: "1"},
|
||||
IsMatch: false,
|
||||
},
|
||||
{
|
||||
Expression: "@silence_jira!=3",
|
||||
Expression: "@silence_ticket!=3",
|
||||
IsValid: true,
|
||||
Alert: models.Alert{State: "suppressed", SilencedBy: []string{"1"}},
|
||||
Silence: models.Silence{ID: "1", JiraID: "x"},
|
||||
Silence: models.Silence{ID: "1", TicketID: "x"},
|
||||
IsMatch: true,
|
||||
},
|
||||
{
|
||||
Expression: "@silence_jira!=4",
|
||||
Expression: "@silence_ticket!=4",
|
||||
IsValid: true,
|
||||
Alert: models.Alert{State: "suppressed", SilencedBy: []string{"1"}},
|
||||
Silence: models.Silence{ID: "1", JiraID: "4"},
|
||||
Silence: models.Silence{ID: "1", TicketID: "4"},
|
||||
IsMatch: false,
|
||||
},
|
||||
{
|
||||
Expression: "@silence_jira!=5",
|
||||
Expression: "@silence_ticket!=5",
|
||||
IsValid: true,
|
||||
Alert: models.Alert{State: "suppressed", SilencedBy: []string{"1"}},
|
||||
Silence: models.Silence{ID: "1"},
|
||||
IsMatch: true,
|
||||
},
|
||||
{
|
||||
Expression: "@silence_jira=~abc",
|
||||
Expression: "@silence_ticket=~abc",
|
||||
IsValid: true,
|
||||
Alert: models.Alert{State: "suppressed", SilencedBy: []string{"1"}},
|
||||
Silence: models.Silence{ID: "1", JiraID: "xxabcxx"},
|
||||
Silence: models.Silence{ID: "1", TicketID: "xxabcxx"},
|
||||
IsMatch: true,
|
||||
},
|
||||
{
|
||||
Expression: "@silence_jira=~abc",
|
||||
Expression: "@silence_ticket=~abc",
|
||||
IsValid: true,
|
||||
Alert: models.Alert{State: "suppressed", SilencedBy: []string{"1"}},
|
||||
Silence: models.Silence{ID: "1", JiraID: "xxx"},
|
||||
Silence: models.Silence{ID: "1", TicketID: "xxx"},
|
||||
IsMatch: false,
|
||||
},
|
||||
{
|
||||
Expression: "@silence_jira=~",
|
||||
Expression: "@silence_ticket=~",
|
||||
IsValid: false,
|
||||
Alert: models.Alert{State: "suppressed", SilencedBy: []string{"1"}},
|
||||
Silence: models.Silence{ID: "1", JiraID: "xxx"},
|
||||
Silence: models.Silence{ID: "1", TicketID: "xxx"},
|
||||
IsMatch: false,
|
||||
},
|
||||
{
|
||||
Expression: "@silence_jira~=",
|
||||
Expression: "@silence_ticket~=",
|
||||
IsValid: false,
|
||||
Alert: models.Alert{State: "suppressed", SilencedBy: []string{"1"}},
|
||||
Silence: models.Silence{ID: "1", JiraID: "xxx"},
|
||||
Silence: models.Silence{ID: "1", TicketID: "xxx"},
|
||||
IsMatch: false,
|
||||
},
|
||||
{
|
||||
Expression: "@silence_jira~=1",
|
||||
Expression: "@silence_ticket~=1",
|
||||
IsValid: false,
|
||||
Alert: models.Alert{State: "suppressed", SilencedBy: []string{"1"}},
|
||||
Silence: models.Silence{ID: "1", JiraID: "xxx"},
|
||||
Silence: models.Silence{ID: "1", TicketID: "xxx"},
|
||||
IsMatch: false,
|
||||
},
|
||||
|
||||
|
||||
@@ -76,11 +76,11 @@ var AllFilters = []filterConfig{
|
||||
Autocomplete: silenceIDAutocomplete,
|
||||
},
|
||||
{
|
||||
Label: "@silence_jira",
|
||||
LabelRe: regexp.MustCompile("^@silence_jira$"),
|
||||
Label: "@silence_ticket",
|
||||
LabelRe: regexp.MustCompile("^@silence_ticket$"),
|
||||
SupportedOperators: []string{regexpOperator, negativeRegexOperator, equalOperator, notEqualOperator},
|
||||
Factory: newSilenceJiraFilter,
|
||||
Autocomplete: silenceJiraIDAutocomplete,
|
||||
Factory: newSilenceTicketFilter,
|
||||
Autocomplete: silenceTicketIDAutocomplete,
|
||||
},
|
||||
{
|
||||
Label: "@silence_author",
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package models
|
||||
|
||||
import "regexp"
|
||||
|
||||
// JiraRule is used to detect JIRA issue IDs in strings and turn those into
|
||||
// links
|
||||
type JiraRule struct {
|
||||
Regex string
|
||||
URI string
|
||||
type LinkDetectRule struct {
|
||||
Regex *regexp.Regexp
|
||||
URITemplate string
|
||||
}
|
||||
@@ -21,8 +21,8 @@ type Silence struct {
|
||||
CreatedBy string `json:"createdBy"`
|
||||
Comment string `json:"comment"`
|
||||
// karma fields
|
||||
JiraID string `json:"jiraID"`
|
||||
JiraURL string `json:"jiraURL"`
|
||||
TicketID string `json:"ticketID"`
|
||||
TicketURL string `json:"ticketURL"`
|
||||
}
|
||||
|
||||
// ManagedSilence is a standalone silence detached from any alert
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
package transform
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"regexp"
|
||||
|
||||
"github.com/prymitive/karma/internal/models"
|
||||
)
|
||||
|
||||
type jiraDetectRule struct {
|
||||
Regexp *regexp.Regexp
|
||||
URL string
|
||||
}
|
||||
|
||||
var jiraDetectRules = []jiraDetectRule{}
|
||||
|
||||
// ParseRules will parse and validate list of JIRA detection rules provided
|
||||
// from config, valid rules will be stored for future use in DetectJIRAs() calls
|
||||
func ParseRules(rules []models.JiraRule) {
|
||||
for _, rule := range rules {
|
||||
if rule.Regex == "" || rule.URI == "" {
|
||||
log.Fatalf("Invalid JIRA rule with regexp '%s' and url '%s'", rule.Regex, rule.URI)
|
||||
}
|
||||
jdr := jiraDetectRule{
|
||||
Regexp: regexp.MustCompile(rule.Regex),
|
||||
URL: rule.URI,
|
||||
}
|
||||
jiraDetectRules = append(jiraDetectRules, jdr)
|
||||
}
|
||||
}
|
||||
|
||||
// DetectJIRAs will try to find JIRA links in Alertmanager silence objects
|
||||
// using regexp rules from configuration that were parsed and populated
|
||||
// by ParseRules call
|
||||
func DetectJIRAs(silence *models.Silence) (jiraID, jiraLink string) {
|
||||
for _, jdr := range jiraDetectRules {
|
||||
jiraID := jdr.Regexp.FindString(silence.Comment)
|
||||
if jiraID != "" {
|
||||
jiraLink := fmt.Sprintf("%s/browse/%s", jdr.URL, jiraID)
|
||||
return jiraID, jiraLink
|
||||
}
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
package transform_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/prymitive/karma/internal/models"
|
||||
"github.com/prymitive/karma/internal/transform"
|
||||
)
|
||||
|
||||
type jiraTest struct {
|
||||
silence models.Silence
|
||||
jiraID string
|
||||
jiraLink string
|
||||
}
|
||||
|
||||
var jiraRules = []models.JiraRule{
|
||||
{
|
||||
Regex: "DEVOPS-[0-9]+",
|
||||
URI: "https://jira.example.com",
|
||||
},
|
||||
{
|
||||
Regex: "PROJECT-[0-9]+",
|
||||
URI: "https://example.com",
|
||||
},
|
||||
}
|
||||
|
||||
var jiraTests = []jiraTest{
|
||||
{
|
||||
silence: models.Silence{
|
||||
Comment: "Lorem ipsum dolor sit amet",
|
||||
},
|
||||
},
|
||||
{
|
||||
silence: models.Silence{
|
||||
Comment: "DVOPS-123",
|
||||
},
|
||||
},
|
||||
{
|
||||
silence: models.Silence{
|
||||
Comment: "DEVOPS team",
|
||||
},
|
||||
},
|
||||
{
|
||||
silence: models.Silence{
|
||||
Comment: "a project-1 b",
|
||||
},
|
||||
},
|
||||
{
|
||||
silence: models.Silence{
|
||||
Comment: "a PROJECT- b",
|
||||
},
|
||||
},
|
||||
{
|
||||
silence: models.Silence{
|
||||
Comment: "DEVOPS-1",
|
||||
},
|
||||
jiraID: "DEVOPS-1",
|
||||
jiraLink: "https://jira.example.com/browse/DEVOPS-1",
|
||||
},
|
||||
{
|
||||
silence: models.Silence{
|
||||
Comment: "DEVOPS-123",
|
||||
},
|
||||
jiraID: "DEVOPS-123",
|
||||
jiraLink: "https://jira.example.com/browse/DEVOPS-123",
|
||||
},
|
||||
{
|
||||
silence: models.Silence{
|
||||
Comment: "a DEVOPS-1 b",
|
||||
},
|
||||
jiraID: "DEVOPS-1",
|
||||
jiraLink: "https://jira.example.com/browse/DEVOPS-1",
|
||||
},
|
||||
{
|
||||
silence: models.Silence{
|
||||
Comment: "PROJECT-9",
|
||||
},
|
||||
jiraID: "PROJECT-9",
|
||||
jiraLink: "https://example.com/browse/PROJECT-9",
|
||||
},
|
||||
}
|
||||
|
||||
func TestDetectJIRAs(t *testing.T) {
|
||||
transform.ParseRules(jiraRules)
|
||||
for _, testCase := range jiraTests {
|
||||
jiraID, jiraLink := transform.DetectJIRAs(&testCase.silence)
|
||||
if jiraID != testCase.jiraID {
|
||||
t.Errorf("Invalid JIRA ID detected in silence comment '%s', expected '%s', got '%s'",
|
||||
testCase.silence.Comment, testCase.jiraID, jiraID)
|
||||
}
|
||||
if jiraID != testCase.jiraID {
|
||||
t.Errorf("Invalid JIRA link detected in silence comment '%s', expected '%s', got '%s'",
|
||||
testCase.silence.Comment, testCase.jiraLink, jiraLink)
|
||||
}
|
||||
}
|
||||
}
|
||||
27
internal/transform/links.go
Normal file
27
internal/transform/links.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package transform
|
||||
|
||||
import (
|
||||
"github.com/prymitive/karma/internal/models"
|
||||
)
|
||||
|
||||
var linkDetectRules []models.LinkDetectRule
|
||||
|
||||
func SetLinkRules(rules []models.LinkDetectRule) {
|
||||
linkDetectRules = rules
|
||||
}
|
||||
|
||||
// DetectLinks will try to find all links in Alertmanager silence objects
|
||||
// using regexp rules from configuration
|
||||
func DetectLinks(silence *models.Silence) (text, uri string) {
|
||||
for _, rule := range linkDetectRules {
|
||||
m := rule.Regex.FindString(silence.Comment)
|
||||
if m != "" {
|
||||
result := []byte{}
|
||||
for _, submatches := range rule.Regex.FindAllStringSubmatchIndex(silence.Comment, -1) {
|
||||
result = rule.Regex.ExpandString(result, rule.URITemplate, silence.Comment, submatches)
|
||||
}
|
||||
return m, string(result)
|
||||
}
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
118
internal/transform/links_test.go
Normal file
118
internal/transform/links_test.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package transform_test
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/prymitive/karma/internal/config"
|
||||
"github.com/prymitive/karma/internal/models"
|
||||
"github.com/prymitive/karma/internal/transform"
|
||||
)
|
||||
|
||||
type linkTest struct {
|
||||
silence models.Silence
|
||||
text string
|
||||
uri string
|
||||
}
|
||||
|
||||
var linkRules = []config.LinkDetectRules{
|
||||
{
|
||||
Regex: "(DEVOPS-[0-9]+)",
|
||||
URITemplate: "https://jira.example.com/browse/$1",
|
||||
},
|
||||
{
|
||||
Regex: "(PROJECT-[0-9]+)",
|
||||
URITemplate: "https://example.com/browse/$1",
|
||||
},
|
||||
{
|
||||
Regex: "(redmine[0-9]+)",
|
||||
URITemplate: "https://redmine.example.com/issue/$1.php",
|
||||
},
|
||||
}
|
||||
|
||||
var linkTests = []linkTest{
|
||||
{
|
||||
silence: models.Silence{
|
||||
Comment: "Lorem ipsum dolor sit amet",
|
||||
},
|
||||
},
|
||||
{
|
||||
silence: models.Silence{
|
||||
Comment: "DVOPS-123",
|
||||
},
|
||||
},
|
||||
{
|
||||
silence: models.Silence{
|
||||
Comment: "DEVOPS team",
|
||||
},
|
||||
},
|
||||
{
|
||||
silence: models.Silence{
|
||||
Comment: "a project-1 b",
|
||||
},
|
||||
},
|
||||
{
|
||||
silence: models.Silence{
|
||||
Comment: "a PROJECT- b",
|
||||
},
|
||||
},
|
||||
{
|
||||
silence: models.Silence{
|
||||
Comment: "DEVOPS-1",
|
||||
},
|
||||
text: "DEVOPS-1",
|
||||
uri: "https://jira.example.com/browse/DEVOPS-1",
|
||||
},
|
||||
{
|
||||
silence: models.Silence{
|
||||
Comment: "DEVOPS-123",
|
||||
},
|
||||
text: "DEVOPS-123",
|
||||
uri: "https://jira.example.com/browse/DEVOPS-123",
|
||||
},
|
||||
{
|
||||
silence: models.Silence{
|
||||
Comment: "a DEVOPS-1 b",
|
||||
},
|
||||
text: "DEVOPS-1",
|
||||
uri: "https://jira.example.com/browse/DEVOPS-1",
|
||||
},
|
||||
{
|
||||
silence: models.Silence{
|
||||
Comment: "PROJECT-9",
|
||||
},
|
||||
text: "PROJECT-9",
|
||||
uri: "https://example.com/browse/PROJECT-9",
|
||||
},
|
||||
{
|
||||
silence: models.Silence{
|
||||
Comment: "redmine0",
|
||||
},
|
||||
text: "redmine0",
|
||||
uri: "https://redmine.example.com/issue/redmine0.php",
|
||||
},
|
||||
}
|
||||
|
||||
func TestDetectTickets(t *testing.T) {
|
||||
linkDetectRules := []models.LinkDetectRule{}
|
||||
for _, rule := range linkRules {
|
||||
re, err := regexp.Compile(rule.Regex)
|
||||
if err != nil {
|
||||
t.Errorf("Invalid link detect rule '%s': %s", rule.Regex, err)
|
||||
}
|
||||
linkDetectRules = append(linkDetectRules, models.LinkDetectRule{Regex: re, URITemplate: rule.URITemplate})
|
||||
}
|
||||
transform.SetLinkRules(linkDetectRules)
|
||||
|
||||
for _, testCase := range linkTests {
|
||||
text, uri := transform.DetectLinks(&testCase.silence)
|
||||
if text != testCase.text {
|
||||
t.Errorf("Invalid ticket ID detected in silence comment '%s', expected '%s', got '%s'",
|
||||
testCase.silence.Comment, testCase.text, text)
|
||||
}
|
||||
if text != testCase.text {
|
||||
t.Errorf("Invalid ticket link detected in silence comment '%s', expected '%s', got '%s'",
|
||||
testCase.silence.Comment, testCase.uri, uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user