diff --git a/cmd/pint/bench_test.go b/cmd/pint/bench_test.go index 0077ac82..7a86504b 100644 --- a/cmd/pint/bench_test.go +++ b/cmd/pint/bench_test.go @@ -16,6 +16,7 @@ import ( "github.com/cloudflare/pint/internal/discovery" "github.com/cloudflare/pint/internal/git" "github.com/cloudflare/pint/internal/log" + "github.com/cloudflare/pint/internal/parser" ) func BenchmarkFindEntries(b *testing.B) { @@ -24,6 +25,7 @@ func BenchmarkFindEntries(b *testing.B) { finder := discovery.NewGlobFinder( []string{"bench/rules"}, git.NewPathFilter(nil, nil, nil), + parser.PrometheusSchema, ) for n := 0; n < b.N; n++ { _, _ = finder.Find() @@ -36,6 +38,7 @@ func BenchmarkCheckRules(b *testing.B) { finder := discovery.NewGlobFinder( []string{"bench/rules"}, git.NewPathFilter(nil, nil, nil), + parser.PrometheusSchema, ) entries, err := finder.Find() if err != nil { diff --git a/cmd/pint/ci.go b/cmd/pint/ci.go index c0a41b4e..c5e62d62 100644 --- a/cmd/pint/ci.go +++ b/cmd/pint/ci.go @@ -14,6 +14,7 @@ import ( "github.com/cloudflare/pint/internal/config" "github.com/cloudflare/pint/internal/discovery" "github.com/cloudflare/pint/internal/git" + "github.com/cloudflare/pint/internal/parser" "github.com/cloudflare/pint/internal/reporter" "github.com/urfave/cli/v2" @@ -100,13 +101,14 @@ func actionCI(c *cli.Context) error { config.MustCompileRegexes(meta.cfg.Parser.Exclude...), config.MustCompileRegexes(meta.cfg.Parser.Relaxed...), ) + schema := parseSchema(meta.cfg.Parser.Schema) - entries, err = discovery.NewGlobFinder([]string{"*"}, filter).Find() + entries, err = discovery.NewGlobFinder([]string{"*"}, filter, schema).Find() if err != nil { return err } - entries, err = discovery.NewGitBranchFinder(git.RunGit, filter, baseBranch, meta.cfg.CI.MaxCommits).Find(entries) + entries, err = discovery.NewGitBranchFinder(git.RunGit, filter, baseBranch, meta.cfg.CI.MaxCommits, schema).Find(entries) if err != nil { return err } @@ -374,3 +376,10 @@ func detectGithubActions(gh *config.GitHub) *config.GitHub { } return gh } + +func parseSchema(s string) parser.Schema { + if s == config.SchemaThanos { + return parser.ThanosSchema + } + return parser.PrometheusSchema +} diff --git a/cmd/pint/lint.go b/cmd/pint/lint.go index be1dead2..d4dd658c 100644 --- a/cmd/pint/lint.go +++ b/cmd/pint/lint.go @@ -75,11 +75,14 @@ func actionLint(c *cli.Context) error { } slog.Info("Finding all rules to check", slog.Any("paths", paths)) - finder := discovery.NewGlobFinder(paths, git.NewPathFilter( - config.MustCompileRegexes(meta.cfg.Parser.Include...), - config.MustCompileRegexes(meta.cfg.Parser.Exclude...), - config.MustCompileRegexes(meta.cfg.Parser.Relaxed...), - )) + finder := discovery.NewGlobFinder( + paths, + git.NewPathFilter( + config.MustCompileRegexes(meta.cfg.Parser.Include...), + config.MustCompileRegexes(meta.cfg.Parser.Exclude...), + config.MustCompileRegexes(meta.cfg.Parser.Relaxed...), + ), + parseSchema(meta.cfg.Parser.Schema)) entries, err := finder.Find() if err != nil { return err diff --git a/cmd/pint/tests/0203_parser_schema_prom_err.txt b/cmd/pint/tests/0203_parser_schema_prom_err.txt new file mode 100644 index 00000000..ec6829cf --- /dev/null +++ b/cmd/pint/tests/0203_parser_schema_prom_err.txt @@ -0,0 +1,27 @@ +! exec pint --no-color lint rules +! stdout . +cmp stderr stderr.txt + +-- stderr.txt -- +level=INFO msg="Loading configuration file" path=.pint.hcl +level=INFO msg="Finding all rules to check" paths=["rules"] +level=WARN msg="Failed to parse file content" err="error at line 7: partial_response_strategy is only valid when parser is configured to use the Thanos rule schema" path=rules/1.yml lines=1-9 +rules/1.yml:7 Fatal: partial_response_strategy is only valid when parser is configured to use the Thanos rule schema (yaml/parse) + 7 | partial_response_strategy: warn + +level=INFO msg="Problems found" Fatal=1 +level=ERROR msg="Fatal error" err="found 1 problem(s) with severity Bug or higher" +-- rules/1.yml -- +groups: +- name: foo + rules: + - alert: foo + expr: up == 0 + - record: bar + partial_response_strategy: warn + expr: sum(up) + +-- .pint.hcl -- +parser { + schema = "prometheus" +} \ No newline at end of file diff --git a/cmd/pint/tests/0204_parser_schema_thanos_err.txt b/cmd/pint/tests/0204_parser_schema_thanos_err.txt new file mode 100644 index 00000000..2e72c9b0 --- /dev/null +++ b/cmd/pint/tests/0204_parser_schema_thanos_err.txt @@ -0,0 +1,26 @@ +! exec pint --no-color lint rules +! stdout . +cmp stderr stderr.txt + +-- stderr.txt -- +level=INFO msg="Loading configuration file" path=.pint.hcl +level=INFO msg="Finding all rules to check" paths=["rules"] +rules/1.yml:7 Fatal: This rule is not a valid Prometheus rule: `invalid partial_response_strategy value: bob`. (yaml/parse) + 7 | partial_response_strategy: bob + +level=INFO msg="Problems found" Fatal=1 +level=ERROR msg="Fatal error" err="found 1 problem(s) with severity Bug or higher" +-- rules/1.yml -- +groups: +- name: foo + rules: + - alert: foo + expr: up == 0 + - record: bar + partial_response_strategy: bob + expr: sum(up) + +-- .pint.hcl -- +parser { + schema = "thanos" +} diff --git a/cmd/pint/tests/0205_parser_schema_thanos_ok.txt b/cmd/pint/tests/0205_parser_schema_thanos_ok.txt new file mode 100644 index 00000000..0cb6b807 --- /dev/null +++ b/cmd/pint/tests/0205_parser_schema_thanos_ok.txt @@ -0,0 +1,22 @@ +exec pint --no-color lint rules +! stdout . +cmp stderr stderr.txt + +-- stderr.txt -- +level=INFO msg="Loading configuration file" path=.pint.hcl +level=INFO msg="Finding all rules to check" paths=["rules"] +-- rules/1.yml -- +groups: +- name: foo + rules: + - alert: foo + partial_response_strategy: warn + expr: up == 0 + - record: bar + partial_response_strategy: abort + expr: sum(up) + +-- .pint.hcl -- +parser { + schema = "thanos" +} diff --git a/cmd/pint/tests/0206_parser_schema_err.txt b/cmd/pint/tests/0206_parser_schema_err.txt new file mode 100644 index 00000000..6079fdc0 --- /dev/null +++ b/cmd/pint/tests/0206_parser_schema_err.txt @@ -0,0 +1,21 @@ +! exec pint --no-color lint rules +! stdout . +cmp stderr stderr.txt + +-- stderr.txt -- +level=INFO msg="Loading configuration file" path=.pint.hcl +level=ERROR msg="Fatal error" err="failed to load config file \".pint.hcl\": unsupported parser scheme: bogus" +-- rules/1.yml -- +groups: +- name: foo + rules: + - alert: foo + expr: up == 0 + - record: bar + partial_response_strategy: bob + expr: sum(up) + +-- .pint.hcl -- +parser { + schema = "bogus" +} diff --git a/cmd/pint/watch.go b/cmd/pint/watch.go index 174365a8..3735b64d 100644 --- a/cmd/pint/watch.go +++ b/cmd/pint/watch.go @@ -19,6 +19,7 @@ import ( "github.com/cloudflare/pint/internal/config" "github.com/cloudflare/pint/internal/discovery" "github.com/cloudflare/pint/internal/git" + "github.com/cloudflare/pint/internal/parser" "github.com/cloudflare/pint/internal/promapi" "github.com/cloudflare/pint/internal/reporter" @@ -202,10 +203,12 @@ func actionWatch(c *cli.Context, meta actionMeta, f pathFinderFunc) error { return err } + schema := parseSchema(meta.cfg.Parser.Schema) + // start timer to run every $interval ack := make(chan bool, 1) mainCtx, mainCancel := context.WithCancel(context.WithValue(context.Background(), config.CommandKey, config.WatchCommand)) - stop := startTimer(mainCtx, meta.workers, meta.isOffline, gen, interval, ack, collector) + stop := startTimer(mainCtx, meta.workers, meta.isOffline, gen, schema, interval, ack, collector) quit := make(chan os.Signal, 1) signal.Notify(quit, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) @@ -229,7 +232,7 @@ func actionWatch(c *cli.Context, meta actionMeta, f pathFinderFunc) error { return nil } -func startTimer(ctx context.Context, workers int, isOffline bool, gen *config.PrometheusGenerator, interval time.Duration, ack chan bool, collector *problemCollector) chan bool { +func startTimer(ctx context.Context, workers int, isOffline bool, gen *config.PrometheusGenerator, schema parser.Schema, interval time.Duration, ack chan bool, collector *problemCollector) chan bool { ticker := time.NewTicker(time.Second) stop := make(chan bool, 1) wasBootstrapped := false @@ -243,7 +246,7 @@ func startTimer(ctx context.Context, workers int, isOffline bool, gen *config.Pr ticker.Reset(interval) wasBootstrapped = true } - if err := collector.scan(ctx, workers, isOffline, gen); err != nil { + if err := collector.scan(ctx, workers, isOffline, gen, schema); err != nil { slog.Error("Got an error when running checks", slog.Any("err", err)) } checkIterationsTotal.Inc() @@ -301,18 +304,22 @@ func newProblemCollector(cfg config.Config, f pathFinderFunc, minSeverity checks } } -func (c *problemCollector) scan(ctx context.Context, workers int, isOffline bool, gen *config.PrometheusGenerator) error { +func (c *problemCollector) scan(ctx context.Context, workers int, isOffline bool, gen *config.PrometheusGenerator, schema parser.Schema) error { paths, err := c.finder(ctx) if err != nil { return fmt.Errorf("failed to get the list of paths to check: %w", err) } slog.Info("Finding all rules to check", slog.Any("paths", paths)) - entries, err := discovery.NewGlobFinder(paths, git.NewPathFilter( - config.MustCompileRegexes(c.cfg.Parser.Include...), - config.MustCompileRegexes(c.cfg.Parser.Exclude...), - config.MustCompileRegexes(c.cfg.Parser.Relaxed...), - )).Find() + entries, err := discovery.NewGlobFinder( + paths, + git.NewPathFilter( + config.MustCompileRegexes(c.cfg.Parser.Include...), + config.MustCompileRegexes(c.cfg.Parser.Exclude...), + config.MustCompileRegexes(c.cfg.Parser.Relaxed...), + ), + schema, + ).Find() if err != nil { return err } diff --git a/docs/configuration.md b/docs/configuration.md index a25cdf3c..dcd65fee 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -64,12 +64,21 @@ Syntax: ```js parser { + schema = "prometheus|thanos" include = [ "(.*)", ... ] exclude = [ "(.*)", ... ] relaxed = [ "(.*)", ... ] } ``` +- `schema` - rule file schema to use, valid values are `prometheus` and `thanos`. + Setting it to `prometheus` means that pint will assume that all rules have the schema + as defined in [alerting rules](https://prometheus.io/docs/prometheus/latest/configuration/alerting_rules/) + and [recording rules](https://prometheus.io/docs/prometheus/latest/configuration/recording_rules/) + Prometheus docs. + Setting it to `thanos` will tell pint to use the schema as defined + in [Thanos Rule](https://thanos.io/tip/components/rule.md/) docs. + Default value is `prometheus`. - `include` - list of file patterns to check when running checks. Only files matching those regexp rules will be checked, other modified files will be ignored. - `exclude` - list of file patterns to ignore when running checks. diff --git a/internal/checks/base_test.go b/internal/checks/base_test.go index 9008abe1..d77b882a 100644 --- a/internal/checks/base_test.go +++ b/internal/checks/base_test.go @@ -187,7 +187,7 @@ func runTests(t *testing.T, testCases []checkTest) { } func parseContent(content string) (entries []discovery.Entry, err error) { - p := parser.NewParser(false) + p := parser.NewParser(false, parser.PrometheusSchema) rules, err := p.Parse([]byte(content)) if err != nil { return nil, err diff --git a/internal/checks/template_test.go b/internal/checks/template_test.go index e7d05b68..7a582461 100644 --- a/internal/checks/template_test.go +++ b/internal/checks/template_test.go @@ -105,7 +105,7 @@ func TestTemplatedRegexpExpand(t *testing.T) { } func newMustRule(content string) parser.Rule { - p := parser.NewParser(false) + p := parser.NewParser(false, parser.PrometheusSchema) rules, err := p.Parse([]byte(content)) if err != nil { panic(err) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 03f3b48e..3ec48214 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -136,7 +136,7 @@ func TestSetDisabledChecks(t *testing.T) { } func newRule(t *testing.T, content string) parser.Rule { - p := parser.NewParser(false) + p := parser.NewParser(false, parser.PrometheusSchema) rules, err := p.Parse([]byte(content)) if err != nil { t.Error(err) diff --git a/internal/config/parser.go b/internal/config/parser.go index d8ce70e6..5cb31453 100644 --- a/internal/config/parser.go +++ b/internal/config/parser.go @@ -1,16 +1,37 @@ package config import ( + "fmt" "regexp" ) +const ( + SchemaPrometheus = "prometheus" + SchemaThanos = "thanos" +) + type Parser struct { + Schema string `hcl:"schema,optional" json:"schema,omitempty"` Relaxed []string `hcl:"relaxed,optional" json:"relaxed,omitempty"` Include []string `hcl:"include,optional" json:"include,omitempty"` Exclude []string `hcl:"exclude,optional" json:"exclude,omitempty"` } +func (p Parser) getSchema() string { + if p.Schema == "" { + return SchemaPrometheus + } + return p.Schema +} + func (p Parser) validate() error { + switch s := p.getSchema(); s { + case SchemaPrometheus: + case SchemaThanos: + default: + return fmt.Errorf("unsupported parser scheme: %s", s) + } + for _, pattern := range p.Relaxed { _, err := regexp.Compile(pattern) if err != nil { diff --git a/internal/config/parser_test.go b/internal/config/parser_test.go index b3cce899..785e3d9e 100644 --- a/internal/config/parser_test.go +++ b/internal/config/parser_test.go @@ -41,6 +41,22 @@ func TestParserSettings(t *testing.T) { }, err: errors.New("error parsing regexp: invalid nested repetition operator: `++`"), }, + { + conf: Parser{ + Schema: SchemaPrometheus, + }, + }, + { + conf: Parser{ + Schema: SchemaThanos, + }, + }, + { + conf: Parser{ + Schema: "xxx", + }, + err: errors.New("unsupported parser scheme: xxx"), + }, } for _, tc := range testCases { diff --git a/internal/discovery/discovery.go b/internal/discovery/discovery.go index 9dad4676..7cba8f0d 100644 --- a/internal/discovery/discovery.go +++ b/internal/discovery/discovery.go @@ -85,7 +85,7 @@ type Entry struct { State ChangeType } -func readRules(reportedPath, sourcePath string, r io.Reader, isStrict bool) (entries []Entry, err error) { +func readRules(reportedPath, sourcePath string, r io.Reader, isStrict bool, schema parser.Schema) (entries []Entry, err error) { content, fileComments, err := parser.ReadContent(r) if err != nil { return nil, err @@ -153,7 +153,7 @@ func readRules(reportedPath, sourcePath string, r io.Reader, isStrict bool) (ent return entries, nil } - p := parser.NewParser(isStrict) + p := parser.NewParser(isStrict, schema) rules, err := p.Parse(content.Body) if err != nil { slog.Warn( diff --git a/internal/discovery/discovery_test.go b/internal/discovery/discovery_test.go index cedc17aa..9224f910 100644 --- a/internal/discovery/discovery_test.go +++ b/internal/discovery/discovery_test.go @@ -24,7 +24,7 @@ func (r failingReader) Read(_ []byte) (int, error) { func TestReadRules(t *testing.T) { mustParse := func(offset int, s string) parser.Rule { - p := parser.NewParser(false) + p := parser.NewParser(false, parser.PrometheusSchema) r, err := p.Parse([]byte(strings.Repeat("\n", offset) + s)) if err != nil { panic(fmt.Sprintf("failed to parse rule:\n---\n%s\n---\nerror: %s", s, err)) @@ -338,7 +338,7 @@ groups: fmt.Sprintf("rPath=%s sPath=%s strict=%v title=%s", tc.reportedPath, tc.sourcePath, tc.isStrict, tc.title), func(t *testing.T) { r := tc.sourceFunc(t) - entries, err := readRules(tc.reportedPath, tc.sourcePath, r, tc.isStrict) + entries, err := readRules(tc.reportedPath, tc.sourcePath, r, tc.isStrict, parser.PrometheusSchema) if tc.err != "" { require.EqualError(t, err, tc.err) } else { diff --git a/internal/discovery/git_branch.go b/internal/discovery/git_branch.go index 7ebf801d..9bc6c916 100644 --- a/internal/discovery/git_branch.go +++ b/internal/discovery/git_branch.go @@ -18,12 +18,14 @@ func NewGitBranchFinder( filter git.PathFilter, baseBranch string, maxCommits int, + schema parser.Schema, ) GitBranchFinder { return GitBranchFinder{ gitCmd: gitCmd, filter: filter, baseBranch: baseBranch, maxCommits: maxCommits, + schema: schema, } } @@ -32,6 +34,7 @@ type GitBranchFinder struct { baseBranch string filter git.PathFilter maxCommits int + schema parser.Schema } func (f GitBranchFinder) Find(allEntries []Entry) (entries []Entry, err error) { @@ -59,6 +62,7 @@ func (f GitBranchFinder) Find(allEntries []Entry) (entries []Entry, err error) { change.Path.Before.Name, bytes.NewReader(change.Body.Before), !f.filter.IsRelaxed(change.Path.Before.Name), + f.schema, ) if err != nil { slog.Debug("Cannot read before rules", slog.String("path", change.Path.Before.Name), slog.Any("err", err)) @@ -68,6 +72,7 @@ func (f GitBranchFinder) Find(allEntries []Entry) (entries []Entry, err error) { change.Path.After.Name, bytes.NewReader(change.Body.After), !f.filter.IsRelaxed(change.Path.After.Name), + f.schema, ) if err != nil { return nil, fmt.Errorf("invalid file syntax: %w", err) diff --git a/internal/discovery/git_branch_test.go b/internal/discovery/git_branch_test.go index c1513919..8a9d000e 100644 --- a/internal/discovery/git_branch_test.go +++ b/internal/discovery/git_branch_test.go @@ -38,7 +38,7 @@ func TestGitBranchFinder(t *testing.T) { includeAll := []*regexp.Regexp{regexp.MustCompile(".*")} mustParse := func(offset int, s string) parser.Rule { - p := parser.NewParser(false) + p := parser.NewParser(false, parser.PrometheusSchema) r, err := p.Parse([]byte(strings.Repeat("\n", offset) + s)) if err != nil { panic(fmt.Sprintf("failed to parse rule:\n---\n%s\n---\nerror: %s", s, err)) @@ -70,6 +70,7 @@ func TestGitBranchFinder(t *testing.T) { git.NewPathFilter(includeAll, nil, nil), "main", 50, + parser.PrometheusSchema, ), entries: nil, err: "failed to get the list of modified files from git: mock git error: [log --reverse --no-merges --first-parent --format=%H --name-status main..HEAD]", @@ -84,6 +85,7 @@ func TestGitBranchFinder(t *testing.T) { git.NewPathFilter(includeAll, nil, nil), "master", 50, + parser.PrometheusSchema, ), entries: nil, err: "failed to get the list of modified files from git: mock git error: [log --reverse --no-merges --first-parent --format=%H --name-status master..HEAD]", @@ -101,7 +103,7 @@ func TestGitBranchFinder(t *testing.T) { commitFile(t, "rules.yml", "# v2-3\n", "v2-3") commitFile(t, "rules.yml", "# v2-4\n", "v2-4") }, - finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, nil), "main", 3), + finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, nil), "main", 3, parser.PrometheusSchema), entries: nil, err: "number of commits to check (4) is higher than maxCommits (3), exiting", }, @@ -120,6 +122,7 @@ func TestGitBranchFinder(t *testing.T) { git.NewPathFilter(includeAll, nil, nil), "main", 4, + parser.PrometheusSchema, ), entries: nil, err: "failed to get the list of modified files from git: mock git error: [log --reverse --no-merges --first-parent --format=%H --name-status main..HEAD]", @@ -139,6 +142,7 @@ func TestGitBranchFinder(t *testing.T) { git.NewPathFilter(includeAll, nil, nil), "main", 4, + parser.PrometheusSchema, ), entries: nil, err: "failed to get commit message for c1: mock git error: [show -s --format=%B c1]", @@ -164,6 +168,7 @@ func TestGitBranchFinder(t *testing.T) { git.NewPathFilter(includeAll, nil, nil), "main", 4, + parser.PrometheusSchema, ), entries: nil, err: "failed to run git blame for rules.yml: mock git error: [blame --line-porcelain c1 -- rules.yml]", @@ -178,7 +183,7 @@ func TestGitBranchFinder(t *testing.T) { commitFile(t, "rules.yml", "# v2\n", "v2") }, - finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, nil), "main", 4), + finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, nil), "main", 4, parser.PrometheusSchema), entries: nil, }, { @@ -203,7 +208,7 @@ groups: expr: count(up == 1) `, "v2") }, - finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, nil), "main", 4), + finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, nil), "main", 4, parser.PrometheusSchema), entries: []discovery.Entry{ { State: discovery.Noop, @@ -238,7 +243,7 @@ groups: expr: count(up == 1) `, "v2") }, - finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, nil), "main", 4), + finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, nil), "main", 4, parser.PrometheusSchema), entries: []discovery.Entry{ { State: discovery.Modified, @@ -267,7 +272,7 @@ groups: expr: count(up == 1) `, "v2") }, - finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, includeAll), "main", 4), + finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, includeAll), "main", 4, parser.PrometheusSchema), entries: []discovery.Entry{ { State: discovery.Modified, @@ -296,7 +301,7 @@ groups: expr: count(up == 1) `, "v2") }, - finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(nil, nil, includeAll), "main", 4), + finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(nil, nil, includeAll), "main", 4, parser.PrometheusSchema), entries: []discovery.Entry{ { State: discovery.Modified, @@ -336,6 +341,7 @@ groups: git.NewPathFilter([]*regexp.Regexp{regexp.MustCompile("^foo#")}, nil, nil), "main", 4, + parser.PrometheusSchema, ), entries: nil, }, @@ -361,7 +367,7 @@ groups: expr: count(up == 1) `, "v2\nskip this commit\n[skip ci]\n") }, - finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, nil), "main", 4), + finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, nil), "main", 4, parser.PrometheusSchema), entries: nil, }, { @@ -386,7 +392,7 @@ groups: expr: count(up == 1) `, "v2\nskip this commit\n[no ci]\n") }, - finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, nil), "main", 4), + finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, nil), "main", 4, parser.PrometheusSchema), entries: nil, }, { @@ -406,7 +412,7 @@ groups: require.NoError(t, err, "git add") gitCommit(t, "v2") }, - finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, includeAll), "main", 4), + finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, includeAll), "main", 4, parser.PrometheusSchema), entries: []discovery.Entry{ { State: discovery.Added, @@ -451,7 +457,7 @@ groups: expr: count(up) `, "v2") }, - finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, nil), "main", 4), + finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, nil), "main", 4, parser.PrometheusSchema), entries: []discovery.Entry{ { State: discovery.Modified, @@ -523,7 +529,7 @@ groups: for: 0s `, "v2") }, - finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, includeAll), "main", 4), + finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, includeAll), "main", 4, parser.PrometheusSchema), entries: []discovery.Entry{ { State: discovery.Modified, @@ -563,7 +569,7 @@ groups: expr: sum(foo) by(job) `, "v2") }, - finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, includeAll), "main", 4), + finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, includeAll), "main", 4, parser.PrometheusSchema), entries: []discovery.Entry{ { State: discovery.Noop, @@ -603,7 +609,7 @@ groups: expr: sum(foo) by(job) `, "v2") }, - finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, includeAll), "main", 4), + finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, includeAll), "main", 4, parser.PrometheusSchema), entries: []discovery.Entry{ { State: discovery.Noop, @@ -647,7 +653,7 @@ groups: expr: sum(foo) by(job) `, "v2") }, - finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, includeAll), "main", 4), + finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, includeAll), "main", 4, parser.PrometheusSchema), entries: []discovery.Entry{ { State: discovery.Noop, @@ -701,7 +707,7 @@ groups: expr: count(up) `, "v2") }, - finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, nil), "main", 4), + finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, nil), "main", 4, parser.PrometheusSchema), entries: []discovery.Entry{ { State: discovery.Added, @@ -753,7 +759,7 @@ groups: expr: sum(foo) by(job) `, "v2") }, - finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, includeAll), "main", 4), + finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, includeAll), "main", 4, parser.PrometheusSchema), entries: []discovery.Entry{ { State: discovery.Noop, @@ -815,7 +821,7 @@ groups: expr: sum(foo) by(job) `, "v2") }, - finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, includeAll), "main", 4), + finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, includeAll), "main", 4, parser.PrometheusSchema), entries: []discovery.Entry{ { State: discovery.Modified, @@ -884,7 +890,7 @@ groups: foo: bar `, "v2") }, - finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, includeAll), "main", 4), + finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, includeAll), "main", 4, parser.PrometheusSchema), entries: []discovery.Entry{ { State: discovery.Noop, @@ -922,7 +928,7 @@ groups: gitCommit(t, "v2") }, - finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, includeAll), "main", 4), + finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, includeAll), "main", 4, parser.PrometheusSchema), entries: []discovery.Entry{ { State: discovery.Moved, @@ -957,7 +963,7 @@ groups: gitCommit(t, "v3") }, - finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, includeAll), "main", 4), + finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, includeAll), "main", 4, parser.PrometheusSchema), entries: []discovery.Entry{ { State: discovery.Moved, @@ -995,7 +1001,7 @@ groups: gitCommit(t, "v2") }, - finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, includeAll), "main", 4), + finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, includeAll), "main", 4, parser.PrometheusSchema), entries: []discovery.Entry{ { State: discovery.Moved, @@ -1055,7 +1061,7 @@ groups: expr: sum(up) `, "v2") }, - finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, nil), "main", 4), + finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, nil), "main", 4, parser.PrometheusSchema), entries: []discovery.Entry{ { State: discovery.Added, @@ -1099,7 +1105,7 @@ groups: expr: sum(foo) by(job) `, "v2") }, - finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, includeAll), "main", 4), + finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, includeAll), "main", 4, parser.PrometheusSchema), entries: []discovery.Entry{ { State: discovery.Modified, @@ -1155,7 +1161,7 @@ groups: expr: sum(foo) by(job) `, "v2") }, - finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, includeAll), "main", 4), + finder: discovery.NewGitBranchFinder(git.RunGit, git.NewPathFilter(includeAll, nil, includeAll), "main", 4, parser.PrometheusSchema), entries: []discovery.Entry{ { State: discovery.Noop, diff --git a/internal/discovery/glob.go b/internal/discovery/glob.go index ba6376fa..2d1409fb 100644 --- a/internal/discovery/glob.go +++ b/internal/discovery/glob.go @@ -9,18 +9,21 @@ import ( "path/filepath" "github.com/cloudflare/pint/internal/git" + "github.com/cloudflare/pint/internal/parser" ) -func NewGlobFinder(patterns []string, filter git.PathFilter) GlobFinder { +func NewGlobFinder(patterns []string, filter git.PathFilter, schema parser.Schema) GlobFinder { return GlobFinder{ patterns: patterns, filter: filter, + schema: schema, } } type GlobFinder struct { patterns []string filter git.PathFilter + schema parser.Schema } func (f GlobFinder) Find() (entries []Entry, err error) { @@ -66,7 +69,7 @@ func (f GlobFinder) Find() (entries []Entry, err error) { if err != nil { return nil, err } - el, err := readRules(fp.target, fp.path, fd, !f.filter.IsRelaxed(fp.target)) + el, err := readRules(fp.target, fp.path, fd, !f.filter.IsRelaxed(fp.target), f.schema) if err != nil { fd.Close() return nil, fmt.Errorf("invalid file syntax: %w", err) diff --git a/internal/discovery/glob_test.go b/internal/discovery/glob_test.go index 9802b43a..5def1d35 100644 --- a/internal/discovery/glob_test.go +++ b/internal/discovery/glob_test.go @@ -22,11 +22,11 @@ func TestGlobPathFinder(t *testing.T) { files map[string]string symlinks map[string]string err string - finder discovery.GlobFinder entries []discovery.Entry + finder discovery.GlobFinder } - p := parser.NewParser(false) + p := parser.NewParser(false, parser.PrometheusSchema) testRuleBody := "# pint file/owner bob\n\n- record: foo\n expr: sum(foo)\n" testRules, err := p.Parse([]byte(testRuleBody)) require.NoError(t, err) @@ -34,32 +34,32 @@ func TestGlobPathFinder(t *testing.T) { testCases := []testCaseT{ { files: map[string]string{}, - finder: discovery.NewGlobFinder([]string{"[]"}, git.NewPathFilter(nil, nil, nil)), + finder: discovery.NewGlobFinder([]string{"[]"}, git.NewPathFilter(nil, nil, nil), parser.PrometheusSchema), err: "failed to expand file path pattern []: syntax error in pattern", }, { files: map[string]string{}, - finder: discovery.NewGlobFinder([]string{"*"}, git.NewPathFilter(nil, nil, nil)), + finder: discovery.NewGlobFinder([]string{"*"}, git.NewPathFilter(nil, nil, nil), parser.PrometheusSchema), err: "no matching files", }, { files: map[string]string{}, - finder: discovery.NewGlobFinder([]string{"*"}, git.NewPathFilter(nil, nil, nil)), + finder: discovery.NewGlobFinder([]string{"*"}, git.NewPathFilter(nil, nil, nil), parser.PrometheusSchema), err: "no matching files", }, { files: map[string]string{}, - finder: discovery.NewGlobFinder([]string{"foo/*"}, git.NewPathFilter(nil, nil, nil)), + finder: discovery.NewGlobFinder([]string{"foo/*"}, git.NewPathFilter(nil, nil, nil), parser.PrometheusSchema), err: "no matching files", }, { files: map[string]string{"bar.yml": testRuleBody}, - finder: discovery.NewGlobFinder([]string{"foo/*"}, git.NewPathFilter(nil, nil, nil)), + finder: discovery.NewGlobFinder([]string{"foo/*"}, git.NewPathFilter(nil, nil, nil), parser.PrometheusSchema), err: "no matching files", }, { files: map[string]string{"bar.yml": testRuleBody}, - finder: discovery.NewGlobFinder([]string{"*"}, git.NewPathFilter(nil, nil, []*regexp.Regexp{regexp.MustCompile(".*")})), + finder: discovery.NewGlobFinder([]string{"*"}, git.NewPathFilter(nil, nil, []*regexp.Regexp{regexp.MustCompile(".*")}), parser.PrometheusSchema), entries: []discovery.Entry{ { State: discovery.Noop, @@ -75,7 +75,7 @@ func TestGlobPathFinder(t *testing.T) { }, { files: map[string]string{"foo/bar.yml": testRuleBody + "\n\n# pint file/owner alice\n"}, - finder: discovery.NewGlobFinder([]string{"*"}, git.NewPathFilter(nil, nil, []*regexp.Regexp{regexp.MustCompile(".*")})), + finder: discovery.NewGlobFinder([]string{"*"}, git.NewPathFilter(nil, nil, []*regexp.Regexp{regexp.MustCompile(".*")}), parser.PrometheusSchema), entries: []discovery.Entry{ { State: discovery.Noop, @@ -91,7 +91,7 @@ func TestGlobPathFinder(t *testing.T) { }, { files: map[string]string{"bar.yml": testRuleBody}, - finder: discovery.NewGlobFinder([]string{"*"}, git.NewPathFilter(nil, nil, nil)), + finder: discovery.NewGlobFinder([]string{"*"}, git.NewPathFilter(nil, nil, nil), parser.PrometheusSchema), entries: []discovery.Entry{ { State: discovery.Noop, @@ -110,7 +110,7 @@ func TestGlobPathFinder(t *testing.T) { }, { files: map[string]string{"bar.yml": "record:::{}\n expr: sum(foo)\n\n# pint file/owner bob\n"}, - finder: discovery.NewGlobFinder([]string{"*"}, git.NewPathFilter(nil, nil, []*regexp.Regexp{regexp.MustCompile(".*")})), + finder: discovery.NewGlobFinder([]string{"*"}, git.NewPathFilter(nil, nil, []*regexp.Regexp{regexp.MustCompile(".*")}), parser.PrometheusSchema), entries: []discovery.Entry{ { State: discovery.Noop, @@ -130,7 +130,7 @@ func TestGlobPathFinder(t *testing.T) { { files: map[string]string{"bar.yml": testRuleBody}, symlinks: map[string]string{"link.yml": "bar.yml"}, - finder: discovery.NewGlobFinder([]string{"*"}, git.NewPathFilter(nil, nil, nil)), + finder: discovery.NewGlobFinder([]string{"*"}, git.NewPathFilter(nil, nil, nil), parser.PrometheusSchema), entries: []discovery.Entry{ { State: discovery.Noop, @@ -166,7 +166,7 @@ func TestGlobPathFinder(t *testing.T) { "b/link.yml": "../a/bar.yml", "b/c/link.yml": "../../a/bar.yml", }, - finder: discovery.NewGlobFinder([]string{"*"}, git.NewPathFilter(nil, nil, nil)), + finder: discovery.NewGlobFinder([]string{"*"}, git.NewPathFilter(nil, nil, nil), parser.PrometheusSchema), entries: []discovery.Entry{ { State: discovery.Noop, @@ -215,7 +215,7 @@ func TestGlobPathFinder(t *testing.T) { "b/link.yml": "../a/bar.yml", "b/c/link.yml": "../a/bar.yml", }, - finder: discovery.NewGlobFinder([]string{"*"}, git.NewPathFilter(nil, nil, nil)), + finder: discovery.NewGlobFinder([]string{"*"}, git.NewPathFilter(nil, nil, nil), parser.PrometheusSchema), err: "b/c/link.yml is a symlink but target file cannot be evaluated: lstat b/a: no such file or directory", }, { @@ -223,14 +223,14 @@ func TestGlobPathFinder(t *testing.T) { symlinks: map[string]string{ "b/c/link.yml": "../../a/bar.yml", }, - finder: discovery.NewGlobFinder([]string{"*"}, git.NewPathFilter(nil, nil, []*regexp.Regexp{regexp.MustCompile(".*")})), + finder: discovery.NewGlobFinder([]string{"*"}, git.NewPathFilter(nil, nil, []*regexp.Regexp{regexp.MustCompile(".*")}), parser.PrometheusSchema), }, { files: map[string]string{"a/bar.yml": "xxx:\nyyy:\n"}, symlinks: map[string]string{ "b/c/link.yml": "../../a/bar.yml", }, - finder: discovery.NewGlobFinder([]string{"*"}, git.NewPathFilter(nil, nil, nil)), + finder: discovery.NewGlobFinder([]string{"*"}, git.NewPathFilter(nil, nil, nil), parser.PrometheusSchema), entries: []discovery.Entry{ { State: discovery.Noop, @@ -265,21 +265,21 @@ func TestGlobPathFinder(t *testing.T) { symlinks: map[string]string{ "b/c/link.yml": "../../a/bar.yml", }, - finder: discovery.NewGlobFinder([]string{"*"}, git.NewPathFilter(nil, nil, []*regexp.Regexp{regexp.MustCompile(".*")})), + finder: discovery.NewGlobFinder([]string{"*"}, git.NewPathFilter(nil, nil, []*regexp.Regexp{regexp.MustCompile(".*")}), parser.PrometheusSchema), }, { files: map[string]string{"a/bar.yml": "xxx:\nyyy:\n"}, symlinks: map[string]string{ "b/c/d": "../../a", }, - finder: discovery.NewGlobFinder([]string{"*"}, git.NewPathFilter(nil, nil, []*regexp.Regexp{regexp.MustCompile(".*")})), + finder: discovery.NewGlobFinder([]string{"*"}, git.NewPathFilter(nil, nil, []*regexp.Regexp{regexp.MustCompile(".*")}), parser.PrometheusSchema), }, { files: map[string]string{"a/bar.yml": testRuleBody}, symlinks: map[string]string{ "b/c/link.yml": "../../a/bar.yml", }, - finder: discovery.NewGlobFinder([]string{"*"}, git.NewPathFilter(nil, nil, nil)), + finder: discovery.NewGlobFinder([]string{"*"}, git.NewPathFilter(nil, nil, nil), parser.PrometheusSchema), entries: []discovery.Entry{ { State: discovery.Noop, @@ -313,7 +313,7 @@ func TestGlobPathFinder(t *testing.T) { symlinks: map[string]string{ "input.yml": "/xx/ccc/fdd", }, - finder: discovery.NewGlobFinder([]string{"*"}, git.NewPathFilter(nil, nil, nil)), + finder: discovery.NewGlobFinder([]string{"*"}, git.NewPathFilter(nil, nil, nil), parser.PrometheusSchema), err: "input.yml is a symlink but target file cannot be evaluated: lstat /xx: no such file or directory", }, } diff --git a/internal/parser/fuzz_test.go b/internal/parser/fuzz_test.go index b5a7ec3c..d03e7a3c 100644 --- a/internal/parser/fuzz_test.go +++ b/internal/parser/fuzz_test.go @@ -276,7 +276,7 @@ labels: for _, tc := range testcases { f.Add(tc) } - p := parser.NewParser(false) + p := parser.NewParser(false, parser.PrometheusSchema) f.Fuzz(func(t *testing.T, s string) { t.Logf("Parsing: [%s]\n", s) _, _ = p.Parse([]byte(s)) diff --git a/internal/parser/models_test.go b/internal/parser/models_test.go index 95bf5a0a..8db4d9d4 100644 --- a/internal/parser/models_test.go +++ b/internal/parser/models_test.go @@ -10,7 +10,7 @@ import ( ) func newMustRule(content string) parser.Rule { - p := parser.NewParser(false) + p := parser.NewParser(false, parser.PrometheusSchema) rules, err := p.Parse([]byte(content)) if err != nil { panic(err) diff --git a/internal/parser/parser.go b/internal/parser/parser.go index 01231a4f..d215b9b3 100644 --- a/internal/parser/parser.go +++ b/internal/parser/parser.go @@ -17,6 +17,7 @@ import ( ) const ( + // Standard Prometheus fields. recordKey = "record" exprKey = "expr" labelsKey = "labels" @@ -24,17 +25,29 @@ const ( forKey = "for" keepFiringForKey = "keep_firing_for" annotationsKey = "annotations" + + // Thanos Rule specific fields. + partialResponseStrategyKey = "partial_response_strategy" ) var ErrRuleCommentOnFile = errors.New("this comment is only valid when attached to a rule") -func NewParser(isStrict bool) Parser { +type Schema int + +const ( + PrometheusSchema Schema = iota + ThanosSchema +) + +func NewParser(isStrict bool, schema Schema) Parser { return Parser{ isStrict: isStrict, + schema: schema, } } type Parser struct { + schema Schema isStrict bool } @@ -62,13 +75,13 @@ func (p Parser) Parse(content []byte) (rules []Rule, err error) { } index++ if p.isStrict { - r, err := parseGroups(content, &doc) + r, err := parseGroups(content, &doc, p.schema) if err.Err != nil { return rules, err } rules = append(rules, r...) } else { - rules = append(rules, parseNode(content, &doc, 0)...) + rules = append(rules, parseNode(content, &doc, 0, p.schema)...) } if index > 1 && p.isStrict { rules = append(rules, Rule{ @@ -86,8 +99,8 @@ To allow for multi-document YAML files set parser->relaxed option in pint config return rules, err } -func parseNode(content []byte, node *yaml.Node, offset int) (rules []Rule) { - ret, isEmpty := parseRule(content, node, offset) +func parseNode(content []byte, node *yaml.Node, offset int, schema Schema) (rules []Rule) { + ret, isEmpty := parseRule(content, node, offset, schema) if !isEmpty { rules = append(rules, ret) return rules @@ -99,15 +112,15 @@ func parseNode(content []byte, node *yaml.Node, offset int) (rules []Rule) { switch root.Kind { case yaml.SequenceNode: for _, n := range root.Content { - rules = append(rules, parseNode(content, n, offset)...) + rules = append(rules, parseNode(content, n, offset, schema)...) } case yaml.MappingNode: - rule, isEmpty = parseRule(content, root, offset) + rule, isEmpty = parseRule(content, root, offset, schema) if !isEmpty { rules = append(rules, rule) } else { for _, n := range root.Content { - rules = append(rules, parseNode(content, n, offset)...) + rules = append(rules, parseNode(content, n, offset, schema)...) } } case yaml.ScalarNode: @@ -115,7 +128,7 @@ func parseNode(content []byte, node *yaml.Node, offset int) (rules []Rule) { c := []byte(root.Value) var n yaml.Node if err := yaml.Unmarshal(c, &n); err == nil { - rules = append(rules, parseNode(c, &n, offset+root.Line)...) + rules = append(rules, parseNode(c, &n, offset+root.Line, schema)...) } } } @@ -123,7 +136,7 @@ func parseNode(content []byte, node *yaml.Node, offset int) (rules []Rule) { return rules } -func parseRule(content []byte, node *yaml.Node, offset int) (rule Rule, _ bool) { +func parseRule(content []byte, node *yaml.Node, offset int, schema Schema) (rule Rule, _ bool) { if node.Kind != yaml.MappingNode { return rule, true } @@ -136,6 +149,7 @@ func parseRule(content []byte, node *yaml.Node, offset int) (rule Rule, _ bool) var forPart *YamlNode var keepFiringForPart *YamlNode var annotationsPart *YamlMap + var partialResponseStrategyPart *YamlNode var recordNode *yaml.Node var alertNode *yaml.Node @@ -144,6 +158,8 @@ func parseRule(content []byte, node *yaml.Node, offset int) (rule Rule, _ bool) var keepFiringForNode *yaml.Node var labelsNode *yaml.Node var annotationsNode *yaml.Node + var partialResponseStrategyNode *yaml.Node + labelsNodes := []yamlMap{} annotationsNodes := []yamlMap{} @@ -232,6 +248,18 @@ func parseRule(content []byte, node *yaml.Node, offset int) (rule Rule, _ bool) annotationsNodes = mappingNodes(part) annotationsPart = newYamlMap(key, part, offset) lines.Last = max(lines.Last, annotationsPart.Lines.Last) + case partialResponseStrategyKey: + if schema != ThanosSchema { + unknownKeys = append(unknownKeys, key) + continue + } + + if partialResponseStrategyPart != nil { + return duplicatedKeyError(lines, part.Line+offset, partialResponseStrategyKey) + } + partialResponseStrategyNode = part + partialResponseStrategyPart = newYamlNodeWithKey(key, part, offset) + lines.Last = max(lines.Last, partialResponseStrategyPart.Lines.Last) default: unknownKeys = append(unknownKeys, key) } @@ -430,6 +458,25 @@ func parseRule(content []byte, node *yaml.Node, offset int) (rule Rule, _ bool) } } + if partialResponseStrategyPart != nil && partialResponseStrategyNode != nil { + val := nodeValue(partialResponseStrategyNode) + if !isTag(partialResponseStrategyNode.ShortTag(), strTag) { + return invalidValueError(lines, partialResponseStrategyNode.Line+offset, partialResponseStrategyKey, describeTag(strTag), describeTag(partialResponseStrategyNode.ShortTag())) + } + switch val { + case "warn": + case "abort": + default: + return Rule{ + Lines: lines, + Error: ParseError{ + Line: partialResponseStrategyPart.Lines.First, + Err: fmt.Errorf("invalid %s value: %s", partialResponseStrategyKey, val), + }, + }, false + } + } + if recordPart != nil && exprPart != nil { rule = Rule{ Lines: lines, diff --git a/internal/parser/parser_test.go b/internal/parser/parser_test.go index 229647ba..4e1f9aef 100644 --- a/internal/parser/parser_test.go +++ b/internal/parser/parser_test.go @@ -19,6 +19,7 @@ func TestParse(t *testing.T) { content []byte output []parser.Rule strict bool + schema parser.Schema } testCases := []testCaseT{ @@ -2880,6 +2881,187 @@ groups: }, }, }, + { + content: []byte(` +groups: +- name: mygroup + rules: + - record: up:count + expr: count(up) + partial_response_strategy: bob +`), + strict: true, + err: "error at line 7: partial_response_strategy is only valid when parser is configured to use the Thanos rule schema", + }, + { + content: []byte(` +groups: +- name: mygroup + rules: + - record: up:count + expr: count(up) + partial_response_strategy: warn +`), + strict: true, + schema: parser.ThanosSchema, + output: []parser.Rule{ + { + Lines: parser.LineRange{First: 5, Last: 7}, + RecordingRule: &parser.RecordingRule{ + Record: parser.YamlNode{ + Lines: parser.LineRange{First: 5, Last: 5}, + Value: "up:count", + }, + Expr: parser.PromQLExpr{ + Value: &parser.YamlNode{ + Lines: parser.LineRange{First: 6, Last: 6}, + Value: "count(up)", + }, + }, + }, + }, + }, + }, + { + content: []byte(` +groups: +- name: mygroup + rules: + - record: up:count + expr: count(up) + partial_response_strategy: abort +`), + strict: true, + schema: parser.ThanosSchema, + output: []parser.Rule{ + { + Lines: parser.LineRange{First: 5, Last: 7}, + RecordingRule: &parser.RecordingRule{ + Record: parser.YamlNode{ + Lines: parser.LineRange{First: 5, Last: 5}, + Value: "up:count", + }, + Expr: parser.PromQLExpr{ + Value: &parser.YamlNode{ + Lines: parser.LineRange{First: 6, Last: 6}, + Value: "count(up)", + }, + }, + }, + }, + }, + }, + { + content: []byte(` +- record: up:count + expr: count(up) + partial_response_strategy: warn +`), + schema: parser.ThanosSchema, + output: []parser.Rule{ + { + Lines: parser.LineRange{First: 2, Last: 4}, + RecordingRule: &parser.RecordingRule{ + Record: parser.YamlNode{ + Lines: parser.LineRange{First: 2, Last: 2}, + Value: "up:count", + }, + Expr: parser.PromQLExpr{ + Value: &parser.YamlNode{ + Lines: parser.LineRange{First: 3, Last: 3}, + Value: "count(up)", + }, + }, + }, + }, + }, + }, + { + content: []byte(` +- record: up:count + expr: count(up) + partial_response_strategy: abort +`), + schema: parser.ThanosSchema, + output: []parser.Rule{ + { + Lines: parser.LineRange{First: 2, Last: 4}, + RecordingRule: &parser.RecordingRule{ + Record: parser.YamlNode{ + Lines: parser.LineRange{First: 2, Last: 2}, + Value: "up:count", + }, + Expr: parser.PromQLExpr{ + Value: &parser.YamlNode{ + Lines: parser.LineRange{First: 3, Last: 3}, + Value: "count(up)", + }, + }, + }, + }, + }, + }, + { + content: []byte(` +groups: +- name: mygroup + rules: + - record: up:count + expr: count(up) + partial_response_strategy: bob +`), + strict: true, + schema: parser.ThanosSchema, + output: []parser.Rule{ + { + Lines: parser.LineRange{First: 5, Last: 7}, + Error: parser.ParseError{ + Line: 7, + Err: errors.New("invalid partial_response_strategy value: bob"), + }, + }, + }, + }, + { + content: []byte(` +groups: +- name: mygroup + rules: + - record: up:count + expr: count(up) + partial_response_strategy: 1 +`), + strict: true, + schema: parser.ThanosSchema, + output: []parser.Rule{ + { + Lines: parser.LineRange{First: 5, Last: 7}, + Error: parser.ParseError{ + Line: 7, + Err: errors.New("partial_response_strategy value must be a string, got integer instead"), + }, + }, + }, + }, + { + content: []byte("- record: foo\n expr: foo\n partial_response_strategy: true\n"), + output: []parser.Rule{ + { + Lines: parser.LineRange{First: 1, Last: 3}, + Error: parser.ParseError{Err: errors.New("invalid key(s) found: partial_response_strategy"), Line: 3}, + }, + }, + }, + { + content: []byte("- record: foo\n expr: foo\n partial_response_strategy: true\n partial_response_strategy: false\n"), + schema: parser.ThanosSchema, + output: []parser.Rule{ + { + Lines: parser.LineRange{First: 1, Last: 4}, + Error: parser.ParseError{Err: errors.New("duplicated partial_response_strategy key"), Line: 4}, + }, + }, + }, } alwaysEqual := cmp.Comparer(func(_, _ interface{}) bool { return true }) @@ -2904,7 +3086,7 @@ groups: t.Run(strconv.Itoa(i+1), func(t *testing.T) { t.Logf("\n--- Content ---%s--- END ---", tc.content) - p := parser.NewParser(tc.strict) + p := parser.NewParser(tc.strict, tc.schema) output, err := p.Parse(tc.content) if tc.err != "" { diff --git a/internal/parser/strict.go b/internal/parser/strict.go index 4b400003..86820699 100644 --- a/internal/parser/strict.go +++ b/internal/parser/strict.go @@ -40,7 +40,7 @@ func describeTag(tag string) string { } } -func parseGroups(content []byte, doc *yaml.Node) (rules []Rule, err ParseError) { +func parseGroups(content []byte, doc *yaml.Node, schema Schema) (rules []Rule, err ParseError) { names := map[string]struct{}{} for _, node := range unpackNodes(doc) { @@ -71,7 +71,7 @@ func parseGroups(content []byte, doc *yaml.Node) (rules []Rule, err ParseError) } } for _, group := range unpackNodes(entry.val) { - name, r, err := parseGroup(content, group) + name, r, err := parseGroup(content, group, schema) if err.Err != nil { return rules, err } @@ -89,7 +89,7 @@ func parseGroups(content []byte, doc *yaml.Node) (rules []Rule, err ParseError) return rules, ParseError{} } -func parseGroup(content []byte, group *yaml.Node) (name string, rules []Rule, err ParseError) { +func parseGroup(content []byte, group *yaml.Node, schema Schema) (name string, rules []Rule, err ParseError) { if !isTag(group.ShortTag(), mapTag) { return "", nil, ParseError{ Line: group.Line, @@ -143,7 +143,7 @@ func parseGroup(content []byte, group *yaml.Node) (name string, rules []Rule, er } } for _, rule := range unpackNodes(entry.val) { - r, err := parseRuleStrict(content, rule) + r, err := parseRuleStrict(content, rule, schema) if err.Err != nil { return "", nil, err } @@ -177,7 +177,7 @@ func parseGroup(content []byte, group *yaml.Node) (name string, rules []Rule, er return name, rules, ParseError{} } -func parseRuleStrict(content []byte, rule *yaml.Node) (Rule, ParseError) { +func parseRuleStrict(content []byte, rule *yaml.Node, schema Schema) (Rule, ParseError) { if !isTag(rule.ShortTag(), mapTag) { return Rule{}, ParseError{ Line: rule.Line, @@ -190,13 +190,20 @@ func parseRuleStrict(content []byte, rule *yaml.Node) (Rule, ParseError) { continue } switch node.Value { - case "record": - case "alert": - case "expr": - case "for": - case "keep_firing_for": - case "labels": - case "annotations": + case recordKey: + case alertKey: + case exprKey: + case forKey: + case keepFiringForKey: + case labelsKey: + case annotationsKey: + case partialResponseStrategyKey: + if schema != ThanosSchema { + return Rule{}, ParseError{ + Line: node.Line, + Err: fmt.Errorf("%s is only valid when parser is configured to use the Thanos rule schema", node.Value), + } + } default: return Rule{}, ParseError{ Line: node.Line, @@ -205,6 +212,6 @@ func parseRuleStrict(content []byte, rule *yaml.Node) (Rule, ParseError) { } } - r, _ := parseRule(content, rule, 0) + r, _ := parseRule(content, rule, 0, schema) return r, ParseError{} } diff --git a/internal/reporter/bitbucket_test.go b/internal/reporter/bitbucket_test.go index c7a9b579..cb37d0da 100644 --- a/internal/reporter/bitbucket_test.go +++ b/internal/reporter/bitbucket_test.go @@ -41,7 +41,7 @@ func TestBitBucketReporter(t *testing.T) { pullRequestActivities reporter.BitBucketPullRequestActivities } - p := parser.NewParser(false) + p := parser.NewParser(false, parser.PrometheusSchema) mockRules, _ := p.Parse([]byte(` - record: target is down expr: up == 0 diff --git a/internal/reporter/checkstyle_test.go b/internal/reporter/checkstyle_test.go index 785b9876..7c66f609 100644 --- a/internal/reporter/checkstyle_test.go +++ b/internal/reporter/checkstyle_test.go @@ -22,7 +22,7 @@ func TestCheckstyleReporter(t *testing.T) { summary reporter.Summary } - p := parser.NewParser(false) + p := parser.NewParser(false, parser.PrometheusSchema) mockRules, _ := p.Parse([]byte(` - record: target is down expr: up == 0 diff --git a/internal/reporter/comments_test.go b/internal/reporter/comments_test.go index fb03c53d..7a8875a4 100644 --- a/internal/reporter/comments_test.go +++ b/internal/reporter/comments_test.go @@ -73,7 +73,7 @@ func (tc testCommenter) IsEqual(_ any, e ExistingComment, p PendingComment) bool } func TestCommenter(t *testing.T) { - p := parser.NewParser(false) + p := parser.NewParser(false, parser.PrometheusSchema) mockRules, _ := p.Parse([]byte(` - record: target is down expr: up == 0 @@ -752,7 +752,7 @@ func TestCommentsCommonPaths(t *testing.T) { maxComments int } - p := parser.NewParser(false) + p := parser.NewParser(false, parser.PrometheusSchema) mockRules, _ := p.Parse([]byte(` - record: target is down expr: up == 0 diff --git a/internal/reporter/github_test.go b/internal/reporter/github_test.go index 8e695eb3..63fc1b9a 100644 --- a/internal/reporter/github_test.go +++ b/internal/reporter/github_test.go @@ -36,7 +36,7 @@ func TestGithubReporter(t *testing.T) { timeout time.Duration } - p := parser.NewParser(false) + p := parser.NewParser(false, parser.PrometheusSchema) mockRules, _ := p.Parse([]byte(` - record: target is down expr: up == 0 diff --git a/internal/reporter/gitlab_test.go b/internal/reporter/gitlab_test.go index 89290e1a..9a6173a9 100644 --- a/internal/reporter/gitlab_test.go +++ b/internal/reporter/gitlab_test.go @@ -50,7 +50,7 @@ func TestGitLabReporter(t *testing.T) { maxComments int } - p := parser.NewParser(false) + p := parser.NewParser(false, parser.PrometheusSchema) mockRules, _ := p.Parse([]byte(` - record: target is down expr: up == 0 @@ -408,7 +408,7 @@ func TestGitLabReporterCommentLine(t *testing.T) { anchor checks.Anchor } - p := parser.NewParser(false) + p := parser.NewParser(false, parser.PrometheusSchema) mockRules, _ := p.Parse([]byte(` - record: target is down expr: up == 0 diff --git a/internal/reporter/teamcity_test.go b/internal/reporter/teamcity_test.go index 2f4aa7ae..1dfa3199 100644 --- a/internal/reporter/teamcity_test.go +++ b/internal/reporter/teamcity_test.go @@ -22,7 +22,7 @@ func TestTeamCityReporter(t *testing.T) { summary reporter.Summary } - p := parser.NewParser(false) + p := parser.NewParser(false, parser.PrometheusSchema) mockRules, _ := p.Parse([]byte(` - record: target is down expr: up == 0