mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-10-18 18:49:05 +02:00
collector: derive the collector maps from one registration table (#2730)
Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
This commit is contained in:
+40
-23
@@ -21,7 +21,10 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"os"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/alecthomas/kingpin/v2"
|
||||
@@ -30,7 +33,9 @@ import (
|
||||
)
|
||||
|
||||
// configFile represents the structure of the windows_exporter configuration file,
|
||||
// including configuration from the collector and web packages.
|
||||
// including configuration from the collector and web packages. Each leaf is a
|
||||
// flag; the field types document the value of the flag, which parses and
|
||||
// validates it. [validateKeys] checks the keys of the file against this schema.
|
||||
type configFile struct {
|
||||
Debug struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
@@ -45,17 +50,17 @@ type configFile struct {
|
||||
Format string `yaml:"format"`
|
||||
File string `yaml:"file"`
|
||||
Rotation struct {
|
||||
MaxSize string `yaml:"max-size"`
|
||||
MaxBackups string `yaml:"max-backups"`
|
||||
Compress bool `yaml:"compress"`
|
||||
MaxSize int `yaml:"max-size"`
|
||||
MaxBackups int `yaml:"max-backups"`
|
||||
Compress bool `yaml:"compress"`
|
||||
} `yaml:"rotation"`
|
||||
} `yaml:"log"`
|
||||
Process struct {
|
||||
Priority string `yaml:"priority"`
|
||||
MemoryLimit string `yaml:"memory-limit"`
|
||||
MemoryLimit int64 `yaml:"memory-limit"`
|
||||
} `yaml:"process"`
|
||||
Scrape struct {
|
||||
TimeoutMargin string `yaml:"timeout-margin"`
|
||||
TimeoutMargin float64 `yaml:"timeout-margin"`
|
||||
} `yaml:"scrape"`
|
||||
Telemetry struct {
|
||||
Path string `yaml:"path"`
|
||||
@@ -76,6 +81,8 @@ type getFlagger interface {
|
||||
// Resolver represents a configuration file resolver for kingpin.
|
||||
type Resolver struct {
|
||||
flags map[string][]string
|
||||
// lines maps the keys of the configuration file to their lines.
|
||||
lines map[string]int
|
||||
}
|
||||
|
||||
// Parse parses the command line arguments and configuration files.
|
||||
@@ -135,12 +142,9 @@ func NewConfigFileResolver(filePath string) (*Resolver, error) {
|
||||
_ = file.Close()
|
||||
}()
|
||||
|
||||
var configFileStructure configFile
|
||||
var document yaml.Node
|
||||
|
||||
decoder := yaml.NewDecoder(file)
|
||||
decoder.KnownFields(true)
|
||||
|
||||
if err = decoder.Decode(&configFileStructure); err != nil {
|
||||
if err = yaml.NewDecoder(file).Decode(&document); err != nil {
|
||||
// Handle EOF error gracefully, indicating no configuration was found.
|
||||
if errors.Is(err, io.EOF) {
|
||||
return &Resolver{flags: flags}, nil
|
||||
@@ -149,18 +153,18 @@ func NewConfigFileResolver(filePath string) (*Resolver, error) {
|
||||
return nil, fmt.Errorf("configuration file validation error: %w", err)
|
||||
}
|
||||
|
||||
_, err = file.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to rewind file: %w", err)
|
||||
}
|
||||
|
||||
// Decoding first rejects duplicate keys and expands aliases within the
|
||||
// limits of the decoder before the schema walks the document.
|
||||
var rawValues map[string]any
|
||||
|
||||
decoder = yaml.NewDecoder(file)
|
||||
if err = decoder.Decode(&rawValues); err != nil {
|
||||
if err = document.Decode(&rawValues); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse configuration file: %w", err)
|
||||
}
|
||||
|
||||
if err = validateKeys(&document, "", reflect.TypeFor[configFile]()); err != nil {
|
||||
return nil, fmt.Errorf("configuration file validation error: %w", err)
|
||||
}
|
||||
|
||||
// Flatten nested YAML values
|
||||
flattenedValues := flatten(rawValues)
|
||||
for k, v := range flattenedValues {
|
||||
@@ -169,16 +173,25 @@ func NewConfigFileResolver(filePath string) (*Resolver, error) {
|
||||
}
|
||||
}
|
||||
|
||||
return &Resolver{flags: flags}, nil
|
||||
lines := map[string]int{}
|
||||
keyLines(&document, "", lines)
|
||||
|
||||
return &Resolver{flags: flags, lines: lines}, nil
|
||||
}
|
||||
|
||||
func (c *Resolver) setDefault(v getFlagger) {
|
||||
for name, values := range c.flags {
|
||||
func (c *Resolver) setDefault(v getFlagger) error {
|
||||
for _, name := range slices.Sorted(maps.Keys(c.flags)) {
|
||||
values := c.flags[name]
|
||||
|
||||
f := v.GetFlag(name)
|
||||
if f == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := checkFlagValue(f, values); err != nil {
|
||||
return fmt.Errorf("line %d: %s: %w", c.lines[name], name, err)
|
||||
}
|
||||
|
||||
// A list sets a repeatable flag, like web.listen-address, once per element.
|
||||
// Other flags take the list as a comma-separated value.
|
||||
if repeatable, ok := f.Model().Value.(interface{ IsCumulative() bool }); ok && repeatable.IsCumulative() {
|
||||
@@ -187,6 +200,8 @@ func (c *Resolver) setDefault(v getFlagger) {
|
||||
f.Default(strings.Join(values, ","))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Bind sets active flags with their default values from the configuration file(s).
|
||||
@@ -197,10 +212,12 @@ func (c *Resolver) Bind(app *kingpin.Application, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
c.setDefault(app)
|
||||
if err = c.setDefault(app); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if pc.SelectedCommand != nil {
|
||||
c.setDefault(pc.SelectedCommand)
|
||||
return c.setDefault(pc.SelectedCommand)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -109,6 +109,21 @@ func TestNewConfigFileResolver(t *testing.T) {
|
||||
contents: "collector:\n registry:\n keys: |-\n - name: test\n key: HKLM\\SOFTWARE\n values:\n - name: test\n metrc: test\n",
|
||||
errorText: "field metrc not found in type registry.Value",
|
||||
},
|
||||
{name: "quoted bool", contents: "log:\n rotation:\n compress: \"true\"\n"},
|
||||
{name: "quoted number", contents: "log:\n rotation:\n max-size: \"10\"\n"},
|
||||
{name: "list as string", contents: "collector:\n mscluster:\n enabled: cluster,node\n"},
|
||||
{name: "string as list", contents: "collectors:\n enabled: [cpu, memory]\n"},
|
||||
{name: "duration as string", contents: "collector:\n hyperv:\n virtual-hard-disk:\n refresh-interval: 30s\n"},
|
||||
{name: "renamed collector block", contents: "collector:\n pagefile:\n smbclient:\n"},
|
||||
{name: "old collector block", contents: "collector:\n paging:\n", errorText: "line 2: field paging not found in collector"},
|
||||
{name: "unknown nested key", contents: "collector:\n cpu:\n unknown: true\n", errorText: "line 3: field unknown not found in collector.cpu"},
|
||||
{name: "unknown top-level key", contents: "unknown: value\n", errorText: "line 1: field unknown not found in the configuration"},
|
||||
{name: "mapping as value", contents: "log:\n level:\n debug: true\n", errorText: "line 3: log.level must be a value or a list of values"},
|
||||
{name: "mapping in list", contents: "web:\n listen-address:\n - port: 9182\n", errorText: "line 3: web.listen-address must be a value or a list of values"},
|
||||
{name: "value as section", contents: "log: debug\n", errorText: "line 1: log must be a mapping"},
|
||||
{name: "merge key", contents: "collector:\n service: &filter\n include: a\n process:\n <<: *filter\n exclude: b\n"},
|
||||
{name: "merged section", contents: "log: &log\n level: debug\nweb:\n <<: *log\n", errorText: "line 2: field level not found in web"},
|
||||
{name: "duplicate key", contents: "log:\n level: debug\n level: info\n", errorText: "already defined"},
|
||||
{
|
||||
name: "unknown wmi option",
|
||||
contents: "collector:\n wmi:\n query: |-\n - name: test\n",
|
||||
@@ -174,6 +189,11 @@ func TestNewConfigFileResolverFlags(t *testing.T) {
|
||||
|
||||
value, values := sampleConfigValue(t, field, block)
|
||||
|
||||
// These flags only accept some strings.
|
||||
if sample, ok := map[string]string{"log.level": "debug", "log.format": "json"}[flag.Name]; ok {
|
||||
value, values = sample, []string{sample}
|
||||
}
|
||||
|
||||
contents := value
|
||||
for _, key := range slices.Backward(keys) {
|
||||
contents = map[string]any{key: contents}
|
||||
@@ -431,6 +451,8 @@ func sampleConfigValue(t *testing.T, typ reflect.Type, block bool) (any, []strin
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return 1, []string{"1"}
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return 1.5, []string{"1.5"}
|
||||
case reflect.Slice, reflect.Interface:
|
||||
return []string{"test1", "test2"}, []string{"test1", "test2"}
|
||||
case reflect.String:
|
||||
@@ -525,3 +547,75 @@ func TestParseEmptyValue(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseFlagValues verifies that the configuration file takes the values the
|
||||
// flags accept, whatever the Go type of the configuration, and that an invalid
|
||||
// value fails with its key and line, even if the command line sets the flag.
|
||||
func TestParseFlagValues(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mainFlags := mainFlagNames(t)
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
config string
|
||||
args []string
|
||||
flag string
|
||||
want string
|
||||
errorText string
|
||||
}{
|
||||
{name: "quoted bool", config: "log:\n rotation:\n compress: \"true\"\n", flag: "log.rotation.compress", want: "true"},
|
||||
{name: "quoted int", config: "log:\n rotation:\n max-size: \"10\"\n", flag: "log.rotation.max-size", want: "10"},
|
||||
{
|
||||
name: "comma-separated list", config: "collector:\n mscluster:\n enabled: cluster,node\n",
|
||||
flag: "collector.mscluster.enabled", want: "cluster,node",
|
||||
},
|
||||
{
|
||||
name: "list of strings", config: "collector:\n mscluster:\n enabled: [cluster, node]\n",
|
||||
flag: "collector.mscluster.enabled", want: "cluster,node",
|
||||
},
|
||||
{
|
||||
name: "repeatable flag", config: "web:\n listen-address: ['127.0.0.1:9182', '[::1]:9182']\n",
|
||||
flag: "web.listen-address", want: "127.0.0.1:9182,[::1]:9182",
|
||||
},
|
||||
{
|
||||
name: "merge key", config: "collector:\n service: &filter\n include: a\n process:\n <<: *filter\n exclude: b\n",
|
||||
flag: "collector.process.include", want: "a",
|
||||
},
|
||||
{
|
||||
name: "CLI overrides config", config: "log:\n rotation:\n compress: true\n",
|
||||
args: []string{"--no-log.rotation.compress"}, flag: "log.rotation.compress", want: "false",
|
||||
},
|
||||
{
|
||||
name: "invalid bool", config: "log:\n rotation:\n compress: maybe\n",
|
||||
errorText: "line 3: log.rotation.compress: strconv.ParseBool",
|
||||
},
|
||||
{
|
||||
name: "invalid int", config: "debug:\n enabled: true\nlog:\n rotation:\n max-size: ten\n",
|
||||
errorText: "line 5: log.rotation.max-size: strconv.Parse",
|
||||
},
|
||||
{
|
||||
name: "invalid value set on the CLI", config: "log:\n rotation:\n compress: maybe\n",
|
||||
args: []string{"--log.rotation.compress"}, errorText: "line 3: log.rotation.compress: strconv.ParseBool",
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
path := filepath.Join(t.TempDir(), "config.yaml")
|
||||
require.NoError(t, os.WriteFile(path, []byte(tc.config), 0o600))
|
||||
|
||||
app := newApp(mainFlags)
|
||||
|
||||
err := config.Parse(app, append([]string{"--config.file=" + path}, tc.args...))
|
||||
if tc.errorText != "" {
|
||||
require.ErrorContains(t, err, tc.errorText)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.want, app.GetFlag(tc.flag).Model().Value.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// Copyright The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build windows
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/alecthomas/kingpin/v2"
|
||||
"go.yaml.in/yaml/v3"
|
||||
)
|
||||
|
||||
// validateKeys checks that node, the value of the dotted key path, only uses
|
||||
// the keys of the schema typ.
|
||||
//
|
||||
// The configuration file sets flags, and the flags parse and validate the
|
||||
// values (see [checkFlagValue]). So the schema only defines the keys: a leaf
|
||||
// takes any scalar or a list of scalars, whatever the type of its field.
|
||||
// Decoding the values into the field types would reject values that the flags
|
||||
// accept, like compress: "true" or a comma-separated list for a list field.
|
||||
//
|
||||
// A struct that decodes itself, like the performancecounter block, is decoded
|
||||
// to validate its contents.
|
||||
func validateKeys(node *yaml.Node, path string, typ reflect.Type) error {
|
||||
switch node.Kind {
|
||||
case yaml.DocumentNode:
|
||||
for _, content := range node.Content {
|
||||
if err := validateKeys(content, path, typ); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
case yaml.AliasNode:
|
||||
return validateKeys(node.Alias, path, typ)
|
||||
case yaml.ScalarNode:
|
||||
if node.Tag == "!!null" {
|
||||
// A key without a value leaves the flags unchanged.
|
||||
return nil
|
||||
}
|
||||
case yaml.MappingNode, yaml.SequenceNode:
|
||||
}
|
||||
|
||||
if reflect.PointerTo(typ).Implements(reflect.TypeFor[yaml.Unmarshaler]()) {
|
||||
//nolint:wrapcheck // the decoder errors name the line and field.
|
||||
return node.Decode(reflect.New(typ).Interface())
|
||||
}
|
||||
|
||||
if typ.Kind() != reflect.Struct {
|
||||
if !isValue(node) {
|
||||
return fmt.Errorf("line %d: %s must be a value or a list of values", node.Line, displayPath(path))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if node.Kind != yaml.MappingNode {
|
||||
return fmt.Errorf("line %d: %s must be a mapping", node.Line, displayPath(path))
|
||||
}
|
||||
|
||||
for i := 0; i+1 < len(node.Content); i += 2 {
|
||||
key, value := node.Content[i], node.Content[i+1]
|
||||
|
||||
if isMerge(key) {
|
||||
// A merge key (<<) adds the keys of other mappings.
|
||||
for _, source := range mergeSources(value) {
|
||||
if err := validateKeys(source, path, typ); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
field, ok := schemaField(typ, key.Value)
|
||||
if !ok {
|
||||
return fmt.Errorf("line %d: field %s not found in %s", key.Line, key.Value, displayPath(path))
|
||||
}
|
||||
|
||||
if err := validateKeys(value, joinPath(path, key.Value), field.Type); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isValue reports whether node is the value of a flag: a scalar, or a list of
|
||||
// scalars for a repeatable or list flag.
|
||||
func isValue(node *yaml.Node) bool {
|
||||
switch node.Kind {
|
||||
case yaml.ScalarNode:
|
||||
return true
|
||||
case yaml.AliasNode:
|
||||
return isValue(node.Alias)
|
||||
case yaml.SequenceNode:
|
||||
for _, item := range node.Content {
|
||||
if item.Kind == yaml.AliasNode {
|
||||
item = item.Alias
|
||||
}
|
||||
|
||||
if item.Kind != yaml.ScalarNode {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
case yaml.DocumentNode, yaml.MappingNode:
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// keyLines records the line of every dotted key path below node in lines.
|
||||
// Explicit keys take precedence over the keys of merged mappings, like in the
|
||||
// decoded values.
|
||||
func keyLines(node *yaml.Node, path string, lines map[string]int) {
|
||||
switch node.Kind {
|
||||
case yaml.DocumentNode:
|
||||
for _, content := range node.Content {
|
||||
keyLines(content, path, lines)
|
||||
}
|
||||
case yaml.AliasNode:
|
||||
keyLines(node.Alias, path, lines)
|
||||
case yaml.MappingNode:
|
||||
for i := 0; i+1 < len(node.Content); i += 2 {
|
||||
if isMerge(node.Content[i]) {
|
||||
for _, source := range mergeSources(node.Content[i+1]) {
|
||||
keyLines(source, path, lines)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i+1 < len(node.Content); i += 2 {
|
||||
key, value := node.Content[i], node.Content[i+1]
|
||||
if isMerge(key) {
|
||||
continue
|
||||
}
|
||||
|
||||
keyPath := joinPath(path, key.Value)
|
||||
lines[keyPath] = key.Line
|
||||
keyLines(value, keyPath, lines)
|
||||
}
|
||||
case yaml.ScalarNode, yaml.SequenceNode:
|
||||
}
|
||||
}
|
||||
|
||||
func isMerge(key *yaml.Node) bool {
|
||||
return key.Kind == yaml.ScalarNode && key.Tag == "!!merge"
|
||||
}
|
||||
|
||||
// mergeSources returns the mappings that a merge key merges: an alias, a
|
||||
// mapping or a list of them.
|
||||
func mergeSources(value *yaml.Node) []*yaml.Node {
|
||||
if value.Kind == yaml.SequenceNode {
|
||||
return value.Content
|
||||
}
|
||||
|
||||
return []*yaml.Node{value}
|
||||
}
|
||||
|
||||
func joinPath(path, key string) string {
|
||||
if path == "" {
|
||||
return key
|
||||
}
|
||||
|
||||
return path + "." + key
|
||||
}
|
||||
|
||||
func displayPath(path string) string {
|
||||
if path == "" {
|
||||
return "the configuration"
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
// schemaField returns the field of the struct typ with the YAML key name,
|
||||
// looking into the fields inlined with the yaml inline option.
|
||||
func schemaField(typ reflect.Type, name string) (reflect.StructField, bool) {
|
||||
for field := range typ.Fields() {
|
||||
if !field.IsExported() {
|
||||
continue
|
||||
}
|
||||
|
||||
key, options, _ := strings.Cut(field.Tag.Get("yaml"), ",")
|
||||
|
||||
if slices.Contains(strings.Split(options, ","), "inline") && field.Type.Kind() == reflect.Struct {
|
||||
if inlined, ok := schemaField(field.Type, name); ok {
|
||||
return inlined, true
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if key == "" {
|
||||
key = strings.ToLower(field.Name)
|
||||
}
|
||||
|
||||
if key != "-" && key == name {
|
||||
return field, true
|
||||
}
|
||||
}
|
||||
|
||||
return reflect.StructField{}, false
|
||||
}
|
||||
|
||||
// checkFlagValue checks that the flag accepts the values from the
|
||||
// configuration file.
|
||||
//
|
||||
// The flags only parse their default values in kingpin.Application.Parse,
|
||||
// which does not tell the configuration key, and does not parse them at all
|
||||
// if the flag is also set on the command line. kingpin cannot copy a flag
|
||||
// value, so the check sets the flag value itself. That is safe: Parse sets it
|
||||
// again, to the same default or to the command-line value. A repeatable flag
|
||||
// would append the values twice, so its list is restored after the check.
|
||||
func checkFlagValue(flag *kingpin.FlagClause, values []string) error {
|
||||
value := flag.Model().Value
|
||||
|
||||
if repeatable, ok := value.(interface{ IsCumulative() bool }); !ok || !repeatable.IsCumulative() {
|
||||
//nolint:wrapcheck // the caller adds the key and line.
|
||||
return value.Set(strings.Join(values, ","))
|
||||
}
|
||||
|
||||
getter, ok := value.(kingpin.Getter)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
list := reflect.ValueOf(getter.Get())
|
||||
if list.Kind() != reflect.Pointer || list.Elem().Kind() != reflect.Slice {
|
||||
// The list cannot be restored; Parse checks the values.
|
||||
return nil
|
||||
}
|
||||
|
||||
saved := reflect.ValueOf(list.Elem().Interface())
|
||||
defer list.Elem().Set(saved)
|
||||
|
||||
for _, v := range values {
|
||||
if err := value.Set(v); err != nil {
|
||||
//nolint:wrapcheck // the caller adds the key and line.
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// Copyright The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build windows
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.yaml.in/yaml/v3"
|
||||
)
|
||||
|
||||
// TestValidateKeysInline verifies that the keys of a field inlined with the
|
||||
// yaml inline option belong to the enclosing mapping.
|
||||
func TestValidateKeysInline(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type Inner struct {
|
||||
Level string `yaml:"level"`
|
||||
}
|
||||
|
||||
type schema struct {
|
||||
Log struct {
|
||||
Inner `yaml:",inline"`
|
||||
|
||||
Format string `yaml:"format"`
|
||||
} `yaml:"log"`
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
contents string
|
||||
errorText string
|
||||
}{
|
||||
{name: "inlined key", contents: "log:\n level: debug\n format: json\n"},
|
||||
{name: "unknown key", contents: "log:\n inner: debug\n", errorText: "line 2: field inner not found in log"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var document yaml.Node
|
||||
require.NoError(t, yaml.Unmarshal([]byte(tc.contents), &document))
|
||||
|
||||
err := validateKeys(&document, "", reflect.TypeFor[schema]())
|
||||
if tc.errorText != "" {
|
||||
require.EqualError(t, err, tc.errorText)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user