Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: strict unmarshalling #71

Merged
merged 3 commits into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- 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.
- Added: support for alert field `keep_firing_for`
- Added: support for the `query_offset` field in the rule group

## [2.14.1]
- Fixed: error message in the `hasSourceTenantsForMetrics` validator
Expand Down
36 changes: 35 additions & 1 deletion pkg/unmarshaler/helpers.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package unmarshaler

import (
"fmt"
"slices"
"strings"

"gopkg.in/yaml.v3"
Expand Down Expand Up @@ -49,7 +51,20 @@ func disabledValidatorsFromComments(comments []string, commentPrefix string) []s
return disabledValidators
}

func unmarshalToNodeAndStruct(value, dstNode *yaml.Node, dstStruct interface{}) error {
func unmarshalToNodeAndStruct(value, dstNode *yaml.Node, dstStruct interface{}, knownFields []string) error {
// Since yaml/v3 Node.Decode doesn't support setting decode options like KnownFields (see https://github.com/go-yaml/yaml/issues/460)
// we need to check the fields manually, thus the function requires a list of known fields.
if value.Kind == yaml.MappingNode {
m := map[string]any{}
if err := value.Decode(m); err != nil {
return err
}
for k := range m {
if !slices.Contains(knownFields, k) {
return fmt.Errorf("unknown field %q when unmarshalling the %T, only supported fields are: %s", k, dstStruct, strings.Join(knownFields, ","))
}
}
}
err := value.Decode(dstNode)
if err != nil {
return err
Expand All @@ -60,3 +75,22 @@ func unmarshalToNodeAndStruct(value, dstNode *yaml.Node, dstStruct interface{})
}
return nil
}

// mustListStructYamlFieldNames returns a list of yaml field names for the given struct.
func mustListStructYamlFieldNames(s interface{}) []string {
y, err := yaml.Marshal(s)
if err != nil {
fmt.Println("failed to marshal", err)
panic(err)
}
m := map[string]any{}
if err := yaml.Unmarshal(y, m); err != nil {
fmt.Println("failed to marshal", err)
panic(err)
}
names := make([]string, 0, len(m))
for k := range m {
names = append(names, k)
}
return names
}
53 changes: 30 additions & 23 deletions pkg/unmarshaler/unmarshaler.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,21 @@ import (
"gopkg.in/yaml.v3"
)

type fakeTestFile struct {
RuleFiles []yaml.Node `yaml:"rule_files,omitempty"`
EvaluationInterval yaml.Node `yaml:"evaluation_interval,omitempty"`
GroupEvalOrder []yaml.Node `yaml:"group_eval_order,omitempty"`
Tests []yaml.Node `yaml:"tests,omitempty"`
}
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)})
)

type RulesFile struct {
Groups GroupsWithComment `yaml:"groups"`
fakeTestFile // Just so we can unmarshal also PromQL test files but ignore them because it has no Groups
Groups GroupsWithComment `yaml:"groups"`
// Just so we can unmarshal also PromQL test files but ignore them because it has no Groups
RuleFiles interface{} `yaml:"rule_files"`
EvaluationInterval interface{} `yaml:"evaluation_interval"`
GroupEvalOrder interface{} `yaml:"group_eval_order"`
Tests interface{} `yaml:"tests"`
}

type RulesFileWithComment struct {
Expand All @@ -33,7 +38,7 @@ func (r *RulesFileWithComment) UnmarshalYAML(value *yaml.Node) error {
r.groupsComments = strings.Split(field.HeadComment, "\n")
}
}
return unmarshalToNodeAndStruct(value, &r.node, &r.RulesFile)
return unmarshalToNodeAndStruct(value, &r.node, &r.RulesFile, rulesFileKnownFields)
}

func (r *RulesFileWithComment) DisabledValidators(commentPrefix string) []string {
Expand All @@ -42,11 +47,11 @@ func (r *RulesFileWithComment) DisabledValidators(commentPrefix string) []string

type GroupsWithComment struct {
node yaml.Node
Groups []RuleGroupWithComment
Groups []RuleGroupWithComment `yaml:"groups"`
}

func (g *GroupsWithComment) UnmarshalYAML(value *yaml.Node) error {
return unmarshalToNodeAndStruct(value, &g.node, &g.Groups)
return unmarshalToNodeAndStruct(value, &g.node, &g.Groups, groupsWithCommentKnownFields)
}

func (g *GroupsWithComment) DisabledValidators(commentPrefix string) []string {
Expand All @@ -55,11 +60,12 @@ func (g *GroupsWithComment) DisabledValidators(commentPrefix string) []string {

type RuleGroup struct {
Name string `yaml:"name"`
Interval model.Duration `yaml:"interval,omitempty"`
PartialResponseStrategy string `yaml:"partial_response_strategy,omitempty"`
SourceTenants []string `yaml:"source_tenants,omitempty"`
Interval model.Duration `yaml:"interval"`
QueryOffset model.Duration `yaml:"query_offset"`
PartialResponseStrategy string `yaml:"partial_response_strategy"` // Thanos only
SourceTenants []string `yaml:"source_tenants"` // Cortex/Mimir only
Rules []RuleWithComment `yaml:"rules"`
Limit int `yaml:"limit,omitempty"`
Limit int `yaml:"limit"`
}

type RuleGroupWithComment struct {
Expand All @@ -68,7 +74,7 @@ type RuleGroupWithComment struct {
}

func (r *RuleGroupWithComment) UnmarshalYAML(value *yaml.Node) error {
return unmarshalToNodeAndStruct(value, &r.node, &r.RuleGroup)
return unmarshalToNodeAndStruct(value, &r.node, &r.RuleGroup, ruleGroupKnownFields)
}

func (r *RuleGroupWithComment) DisabledValidators(commentPrefix string) []string {
Expand All @@ -82,17 +88,18 @@ type RuleWithComment struct {

func (r *RuleWithComment) OriginalRule() rulefmt.Rule {
return rulefmt.Rule{
Record: r.rule.Record.Value,
Alert: r.rule.Alert.Value,
Expr: r.rule.Expr.Value,
For: r.rule.For,
Labels: r.rule.Labels,
Annotations: r.rule.Annotations,
Record: r.rule.Record.Value,
Alert: r.rule.Alert.Value,
Expr: r.rule.Expr.Value,
For: r.rule.For,
Labels: r.rule.Labels,
Annotations: r.rule.Annotations,
KeepFiringFor: r.rule.KeepFiringFor,
}
}

func (r *RuleWithComment) UnmarshalYAML(value *yaml.Node) error {
return unmarshalToNodeAndStruct(value, &r.node, &r.rule)
return unmarshalToNodeAndStruct(value, &r.node, &r.rule, ruleNodeKnownFields)
}

func (r *RuleWithComment) DisabledValidators(commentPrefix string) []string {
Expand Down