Skip to content

Commit 412a1e5

Browse files
authored
Add NewPatternFromString and bundle.SetDefault; use it for volumes & dashboards (#2734)
## Changes - New dyn.NewPatternFromString to construct pattern from strings. - New bundle.SetDefault() for easier setting of defaults. - Replace configure_dashboards_defaults.go and configure_volume_defaults.go with new this new helper. There could be slight behaviour change: we no longer ignore IsCannotTraverseNilError error. Per team discussions, null should not be set in yaml or python. ## Why - It's more declarative, so less error-prone. - The string syntax for patterns is easier to read (and write): `"resources.dashboards.*.parent_path"` vs `dyn.NewPattern(dyn.Key("resources"), dyn.Key("dashboards"), dyn.AnyKey(), dyn.Key("parent_path"))` - We're going to migrate many defaults from terraform, so need a compact representation. ## Tests Unit tests for old mutators were converted to acceptance tests in #2733
1 parent 526a70a commit 412a1e5

File tree

10 files changed

+407
-124
lines changed

10 files changed

+407
-124
lines changed

bundle/config/mutator/resourcemutator/configure_dashboard_defaults.go

Lines changed: 0 additions & 70 deletions
This file was deleted.

bundle/config/mutator/resourcemutator/configure_volume_defaults.go

Lines changed: 0 additions & 44 deletions
This file was deleted.

bundle/config/mutator/resourcemutator/resource_mutator.go

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import (
1818
//
1919
// If bundle is modified outside of 'resources' section, these changes are discarded.
2020
func applyInitializeMutators(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
21-
return bundle.ApplySeq(
21+
diags := bundle.ApplySeq(
2222
ctx,
2323
b,
2424
// Reads (typed): b.Config.RunAs, b.Config.Workspace.CurrentUser (validates run_as configuration)
@@ -35,16 +35,29 @@ func applyInitializeMutators(ctx context.Context, b *bundle.Bundle) diag.Diagnos
3535
// OR corresponding fields on ForEachTask if that is present
3636
// Overrides job compute settings with a specified cluster ID for development or testing
3737
OverrideCompute(),
38+
)
39+
40+
if diags.HasError() {
41+
return diags
42+
}
3843

39-
// Reads (dynamic): resources.dashboards.* (checks for existing parent_path and embed_credentials)
40-
// Updates (dynamic): resources.dashboards.*.parent_path (sets to workspace.resource_path if not set)
41-
// Updates (dynamic): resources.dashboards.*.embed_credentials (sets to false if not set)
42-
ConfigureDashboardDefaults(),
44+
defaults := []struct {
45+
pattern string
46+
value any
47+
}{
48+
{"resources.dashboards.*.parent_path", b.Config.Workspace.ResourcePath},
49+
{"resources.dashboards.*.embed_credentials", false},
50+
{"resources.volumes.*.volume_type", "MANAGED"},
51+
}
4352

44-
// Reads (dynamic): resources.volumes.* (checks for existing volume_type)
45-
// Updates (dynamic): resources.volumes.*.volume_type (sets to "MANAGED" if not set)
46-
ConfigureVolumeDefaults(),
53+
for _, defaultDef := range defaults {
54+
diags = diags.Extend(bundle.SetDefault(ctx, b, defaultDef.pattern, defaultDef.value))
55+
if diags.HasError() {
56+
return diags
57+
}
58+
}
4759

60+
diags = diags.Extend(bundle.ApplySeq(ctx, b,
4861
ApplyPresets(),
4962

5063
// Reads (typed): b.Config.Resources.Jobs (checks job configurations)
@@ -62,7 +75,9 @@ func applyInitializeMutators(ctx context.Context, b *bundle.Bundle) diag.Diagnos
6275
// Updates (dynamic): resources.*.*.permissions (removes permissions entries where user_name or service_principal_name matches current user)
6376
// Removes the current user from all resource permissions as the Terraform provider implicitly grants ownership
6477
FilterCurrentUser(),
65-
)
78+
))
79+
80+
return diags
6681
}
6782

6883
// Normalization is applied multiple times if resource is modified during initialization

bundle/set_default.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package bundle
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/databricks/cli/libs/diag"
8+
"github.com/databricks/cli/libs/dyn"
9+
)
10+
11+
type setDefault struct {
12+
pattern dyn.Pattern
13+
key dyn.Path
14+
value any
15+
}
16+
17+
func SetDefaultMutator(pattern dyn.Pattern, key string, value any) Mutator {
18+
return &setDefault{
19+
pattern: pattern,
20+
key: dyn.NewPath(dyn.Key(key)),
21+
value: value,
22+
}
23+
}
24+
25+
func (m *setDefault) Name() string {
26+
return fmt.Sprintf("SetDefaultMutator(%v, %v, %v)", m.pattern, m.key, m.value)
27+
}
28+
29+
func (m *setDefault) Apply(ctx context.Context, b *Bundle) diag.Diagnostics {
30+
err := b.Config.Mutate(func(v dyn.Value) (dyn.Value, error) {
31+
return dyn.MapByPattern(v, m.pattern, func(p dyn.Path, v dyn.Value) (dyn.Value, error) {
32+
_, err := dyn.GetByPath(v, m.key)
33+
switch {
34+
case dyn.IsNoSuchKeyError(err):
35+
return dyn.SetByPath(v, m.key, dyn.V(m.value))
36+
default:
37+
return v, err
38+
}
39+
})
40+
})
41+
if err != nil {
42+
return diag.FromErr(err)
43+
}
44+
45+
return nil
46+
}
47+
48+
func SetDefault(ctx context.Context, b *Bundle, pattern string, value any) diag.Diagnostics {
49+
pat, err := dyn.NewPatternFromString(pattern)
50+
if err != nil {
51+
return diag.FromErr(fmt.Errorf("Internal error: invalid pattern: %s: %w", pattern, err))
52+
}
53+
54+
pat, key := pat.SplitKey()
55+
if pat == nil || key == "" {
56+
return diag.FromErr(fmt.Errorf("Internal error: invalid pattern: %s", pattern))
57+
}
58+
59+
m := SetDefaultMutator(pat, key, value)
60+
return Apply(ctx, b, m)
61+
}

libs/dyn/path_string.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ func NewPathFromString(input string) (Path, error) {
3939

4040
for p != "" {
4141
// Every component may have a leading dot.
42-
if p != "" && p[0] == '.' {
42+
if p[0] == '.' {
4343
p = p[1:]
4444
}
4545

libs/dyn/path_string_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,21 @@ func TestNewPathFromString(t *testing.T) {
8888
input: "foo[1]bar",
8989
err: errors.New("invalid path: foo[1]bar"),
9090
},
91+
{
92+
// * is parsed as regular string in NewPathFromString
93+
input: "foo.*",
94+
output: NewPath(Key("foo"), Key("*")),
95+
},
96+
{
97+
// * is parsed as regular string in NewPathFromString
98+
input: "foo.*.bar",
99+
output: NewPath(Key("foo"), Key("*"), Key("bar")),
100+
},
101+
{
102+
// This is an invalid path (but would be valid for patterns)
103+
input: "foo[*].bar",
104+
err: errors.New("invalid path: foo[*].bar"),
105+
},
91106
} {
92107
p, err := NewPathFromString(tc.input)
93108
if tc.err != nil {

libs/dyn/pattern.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,25 @@ func NewPatternFromPath(p Path) Pattern {
3232
return cs
3333
}
3434

35+
// Split <pattern>.<string_key> into <pattern> and <string_key>
36+
// The last component must be dyn.Key() and there must be at least two components.
37+
func (p Pattern) SplitKey() (Pattern, string) {
38+
if len(p) <= 1 {
39+
return nil, ""
40+
}
41+
parent := p[:len(p)-1]
42+
leaf := p[len(p)-1]
43+
pc, ok := leaf.(pathComponent)
44+
if !ok {
45+
return nil, ""
46+
}
47+
key := pc.Key()
48+
if key == "" {
49+
return nil, ""
50+
}
51+
return parent, key
52+
}
53+
3554
// Append appends the given components to the pattern.
3655
func (p Pattern) Append(cs ...patternComponent) Pattern {
3756
out := make(Pattern, len(p)+len(cs))

libs/dyn/pattern_string.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package dyn
2+
3+
import (
4+
"fmt"
5+
"strconv"
6+
"strings"
7+
)
8+
9+
// MustPatternFromString is like NewPatternFromString but panics on error.
10+
func MustPatternFromString(input string) Pattern {
11+
p, err := NewPatternFromString(input)
12+
if err != nil {
13+
panic(err)
14+
}
15+
return p
16+
}
17+
18+
// NewPatternFromString parses a pattern from a string.
19+
//
20+
// The string must be a sequence of keys and indices separated by dots.
21+
// Indices must be enclosed in square brackets.
22+
// The string may include a leading dot.
23+
// The wildcard character '*' can be used to match any key or index.
24+
//
25+
// Examples:
26+
// - foo.bar
27+
// - foo[1].bar
28+
// - foo.*.bar
29+
// - foo[*].bar
30+
// - .
31+
func NewPatternFromString(input string) (Pattern, error) {
32+
var pattern Pattern
33+
34+
p := input
35+
36+
// Trim leading dot.
37+
if p != "" && p[0] == '.' {
38+
p = p[1:]
39+
}
40+
41+
for p != "" {
42+
// Every component may have a leading dot.
43+
if p[0] == '.' {
44+
p = p[1:]
45+
}
46+
47+
if p == "" {
48+
return nil, fmt.Errorf("invalid pattern: %s", input)
49+
}
50+
51+
if p[0] == '[' {
52+
// Find next ]
53+
i := strings.Index(p, "]")
54+
if i < 0 {
55+
return nil, fmt.Errorf("invalid pattern: %s", input)
56+
}
57+
58+
// Check for wildcard
59+
if p[1:i] == "*" {
60+
pattern = append(pattern, AnyIndex())
61+
} else {
62+
// Parse index
63+
j, err := strconv.Atoi(p[1:i])
64+
if err != nil {
65+
return nil, fmt.Errorf("invalid pattern: %s", input)
66+
}
67+
68+
// Append index
69+
pattern = append(pattern, Index(j))
70+
}
71+
72+
p = p[i+1:]
73+
74+
// The next character must be a . or [
75+
if p != "" && strings.IndexAny(p, ".[") != 0 {
76+
return nil, fmt.Errorf("invalid pattern: %s", input)
77+
}
78+
} else {
79+
// Find next . or [
80+
i := strings.IndexAny(p, ".[")
81+
if i < 0 {
82+
i = len(p)
83+
}
84+
85+
if i == 0 {
86+
return nil, fmt.Errorf("invalid pattern: %s", input)
87+
}
88+
89+
// Check for wildcard
90+
if p[:i] == "*" {
91+
pattern = append(pattern, AnyKey())
92+
} else {
93+
// Append key
94+
pattern = append(pattern, Key(p[:i]))
95+
}
96+
97+
p = p[i:]
98+
}
99+
}
100+
101+
return pattern, nil
102+
}

0 commit comments

Comments
 (0)