diff --git a/acceptance/bundle/multi_profile/auto_select/.databrickscfg b/acceptance/bundle/multi_profile/auto_select/.databrickscfg new file mode 100644 index 0000000000..5fc2fc87a6 --- /dev/null +++ b/acceptance/bundle/multi_profile/auto_select/.databrickscfg @@ -0,0 +1,8 @@ +[ws-profile] +host = $DATABRICKS_HOST +token = $DATABRICKS_TOKEN + +[acc-profile] +host = $DATABRICKS_HOST +account_id = abc123 +token = acc-token diff --git a/acceptance/bundle/multi_profile/auto_select/databricks.yml b/acceptance/bundle/multi_profile/auto_select/databricks.yml new file mode 100644 index 0000000000..50ac852b94 --- /dev/null +++ b/acceptance/bundle/multi_profile/auto_select/databricks.yml @@ -0,0 +1,2 @@ +bundle: + name: multi-profile-auto-select diff --git a/acceptance/bundle/multi_profile/auto_select/out.test.toml b/acceptance/bundle/multi_profile/auto_select/out.test.toml new file mode 100644 index 0000000000..d560f1de04 --- /dev/null +++ b/acceptance/bundle/multi_profile/auto_select/out.test.toml @@ -0,0 +1,5 @@ +Local = true +Cloud = false + +[EnvMatrix] + DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/bundle/multi_profile/auto_select/output.txt b/acceptance/bundle/multi_profile/auto_select/output.txt new file mode 100644 index 0000000000..4140d9ff34 --- /dev/null +++ b/acceptance/bundle/multi_profile/auto_select/output.txt @@ -0,0 +1,5 @@ + +>>> [CLI] bundle validate -o json +{ + "name": "multi-profile-auto-select" +} diff --git a/acceptance/bundle/multi_profile/auto_select/script b/acceptance/bundle/multi_profile/auto_select/script new file mode 100644 index 0000000000..c785d4e0b4 --- /dev/null +++ b/acceptance/bundle/multi_profile/auto_select/script @@ -0,0 +1,21 @@ +# Set up .databrickscfg with two profiles for the same host: +# one workspace profile and one account profile. +envsubst < .databrickscfg > out && mv out .databrickscfg + +# Write databricks.yml with the actual host URL. +cat > databricks.yml << EOF +bundle: + name: multi-profile-auto-select + +workspace: + host: $DATABRICKS_HOST +EOF + +export DATABRICKS_CONFIG_FILE=.databrickscfg +unset DATABRICKS_HOST +unset DATABRICKS_TOKEN + +# Only one workspace-compatible profile exists (ws-profile). +# The account-only profile (acc-profile) is filtered out. +# Auto-select happens silently and validate succeeds. +trace $CLI bundle validate -o json | jq '{name: .bundle.name}' diff --git a/acceptance/bundle/multi_profile/env_auth_skip/.databrickscfg b/acceptance/bundle/multi_profile/env_auth_skip/.databrickscfg new file mode 100644 index 0000000000..d0825fa0d7 --- /dev/null +++ b/acceptance/bundle/multi_profile/env_auth_skip/.databrickscfg @@ -0,0 +1,7 @@ +[profile-1] +host = $DATABRICKS_HOST +token = t1 + +[profile-2] +host = $DATABRICKS_HOST +token = t2 diff --git a/acceptance/bundle/multi_profile/env_auth_skip/databricks.yml b/acceptance/bundle/multi_profile/env_auth_skip/databricks.yml new file mode 100644 index 0000000000..cc3be2a1dc --- /dev/null +++ b/acceptance/bundle/multi_profile/env_auth_skip/databricks.yml @@ -0,0 +1,2 @@ +bundle: + name: multi-profile-env-skip diff --git a/acceptance/bundle/multi_profile/env_auth_skip/out.test.toml b/acceptance/bundle/multi_profile/env_auth_skip/out.test.toml new file mode 100644 index 0000000000..d560f1de04 --- /dev/null +++ b/acceptance/bundle/multi_profile/env_auth_skip/out.test.toml @@ -0,0 +1,5 @@ +Local = true +Cloud = false + +[EnvMatrix] + DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/bundle/multi_profile/env_auth_skip/output.txt b/acceptance/bundle/multi_profile/env_auth_skip/output.txt new file mode 100644 index 0000000000..cff59934e0 --- /dev/null +++ b/acceptance/bundle/multi_profile/env_auth_skip/output.txt @@ -0,0 +1,5 @@ + +>>> [CLI] bundle validate -o json +{ + "name": "multi-profile-env-skip" +} diff --git a/acceptance/bundle/multi_profile/env_auth_skip/script b/acceptance/bundle/multi_profile/env_auth_skip/script new file mode 100644 index 0000000000..5f934dfc10 --- /dev/null +++ b/acceptance/bundle/multi_profile/env_auth_skip/script @@ -0,0 +1,16 @@ +# Set up .databrickscfg with two workspace profiles for the same host. +envsubst < .databrickscfg > out && mv out .databrickscfg + +cat > databricks.yml << EOF +bundle: + name: multi-profile-env-skip + +workspace: + host: $DATABRICKS_HOST +EOF + +export DATABRICKS_CONFIG_FILE=.databrickscfg +# Keep DATABRICKS_HOST and DATABRICKS_TOKEN set — env auth takes precedence +# over host-based profile matching, so errMultipleProfiles never fires. + +trace $CLI bundle validate -o json | jq '{name: .bundle.name}' diff --git a/acceptance/bundle/multi_profile/no_workspace_profiles/.databrickscfg b/acceptance/bundle/multi_profile/no_workspace_profiles/.databrickscfg new file mode 100644 index 0000000000..18253c223e --- /dev/null +++ b/acceptance/bundle/multi_profile/no_workspace_profiles/.databrickscfg @@ -0,0 +1,9 @@ +[acc-1] +host = $DATABRICKS_HOST +account_id = abc +token = t1 + +[acc-2] +host = $DATABRICKS_HOST +account_id = def +token = t2 diff --git a/acceptance/bundle/multi_profile/no_workspace_profiles/databricks.yml b/acceptance/bundle/multi_profile/no_workspace_profiles/databricks.yml new file mode 100644 index 0000000000..298f0763d7 --- /dev/null +++ b/acceptance/bundle/multi_profile/no_workspace_profiles/databricks.yml @@ -0,0 +1,2 @@ +bundle: + name: multi-profile-no-ws diff --git a/acceptance/bundle/multi_profile/no_workspace_profiles/out.test.toml b/acceptance/bundle/multi_profile/no_workspace_profiles/out.test.toml new file mode 100644 index 0000000000..d560f1de04 --- /dev/null +++ b/acceptance/bundle/multi_profile/no_workspace_profiles/out.test.toml @@ -0,0 +1,5 @@ +Local = true +Cloud = false + +[EnvMatrix] + DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/bundle/multi_profile/no_workspace_profiles/output.txt b/acceptance/bundle/multi_profile/no_workspace_profiles/output.txt new file mode 100644 index 0000000000..02a544efb5 --- /dev/null +++ b/acceptance/bundle/multi_profile/no_workspace_profiles/output.txt @@ -0,0 +1,12 @@ + +>>> [CLI] bundle validate +Error: cannot resolve bundle auth configuration: resolve: [DATABRICKS_URL]: multiple profiles matched: acc-1, acc-2: please set DATABRICKS_CONFIG_PROFILE or provide --profile flag to specify one. Config: host=[DATABRICKS_URL], config_file=.databrickscfg, databricks_cli_path=[CLI]. Env: DATABRICKS_CONFIG_FILE, DATABRICKS_CLI_PATH + +Name: multi-profile-no-ws +Target: default +Workspace: + Host: [DATABRICKS_URL] + +Found 1 error + +Exit code: 1 diff --git a/acceptance/bundle/multi_profile/no_workspace_profiles/script b/acceptance/bundle/multi_profile/no_workspace_profiles/script new file mode 100644 index 0000000000..97dda0e2af --- /dev/null +++ b/acceptance/bundle/multi_profile/no_workspace_profiles/script @@ -0,0 +1,17 @@ +# Set up .databrickscfg with two account-only profiles for the same host. +envsubst < .databrickscfg > out && mv out .databrickscfg + +cat > databricks.yml << EOF +bundle: + name: multi-profile-no-ws + +workspace: + host: $DATABRICKS_HOST +EOF + +export DATABRICKS_CONFIG_FILE=.databrickscfg +unset DATABRICKS_HOST +unset DATABRICKS_TOKEN + +# No workspace-compatible profiles → original multi-profile error returned. +errcode trace $CLI bundle validate diff --git a/acceptance/bundle/multi_profile/non_interactive_error/.databrickscfg b/acceptance/bundle/multi_profile/non_interactive_error/.databrickscfg new file mode 100644 index 0000000000..d0825fa0d7 --- /dev/null +++ b/acceptance/bundle/multi_profile/non_interactive_error/.databrickscfg @@ -0,0 +1,7 @@ +[profile-1] +host = $DATABRICKS_HOST +token = t1 + +[profile-2] +host = $DATABRICKS_HOST +token = t2 diff --git a/acceptance/bundle/multi_profile/non_interactive_error/databricks.yml b/acceptance/bundle/multi_profile/non_interactive_error/databricks.yml new file mode 100644 index 0000000000..529bc314c7 --- /dev/null +++ b/acceptance/bundle/multi_profile/non_interactive_error/databricks.yml @@ -0,0 +1,2 @@ +bundle: + name: multi-profile-non-interactive diff --git a/acceptance/bundle/multi_profile/non_interactive_error/out.test.toml b/acceptance/bundle/multi_profile/non_interactive_error/out.test.toml new file mode 100644 index 0000000000..d560f1de04 --- /dev/null +++ b/acceptance/bundle/multi_profile/non_interactive_error/out.test.toml @@ -0,0 +1,5 @@ +Local = true +Cloud = false + +[EnvMatrix] + DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/bundle/multi_profile/non_interactive_error/output.txt b/acceptance/bundle/multi_profile/non_interactive_error/output.txt new file mode 100644 index 0000000000..d3621f1274 --- /dev/null +++ b/acceptance/bundle/multi_profile/non_interactive_error/output.txt @@ -0,0 +1,23 @@ + +>>> [CLI] bundle validate +Error: cannot resolve bundle auth configuration: resolve: [DATABRICKS_URL]: multiple profiles matched: profile-1, profile-2: please set DATABRICKS_CONFIG_PROFILE or provide --profile flag to specify one. Config: host=[DATABRICKS_URL], config_file=.databrickscfg, databricks_cli_path=[CLI]. Env: DATABRICKS_CONFIG_FILE, DATABRICKS_CLI_PATH + +Matching workspace profiles: profile-1, profile-2 + +Fix (pick one): + 1. Set profile in databricks.yml: + workspace: + profile: profile-1 + 2. Pass a flag: + databricks bundle validate --profile profile-1 + 3. Set env var: + DATABRICKS_CONFIG_PROFILE=profile-1 + +Name: multi-profile-non-interactive +Target: default +Workspace: + Host: [DATABRICKS_URL] + +Found 1 error + +Exit code: 1 diff --git a/acceptance/bundle/multi_profile/non_interactive_error/script b/acceptance/bundle/multi_profile/non_interactive_error/script new file mode 100644 index 0000000000..85003ad471 --- /dev/null +++ b/acceptance/bundle/multi_profile/non_interactive_error/script @@ -0,0 +1,17 @@ +# Set up .databrickscfg with two workspace profiles for the same host. +envsubst < .databrickscfg > out && mv out .databrickscfg + +cat > databricks.yml << EOF +bundle: + name: multi-profile-non-interactive + +workspace: + host: $DATABRICKS_HOST +EOF + +export DATABRICKS_CONFIG_FILE=.databrickscfg +unset DATABRICKS_HOST +unset DATABRICKS_TOKEN + +# Multiple workspace profiles, non-interactive → error with guidance. +errcode trace $CLI bundle validate diff --git a/acceptance/bundle/multi_profile/test.toml b/acceptance/bundle/multi_profile/test.toml new file mode 100644 index 0000000000..85e02532c9 --- /dev/null +++ b/acceptance/bundle/multi_profile/test.toml @@ -0,0 +1,3 @@ +Local = true +Cloud = false +Ignore = [".databricks"] diff --git a/bundle/bundle.go b/bundle/bundle.go index f5a258a013..b2dae07554 100644 --- a/bundle/bundle.go +++ b/bundle/bundle.go @@ -245,6 +245,14 @@ func (b *Bundle) SetWorkpaceClient(w *databricks.WorkspaceClient) { b.client = w } +// ClearWorkspaceClient resets the workspace client cache, allowing +// WorkspaceClientE() to attempt client creation again on the next call. +func (b *Bundle) ClearWorkspaceClient() { + b.clientOnce = sync.Once{} + b.client = nil + b.clientErr = nil +} + // LocalStateDir returns directory to use for temporary files for this bundle without creating // Scoped to the bundle's target. func (b *Bundle) GetLocalStateDir(ctx context.Context, paths ...string) string { diff --git a/bundle/bundle_test.go b/bundle/bundle_test.go index 9cc9c4de4b..5b2aff58e0 100644 --- a/bundle/bundle_test.go +++ b/bundle/bundle_test.go @@ -179,3 +179,28 @@ func TestBundleGetResourceConfigJobsPointer(t *testing.T) { require.EqualError(t, err, "no such resource type in the config: \"not_found\"") require.Nil(t, res) } + +func TestClearWorkspaceClient(t *testing.T) { + // First attempt: profile "profile-A" doesn't exist → error mentions "profile-A". + b := &Bundle{} + b.Config.Workspace.Host = "https://nonexistent.example.com" + b.Config.Workspace.Profile = "profile-A" + + _, err1 := b.WorkspaceClientE() + require.Error(t, err1) + assert.Contains(t, err1.Error(), "profile-A") + + // Without retry, second call returns the same cached error (same object). + _, err1b := b.WorkspaceClientE() + assert.Same(t, err1, err1b, "expected same cached error without retry") + + // After retry, change the profile to "profile-B" and call again. + // If retry didn't re-execute, the error would still mention "profile-A". + b.ClearWorkspaceClient() + b.Config.Workspace.Profile = "profile-B" + + _, err2 := b.WorkspaceClientE() + require.Error(t, err2) + assert.Contains(t, err2.Error(), "profile-B", "expected re-execution to pick up new profile") + assert.NotContains(t, err2.Error(), "profile-A", "stale cached error should not appear") +} diff --git a/cmd/root/auth.go b/cmd/root/auth.go index 2ae0789eda..60360bd3e6 100644 --- a/cmd/root/auth.go +++ b/cmd/root/auth.go @@ -234,6 +234,27 @@ func MustWorkspaceClient(cmd *cobra.Command, args []string) error { return nil } +// promptForProfileByHost prompts the user to select a profile when multiple +// profiles match the same host. +func promptForProfileByHost(ctx context.Context, profiles profile.Profiles, host string) (string, error) { + i, _, err := cmdio.RunSelect(ctx, &promptui.Select{ + Label: "Multiple profiles match host " + host, + Items: profiles, + Searcher: profiles.SearchCaseInsensitive, + StartInSearchMode: true, + Templates: &promptui.SelectTemplates{ + Label: "{{ . | faint }}", + Active: `{{.Name | bold}}{{if .AccountID}} (account: {{.AccountID|faint}}){{end}}{{if .WorkspaceID}} (workspace: {{.WorkspaceID|faint}}){{end}}`, + Inactive: `{{.Name}}{{if .AccountID}} (account: {{.AccountID}}){{end}}{{if .WorkspaceID}} (workspace: {{.WorkspaceID}}){{end}}`, + Selected: `{{ "Using profile" | faint }}: {{ .Name | bold }}`, + }, + }) + if err != nil { + return "", err + } + return profiles[i].Name, nil +} + func AskForWorkspaceProfile(ctx context.Context) (string, error) { profiler := profile.GetProfiler(ctx) path, err := profiler.GetPath(ctx) diff --git a/cmd/root/bundle.go b/cmd/root/bundle.go index 3876b7fec4..123990acff 100644 --- a/cmd/root/bundle.go +++ b/cmd/root/bundle.go @@ -2,11 +2,17 @@ package root import ( "context" + "errors" + "fmt" + "strings" "github.com/databricks/cli/bundle" "github.com/databricks/cli/bundle/env" "github.com/databricks/cli/bundle/phases" "github.com/databricks/cli/libs/cmdctx" + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/databrickscfg" + "github.com/databricks/cli/libs/databrickscfg/profile" envlib "github.com/databricks/cli/libs/env" "github.com/databricks/cli/libs/logdiag" "github.com/spf13/cobra" @@ -72,6 +78,60 @@ func configureProfile(cmd *cobra.Command, b *bundle.Bundle) { }) } +// resolveProfileAmbiguity resolves a multi-profile match by filtering to +// workspace-compatible profiles and either auto-selecting, prompting, or +// returning a guidance error. +func resolveProfileAmbiguity(cmd *cobra.Command, b *bundle.Bundle, originalErr error, names []string) (string, error) { + ctx := cmd.Context() + + namesMatcher := profile.MatchProfileNames(names...) + profiler := profile.GetProfiler(ctx) + profiles, err := profiler.LoadProfiles(ctx, func(p profile.Profile) bool { + return namesMatcher(p) && profile.MatchWorkspaceProfiles(p) + }) + if err != nil { + if errors.Is(err, profile.ErrNoConfiguration) { + return "", originalErr + } + return "", err + } + + switch len(profiles) { + case 0: + return "", originalErr + case 1: + // Exactly one workspace-compatible profile — auto-select. + // This happens when multiple profiles match a host but only one + // is workspace-compatible (the rest are account-only). + return profiles[0].Name, nil + } + + // Multiple workspace-compatible profiles — need interactive selection. + _, hasProfileFlag := profileFlagValue(cmd) + allowPrompt := !hasProfileFlag && !shouldSkipPrompt(ctx) + if !allowPrompt || !cmdio.IsPromptSupported(ctx) { + return "", fmt.Errorf( + "%w\n\nMatching workspace profiles: %s\n\n"+ + "Fix (pick one):\n"+ + " 1. Set profile in databricks.yml:\n"+ + " workspace:\n"+ + " profile: %s\n"+ + " 2. Pass a flag:\n"+ + " %s --profile %s\n"+ + " 3. Set env var:\n"+ + " DATABRICKS_CONFIG_PROFILE=%s", + originalErr, + strings.Join(profiles.Names(), ", "), + profiles[0].Name, + cmd.CommandPath(), + profiles[0].Name, + profiles[0].Name, + ) + } + + return promptForProfileByHost(ctx, profiles, b.Config.Workspace.Host) +} + // configureBundle loads the bundle configuration and configures flag values, if any. func configureBundle(cmd *cobra.Command, b *bundle.Bundle) { // Load bundle and select target. @@ -96,9 +156,27 @@ func configureBundle(cmd *cobra.Command, b *bundle.Bundle) { // is a fast operation. It does not perform network I/O or invoke processes (for example the Azure CLI). client, err := b.WorkspaceClientE() if err != nil { - logdiag.LogError(ctx, err) - return + names, isMulti := databrickscfg.AsMultipleProfiles(err) + if !isMulti { + logdiag.LogError(ctx, err) + return + } + + selected, resolveErr := resolveProfileAmbiguity(cmd, b, err, names) + if resolveErr != nil { + logdiag.LogError(ctx, resolveErr) + return + } + + b.Config.Workspace.Profile = selected + b.ClearWorkspaceClient() + client, err = b.WorkspaceClientE() + if err != nil { + logdiag.LogError(ctx, err) + return + } } + ctx = cmdctx.SetConfigUsed(ctx, client.Config) cmd.SetContext(ctx) } diff --git a/cmd/root/bundle_test.go b/cmd/root/bundle_test.go index 12fb667d8d..684117cbe3 100644 --- a/cmd/root/bundle_test.go +++ b/cmd/root/bundle_test.go @@ -7,9 +7,11 @@ import ( "path/filepath" "runtime" "testing" + "time" "github.com/databricks/cli/internal/testutil" "github.com/databricks/cli/libs/cmdctx" + "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/diag" "github.com/databricks/cli/libs/logdiag" "github.com/spf13/cobra" @@ -34,6 +36,7 @@ func setupDatabricksCfg(t *testing.T) { func emptyCommand(t *testing.T) *cobra.Command { ctx := context.Background() + ctx = cmdio.MockDiscard(ctx) cmd := &cobra.Command{} cmd.SetContext(ctx) initProfileFlag(cmd) @@ -97,6 +100,8 @@ func TestBundleConfigureWithMultipleMatches(t *testing.T) { diags := setupWithHost(t, cmd, "https://a.com") require.Len(t, diags, 1) assert.Contains(t, diags[0].Summary, "multiple profiles matched: PROFILE-1, PROFILE-2") + assert.Contains(t, diags[0].Summary, "Matching workspace profiles: PROFILE-1, PROFILE-2") + assert.Contains(t, diags[0].Summary, "DATABRICKS_CONFIG_PROFILE=PROFILE-1") } func TestBundleConfigureWithNonExistentProfileFlag(t *testing.T) { @@ -217,6 +222,52 @@ func TestBundleConfigureProfileFlagAndEnvVariable(t *testing.T) { assert.Equal(t, "PROFILE-2", cmdctx.ConfigUsed(cmd.Context()).Profile) } +func TestBundleConfigureMultiMatchInteractivePromptFires(t *testing.T) { + testutil.CleanupEnvironment(t) + + setupDatabricksCfg(t) + + rootPath := t.TempDir() + testutil.Chdir(t, rootPath) + + contents := ` +workspace: + host: "https://a.com" +` + err := os.WriteFile(filepath.Join(rootPath, "databricks.yml"), []byte(contents), 0o644) + require.NoError(t, err) + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + ctx, io := cmdio.SetupTest(ctx, cmdio.TestOptions{PromptSupported: true}) + defer io.Done() + + cmd := &cobra.Command{} + cmd.SetContext(ctx) + initProfileFlag(cmd) + + done := make(chan struct{}) + go func() { + defer close(done) + ctx := logdiag.InitContext(cmd.Context()) + logdiag.SetCollect(ctx, true) + cmd.SetContext(ctx) + _ = MustConfigureBundle(cmd) + }() + + // Verify the prompt fires by reading output from stderr. + // promptui with StartInSearchMode writes a search cursor first. + line, _, readErr := io.Stderr.ReadLine() + if assert.NoError(t, readErr, "expected prompt output on stderr") { + assert.Contains(t, string(line), "Search:") + } + + // Cancel to unblock the prompt. + cancel() + <-done +} + func TestTargetFlagFull(t *testing.T) { cmd := emptyCommand(t) initTargetFlag(cmd) diff --git a/libs/databrickscfg/loader.go b/libs/databrickscfg/loader.go index 84c8398bfb..7566d19a4a 100644 --- a/libs/databrickscfg/loader.go +++ b/libs/databrickscfg/loader.go @@ -22,6 +22,16 @@ func (e errMultipleProfiles) Error() string { return "multiple profiles matched: " + strings.Join(e, ", ") } +// AsMultipleProfiles checks if the error is caused by multiple profiles +// matching the same host. If so, it returns the matching profile names. +func AsMultipleProfiles(err error) ([]string, bool) { + var e errMultipleProfiles + if errors.As(err, &e) { + return []string(e), true + } + return nil, false +} + func findMatchingProfile(configFile *config.File, matcher func(*ini.Section) bool) (*ini.Section, error) { // Look for sections in the configuration file that match the configured host. var matching []*ini.Section diff --git a/libs/databrickscfg/loader_test.go b/libs/databrickscfg/loader_test.go index c42fcdbdd1..425620abb5 100644 --- a/libs/databrickscfg/loader_test.go +++ b/libs/databrickscfg/loader_test.go @@ -1,10 +1,12 @@ package databrickscfg import ( + "errors" "testing" "github.com/databricks/databricks-sdk-go/config" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestLoaderSkipsEmptyHost(t *testing.T) { @@ -133,3 +135,32 @@ func TestLoaderErrorsOnMultipleMatches(t *testing.T) { assert.Error(t, err) assert.ErrorContains(t, err, "https://foo: multiple profiles matched: foo1, foo2") } + +func TestAsMultipleProfilesExtractsNames(t *testing.T) { + cfg := config.Config{ + Loaders: []config.Loader{ + ResolveProfileFromHost, + }, + ConfigFile: "profile/testdata/databrickscfg", + Host: "https://foo/bar", + } + + err := cfg.EnsureResolved() + require.Error(t, err) + + names, ok := AsMultipleProfiles(err) + assert.True(t, ok) + assert.Equal(t, []string{"foo1", "foo2"}, names) +} + +func TestAsMultipleProfilesReturnsFalseForUnrelatedError(t *testing.T) { + names, ok := AsMultipleProfiles(errors.New("some other error")) + assert.False(t, ok) + assert.Nil(t, names) +} + +func TestAsMultipleProfilesReturnsFalseForNil(t *testing.T) { + names, ok := AsMultipleProfiles(nil) + assert.False(t, ok) + assert.Nil(t, names) +} diff --git a/libs/databrickscfg/profile/profiler.go b/libs/databrickscfg/profile/profiler.go index 53ff7b305d..8eff2675b9 100644 --- a/libs/databrickscfg/profile/profiler.go +++ b/libs/databrickscfg/profile/profiler.go @@ -26,6 +26,18 @@ func MatchAllProfiles(p Profile) bool { return true } +// MatchProfileNames returns a match function that matches profiles by name. +func MatchProfileNames(names ...string) ProfileMatchFunction { + nameSet := make(map[string]struct{}, len(names)) + for _, n := range names { + nameSet[n] = struct{}{} + } + return func(p Profile) bool { + _, ok := nameSet[p.Name] + return ok + } +} + func WithName(name string) ProfileMatchFunction { return func(p Profile) bool { return p.Name == name diff --git a/libs/databrickscfg/profile/profiler_test.go b/libs/databrickscfg/profile/profiler_test.go index 75f4fa57d5..aa13e76a46 100644 --- a/libs/databrickscfg/profile/profiler_test.go +++ b/libs/databrickscfg/profile/profiler_test.go @@ -59,6 +59,20 @@ func TestWithHost(t *testing.T) { } } +func TestMatchProfileNames(t *testing.T) { + fn := MatchProfileNames("dev", "staging") + + assert.True(t, fn(Profile{Name: "dev"})) + assert.True(t, fn(Profile{Name: "staging"})) + assert.False(t, fn(Profile{Name: "production"})) + assert.False(t, fn(Profile{Name: ""})) +} + +func TestMatchProfileNamesEmpty(t *testing.T) { + fn := MatchProfileNames() + assert.False(t, fn(Profile{Name: "anything"})) +} + func TestWithHostAndAccountID(t *testing.T) { cases := []struct { name string