Skip to content

Commit 01e2f65

Browse files
denikIsaac
andcommitted
walk: generated string walker for references
walk.Strings visits the strings of the configuration with plain field access (generated, one function per struct type that contains strings), filtered inline: WithReferences passes the strings that contain "${". The callback can return a replacement, which is stored in the field. RewriteWorkspacePrefix, TFOnlyReferences and reverse interpolation use it instead of walking every value of the configuration; pure references in non-string fields come from Root.References. 1000 jobs (main / #6954 / this): RewriteWorkspacePrefix 114 / 19 / 1.8 ms, TFOnlyReferences 91 / 19 / 1.9 ms. Co-authored-by: Isaac <no-reply@databricks.com>
1 parent 86ace13 commit 01e2f65

13 files changed

Lines changed: 12657 additions & 65 deletions

File tree

‎.agents/rules/auto-generated-files.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ globs:
1515
- "internal/mocks/**/*.go"
1616
- "bundle/direct/dresources/configs/*.generated.yml"
1717
- "bundle/config/validate/*_walk.go"
18+
- "bundle/config/walk/*_walk.go"
1819
- "bundle/schema/jsonschema.json"
1920
- "python/databricks/bundles/version.py"
2021
- "python/databricks/bundles/*/__init__.py"
@@ -34,6 +35,7 @@ paths:
3435
- "internal/mocks/**/*.go"
3536
- "bundle/direct/dresources/configs/*.generated.yml"
3637
- "bundle/config/validate/*_walk.go"
38+
- "bundle/config/walk/*_walk.go"
3739
- "bundle/schema/jsonschema.json"
3840
- "python/databricks/bundles/version.py"
3941
- "python/databricks/bundles/*/__init__.py"

‎.agents/skills/bump-sdk/SKILL.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ allowed-tools: Read, Edit, Write, Bash, Glob, Grep, WebFetch, AskUserQuestion
99

1010
The SDK version lives in `go.mod` (`github.com/databricks/databricks-sdk-go`) and the pinned spec SHA lives in `.codegen/_openapi_sha`.
1111
These two move as a pair; everything else in this skill is regenerated from them or is fallout you fix by hand.
12-
Do not hand-edit generated files (`.codegen/cli.json`, `cmd/workspace/*`, `cmd/account/*`, `bundle/schema/jsonschema.json`, `bundle/config/validate/*_walk.go`, `bundle/direct/dresources/configs/*.generated.yml`, `python/databricks/bundles/**`); regenerate them.
12+
Do not hand-edit generated files (`.codegen/cli.json`, `cmd/workspace/*`, `cmd/account/*`, `bundle/schema/jsonschema.json`, `bundle/config/validate/*_walk.go`, `bundle/config/walk/*_walk.go`, `bundle/direct/dresources/configs/*.generated.yml`, `python/databricks/bundles/**`); regenerate them.
1313

1414
The Python tasks (`pydabs-*`, and the `pydabs-codegen` step inside `generate-check`) all run through `uv`. If one fails because `uv` is missing or because the host's `python3` is too old (e.g. 3.9), install `uv` (`curl -LsSf https://astral.sh/uv/install.sh | sh`) rather than touching the system Python: `uv run` provisions the interpreter each package pins (`>=3.10`, and `==3.13.*` under `python/codegen/`) and downloads it if needed. Do not chase the system Python version.
1515
The acceptance goldens are the exception: they need CI's `uv` index, see step 4.

‎Taskfile.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,6 +1002,7 @@ tasks:
10021002
- "{{.EMBED_SOURCES}}"
10031003
generates:
10041004
- bundle/config/validate/*_walk.go
1005+
- bundle/config/walk/*_walk.go
10051006
cmds:
10061007
- "go run ./bundle/internal/validation/."
10071008

‎bundle/config/mutator/rewrite_workspace_prefix.go‎

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import (
66
"strings"
77

88
"github.com/databricks/cli/bundle"
9+
"github.com/databricks/cli/bundle/config/walk"
910
"github.com/databricks/cli/libs/diag"
1011
"github.com/databricks/cli/libs/structs/structpath"
11-
"github.com/databricks/cli/libs/structs/structvar"
1212
)
1313

1414
type rewriteWorkspacePrefix struct{}
@@ -47,12 +47,7 @@ func (m *rewriteWorkspacePrefix) Apply(ctx context.Context, b *bundle.Bundle) di
4747

4848
// Walk through the bundle configuration, check all the string leafs and
4949
// see if any of the prefixes are used in the remote path.
50-
err := structvar.Walk(b.Config.View(), func(p *structpath.PathNode, v structvar.View) error {
51-
vv, ok := v.AsString()
52-
if !ok {
53-
return nil
54-
}
55-
50+
walk.Strings(&b.Config, walk.WithReferences, func(n *walk.Node, vv string) (string, bool) {
5651
newPath := vv
5752
for _, rewrite := range paths {
5853
if !strings.Contains(newPath, rewrite.pattern) {
@@ -64,21 +59,14 @@ func (m *rewriteWorkspacePrefix) Apply(ctx context.Context, b *bundle.Bundle) di
6459
Severity: diag.Warning,
6560
Summary: fmt.Sprintf("substring %q found in %q. Please update this to %q.", rewrite.pattern, vv, newPath),
6661
Detail: "For more information, please refer to: https://docs.databricks.com/en/release-notes/dev-tools/bundles.html#workspace-paths",
67-
Locations: v.Locations(),
68-
Paths: []*structpath.PathNode{p},
62+
Locations: n.Locations().Get(),
63+
Paths: []*structpath.PathNode{n.Path()},
6964
})
7065
}
7166

72-
if newPath == vv {
73-
return nil
74-
}
75-
7667
// Remove the workspace prefix from the string.
77-
return b.Config.Set(p, newPath)
68+
return newPath, newPath != vv
7869
})
79-
if err != nil {
80-
return diag.FromErr(err)
81-
}
8270

8371
return diags
8472
}

‎bundle/config/validate/tf_only_references.go‎

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"strings"
77

88
"github.com/databricks/cli/bundle"
9+
"github.com/databricks/cli/bundle/config/walk"
910
"github.com/databricks/cli/bundle/terraform_dabs_map"
1011
"github.com/databricks/cli/libs/diag"
1112
"github.com/databricks/cli/libs/structs/structpath"
@@ -28,26 +29,15 @@ func (m *tfOnlyReferences) Name() string {
2829
func (m *tfOnlyReferences) Apply(_ context.Context, b *bundle.Bundle) diag.Diagnostics {
2930
var diags diag.Diagnostics
3031

31-
// Walk the entire config looking for ${resources.*} references.
32-
_ = structvar.Walk(b.Config.View(), func(_ *structpath.PathNode, v structvar.View) error {
33-
s, ok := v.AsString()
34-
if !ok {
35-
return nil
36-
}
37-
ref, ok := structvar.NewRef(s)
38-
if !ok {
39-
return nil
40-
}
41-
for _, r := range ref.References() {
42-
if !strings.HasPrefix(r, "resources.") {
43-
continue
44-
}
45-
if d := checkTFOnlyReference(r, v.Location()); d != nil {
46-
diags = append(diags, *d)
47-
}
48-
}
49-
return nil
32+
// Look for ${resources.*} references in strings and in fields that hold a pure
33+
// reference.
34+
walk.Strings(&b.Config, walk.WithReferences, func(n *walk.Node, s string) (string, bool) {
35+
diags = append(diags, checkTFOnlyReferences(s, n.Location())...)
36+
return "", false
5037
})
38+
for p, ref := range b.Config.References() {
39+
diags = append(diags, checkTFOnlyReferences(ref, b.Config.ExactLocation(p))...)
40+
}
5141

5242
if len(diags) > 0 {
5343
b.Metrics.AddBoolValue("has_tf_only_references", true)
@@ -56,6 +46,24 @@ func (m *tfOnlyReferences) Apply(_ context.Context, b *bundle.Bundle) diag.Diagn
5646
return diags
5747
}
5848

49+
// checkTFOnlyReferences checks the ${resources.*} references in s.
50+
func checkTFOnlyReferences(s string, loc diag.Location) diag.Diagnostics {
51+
ref, ok := structvar.NewRef(s)
52+
if !ok {
53+
return nil
54+
}
55+
var diags diag.Diagnostics
56+
for _, r := range ref.References() {
57+
if !strings.HasPrefix(r, "resources.") {
58+
continue
59+
}
60+
if d := checkTFOnlyReference(r, loc); d != nil {
61+
diags = append(diags, *d)
62+
}
63+
}
64+
return diags
65+
}
66+
5967
// checkTFOnlyReference checks a single reference string like
6068
// "resources.jobs.src.always_running" and returns a diagnostic when it refers
6169
// to a TF-only field, or nil otherwise.

‎bundle/config/walk/.gitattributes‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*_walk.go linguist-generated=true

‎bundle/config/walk/node.go‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package walk
44

55
import (
66
"github.com/databricks/cli/bundle/config"
7+
"github.com/databricks/cli/libs/diag"
78
"github.com/databricks/cli/libs/structs/structpath"
89
"github.com/databricks/cli/libs/structs/structvar"
910
)
@@ -52,6 +53,15 @@ func (n *Node) Locations() *structvar.Locations {
5253
return n.locs
5354
}
5455

56+
// Location returns the first location of the value at n.
57+
func (n *Node) Location() diag.Location {
58+
locs := n.locs.Get()
59+
if len(locs) == 0 {
60+
return diag.Location{}
61+
}
62+
return locs[0]
63+
}
64+
5565
// Written reports whether the key k of n is written in the source.
5666
func (n *Node) Written(k string) bool {
5767
return n.locs.Key(k) != nil

‎bundle/config/walk/strings.go‎

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package walk
2+
3+
import (
4+
"maps"
5+
"slices"
6+
"strings"
7+
8+
"github.com/databricks/cli/bundle/config"
9+
)
10+
11+
// Mode selects the strings [Strings] visits.
12+
type Mode int
13+
14+
const (
15+
// WithReferences visits the strings that contain a variable reference ("${").
16+
WithReferences Mode = iota
17+
)
18+
19+
// StringFunc is called for a visited string s at n. It returns a replacement and
20+
// whether to store it.
21+
type StringFunc func(n *Node, s string) (string, bool)
22+
23+
// Strings calls fn for the strings of the configuration selected by mode, with plain
24+
// field access (strings_walk.go is generated by bundle/internal/validation). Pure
25+
// references in fields that cannot hold a string are not strings: see
26+
// [config.Root.References].
27+
//
28+
// TODO: processors that run back to back (e.g. reference collection and the
29+
// workspace-prefix rewrite) could share one walk: visit every string with its kind
30+
// (empty, plain, containing "${", pure reference) and dispatch to each processor,
31+
// paying for the traversal once. Phase ordering limits which ones can be combined.
32+
func Strings(root *config.Root, mode Mode, fn StringFunc) {
33+
c := &stringsWalk{mode: mode, fn: fn}
34+
n := Root(root)
35+
stringsConfigRoot(c, &n, root)
36+
}
37+
38+
type stringsWalk struct {
39+
mode Mode
40+
fn StringFunc
41+
}
42+
43+
func (c *stringsWalk) match(s string) bool {
44+
return strings.Contains(s, "${")
45+
}
46+
47+
// any visits the strings of a dynamic value (e.g. a variable value): the maps, slices
48+
// and strings that decoding produces.
49+
func (c *stringsWalk) any(n *Node, v *any) {
50+
switch x := (*v).(type) {
51+
case string:
52+
if c.match(x) {
53+
if r, ok := c.fn(n, x); ok {
54+
*v = r
55+
}
56+
}
57+
case map[string]any:
58+
for _, k := range sortedKeys(x) {
59+
e := x[k]
60+
ch := n.Child(k)
61+
c.any(&ch, &e)
62+
x[k] = e
63+
}
64+
case []any:
65+
for i := range x {
66+
ch := n.Elem(i)
67+
c.any(&ch, &x[i])
68+
}
69+
default:
70+
}
71+
}
72+
73+
// sortedKeys returns the keys of m in the order maps are visited.
74+
func sortedKeys[K ~string, V any](m map[K]V) []K {
75+
return slices.Sorted(maps.Keys(m))
76+
}

0 commit comments

Comments
 (0)