Skip to content

Interactive profile selection on multiple host matches#4604

Open
simonfaltum wants to merge 5 commits intomainfrom
simonfaltum/interactive-profile-multi-match
Open

Interactive profile selection on multiple host matches#4604
simonfaltum wants to merge 5 commits intomainfrom
simonfaltum/interactive-profile-multi-match

Conversation

@simonfaltum
Copy link
Member

@simonfaltum simonfaltum commented Feb 26, 2026

Why

When you have two profiles in ~/.databrickscfg that point to the same workspace (e.g. dev and staging both with host = https://myworkspace.cloud.databricks.com), and you run a bundle command from a bundle that sets workspace.host but not profile, the CLI crashes with a confusing error telling you to set DATABRICKS_CONFIG_PROFILE or --profile.

With the host-agnostic auth work, users are encouraged to have multiple profiles pointing at the same workspace (different auth methods, different roles). This "multiple profiles matched" error will become much more common. Today it's a paper cut; soon it'll be a wall.

Changes

Before: The CLI errored immediately with multiple profiles matched: dev, staging: please set DATABRICKS_CONFIG_PROFILE or provide --profile flag. No way to recover without restarting the command with a flag or env var.

Now: When the CLI detects multiple profiles matching the same host, instead of erroring it:

  1. Filters to workspace-compatible profiles (excludes account-only profiles)
  2. If only one workspace profile remains — auto-selects it silently (no prompt, works in CI)
  3. If multiple remain and the terminal is interactive — shows a picker so the user can choose
  4. If multiple remain and non-interactive (CI, scripts) — returns the original error wrapped with actionable guidance showing three concrete fix options (databricks.yml, --profile flag, env var)

Implementation is catch-and-retry in configureBundle: let WorkspaceClientE() run normally, catch errMultipleProfiles, resolve the ambiguity, set the profile on the workspace config, reset the sync.Once cache via RetryWorkspaceClient(), and retry. On retry the SDK uses named-profile resolution (since Profile is now set), which is the correct code path.

New helpers:

  • AsMultipleProfiles(err) — extracts profile names from the error chain
  • MatchProfileNames(names...) — filters profiles by name
  • RetryWorkspaceClient() — resets the workspace client cache for retry
  • promptForProfileByHost() — interactive select UI for profile disambiguation

Scope is bundle-only. ResolveProfileFromHost is only wired in Workspace.Client(), so non-bundle commands are unaffected.

Test plan

  • AsMultipleProfiles extracts names through the full wrapping chain; returns false for unrelated/nil errors
  • MatchProfileNames matches correct profiles, rejects others, handles empty input
  • RetryWorkspaceClient proves re-execution by changing config between attempts and asserting the error message changes
  • Auto-select: two profiles match host, only one is workspace-compatible → auto-selected without prompt
  • No workspace profiles: all matched profiles are account-only → original error returned
  • Non-interactive multi-match: two workspace profiles, prompt not supported → error with guidance
  • SkipPrompt respected: error returned, no prompt
  • Interactive prompt fires: PromptSupported: true → select prompt starts on stderr
  • Env-auth skip: DATABRICKS_TOKEN set → errMultipleProfiles never fires
  • All existing bundle config tests continue to pass
  • make checks, make lintfull pass

When multiple profiles in ~/.databrickscfg match the same host, instead
of erroring, the CLI now: filters to workspace-compatible profiles,
auto-selects if only one remains, prompts interactively if multiple
remain, or returns an actionable error in non-interactive mode.
- Use cmd.CommandPath() instead of hardcoded "databricks bundle deploy"
  in the non-interactive fix hint, so guidance matches the actual command
- Add interactive prompt test that verifies the select prompt fires when
  PromptSupported is true and multiple workspace profiles match
- Improve RetryWorkspaceClient test to prove re-execution by changing the
  profile between attempts and asserting the error message changes
@eng-dev-ecosystem-bot
Copy link
Collaborator

eng-dev-ecosystem-bot commented Feb 26, 2026

Commit: d9ecb10

Run: 22450162339

Env 🟨​KNOWN 💚​RECOVERED 🙈​SKIP ✅​pass 🙈​skip Time
🟨​ aws linux 7 1 7 268 772 6:18
🟨​ aws windows 7 1 7 270 770 6:26
💚​ aws-ucws linux 8 7 364 688 6:47
💚​ aws-ucws windows 8 7 366 686 5:58
💚​ azure linux 2 9 271 770 6:03
💚​ azure windows 2 9 273 768 6:46
💚​ azure-ucws linux 2 9 369 684 7:17
💚​ azure-ucws windows 2 9 371 682 5:49
💚​ gcp linux 2 9 267 773 5:48
💚​ gcp windows 2 9 269 771 5:12
15 interesting tests: 7 KNOWN, 7 SKIP, 1 RECOVERED
Test Name aws linux aws windows aws-ucws linux aws-ucws windows azure linux azure windows azure-ucws linux azure-ucws windows gcp linux gcp windows
🟨​ TestAccept 🟨​K 🟨​K 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R
🙈​ TestAccept/bundle/resources/permissions 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🟨​ TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/with_permissions 🟨​K 🟨​K 💚​R 💚​R 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🟨​ TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/with_permissions/DATABRICKS_BUNDLE_ENGINE=direct 🟨​K 🟨​K 💚​R 💚​R
🟨​ TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/with_permissions/DATABRICKS_BUNDLE_ENGINE=terraform 🟨​K 🟨​K 💚​R 💚​R
🟨​ TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/without_permissions 🟨​K 🟨​K 💚​R 💚​R 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🟨​ TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/without_permissions/DATABRICKS_BUNDLE_ENGINE=direct 🟨​K 🟨​K 💚​R 💚​R
🟨​ TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/without_permissions/DATABRICKS_BUNDLE_ENGINE=terraform 🟨​K 🟨​K 💚​R 💚​R
🙈​ TestAccept/bundle/resources/postgres_branches/basic 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/postgres_branches/recreate 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/postgres_branches/update_protected 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/postgres_branches/without_branch_id 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/postgres_endpoints/recreate 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/synced_database_tables/basic 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
💚​ TestAccept/ssh/connection 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R
Top 20 slowest tests (at least 2 minutes):
duration env testname
4:19 azure windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
3:56 azure-ucws linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
3:54 azure linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
3:46 gcp linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
3:41 gcp windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
3:40 azure-ucws windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
3:38 gcp linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
3:22 aws-ucws windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
3:21 aws windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
3:19 aws-ucws linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
3:10 gcp windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
2:53 aws linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
2:52 aws windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
2:50 aws linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
2:49 aws-ucws windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
2:42 azure windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
2:42 aws-ucws linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
2:11 azure linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
2:08 azure-ucws windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
2:07 azure-ucws linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct

bundle/bundle.go Outdated
// WorkspaceClientE() to attempt client creation again on the next call.
// This is used after resolving profile ambiguity to retry with the
// selected profile.
func (b *Bundle) RetryWorkspaceClient() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe a better name is ClearWorkspaceClient?

Copy link
Member Author

Choose a reason for hiding this comment

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

Makes sense, ClearWorkspaceClient is more accurate since it just clears the cache. Renamed.


// 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) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you please add a screenshot / video what the selection UI looks like to the PR description?

Copy link
Member Author

Choose a reason for hiding this comment

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

Will add a screenshot once I have an environment with two workspace profiles set up for the same host. The prompt shows a searchable list with profile names and their account/workspace IDs.

logdiag.LogError(ctx, err)
return
// Check if this is a multi-profile ambiguity error.
names, isMulti := databrickscfg.AsMultipleProfiles(err)
Copy link
Contributor

Choose a reason for hiding this comment

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

style: can you refactor this to a separate method?

Copy link
Member Author

Choose a reason for hiding this comment

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

Agreed, extracted to a dedicated function. Much cleaner.

case 1:
// Exactly one workspace-compatible profile — auto-select.
// This is deterministic and works in non-interactive mode.
selected = profiles[0].Name
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we hit this line of code? I would assume this error branch would never trigger if the profile was successfully determined.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes I think we do though it might not be happening until we live in a host-agnostic world. The SDK's ResolveProfileFromHost returns all profiles matching the host including account-only profiles. After we filter to workspace-compatible ones, we may end up with just one. The test TestBundleConfigureMultiMatchAutoSelectSingleWorkspace covers this: two profiles for the same host, one workspace and one account-only, and the workspace one is auto-selected.

cmd := emptyCommand(t)
ctx := logdiag.InitContext(cmd.Context())
logdiag.SetCollect(ctx, true)
cmd.SetContext(ctx)
Copy link
Contributor

Choose a reason for hiding this comment

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

Lets convert any tests that run commands non-interactively to acceptance tests? They are often a better framework when testing CLI commands end to end.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done, converted the non-interactive tests to acceptance tests under acceptance/bundle/multi_profile/. Kept the interactive prompt test as a unit test since acceptance tests can't drive interactive prompts.

bundle/bundle.go Outdated
// WorkspaceClientE() to attempt client creation again on the next call.
// This is used after resolving profile ambiguity to retry with the
// selected profile.
func (b *Bundle) RetryWorkspaceClient() {
Copy link
Contributor

Choose a reason for hiding this comment

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

It might be better to encapsulate the prompt in the WorkspaceClientE method rather than clear the client and then set the client again. WDYT?

Copy link
Member Author

Choose a reason for hiding this comment

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

I considered this but WorkspaceClientE lives in bundle/ which doesn't (and shouldn't) know about prompts or cmdio. The prompt depends on cobra. Command context and cmdio.RunSelect which are cmd-layer concerns.

- Rename RetryWorkspaceClient to ClearWorkspaceClient
- Extract multi-profile handling into resolveProfileAmbiguity
- Convert 4 non-interactive unit tests to acceptance tests
- Keep interactive prompt test as unit test
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants