Skip to content

Commit

Permalink
Implement regex matching for annotation keys
Browse files Browse the repository at this point in the history
  • Loading branch information
Rhys M committed Apr 27, 2023
1 parent 948facf commit fee1dd5
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 35 deletions.
24 changes: 24 additions & 0 deletions cmd/pint/tests/0136_annotation_regex_key.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
pint.ok --no-color lint rules
! stdout .
cmp stderr stderr.txt

-- stderr.txt --
level=info msg="Loading configuration file" path=.pint.hcl
-- rules/0001.yml --
- alert: Instance Is Down 1
expr: up == 0
annotations:
annotation_foo: bar
annotation_bar: bar

-- .pint.hcl --
parser {
relaxed = [".*"]
}
rule {
annotation "annotation_.*" {
required = true
value = "bar"
severity = "bug"
}
}
29 changes: 29 additions & 0 deletions cmd/pint/tests/0137_annotation_regex_key_fail.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
pint.error --no-color lint rules
! stdout .
cmp stderr stderr.txt

-- stderr.txt --
level=info msg="Loading configuration file" path=.pint.hcl
rules/0001.yml:4 Bug: annotation_.* annotation value must match "^bar$" (alerts/annotation)
4 | annotation_foo: foo

level=info msg="Problems found" Bug=1
level=fatal msg="Fatal error" error="found 1 problem(s) with severity Bug or higher"
-- rules/0001.yml --
- alert: Instance Is Down 1
expr: up == 0
annotations:
annotation_foo: foo
annotation_bar: bar

-- .pint.hcl --
parser {
relaxed = [".*"]
}
rule {
annotation "annotation_.*" {
required = true
value = "bar"
severity = "bug"
}
}
46 changes: 25 additions & 21 deletions internal/checks/alerts_annotation.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ const (
AnnotationCheckName = "alerts/annotation"
)

