diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index b406a874e8..981c6005b9 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -5,6 +5,7 @@ ### Notable Changes ### CLI +* Auth commands now resolve positional arguments as profile names first, with host fallback ([#4840](https://github.com/databricks/cli/pull/4840)) ### Bundles * engine/direct: Fix drift in grants resource due to privilege reordering ([#4794](https://github.com/databricks/cli/pull/4794)) diff --git a/cmd/auth/login.go b/cmd/auth/login.go index 29732066ce..08ca2d4c72 100644 --- a/cmd/auth/login.go +++ b/cmd/auth/login.go @@ -94,7 +94,7 @@ func newLoginCommand(authArguments *auth.AuthArguments) *cobra.Command { defaultConfigPath = "%USERPROFILE%\\.databrickscfg" } cmd := &cobra.Command{ - Use: "login [HOST]", + Use: "login [PROFILE_OR_HOST]", Short: "Log into a Databricks workspace or account", Long: fmt.Sprintf(`Log into a Databricks workspace or account. This command logs you into the Databricks workspace or account and saves @@ -110,7 +110,9 @@ you can refer to the documentation linked below. GCP: https://docs.gcp.databricks.com/dev-tools/auth/index.html -If no host is provided (via --host, as a positional argument, or from an existing +The positional argument is resolved as a profile name first. If no profile with +that name exists and the argument looks like a URL, it is used as a host. If no +host is provided (via --host, as a positional argument, or from an existing profile), the CLI will open login.databricks.com where you can authenticate and select a workspace. The workspace URL will be discovered automatically. @@ -165,6 +167,24 @@ depends on the existing profiles you have set in your configuration file return errors.New("please either configure serverless or cluster, not both") } + // Resolve positional argument as profile or host. + if len(args) > 0 && authArguments.Host != "" { + return errors.New("please only provide a positional argument or --host, not both") + } + if profileName == "" && len(args) == 1 { + resolvedProfile, resolvedHost, err := resolvePositionalArg(ctx, args[0], profile.DefaultProfiler) + if err != nil { + return err + } + if resolvedProfile != "" { + profileName = resolvedProfile + args = nil + } else { + authArguments.Host = resolvedHost + args = nil + } + } + // If the user has not specified a profile name, prompt for one. if profileName == "" { var err error diff --git a/cmd/auth/login_test.go b/cmd/auth/login_test.go index eee4a5c38f..876301b832 100644 --- a/cmd/auth/login_test.go +++ b/cmd/auth/login_test.go @@ -913,3 +913,14 @@ auth_type = databricks-cli assert.Equal(t, "https://new-workspace.example.com", savedProfile.Host) assert.Equal(t, "222222", savedProfile.WorkspaceID, "workspace_id should be updated to fresh introspection value") } + +func TestLoginRejectsHostFlagWithPositionalArg(t *testing.T) { + ctx := cmdio.MockDiscard(t.Context()) + authArgs := &auth.AuthArguments{Host: "https://example.com"} + cmd := newLoginCommand(authArgs) + cmd.Flags().String("profile", "", "") + cmd.SetContext(ctx) + cmd.SetArgs([]string{"myprofile"}) + err := cmd.Execute() + assert.ErrorContains(t, err, "please only provide a positional argument or --host, not both") +} diff --git a/cmd/auth/logout.go b/cmd/auth/logout.go index 864ac34033..a6fcb551d4 100644 --- a/cmd/auth/logout.go +++ b/cmd/auth/logout.go @@ -28,7 +28,7 @@ You will need to run {{ "databricks auth login" | bold }} to re-authenticate. func newLogoutCommand() *cobra.Command { cmd := &cobra.Command{ - Use: "logout [PROFILE]", + Use: "logout [PROFILE_OR_HOST]", Short: "Log out of a Databricks profile", Args: cobra.MaximumNArgs(1), Hidden: true, @@ -66,26 +66,37 @@ the profile is an error. } var force bool - var profileName string var deleteProfile bool cmd.Flags().BoolVar(&force, "force", false, "Skip confirmation prompt") - cmd.Flags().StringVar(&profileName, "profile", "", "The profile to log out of") cmd.Flags().BoolVar(&deleteProfile, "delete", false, "Delete the profile from the config file") cmd.RunE = func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() profiler := profile.DefaultProfiler + profileName := "" + profileFlag := cmd.Flag("profile") + if profileFlag != nil { + profileName = profileFlag.Value.String() + } + // Resolve the positional argument to a profile name. - if profileName != "" && len(args) == 1 { + if profileFlag != nil && profileFlag.Changed && len(args) == 1 { return errors.New("providing both --profile and a positional argument is not supported") } if profileName == "" && len(args) == 1 { - resolved, err := resolveLogoutArg(ctx, args[0], profiler) + resolvedProfile, resolvedHost, err := resolvePositionalArg(ctx, args[0], profiler) if err != nil { return err } - profileName = resolved + if resolvedProfile != "" { + profileName = resolvedProfile + } else { + profileName, err = resolveHostToProfile(ctx, resolvedHost, profiler) + if err != nil { + return err + } + } } if profileName == "" { @@ -289,55 +300,3 @@ func hostCacheKeyAndMatchFn(p profile.Profile) (string, profile.ProfileMatchFunc return host, profile.WithHost(host) } - -// resolveLogoutArg resolves a positional argument to a profile name. It first -// tries to match the argument as a profile name, then as a host URL. If the -// host matches multiple profiles in a non-interactive context, it returns an -// error listing the matching profile names. -func resolveLogoutArg(ctx context.Context, arg string, profiler profile.Profiler) (string, error) { - // Try as profile name first. - candidateProfile, err := loadProfileByName(ctx, arg, profiler) - if err != nil { - return "", err - } - if candidateProfile != nil { - return arg, nil - } - - // Try as host URL. - canonicalHost := (&config.Config{Host: arg}).CanonicalHostName() - hostProfiles, err := profiler.LoadProfiles(ctx, profile.WithHost(canonicalHost)) - if err != nil { - return "", err - } - - switch len(hostProfiles) { - case 1: - return hostProfiles[0].Name, nil - case 0: - allProfiles, err := profiler.LoadProfiles(ctx, profile.MatchAllProfiles) - if err != nil { - return "", fmt.Errorf("no profile found matching %q", arg) - } - names := strings.Join(allProfiles.Names(), ", ") - return "", fmt.Errorf("no profile found matching %q. Available profiles: %s", arg, names) - default: - // Multiple profiles match the host. - if cmdio.IsPromptSupported(ctx) { - selected, err := profile.SelectProfile(ctx, profile.SelectConfig{ - Label: fmt.Sprintf("Multiple profiles found for %q. Select one to log out of", arg), - Profiles: hostProfiles, - StartInSearchMode: len(hostProfiles) > 5, - ActiveTemplate: `▸ {{.PaddedName | bold}}{{if .AccountID}} (account: {{.AccountID}}){{else}} ({{.Host}}){{end}}`, - InactiveTemplate: ` {{.PaddedName}}{{if .AccountID}} (account: {{.AccountID | faint}}){{else}} ({{.Host | faint}}){{end}}`, - SelectedTemplate: `{{ "Selected profile" | faint }}: {{ .Name | bold }}`, - }) - if err != nil { - return "", err - } - return selected, nil - } - names := strings.Join(hostProfiles.Names(), ", ") - return "", fmt.Errorf("multiple profiles found matching host %q: %s. Please specify the profile name directly", arg, names) - } -} diff --git a/cmd/auth/logout_test.go b/cmd/auth/logout_test.go index 7468a0779b..2c0aa5abf9 100644 --- a/cmd/auth/logout_test.go +++ b/cmd/auth/logout_test.go @@ -8,6 +8,7 @@ import ( "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/databrickscfg" "github.com/databricks/cli/libs/databrickscfg/profile" + "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/oauth2" @@ -262,94 +263,13 @@ func TestLogoutNoTokensWithDelete(t *testing.T) { assert.Empty(t, profiles) } -func TestLogoutResolveArgMatchesProfileName(t *testing.T) { - ctx := cmdio.MockDiscard(t.Context()) - profiler := profile.InMemoryProfiler{ - Profiles: profile.Profiles{ - {Name: "dev", Host: "https://dev.cloud.databricks.com", AuthType: "databricks-cli"}, - {Name: "staging", Host: "https://staging.cloud.databricks.com", AuthType: "databricks-cli"}, - }, - } - - resolved, err := resolveLogoutArg(ctx, "dev", profiler) - require.NoError(t, err) - assert.Equal(t, "dev", resolved) -} - -func TestLogoutResolveArgMatchesHostWithOneProfile(t *testing.T) { - ctx := cmdio.MockDiscard(t.Context()) - profiler := profile.InMemoryProfiler{ - Profiles: profile.Profiles{ - {Name: "dev", Host: "https://dev.cloud.databricks.com", AuthType: "databricks-cli"}, - {Name: "staging", Host: "https://staging.cloud.databricks.com", AuthType: "databricks-cli"}, - }, - } - - resolved, err := resolveLogoutArg(ctx, "https://dev.cloud.databricks.com", profiler) - require.NoError(t, err) - assert.Equal(t, "dev", resolved) -} - -func TestLogoutResolveArgMatchesHostWithMultipleProfiles(t *testing.T) { - 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"}, - }, - } - - _, err := resolveLogoutArg(ctx, "https://shared.cloud.databricks.com", profiler) - assert.ErrorContains(t, err, "multiple profiles found matching host") - assert.ErrorContains(t, err, "dev1") - assert.ErrorContains(t, err, "dev2") -} - -func TestLogoutResolveArgMatchesNothing(t *testing.T) { - ctx := cmdio.MockDiscard(t.Context()) - profiler := profile.InMemoryProfiler{ - Profiles: profile.Profiles{ - {Name: "dev", Host: "https://dev.cloud.databricks.com", AuthType: "databricks-cli"}, - {Name: "staging", Host: "https://staging.cloud.databricks.com", AuthType: "databricks-cli"}, - }, - } - - _, err := resolveLogoutArg(ctx, "https://unknown.cloud.databricks.com", profiler) - assert.ErrorContains(t, err, `no profile found matching "https://unknown.cloud.databricks.com"`) - assert.ErrorContains(t, err, "dev") - assert.ErrorContains(t, err, "staging") -} - -func TestLogoutResolveArgCanonicalizesHost(t *testing.T) { - profiler := profile.InMemoryProfiler{ - Profiles: profile.Profiles{ - {Name: "dev", Host: "https://dev.cloud.databricks.com", AuthType: "databricks-cli"}, - }, - } - - cases := []struct { - name string - arg string - }{ - {name: "canonical URL", arg: "https://dev.cloud.databricks.com"}, - {name: "trailing slash", arg: "https://dev.cloud.databricks.com/"}, - {name: "no scheme", arg: "dev.cloud.databricks.com"}, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - ctx := cmdio.MockDiscard(t.Context()) - resolved, err := resolveLogoutArg(ctx, tc.arg, profiler) - require.NoError(t, err) - assert.Equal(t, "dev", resolved) - }) - } -} - func TestLogoutProfileFlagAndPositionalArgConflict(t *testing.T) { + parent := &cobra.Command{Use: "root"} + parent.PersistentFlags().StringP("profile", "p", "", "~/.databrickscfg profile") cmd := newLogoutCommand() - cmd.SetArgs([]string{"myprofile", "--profile", "other"}) - err := cmd.Execute() + parent.AddCommand(cmd) + parent.SetArgs([]string{"logout", "myprofile", "--profile", "other"}) + err := parent.Execute() assert.ErrorContains(t, err, "providing both --profile and a positional argument is not supported") } diff --git a/cmd/auth/resolve.go b/cmd/auth/resolve.go new file mode 100644 index 0000000000..7c0e477323 --- /dev/null +++ b/cmd/auth/resolve.go @@ -0,0 +1,89 @@ +package auth + +import ( + "context" + "fmt" + "strconv" + "strings" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/databrickscfg/profile" + "github.com/databricks/databricks-sdk-go/config" +) + +// looksLikeHost returns true if the argument looks like a host URL rather than +// a profile name. Profile names are short identifiers (e.g., "logfood", +// "DEFAULT"), while host URLs contain dots or start with "http". +func looksLikeHost(arg string) bool { + if strings.Contains(arg, ".") || strings.HasPrefix(arg, "http://") || strings.HasPrefix(arg, "https://") { + return true + } + // Match host:port pattern without dots or scheme (e.g., localhost:8080). + if i := strings.LastIndex(arg, ":"); i > 0 { + if _, err := strconv.Atoi(arg[i+1:]); err == nil { + return true + } + } + return false +} + +// resolvePositionalArg resolves a positional argument to either a profile name +// or a host. It tries the argument as a profile name first. If no profile +// matches and the argument looks like a host URL, it returns it as a host. If +// no profile matches and the argument does not look like a host, it returns an +// error. +func resolvePositionalArg(ctx context.Context, arg string, profiler profile.Profiler) (profileName, host string, err error) { + candidateProfile, err := loadProfileByName(ctx, arg, profiler) + if err != nil { + return "", "", err + } + if candidateProfile != nil { + return arg, "", nil + } + + if looksLikeHost(arg) { + return "", arg, nil + } + + return "", "", fmt.Errorf("no profile named %q found", arg) +} + +// resolveHostToProfile resolves a host URL to a profile name. If multiple +// profiles match the host, it prompts the user to select one (or errors in +// non-interactive mode). If no profiles match, it returns an error. +func resolveHostToProfile(ctx context.Context, host string, profiler profile.Profiler) (string, error) { + canonicalHost := (&config.Config{Host: host}).CanonicalHostName() + hostProfiles, err := profiler.LoadProfiles(ctx, profile.WithHost(canonicalHost)) + if err != nil { + return "", err + } + + switch len(hostProfiles) { + case 1: + return hostProfiles[0].Name, nil + case 0: + allProfiles, err := profiler.LoadProfiles(ctx, profile.MatchAllProfiles) + if err != nil { + return "", fmt.Errorf("no profile found matching host %q", host) + } + names := strings.Join(allProfiles.Names(), ", ") + return "", fmt.Errorf("no profile found matching host %q. Available profiles: %s", host, names) + default: + if cmdio.IsPromptSupported(ctx) { + selected, err := profile.SelectProfile(ctx, profile.SelectConfig{ + Label: fmt.Sprintf("Multiple profiles found for %q. Select one to use", host), + Profiles: hostProfiles, + StartInSearchMode: len(hostProfiles) > 5, + ActiveTemplate: "▸ {{.PaddedName | bold}}{{if .AccountID}} (account: {{.AccountID}}){{else}} ({{.Host}}){{end}}", + InactiveTemplate: " {{.PaddedName}}{{if .AccountID}} (account: {{.AccountID | faint}}){{else}} ({{.Host | faint}}){{end}}", + SelectedTemplate: `{{ "Selected profile" | faint }}: {{ .Name | bold }}`, + }) + if err != nil { + return "", err + } + return selected, nil + } + names := strings.Join(hostProfiles.Names(), ", ") + return "", fmt.Errorf("multiple profiles found matching host %q: %s. Please specify the profile name directly", host, names) + } +} diff --git a/cmd/auth/resolve_test.go b/cmd/auth/resolve_test.go new file mode 100644 index 0000000000..1180b22038 --- /dev/null +++ b/cmd/auth/resolve_test.go @@ -0,0 +1,169 @@ +package auth + +import ( + "testing" + + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/databrickscfg/profile" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestResolvePositionalArg(t *testing.T) { + cases := []struct { + name string + profiles profile.Profiles + arg string + wantProfile string + wantHost string + wantErr string + }{ + { + name: "matches profile", + profiles: profile.Profiles{ + {Name: "logfood", Host: "https://logfood.cloud.databricks.com"}, + }, + arg: "logfood", + wantProfile: "logfood", + wantHost: "", + }, + { + name: "falls back to https host", + profiles: profile.Profiles{ + {Name: "logfood", Host: "https://logfood.cloud.databricks.com"}, + }, + arg: "https://other.cloud.databricks.com", + wantProfile: "", + wantHost: "https://other.cloud.databricks.com", + }, + { + name: "falls back to host with dot", + profiles: profile.Profiles{}, + arg: "my-workspace.cloud.databricks.com", + wantProfile: "", + wantHost: "my-workspace.cloud.databricks.com", + }, + { + name: "errors for non-host non-profile", + profiles: profile.Profiles{ + {Name: "logfood", Host: "https://logfood.cloud.databricks.com"}, + }, + arg: "e2-logfood", + wantErr: `no profile named "e2-logfood" found`, + }, + { + name: "http prefix", + profiles: profile.Profiles{}, + arg: "http://localhost:8080", + wantProfile: "", + wantHost: "http://localhost:8080", + }, + { + name: "host:port without dots or scheme", + profiles: profile.Profiles{}, + arg: "localhost:8080", + wantProfile: "", + wantHost: "localhost:8080", + }, + { + name: "empty profiles error", + profiles: profile.Profiles{}, + arg: "myprofile", + wantErr: `no profile named "myprofile" found`, + }, + { + name: "profile with dot in name", + profiles: profile.Profiles{ + {Name: "default.dev", Host: "https://dev.cloud.databricks.com"}, + }, + arg: "default.dev", + wantProfile: "default.dev", + wantHost: "", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + ctx := cmdio.MockDiscard(t.Context()) + profiler := profile.InMemoryProfiler{Profiles: tc.profiles} + profileName, host, err := resolvePositionalArg(ctx, tc.arg, profiler) + if tc.wantErr != "" { + assert.ErrorContains(t, err, tc.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tc.wantProfile, profileName) + assert.Equal(t, tc.wantHost, host) + }) + } +} + +func TestResolveHostToProfileMatchesOneProfile(t *testing.T) { + ctx := cmdio.MockDiscard(t.Context()) + profiler := profile.InMemoryProfiler{ + Profiles: profile.Profiles{ + {Name: "dev", Host: "https://dev.cloud.databricks.com", AuthType: "databricks-cli"}, + {Name: "staging", Host: "https://staging.cloud.databricks.com", AuthType: "databricks-cli"}, + }, + } + + resolved, err := resolveHostToProfile(ctx, "https://dev.cloud.databricks.com", profiler) + require.NoError(t, err) + assert.Equal(t, "dev", resolved) +} + +func TestResolveHostToProfileMatchesMultipleProfiles(t *testing.T) { + 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"}, + }, + } + + _, err := resolveHostToProfile(ctx, "https://shared.cloud.databricks.com", profiler) + assert.ErrorContains(t, err, "multiple profiles found matching host") + assert.ErrorContains(t, err, "dev1") + assert.ErrorContains(t, err, "dev2") +} + +func TestResolveHostToProfileMatchesNothing(t *testing.T) { + ctx := cmdio.MockDiscard(t.Context()) + profiler := profile.InMemoryProfiler{ + Profiles: profile.Profiles{ + {Name: "dev", Host: "https://dev.cloud.databricks.com", AuthType: "databricks-cli"}, + {Name: "staging", Host: "https://staging.cloud.databricks.com", AuthType: "databricks-cli"}, + }, + } + + _, err := resolveHostToProfile(ctx, "https://unknown.cloud.databricks.com", profiler) + assert.ErrorContains(t, err, `no profile found matching host "https://unknown.cloud.databricks.com"`) + assert.ErrorContains(t, err, "dev") + assert.ErrorContains(t, err, "staging") +} + +func TestResolveHostToProfileCanonicalizesHost(t *testing.T) { + profiler := profile.InMemoryProfiler{ + Profiles: profile.Profiles{ + {Name: "dev", Host: "https://dev.cloud.databricks.com", AuthType: "databricks-cli"}, + }, + } + + cases := []struct { + name string + arg string + }{ + {name: "canonical URL", arg: "https://dev.cloud.databricks.com"}, + {name: "trailing slash", arg: "https://dev.cloud.databricks.com/"}, + {name: "no scheme", arg: "dev.cloud.databricks.com"}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + ctx := cmdio.MockDiscard(t.Context()) + resolved, err := resolveHostToProfile(ctx, tc.arg, profiler) + require.NoError(t, err) + assert.Equal(t, "dev", resolved) + }) + } +} diff --git a/cmd/auth/token.go b/cmd/auth/token.go index 83e9d9fc0a..95979359e8 100644 --- a/cmd/auth/token.go +++ b/cmd/auth/token.go @@ -51,7 +51,7 @@ func applyUnifiedHostFlags(p *profile.Profile, args *auth.AuthArguments) { func newTokenCommand(authArguments *auth.AuthArguments) *cobra.Command { cmd := &cobra.Command{ - Use: "token [HOST_OR_PROFILE]", + Use: "token [PROFILE_OR_HOST]", Short: "Get authentication token", Long: `Get authentication token from the local cache in ~/.databricks/token-cache.json. Refresh the access token if it is expired. Note: This command only works with @@ -122,26 +122,32 @@ func loadToken(ctx context.Context, args loadTokenArgs) (*oauth2.Token, error) { return nil, errors.New("providing both a profile and host is not supported") } - // When no explicit --profile flag is provided, check the env var. This - // handles the case where downstream tools (like the Terraform provider) - // pass --host but not --profile, while DATABRICKS_CONFIG_PROFILE is set. - if args.profileName == "" { - args.profileName = env.Get(ctx, "DATABRICKS_CONFIG_PROFILE") - } - - // If no --profile flag, try resolving the positional arg as a profile name. - // If it matches, use it. If not, fall through to host treatment. + // Resolve the positional arg as a profile name first, then as a host. + // Error if it matches neither. This runs before the DATABRICKS_CONFIG_PROFILE + // env var check so that an explicit positional argument always goes through + // profile-first resolution. if args.profileName == "" && len(args.args) == 1 { - candidateProfile, err := loadProfileByName(ctx, args.args[0], args.profiler) + resolvedProfile, resolvedHost, err := resolvePositionalArg(ctx, args.args[0], args.profiler) if err != nil { return nil, err } - if candidateProfile != nil { - args.profileName = args.args[0] + if resolvedProfile != "" { + args.profileName = resolvedProfile + args.args = nil + } else { + args.authArguments.Host = resolvedHost args.args = nil } } + // When no explicit --profile flag or positional arg is provided, check the + // env var. This handles the case where downstream tools (like the Terraform + // provider) pass --host but not --profile, while DATABRICKS_CONFIG_PROFILE + // is set. + if args.profileName == "" { + args.profileName = env.Get(ctx, "DATABRICKS_CONFIG_PROFILE") + } + existingProfile, err := loadProfileByName(ctx, args.profileName, args.profiler) if err != nil { return nil, err diff --git a/cmd/auth/token_test.go b/cmd/auth/token_test.go index aa343eb372..a85373ca8c 100644 --- a/cmd/auth/token_test.go +++ b/cmd/auth/token_test.go @@ -367,7 +367,7 @@ func TestToken_loadToken(t *testing.T) { args: loadTokenArgs{ authArguments: &auth.AuthArguments{}, profileName: "", - args: []string{"nonexistent"}, + args: []string{"nonexistent.cloud.databricks.com"}, tokenTimeout: 1 * time.Hour, profiler: profiler, persistentAuthOpts: []u2m.PersistentAuthOption{ @@ -376,9 +376,20 @@ func TestToken_loadToken(t *testing.T) { }, }, wantErr: "cache: databricks OAuth is not configured for this host. " + - "Try logging in again with `databricks auth login --host https://nonexistent` before retrying. " + + "Try logging in again with `databricks auth login --host https://nonexistent.cloud.databricks.com` before retrying. " + "If this fails, please report this issue to the Databricks CLI maintainers at https://github.com/databricks/cli/issues/new", }, + { + name: "errors with clear message for non-host non-profile positional arg", + args: loadTokenArgs{ + authArguments: &auth.AuthArguments{}, + profileName: "", + args: []string{"e2-logfood"}, + tokenTimeout: 1 * time.Hour, + profiler: profiler, + }, + wantErr: `no profile named "e2-logfood" found`, + }, { name: "scheme-less account host ambiguity detected correctly", args: loadTokenArgs{ @@ -678,6 +689,20 @@ func TestToken_loadToken(t *testing.T) { }, validateToken: validateToken, }, + { + name: "DATABRICKS_CONFIG_PROFILE with positional typo runs resolver first", + setupCtx: func(ctx context.Context) context.Context { + return env.Set(ctx, "DATABRICKS_CONFIG_PROFILE", "active") + }, + args: loadTokenArgs{ + authArguments: &auth.AuthArguments{}, + profileName: "", + args: []string{"e2-logfood"}, + tokenTimeout: 1 * time.Hour, + profiler: profiler, + }, + wantErr: `no profile named "e2-logfood" found`, + }, { name: "host flag with profile env var disambiguates multi-profile", setupCtx: func(ctx context.Context) context.Context {