internal/pkg/agent/transpiler/vars.go (251 lines of code) (raw):

// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. package transpiler import ( "errors" "fmt" "regexp" "strings" "unicode" "github.com/elastic/elastic-agent-libs/mapstr" "github.com/elastic/elastic-agent/internal/pkg/core/composable" ) const varsSeparator = "." var varsRegex = regexp.MustCompile(`\$\$?{([\p{L}\d\s\\\-_|.'":\/]*)}`) // ErrNoMatch is return when the replace didn't fail, just that no vars match to perform the replace. var ErrNoMatch = errors.New("no matching vars") // Vars is a context of variables that also contain a list of processors that go with the mapping. type Vars struct { id string tree *AST processorsKey string processors Processors fetchContextProviders mapstr.M defaultProvider string } // NewVars returns a new instance of vars. func NewVars(id string, mapping map[string]interface{}, fetchContextProviders mapstr.M, defaultProvider string) (*Vars, error) { return NewVarsWithProcessors(id, mapping, "", nil, fetchContextProviders, defaultProvider) } // NewVarsFromAst returns a new instance of vars. It takes the mapping as an *AST. func NewVarsFromAst(id string, tree *AST, fetchContextProviders mapstr.M, defaultProvider string) *Vars { return &Vars{id, tree, "", nil, fetchContextProviders, defaultProvider} } // NewVarsWithProcessors returns a new instance of vars with attachment of processors. func NewVarsWithProcessors(id string, mapping map[string]interface{}, processorKey string, processors Processors, fetchContextProviders mapstr.M, defaultProvider string) (*Vars, error) { tree, err := NewAST(mapping) if err != nil { return nil, err } return &Vars{id, tree, processorKey, processors, fetchContextProviders, defaultProvider}, nil } // NewVarsWithProcessorsFromAst returns a new instance of vars with attachment of processors. It takes the mapping as an *AST. func NewVarsWithProcessorsFromAst(id string, tree *AST, processorKey string, processors Processors, fetchContextProviders mapstr.M, defaultProvider string) *Vars { return &Vars{id, tree, processorKey, processors, fetchContextProviders, defaultProvider} } // Replace returns a new value based on variable replacement. func (v *Vars) Replace(value string) (Node, error) { return replaceVars(value, func(variable string) (Node, Processors, bool) { var processors Processors node, ok := v.lookupNode(variable) if ok && v.processorsKey != "" && varPrefixMatched(variable, v.processorsKey) { processors = v.processors } return node, processors, ok }, true, v.defaultProvider) } // ID returns the unique ID for the vars. func (v *Vars) ID() string { return v.id } // Lookup returns the value from the vars. func (v *Vars) Lookup(name string) (interface{}, bool) { // lookup in the AST tree return v.tree.Lookup(name) } // Map transforms the variables into a map[string]interface{} and will abort and return any errors related // to type conversion. func (v *Vars) Map() (map[string]interface{}, error) { return v.tree.Map() } // lookupNode performs a lookup on the AST, but keeps the result as a `Node`. // // This is different from `Lookup` which returns the actual type, not the AST type. func (v *Vars) lookupNode(name string) (Node, bool) { // check if the value can be retrieved from a FetchContextProvider for providerName, provider := range v.fetchContextProviders { if varPrefixMatched(name, providerName) { fetchProvider, ok := provider.(composable.FetchContextProvider) if !ok { return &StrVal{value: ""}, false } fval, found := fetchProvider.Fetch(name) if found { return &StrVal{value: fval}, true } return &StrVal{value: ""}, false } } // lookup in the AST tree return Lookup(v.tree, name) } func replaceVars(value string, replacer func(variable string) (Node, Processors, bool), reqMatch bool, defaultProvider string) (Node, error) { var processors Processors matchIdxs := varsRegex.FindAllSubmatchIndex([]byte(value), -1) if !validBrackets(value, matchIdxs) { return nil, fmt.Errorf("starting ${ is missing ending }") } result := "" lastIndex := 0 for _, r := range matchIdxs { for i := 0; i < len(r); i += 4 { if value[r[i]+1] == '$' { // match on an escaped var, append the raw string with the '$' prefix removed result += value[lastIndex:r[0]] + value[r[i]+1:r[i+1]] lastIndex = r[1] continue } // match on a non-escaped var vars, err := extractVars(value[r[i+2]:r[i+3]], defaultProvider) if err != nil { return nil, fmt.Errorf(`error parsing variable "%s": %w`, value[r[i]:r[i+1]], err) } set := false for _, val := range vars { switch val.(type) { case *constString: result += value[lastIndex:r[0]] + val.Value() set = true case *varString: node, nodeProcessors, ok := replacer(val.Value()) if ok { node := nodeToValue(node) if nodeProcessors != nil { processors = nodeProcessors } if r[i] == 0 && r[i+1] == len(value) { // possible for complete replacement of object, because the variable // is not inside of a string return attachProcessors(node, processors), nil } result += value[lastIndex:r[0]] + node.String() set = true } } if set { break } } if !set && reqMatch { return NewStrVal(""), fmt.Errorf("%w: %s", ErrNoMatch, toRepresentation(vars)) } lastIndex = r[1] } } return NewStrValWithProcessors(result+value[lastIndex:], processors), nil } func toRepresentation(vars []varI) string { var sb strings.Builder sb.WriteString("${") for i, val := range vars { switch val.(type) { case *constString: sb.WriteString(`'`) sb.WriteString(val.Value()) sb.WriteString(`'`) case *varString: sb.WriteString(val.Value()) if i < len(vars)-1 { sb.WriteString("|") } } } sb.WriteString("}") return sb.String() } // nodeToValue ensures that the node is an actual value. func nodeToValue(node Node) Node { switch n := node.(type) { case *Key: return n.value } return node } // validBrackets returns true when all starting {$ have a matching ending }. func validBrackets(s string, matchIdxs [][]int) bool { result := "" lastIndex := 0 match := false for _, r := range matchIdxs { match = true for i := 0; i < len(r); i += 4 { result += s[lastIndex:r[0]] lastIndex = r[1] } } if !match { return !strings.Contains(s, "${") } return !strings.Contains(result, "${") } type varI interface { Value() string } type varString struct { value string } func (v *varString) Value() string { return v.value } type constString struct { value string } func (v *constString) Value() string { return v.value } func extractVars(i string, defaultProvider string) ([]varI, error) { const out = rune(0) quote := out constant := false escape := false is := make([]rune, 0, len(i)) res := make([]varI, 0) for _, r := range i { if r == '|' { if escape { return nil, fmt.Errorf(`variable pipe cannot be escaped; remove \ before |`) } if quote == out { if constant { res = append(res, &constString{string(is)}) } else if len(is) > 0 { if is[len(is)-1] == '.' { return nil, fmt.Errorf("variable cannot end with '.'") } res = append(res, &varString{maybeAddDefaultProvider(string(is), defaultProvider)}) } is = is[:0] // slice to zero length; to keep allocated memory constant = false } else { is = append(is, r) } continue } if !escape && (r == '"' || r == '\'') { if quote == out { // start of unescaped quote quote = r constant = true } else if quote == r { // end of unescaped quote quote = out } else { is = append(is, r) } continue } // escape because of backslash (\); except when it is the second backslash of a pair escape = !escape && r == '\\' if r == '\\' { if !escape { is = append(is, r) } } else if quote != out || !unicode.IsSpace(r) { is = append(is, r) } } if quote != out { return nil, fmt.Errorf(`starting %s is missing ending %s`, string(quote), string(quote)) } if constant { res = append(res, &constString{string(is)}) } else if len(is) > 0 { if is[len(is)-1] == '.' { return nil, fmt.Errorf("variable cannot end with '.'") } res = append(res, &varString{maybeAddDefaultProvider(string(is), defaultProvider)}) } return res, nil } func varPrefixMatched(val string, key string) bool { s := strings.SplitN(val, varsSeparator, 2) return s[0] == key } // maybeAddDefaultProvider adds a defaultProvide as a prefix on the value only in the case that // the defaultProvider is set and the val doesn't contain any varsSeparator. // // This is done here and not at resolve time of the variable because the Observe flow of the AST // for the variables provider needs to known exactly which providers to run. It also is an issue with // using fetch providers because we would have to hit each to determine if that variable was present first // before apply the default and we do not want that behavior. func maybeAddDefaultProvider(val string, defaultProvider string) string { if defaultProvider == "" || strings.Contains(val, varsSeparator) { // no default set or already has a provider in the variable name return val } // at this point they variable doesn't have a provider return fmt.Sprintf("%s.%s", defaultProvider, val) }