238 lines
6.9 KiB
Go
238 lines
6.9 KiB
Go
// Copyright The OpenTelemetry Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package confmap // import "go.opentelemetry.io/collector/confmap"
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
// 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 (
|
|
// Need to match new line as well in the OpaqueValue, so setting the "s" flag. See https://pkg.go.dev/regexp/syntax.
|
|
uriRegexp = regexp.MustCompile(`(?s:^(?P<Scheme>` + schemePattern + `):(?P<OpaqueValue>.*)$)`)
|
|
|
|
errTooManyRecursiveExpansions = errors.New("too many recursive expansions")
|
|
)
|
|
|
|
func (mr *Resolver) expandValueRecursively(ctx context.Context, value any) (any, error) {
|
|
for i := 0; i < 1000; i++ {
|
|
val, changed, err := mr.expandValue(ctx, value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !changed {
|
|
return val, nil
|
|
}
|
|
value = val
|
|
}
|
|
return nil, errTooManyRecursiveExpansions
|
|
}
|
|
|
|
func (mr *Resolver) expandValue(ctx context.Context, value any) (any, bool, error) {
|
|
switch v := value.(type) {
|
|
case expandedValue:
|
|
expanded, changed, err := mr.expandValue(ctx, v.Value)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
switch exp := expanded.(type) {
|
|
case expandedValue, string:
|
|
// Return expanded values or strings verbatim.
|
|
return exp, changed, nil
|
|
}
|
|
|
|
// At this point we don't know the target field type, so we need to expand the original representation as well.
|
|
originalExpanded, originalChanged, err := mr.expandValue(ctx, v.Original)
|
|
if err != nil {
|
|
// The original representation is not valid, return the expanded value.
|
|
return expanded, changed, nil
|
|
}
|
|
|
|
if originalExpanded, ok := originalExpanded.(string); ok {
|
|
// If the original representation is a string, return the expanded value with the original representation.
|
|
return expandedValue{
|
|
Value: expanded,
|
|
Original: originalExpanded,
|
|
}, changed || originalChanged, nil
|
|
}
|
|
|
|
return expanded, changed, nil
|
|
case string:
|
|
if !strings.Contains(v, "${") || !strings.Contains(v, "}") {
|
|
// No URIs to expand.
|
|
return value, false, nil
|
|
}
|
|
// Embedded or nested URIs.
|
|
return mr.findAndExpandURI(ctx, v)
|
|
case []any:
|
|
nslice := make([]any, 0, len(v))
|
|
nchanged := false
|
|
for _, vint := range v {
|
|
val, changed, err := mr.expandValue(ctx, vint)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
nslice = append(nslice, val)
|
|
nchanged = nchanged || changed
|
|
}
|
|
return nslice, nchanged, nil
|
|
case map[string]any:
|
|
nmap := map[string]any{}
|
|
nchanged := false
|
|
for mk, mv := range v {
|
|
val, changed, err := mr.expandValue(ctx, mv)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
nmap[mk] = val
|
|
nchanged = nchanged || changed
|
|
}
|
|
return nmap, nchanged, nil
|
|
}
|
|
return value, false, nil
|
|
}
|
|
|
|
// findURI attempts to find the first potentially expandable URI in input. It returns a potentially expandable
|
|
// URI, or an empty string if none are found.
|
|
// Note: findURI is only called when input contains a closing bracket.
|
|
// We do not support escaping nested URIs (such as ${env:$${FOO}}, since that would result in an invalid outer URI (${env:${FOO}}).
|
|
func (mr *Resolver) findURI(input string) string {
|
|
closeIndex := strings.Index(input, "}")
|
|
remaining := input[closeIndex+1:]
|
|
openIndex := strings.LastIndex(input[:closeIndex+1], "${")
|
|
|
|
// if there is any of:
|
|
// - a missing "${"
|
|
// - there is no default scheme AND no scheme is detected because no `:` is found.
|
|
// then check the next URI.
|
|
if openIndex < 0 || (mr.defaultScheme == "" && !strings.Contains(input[openIndex:closeIndex+1], ":")) {
|
|
// if remaining does not contain "}", there are no URIs left: stop recursion.
|
|
if !strings.Contains(remaining, "}") {
|
|
return ""
|
|
}
|
|
return mr.findURI(remaining)
|
|
}
|
|
|
|
index := openIndex - 1
|
|
currentRune := '$'
|
|
count := 0
|
|
for index >= 0 && currentRune == '$' {
|
|
currentRune = rune(input[index])
|
|
if currentRune == '$' {
|
|
count++
|
|
}
|
|
index--
|
|
}
|
|
// if we found an odd number of immediately $ preceding ${, then the expansion is escaped
|
|
if count%2 == 1 {
|
|
return ""
|
|
}
|
|
|
|
return input[openIndex : closeIndex+1]
|
|
}
|
|
|
|
// expandedValue holds the YAML parsed value and original representation of a value.
|
|
// It keeps track of the original representation to be used by the 'useExpandValue' hook
|
|
// if the target field is a string. We need to keep both representations because we don't know
|
|
// what the target field type is until `Unmarshal` is called.
|
|
type expandedValue struct {
|
|
// Value is the expanded value.
|
|
Value any
|
|
// Original is the original representation of the value.
|
|
Original string
|
|
}
|
|
|
|
// findAndExpandURI attempts to find and expand the first occurrence of an expandable URI in input. If an expandable URI is found it
|
|
// returns the input with the URI expanded, true and nil. Otherwise, it returns the unchanged input, false and the expanding error.
|
|
// This method expects input to start with ${ and end with }
|
|
func (mr *Resolver) findAndExpandURI(ctx context.Context, input string) (any, bool, error) {
|
|
uri := mr.findURI(input)
|
|
if uri == "" {
|
|
// No URI found, return.
|
|
return input, false, nil
|
|
}
|
|
if uri == input {
|
|
// If the value is a single URI, then the return value can be anything.
|
|
// This is the case `foo: ${file:some_extra_config.yml}`.
|
|
ret, err := mr.expandURI(ctx, input)
|
|
if err != nil {
|
|
return input, false, err
|
|
}
|
|
|
|
val, err := ret.AsRaw()
|
|
if err != nil {
|
|
return input, false, err
|
|
}
|
|
|
|
if asStr, err2 := ret.AsString(); err2 == nil {
|
|
return expandedValue{
|
|
Value: val,
|
|
Original: asStr,
|
|
}, true, nil
|
|
}
|
|
|
|
return val, true, nil
|
|
}
|
|
expanded, err := mr.expandURI(ctx, uri)
|
|
if err != nil {
|
|
return input, false, err
|
|
}
|
|
|
|
repl, err := expanded.AsString()
|
|
if err != nil {
|
|
return input, false, fmt.Errorf("expanding %v: %w", uri, err)
|
|
}
|
|
return strings.ReplaceAll(input, uri, repl), true, err
|
|
}
|
|
|
|
func (mr *Resolver) expandURI(ctx context.Context, input string) (*Retrieved, error) {
|
|
// strip ${ and }
|
|
uri := input[2 : len(input)-1]
|
|
|
|
if !strings.Contains(uri, ":") {
|
|
uri = fmt.Sprintf("%s:%s", mr.defaultScheme, uri)
|
|
}
|
|
|
|
lURI, err := newLocation(uri)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if strings.Contains(lURI.opaqueValue, "$") {
|
|
return nil, fmt.Errorf("the uri %q contains unsupported characters ('$')", lURI.asString())
|
|
}
|
|
ret, err := mr.retrieveValue(ctx, lURI)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
mr.closers = append(mr.closers, ret.Close)
|
|
return ret, nil
|
|
}
|
|
|
|
type location struct {
|
|
scheme string
|
|
opaqueValue string
|
|
}
|
|
|
|
func (c location) asString() string {
|
|
return c.scheme + ":" + c.opaqueValue
|
|
}
|
|
|
|
func newLocation(uri string) (location, error) {
|
|
submatches := uriRegexp.FindStringSubmatch(uri)
|
|
if len(submatches) != 3 {
|
|
return location{}, fmt.Errorf("invalid uri: %q", uri)
|
|
}
|
|
return location{scheme: submatches[1], opaqueValue: submatches[2]}, nil
|
|
}
|