github.com/databricks/cli@v0.203.0/bundle/config/interpolation/interpolation.go (about)

     1  package interpolation
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"reflect"
     8  	"regexp"
     9  	"sort"
    10  	"strings"
    11  
    12  	"github.com/databricks/cli/bundle"
    13  	"github.com/databricks/cli/bundle/config/variable"
    14  	"golang.org/x/exp/maps"
    15  	"golang.org/x/exp/slices"
    16  )
    17  
    18  const Delimiter = "."
    19  
    20  // must start with alphabet, support hyphens and underscores in middle but must end with character
    21  var re = regexp.MustCompile(`\$\{([a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\.[a-zA-Z]+([-_]?[a-zA-Z0-9]+)*)*)\}`)
    22  
    23  type stringField struct {
    24  	path string
    25  
    26  	getter
    27  	setter
    28  }
    29  
    30  func newStringField(path string, g getter, s setter) *stringField {
    31  	return &stringField{
    32  		path: path,
    33  
    34  		getter: g,
    35  		setter: s,
    36  	}
    37  }
    38  
    39  func (s *stringField) dependsOn() []string {
    40  	var out []string
    41  	m := re.FindAllStringSubmatch(s.Get(), -1)
    42  	for i := range m {
    43  		out = append(out, m[i][1])
    44  	}
    45  	return out
    46  }
    47  
    48  func (s *stringField) interpolate(fns []LookupFunction, lookup map[string]string) {
    49  	out := re.ReplaceAllStringFunc(s.Get(), func(s string) string {
    50  		// Turn the whole match into the submatch.
    51  		match := re.FindStringSubmatch(s)
    52  		for _, fn := range fns {
    53  			v, err := fn(match[1], lookup)
    54  			if errors.Is(err, ErrSkipInterpolation) {
    55  				continue
    56  			}
    57  			if err != nil {
    58  				panic(err)
    59  			}
    60  			return v
    61  		}
    62  
    63  		// No substitution.
    64  		return s
    65  	})
    66  
    67  	s.Set(out)
    68  }
    69  
    70  type accumulator struct {
    71  	// all string fields in the bundle config
    72  	strings map[string]*stringField
    73  
    74  	// contains path -> resolved_string mapping for string fields in the config
    75  	// The resolved strings will NOT contain any variable references that could
    76  	// have been resolved, however there might still be references that cannot
    77  	// be resolved
    78  	memo map[string]string
    79  }
    80  
    81  // jsonFieldName returns the name in a field's `json` tag.
    82  // Returns the empty string if it isn't set.
    83  func jsonFieldName(sf reflect.StructField) string {
    84  	tag, ok := sf.Tag.Lookup("json")
    85  	if !ok {
    86  		return ""
    87  	}
    88  	parts := strings.Split(tag, ",")
    89  	if parts[0] == "-" {
    90  		return ""
    91  	}
    92  	return parts[0]
    93  }
    94  
    95  func (a *accumulator) walkStruct(scope []string, rv reflect.Value) {
    96  	num := rv.NumField()
    97  	for i := 0; i < num; i++ {
    98  		sf := rv.Type().Field(i)
    99  		f := rv.Field(i)
   100  
   101  		// Walk field with the same scope for anonymous (embedded) fields.
   102  		if sf.Anonymous {
   103  			a.walk(scope, f, anySetter{f})
   104  			continue
   105  		}
   106  
   107  		// Skip unnamed fields.
   108  		fieldName := jsonFieldName(rv.Type().Field(i))
   109  		if fieldName == "" {
   110  			continue
   111  		}
   112  
   113  		a.walk(append(scope, fieldName), f, anySetter{f})
   114  	}
   115  }
   116  
   117  func (a *accumulator) walk(scope []string, rv reflect.Value, s setter) {
   118  	// Dereference pointer.
   119  	if rv.Type().Kind() == reflect.Pointer {
   120  		// Skip nil pointers.
   121  		if rv.IsNil() {
   122  			return
   123  		}
   124  		rv = rv.Elem()
   125  		s = anySetter{rv}
   126  	}
   127  
   128  	switch rv.Type().Kind() {
   129  	case reflect.String:
   130  		path := strings.Join(scope, Delimiter)
   131  		a.strings[path] = newStringField(path, anyGetter{rv}, s)
   132  
   133  		// register alias for variable value. `var.foo` would be the alias for
   134  		// `variables.foo.value`
   135  		if len(scope) == 3 && scope[0] == "variables" && scope[2] == "value" {
   136  			aliasPath := strings.Join([]string{variable.VariableReferencePrefix, scope[1]}, Delimiter)
   137  			a.strings[aliasPath] = a.strings[path]
   138  		}
   139  	case reflect.Struct:
   140  		a.walkStruct(scope, rv)
   141  	case reflect.Map:
   142  		if rv.Type().Key().Kind() != reflect.String {
   143  			panic("only support string keys in map")
   144  		}
   145  		keys := rv.MapKeys()
   146  		for _, key := range keys {
   147  			a.walk(append(scope, key.String()), rv.MapIndex(key), mapSetter{rv, key})
   148  		}
   149  	case reflect.Slice:
   150  		n := rv.Len()
   151  		name := scope[len(scope)-1]
   152  		base := scope[:len(scope)-1]
   153  		for i := 0; i < n; i++ {
   154  			element := rv.Index(i)
   155  			a.walk(append(base, fmt.Sprintf("%s[%d]", name, i)), element, anySetter{element})
   156  		}
   157  	}
   158  }
   159  
   160  // walk and gather all string fields in the config
   161  func (a *accumulator) start(v any) {
   162  	rv := reflect.ValueOf(v)
   163  	if rv.Type().Kind() != reflect.Pointer {
   164  		panic("expect pointer")
   165  	}
   166  	rv = rv.Elem()
   167  	if rv.Type().Kind() != reflect.Struct {
   168  		panic("expect struct")
   169  	}
   170  
   171  	a.strings = make(map[string]*stringField)
   172  	a.memo = make(map[string]string)
   173  	a.walk([]string{}, rv, nilSetter{})
   174  }
   175  
   176  // recursively interpolate variables in a depth first manner
   177  func (a *accumulator) Resolve(path string, seenPaths []string, fns ...LookupFunction) error {
   178  	// return early if the path is already resolved
   179  	if _, ok := a.memo[path]; ok {
   180  		return nil
   181  	}
   182  
   183  	// fetch the string node to resolve
   184  	field, ok := a.strings[path]
   185  	if !ok {
   186  		return fmt.Errorf("could not resolve reference %s", path)
   187  	}
   188  
   189  	// return early if the string field has no variables to interpolate
   190  	if len(field.dependsOn()) == 0 {
   191  		a.memo[path] = field.Get()
   192  		return nil
   193  	}
   194  
   195  	// resolve all variables refered in the root string field
   196  	for _, childFieldPath := range field.dependsOn() {
   197  		// error if there is a loop in variable interpolation
   198  		if slices.Contains(seenPaths, childFieldPath) {
   199  			return fmt.Errorf("cycle detected in field resolution: %s", strings.Join(append(seenPaths, childFieldPath), " -> "))
   200  		}
   201  
   202  		// recursive resolve variables in the child fields
   203  		err := a.Resolve(childFieldPath, append(seenPaths, childFieldPath), fns...)
   204  		if err != nil {
   205  			return err
   206  		}
   207  	}
   208  
   209  	// interpolate root string once all variable references in it have been resolved
   210  	field.interpolate(fns, a.memo)
   211  
   212  	// record interpolated string in memo
   213  	a.memo[path] = field.Get()
   214  	return nil
   215  }
   216  
   217  // Interpolate all string fields in the config
   218  func (a *accumulator) expand(fns ...LookupFunction) error {
   219  	// sorting paths for stable order of iteration
   220  	paths := maps.Keys(a.strings)
   221  	sort.Strings(paths)
   222  
   223  	// iterate over paths for all strings fields in the config
   224  	for _, path := range paths {
   225  		err := a.Resolve(path, []string{path}, fns...)
   226  		if err != nil {
   227  			return err
   228  		}
   229  	}
   230  	return nil
   231  }
   232  
   233  type interpolate struct {
   234  	fns []LookupFunction
   235  }
   236  
   237  func (m *interpolate) expand(v any) error {
   238  	a := accumulator{}
   239  	a.start(v)
   240  	return a.expand(m.fns...)
   241  }
   242  
   243  func Interpolate(fns ...LookupFunction) bundle.Mutator {
   244  	return &interpolate{fns: fns}
   245  }
   246  
   247  func (m *interpolate) Name() string {
   248  	return "Interpolate"
   249  }
   250  
   251  func (m *interpolate) Apply(_ context.Context, b *bundle.Bundle) error {
   252  	return m.expand(&b.Config)
   253  }