From 468f72886cf163dd8d79c08ea9a47b8f12499bd8 Mon Sep 17 00:00:00 2001 From: Lukasz Mierzwa Date: Tue, 3 Dec 2024 14:55:03 +0000 Subject: [PATCH 1/2] Add support for parser schemas --- cmd/pint/bench_test.go | 3 + cmd/pint/ci.go | 13 +- cmd/pint/lint.go | 13 +- .../tests/0203_parser_schema_prom_err.txt | 27 +++ .../tests/0204_parser_schema_thanos_err.txt | 26 +++ .../tests/0205_parser_schema_thanos_ok.txt | 22 +++ cmd/pint/tests/0206_parser_schema_err.txt | 21 ++ cmd/pint/watch.go | 25 ++- docs/changelog.md | 15 ++ docs/configuration.md | 9 + internal/checks/base_test.go | 2 +- internal/checks/template_test.go | 2 +- internal/config/config_test.go | 2 +- internal/config/parser.go | 21 ++ internal/config/parser_test.go | 16 ++ internal/discovery/discovery.go | 4 +- internal/discovery/discovery_test.go | 4 +- internal/discovery/git_branch.go | 5 + internal/discovery/git_branch_test.go | 56 +++--- internal/discovery/glob.go | 7 +- internal/discovery/glob_test.go | 40 ++-- internal/parser/fuzz_test.go | 2 +- internal/parser/models_test.go | 2 +- internal/parser/parser.go | 67 ++++++- internal/parser/parser_test.go | 184 +++++++++++++++++- internal/parser/strict.go | 33 ++-- internal/reporter/bitbucket_test.go | 2 +- internal/reporter/checkstyle_test.go | 2 +- internal/reporter/comments_test.go | 4 +- internal/reporter/github_test.go | 2 +- internal/reporter/gitlab_test.go | 4 +- internal/reporter/teamcity_test.go | 2 +- 32 files changed, 533 insertions(+), 104 deletions(-) create mode 100644 cmd/pint/tests/0203_parser_schema_prom_err.txt create mode 100644 cmd/pint/tests/0204_parser_schema_thanos_err.txt create mode 100644 cmd/pint/tests/0205_parser_schema_thanos_ok.txt create mode 100644 cmd/pint/tests/0206_parser_schema_err.txt 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/changelog.md b/docs/changelog.md index 305a1aae..0d9f9e6d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,20 @@ # Changelog +## v0.69.0 + +### Added + +- Added `schema` option to the `parser` configuration block for setting rule validation + schema. Default value is `prometheus` and tells pint to expect rules with the schema + expected by Prometheus itself. If you use pint to validate rules loaded into Thanos Rule + component then set `schema` to `thanos` in your pint config file: + + ```js + parser { + schema = "thanos" + } + ``` + ## v0.68.0 ### Added 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 From 60017bfb3f1ce98a5c8c7aba818db20b7e968759 Mon Sep 17 00:00:00 2001 From: Lukasz Mierzwa Date: Tue, 3 Dec 2024 15:21:55 +0000 Subject: [PATCH 2/2] Add a note about mimir and such --- docs/index.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/index.md b/docs/index.md index 346b2eeb..aae1bf45 100644 --- a/docs/index.md +++ b/docs/index.md @@ -32,6 +32,11 @@ If you run pint against a different service, like [Thanos](https://thanos.io/) s might return problems due to API call errors, since not all Prometheus HTTP APIs are supported by it. In that case, you might want to disable failing checks in the pint configuration file. +**IMPORTANT** `pint` is a tool we wrote to work with our **Prometheus** deployment. It's not intended to be +used with other services that offer partial compatibility with Prometheus, there are **NO PLANS** +to add support for any other services. The only reason we would add support for other systems is if +we started to use them ourselves. + ## Usage There are three modes it works in: