Skip to content

Commit c19a115

Browse files
authored
auth: honor --host on auth describe and prefer default profile when host is ambiguous (#5343)
## Why \`databricks auth describe --host X\` silently ignored the flag. The value was bound to the parent \`auth\` 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 than the one named) 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. Reported in a bug bash. Reproduced against \`db-deco-test.databricks.com\` (which has two matching profiles in my .databrickscfg): \`\`\` $ databricks auth describe --host https://db-deco-test.databricks.com Host: https://dogfood.staging.databricks.com ← actually dogfood, not db-deco-test ✓ host: https://dogfood.staging.databricks.com (from --host flag) ← misleading label ✓ profile: dogfood \`\`\` ## Changes - \`describe.go\`: when \`--host\` is set without \`--profile\`, resolve the host to a profile name via the existing \`resolveHostToProfile\` and set \`--profile\` so the downstream \`MustAnyClient\` uses it. \`DATABRICKS_CONFIG_PROFILE\` is left alone — it's an explicit user signal. - \`resolve.go\`: when multiple profiles match a host, prefer \`[__settings__].default_profile\` if it's one of the matches before falling back to the picker or ambiguity error. The same helper is used by \`auth logout\`, which gets the same UX improvement (no prompt when the user's default already disambiguates). After the fix, against the same host: \`\`\` $ databricks auth describe --host https://db-deco-test.databricks.com Error: multiple profiles found matching host \"https://db-deco-test.databricks.com\": spog-deco-aws, db-deco-test-chrisst. Please specify the profile name directly \`\`\` And with \`default_profile = spog-deco-aws\` in \`[__settings__]\`, the same command auto-selects \`spog-deco-aws\` without prompting. ## Test plan - [x] Reproduced against \`db-deco-test.databricks.com\`: confirmed the original bug (default profile silently used, misleading source label). - [x] After the fix: single-host-match case picks the matching profile, multi-match case prefers the configured default, no-match case gives a clean error. - [x] No-flag case still uses \`default_profile\` as before — no regression. - [x] New unit test: \`TestResolveHostToProfilePrefersConfiguredDefault\`. - [x] \`go test ./cmd/auth/...\`, \`./task checks\`, \`./task lint-q\`.
1 parent c8ea3a8 commit c19a115

4 files changed

Lines changed: 146 additions & 0 deletions

File tree

cmd/auth/describe.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/databricks/cli/libs/auth/storage"
1111
"github.com/databricks/cli/libs/cmdctx"
1212
"github.com/databricks/cli/libs/cmdio"
13+
"github.com/databricks/cli/libs/databrickscfg/profile"
1314
"github.com/databricks/cli/libs/env"
1415
"github.com/databricks/cli/libs/flags"
1516
"github.com/databricks/cli/libs/log"
@@ -65,6 +66,9 @@ func newDescribeCommand() *cobra.Command {
6566

6667
cmd.RunE = func(cmd *cobra.Command, args []string) error {
6768
ctx := cmd.Context()
69+
if err := resolveProfileFromHostFlag(cmd, profile.DefaultProfiler); err != nil {
70+
return err
71+
}
6872
var status *authStatus
6973
var err error
7074
status, err = getAuthStatus(cmd, args, showSensitive, func(cmd *cobra.Command, args []string) (*config.Config, bool, error) {
@@ -85,6 +89,33 @@ func newDescribeCommand() *cobra.Command {
8589
return cmd
8690
}
8791

92+
// resolveProfileFromHostFlag translates an explicit --host into a --profile
93+
// for `auth describe`. Without this, the downstream profile resolver ignores
94+
// --host and either falls back to [__settings__].default_profile (silently
95+
// describing a different host than the one the user named) or errors with the
96+
// SDK's default-credentials message even though a host-matching profile
97+
// exists. DATABRICKS_CONFIG_PROFILE is left alone — it's an explicit signal
98+
// the user already made.
99+
func resolveProfileFromHostFlag(cmd *cobra.Command, profiler profile.Profiler) error {
100+
hostFlag := cmd.Flag("host")
101+
profileFlag := cmd.Flag("profile")
102+
if hostFlag == nil || profileFlag == nil {
103+
return nil
104+
}
105+
if !hostFlag.Changed || profileFlag.Changed {
106+
return nil
107+
}
108+
ctx := cmd.Context()
109+
if env.Get(ctx, "DATABRICKS_CONFIG_PROFILE") != "" {
110+
return nil
111+
}
112+
profileName, err := resolveHostToProfile(ctx, hostFlag.Value.String(), profiler)
113+
if err != nil {
114+
return err
115+
}
116+
return profileFlag.Value.Set(profileName)
117+
}
118+
88119
type tryAuth func(cmd *cobra.Command, args []string) (*config.Config, bool, error)
89120

90121
func getAuthStatus(cmd *cobra.Command, args []string, showSensitive bool, fn tryAuth) (*authStatus, error) {

cmd/auth/describe_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ package auth
22

33
import (
44
"errors"
5+
"os"
56
"path/filepath"
67
"testing"
78

89
"github.com/databricks/cli/libs/auth/storage"
910
"github.com/databricks/cli/libs/cmdctx"
11+
"github.com/databricks/cli/libs/databrickscfg/profile"
1012
"github.com/databricks/databricks-sdk-go/config"
1113
"github.com/databricks/databricks-sdk-go/experimental/mocks"
1214
"github.com/databricks/databricks-sdk-go/service/iam"
@@ -16,6 +18,67 @@ import (
1618
"github.com/stretchr/testify/require"
1719
)
1820

21+
func newHostProfileCmd(t *testing.T) *cobra.Command {
22+
t.Helper()
23+
cmd := &cobra.Command{}
24+
cmd.Flags().String("host", "", "")
25+
cmd.Flags().String("profile", "", "")
26+
cmd.SetContext(t.Context())
27+
return cmd
28+
}
29+
30+
func TestResolveProfileFromHostFlag(t *testing.T) {
31+
cfgPath := filepath.Join(t.TempDir(), ".databrickscfg")
32+
require.NoError(t, os.WriteFile(cfgPath, []byte(""), 0o600))
33+
t.Setenv("DATABRICKS_CONFIG_FILE", cfgPath)
34+
35+
profiler := profile.InMemoryProfiler{
36+
Profiles: profile.Profiles{
37+
{Name: "dev", Host: "https://dev.cloud.databricks.com", AuthType: "databricks-cli"},
38+
},
39+
}
40+
41+
t.Run("no flags set is a no-op", func(t *testing.T) {
42+
cmd := newHostProfileCmd(t)
43+
require.NoError(t, resolveProfileFromHostFlag(cmd, profiler))
44+
assert.Empty(t, cmd.Flag("profile").Value.String())
45+
})
46+
47+
t.Run("--profile already set wins; --host is ignored", func(t *testing.T) {
48+
cmd := newHostProfileCmd(t)
49+
require.NoError(t, cmd.Flags().Set("host", "https://dev.cloud.databricks.com"))
50+
require.NoError(t, cmd.Flags().Set("profile", "explicit"))
51+
require.NoError(t, resolveProfileFromHostFlag(cmd, profiler))
52+
assert.Equal(t, "explicit", cmd.Flag("profile").Value.String())
53+
})
54+
55+
t.Run("--host with a single match wires --profile", func(t *testing.T) {
56+
cmd := newHostProfileCmd(t)
57+
require.NoError(t, cmd.Flags().Set("host", "https://dev.cloud.databricks.com"))
58+
require.NoError(t, resolveProfileFromHostFlag(cmd, profiler))
59+
assert.Equal(t, "dev", cmd.Flag("profile").Value.String())
60+
})
61+
62+
t.Run("--host with no match surfaces a clear error", func(t *testing.T) {
63+
cmd := newHostProfileCmd(t)
64+
require.NoError(t, cmd.Flags().Set("host", "https://nope.cloud.databricks.com"))
65+
err := resolveProfileFromHostFlag(cmd, profiler)
66+
require.Error(t, err)
67+
assert.Contains(t, err.Error(), "no profile found matching host")
68+
assert.Empty(t, cmd.Flag("profile").Value.String())
69+
})
70+
71+
t.Run("DATABRICKS_CONFIG_PROFILE is left alone", func(t *testing.T) {
72+
t.Setenv("DATABRICKS_CONFIG_PROFILE", "from-env")
73+
cmd := newHostProfileCmd(t)
74+
require.NoError(t, cmd.Flags().Set("host", "https://dev.cloud.databricks.com"))
75+
require.NoError(t, resolveProfileFromHostFlag(cmd, profiler))
76+
// We don't overwrite --profile when the user signalled an explicit
77+
// choice via the env var.
78+
assert.Empty(t, cmd.Flag("profile").Value.String())
79+
})
80+
}
81+
1982
func TestGetWorkspaceAuthStatus(t *testing.T) {
2083
ctx := t.Context()
2184
m := mocks.NewMockWorkspaceClient(t)

cmd/auth/resolve.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import (
88
"strings"
99

1010
"github.com/databricks/cli/libs/cmdio"
11+
"github.com/databricks/cli/libs/databrickscfg"
1112
"github.com/databricks/cli/libs/databrickscfg/profile"
13+
"github.com/databricks/cli/libs/env"
14+
"github.com/databricks/cli/libs/log"
1215
"github.com/databricks/databricks-sdk-go/config"
1316
)
1417

@@ -72,6 +75,17 @@ func resolveHostToProfile(ctx context.Context, host string, profiler profile.Pro
7275
names := strings.Join(allProfiles.Names(), ", ")
7376
return "", fmt.Errorf("no profile found matching host %q. Available profiles: %s", host, names)
7477
default:
78+
// Prefer the configured default profile when it's one of the host
79+
// matches, so commands that pass --host don't trip the picker for
80+
// users who already picked a default.
81+
if defaultProfile, _ := databrickscfg.GetDefaultProfile(ctx, env.Get(ctx, "DATABRICKS_CONFIG_FILE")); defaultProfile != "" {
82+
for _, p := range hostProfiles {
83+
if p.Name == defaultProfile {
84+
log.Debugf(ctx, "multiple profiles match host %q; using default profile %q", host, defaultProfile)
85+
return p.Name, nil
86+
}
87+
}
88+
}
7589
if cmdio.IsPromptSupported(ctx) {
7690
selected, err := profile.SelectProfile(ctx, profile.SelectConfig{
7791
Label: fmt.Sprintf("Multiple profiles found for %q. Select one to use", host),

cmd/auth/resolve_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package auth
22

33
import (
4+
"os"
5+
"path/filepath"
46
"testing"
57

68
"github.com/databricks/cli/libs/cmdio"
@@ -113,6 +115,12 @@ func TestResolveHostToProfileMatchesOneProfile(t *testing.T) {
113115
}
114116

115117
func TestResolveHostToProfileMatchesMultipleProfiles(t *testing.T) {
118+
// Point at an isolated config file with no default_profile so the new
119+
// prefer-default branch doesn't pick up the caller's real .databrickscfg.
120+
cfgPath := filepath.Join(t.TempDir(), ".databrickscfg")
121+
require.NoError(t, os.WriteFile(cfgPath, []byte(""), 0o600))
122+
t.Setenv("DATABRICKS_CONFIG_FILE", cfgPath)
123+
116124
ctx := cmdio.MockDiscard(t.Context())
117125
profiler := profile.InMemoryProfiler{
118126
Profiles: profile.Profiles{
@@ -127,6 +135,36 @@ func TestResolveHostToProfileMatchesMultipleProfiles(t *testing.T) {
127135
assert.ErrorContains(t, err, "dev2")
128136
}
129137

138+
func TestResolveHostToProfilePrefersConfiguredDefault(t *testing.T) {
139+
cfgPath := filepath.Join(t.TempDir(), ".databrickscfg")
140+
err := os.WriteFile(cfgPath, []byte(`
141+
[__settings__]
142+
default_profile = dev2
143+
144+
[dev1]
145+
host = https://shared.cloud.databricks.com
146+
auth_type = databricks-cli
147+
148+
[dev2]
149+
host = https://shared.cloud.databricks.com
150+
auth_type = databricks-cli
151+
`), 0o600)
152+
require.NoError(t, err)
153+
t.Setenv("DATABRICKS_CONFIG_FILE", cfgPath)
154+
155+
ctx := cmdio.MockDiscard(t.Context())
156+
profiler := profile.InMemoryProfiler{
157+
Profiles: profile.Profiles{
158+
{Name: "dev1", Host: "https://shared.cloud.databricks.com", AuthType: "databricks-cli"},
159+
{Name: "dev2", Host: "https://shared.cloud.databricks.com", AuthType: "databricks-cli"},
160+
},
161+
}
162+
163+
resolved, err := resolveHostToProfile(ctx, "https://shared.cloud.databricks.com", profiler)
164+
require.NoError(t, err)
165+
assert.Equal(t, "dev2", resolved)
166+
}
167+
130168
func TestResolveHostToProfileMatchesNothing(t *testing.T) {
131169
ctx := cmdio.MockDiscard(t.Context())
132170
profiler := profile.InMemoryProfiler{

0 commit comments

Comments
 (0)