From e119cd244b6cebfbc7ea4e96df39cc3dff972c6c Mon Sep 17 00:00:00 2001 From: simon Date: Wed, 27 May 2026 14:10:02 +0200 Subject: [PATCH 1/2] auth: honor --host on `databricks auth describe` and prefer default profile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `databricks auth describe --host X` silently ignored the flag: the value was bound to the parent command's `authArguments.Host` but never read by the describe flow, so the SDK fell back to [__settings__].default_profile (silently describing a different host) or the "default auth: cannot configure default credentials" error even when a host-matching profile existed. The display still labelled the value "(from --host flag)", making the mismatch hard to spot. - describe.go: when --host is set without --profile, resolve the host to a profile name (via the existing `resolveHostToProfile`) and set --profile so MustAnyClient uses it. DATABRICKS_CONFIG_PROFILE is left alone — it's an explicit user signal. - resolve.go: when multiple profiles match a host, prefer the [__settings__].default_profile if it's one of them, before falling back to the picker or ambiguity error. Same helper is used by `auth logout`, which gets the same UX improvement. Co-authored-by: Isaac --- cmd/auth/describe.go | 31 +++++++++++++++++++++++++++++++ cmd/auth/resolve.go | 14 ++++++++++++++ cmd/auth/resolve_test.go | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+) diff --git a/cmd/auth/describe.go b/cmd/auth/describe.go index b4abe63d65d..94830221c16 100644 --- a/cmd/auth/describe.go +++ b/cmd/auth/describe.go @@ -10,6 +10,7 @@ import ( "github.com/databricks/cli/libs/auth/storage" "github.com/databricks/cli/libs/cmdctx" "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/databrickscfg/profile" "github.com/databricks/cli/libs/env" "github.com/databricks/cli/libs/flags" "github.com/databricks/cli/libs/log" @@ -65,6 +66,9 @@ func newDescribeCommand() *cobra.Command { cmd.RunE = func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() + if err := resolveProfileFromHostFlag(cmd, profile.DefaultProfiler); err != nil { + return err + } var status *authStatus var err error status, err = getAuthStatus(cmd, args, showSensitive, func(cmd *cobra.Command, args []string) (*config.Config, bool, error) { @@ -85,6 +89,33 @@ func newDescribeCommand() *cobra.Command { return cmd } +// resolveProfileFromHostFlag translates an explicit --host into a --profile +// for `auth describe`. Without this, the downstream profile resolver ignores +// --host and either falls back to [__settings__].default_profile (silently +// describing a different host than the one the user named) or errors with the +// SDK's default-credentials message even though a host-matching profile +// exists. DATABRICKS_CONFIG_PROFILE is left alone — it's an explicit signal +// the user already made. +func resolveProfileFromHostFlag(cmd *cobra.Command, profiler profile.Profiler) error { + hostFlag := cmd.Flag("host") + profileFlag := cmd.Flag("profile") + if hostFlag == nil || profileFlag == nil { + return nil + } + if !hostFlag.Changed || profileFlag.Changed { + return nil + } + ctx := cmd.Context() + if env.Get(ctx, "DATABRICKS_CONFIG_PROFILE") != "" { + return nil + } + profileName, err := resolveHostToProfile(ctx, hostFlag.Value.String(), profiler) + if err != nil { + return err + } + return profileFlag.Value.Set(profileName) +} + type tryAuth func(cmd *cobra.Command, args []string) (*config.Config, bool, error) func getAuthStatus(cmd *cobra.Command, args []string, showSensitive bool, fn tryAuth) (*authStatus, error) { diff --git a/cmd/auth/resolve.go b/cmd/auth/resolve.go index e6cc199c38f..2117135d2cb 100644 --- a/cmd/auth/resolve.go +++ b/cmd/auth/resolve.go @@ -8,7 +8,10 @@ import ( "strings" "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/databrickscfg" "github.com/databricks/cli/libs/databrickscfg/profile" + "github.com/databricks/cli/libs/env" + "github.com/databricks/cli/libs/log" "github.com/databricks/databricks-sdk-go/config" ) @@ -72,6 +75,17 @@ func resolveHostToProfile(ctx context.Context, host string, profiler profile.Pro names := strings.Join(allProfiles.Names(), ", ") return "", fmt.Errorf("no profile found matching host %q. Available profiles: %s", host, names) default: + // Prefer the configured default profile when it's one of the host + // matches, so commands that pass --host don't trip the picker for + // users who already picked a default. + if defaultProfile, _ := databrickscfg.GetDefaultProfile(ctx, env.Get(ctx, "DATABRICKS_CONFIG_FILE")); defaultProfile != "" { + for _, p := range hostProfiles { + if p.Name == defaultProfile { + log.Debugf(ctx, "multiple profiles match host %q; using default profile %q", host, defaultProfile) + return p.Name, nil + } + } + } if cmdio.IsPromptSupported(ctx) { selected, err := profile.SelectProfile(ctx, profile.SelectConfig{ Label: fmt.Sprintf("Multiple profiles found for %q. Select one to use", host), diff --git a/cmd/auth/resolve_test.go b/cmd/auth/resolve_test.go index 8f4fcbbc111..4e63e80bfe7 100644 --- a/cmd/auth/resolve_test.go +++ b/cmd/auth/resolve_test.go @@ -1,6 +1,8 @@ package auth import ( + "os" + "path/filepath" "testing" "github.com/databricks/cli/libs/cmdio" @@ -127,6 +129,36 @@ func TestResolveHostToProfileMatchesMultipleProfiles(t *testing.T) { assert.ErrorContains(t, err, "dev2") } +func TestResolveHostToProfilePrefersConfiguredDefault(t *testing.T) { + cfgPath := filepath.Join(t.TempDir(), ".databrickscfg") + err := os.WriteFile(cfgPath, []byte(` +[__settings__] +default_profile = dev2 + +[dev1] +host = https://shared.cloud.databricks.com +auth_type = databricks-cli + +[dev2] +host = https://shared.cloud.databricks.com +auth_type = databricks-cli +`), 0o600) + require.NoError(t, err) + t.Setenv("DATABRICKS_CONFIG_FILE", cfgPath) + + ctx := cmdio.MockDiscard(t.Context()) + profiler := profile.InMemoryProfiler{ + Profiles: profile.Profiles{ + {Name: "dev1", Host: "https://shared.cloud.databricks.com", AuthType: "databricks-cli"}, + {Name: "dev2", Host: "https://shared.cloud.databricks.com", AuthType: "databricks-cli"}, + }, + } + + resolved, err := resolveHostToProfile(ctx, "https://shared.cloud.databricks.com", profiler) + require.NoError(t, err) + assert.Equal(t, "dev2", resolved) +} + func TestResolveHostToProfileMatchesNothing(t *testing.T) { ctx := cmdio.MockDiscard(t.Context()) profiler := profile.InMemoryProfiler{ From 9fe91b97311d6d8222b8b8bd831c471f96ee746f Mon Sep 17 00:00:00 2001 From: simon Date: Wed, 27 May 2026 15:34:42 +0200 Subject: [PATCH 2/2] auth: isolate ambiguity test and add direct coverage for --host wiring GPT review caught two test gaps: - TestResolveHostToProfileMatchesMultipleProfiles relied on whatever default_profile happened to be in the caller's real .databrickscfg. After the new prefer-default branch, a local default could inadvertently resolve the call instead of returning the expected ambiguity error. Point DATABRICKS_CONFIG_FILE at an empty temp file for the test. - The auth describe --host wiring (resolveProfileFromHostFlag) had no direct test; the only coverage was indirect through resolveHostToProfile. Add focused subtests for the no-op, --profile precedence, single-match, no-match, and DATABRICKS_CONFIG_PROFILE bail-out cases. Co-authored-by: Isaac --- cmd/auth/describe_test.go | 63 +++++++++++++++++++++++++++++++++++++++ cmd/auth/resolve_test.go | 6 ++++ 2 files changed, 69 insertions(+) diff --git a/cmd/auth/describe_test.go b/cmd/auth/describe_test.go index 3fb26c06d93..84a4bdc3df0 100644 --- a/cmd/auth/describe_test.go +++ b/cmd/auth/describe_test.go @@ -2,11 +2,13 @@ package auth import ( "errors" + "os" "path/filepath" "testing" "github.com/databricks/cli/libs/auth/storage" "github.com/databricks/cli/libs/cmdctx" + "github.com/databricks/cli/libs/databrickscfg/profile" "github.com/databricks/databricks-sdk-go/config" "github.com/databricks/databricks-sdk-go/experimental/mocks" "github.com/databricks/databricks-sdk-go/service/iam" @@ -16,6 +18,67 @@ import ( "github.com/stretchr/testify/require" ) +func newHostProfileCmd(t *testing.T) *cobra.Command { + t.Helper() + cmd := &cobra.Command{} + cmd.Flags().String("host", "", "") + cmd.Flags().String("profile", "", "") + cmd.SetContext(t.Context()) + return cmd +} + +func TestResolveProfileFromHostFlag(t *testing.T) { + cfgPath := filepath.Join(t.TempDir(), ".databrickscfg") + require.NoError(t, os.WriteFile(cfgPath, []byte(""), 0o600)) + t.Setenv("DATABRICKS_CONFIG_FILE", cfgPath) + + profiler := profile.InMemoryProfiler{ + Profiles: profile.Profiles{ + {Name: "dev", Host: "https://dev.cloud.databricks.com", AuthType: "databricks-cli"}, + }, + } + + t.Run("no flags set is a no-op", func(t *testing.T) { + cmd := newHostProfileCmd(t) + require.NoError(t, resolveProfileFromHostFlag(cmd, profiler)) + assert.Empty(t, cmd.Flag("profile").Value.String()) + }) + + t.Run("--profile already set wins; --host is ignored", func(t *testing.T) { + cmd := newHostProfileCmd(t) + require.NoError(t, cmd.Flags().Set("host", "https://dev.cloud.databricks.com")) + require.NoError(t, cmd.Flags().Set("profile", "explicit")) + require.NoError(t, resolveProfileFromHostFlag(cmd, profiler)) + assert.Equal(t, "explicit", cmd.Flag("profile").Value.String()) + }) + + t.Run("--host with a single match wires --profile", func(t *testing.T) { + cmd := newHostProfileCmd(t) + require.NoError(t, cmd.Flags().Set("host", "https://dev.cloud.databricks.com")) + require.NoError(t, resolveProfileFromHostFlag(cmd, profiler)) + assert.Equal(t, "dev", cmd.Flag("profile").Value.String()) + }) + + t.Run("--host with no match surfaces a clear error", func(t *testing.T) { + cmd := newHostProfileCmd(t) + require.NoError(t, cmd.Flags().Set("host", "https://nope.cloud.databricks.com")) + err := resolveProfileFromHostFlag(cmd, profiler) + require.Error(t, err) + assert.Contains(t, err.Error(), "no profile found matching host") + assert.Empty(t, cmd.Flag("profile").Value.String()) + }) + + t.Run("DATABRICKS_CONFIG_PROFILE is left alone", func(t *testing.T) { + t.Setenv("DATABRICKS_CONFIG_PROFILE", "from-env") + cmd := newHostProfileCmd(t) + require.NoError(t, cmd.Flags().Set("host", "https://dev.cloud.databricks.com")) + require.NoError(t, resolveProfileFromHostFlag(cmd, profiler)) + // We don't overwrite --profile when the user signalled an explicit + // choice via the env var. + assert.Empty(t, cmd.Flag("profile").Value.String()) + }) +} + func TestGetWorkspaceAuthStatus(t *testing.T) { ctx := t.Context() m := mocks.NewMockWorkspaceClient(t) diff --git a/cmd/auth/resolve_test.go b/cmd/auth/resolve_test.go index 4e63e80bfe7..2cb2fb6ff36 100644 --- a/cmd/auth/resolve_test.go +++ b/cmd/auth/resolve_test.go @@ -115,6 +115,12 @@ func TestResolveHostToProfileMatchesOneProfile(t *testing.T) { } func TestResolveHostToProfileMatchesMultipleProfiles(t *testing.T) { + // Point at an isolated config file with no default_profile so the new + // prefer-default branch doesn't pick up the caller's real .databrickscfg. + cfgPath := filepath.Join(t.TempDir(), ".databrickscfg") + require.NoError(t, os.WriteFile(cfgPath, []byte(""), 0o600)) + t.Setenv("DATABRICKS_CONFIG_FILE", cfgPath) + ctx := cmdio.MockDiscard(t.Context()) profiler := profile.InMemoryProfiler{ Profiles: profile.Profiles{