Skip to content

Commit

Permalink
Add support for concatenating multiple embedded uris, or uris with ot…
Browse files Browse the repository at this point in the history
…her string parts

Signed-off-by: Bogdan Drutu <[email protected]>
  • Loading branch information
bogdandrutu committed Jan 29, 2023
1 parent e520829 commit a9ff48f
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 26 deletions.
11 changes: 11 additions & 0 deletions .chloggen/supportconcat.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
change_type: enhancement

# The name of the component, or a single word describing the area of concern, (e.g. otlpreceiver)
component: confmap

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: Add support to resolve embedded uris inside a string, concatenate results.

# One or more tracking issues or pull requests related to the change
issues: [6932]
86 changes: 60 additions & 26 deletions confmap/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,24 +26,30 @@ import (
"go.opentelemetry.io/collector/featuregate"
)

// schemePattern defines the regexp pattern for scheme names.
// Scheme name consist of a sequence of characters beginning with a letter and followed by any
// combination of letters, digits, plus ("+"), period ("."), or hyphen ("-").
const schemePattern = `[A-Za-z][A-Za-z0-9+.-]+`

var (
// follows drive-letter specification:
// https://datatracker.ietf.org/doc/html/draft-kerwin-file-scheme-07.html#section-2.2
driverLetterRegexp = regexp.MustCompile("^[A-z]:")

// Scheme name consist of a sequence of characters beginning with a letter and followed by any
// combination of letters, digits, plus ("+"), period ("."), or hyphen ("-").
// Need to match new line as well in the OpaqueValue, so setting the "s" flag. See https://pkg.go.dev/regexp/syntax.
locationRegexp = regexp.MustCompile(`(?s:^(?P<Scheme>[A-Za-z][A-Za-z0-9+.-]+):(?P<OpaqueValue>.*)$)`)
uriRegexp = regexp.MustCompile(`(?s:^(?P<Scheme>` + schemePattern + `):(?P<OpaqueValue>.*)$)`)

// embeddedURI matches "embedded" provider uris into a string value.
embeddedURI = regexp.MustCompile(`\${` + schemePattern + `:.*?}`)

errTooManyRecursiveExpansions = errors.New("too many recursive expansions")
)

// TODO: Remove this if by v0.64.0 no complains from distros.
var expandEnabledGauge = featuregate.GlobalRegistry().MustRegister(
"confmap.expandEnabled",
featuregate.StageBeta,
featuregate.WithRegisterDescription("controls whether expending embedded external config providers URIs"))
featuregate.StageStable,
featuregate.WithRegisterDescription("controls whether expending embedded external config providers URIs"),
featuregate.WithRegisterRemovalVersion("v0.71.0"))

// Resolver resolves a configuration as a Conf.
type Resolver struct {
Expand Down Expand Up @@ -168,6 +174,7 @@ func (mr *Resolver) Resolve(ctx context.Context) (*Conf, error) {
}
retMap = NewFromStringMap(cfgMap)
}

