Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a default check to ensure label values are strings #839

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
/dist/
/.vscode/
/cmd/pint/bench/rules
/.idea
34 changes: 34 additions & 0 deletions cmd/pint/tests/0166_label_value_not_a_string.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
pint.error --no-color lint rules
! stdout .
cmp stderr stderr.txt

-- stderr.txt --
level=INFO msg="Finding all rules to check" paths=["rules"]
rules/1.yaml:7 Bug: alerting rule `colo:test1:alert` has label `job` with non-string value, got `!!bool`. (rule/label/value_type)
7 | job: true

rules/1.yaml:11 Bug: recording rule `colo:test1` has label `job` with non-string value, got `!!bool`. (rule/label/value_type)
11 | job: true

level=INFO msg="Problems found" Bug=2
level=ERROR msg="Fatal error" err="found 1 problem(s) with severity Bug or higher"
-- rules/1.yaml --
groups:
- name: foo
rules:
- alert: "colo:test1:alert"
expr: sum(foo) > 0
labels:
job: true
- record: "colo:test1"
expr: sum(foo)
labels:
job: true
- record: "colo:test2"
expr: sum(foo)
labels:
job: "true"
- alert: "colo:test2:alert"
expr: sum(foo) > 0
labels:
job: "true"
1 change: 1 addition & 0 deletions internal/checks/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ var (
RuleDuplicateCheckName,
RuleForCheckName,
LabelCheckName,
RuleLabelValueTypeName,
RuleLinkCheckName,
RejectCheckName,
}
Expand Down
82 changes: 82 additions & 0 deletions internal/checks/rule_label_value_type.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package checks

import (
"context"
"fmt"

"github.com/cloudflare/pint/internal/discovery"
"github.com/cloudflare/pint/internal/parser"
)

const (
RuleLabelValueTypeName = "rule/label/value_type"
)

func NewRuleLabelValueTypeCheck() RuleLabelValueTypeCheck {
return RuleLabelValueTypeCheck{}
}

type RuleLabelValueTypeCheck struct{}

func (c RuleLabelValueTypeCheck) Meta() CheckMeta {
return CheckMeta{
States: []discovery.ChangeType{
discovery.Noop,
discovery.Added,
discovery.Modified,
discovery.Moved,
},
IsOnline: false,
}
}

func (c RuleLabelValueTypeCheck) String() string {
return RuleLabelValueTypeName
}

func (c RuleLabelValueTypeCheck) Reporter() string {
return RuleLabelValueTypeName
}

func (c RuleLabelValueTypeCheck) Check(ctx context.Context, _ string, rule parser.Rule, _ []discovery.Entry) (problems []Problem) {
if rule.RecordingRule != nil {
problems = append(problems, c.checkRecordingRule(rule)...)
}

if rule.AlertingRule != nil {
problems = append(problems, c.checkAlertingRule(rule)...)
}

return problems
}

func (c RuleLabelValueTypeCheck) checkRecordingRule(rule parser.Rule) (problems []Problem) {
if rule.RecordingRule.Labels == nil {
return problems
}
problems = append(problems, c.checkRuleLabelsValueType(rule.Name(), "recording", rule.RecordingRule.Labels, problems)...)
return problems
}

func (c RuleLabelValueTypeCheck) checkAlertingRule(rule parser.Rule) (problems []Problem) {
if rule.AlertingRule.Labels == nil {
return problems
}
problems = append(problems, c.checkRuleLabelsValueType(rule.Name(), "alerting", rule.AlertingRule.Labels, problems)...)
return problems
}

func (c RuleLabelValueTypeCheck) checkRuleLabelsValueType(ruleName, ruleType string, labels *parser.YamlMap, problems []Problem) []Problem {
for _, label := range labels.Items {
if label.Value.Tag != "!!str" {
problems = append(problems, Problem{
Lines: label.Value.Lines,
Reporter: c.Reporter(),
Text: fmt.Sprintf("%s rule `%s` has label `%s` with non-string value, got `%s`.", ruleType, ruleName, label.Key.Value, label.Value.Tag),
Severity: Bug,
})
}

}
return problems
}
86 changes: 86 additions & 0 deletions internal/checks/rule_label_value_type_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package checks_test

