mirror of https://github.com/databricks/cli.git
Parameterize interpolation function (#117)
By specifying a function typed `LookupFunction` the caller can customize which path expressions to interpolate and which ones to skip. When we express dependencies between resources their values are known by Terraform at deploy time. Therefore, we have to skip interpolation for `${resources.jobs.my_job.id}` and instead rewrite it to `${databricks_job.my_job.id}` before passing it along to Terraform.
This commit is contained in:
parent
487bf6fd5c
commit
cdc776d89e
|
@ -10,46 +10,48 @@ import (
|
|||
"github.com/databricks/bricks/bundle"
|
||||
)
|
||||
|
||||
const Delimiter = "."
|
||||
|
||||
var re = regexp.MustCompile(`\$\{(\w+(\.\w+)*)\}`)
|
||||
|
||||
type stringField struct {
|
||||
rv reflect.Value
|
||||
s setter
|
||||
path string
|
||||
|
||||
getter
|
||||
setter
|
||||
}
|
||||
|
||||
func newStringField(path string, rv reflect.Value, s setter) *stringField {
|
||||
func newStringField(path string, g getter, s setter) *stringField {
|
||||
return &stringField{
|
||||
rv: rv,
|
||||
s: s,
|
||||
}
|
||||
}
|
||||
path: path,
|
||||
|
||||
func (s *stringField) String() string {
|
||||
return s.rv.String()
|
||||
getter: g,
|
||||
setter: s,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *stringField) dependsOn() []string {
|
||||
var out []string
|
||||
m := re.FindAllStringSubmatch(s.String(), -1)
|
||||
m := re.FindAllStringSubmatch(s.Get(), -1)
|
||||
for i := range m {
|
||||
out = append(out, m[i][1])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *stringField) interpolate(lookup map[string]string) {
|
||||
out := re.ReplaceAllStringFunc(s.String(), func(s string) string {
|
||||
func (s *stringField) interpolate(fn LookupFunction, lookup map[string]string) {
|
||||
out := re.ReplaceAllStringFunc(s.Get(), func(s string) string {
|
||||
// Turn the whole match into the submatch.
|
||||
match := re.FindStringSubmatch(s)
|
||||
path := match[1]
|
||||
v, ok := lookup[path]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("expected to find value for path: %s", path))
|
||||
v, err := fn(match[1], lookup)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return v
|
||||
})
|
||||
|
||||
s.s.Set(out)
|
||||
s.Set(out)
|
||||
}
|
||||
|
||||
type accumulator struct {
|
||||
|
@ -105,8 +107,8 @@ func (a *accumulator) walk(scope []string, rv reflect.Value, s setter) {
|
|||
|
||||
switch rv.Type().Kind() {
|
||||
case reflect.String:
|
||||
path := strings.Join(scope, ".")
|
||||
a.strings[path] = newStringField(path, rv, s)
|
||||
path := strings.Join(scope, Delimiter)
|
||||
a.strings[path] = newStringField(path, anyGetter{rv}, s)
|
||||
case reflect.Struct:
|
||||
a.walkStruct(scope, rv)
|
||||
case reflect.Map:
|
||||
|
@ -142,12 +144,12 @@ func (a *accumulator) gather(paths []string) (map[string]string, error) {
|
|||
if len(deps) > 0 {
|
||||
return nil, fmt.Errorf("%s depends on %s", path, strings.Join(deps, ", "))
|
||||
}
|
||||
out[path] = f.rv.String()
|
||||
out[path] = f.Get()
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func expand(v any) error {
|
||||
func (a *accumulator) start(v any) {
|
||||
rv := reflect.ValueOf(v)
|
||||
if rv.Type().Kind() != reflect.Pointer {
|
||||
panic("expect pointer")
|
||||
|
@ -156,33 +158,42 @@ func expand(v any) error {
|
|||
if rv.Type().Kind() != reflect.Struct {
|
||||
panic("expect struct")
|
||||
}
|
||||
acc := &accumulator{
|
||||
strings: make(map[string]*stringField),
|
||||
}
|
||||
acc.walk([]string{}, rv, nilSetter{})
|
||||
|
||||
for path, v := range acc.strings {
|
||||
a.strings = make(map[string]*stringField)
|
||||
a.walk([]string{}, rv, nilSetter{})
|
||||
}
|
||||
|
||||
func (a *accumulator) expand(fn LookupFunction) error {
|
||||
for path, v := range a.strings {
|
||||
ds := v.dependsOn()
|
||||
if len(ds) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Create map to be used for interpolation
|
||||
m, err := acc.gather(ds)
|
||||
m, err := a.gather(ds)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot interpolate %s: %w", path, err)
|
||||
}
|
||||
|
||||
v.interpolate(m)
|
||||
v.interpolate(fn, m)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type interpolate struct{}
|
||||
type interpolate struct {
|
||||
fn LookupFunction
|
||||
}
|
||||
|
||||
func Interpolate() bundle.Mutator {
|
||||
return &interpolate{}
|
||||
func (m *interpolate) expand(v any) error {
|
||||
a := accumulator{}
|
||||
a.start(v)
|
||||
return a.expand(m.fn)
|
||||
}
|
||||
|
||||
func Interpolate(fn LookupFunction) bundle.Mutator {
|
||||
return &interpolate{fn: fn}
|
||||
}
|
||||
|
||||
func (m *interpolate) Name() string {
|
||||
|
@ -190,6 +201,5 @@ func (m *interpolate) Name() string {
|
|||
}
|
||||
|
||||
func (m *interpolate) Apply(_ context.Context, b *bundle.Bundle) ([]bundle.Mutator, error) {
|
||||
err := expand(&b.Config)
|
||||
return nil, err
|
||||
return nil, m.expand(&b.Config)
|
||||
}
|
||||
|
|
|
@ -28,6 +28,12 @@ type foo struct {
|
|||
F map[string]string `json:"f"`
|
||||
}
|
||||
|
||||
func expand(v any) error {
|
||||
a := accumulator{}
|
||||
a.start(v)
|
||||
return a.expand(DefaultLookup)
|
||||
}
|
||||
|
||||
func TestInterpolationVariables(t *testing.T) {
|
||||
f := foo{
|
||||
A: "a",
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
package interpolation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// LookupFunction returns the value to rewrite a path expression to.
|
||||
type LookupFunction func(path string, depends map[string]string) (string, error)
|
||||
|
||||
// DefaultLookup looks up the specified path in the map.
|
||||
// It returns an error if it doesn't exist.
|
||||
func DefaultLookup(path string, lookup map[string]string) (string, error) {
|
||||
v, ok := lookup[path]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("expected to find value for path: %s", path)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// ExcludeLookupsInPath is a lookup function that skips lookups for the specified path.
|
||||
func ExcludeLookupsInPath(exclude ...string) LookupFunction {
|
||||
return func(path string, lookup map[string]string) (string, error) {
|
||||
parts := strings.Split(path, Delimiter)
|
||||
|
||||
// Skip interpolation of this path.
|
||||
if len(parts) >= len(exclude) && slices.Compare(exclude, parts[0:len(exclude)]) == 0 {
|
||||
return fmt.Sprintf("${%s}", path), nil
|
||||
}
|
||||
|
||||
return DefaultLookup(path, lookup)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package interpolation
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestExcludePath(t *testing.T) {
|
||||
tmp := struct {
|
||||
A map[string]string `json:"a"`
|
||||
B map[string]string `json:"b"`
|
||||
C map[string]string `json:"c"`
|
||||
}{
|
||||
A: map[string]string{
|
||||
"x": "1",
|
||||
},
|
||||
B: map[string]string{
|
||||
"x": "2",
|
||||
},
|
||||
C: map[string]string{
|
||||
"ax": "${a.x}",
|
||||
"bx": "${b.x}",
|
||||
},
|
||||
}
|
||||
|
||||
m := interpolate{
|
||||
fn: ExcludeLookupsInPath("a"),
|
||||
}
|
||||
|
||||
err := m.expand(&tmp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "1", tmp.A["x"])
|
||||
assert.Equal(t, "2", tmp.B["x"])
|
||||
assert.Equal(t, "${a.x}", tmp.C["ax"])
|
||||
assert.Equal(t, "2", tmp.C["bx"])
|
||||
}
|
|
@ -34,3 +34,15 @@ type mapSetter struct {
|
|||
func (s mapSetter) Set(str string) {
|
||||
s.m.SetMapIndex(s.k, reflect.ValueOf(str))
|
||||
}
|
||||
|
||||
type getter interface {
|
||||
Get() string
|
||||
}
|
||||
|
||||
type anyGetter struct {
|
||||
rv reflect.Value
|
||||
}
|
||||
|
||||
func (g anyGetter) Get() string {
|
||||
return g.rv.String()
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue