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 }