// Apply the converters in the given order.
for _, confConv := range mr.converters {
if err := confConv.Convert(ctx, retMap); err != nil {
Expand Down Expand Up @@ -234,28 +241,38 @@ func (mr *Resolver) expandValueRecursively(ctx context.Context, value interface{
func (mr *Resolver) expandValue(ctx context.Context, value interface{}) (interface{}, bool, error) {
switch v := value.(type) {
case string:
// If it doesn't have the format "${scheme:opaque}" no need to expand.
if !strings.HasPrefix(v, "${") || !strings.HasSuffix(v, "}") || !strings.Contains(v, ":") {
return value, false, nil
}
lURI, err := newLocation(v[2 : len(v)-1])
if err != nil {
// Cannot return error, since a case like "${HOST}:${PORT}" is invalid location,
// but is supported in the legacy implementation.
// If no embedded "uris" no need to expand. embeddedURI regexp matches uriRegexp as well.
if !embeddedURI.MatchString(v) {
return value, false, nil
}
if strings.Contains(lURI.opaqueValue, "$") {
return nil, false, fmt.Errorf("the uri %q contains unsupported characters ('$')", lURI.asString())
}
ret, err := mr.retrieveValue(ctx, lURI)
if err != nil {
return nil, false, err

// If the value is a single URI, then the return value can be anything.
// This is the case `foo: ${file:some_extra_config.yml}`.
if uriRegexp.MatchString(v) {
return mr.expandStringURI(ctx, v)
}
mr.closers = append(mr.closers, ret.Close)
val, err := ret.AsRaw()
return val, true, err
case []interface{}:
nslice := make([]interface{}, 0, len(v))

// If the URI is embedded into the string, return value must be a string, and we have to concatenate all strings.
var nerr error
var nchanged bool
nv := embeddedURI.ReplaceAllStringFunc(v, func(s string) string {
ret, changed, err := mr.expandStringURI(ctx, s)
nchanged = nchanged || changed
nerr = multierr.Append(nerr, err)
if err != nil {
return ""
}
switch val := ret.(type) {
case string:
return val
default:
nerr = multierr.Append(nerr, fmt.Errorf("expending %v, expected string or []byte value type, got %T", s, val))
return v
}
})
return nv, nchanged, nerr
case []any:
nslice := make([]any, 0, len(v))
nchanged := false
for _, vint := range v {
val, changed, err := mr.expandValue(ctx, vint)
Expand All @@ -282,6 +299,23 @@ func (mr *Resolver) expandValue(ctx context.Context, value interface{}) (interfa
return value, false, nil
}

func (mr *Resolver) expandStringURI(ctx context.Context, uri string) (any, bool, error) {
lURI, err := newLocation(uri[2 : len(uri)-1])
if err != nil {
return nil, false, err
}
if strings.Contains(lURI.opaqueValue, "$") {
return nil, false, fmt.Errorf("the uri %q contains unsupported characters ('$')", lURI.asString())
}
ret, err := mr.retrieveValue(ctx, lURI)
if err != nil {
return nil, false, err
}
mr.closers = append(mr.closers, ret.Close)
val, err := ret.AsRaw()
return val, true, err
}

type location struct {
scheme string
opaqueValue string
Expand All @@ -292,7 +326,7 @@ func (c location) asString() string {
}

func newLocation(uri string) (location, error) {
submatches := locationRegexp.FindStringSubmatch(uri)
submatches := uriRegexp.FindStringSubmatch(uri)
if len(submatches) != 3 {
return location{}, fmt.Errorf("invalid uri: %q", uri)
}
Expand Down
37 changes: 37 additions & 0 deletions confmap/resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,43 @@ func TestResolverExpandMapAndSliceValues(t *testing.T) {
assert.Equal(t, expectedMap, cfgMap.ToStringMap())
}

func TestResolverExpandStringValues(t *testing.T) {
provider := newFakeProvider("input", func(context.Context, string, WatcherFunc) (*Retrieved, error) {
return NewRetrieved(map[string]any{
"test_no_match": "${HOST}:${PORT}",
"test_match_value": "${env:HOST_PORT}",
"test_match_embedded": "${env:HOST}:3043",
"test_match_embedded_multi": "${env:HOST}:${env:PORT}",
})
})

testProvider := newFakeProvider("env", func(_ context.Context, uri string, _ WatcherFunc) (*Retrieved, error) {
switch uri {
case "env:HOST_PORT":
return NewRetrieved("localhost:3042")
case "env:HOST":
return NewRetrieved("localhost")
case "env:PORT":
return NewRetrieved("3044")

}
return nil, errors.New("impossible")
})

resolver, err := NewResolver(ResolverSettings{URIs: []string{"input:"}, Providers: makeMapProvidersMap(provider, testProvider), Converters: nil})
require.NoError(t, err)

cfgMap, err := resolver.Resolve(context.Background())
require.NoError(t, err)
expectedMap := map[string]any{
"test_no_match": "${HOST}:${PORT}",
"test_match_value": "localhost:3042",
"test_match_embedded": "localhost:3043",
"test_match_embedded_multi": "localhost:3044",
}
assert.Equal(t, expectedMap, cfgMap.ToStringMap())
}

func TestResolverInfiniteExpand(t *testing.T) {
const receiverValue = "${test:VALUE}"
provider := newFakeProvider("input", func(context.Context, string, WatcherFunc) (*Retrieved, error) {
Expand Down

0 comments on commit a9ff48f

Please sign in to comment.