Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .nextchanges/bundles/7128.md
Original file line number Diff line number Diff line change
@@ -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))
39 changes: 4 additions & 35 deletions bundle/config/mutator/resolve_select.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package mutator

import (
"context"
"strings"

"github.com/databricks/cli/bundle"
"github.com/databricks/cli/bundle/metrics"
Expand Down Expand Up @@ -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
Expand Down
47 changes: 30 additions & 17 deletions bundle/config/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"net/url"
"strings"

"github.com/databricks/cli/bundle/config/resources"
"github.com/databricks/databricks-sdk-go"
Expand Down Expand Up @@ -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 {
Expand Down
39 changes: 37 additions & 2 deletions bundle/config/resources_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
})
}
}
2 changes: 1 addition & 1 deletion cmd/apps/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/bundle/deployment/bind_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
4 changes: 2 additions & 2 deletions cmd/bundle/deployment/unbind.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Loading