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