Skip to content

Interactive profile selection on multiple host matches#4604

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

Interactive profile selection on multiple host matches#4604
simonfaltum wants to merge 4 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: 676e720

Run: 22435847000

Env 🟨​KNOWN 🔄​flaky 💚​RECOVERED 🙈​SKIP ✅​pass 🙈​skip Time
🟨​ aws linux 7 2 1 7 266 767 8:29
🟨​ aws windows 7 1 7 270 765 6:06
💚​ aws-ucws linux 8 7 364 683 5:59
💚​ aws-ucws windows 8 7 366 681 5:28
💚​ azure linux 2 9 271 765 6:08
💚​ azure windows 2 9 273 763 4:48
🔄​ azure-ucws linux 2 2 9 367 679 6:39
💚​ azure-ucws windows 2 9 371 677 5:57
💚​ gcp linux 2 9 267 768 5:30
💚​ gcp windows 2 9 269 766 4:40
19 interesting tests: 7 KNOWN, 7 SKIP, 4 flaky, 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
🔄​ TestSparkJarTaskDeployAndRunOnWorkspace 🔄​f ✅​p ✅​p ✅​p ✅​p ✅​p ✅​p ✅​p ✅​p ✅​p
🔄​ TestSparkJarTaskDeployAndRunOnWorkspace/Databricks_Runtime_14.3_LTS 🔄​f ✅​p ✅​p ✅​p ✅​p ✅​p ✅​p ✅​p ✅​p ✅​p
🔄​ TestFilerWorkspaceNotebook ✅​p ✅​p ✅​p ✅​p ✅​p ✅​p 🔄​f ✅​p ✅​p ✅​p
🔄​ TestFilerWorkspaceNotebook/rJupyterNb.ipynb ✅​p ✅​p ✅​p ✅​p ✅​p ✅​p 🔄​f ✅​p ✅​p ✅​p
Top 20 slowest tests (at least 2 minutes):
duration env testname
3:47 gcp windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
3:21 gcp linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
3:10 aws linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
3:09 gcp windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
3:09 gcp linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
2:56 aws linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
2:52 aws-ucws windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
2:51 aws-ucws windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
2:51 aws windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
2:51 aws-ucws linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
2:47 azure-ucws windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
2:46 aws-ucws linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
2:41 aws windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
2:19 azure-ucws linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
2:17 azure linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
2:15 azure windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
2:13 azure linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
2:13 azure windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
2:13 azure-ucws linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
2:09 azure-ucws windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct

// 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?

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.

// 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?

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