Files
windows_exporter/internal/config/schema.go
T

265 lines
6.9 KiB
Go

// 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
}