Skip to content

Commit 62b06f5

Browse files
committed
Improve yaml empty node validation by inspecting the ast directly instead of checking the struct after unmarshaling
Signed-off-by: Joe Kralicky <joe.kralicky@suse.com>
1 parent 4e162a0 commit 62b06f5

File tree

3 files changed

+58
-33
lines changed

3 files changed

+58
-33
lines changed

cmd/cortex/main.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/prometheus/client_golang/prometheus"
1919
"github.com/prometheus/common/version"
2020
"gopkg.in/yaml.v2"
21+
yamlv3 "gopkg.in/yaml.v3"
2122

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

254+
if err := validateYAMLEmptyNodes(buf); err != nil {
255+
return err
256+
}
257+
253258
return nil
254259
}
255260

@@ -262,6 +267,43 @@ func DumpYaml(cfg *cortex.Config) {
262267
}
263268
}
264269

270+
// validateYAMLEmptyNodes ensure that no empty node has been specified in the YAML config file.
271+
// When an empty node is defined in YAML, the YAML parser sets the whole struct to its zero value
272+
// and so we loose all default values. It's very difficult to detect this case for the user, so we
273+
// try to prevent it (on the root level) with this custom validation.
274+
func validateYAMLEmptyNodes(buf []byte) error {
275+
var document yamlv3.Node
276+
if err := yamlv3.Unmarshal(buf, &document); err != nil {
277+
return err
278+
}
279+
if document.Kind != yamlv3.DocumentNode {
280+
return errors.New("invalid YAML document")
281+
}
282+
for _, node := range document.Content {
283+
if node.Kind != yamlv3.MappingNode || len(node.Content) < 2 || node.Content[0].Kind != yamlv3.ScalarNode {
284+
continue
285+
}
286+
foundEmptyNode := false
287+
switch node.Content[1].Kind {
288+
case yamlv3.MappingNode:
289+
if len(node.Content[1].Content) == 0 {
290+
// "key: {}"
291+
foundEmptyNode = true
292+
}
293+
case yamlv3.ScalarNode:
294+
if node.Content[1].Tag == "!!null" {
295+
// "key: null" or "key:"
296+
foundEmptyNode = true
297+
}
298+
}
299+
if foundEmptyNode {
300+
return fmt.Errorf("the %s configuration in YAML has been specified as an empty YAML node", node.Content[0].Value)
301+
}
302+
return nil
303+
}
304+
return nil
305+
}
306+
265307
// expandEnv replaces ${var} or $var in config according to the values of the current environment variables.
266308
// The replacement is case-sensitive. References to undefined variables are replaced by the empty string.
267309
// A default value can be given by using the form ${var:default value}.

cmd/cortex/main_test.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,22 @@ func TestFlagParsing(t *testing.T) {
7676

7777
"root level configuration option specified as an empty node in YAML": {
7878
yaml: "querier:",
79-
stderrMessage: "the Querier configuration in YAML has been specified as an empty YAML node",
79+
stderrMessage: "the querier configuration in YAML has been specified as an empty YAML node",
80+
},
81+
82+
"root level configuration option specified as an empty object node in YAML": {
83+
yaml: "querier: {}",
84+
stderrMessage: "the querier configuration in YAML has been specified as an empty YAML node",
85+
},
86+
87+
"root level configuration option specified as a null node in YAML": {
88+
yaml: "querier: null",
89+
stderrMessage: "the querier configuration in YAML has been specified as an empty YAML node",
90+
},
91+
92+
"root level configuration option specified with all zero values": {
93+
yaml: "flusher: { exit_after_flush: false }",
94+
stderrExcluded: "empty YAML node",
8095
},
8196

8297
"version": {

pkg/cortex/cortex.go

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"fmt"
88
"net/http"
99
"os"
10-
"reflect"
1110
"strings"
1211

1312
"github.com/go-kit/log"
@@ -174,10 +173,6 @@ func (c *Config) RegisterFlags(f *flag.FlagSet) {
174173
// Validate the cortex config and returns an error if the validation
175174
// doesn't pass
176175
func (c *Config) Validate(log log.Logger) error {
177-
if err := c.validateYAMLEmptyNodes(); err != nil {
178-
return err
179-
}
180-
181176
if c.HTTPPrefix != "" && !strings.HasPrefix(c.HTTPPrefix, "/") {
182177
return errInvalidHTTPPrefix
183178
}
@@ -236,33 +231,6 @@ func (c *Config) isModuleEnabled(m string) bool {
236231
return util.StringsContain(c.Target, m)
237232
}
238233

239-
// validateYAMLEmptyNodes ensure that no empty node has been specified in the YAML config file.
240-
// When an empty node is defined in YAML, the YAML parser sets the whole struct to its zero value
241-
// and so we loose all default values. It's very difficult to detect this case for the user, so we
242-
// try to prevent it (on the root level) with this custom validation.
243-
func (c *Config) validateYAMLEmptyNodes() error {
244-
defaults := Config{}
245-
flagext.DefaultValues(&defaults)
246-
247-
defStruct := reflect.ValueOf(defaults)
248-
cfgStruct := reflect.ValueOf(*c)
249-
250-
// We expect all structs are the exact same. This check should never fail.
251-
if cfgStruct.NumField() != defStruct.NumField() {
252-
return errors.New("unable to validate configuration because of mismatching internal config data structure")
253-
}
254-
255-
for i := 0; i < cfgStruct.NumField(); i++ {
256-
// If the struct has been reset due to empty YAML value and the zero struct value
257-
// doesn't match the default one, then we should warn the user about the issue.
258-
if cfgStruct.Field(i).Kind() == reflect.Struct && cfgStruct.Field(i).IsZero() && !defStruct.Field(i).IsZero() {
259-
return fmt.Errorf("the %s configuration in YAML has been specified as an empty YAML node", cfgStruct.Type().Field(i).Name)
260-
}
261-
}
262-
263-
return nil
264-
}
265-
266234
func (c *Config) registerServerFlagsWithChangedDefaultValues(fs *flag.FlagSet) {
267235
throwaway := flag.NewFlagSet("throwaway", flag.PanicOnError)
268236

0 commit comments

Comments
 (0)