mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-10-17 18:19:04 +02:00
698 lines
23 KiB
Go
698 lines
23 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_test
|
|
|
|
import (
|
|
"fmt"
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"regexp"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/alecthomas/kingpin/v2"
|
|
"github.com/prometheus-community/windows_exporter/internal/config"
|
|
"github.com/prometheus-community/windows_exporter/internal/log"
|
|
logflag "github.com/prometheus-community/windows_exporter/internal/log/flag"
|
|
"github.com/prometheus-community/windows_exporter/pkg/collector"
|
|
webflag "github.com/prometheus/exporter-toolkit/web/kingpinflag"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"go.yaml.in/yaml/v3"
|
|
)
|
|
|
|
func TestParseConfigFile(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
args []string
|
|
want string
|
|
}{
|
|
{name: "absent"},
|
|
{name: "long joined", args: []string{"--config.file=test.yaml"}, want: "test.yaml"},
|
|
{name: "short joined", args: []string{"-config.file=test.yaml"}, want: "test.yaml"},
|
|
{name: "long separate", args: []string{"--config.file", "test.yaml"}, want: "test.yaml"},
|
|
{name: "short separate", args: []string{"-config.file", "test.yaml"}, want: "test.yaml"},
|
|
{name: "after other flags", args: []string{"--log.level=debug", "--config.file", "test.yaml"}, want: "test.yaml"},
|
|
{name: "missing value", args: []string{"--config.file"}},
|
|
{name: "empty value", args: []string{"--config.file="}},
|
|
{name: "unrelated suffix", args: []string{"--other-config.file", "test.yaml"}},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
require.Equal(t, tc.want, config.ParseConfigFile(tc.args))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewConfigFileResolver(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
contents string
|
|
errorText string
|
|
}{
|
|
{name: "empty"},
|
|
{name: "valid", contents: "log:\n level: debug\nweb:\n listen-address: ['127.0.0.1:9182', '[::1]:9182']\n"},
|
|
{name: "unknown key", contents: "unknown: value", errorText: "configuration file validation error"},
|
|
{name: "unknown collector option", contents: "collector:\n cpu:\n unknown: true\n", errorText: "configuration file validation error"},
|
|
{name: "malformed YAML", contents: "log: [", errorText: "configuration file validation error"},
|
|
{name: "empty value", contents: "log:\n file:\n"},
|
|
{name: "objects as JSON", contents: "collector:\n performancecounter:\n objects: '[{\"name\": \"memory\", \"object\": \"Memory\"}]'\n"},
|
|
{name: "objects without value", contents: "collector:\n performancecounter:\n objects:\n"},
|
|
{
|
|
name: "unknown performancecounter option",
|
|
contents: "collector:\n performancecounter:\n invalid: |-\n - name: memory\n object: Memory\n",
|
|
errorText: "line 3: field invalid not found",
|
|
},
|
|
{
|
|
name: "unknown performancecounter object field",
|
|
contents: "collector:\n performancecounter:\n objects: |-\n - name: memory\n invalid: Memory\n",
|
|
errorText: "field invalid not found in type performancecounter.Object",
|
|
},
|
|
{
|
|
name: "performancecounter objects as list",
|
|
contents: "collector:\n performancecounter:\n objects:\n - name: memory\n object: Memory\n",
|
|
errorText: "line 4: objects must be a string",
|
|
},
|
|
{
|
|
name: "unknown registry option",
|
|
contents: "collector:\n registry:\n key: |-\n - name: test\n",
|
|
errorText: "line 3: field key not found",
|
|
},
|
|
{
|
|
name: "unknown registry value field",
|
|
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",
|
|
errorText: "line 3: field query not found",
|
|
},
|
|
} {
|
|
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.contents), 0o600))
|
|
|
|
resolver, err := config.NewConfigFileResolver(path)
|
|
if tc.errorText != "" {
|
|
require.ErrorContains(t, err, tc.errorText)
|
|
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
require.NotNil(t, resolver)
|
|
})
|
|
}
|
|
|
|
t.Run("missing file", func(t *testing.T) {
|
|
t.Parallel()
|
|
_, err := config.NewConfigFileResolver(filepath.Join(t.TempDir(), "missing.yaml"))
|
|
require.ErrorContains(t, err, "failed to open configuration file")
|
|
})
|
|
}
|
|
|
|
// TestNewConfigFileResolverFlags verifies that every flag can be set from the
|
|
// configuration file: the nested key derived from the flag name must exist in
|
|
// the configuration file schema, pass the strict validation, and be bound back
|
|
// to the same flag.
|
|
func TestNewConfigFileResolverFlags(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
mainFlags := mainFlagNames(t)
|
|
|
|
// kingpin's built-in flags, like --help, are not configuration options.
|
|
builtin := kingpin.New("test", "test")
|
|
|
|
// cliOnly lists the flags that have no configuration file key by design.
|
|
cliOnly := map[string]string{
|
|
"config.file": "selects the configuration file itself",
|
|
"collector.exchange.list": "prints the available sub-collectors and exits",
|
|
"scrape.stdout": "CLI-only diagnostic mode; not valid in a config file",
|
|
}
|
|
|
|
app := newApp(mainFlags)
|
|
|
|
// A renamed or removed flag must not stay exempt under its old name.
|
|
for name := range cliOnly {
|
|
require.NotNil(t, app.GetFlag(name), "the CLI-only flag %s does not exist", name)
|
|
}
|
|
|
|
for _, flag := range app.Model().Flags {
|
|
if _, ok := cliOnly[flag.Name]; ok || builtin.GetFlag(flag.Name) != nil {
|
|
continue
|
|
}
|
|
|
|
t.Run(flag.Name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
keys := strings.Split(flag.Name, ".")
|
|
|
|
field, block, ok := configFieldType(reflect.TypeFor[config.ConfigFile](), keys)
|
|
require.True(t, ok, "the configuration file has no key %s", flag.Name)
|
|
|
|
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}
|
|
}
|
|
|
|
data, err := yaml.Marshal(contents)
|
|
require.NoError(t, err)
|
|
|
|
path := filepath.Join(t.TempDir(), "config.yaml")
|
|
require.NoError(t, os.WriteFile(path, data, 0o600))
|
|
|
|
resolver, err := config.NewConfigFileResolver(path)
|
|
require.NoError(t, err, "config:\n%s", data)
|
|
|
|
app := newApp(mainFlags)
|
|
require.NoError(t, resolver.Bind(app, nil))
|
|
|
|
want := values
|
|
if repeatable, ok := flag.Value.(interface{ IsCumulative() bool }); !ok || !repeatable.IsCumulative() {
|
|
want = []string{strings.Join(values, ",")}
|
|
}
|
|
|
|
require.Equal(t, want, app.GetFlag(flag.Name).Model().Default, "config:\n%s", data)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestConfigFileKeys verifies the reverse: every key of the configuration file
|
|
// schema sets a flag, so no key passes the validation only to be ignored.
|
|
func TestConfigFileKeys(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
app := newApp(mainFlagNames(t))
|
|
|
|
for _, key := range configKeys(reflect.TypeFor[config.ConfigFile](), "") {
|
|
assert.NotNil(t, app.GetFlag(key), "the configuration file key %s sets no flag", key)
|
|
}
|
|
}
|
|
|
|
// TestNewConfigFileResolverExamples verifies that the example configuration
|
|
// files, and the configuration examples in the documentation of the collectors
|
|
// that take their list as a string, load and parse.
|
|
func TestNewConfigFileResolverExamples(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
examples := map[string]string{}
|
|
|
|
for _, path := range []string{"../../config.yaml", "../../docs/example_config.yml"} {
|
|
data, err := os.ReadFile(path)
|
|
require.NoError(t, err)
|
|
|
|
examples[path] = string(data)
|
|
}
|
|
|
|
codeBlock := regexp.MustCompile("(?ms)^```(?:yaml|json)\n(.*?)^```")
|
|
|
|
for _, doc := range []struct{ path, block, key string }{
|
|
{"../../docs/collector.performancecounter.md", "performancecounter", "objects"},
|
|
{"../../docs/collector.registry.md", "registry", "keys"},
|
|
{"../../docs/collector.wmi.md", "wmi", "queries"},
|
|
} {
|
|
data, err := os.ReadFile(doc.path)
|
|
require.NoError(t, err)
|
|
|
|
for i, match := range codeBlock.FindAllStringSubmatch(strings.ReplaceAll(string(data), "\r\n", "\n"), -1) {
|
|
example := match[1]
|
|
|
|
switch {
|
|
case strings.HasPrefix(example, "collector:"):
|
|
// a configuration file
|
|
case strings.HasPrefix(example, "- "), strings.HasPrefix(example, "["):
|
|
// a schema example, which is the value of the flag
|
|
example = fmt.Sprintf("collector:\n %s:\n %s: |-\n%s", doc.block, doc.key, indent(example, " "))
|
|
default:
|
|
// a fragment or an alerting rule
|
|
continue
|
|
}
|
|
|
|
examples[fmt.Sprintf("%s block %d", doc.path, i+1)] = example
|
|
}
|
|
}
|
|
|
|
mainFlags := mainFlagNames(t)
|
|
|
|
for name, contents := range examples {
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
path := filepath.Join(t.TempDir(), "config.yaml")
|
|
require.NoError(t, os.WriteFile(path, []byte(contents), 0o600))
|
|
|
|
resolver, err := config.NewConfigFileResolver(path)
|
|
require.NoError(t, err, "config:\n%s", contents)
|
|
|
|
app := newApp(mainFlags)
|
|
require.NoError(t, resolver.Bind(app, nil))
|
|
|
|
_, err = app.Parse(nil)
|
|
require.NoError(t, err, "config:\n%s", contents)
|
|
})
|
|
}
|
|
}
|
|
|
|
// newApp returns a kingpin application with the flags of windows_exporter: the
|
|
// flags declared in cmd/windows_exporter, named by mainFlags, and the flags of
|
|
// the packages it uses.
|
|
func newApp(mainFlags []string) *kingpin.Application {
|
|
app := kingpin.New("test", "test")
|
|
|
|
for _, name := range mainFlags {
|
|
app.Flag(name, "").String()
|
|
}
|
|
|
|
logFile := &log.AllowedFile{}
|
|
_ = logFile.Set("stdout")
|
|
|
|
webflag.AddFlags(app, ":9182")
|
|
logflag.AddFlags(app, &log.Config{File: logFile})
|
|
collector.NewWithFlags(app)
|
|
|
|
return app
|
|
}
|
|
|
|
// mainFlagNames returns the names of the flags declared in cmd/windows_exporter.
|
|
// It is a main package, so the names are read from its source.
|
|
func mainFlagNames(t *testing.T) []string {
|
|
t.Helper()
|
|
|
|
files, err := filepath.Glob(filepath.Join("..", "..", "cmd", "windows_exporter", "*.go"))
|
|
require.NoError(t, err)
|
|
|
|
var names []string
|
|
|
|
fset := token.NewFileSet()
|
|
|
|
for _, path := range files {
|
|
if strings.HasSuffix(path, "_test.go") {
|
|
continue
|
|
}
|
|
|
|
file, err := parser.ParseFile(fset, path, nil, parser.SkipObjectResolution)
|
|
require.NoError(t, err)
|
|
|
|
ast.Inspect(file, func(node ast.Node) bool {
|
|
call, ok := node.(*ast.CallExpr)
|
|
if !ok || len(call.Args) == 0 {
|
|
return true
|
|
}
|
|
|
|
if fun, ok := call.Fun.(*ast.SelectorExpr); !ok || fun.Sel.Name != "Flag" {
|
|
return true
|
|
}
|
|
|
|
lit, ok := call.Args[0].(*ast.BasicLit)
|
|
require.True(t, ok && lit.Kind == token.STRING, "%s: the flag name is not a string literal", fset.Position(call.Pos()))
|
|
|
|
name, err := strconv.Unquote(lit.Value)
|
|
require.NoError(t, err)
|
|
|
|
names = append(names, name)
|
|
|
|
return true
|
|
})
|
|
}
|
|
|
|
require.Contains(t, names, "collectors.enabled", "no flags found in cmd/windows_exporter")
|
|
|
|
return names
|
|
}
|
|
|
|
// configFieldType returns the type of the struct field reached by following the
|
|
// yaml keys, starting at typ. block reports whether the field belongs to a block
|
|
// with its own decoding, which takes the value as a string.
|
|
func configFieldType(typ reflect.Type, keys []string) (reflect.Type, bool, bool) {
|
|
if len(keys) == 0 {
|
|
return typ, false, true
|
|
}
|
|
|
|
if typ.Kind() != reflect.Struct {
|
|
return nil, false, false
|
|
}
|
|
|
|
for field := range typ.Fields() {
|
|
if yamlName(field) == keys[0] {
|
|
fieldType, block, ok := configFieldType(field.Type, keys[1:])
|
|
|
|
return fieldType, block || reflect.PointerTo(typ).Implements(reflect.TypeFor[yaml.Unmarshaler]()), ok
|
|
}
|
|
}
|
|
|
|
return nil, false, false
|
|
}
|
|
|
|
// configKeys returns the dotted keys of the leaves of the configuration file
|
|
// schema typ.
|
|
func configKeys(typ reflect.Type, prefix string) []string {
|
|
var keys []string
|
|
|
|
for field := range typ.Fields() {
|
|
key := yamlName(field)
|
|
if key == "" {
|
|
continue
|
|
}
|
|
|
|
if prefix != "" {
|
|
key = prefix + "." + key
|
|
}
|
|
|
|
if field.Type.Kind() == reflect.Struct {
|
|
keys = append(keys, configKeys(field.Type, key)...)
|
|
} else {
|
|
keys = append(keys, key)
|
|
}
|
|
}
|
|
|
|
return keys
|
|
}
|
|
|
|
// yamlName returns the key of field in YAML, or "" if it has none.
|
|
func yamlName(field reflect.StructField) string {
|
|
if !field.IsExported() {
|
|
return ""
|
|
}
|
|
|
|
name, _, _ := strings.Cut(field.Tag.Get("yaml"), ",")
|
|
|
|
switch name {
|
|
case "-":
|
|
return ""
|
|
case "":
|
|
return strings.ToLower(field.Name)
|
|
default:
|
|
return name
|
|
}
|
|
}
|
|
|
|
// sampleConfigValue returns a configuration file value that decodes into typ,
|
|
// together with the values the resolver flattens it to. A field of a block with
|
|
// its own decoding takes the value as a string.
|
|
func sampleConfigValue(t *testing.T, typ reflect.Type, block bool) (any, []string) {
|
|
t.Helper()
|
|
|
|
switch {
|
|
case block:
|
|
return "[]", []string{"[]"}
|
|
case typ == reflect.TypeFor[time.Duration]():
|
|
return "1m", []string{"1m"}
|
|
case typ == reflect.TypeFor[*regexp.Regexp]():
|
|
return "test", []string{"test"}
|
|
}
|
|
|
|
switch typ.Kind() {
|
|
case reflect.Bool:
|
|
return true, []string{"true"}
|
|
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:
|
|
return "test", []string{"test"}
|
|
default:
|
|
t.Fatalf("no sample configuration value for type %s", typ)
|
|
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
// indent prefixes every non-empty line of s.
|
|
func indent(s, prefix string) string {
|
|
lines := strings.Split(strings.TrimRight(s, "\n"), "\n")
|
|
for i, line := range lines {
|
|
if line != "" {
|
|
lines[i] = prefix + line
|
|
}
|
|
}
|
|
|
|
return strings.Join(lines, "\n") + "\n"
|
|
}
|
|
|
|
func TestParse(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
extraArgs []string
|
|
want string
|
|
errorText string
|
|
}{
|
|
{name: "config defaults", want: "debug"},
|
|
{name: "CLI overrides config", extraArgs: []string{"--log.level=error"}, want: "error"},
|
|
{name: "invalid CLI flag", extraArgs: []string{"--unknown"}, errorText: "failed to bind configuration"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
path := filepath.Join(t.TempDir(), "config.yaml")
|
|
require.NoError(t, os.WriteFile(path, []byte("log:\n level: debug\n"), 0o600))
|
|
|
|
app := kingpin.New("test", "test")
|
|
app.Flag("config.file", "config").String()
|
|
level := app.Flag("log.level", "level").Default("info").String()
|
|
|
|
args := append([]string{"--config.file=" + path}, tc.extraArgs...)
|
|
|
|
err := config.Parse(app, args)
|
|
if tc.errorText != "" {
|
|
require.ErrorContains(t, err, tc.errorText)
|
|
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
require.Equal(t, tc.want, *level)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestParseEmptyValue verifies that an empty value in the configuration file
|
|
// replaces a flag's non-empty default instead of keeping it.
|
|
func TestParseEmptyValue(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
config string
|
|
extraArgs []string
|
|
want string
|
|
}{
|
|
{name: "default", config: "collector:\n scheduled_task:\n", want: "/Microsoft/.+"},
|
|
{name: "empty string", config: "collector:\n scheduled_task:\n exclude: \"\"\n", want: ""},
|
|
{
|
|
name: "CLI overrides config", config: "collector:\n scheduled_task:\n exclude: /Vendor/.+\n",
|
|
extraArgs: []string{"--collector.scheduled_task.exclude="}, want: "",
|
|
},
|
|
} {
|
|
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 := kingpin.New("test", "test")
|
|
app.Flag("config.file", "config").String()
|
|
collector.NewWithFlags(app)
|
|
|
|
require.NoError(t, config.Parse(app, append([]string{"--config.file=" + path}, tc.extraArgs...)))
|
|
require.Equal(t, tc.want, app.GetFlag("collector.scheduled_task.exclude").Model().Value.String())
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestRegexpFlags verifies that every include and exclude flag of the
|
|
// collectors accepts a value from the configuration file, and that an invalid
|
|
// expression fails the parsing with an error that names the flag.
|
|
func TestRegexpFlags(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, name := range []string{
|
|
"collector.iis.app-exclude",
|
|
"collector.iis.app-include",
|
|
"collector.iis.site-exclude",
|
|
"collector.iis.site-include",
|
|
"collector.logical_disk.volume-exclude",
|
|
"collector.logical_disk.volume-include",
|
|
"collector.net.nic-exclude",
|
|
"collector.net.nic-include",
|
|
"collector.netframework.process-exclude",
|
|
"collector.netframework.process-include",
|
|
"collector.physical_disk.disk-exclude",
|
|
"collector.physical_disk.disk-include",
|
|
"collector.printer.exclude",
|
|
"collector.printer.include",
|
|
"collector.process.exclude",
|
|
"collector.process.include",
|
|
"collector.scheduled_task.exclude",
|
|
"collector.scheduled_task.include",
|
|
"collector.service.exclude",
|
|
"collector.service.include",
|
|
"collector.smtp.server-exclude",
|
|
"collector.smtp.server-include",
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
app := kingpin.New("test", "test")
|
|
app.Flag("config.file", "config").String()
|
|
collector.NewWithFlags(app)
|
|
|
|
require.NotNil(t, app.GetFlag(name))
|
|
|
|
err := config.Parse(app, []string{"--" + name + "=("})
|
|
require.ErrorContains(t, err, name+": error parsing regexp: missing closing ): `^(?:()$`")
|
|
|
|
parts := strings.SplitN(name, ".", 3)
|
|
path := filepath.Join(t.TempDir(), "config.yaml")
|
|
data := fmt.Sprintf("%s:\n %s:\n %s: \"foo|bar\"\n", parts[0], parts[1], parts[2])
|
|
require.NoError(t, os.WriteFile(path, []byte(data), 0o600))
|
|
|
|
app = kingpin.New("test", "test")
|
|
app.Flag("config.file", "config").String()
|
|
collector.NewWithFlags(app)
|
|
|
|
require.NoError(t, config.Parse(app, []string{"--config.file=" + path}))
|
|
require.Equal(t, "foo|bar", app.GetFlag(name).Model().Value.String())
|
|
|
|
// An invalid expression in the configuration file names its key and line once.
|
|
data = fmt.Sprintf("%s:\n %s:\n %s: \"(\"\n", parts[0], parts[1], parts[2])
|
|
require.NoError(t, os.WriteFile(path, []byte(data), 0o600))
|
|
|
|
app = kingpin.New("test", "test")
|
|
app.Flag("config.file", "config").String()
|
|
collector.NewWithFlags(app)
|
|
|
|
err = config.Parse(app, []string{"--config.file=" + path})
|
|
require.ErrorContains(t, err, "line 3: "+name+": error parsing regexp: missing closing ): `^(?:()$`")
|
|
require.Equal(t, 1, strings.Count(err.Error(), name))
|
|
})
|
|
}
|
|
}
|
|
|
|
// 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())
|
|
})
|
|
}
|
|
}
|