diff --git a/CHANGELOG.md b/CHANGELOG.md index 19046dc..ede7a07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 To enable them, you need to set some of the new flags described below - Changed: The Prometheus results cache format has changed to reduce it's size and improve performance. **Delete the old cache file** before upgrade. Also now if the cache contains time of creation and URL of the Prometheus it has data for. From now on, if the URL does not match, the case is pruned. +- Added: :rocket: Support for validation of rule files in the [Jsonnet](https://jsonnet.org/) format. - Added: New flags `--support-thanos`, `--support-mimir`, `--support-loki` to enable special rule file fields of Thanos, Mimir or Loki - Added: :tada: **Support for validation of Loki rules!** Now you can validate Loki rules as well. First two validators are: - `expressionIsValidLogQL` to check if the expression is a valid LogQL query diff --git a/README.md b/README.md index 16481c2..1e858a0 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ version validate [] ... - Validate Prometheus rule files using validation rules from config file. + Validate Prometheus rule files in YAML or jsonnet format using validation rules from config file(s). -d, --disable-rule=DISABLE-RULE ... Allows to disable any validation rules by it's name. Can be passed multiple times. @@ -98,6 +98,10 @@ validation-docs [] Format of the output. ``` +#### Jsonnet support +Promruval supports the default YAML format (`.yaml` or `.yml`) of rule files but also supports rules written in [Jsonnet](https://jsonnet.org/) (`.jsonnet`). +If will be rendered using the [go-jsonnet](https://github.com/google/go-jsonnet) library and then validated as usual, so you don't have to evaluate those by yourself before running the validation. + #### Configuration composition The `--config-file` flag can be passed multiple times. Promruval will append the additional validation rules from the diff --git a/go.mod b/go.mod index 7e48804..109ed61 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 github.com/bmatcuk/doublestar/v4 v4.6.1 github.com/creasty/defaults v1.7.0 + github.com/google/go-jsonnet v0.20.0 github.com/grafana/dskit v0.0.0-20240528015923-27d7d41066d3 github.com/prometheus/client_golang v1.19.1 github.com/prometheus/common v0.55.0 @@ -108,6 +109,7 @@ require ( k8s.io/client-go v0.30.2 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect ) require ( diff --git a/go.sum b/go.sum index 7599b36..ca815c5 100644 --- a/go.sum +++ b/go.sum @@ -179,6 +179,8 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-jsonnet v0.20.0 h1:WG4TTSARuV7bSm4PMB4ohjxe33IHT5WVTrJSU33uT4g= +github.com/google/go-jsonnet v0.20.0/go.mod h1:VbgWF9JX7ztlv770x/TolZNGGFfiHEVx9G6ca2eUmeA= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -402,6 +404,8 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUt github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sercand/kuberesolver/v5 v5.1.1 h1:CYH+d67G0sGBj7q5wLK61yzqJJ8gLLC8aeprPTHb6yY= github.com/sercand/kuberesolver/v5 v5.1.1/go.mod h1:Fs1KbKhVRnB2aDWN12NjKCB+RgYMWZJ294T3BtmVCpQ= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= @@ -697,3 +701,5 @@ k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1 k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/main.go b/main.go index e433f35..5a1e3d2 100644 --- a/main.go +++ b/main.go @@ -32,8 +32,8 @@ var ( versionCmd = app.Command("version", "Print version and build information.") - validateCmd = app.Command("validate", "Validate Prometheus rule files using validation rules from config file.") - filePaths = validateCmd.Arg("path", "File paths to be validated, can use even double star globs or ~. Will be expanded if not done by bash.").Required().Strings() + validateCmd = app.Command("validate", "Validate Prometheus rule files in YAML or jsonnet format using validation rules from config file(s).") + filePaths = validateCmd.Arg("path", "Rule file paths to be validated (.yaml, .yml or .jsonnet), can use even double star globs or ~. Will be expanded if not done by bash.").Required().Strings() disabledRules = validateCmd.Flag("disable-rule", "Allows to disable any validation rules by it's name. Can be passed multiple times.").Short('d').Strings() enabledRules = validateCmd.Flag("enable-rule", "Only enable these validation rules. Can be passed multiple times.").Short('e').Strings() validationOutputFormat = validateCmd.Flag("output", "Format of the output.").Short('o').PlaceHolder("[text,json,yaml]").Default("text").Enum("text", "json", "yaml") diff --git a/pkg/validate/validate.go b/pkg/validate/validate.go index 55295ec..443722b 100644 --- a/pkg/validate/validate.go +++ b/pkg/validate/validate.go @@ -15,6 +15,7 @@ import ( "github.com/fusakla/promruval/v2/pkg/unmarshaler" "github.com/fusakla/promruval/v2/pkg/validationrule" "github.com/fusakla/promruval/v2/pkg/validator" + "github.com/google/go-jsonnet" "github.com/prometheus/prometheus/model/rulefmt" log "github.com/sirupsen/logrus" "gopkg.in/yaml.v3" @@ -42,23 +43,38 @@ func Files(fileNames []string, validationRules []*validationrule.ValidationRule, for _, r := range validationRules { validationReport.ValidationRules = append(validationReport.ValidationRules, r) } + jsonnetVM := jsonnet.MakeVM() start := time.Now() fileCount := len(fileNames) for i, fileName := range fileNames { log.Infof("processing file %d/%d %s", i+1, fileCount, fileName) validationReport.FilesCount++ fileReport := validationReport.NewFileReport(fileName) - f, err := os.Open(fileName) - if err != nil { - validationReport.Failed = true - fileReport.Valid = false - fileReport.Errors = []error{fmt.Errorf("cannot read file %s: %w", fileName, err)} - continue + var yamlReader io.Reader + if strings.HasSuffix(fileName, ".yaml") || strings.HasSuffix(fileName, ".yml") { + var err error + yamlReader, err = os.Open(fileName) + if err != nil { + validationReport.Failed = true + fileReport.Valid = false + fileReport.Errors = []error{fmt.Errorf("cannot read file %s: %w", fileName, err)} + continue + } + } else if strings.HasSuffix(fileName, ".jsonnet") { + log.Debugf("evaluating jsonnet file %s", fileName) + jsonnetOutput, err := jsonnetVM.EvaluateFile(fileName) + if err != nil { + validationReport.Failed = true + fileReport.Valid = false + fileReport.Errors = []error{fmt.Errorf("cannot evaluate jsonnet file %s: %w", fileName, err)} + continue + } + yamlReader = strings.NewReader(jsonnetOutput) } var rf unmarshaler.RulesFileWithComment - decoder := yaml.NewDecoder(f) + decoder := yaml.NewDecoder(yamlReader) decoder.KnownFields(true) - err = decoder.Decode(&rf) + err := decoder.Decode(&rf) if err != nil { if errors.Is(err, io.EOF) { continue