func NewAnnotationCheck(key string, valueRe *TemplatedRegexp, isReguired bool, severity Severity) AnnotationCheck {
return AnnotationCheck{key: key, valueRe: valueRe, isReguired: isReguired, severity: severity}
func NewAnnotationCheck(keyRe *TemplatedRegexp, valueRe *TemplatedRegexp, isReguired bool, severity Severity) AnnotationCheck {
return AnnotationCheck{keyRe: keyRe, valueRe: valueRe, isReguired: isReguired, severity: severity}
}

type AnnotationCheck struct {
key string
keyRe *TemplatedRegexp
valueRe *TemplatedRegexp
isReguired bool
severity Severity
Expand All @@ -29,9 +29,9 @@ func (c AnnotationCheck) Meta() CheckMeta {

func (c AnnotationCheck) String() string {
if c.valueRe != nil {
return fmt.Sprintf("%s(%s=~%s:%v)", AnnotationCheckName, c.key, c.valueRe.anchored, c.isReguired)
return fmt.Sprintf("%s(%s=~%s:%v)", AnnotationCheckName, c.keyRe.original, c.valueRe.anchored, c.isReguired)
}
return fmt.Sprintf("%s(%s:%v)", AnnotationCheckName, c.key, c.isReguired)
return fmt.Sprintf("%s(%s:%v)", AnnotationCheckName, c.keyRe.original, c.isReguired)
}

func (c AnnotationCheck) Reporter() string {
Expand All @@ -49,33 +49,37 @@ func (c AnnotationCheck) Check(_ context.Context, _ string, rule parser.Rule, _
Fragment: fmt.Sprintf("%s: %s", rule.AlertingRule.Alert.Key.Value, rule.AlertingRule.Alert.Value.Value),
Lines: rule.Lines(),
Reporter: c.Reporter(),
Text: fmt.Sprintf("%s annotation is required", c.key),
Text: fmt.Sprintf("%s annotation is required", c.keyRe.original),
Severity: c.severity,
})
}
return
}

val := rule.AlertingRule.Annotations.GetValue(c.key)
if val == nil {
if c.isReguired {
problems = append(problems, Problem{
Fragment: fmt.Sprintf("%s:", rule.AlertingRule.Annotations.Key.Value),
Lines: rule.AlertingRule.Annotations.Lines(),
Reporter: c.Reporter(),
Text: fmt.Sprintf("%s annotation is required", c.key),
Severity: c.severity,
})
var foundAnnotation bool

for _, annotation := range rule.AlertingRule.Annotations.Items {
if c.keyRe.MustExpand(rule).MatchString(annotation.Key.Value) {
foundAnnotation = true
if c.valueRe != nil && !c.valueRe.MustExpand(rule).MatchString(annotation.Value.Value) {
problems = append(problems, Problem{
Fragment: fmt.Sprintf("%s: %s", annotation.Key.Value, annotation.Value.Value),
Lines: annotation.Value.Position.Lines,
Reporter: c.Reporter(),
Text: fmt.Sprintf("%s annotation value must match %q", c.keyRe.original, c.valueRe.anchored),
Severity: c.severity,
})
return
}
}
return
}

if c.valueRe != nil && !c.valueRe.MustExpand(rule).MatchString(val.Value) {
if !foundAnnotation && c.isReguired {
problems = append(problems, Problem{
Fragment: fmt.Sprintf("%s: %s", c.key, val.Value),
Lines: val.Position.Lines,
Fragment: fmt.Sprintf("%s:", rule.AlertingRule.Annotations.Key.Value),
Lines: rule.AlertingRule.Annotations.Lines(),
Reporter: c.Reporter(),
Text: fmt.Sprintf("%s annotation value must match %q", c.key, c.valueRe.anchored),
Text: fmt.Sprintf("%s annotation is required", c.keyRe.original),
Severity: c.severity,
})
return
Expand Down
80 changes: 68 additions & 12 deletions internal/checks/alerts_annotation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ func TestAnnotationCheck(t *testing.T) {
description: "ignores recording rules",
content: "- record: foo\n expr: sum(foo) without(\n",
checker: func(_ *promapi.FailoverGroup) checks.RuleChecker {
return checks.NewAnnotationCheck("severity", checks.MustTemplatedRegexp("critical"), true, checks.Warning)
return checks.NewAnnotationCheck(checks.MustTemplatedRegexp("severity"), checks.MustTemplatedRegexp("critical"), true, checks.Warning)
},
prometheus: noProm,
problems: noProblems,
Expand All @@ -22,7 +22,7 @@ func TestAnnotationCheck(t *testing.T) {
description: "doesn't ignore rules with syntax errors",
content: "- alert: foo\n expr: sum(foo) without(\n",
checker: func(_ *promapi.FailoverGroup) checks.RuleChecker {
return checks.NewAnnotationCheck("severity", checks.MustTemplatedRegexp("critical"), true, checks.Warning)
return checks.NewAnnotationCheck(checks.MustTemplatedRegexp("severity"), checks.MustTemplatedRegexp("critical"), true, checks.Warning)
},
prometheus: noProm,
problems: func(uri string) []checks.Problem {
Expand All @@ -41,7 +41,7 @@ func TestAnnotationCheck(t *testing.T) {
description: "no annotations / required",
content: "- alert: foo\n expr: sum(foo)\n",
checker: func(_ *promapi.FailoverGroup) checks.RuleChecker {
return checks.NewAnnotationCheck("severity", checks.MustTemplatedRegexp("critical"), true, checks.Warning)
return checks.NewAnnotationCheck(checks.MustTemplatedRegexp("severity"), checks.MustTemplatedRegexp("critical"), true, checks.Warning)
},
prometheus: noProm,
problems: func(uri string) []checks.Problem {
Expand All @@ -60,7 +60,7 @@ func TestAnnotationCheck(t *testing.T) {
description: "no annotations / not required",
content: "- alert: foo\n expr: sum(foo)\n",
checker: func(_ *promapi.FailoverGroup) checks.RuleChecker {
return checks.NewAnnotationCheck("severity", checks.MustTemplatedRegexp("critical"), false, checks.Warning)
return checks.NewAnnotationCheck(checks.MustTemplatedRegexp("severity"), checks.MustTemplatedRegexp("critical"), false, checks.Warning)
},
prometheus: noProm,
problems: noProblems,
Expand All @@ -69,7 +69,7 @@ func TestAnnotationCheck(t *testing.T) {
description: "missing annotation / required",
content: "- alert: foo\n expr: sum(foo)\n annotations:\n foo: bar\n",
checker: func(_ *promapi.FailoverGroup) checks.RuleChecker {
return checks.NewAnnotationCheck("severity", checks.MustTemplatedRegexp("critical"), true, checks.Warning)
return checks.NewAnnotationCheck(checks.MustTemplatedRegexp("severity"), checks.MustTemplatedRegexp("critical"), true, checks.Warning)
},
prometheus: noProm,
problems: func(uri string) []checks.Problem {
Expand All @@ -88,7 +88,7 @@ func TestAnnotationCheck(t *testing.T) {
description: "missing annotation / not required",
content: "- alert: foo\n expr: sum(foo)\n annotations:\n foo: bar\n",
checker: func(_ *promapi.FailoverGroup) checks.RuleChecker {
return checks.NewAnnotationCheck("severity", checks.MustTemplatedRegexp("critical"), false, checks.Warning)
return checks.NewAnnotationCheck(checks.MustTemplatedRegexp("severity"), checks.MustTemplatedRegexp("critical"), false, checks.Warning)
},
prometheus: noProm,
problems: noProblems,
Expand All @@ -97,7 +97,7 @@ func TestAnnotationCheck(t *testing.T) {
description: "wrong annotation value / required",
content: "- alert: foo\n expr: sum(foo)\n annotations:\n severity: bar\n",
checker: func(_ *promapi.FailoverGroup) checks.RuleChecker {
return checks.NewAnnotationCheck("severity", checks.MustTemplatedRegexp("critical"), true, checks.Warning)
return checks.NewAnnotationCheck(checks.MustTemplatedRegexp("severity"), checks.MustTemplatedRegexp("critical"), true, checks.Warning)
},
prometheus: noProm,
problems: func(uri string) []checks.Problem {
Expand All @@ -116,7 +116,7 @@ func TestAnnotationCheck(t *testing.T) {
description: "wrong annotation value / not required",
content: "- alert: foo\n expr: sum(foo)\n annotations:\n severity: bar\n",
checker: func(_ *promapi.FailoverGroup) checks.RuleChecker {
return checks.NewAnnotationCheck("severity", checks.MustTemplatedRegexp("critical"), false, checks.Warning)
return checks.NewAnnotationCheck(checks.MustTemplatedRegexp("severity"), checks.MustTemplatedRegexp("critical"), false, checks.Warning)
},
prometheus: noProm,
problems: func(uri string) []checks.Problem {
Expand All @@ -135,7 +135,7 @@ func TestAnnotationCheck(t *testing.T) {
description: "valid annotation / required",
content: "- alert: foo\n expr: sum(foo)\n annotations:\n severity: info\n",
checker: func(_ *promapi.FailoverGroup) checks.RuleChecker {
return checks.NewAnnotationCheck("severity", checks.MustTemplatedRegexp("critical|info|debug"), true, checks.Warning)
return checks.NewAnnotationCheck(checks.MustTemplatedRegexp("severity"), checks.MustTemplatedRegexp("critical|info|debug"), true, checks.Warning)
},
prometheus: noProm,
problems: noProblems,
Expand All @@ -144,7 +144,7 @@ func TestAnnotationCheck(t *testing.T) {
description: "valid annotation / not required",
content: "- alert: foo\n expr: sum(foo)\n annotations:\n severity: info\n",
checker: func(_ *promapi.FailoverGroup) checks.RuleChecker {
return checks.NewAnnotationCheck("severity", checks.MustTemplatedRegexp("critical|info|debug"), false, checks.Warning)
return checks.NewAnnotationCheck(checks.MustTemplatedRegexp("severity"), checks.MustTemplatedRegexp("critical|info|debug"), false, checks.Warning)
},
prometheus: noProm,
problems: noProblems,
Expand All @@ -153,7 +153,7 @@ func TestAnnotationCheck(t *testing.T) {
description: "templated annotation value / passing",
content: "- alert: foo\n expr: sum(foo)\n for: 5m\n annotations:\n for: 5m\n",
checker: func(_ *promapi.FailoverGroup) checks.RuleChecker {
return checks.NewAnnotationCheck("for", checks.MustTemplatedRegexp("{{ $for }}"), true, checks.Bug)
return checks.NewAnnotationCheck(checks.MustTemplatedRegexp("for"), checks.MustTemplatedRegexp("{{ $for }}"), true, checks.Bug)
},
prometheus: noProm,
problems: noProblems,
Expand All @@ -162,7 +162,7 @@ func TestAnnotationCheck(t *testing.T) {
description: "templated annotation value / passing",
content: "- alert: foo\n expr: sum(foo)\n for: 5m\n annotations:\n for: 4m\n",
checker: func(_ *promapi.FailoverGroup) checks.RuleChecker {
return checks.NewAnnotationCheck("for", checks.MustTemplatedRegexp("{{ $for }}"), true, checks.Bug)
return checks.NewAnnotationCheck(checks.MustTemplatedRegexp("for"), checks.MustTemplatedRegexp("{{ $for }}"), true, checks.Bug)
},
prometheus: noProm,
problems: func(uri string) []checks.Problem {
Expand All @@ -177,6 +177,62 @@ func TestAnnotationCheck(t *testing.T) {
}
},
},
{
description: "valid annotation key regex / required",
content: "- alert: foo\n expr: sum(foo)\n annotations:\n annotation_1: info\n",
checker: func(_ *promapi.FailoverGroup) checks.RuleChecker {
return checks.NewAnnotationCheck(checks.MustTemplatedRegexp("annotation_.*"), checks.MustTemplatedRegexp("critical|info|debug"), true, checks.Warning)
},
prometheus: noProm,
problems: noProblems,
},
{
description: "valid annotation key regex / not required",
content: "- alert: foo\n expr: sum(foo)\n annotations:\n annotation_1: info\n",
checker: func(_ *promapi.FailoverGroup) checks.RuleChecker {
return checks.NewAnnotationCheck(checks.MustTemplatedRegexp("annotation_.*"), checks.MustTemplatedRegexp("critical|info|debug"), false, checks.Warning)
},
prometheus: noProm,
problems: noProblems,
},
{
description: "wrong annotation key regex value / required",
content: "- alert: foo\n expr: sum(foo)\n annotations:\n annotation_1: bar\n",
checker: func(_ *promapi.FailoverGroup) checks.RuleChecker {
return checks.NewAnnotationCheck(checks.MustTemplatedRegexp("annotation_.*"), checks.MustTemplatedRegexp("critical"), true, checks.Warning)
},
prometheus: noProm,
problems: func(uri string) []checks.Problem {
return []checks.Problem{
{
Fragment: "annotation_1: bar",
Lines: []int{4},
Reporter: checks.AnnotationCheckName,
Text: `annotation_.* annotation value must match "^critical$"`,
Severity: checks.Warning,
},
}
},
},
{
description: "wrong annotation key regex value / not required",
content: "- alert: foo\n expr: sum(foo)\n annotations:\n annotation_1: bar\n",
checker: func(_ *promapi.FailoverGroup) checks.RuleChecker {
return checks.NewAnnotationCheck(checks.MustTemplatedRegexp("annotation_.*"), checks.MustTemplatedRegexp("critical"), false, checks.Warning)
},
prometheus: noProm,
problems: func(uri string) []checks.Problem {
return []checks.Problem{
{
Fragment: "annotation_1: bar",
Lines: []int{4},
Reporter: checks.AnnotationCheckName,
Text: `annotation_.* annotation value must match "^critical$"`,
Severity: checks.Warning,
},
}
},
},
}
runTests(t, testCases)
}
3 changes: 2 additions & 1 deletion internal/checks/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
)

func NewTemplatedRegexp(s string) (*TemplatedRegexp, error) {
tr := TemplatedRegexp{anchored: "^" + s + "$"}
tr := TemplatedRegexp{anchored: "^" + s + "$", original: s}
_, err := tr.Expand(parser.Rule{})
if err != nil {
return nil, err
Expand All @@ -26,6 +26,7 @@ func MustTemplatedRegexp(re string) *TemplatedRegexp {

type TemplatedRegexp struct {
anchored string
original string
}

func (tr TemplatedRegexp) Expand(rule parser.Rule) (*regexp.Regexp, error) {
Expand Down
2 changes: 1 addition & 1 deletion internal/config/rule.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ func (rule Rule) resolveChecks(ctx context.Context, path string, r parser.Rule,
severity := ann.getSeverity(checks.Warning)
enabled = append(enabled, checkMeta{
name: checks.AnnotationCheckName,
check: checks.NewAnnotationCheck(ann.Key, valueRegex, ann.Required, severity),
check: checks.NewAnnotationCheck(checks.MustTemplatedRegexp(ann.Key), valueRegex, ann.Required, severity),
})
}
}
Expand Down

0 comments on commit fee1dd5

Please sign in to comment.