Skip to content

Commit

Permalink
Improve yaml empty node validation by inspecting the ast directly ins…
Browse files Browse the repository at this point in the history
…tead of checking the struct after unmarshaling

Signed-off-by: Joe Kralicky <[email protected]>
  • Loading branch information
kralicky committed Aug 23, 2023
1 parent 09f593c commit 78b0b56
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 33 deletions.
42 changes: 42 additions & 0 deletions cmd/cortex/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/version"
"gopkg.in/yaml.v2"
yamlv3 "gopkg.in/yaml.v3"

"github.com/cortexproject/cortex/pkg/cortex"
"github.com/cortexproject/cortex/pkg/tracing"
Expand Down Expand Up @@ -250,6 +251,10 @@ func LoadConfig(filename string, expandENV bool, cfg *cortex.Config) error {
return errors.Wrap(err, "Error parsing config file")
}

if err := validateYAMLEmptyNodes(buf); err != nil {
return err
}

return nil
}

Expand All @@ -262,6 +267,43 @@ func DumpYaml(cfg *cortex.Config) {
}
}

// validateYAMLEmptyNodes ensure that no empty node has been specified in the YAML config file.
// When an empty node is defined in YAML, the YAML parser sets the whole struct to its zero value
// and so we loose all default values. It's very difficult to detect this case for the user, so we
// try to prevent it (on the root level) with this custom validation.
func validateYAMLEmptyNodes(buf []byte) error {
var document yamlv3.Node
if err := yamlv3.Unmarshal(buf, &document); err != nil {
return err
}
if document.Kind != yamlv3.DocumentNode {
return errors.New("invalid YAML document")
}
for _, node := range document.Content {
if node.Kind != yamlv3.MappingNode || len(node.Content) < 2 || node.Content[0].Kind != yamlv3.ScalarNode {
continue
}
foundEmptyNode := false
switch node.Content[1].Kind {
case yamlv3.MappingNode:
if len(node.Content[1].Content) == 0 {
// "key: {}"
foundEmptyNode = true
}
case yamlv3.ScalarNode:
if node.Content[1].Tag == "!!null" {
// "key: null" or "key:"
foundEmptyNode = true
}
}
if foundEmptyNode {
return fmt.Errorf("the %s configuration in YAML has been specified as an empty YAML node", node.Content[0].Value)
}
return nil
}
return nil
}

// expandEnv replaces ${var} or $var in config according to the values of the current environment variables.
// The replacement is case-sensitive. References to undefined variables are replaced by the empty string.
// A default value can be given by using the form ${var:default value}.
Expand Down
17 changes: 16 additions & 1 deletion cmd/cortex/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,22 @@ func TestFlagParsing(t *testing.T) {

"root level configuration option specified as an empty node in YAML": {
yaml: "querier:",
stderrMessage: "the Querier configuration in YAML has been specified as an empty YAML node",
stderrMessage: "the querier configuration in YAML has been specified as an empty YAML node",
},

"root level configuration option specified as an empty object node in YAML": {
yaml: "querier: {}",
stderrMessage: "the querier configuration in YAML has been specified as an empty YAML node",
},

"root level configuration option specified as a null node in YAML": {
yaml: "querier: null",
stderrMessage: "the querier configuration in YAML has been specified as an empty YAML node",
},

"root level configuration option specified with all zero values": {
yaml: "flusher: { exit_after_flush: false }",
stderrExcluded: "empty YAML node",
},

"version": {
Expand Down
32 changes: 0 additions & 32 deletions pkg/cortex/cortex.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"fmt"
"net/http"
"os"
"reflect"
"strings"

"github.com/go-kit/log"
Expand Down Expand Up @@ -174,10 +173,6 @@ func (c *Config) RegisterFlags(f *flag.FlagSet) {
// Validate the cortex config and returns an error if the validation
// doesn't pass
func (c *Config) Validate(log log.Logger) error {
if err := c.validateYAMLEmptyNodes(); err != nil {
return err
}

if c.HTTPPrefix != "" && !strings.HasPrefix(c.HTTPPrefix, "/") {
return errInvalidHTTPPrefix
}
Expand Down Expand Up @@ -236,33 +231,6 @@ func (c *Config) isModuleEnabled(m string) bool {
return util.StringsContain(c.Target, m)
}

// validateYAMLEmptyNodes ensure that no empty node has been specified in the YAML config file.
// When an empty node is defined in YAML, the YAML parser sets the whole struct to its zero value
// and so we loose all default values. It's very difficult to detect this case for the user, so we
// try to prevent it (on the root level) with this custom validation.
func (c *Config) validateYAMLEmptyNodes() error {
defaults := Config{}
flagext.DefaultValues(&defaults)

defStruct := reflect.ValueOf(defaults)
cfgStruct := reflect.ValueOf(*c)

// We expect all structs are the exact same. This check should never fail.
if cfgStruct.NumField() != defStruct.NumField() {
return errors.New("unable to validate configuration because of mismatching internal config data structure")
}

for i := 0; i < cfgStruct.NumField(); i++ {
// If the struct has been reset due to empty YAML value and the zero struct value
// doesn't match the default one, then we should warn the user about the issue.
if cfgStruct.Field(i).Kind() == reflect.Struct && cfgStruct.Field(i).IsZero() && !defStruct.Field(i).IsZero() {
return fmt.Errorf("the %s configuration in YAML has been specified as an empty YAML node", cfgStruct.Type().Field(i).Name)
}
}

return nil
}

func (c *Config) registerServerFlagsWithChangedDefaultValues(fs *flag.FlagSet) {
throwaway := flag.NewFlagSet("throwaway", flag.PanicOnError)

Expand Down

0 comments on commit 78b0b56

Please sign in to comment.