Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
39 changes: 39 additions & 0 deletions bundle/resources/completion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package resources

import (
"github.com/databricks/cli/bundle"
"golang.org/x/exp/maps"
)

func CompletionMap(b *bundle.Bundle) map[string]string {
out := make(map[string]string)
keyOnly, keyWithType := Keys(b)

// Keep track of resources we have seen by their fully qualified key.
seen := make(map[string]bool)

// First add resources that can be identified by key alone.
for k, v := range keyOnly {
// Invariant: len(v) >= 1. See [ResourceKeys].
if len(v) == 1 {
seen[v[0].key] = true
out[k] = v[0].resource.GetName()
}
}

// Then add resources that can only be identified by their type and key.
for k, v := range keyWithType {
// Invariant: len(v) == 1. See [ResourceKeys].
_, ok := seen[v[0].key]
if ok {
continue
}
out[k] = v[0].resource.GetName()
}

return out
}

func Completions(b *bundle.Bundle) []string {
return maps.Keys(CompletionMap(b))
}
1 change: 1 addition & 0 deletions bundle/resources/completion_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package resources
64 changes: 64 additions & 0 deletions bundle/resources/keys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package resources

import (
"fmt"

"github.com/databricks/cli/bundle"
"github.com/databricks/cli/bundle/config"
)

type pair struct {
key string
resource config.ConfigResource
}

// lookup maps identifiers to a list of resources that match that identifier.
// The list can have more than 1 entry if resources of different types use the
// same key. When this happens, the user should disambiguate between them.
type lookup map[string][]pair

// Keys computes maps that index resources by their key (e.g. `my_job`) and by their key
// prefixed by their type (e.g. `jobs.my_job`). The resource key alone may be ambiguous (it is
// possible for resources of different types to have the same key), but the key prefixed by
// the type is guaranteed to be unique.
func Keys(b *bundle.Bundle) (keyOnly lookup, keyWithType lookup) {
keyOnly = make(lookup)
keyWithType = make(lookup)

// Collect all resources by their key and prefixed key.
for _, group := range b.Config.Resources.AllResources() {
typ := group.Description.PluralName
for k, v := range group.Resources {
kt := fmt.Sprintf("%s.%s", typ, k)
p := pair{key: kt, resource: v}
keyOnly[k] = append(keyOnly[k], p)
keyWithType[kt] = append(keyWithType[kt], p)
}
}

return
}

// Lookup returns the resource with the given key.
// It first attempts to find a resource with the key alone.
// If this fails, it tries the key prefixed by the resource type.
// If this also fails, it returns an error.
func Lookup(b *bundle.Bundle, key string) (config.ConfigResource, error) {
keyOnly, keyWithType := Keys(b)

// First try to find the resource by key alone.
if res, ok := keyOnly[key]; ok {
if len(res) == 1 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we add the case if len > 1 then we return the error that the resource is ambiguous?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

I looked through the commit history and found that we added validation of uniqueness early last year. This makes the probability of seeing ambiguous keys very low (I think it's still possible if a resource with a duplicate key is defined in the target overrides). Relevant PRs: #1614 and #332.

return res[0].resource, nil
}
}

// Then try to find the resource by key and type.
if res, ok := keyWithType[key]; ok {
if len(res) == 1 {
return res[0].resource, nil
}
}

return nil, fmt.Errorf("resource with key %q not found", key)
}
1 change: 1 addition & 0 deletions bundle/resources/keys_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package resources
1 change: 1 addition & 0 deletions cmd/bundle/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,6 @@ func New() *cobra.Command {
cmd.AddCommand(newGenerateCommand())
cmd.AddCommand(newDebugCommand())
cmd.AddCommand(deployment.NewDeploymentCommand())
cmd.AddCommand(newOpenCommand())
return cmd
}
125 changes: 125 additions & 0 deletions cmd/bundle/open.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package bundle

import (
"errors"
"fmt"
"os"
"path/filepath"

"github.com/databricks/cli/bundle"
"github.com/databricks/cli/bundle/config/mutator"
"github.com/databricks/cli/bundle/deploy/terraform"
"github.com/databricks/cli/bundle/phases"
"github.com/databricks/cli/bundle/resources"
"github.com/databricks/cli/cmd/bundle/utils"
"github.com/databricks/cli/cmd/root"
"github.com/databricks/cli/libs/cmdio"
"github.com/spf13/cobra"

"github.com/pkg/browser"
)

func newOpenCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "open",
Short: "Open the web UI for a resource",
Args: root.MaximumNArgs(1),
}

var forcePull bool
cmd.Flags().BoolVar(&forcePull, "force-pull", false, "Skip local cache and load the state from the remote workspace")

cmd.RunE = func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
b, diags := utils.ConfigureBundleWithVariables(cmd)
if err := diags.Error(); err != nil {
return diags.Error()
}

diags = bundle.Apply(ctx, b, phases.Initialize())
if err := diags.Error(); err != nil {
return err
}

// If no arguments are specified, prompt the user to select something to run.
if len(args) == 0 && cmdio.IsPromptSupported(ctx) {
// Invert completions from KEY -> NAME, to NAME -> KEY.
inv := make(map[string]string)
for k, v := range resources.CompletionMap(b) {
inv[v] = k
}
id, err := cmdio.Select(ctx, inv, "Resource to open")
if err != nil {
return err
}
args = append(args, id)
}

if len(args) < 1 {
return fmt.Errorf("expected a KEY of the resource to open")
}

cacheDir, err := terraform.Dir(ctx, b)
if err != nil {
return err
}
_, stateFileErr := os.Stat(filepath.Join(cacheDir, terraform.TerraformStateFileName))
_, configFileErr := os.Stat(filepath.Join(cacheDir, terraform.TerraformConfigFileName))
noCache := errors.Is(stateFileErr, os.ErrNotExist) || errors.Is(configFileErr, os.ErrNotExist)

if forcePull || noCache {
diags = bundle.Apply(ctx, b, bundle.Seq(
terraform.StatePull(),
terraform.Interpolate(),
terraform.Write(),
))
if err := diags.Error(); err != nil {
return err
}
}

diags = bundle.Apply(ctx, b, bundle.Seq(
terraform.Load(),
mutator.InitializeURLs(),
))
if err := diags.Error(); err != nil {
return err
}

// Locate resource to open.
resource, err := resources.Lookup(b, args[0])
if err != nil {
return err
}

// Confirm that the resource has a URL.
url := resource.GetURL()
if url == "" {
return errors.New("resource does not have a URL associated with it (has it been deployed?)")
}

return browser.OpenURL(url)
}

cmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
b, diags := root.MustConfigureBundle(cmd)
if err := diags.Error(); err != nil {
cobra.CompErrorln(err.Error())
return nil, cobra.ShellCompDirectiveError
}

// No completion in the context of a bundle.
// Source and destination paths are taken from bundle configuration.
if b == nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}

if len(args) == 0 {
return resources.Completions(b), cobra.ShellCompDirectiveNoFileComp
} else {
return nil, cobra.ShellCompDirectiveNoFileComp
}
}

return cmd
}