rulefmt: warn when multi-document rule files are detected (#18114)
Some checks failed
buf.build / lint and publish (push) Has been cancelled
CI / Go tests (push) Has been cancelled
CI / More Go tests (push) Has been cancelled
CI / Go tests for Prometheus upgrades and downgrades (push) Has been cancelled
CI / Go tests with previous Go version (push) Has been cancelled
CI / UI tests (push) Has been cancelled
CI / Go tests on Windows (push) Has been cancelled
CI / Mixins tests (push) Has been cancelled
CI / Compliance testing (push) Has been cancelled
CI / Build Prometheus for common architectures (0) (push) Has been cancelled
CI / Build Prometheus for common architectures (1) (push) Has been cancelled
CI / Build Prometheus for common architectures (2) (push) Has been cancelled
CI / Build Prometheus for all architectures (0) (push) Has been cancelled
CI / Build Prometheus for all architectures (1) (push) Has been cancelled
CI / Build Prometheus for all architectures (10) (push) Has been cancelled
CI / Build Prometheus for all architectures (11) (push) Has been cancelled
CI / Build Prometheus for all architectures (2) (push) Has been cancelled
CI / Build Prometheus for all architectures (3) (push) Has been cancelled
CI / Build Prometheus for all architectures (4) (push) Has been cancelled
CI / Build Prometheus for all architectures (5) (push) Has been cancelled
CI / Build Prometheus for all architectures (6) (push) Has been cancelled
CI / Build Prometheus for all architectures (7) (push) Has been cancelled
CI / Build Prometheus for all architectures (8) (push) Has been cancelled
CI / Build Prometheus for all architectures (9) (push) Has been cancelled
CI / Report status of build Prometheus for all architectures (push) Has been cancelled
CI / Check generated parser (push) Has been cancelled
CI / golangci-lint (push) Has been cancelled
CI / fuzzing (push) Has been cancelled
CI / codeql (push) Has been cancelled
CI / Publish main branch artifacts (push) Has been cancelled
CI / Publish release artefacts (push) Has been cancelled
CI / Publish UI on npm Registry (push) Has been cancelled
Scorecards supply-chain security / Scorecards analysis (push) Has been cancelled
Sync repo files / repo_sync (push) Has been cancelled

Signed-off-by: Divyansh Mishra <divyanshmishra@Divyanshs-MacBook-Air-3.local>
Co-authored-by: Divyansh Mishra <mishraa-G@users.noreply.github.com>
Co-authored-by: Divyansh Mishra <divyanshmishra@Divyanshs-MacBook-Air-3.local>
This commit is contained in:
mishraa-G
2026-04-09 18:10:18 +05:30
committed by GitHub
parent 06e0b5af5b
commit a6ffcaa352
7 changed files with 122 additions and 31 deletions

View File

@@ -19,6 +19,7 @@ import (
"errors"
"fmt"
"io"
"log/slog"
"os"
"strings"
"time"
@@ -339,7 +340,13 @@ func testTemplateParsing(rl *Rule) (errs []error) {
}
// Parse parses and validates a set of rules.
func Parse(content []byte, ignoreUnknownFields bool, nameValidationScheme model.ValidationScheme, p parser.Parser) (*RuleGroups, []error) {
func Parse(
content []byte,
ignoreUnknownFields bool,
nameValidationScheme model.ValidationScheme,
p parser.Parser,
logger *slog.Logger,
) (*RuleGroups, []error) {
var (
groups RuleGroups
node ruleGroups
@@ -355,6 +362,13 @@ func Parse(content []byte, ignoreUnknownFields bool, nameValidationScheme model.
if err != nil && !errors.Is(err, io.EOF) {
errs = append(errs, err)
}
// Check for a second document.
var secondDoc any
err = decoder.Decode(&secondDoc)
if !errors.Is(err, io.EOF) {
logger.Warn("Multiple document yaml rules files are not supported, only the first document is processed")
}
err = yaml.Unmarshal(content, &node)
if err != nil {
errs = append(errs, err)
@@ -368,12 +382,18 @@ func Parse(content []byte, ignoreUnknownFields bool, nameValidationScheme model.
}
// ParseFile reads and parses rules from a file.
func ParseFile(file string, ignoreUnknownFields bool, nameValidationScheme model.ValidationScheme, p parser.Parser) (*RuleGroups, []error) {
func ParseFile(
file string,
ignoreUnknownFields bool,
nameValidationScheme model.ValidationScheme,
p parser.Parser,
logger *slog.Logger,
) (*RuleGroups, []error) {
b, err := os.ReadFile(file)
if err != nil {
return nil, []error{fmt.Errorf("%s: %w", file, err)}
}
rgs, errs := Parse(b, ignoreUnknownFields, nameValidationScheme, p)
rgs, errs := Parse(b, ignoreUnknownFields, nameValidationScheme, p, logger)
for i := range errs {
errs[i] = fmt.Errorf("%s: %w", file, errs[i])
}

View File

@@ -14,29 +14,35 @@
package rulefmt
import (
"bytes"
"errors"
"io"
"log/slog"
"path/filepath"
"testing"
"github.com/prometheus/common/model"
"github.com/prometheus/common/promslog"
"github.com/stretchr/testify/require"
"go.yaml.in/yaml/v3"
"github.com/prometheus/prometheus/promql/parser"
)
var testParser = parser.NewParser(parser.Options{})
var (
testParser = parser.NewParser(parser.Options{})
testLogger = promslog.NewNopLogger()
)
func TestParseFileSuccess(t *testing.T) {
_, errs := ParseFile("testdata/test.yaml", false, model.UTF8Validation, testParser)
_, errs := ParseFile("testdata/test.yaml", false, model.UTF8Validation, testParser, testLogger)
require.Empty(t, errs, "unexpected errors parsing file")
_, errs = ParseFile("testdata/utf-8_lname.good.yaml", false, model.UTF8Validation, testParser)
_, errs = ParseFile("testdata/utf-8_lname.good.yaml", false, model.UTF8Validation, testParser, testLogger)
require.Empty(t, errs, "unexpected errors parsing file")
_, errs = ParseFile("testdata/utf-8_annotation.good.yaml", false, model.UTF8Validation, testParser)
_, errs = ParseFile("testdata/utf-8_annotation.good.yaml", false, model.UTF8Validation, testParser, testLogger)
require.Empty(t, errs, "unexpected errors parsing file")
_, errs = ParseFile("testdata/legacy_validation_annotation.good.yaml", false, model.LegacyValidation, testParser)
_, errs = ParseFile("testdata/legacy_validation_annotation.good.yaml", false, model.LegacyValidation, testParser, testLogger)
require.Empty(t, errs, "unexpected errors parsing file")
}
@@ -45,7 +51,7 @@ func TestParseFileSuccessWithAliases(t *testing.T) {
/
sum without(instance) (rate(requests_total[5m]))
`
rgs, errs := ParseFile("testdata/test_aliases.yaml", false, model.UTF8Validation, testParser)
rgs, errs := ParseFile("testdata/test_aliases.yaml", false, model.UTF8Validation, testParser, testLogger)
require.Empty(t, errs, "unexpected errors parsing file")
for _, rg := range rgs.Groups {
require.Equal(t, "HighAlert", rg.Rules[0].Alert)
@@ -123,7 +129,7 @@ func TestParseFileFailure(t *testing.T) {
if c.nameValidationScheme == model.UnsetValidation {
c.nameValidationScheme = model.UTF8Validation
}
_, errs := ParseFile(filepath.Join("testdata", c.filename), false, c.nameValidationScheme, testParser)
_, errs := ParseFile(filepath.Join("testdata", c.filename), false, c.nameValidationScheme, testParser, testLogger)
require.NotEmpty(t, errs, "Expected error parsing %s but got none", c.filename)
require.ErrorContainsf(t, errs[0], c.errMsg, "Expected error for %s.", c.filename)
})
@@ -219,7 +225,7 @@ groups:
}
for _, tst := range tests {
rgs, errs := Parse([]byte(tst.ruleString), false, model.UTF8Validation, testParser)
rgs, errs := Parse([]byte(tst.ruleString), false, model.UTF8Validation, testParser, testLogger)
require.NotNil(t, rgs, "Rule parsing, rule=\n"+tst.ruleString)
passed := (tst.shouldPass && len(errs) == 0) || (!tst.shouldPass && len(errs) > 0)
require.True(t, passed, "Rule validation failed, rule=\n"+tst.ruleString)
@@ -246,7 +252,7 @@ groups:
annotations:
summary: "Instance {{ $labels.instance }} up"
`
_, errs := Parse([]byte(group), false, model.UTF8Validation, testParser)
_, errs := Parse([]byte(group), false, model.UTF8Validation, testParser, testLogger)
require.Len(t, errs, 2, "Expected two errors")
var err00 *Error
require.ErrorAs(t, errs[0], &err00)
@@ -391,3 +397,59 @@ func TestErrorUnwrap(t *testing.T) {
})
}
}
func TestMultiDocParse(t *testing.T) {
const (
valid = `
groups:
- name: example
rules:
- alert: InstanceDown
expr: up == 0
`
multi = `
groups:
- name: example
rules:
- alert: InstanceDown
expr: up == 0
---
groups:
- name: example2
rules:
- alert: InstanceDown2
expr: up == 0
`
multiEmpty = `
groups:
- name: example
rules:
- alert: InstanceDown
expr: up == 0
---
`
)
var buf bytes.Buffer
bufLogger := slog.New(slog.NewTextHandler(&buf, nil))
rgs, errs := Parse([]byte(valid), false, model.UTF8Validation, testParser, bufLogger)
require.Empty(t, errs)
require.NotNil(t, rgs)
require.Len(t, rgs.Groups, 1)
require.NotContains(t, buf.String(), "Multiple document yaml rules files are not supported")
buf.Reset()
rgs, errs = Parse([]byte(multi), false, model.UTF8Validation, testParser, bufLogger)
require.Empty(t, errs)
require.NotNil(t, rgs)
require.Len(t, rgs.Groups, 1)
require.Contains(t, buf.String(), "Multiple document yaml rules files are not supported")
buf.Reset()
rgs, errs = Parse([]byte(multiEmpty), false, model.UTF8Validation, testParser, bufLogger)
require.Empty(t, errs)
require.NotNil(t, rgs)
require.Len(t, rgs.Groups, 1)
require.Contains(t, buf.String(), "Multiple document yaml rules files are not supported")
}