diff --git a/CHANGELOG.md b/CHANGELOG.md index 7172abd..d1bfd8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,16 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -- Updated: Prometheus and other dependencies -- CI: Updated Github actions for golangcilint and goreleaser - Fixed: :warning: **Unmarshalling of the rule files is strict again**, this behavior was unintentionally brought when adding support for yaml comments. +- Changed: :warning: **Renamed `hasValidPartialStrategy` to `hasValidPartialResponseStrategy`** as it was documented so it is actually a fix +- Changed: :warning: **Disallow special rule file fields of Thanos, Mimir or Loki by default** + To enable them, you need to set some of the new flags described below +- 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 + - `logQlExpressionUsesRangeAggregation` to check if the LogQL expression uses range aggregation - Added: support for alert field `keep_firing_for` - Added: support for the `query_offset` field in the rule group - Added: new validator `expressionIsValidPromQL` to check if the expression is a valid PromQL query -- Added: :tada: **Support for 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 - - `logQlExpressionUsesRangeAggregation` to check if the LogQL expression uses range aggregation -- Changed: :warning: **Renamed `hasValidPartialStrategy` to `hasValidPartialResponseStrategy` as it was documented so it is actually a fix** +- Updated: Prometheus and other dependencies +- CI: Updated Github actions for golangcilint and goreleaser ## [2.14.1] - Fixed: error message in the `hasSourceTenantsForMetrics` validator diff --git a/Makefile b/Makefile index 9618318..e4a5603 100644 --- a/Makefile +++ b/Makefile @@ -34,18 +34,23 @@ build: GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o $(PROMRUVAL_BIN) E2E_TESTS_VALIDATIONS_FILE := examples/validation.yaml -E2E_TESTS_LOKI_VALIDATIONS_FILE := examples/validation_loki.yaml E2E_TESTS_ADDITIONAL_VALIDATIONS_FILE := examples/additional-validation.yaml -E2E_TESTS_LOKI_RULES_FILES := examples/loki_rules/*.yaml E2E_TESTS_RULES_FILES := examples/rules/*.yaml E2E_TESTS_DOCS_FILE_MD := examples/human_readable.md E2E_TESTS_DOCS_FILE_HTML := examples/human_readable.html + +E2E_TESTS_LOKI_DIR := examples/loki/ +E2E_TESTS_MIMIR_DIR := examples/mimir/ +E2E_TESTS_THANOS_DIR := examples/thanos/ e2e-test: build $(PROMRUVAL_BIN) validate --config-file $(E2E_TESTS_VALIDATIONS_FILE) --config-file $(E2E_TESTS_ADDITIONAL_VALIDATIONS_FILE) $(E2E_TESTS_RULES_FILES) - $(PROMRUVAL_BIN) validate --config-file $(E2E_TESTS_LOKI_VALIDATIONS_FILE) $(E2E_TESTS_LOKI_RULES_FILES) $(PROMRUVAL_BIN) validation-docs --config-file $(E2E_TESTS_VALIDATIONS_FILE) --config-file $(E2E_TESTS_ADDITIONAL_VALIDATIONS_FILE) > $(E2E_TESTS_DOCS_FILE_MD) $(PROMRUVAL_BIN) validation-docs --config-file $(E2E_TESTS_VALIDATIONS_FILE) --config-file $(E2E_TESTS_ADDITIONAL_VALIDATIONS_FILE) --output=html > $(E2E_TESTS_DOCS_FILE_HTML) + $(PROMRUVAL_BIN) validate --support-loki --config-file $(E2E_TESTS_LOKI_DIR)/validation.yaml $(E2E_TESTS_LOKI_DIR)/rules.yaml + $(PROMRUVAL_BIN) validate --support-thanos --config-file $(E2E_TESTS_THANOS_DIR)/validation.yaml $(E2E_TESTS_THANOS_DIR)/rules.yaml + $(PROMRUVAL_BIN) validate --support-mimir --config-file $(E2E_TESTS_MIMIR_DIR)/validation.yaml $(E2E_TESTS_MIMIR_DIR)/rules.yaml + docker: build docker build -t fusakla/promruval . diff --git a/README.md b/README.md index 413a8d7..1644748 100644 --- a/README.md +++ b/README.md @@ -59,26 +59,26 @@ make build ```bash $ ./promruval --help-long -usage: promruval --config-file=CONFIG-FILE [] [ ...] +usage: promruval [] [ ...] Prometheus rules validation tool. Flags: - --help Show context-sensitive help (also try --help-long and --help-man). + --[no-]help Show context-sensitive help (also try --help-long and --help-man). -c, --config-file=CONFIG-FILE ... - Path to validation config file. Can be passed multiple times, only validationRules will be reflected from the additional configs. - --debug Enable debug logging. + Path to validation config file. Can be passed multiple times, only validationRules will be reflected from the additional configs. + --[no-]debug Enable debug logging. Commands: - help [...] +help [...] Show help. - version +version Print version and build information. - validate [] ... +validate [] ... Validate Prometheus rule files using validation rules from config file. -d, --disable-rule=DISABLE-RULE ... @@ -86,9 +86,12 @@ Commands: -e, --enable-rule=ENABLE-RULE ... Only enable these validation rules. Can be passed multiple times. -o, --output=[text,json,yaml] Format of the output. - --color Use color output. + --[no-]color Use color output. + --[no-]support-loki Support Loki rules format. + --[no-]support-mimir Support Mimir rules format. + --[no-]support-thanos Support Thanos rules format. - validation-docs [] +validation-docs [] Print human readable form of the validation rules from config file. -o, --output=[text,markdown,html] @@ -299,14 +302,18 @@ groups: ### Other monitoring solutions support #### Thanos -Thanos has only one special case which is the `partial_response_strategy` setting on the group level which is tolerated -in the config and can ve validated using the [`hasValidPartialResponseStrategy`](./docs/validations.md#hasvalidpartialresponsestrategy) validation. +If you want to validate Thanos rules, use the `promruval validate --support-thanos` flag, otherwise you might get errors on unknown fields such as `partial_response_strategy`. -#### Mimir/Cortex -Mimir/Cortex has only one special case which is the `source_tenants` setting on the group level which is tolerated -and can ve validated using the [`hasSourceTenantsForMetrics`](./docs/validations.md#hassourcetenantsformetrics) or [`hasAllowedSourceTenants`](./docs/validations.md#hasallowedsourcetenants) validations for example. +You can validate it using the [`hasValidPartialResponseStrategy`](./docs/validations.md#hasvalidpartialresponsestrategy) validation. + +#### Mimir +If you want to validate Mimir rules, use the `promruval validate --support-mimir` flag, otherwise you might get errors on unknown fields such as `source_tenants`. + +The `source_tenants` can be validated using the [`hasSourceTenantsForMetrics`](./docs/validations.md#hassourcetenantsformetrics) or [`hasAllowedSourceTenants`](./docs/validations.md#hasallowedsourcetenants) validations for example. #### Loki +If you want to validate Mimir rules, use the `promruval validate --support-loki` flag, otherwise you might get errors on unknown fields such as `namespace` or `remote_write`. + Since Loki has almost identical rule config as Prometheus, you can use the same validations for Loki rules. Loki has special validations for its expressions since it uses different query language [LogQL](https://grafana.com/docs/loki/latest/query/). To see the LogQL specific validations see the [here](./docs/validations.md#logql-expression-validators). diff --git a/examples/human_readable.html b/examples/human_readable.html index 681f54b..5c233ce 100644 --- a/examples/human_readable.html +++ b/examples/human_readable.html @@ -36,14 +36,6 @@

