opentelemetry-collector/confmap/expand_test.go

566 lines
17 KiB
Go

// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package confmap // import "go.opentelemetry.io/collector/confmap"
import (
"context"
"errors"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestResolverExpandEnvVars(t *testing.T) {
testCases := []struct {
name string // test case name (also file name containing config yaml)
}{
{name: "expand-with-no-env.yaml"},
{name: "expand-with-partial-env.yaml"},
{name: "expand-with-all-env.yaml"},
}
envs := map[string]string{
"EXTRA": "some string",
"EXTRA_MAP_VALUE_1": "some map value_1",
"EXTRA_MAP_VALUE_2": "some map value_2",
"EXTRA_LIST_MAP_VALUE_1": "some list map value_1",
"EXTRA_LIST_MAP_VALUE_2": "some list map value_2",
"EXTRA_LIST_VALUE_1": "some list value_1",
"EXTRA_LIST_VALUE_2": "some list value_2",
}
expectedCfgMap := newConfFromFile(t, filepath.Join("testdata", "expand-with-no-env.yaml"))
fileProvider := newFakeProvider("file", func(_ context.Context, uri string, _ WatcherFunc) (*Retrieved, error) {
return NewRetrieved(newConfFromFile(t, uri[5:]))
})
envProvider := newFakeProvider("env", func(_ context.Context, uri string, _ WatcherFunc) (*Retrieved, error) {
return NewRetrieved(envs[uri[4:]])
})
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
resolver, err := NewResolver(ResolverSettings{URIs: []string{filepath.Join("testdata", tt.name)}, ProviderFactories: []ProviderFactory{fileProvider, envProvider}, ConverterFactories: nil})
require.NoError(t, err)
// Test that expanded configs are the same with the simple config with no env vars.
cfgMap, err := resolver.Resolve(context.Background())
require.NoError(t, err)
assert.Equal(t, expectedCfgMap, cfgMap.ToStringMap())
})
}
}
func TestResolverDoneNotExpandOldEnvVars(t *testing.T) {
expectedCfgMap := map[string]any{"test.1": "${EXTRA}", "test.2": "$EXTRA", "test.3": "${EXTRA}:${EXTRA}"}
fileProvider := newFakeProvider("test", func(context.Context, string, WatcherFunc) (*Retrieved, error) {
return NewRetrieved(expectedCfgMap)
})
envProvider := newFakeProvider("env", func(context.Context, string, WatcherFunc) (*Retrieved, error) {
return NewRetrieved("some string")
})
resolver, err := NewResolver(ResolverSettings{URIs: []string{"test:"}, ProviderFactories: []ProviderFactory{fileProvider, envProvider}, ConverterFactories: nil})
require.NoError(t, err)
// Test that expanded configs are the same with the simple config with no env vars.
cfgMap, err := resolver.Resolve(context.Background())
require.NoError(t, err)
assert.Equal(t, expectedCfgMap, cfgMap.ToStringMap())
}
func TestResolverExpandMapAndSliceValues(t *testing.T) {
provider := newFakeProvider("input", func(context.Context, string, WatcherFunc) (*Retrieved, error) {
return NewRetrieved(map[string]any{
"test_map": map[string]any{"recv": "${test:MAP_VALUE}"},
"test_slice": []any{"${test:MAP_VALUE}"},
})
})
const receiverExtraMapValue = "some map value"
testProvider := newFakeProvider("test", func(context.Context, string, WatcherFunc) (*Retrieved, error) {
return NewRetrieved(receiverExtraMapValue)
})
resolver, err := NewResolver(ResolverSettings{URIs: []string{"input:"}, ProviderFactories: []ProviderFactory{provider, testProvider}, ConverterFactories: nil})
require.NoError(t, err)
cfgMap, err := resolver.Resolve(context.Background())
require.NoError(t, err)
expectedMap := map[string]any{
"test_map": map[string]any{"recv": receiverExtraMapValue},
"test_slice": []any{receiverExtraMapValue},
}
assert.Equal(t, expectedMap, cfgMap.ToStringMap())
}
func TestResolverExpandStringValues(t *testing.T) {
tests := []struct {
name string
input string
output any
defaultProvider bool
}{
// Embedded.
{
name: "NoMatchOldStyle",
input: "${HOST}:${PORT}",
output: "${HOST}:${PORT}",
},
{
name: "NoMatchOldStyleDefaultProvider",
input: "${HOST}:${PORT}",
output: "localhost:3044",
defaultProvider: true,
},
{
name: "NoMatchOldStyleNoBrackets",
input: "${HOST}:$PORT",
output: "${HOST}:$PORT",
},
{
name: "NoMatchOldStyleNoBracketsDefaultProvider",
input: "${HOST}:$PORT",
output: "localhost:$PORT",
defaultProvider: true,
},
{
name: "ComplexValue",
input: "${env:COMPLEX_VALUE}",
output: []any{"localhost:3042"},
},
{
name: "Embedded",
input: "${env:HOST}:3043",
output: "localhost:3043",
},
{
name: "EmbeddedMulti",
input: "${env:HOST}:${env:PORT}",
output: "localhost:3044",
},
{
name: "EmbeddedConcat",
input: "https://${env:HOST}:3045",
output: "https://localhost:3045",
},
{
name: "EmbeddedNewAndOldStyle",
input: "${env:HOST}:${PORT}",
output: "localhost:${PORT}",
},
{
name: "EmbeddedNewAndOldStyleDefaultProvider",
input: "${env:HOST}:${PORT}",
output: "localhost:3044",
defaultProvider: true,
},
{
name: "Int",
input: "test_${env:INT}",
output: "test_1",
},
{
name: "Int32",
input: "test_${env:INT32}",
output: "test_32",
},
{
name: "Int64",
input: "test_${env:INT64}",
output: "test_64",
},
{
name: "Float32",
input: "test_${env:FLOAT32}",
output: "test_3.25",
},
{
name: "Float64",
input: "test_${env:FLOAT64}",
output: "test_6.4",
},
{
name: "Bool",
input: "test_${env:BOOL}",
output: "test_true",
},
{
name: "Timestamp",
input: "test_${env:TIMESTAMP}",
output: "test_2023-03-20T03:17:55.432328Z",
},
{
name: "MultipleSameMatches",
input: "test_${env:BOOL}_test_${env:BOOL}",
output: "test_true_test_true",
},
// Nested.
{
name: "Nested",
input: "${test:localhost:${env:PORT}}",
output: "localhost:3044",
},
{
name: "NestedDefaultProvider",
input: "${test:localhost:${PORT}}",
output: "localhost:3044",
defaultProvider: true,
},
{
name: "EmbeddedInNested",
input: "${test:${env:HOST}:${env:PORT}}",
output: "localhost:3044",
},
{
name: "EmbeddedInNestedDefaultProvider",
input: "${test:${HOST}:${PORT}}",
output: "localhost:3044",
defaultProvider: true,
},
{
name: "EmbeddedAndNested",
input: "${test:localhost:${env:PORT}}?os=${env:OS}",
output: "localhost:3044?os=ubuntu",
},
{
name: "NestedMultiple",
input: "${test:1${test:2${test:3${test:4${test:5${test:6}}}}}}",
output: "123456",
},
// No expand.
{
name: "NoMatchMissingOpeningBracket",
input: "env:HOST}",
output: "env:HOST}",
},
{
name: "NoMatchMissingOpeningBracketDefaultProvider",
input: "env:HOST}",
output: "env:HOST}",
defaultProvider: true,
},
{
name: "NoMatchMissingClosingBracket",
input: "${HOST",
output: "${HOST",
},
{
name: "NoMatchMissingClosingBracketDefaultProvider",
input: "${HOST",
output: "${HOST",
defaultProvider: true,
},
{
name: "NoMatchBracketsWithout$",
input: "HO{ST}",
output: "HO{ST}",
},
{
name: "NoMatchBracketsWithout$DefaultProvider",
input: "HO{ST}",
output: "HO{ST}",
defaultProvider: true,
},
{
name: "NoMatchOnlyMissingClosingBracket",
input: "${env:HOST${env:PORT?os=${env:OS",
output: "${env:HOST${env:PORT?os=${env:OS",
},
{
name: "NoMatchOnlyMissingClosingBracketDefaultProvider",
input: "${env:HOST${env:PORT?os=${env:OS",
output: "${env:HOST${env:PORT?os=${env:OS",
defaultProvider: true,
},
{
name: "NoMatchOnlyMissingOpeningBracket",
input: "env:HOST}env:PORT}?os=env:OS}",
output: "env:HOST}env:PORT}?os=env:OS}",
},
{
name: "NoMatchOnlyMissingOpeningBracketDefaultProvider",
input: "env:HOST}env:PORT}?os=env:OS}",
output: "env:HOST}env:PORT}?os=env:OS}",
defaultProvider: true,
},
{
name: "NoMatchCloseBeforeOpen",
input: "env:HOST}${env:PORT",
output: "env:HOST}${env:PORT",
},
{
name: "NoMatchCloseBeforeOpenDefaultProvider",
input: "env:HOST}${env:PORT",
output: "env:HOST}${env:PORT",
defaultProvider: true,
},
{
name: "NoMatchOldStyleNested",
input: "${test:localhost:${PORT}}",
output: "${test:localhost:${PORT}}",
},
// Partial expand.
{
name: "PartialMatchMissingOpeningBracketFirst",
input: "env:HOST}${env:PORT}",
output: "env:HOST}3044",
},
{
name: "PartialMatchMissingOpeningBracketFirstDefaultProvider",
input: "env:HOST}${PORT}",
output: "env:HOST}3044",
defaultProvider: true,
},
{
name: "PartialMatchMissingOpeningBracketLast",
input: "${env:HOST}env:PORT}",
output: "localhostenv:PORT}",
},
{
name: "PartialMatchMissingClosingBracketFirst",
input: "${env:HOST${env:PORT}",
output: "${env:HOST3044",
},
{
name: "PartialMatchMissingClosingBracketLast",
input: "${env:HOST}${env:PORT",
output: "localhost${env:PORT",
},
{
name: "PartialMatchMultipleMissingOpen",
input: "env:HOST}env:PORT}?os=${env:OS}",
output: "env:HOST}env:PORT}?os=ubuntu",
},
{
name: "PartialMatchMultipleMissingClosing",
input: "${env:HOST${env:PORT?os=${env:OS}",
output: "${env:HOST${env:PORT?os=ubuntu",
},
{
name: "PartialMatchMoreClosingBrackets",
input: "${env:HOST}}}}}}${env:PORT?os=${env:OS}",
output: "localhost}}}}}${env:PORT?os=ubuntu",
},
{
name: "PartialMatchMoreOpeningBrackets",
input: "${env:HOST}${${${${${env:PORT}?os=${env:OS}",
output: "localhost${${${${3044?os=ubuntu",
},
{
name: "PartialMatchAlternatingMissingOpening",
input: "env:HOST}${env:PORT}?os=env:OS}&pr=${env:PR}",
output: "env:HOST}3044?os=env:OS}&pr=amd",
},
{
name: "PartialMatchAlternatingMissingClosing",
input: "${env:HOST${env:PORT}?os=${env:OS&pr=${env:PR}",
output: "${env:HOST3044?os=${env:OS&pr=amd",
},
{
name: "SchemeAfterNoSchemeIsExpanded",
input: "${HOST}${env:PORT}",
output: "${HOST}3044",
},
{
name: "SchemeAfterNoSchemeIsExpandedDefaultProvider",
input: "${HOST}${env:PORT}",
output: "localhost3044",
defaultProvider: true,
},
{
name: "SchemeBeforeNoSchemeIsExpanded",
input: "${env:HOST}${PORT}",
output: "localhost${PORT}",
},
{
name: "SchemeBeforeNoSchemeIsExpandedDefaultProvider",
input: "${env:HOST}${PORT}",
output: "localhost3044",
defaultProvider: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
provider := newFakeProvider("input", func(context.Context, string, WatcherFunc) (*Retrieved, error) {
return NewRetrieved(map[string]any{tt.name: tt.input})
})
testProvider := newFakeProvider("test", func(_ context.Context, uri string, _ WatcherFunc) (*Retrieved, error) {
return NewRetrieved(uri[5:])
})
envProvider := newEnvProvider()
set := ResolverSettings{URIs: []string{"input:"}, ProviderFactories: []ProviderFactory{provider, envProvider, testProvider}, ConverterFactories: nil}
if tt.defaultProvider {
set.DefaultScheme = "env"
}
resolver, err := NewResolver(set)
require.NoError(t, err)
cfgMap, err := resolver.Resolve(context.Background())
require.NoError(t, err)
assert.Equal(t, map[string]any{tt.name: tt.output}, cfgMap.ToStringMap())
})
}
}
func newEnvProvider() ProviderFactory {
return newFakeProvider("env", func(_ context.Context, uri string, _ WatcherFunc) (*Retrieved, error) {
// When using `env` as the default scheme for tests, the uri will not include `env:`.
// Instead of duplicating the switch cases, the scheme is added instead.
if uri[0:4] != "env:" {
uri = "env:" + uri
}
switch uri {
case "env:COMPLEX_VALUE":
return NewRetrievedFromYAML([]byte("[localhost:3042]"))
case "env:HOST":
return NewRetrievedFromYAML([]byte("localhost"))
case "env:TIMESTAMP":
return NewRetrievedFromYAML([]byte("2023-03-20T03:17:55.432328Z"))
case "env:OS":
return NewRetrievedFromYAML([]byte("ubuntu"))
case "env:PR":
return NewRetrievedFromYAML([]byte("amd"))
case "env:PORT":
return NewRetrievedFromYAML([]byte("3044"))
case "env:INT":
return NewRetrievedFromYAML([]byte("1"))
case "env:INT32":
return NewRetrieved(int32(32), withStringRepresentation("32"))
case "env:INT64":
return NewRetrieved(int64(64), withStringRepresentation("64"))
case "env:FLOAT32":
return NewRetrieved(float32(3.25), withStringRepresentation("3.25"))
case "env:FLOAT64":
return NewRetrieved(float64(6.4), withStringRepresentation("6.4"))
case "env:BOOL":
return NewRetrievedFromYAML([]byte("true"))
}
return nil, errors.New("impossible")
})
}
func TestResolverExpandReturnError(t *testing.T) {
tests := []struct {
name string
input any
}{
{
name: "string_value",
input: "${test:VALUE}",
},
{
name: "slice_value",
input: []any{"${test:VALUE}"},
},
{
name: "map_value",
input: map[string]any{"test": "${test:VALUE}"},
},
{
name: "string_embedded_value",
input: "https://${test:HOST}:3045",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
provider := newFakeProvider("input", func(context.Context, string, WatcherFunc) (*Retrieved, error) {
return NewRetrieved(map[string]any{tt.name: tt.input})
})
myErr := errors.New(tt.name)
testProvider := newFakeProvider("test", func(context.Context, string, WatcherFunc) (*Retrieved, error) {
return nil, myErr
})
resolver, err := NewResolver(ResolverSettings{URIs: []string{"input:"}, ProviderFactories: []ProviderFactory{provider, testProvider}, ConverterFactories: nil})
require.NoError(t, err)
_, err = resolver.Resolve(context.Background())
assert.ErrorIs(t, err, myErr)
})
}
}
func TestResolverInfiniteExpand(t *testing.T) {
const receiverValue = "${test:VALUE}"
provider := newFakeProvider("input", func(context.Context, string, WatcherFunc) (*Retrieved, error) {
return NewRetrieved(map[string]any{"test": receiverValue})
})
testProvider := newFakeProvider("test", func(context.Context, string, WatcherFunc) (*Retrieved, error) {
return NewRetrieved(receiverValue)
})
resolver, err := NewResolver(ResolverSettings{URIs: []string{"input:"}, ProviderFactories: []ProviderFactory{provider, testProvider}, ConverterFactories: nil})
require.NoError(t, err)
_, err = resolver.Resolve(context.Background())
assert.ErrorIs(t, err, errTooManyRecursiveExpansions)
}
func TestResolverExpandInvalidScheme(t *testing.T) {
provider := newFakeProvider("input", func(context.Context, string, WatcherFunc) (*Retrieved, error) {
return NewRetrieved(map[string]any{"test": "${g_c_s:VALUE}"})
})
testProvider := newFakeProvider("g_c_s", func(context.Context, string, WatcherFunc) (*Retrieved, error) {
panic("must not be called")
})
_, err := NewResolver(ResolverSettings{URIs: []string{"input:"}, ProviderFactories: []ProviderFactory{provider, testProvider}, ConverterFactories: nil})
assert.ErrorContains(t, err, "invalid 'confmap.Provider' scheme")
}
func TestResolverExpandInvalidOpaqueValue(t *testing.T) {
provider := newFakeProvider("input", func(context.Context, string, WatcherFunc) (*Retrieved, error) {
return NewRetrieved(map[string]any{"test": []any{map[string]any{"test": "${test:$VALUE}"}}})
})
testProvider := newFakeProvider("test", func(context.Context, string, WatcherFunc) (*Retrieved, error) {
panic("must not be called")
})
resolver, err := NewResolver(ResolverSettings{URIs: []string{"input:"}, ProviderFactories: []ProviderFactory{provider, testProvider}, ConverterFactories: nil})
require.NoError(t, err)
_, err = resolver.Resolve(context.Background())
assert.EqualError(t, err, `the uri "test:$VALUE" contains unsupported characters ('$')`)
}
func TestResolverExpandUnsupportedScheme(t *testing.T) {
provider := newFakeProvider("input", func(context.Context, string, WatcherFunc) (*Retrieved, error) {
return NewRetrieved(map[string]any{"test": "${unsupported:VALUE}"})
})
testProvider := newFakeProvider("test", func(context.Context, string, WatcherFunc) (*Retrieved, error) {
panic("must not be called")
})
resolver, err := NewResolver(ResolverSettings{URIs: []string{"input:"}, ProviderFactories: []ProviderFactory{provider, testProvider}, ConverterFactories: nil})
require.NoError(t, err)
_, err = resolver.Resolve(context.Background())
assert.EqualError(t, err, `scheme "unsupported" is not supported for uri "unsupported:VALUE"`)
}
func TestResolverDefaultProviderExpand(t *testing.T) {
provider := newFakeProvider("input", func(context.Context, string, WatcherFunc) (*Retrieved, error) {
return NewRetrieved(map[string]any{"foo": "${HOST}"})
})
resolver, err := NewResolver(ResolverSettings{URIs: []string{"input:"}, ProviderFactories: []ProviderFactory{provider, newEnvProvider()}, DefaultScheme: "env", ConverterFactories: nil})
require.NoError(t, err)
cfgMap, err := resolver.Resolve(context.Background())
require.NoError(t, err)
assert.Equal(t, map[string]any{"foo": "localhost"}, cfgMap.ToStringMap())
}