import (
"testing"

"github.com/cloudflare/pint/internal/checks"
"github.com/cloudflare/pint/internal/parser"
"github.com/cloudflare/pint/internal/promapi"
)

func TestRuleLabelValueTypeCheck(t *testing.T) {
testCases := []checkTest{
{
description: "label is not a string in recording rule / required",
content: "- record: foo\n expr: rate(foo[1m])\n labels:\n foo: true\n bar: 1\n",
checker: func(_ *promapi.FailoverGroup) checks.RuleChecker {
return checks.NewRuleLabelValueTypeCheck()
},
prometheus: noProm,
problems: func(uri string) []checks.Problem {
return []checks.Problem{
{
Lines: parser.LineRange{
First: 4,
Last: 4,
},
Reporter: checks.RuleLabelValueTypeName,
Text: "recording rule `foo` has label `foo` with non-string value, got `!!bool`.",
Severity: checks.Bug,
},
{
Lines: parser.LineRange{
First: 5,
Last: 5,
},
Reporter: checks.RuleLabelValueTypeName,
Text: "recording rule `foo` has label `bar` with non-string value, got `!!int`.",
Severity: checks.Bug,
},
}
},
},
{
description: "label is not a string in alerting rule / required",
content: "- alert: foo\n expr: rate(foo[1m]) > 0\n labels:\n foo: true\n",
checker: func(_ *promapi.FailoverGroup) checks.RuleChecker {
return checks.NewRuleLabelValueTypeCheck()
},
prometheus: noProm,
problems: func(uri string) []checks.Problem {
return []checks.Problem{{
Lines: parser.LineRange{
First: 4,
Last: 4,
},
Reporter: checks.RuleLabelValueTypeName,
Text: "alerting rule `foo` has label `foo` with non-string value, got `!!bool`.",
Severity: checks.Bug,
}}
},
},
{
description: "label is a string in recording rule / required",
content: "- record: foo\n expr: rate(foo[1m]) > 0\n labels:\n foo: \"true\"\n",
checker: func(_ *promapi.FailoverGroup) checks.RuleChecker {
return checks.NewRuleLabelValueTypeCheck()
},
prometheus: noProm,
problems: func(uri string) []checks.Problem {
return nil
},
},
{
description: "label is a string in alerting rule / required",
content: "- alert: foo\n expr: rate(foo[1m]) > 0\n labels:\n foo: \"true\"\n",
checker: func(_ *promapi.FailoverGroup) checks.RuleChecker {
return checks.NewRuleLabelValueTypeCheck()
},
prometheus: noProm,
problems: func(uri string) []checks.Problem {
return nil
},
},
}
runTests(t, testCases)
}
4 changes: 4 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ func (cfg *Config) GetChecksForRule(ctx context.Context, gen *PrometheusGenerato
name: checks.RuleDependencyCheckName,
check: checks.NewRuleDependencyCheck(),
},
{
name: checks.RuleLabelValueTypeName,
check: checks.NewRuleLabelValueTypeCheck(),
},
}

proms := gen.ServersForPath(entry.SourcePath)
Expand Down
2 changes: 2 additions & 0 deletions internal/parser/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ func mergeComments(node *yaml.Node) (comments []string) {

type YamlNode struct {
Value string
Tag string
Lines LineRange
}

Expand All @@ -64,6 +65,7 @@ func newYamlNode(node *yaml.Node, offset int) *YamlNode {
n := YamlNode{
Lines: nodeLines(node, offset),
Value: node.Value,
Tag: node.ShortTag(),
}
if node.Alias != nil {
n.Value = node.Alias.Value
Expand Down