check-prometheus-limitations

All rules does not use any of the cluster,locality,prometheus-type,replica labels is in its expression -

check-source-tenants

-
    -
  • All rules rule group, the rule belongs to, has the required source_tenants configured, according to the mapping of metric names to tenants: -
    k8s: ^container_.*$ (Metrics from cAdvisor) -
    k8s: ^kube_.*$ (Metrics from KSM) -
    mysql: ^mysql_.*$ (MySQL metrics from the MySQL team)
  • -
-

check-metric-name

  • Alert expression uses metric name in selectors
  • @@ -53,9 +45,7 @@

    check-metric-name

    check-groups

      -
    • Group does not have other source_tenants than: tenant1, tenant2, k8s
    • Group evaluation interval is between 20s and 106751d23h47m16s854ms if set
    • -
    • Group has valid partial_response_strategy (one of warn or abort) if set
    • Group has at most 10 rules
    • Group does not have higher limit configured then 100
    diff --git a/examples/human_readable.md b/examples/human_readable.md index 65b4542..4034a8a 100644 --- a/examples/human_readable.md +++ b/examples/human_readable.md @@ -26,21 +26,13 @@ Validation rules: - All rules expression does not use data older than `6h0m0s` - All rules does not use any of the `cluster`,`locality`,`prometheus-type`,`replica` labels is in its expression - check-source-tenants - - All rules rule group, the rule belongs to, has the required `source_tenants` configured, according to the mapping of metric names to tenants: - `k8s`: `^container_.*$` (Metrics from cAdvisor) - `k8s`: `^kube_.*$` (Metrics from KSM) - `mysql`: `^mysql_.*$` (MySQL metrics from the MySQL team) - check-metric-name - Alert expression uses metric name in selectors - Alert labels are valid templates - Alert `keep_firing_for` is not longer than `1h` check-groups - - Group does not have other `source_tenants` than: `tenant1`, `tenant2`, `k8s` - Group evaluation interval is between `20s` and `106751d23h47m16s854ms` if set - - Group has valid partial_response_strategy (one of `warn` or `abort`) if set - Group has at most 10 rules - Group does not have higher `limit` configured then 100 diff --git a/examples/loki/rules.yaml b/examples/loki/rules.yaml new file mode 100644 index 0000000..4bf8603 --- /dev/null +++ b/examples/loki/rules.yaml @@ -0,0 +1,6 @@ +# ignore_validations: hasAllowedLimit +namespace: foo +groups: + - name: group1 + remote_write: + - url: http://localhost:1234 diff --git a/examples/validation_loki.yaml b/examples/loki/validation.yaml similarity index 100% rename from examples/validation_loki.yaml rename to examples/loki/validation.yaml diff --git a/examples/loki_rules/rules.yaml b/examples/loki_rules/rules.yaml deleted file mode 100644 index b05921d..0000000 --- a/examples/loki_rules/rules.yaml +++ /dev/null @@ -1,14 +0,0 @@ -# ignore_validations: hasAllowedLimit -namespace: foo -groups: - - name: group1 - remote_write: - - url: http://localhost:1234 - interval: 1m - limit: 10 - rules: - # foo bar - - record: recorded_metrics - expr: sum(rate({foo="bar"}[5m])) > 5 - labels: - foo: bar diff --git a/examples/mimir/rules.yaml b/examples/mimir/rules.yaml new file mode 100644 index 0000000..6039d96 --- /dev/null +++ b/examples/mimir/rules.yaml @@ -0,0 +1,11 @@ +groups: + - name: group1 + source_tenants: ["k8s", "bar"] + rules: + - alert: test + expr: avg_over_time(max_over_time(container_cpu_seconds_total{job="prometheus"}[10h] offset 10d)[10m:10m]) + for: 4w + keep_firing_for: 5m + labels: + severity: critica + team: foo diff --git a/examples/mimir/validation.yaml b/examples/mimir/validation.yaml new file mode 100644 index 0000000..1178183 --- /dev/null +++ b/examples/mimir/validation.yaml @@ -0,0 +1,16 @@ +validationRules: + - name: check-mimir + scope: Group + validations: + - type: hasAllowedSourceTenants + params: + allowedSourceTenants: ["k8s", "bar"] + - name: check-source-tenants + scope: All rules + validations: + - type: hasSourceTenantsForMetrics + params: + sourceTenants: + "k8s": + - regexp: "container_.*" + description: "Metrics from cAdvisor" diff --git a/examples/rules/rules.yaml b/examples/rules/rules.yaml index 3c2ccde..6ba36aa 100644 --- a/examples/rules/rules.yaml +++ b/examples/rules/rules.yaml @@ -1,7 +1,6 @@ # ignore_validations: hasAllowedLimit groups: - name: group1 - partial_response_strategy: abort interval: 1m limit: 10 rules: @@ -13,8 +12,6 @@ groups: # ignore_validations: labelHasAllowedValue - name: testGroup - partial_response_strategy: "warn" - source_tenants: ["tenant1", "tenant2"] limit: 1000 rules: # Comment before. @@ -35,7 +32,6 @@ groups: disabled_validation_rules: check-team-label,check-prometheus-limitations - name: testIgnoreValidationsInExpr - source_tenants: ["k8s"] limit: 10 rules: - alert: test diff --git a/examples/thanos/rules.yaml b/examples/thanos/rules.yaml new file mode 100644 index 0000000..e4eec8d --- /dev/null +++ b/examples/thanos/rules.yaml @@ -0,0 +1,3 @@ +groups: + - name: group1 + partial_response_strategy: "warn" diff --git a/examples/thanos/validation.yaml b/examples/thanos/validation.yaml new file mode 100644 index 0000000..50a600a --- /dev/null +++ b/examples/thanos/validation.yaml @@ -0,0 +1,5 @@ +validationRules: + - name: check-thanos-rules + scope: Group + validations: + - type: hasValidPartialResponseStrategy diff --git a/examples/validation.yaml b/examples/validation.yaml index 741222e..3c00f4e 100644 --- a/examples/validation.yaml +++ b/examples/validation.yaml @@ -71,21 +71,6 @@ validationRules: params: labels: ["cluster", "locality", "prometheus-type", "replica"] - - name: check-source-tenants - scope: All rules - validations: - - type: hasSourceTenantsForMetrics - params: - sourceTenants: - "k8s": - - regexp: "container_.*" - description: "Metrics from cAdvisor" - - regexp: "kube_.*" - description: "Metrics from KSM" - "mysql": - - regexp: "mysql_.*" - description: "MySQL metrics from the MySQL team" - - name: check-metric-name scope: Alert validations: @@ -98,14 +83,10 @@ validationRules: - name: check-groups scope: Group validations: - - type: hasAllowedSourceTenants - params: - allowedSourceTenants: ["tenant1", "tenant2", "k8s"] - type: hasAllowedEvaluationInterval params: minimum: "20s" intervalMustBeSet: false - - type: hasValidPartialResponseStrategy - type: maxRulesPerGroup params: limit: 10 diff --git a/go.mod b/go.mod index f930159..c9f087d 100644 --- a/go.mod +++ b/go.mod @@ -126,7 +126,7 @@ require ( github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-cmp v0.6.0 github.com/grafana/loki/v3 v3.1.0 github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect github.com/json-iterator/go v1.1.12 // indirect diff --git a/main.go b/main.go index 3125ea2..5ea5b6f 100644 --- a/main.go +++ b/main.go @@ -10,6 +10,7 @@ import ( "github.com/fusakla/promruval/v2/pkg/config" "github.com/fusakla/promruval/v2/pkg/prometheus" "github.com/fusakla/promruval/v2/pkg/report" + "github.com/fusakla/promruval/v2/pkg/unmarshaler" "github.com/fusakla/promruval/v2/pkg/validate" "github.com/fusakla/promruval/v2/pkg/validationrule" "github.com/fusakla/promruval/v2/pkg/validator" @@ -35,6 +36,9 @@ var ( 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") color = validateCmd.Flag("color", "Use color output.").Bool() + supportLoki = validateCmd.Flag("support-loki", "Support Loki rules format.").Bool() + supportMimir = validateCmd.Flag("support-mimir", "Support Mimir rules format.").Bool() + supportThanos = validateCmd.Flag("support-thanos", "Support Thanos rules format.").Bool() docsCmd = app.Command("validation-docs", "Print human readable form of the validation rules from config file.") docsOutputFormat = docsCmd.Flag("output", "Format of the output.").Short('o').PlaceHolder("[text,markdown,html]").Default("text").Enum("text", "markdown", "html") @@ -147,6 +151,18 @@ func main() { filesToBeValidated = append(filesToBeValidated, paths...) } + if *supportLoki { + unmarshaler.SupportLoki(true) + } + + if *supportMimir { + unmarshaler.SupportMimir(true) + } + + if *supportThanos { + unmarshaler.SupportThanos(true) + } + var prometheusClient *prometheus.Client if mainValidationConfig.Prometheus.URL != "" { prometheusClient, err = prometheus.NewClient(mainValidationConfig.Prometheus) diff --git a/pkg/unmarshaler/helpers.go b/pkg/unmarshaler/helpers.go index dad690a..5640cd4 100644 --- a/pkg/unmarshaler/helpers.go +++ b/pkg/unmarshaler/helpers.go @@ -77,7 +77,7 @@ func unmarshalToNodeAndStruct(value, dstNode *yaml.Node, dstStruct interface{}, } // mustListStructYamlFieldNames returns a list of yaml field names for the given struct. -func mustListStructYamlFieldNames(s interface{}) []string { +func mustListStructYamlFieldNames(s interface{}, ignoreFields []string) []string { y, err := yaml.Marshal(s) if err != nil { fmt.Println("failed to marshal", err) @@ -88,8 +88,11 @@ func mustListStructYamlFieldNames(s interface{}) []string { fmt.Println("failed to marshal", err) panic(err) } - names := make([]string, 0, len(m)) + names := []string{} for k := range m { + if slices.Contains(ignoreFields, k) { + continue + } names = append(names, k) } return names diff --git a/pkg/unmarshaler/unmarshaler.go b/pkg/unmarshaler/unmarshaler.go index b0c38cd..eaff948 100644 --- a/pkg/unmarshaler/unmarshaler.go +++ b/pkg/unmarshaler/unmarshaler.go @@ -11,13 +11,23 @@ import ( ) var ( - // Struct fields marked as omitempty MUST be set to non-default value so they appear in marshalled yaml. - rulesFileKnownFields = mustListStructYamlFieldNames(RulesFile{}) - groupsWithCommentKnownFields = mustListStructYamlFieldNames(GroupsWithComment{}) - ruleGroupKnownFields = mustListStructYamlFieldNames(RuleGroup{}) - ruleNodeKnownFields = mustListStructYamlFieldNames(rulefmt.RuleNode{Record: yaml.Node{Kind: yaml.SequenceNode}, Alert: yaml.Node{Kind: yaml.SequenceNode}, For: model.Duration(1), Labels: map[string]string{"foo": "bar"}, Annotations: map[string]string{"foo": "bar"}, KeepFiringFor: model.Duration(1)}) + supportLoki = false + supportMimir = false + supportThanos = false ) +func SupportLoki(support bool) { + supportLoki = support +} + +func SupportMimir(support bool) { + supportMimir = support +} + +func SupportThanos(support bool) { + supportThanos = support +} + type RulesFile struct { Groups GroupsWithComment `yaml:"groups"` // Just so we can unmarshal also PromQL test files but ignore them because it has no Groups @@ -29,6 +39,14 @@ type RulesFile struct { Namespace string `yaml:"namespace"` } +func (r *RulesFile) knownFields() []string { + ignoredFileds := []string{} + if !supportLoki { + ignoredFileds = append(ignoredFileds, "namespace") + } + return mustListStructYamlFieldNames(r, ignoredFileds) +} + type RulesFileWithComment struct { node yaml.Node groupsComments []string @@ -41,7 +59,7 @@ func (r *RulesFileWithComment) UnmarshalYAML(value *yaml.Node) error { r.groupsComments = strings.Split(field.HeadComment, "\n") } } - return unmarshalToNodeAndStruct(value, &r.node, &r.RulesFile, rulesFileKnownFields) + return unmarshalToNodeAndStruct(value, &r.node, &r.RulesFile, r.RulesFile.knownFields()) } func (r *RulesFileWithComment) DisabledValidators(commentPrefix string) []string { @@ -54,7 +72,7 @@ type GroupsWithComment struct { } func (g *GroupsWithComment) UnmarshalYAML(value *yaml.Node) error { - return unmarshalToNodeAndStruct(value, &g.node, &g.Groups, groupsWithCommentKnownFields) + return unmarshalToNodeAndStruct(value, &g.node, &g.Groups, mustListStructYamlFieldNames(g, []string{})) } func (g *GroupsWithComment) DisabledValidators(commentPrefix string) []string { @@ -76,13 +94,27 @@ type RuleGroup struct { RWConfigs []loki.RemoteWriteConfig `yaml:"remote_write"` } +func (r *RuleGroup) knownFields() []string { + ignoredFileds := []string{} + if !supportLoki { + ignoredFileds = append(ignoredFileds, "remote_write") + } + if !supportThanos { + ignoredFileds = append(ignoredFileds, "partial_response_strategy") + } + if !supportMimir { + ignoredFileds = append(ignoredFileds, "source_tenants") + } + return mustListStructYamlFieldNames(r, ignoredFileds) +} + type RuleGroupWithComment struct { node yaml.Node RuleGroup } func (r *RuleGroupWithComment) UnmarshalYAML(value *yaml.Node) error { - return unmarshalToNodeAndStruct(value, &r.node, &r.RuleGroup, ruleGroupKnownFields) + return unmarshalToNodeAndStruct(value, &r.node, &r.RuleGroup, r.RuleGroup.knownFields()) } func (r *RuleGroupWithComment) DisabledValidators(commentPrefix string) []string { @@ -94,6 +126,11 @@ type RuleWithComment struct { rule rulefmt.RuleNode } +func (r *RuleWithComment) knownFields() []string { + // Struct fields marked as omitempty MUST be set to non-default value so they appear in marshalled yaml. + return mustListStructYamlFieldNames(rulefmt.RuleNode{Record: yaml.Node{Kind: yaml.SequenceNode}, Alert: yaml.Node{Kind: yaml.SequenceNode}, For: model.Duration(1), Labels: map[string]string{"foo": "bar"}, Annotations: map[string]string{"foo": "bar"}, KeepFiringFor: model.Duration(1)}, []string{}) +} + func (r *RuleWithComment) OriginalRule() rulefmt.Rule { return rulefmt.Rule{ Record: r.rule.Record.Value, @@ -107,7 +144,7 @@ func (r *RuleWithComment) OriginalRule() rulefmt.Rule { } func (r *RuleWithComment) UnmarshalYAML(value *yaml.Node) error { - return unmarshalToNodeAndStruct(value, &r.node, &r.rule, ruleNodeKnownFields) + return unmarshalToNodeAndStruct(value, &r.node, &r.rule, r.knownFields()) } func (r *RuleWithComment) DisabledValidators(commentPrefix string) []string { diff --git a/pkg/unmarshaler/unmarshaler_test.go b/pkg/unmarshaler/unmarshaler_test.go new file mode 100644 index 0000000..a5ef4b9 --- /dev/null +++ b/pkg/unmarshaler/unmarshaler_test.go @@ -0,0 +1,279 @@ +package unmarshaler + +import ( + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + loki "github.com/grafana/loki/v3/pkg/tool/rules/rwrulefmt" + "github.com/prometheus/common/model" + "github.com/prometheus/prometheus/model/rulefmt" + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" +) + +func TestUnmarshalling(t *testing.T) { + type testCase struct { + name string + input string + beforeExecute func() + afterExecute func() + expected RulesFileWithComment + error bool + } + + testCases := []testCase{ + { + name: "valid rules file with rule and alert", + input: ` +groups: + - name: group1 + interval: 10s + query_offset: 5s + rules: + - alert: alert1 + expr: expr1 + for: 10m + keep_firing_for: 1h + labels: + foo: bar + annotations: + foo: bar + - name: group2 + rules: + - record: record1 + expr: expr1 + labels: + foo: bar +`, + expected: RulesFileWithComment{ + RulesFile: RulesFile{ + Groups: GroupsWithComment{ + Groups: []RuleGroupWithComment{ + { + RuleGroup: RuleGroup{ + Name: "group1", + Interval: model.Duration(time.Second * 10), + QueryOffset: model.Duration(time.Second * 5), + Rules: []RuleWithComment{ + { + rule: rulefmt.RuleNode{ + Alert: yaml.Node{Kind: yaml.ScalarNode, Value: "alert1"}, + Expr: yaml.Node{Kind: yaml.ScalarNode, Value: "expr1"}, + For: model.Duration(time.Minute * 10), + Labels: map[string]string{"foo": "bar"}, + Annotations: map[string]string{"foo": "bar"}, + KeepFiringFor: model.Duration(time.Hour), + }, + }, + }, + }, + }, + { + RuleGroup: RuleGroup{ + Name: "group2", + Rules: []RuleWithComment{ + { + rule: rulefmt.RuleNode{ + Record: yaml.Node{Kind: yaml.ScalarNode, Value: "record1"}, + Expr: yaml.Node{Kind: yaml.ScalarNode, Value: "expr1"}, + Labels: map[string]string{"foo": "bar"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + error: false, + }, + + { + name: "successfully load rule test file", + input: ` +rule_files: [] +evaluation_interval: 1m +group_eval_order: ??? +tests: [] +`, + expected: RulesFileWithComment{ + RulesFile: RulesFile{ + RuleFiles: []interface{}{}, + EvaluationInterval: "1m", + GroupEvalOrder: "???", + Tests: []interface{}{}, + }, + }, + error: false, + }, + + // =================== THANOS ===================== + { + name: "thanos disallowed fields: partial_response_strategy", + input: ` +groups: + - name: group1 + partial_response_strategy: warn + rules: + - alert: alert1 + expr: expr1 +`, + error: true, + }, + { + name: "thanos invalid partial strategy", + input: ` +groups: + - name: group1 + partial_response_strategy: foo +`, + error: true, + }, + { + name: "thanos allowed fields", + beforeExecute: func() { SupportThanos(true) }, + afterExecute: func() { SupportThanos(false) }, + input: ` +groups: + - name: group1 + partial_response_strategy: warn +`, + expected: RulesFileWithComment{ + RulesFile: RulesFile{ + Groups: GroupsWithComment{ + Groups: []RuleGroupWithComment{ + { + RuleGroup: RuleGroup{ + Name: "group1", + PartialResponseStrategy: "warn", + }, + }, + }, + }, + }, + }, + error: false, + }, + + // =================== LOKI ===================== + { + name: "loki disallowed fields: namespace", + input: ` +namespace: foo +groups: [] +`, + error: true, + }, + { + name: "loki disallowed fields: remote_write", + input: ` +groups: + - name: group1 + remote_write: + - url: http://localhost:3100/loki/api/v1/push +`, + error: true, + }, + { + name: "loki allowed fields", + beforeExecute: func() { SupportLoki(true) }, + afterExecute: func() { SupportLoki(false) }, + input: ` +namespace: foo +groups: + - name: group1 + remote_write: + - url: http://localhost:3100/loki/api/v1/push +`, + expected: RulesFileWithComment{ + RulesFile: RulesFile{ + Namespace: "foo", + Groups: GroupsWithComment{ + Groups: []RuleGroupWithComment{ + { + RuleGroup: RuleGroup{ + Name: "group1", + RWConfigs: []loki.RemoteWriteConfig{ + { + URL: "http://localhost:3100/loki/api/v1/push", + }, + }, + }, + }, + }, + }, + }, + }, + error: false, + }, + + // =================== Mimir ===================== + { + name: "mimir disallowed fields: source_tenants", + input: ` +groups: + - name: group1 + source_tenants: ["tenant1"] +`, + error: true, + }, + { + name: "mimir invalid source_tenants", + input: ` +groups: + - name: group1 + source_tenants: foo +`, + error: true, + }, + { + name: "mimir allowed fields", + beforeExecute: func() { SupportMimir(true) }, + afterExecute: func() { SupportMimir(false) }, + input: ` +groups: + - name: group1 + source_tenants: ["tenant1", "tenant2"] +`, + expected: RulesFileWithComment{ + RulesFile: RulesFile{ + Groups: GroupsWithComment{ + Groups: []RuleGroupWithComment{ + { + RuleGroup: RuleGroup{ + Name: "group1", + SourceTenants: []string{"tenant1", "tenant2"}, + }, + }, + }, + }, + }, + }, + error: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if tc.beforeExecute != nil { + tc.beforeExecute() + } + var actual RulesFileWithComment + err := yaml.Unmarshal([]byte(tc.input), &actual) + if tc.afterExecute != nil { + tc.afterExecute() + } + if tc.error { + assert.Error(t, err) + } else { + assert.NoError(t, err) + if diff := cmp.Diff(tc.expected, actual, cmpopts.IgnoreUnexported(RulesFileWithComment{}, GroupsWithComment{}, RuleGroupWithComment{}, RuleWithComment{})); diff != "" { + t.Errorf("Diff in unmarshalled struct: mismatch (-want +got):\n%s", diff) + } + } + }) + } +} diff --git a/promruval.png b/promruval.png deleted file mode 100644 index 33136f3..0000000 Binary files a/promruval.png and /dev/null differ