From e6b0169ba5bc6d7e88bcf83c4f50a7924b6729ba Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 20 Oct 2026 15:31:39 +0200 Subject: [PATCH 1/2] Add Resources.ResolveKey and use it in select, bind and unbind Co-authored-by: Isaac --- bundle/config/mutator/resolve_select.go | 39 +++----------------- bundle/config/resources.go | 47 ++++++++++++++++--------- bundle/config/resources_test.go | 39 ++++++++++++++++++-- cmd/apps/import.go | 2 +- cmd/bundle/deployment/bind_resource.go | 2 +- cmd/bundle/deployment/unbind.go | 4 +-- 6 files changed, 75 insertions(+), 58 deletions(-) diff --git a/bundle/config/mutator/resolve_select.go b/bundle/config/mutator/resolve_select.go index 28d9751c322..0c9b42634e9 100644 --- a/bundle/config/mutator/resolve_select.go +++ b/bundle/config/mutator/resolve_select.go @@ -2,7 +2,6 @@ package mutator import ( "context" - "strings" "github.com/databricks/cli/bundle" "github.com/databricks/cli/bundle/metrics" @@ -32,43 +31,13 @@ func (m *resolveSelect) Apply(_ context.Context, b *bundle.Bundle) diag.Diagnost b.Metrics.SetBoolValue(metrics.SelectUsed, true) - // Build reverse index: unqualified name → []"type.name" matches. - byName := map[string][]string{} - for _, group := range b.Config.Resources.AllResources() { - typeName := group.Description.PluralName - for name := range group.Resources { - byName[name] = append(byName[name], typeName+"."+name) - } - } - resolved := make([]string, 0, len(b.Select)) for _, selector := range b.Select { - if strings.Contains(selector, ".") { - typeName, name, _ := strings.Cut(selector, ".") - found := false - for _, group := range b.Config.Resources.AllResources() { - if group.Description.PluralName == typeName { - if _, ok := group.Resources[name]; ok { - found = true - } - break - } - } - if !found { - return diag.Errorf("no such resource: %s", selector) - } - resolved = append(resolved, selector) - } else { - matches := byName[selector] - switch len(matches) { - case 0: - return diag.Errorf("no such resource: %s", selector) - case 1: - resolved = append(resolved, matches[0]) - default: - return diag.Errorf("ambiguous resource: %s (can resolve to %s); use a qualified name to disambiguate", selector, strings.Join(matches, ", ")) - } + res, key, err := b.Config.Resources.ResolveKey(selector) + if err != nil { + return diag.FromErr(err) } + resolved = append(resolved, res.ResourceDescription().PluralName+"."+key) } b.Select = resolved diff --git a/bundle/config/resources.go b/bundle/config/resources.go index f13a44617c4..8c9c42f7bc5 100644 --- a/bundle/config/resources.go +++ b/bundle/config/resources.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/url" + "strings" "github.com/databricks/cli/bundle/config/resources" "github.com/databricks/databricks-sdk-go" @@ -150,29 +151,41 @@ func (r *Resources) AllResources() []ResourceGroup { } } -// FindResourceByConfigKey searches all resource maps for a resource with the given key. -func (r *Resources) FindResourceByConfigKey(key string) (ConfigResource, error) { - var found []ConfigResource - - for _, group := range r.AllResources() { - if res, ok := group.Resources[key]; ok { - found = append(found, res) +// ResolveKey finds a resource by "key" or "type.key", e.g. "foo" or "jobs.foo", and returns it +// together with its key. A bare key must match exactly one resource across all types. +func (r *Resources) ResolveKey(ref string) (ConfigResource, string, error) { + groups := r.AllResources() + + if typeName, key, ok := strings.Cut(ref, "."); ok { + for _, group := range groups { + if group.Description.PluralName != typeName { + continue + } + if res, ok := group.Resources[key]; ok { + return res, key, nil + } + break } + return nil, "", fmt.Errorf("no such resource: %s", ref) } - if len(found) == 0 { - return nil, fmt.Errorf("no such resource: %s", key) - } - - if len(found) > 1 { - keys := make([]string, 0, len(found)) - for _, r := range found { - keys = append(keys, fmt.Sprintf("%s.%s", r.ResourceDescription().PluralName, key)) + var found []ConfigResource + var matches []string + for _, group := range groups { + if res, ok := group.Resources[ref]; ok { + found = append(found, res) + matches = append(matches, group.Description.PluralName+"."+ref) } - return nil, fmt.Errorf("ambiguous: %s (can resolve to all of %s)", key, keys) } - return found[0], nil + switch len(found) { + case 0: + return nil, "", fmt.Errorf("no such resource: %s", ref) + case 1: + return found[0], ref, nil + default: + return nil, "", fmt.Errorf("ambiguous resource: %s (can resolve to %s); use a qualified name to disambiguate", ref, strings.Join(matches, ", ")) + } } func (r *Resources) HasInternalResources() bool { diff --git a/bundle/config/resources_test.go b/bundle/config/resources_test.go index c666c8964c0..d69c9bc0a6d 100644 --- a/bundle/config/resources_test.go +++ b/bundle/config/resources_test.go @@ -434,8 +434,8 @@ func TestResourcesBindSupport(t *testing.T) { t.Fatalf("Expected at least one resource in group %s", group.Description) } for _, resource := range group.Resources { - // bind operation requires resource to be returned from FindResourceByConfigKey - r, err := supportedResources.FindResourceByConfigKey("my_" + resource.ResourceDescription().SingularName) + // bind operation requires resource to be returned from ResolveKey + r, _, err := supportedResources.ResolveKey("my_" + resource.ResourceDescription().SingularName) assert.NoError(t, err) // bind operation requires Exists to return true @@ -477,3 +477,38 @@ func TestAllInteralResourcesAreMarkedAsInternal(t *testing.T) { assert.True(t, resources.HasInternalResources()) } } + +func TestResolveKey(t *testing.T) { + r := &Resources{ + Jobs: map[string]*resources.Job{"foo": {}, "thing": {}}, + Pipelines: map[string]*resources.Pipeline{"bar": {}, "thing": {}}, + } + + tests := []struct { + ref string + wantType string + wantKey string + wantErr string + }{ + {ref: "foo", wantType: "jobs", wantKey: "foo"}, + {ref: "jobs.foo", wantType: "jobs", wantKey: "foo"}, + {ref: "pipelines.thing", wantType: "pipelines", wantKey: "thing"}, + {ref: "missing", wantErr: "no such resource: missing"}, + {ref: "jobs.bar", wantErr: "no such resource: jobs.bar"}, + {ref: "nosuchtype.foo", wantErr: "no such resource: nosuchtype.foo"}, + {ref: "thing", wantErr: "ambiguous resource: thing (can resolve to jobs.thing, pipelines.thing); use a qualified name to disambiguate"}, + } + + for _, tc := range tests { + t.Run(tc.ref, func(t *testing.T) { + res, key, err := r.ResolveKey(tc.ref) + if tc.wantErr != "" { + assert.EqualError(t, err, tc.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tc.wantType, res.ResourceDescription().PluralName) + assert.Equal(t, tc.wantKey, key) + }) + } +} diff --git a/cmd/apps/import.go b/cmd/apps/import.go index 355c51c690e..f552c3182ce 100644 --- a/cmd/apps/import.go +++ b/cmd/apps/import.go @@ -316,7 +316,7 @@ func runImport(ctx context.Context, w *databricks.WorkspaceClient, appName, outp } // Find the app resource - resource, err := b.Config.Resources.FindResourceByConfigKey(appKey) + resource, _, err := b.Config.Resources.ResolveKey(appKey) if err != nil { return fmt.Errorf("failed to find resource: %w", err) } diff --git a/cmd/bundle/deployment/bind_resource.go b/cmd/bundle/deployment/bind_resource.go index ef38c972362..7a9c01fe1f1 100644 --- a/cmd/bundle/deployment/bind_resource.go +++ b/cmd/bundle/deployment/bind_resource.go @@ -27,7 +27,7 @@ func BindResource(cmd *cobra.Command, resourceKey, resourceId string, autoApprov } ctx := cmd.Context() - resource, err := b.Config.Resources.FindResourceByConfigKey(resourceKey) + resource, resourceKey, err := b.Config.Resources.ResolveKey(resourceKey) if err != nil { return err } diff --git a/cmd/bundle/deployment/unbind.go b/cmd/bundle/deployment/unbind.go index 585767676ef..3d0c927bbd6 100644 --- a/cmd/bundle/deployment/unbind.go +++ b/cmd/bundle/deployment/unbind.go @@ -62,12 +62,12 @@ To re-bind the resource later, use: } ctx := cmd.Context() - resource, err := b.Config.Resources.FindResourceByConfigKey(args[0]) + resource, resourceKey, err := b.Config.Resources.ResolveKey(args[0]) if err != nil { return err } - phases.Unbind(ctx, b, resource.ResourceDescription().PluralName, args[0], stateDesc.Engine) + phases.Unbind(ctx, b, resource.ResourceDescription().PluralName, resourceKey, stateDesc.Engine) if logdiag.HasError(ctx) { return root.ErrAlreadyPrinted } From bffe2397cba44b36d5901e70611637fd1847f797 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Tue, 20 Oct 2026 15:31:58 +0200 Subject: [PATCH 2/2] Add changelog fragment Co-authored-by: Isaac --- .nextchanges/bundles/7128.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 .nextchanges/bundles/7128.md diff --git a/.nextchanges/bundles/7128.md b/.nextchanges/bundles/7128.md new file mode 100644 index 00000000000..e52632eeeaf --- /dev/null +++ b/.nextchanges/bundles/7128.md @@ -0,0 +1 @@ +* `bundle deployment bind` and `unbind` now accept a resource as `type.key`, e.g. `jobs.foo`. ([#7128](https://github.com/databricks/cli/pull/7128))