Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions acceptance/cmd/auth/token/no-args-no-profiles/out.test.toml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions acceptance/cmd/auth/token/no-args-no-profiles/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Error: no profiles configured. Run 'databricks auth login' to create a profile

Exit code: 1
8 changes: 8 additions & 0 deletions acceptance/cmd/auth/token/no-args-no-profiles/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
sethome "./home"

unset DATABRICKS_HOST
unset DATABRICKS_TOKEN
unset DATABRICKS_CONFIG_PROFILE

# No config file, non-interactive: should error with login hint
errcode $CLI auth token
3 changes: 3 additions & 0 deletions acceptance/cmd/auth/token/no-args-no-profiles/test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Ignore = [
"home"
]

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions acceptance/cmd/auth/token/no-args-with-profiles/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Error: no profile specified. Use --profile <name> to specify which profile to use

Exit code: 1
15 changes: 15 additions & 0 deletions acceptance/cmd/auth/token/no-args-with-profiles/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
sethome "./home"

unset DATABRICKS_HOST
unset DATABRICKS_TOKEN
unset DATABRICKS_CONFIG_PROFILE

# Create a .databrickscfg with a profile
cat > "./home/.databrickscfg" <<'ENDCFG'
[myprofile]
host = https://myworkspace.cloud.databricks.com
auth_type = databricks-cli
ENDCFG

# No arguments, non-interactive: should error with profile hint
errcode $CLI auth token
3 changes: 3 additions & 0 deletions acceptance/cmd/auth/token/no-args-with-profiles/test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Ignore = [
"home"
]
228 changes: 221 additions & 7 deletions cmd/auth/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ import (
"encoding/json"
"errors"
"fmt"
"os"
"strings"
"time"

"github.com/databricks/cli/libs/auth"
"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/databricks-sdk-go/config"
"github.com/databricks/databricks-sdk-go/credentials/u2m"
"github.com/databricks/databricks-sdk-go/credentials/u2m/cache"
Expand All @@ -24,6 +27,30 @@ func helpfulError(ctx context.Context, profile string, persistentAuth u2m.OAuthA
return fmt.Sprintf("Try logging in again with `%s` before retrying. If this fails, please report this issue to the Databricks CLI maintainers at https://github.com/databricks/cli/issues/new", loginMsg)
}

// profileSelectionResult represents the user's choice from the interactive
// profile picker.
type profileSelectionResult int

const (
profileSelected profileSelectionResult = iota // User picked a profile
enterHostSelected // User chose "Enter a host URL manually"
createNewSelected // User chose "Create a new profile"
)

// applyUnifiedHostFlags copies unified host fields from the profile to the
// auth arguments when they are not already set.
func applyUnifiedHostFlags(p *profile.Profile, args *auth.AuthArguments) {
if p == nil {
return
}
if !args.IsUnifiedHost && p.IsUnifiedHost {
args.IsUnifiedHost = p.IsUnifiedHost
}
if args.WorkspaceID == "" && p.WorkspaceID != "" {
args.WorkspaceID = p.WorkspaceID
}
}

func newTokenCommand(authArguments *auth.AuthArguments) *cobra.Command {
cmd := &cobra.Command{
Use: "token [HOST_OR_PROFILE]",
Expand Down Expand Up @@ -115,14 +142,18 @@ func loadToken(ctx context.Context, args loadTokenArgs) (*oauth2.Token, error) {
return nil, err
}

// Load unified host flags from the profile if available
if existingProfile != nil {
if !args.authArguments.IsUnifiedHost && existingProfile.IsUnifiedHost {
args.authArguments.IsUnifiedHost = existingProfile.IsUnifiedHost
}
if args.authArguments.WorkspaceID == "" && existingProfile.WorkspaceID != "" {
args.authArguments.WorkspaceID = existingProfile.WorkspaceID
applyUnifiedHostFlags(existingProfile, args.authArguments)

// When no explicit profile, host, or positional args are provided, attempt to
// resolve the target through environment variables or interactive profile selection.
if args.profileName == "" && args.authArguments.Host == "" && len(args.args) == 0 {
var resolvedProfile string
resolvedProfile, existingProfile, err = resolveNoArgsToken(ctx, args.profiler, args.authArguments)
if err != nil {
return nil, err
}
args.profileName = resolvedProfile
applyUnifiedHostFlags(existingProfile, args.authArguments)
}

err = setHostAndAccountId(ctx, existingProfile, args.authArguments, args.args)
Expand Down Expand Up @@ -228,3 +259,186 @@ func askForMatchingProfile(ctx context.Context, profiles profile.Profiles, host
}
return profiles[i].Name, nil
}

// resolveNoArgsToken resolves a profile or host when `auth token` is invoked
// with no explicit profile, host, or positional arguments. It checks environment
// variables first, then falls back to interactive profile selection or a clear
// non-interactive error.
//
// Returns the resolved profile name and profile (if any). The host and related
// fields on authArgs are updated in place when resolved via environment variables.
func resolveNoArgsToken(ctx context.Context, profiler profile.Profiler, authArgs *auth.AuthArguments) (string, *profile.Profile, error) {
// Step 1: Try DATABRICKS_HOST env var (highest priority).
if envHost := env.Get(ctx, "DATABRICKS_HOST"); envHost != "" {
authArgs.Host = envHost
if v := env.Get(ctx, "DATABRICKS_ACCOUNT_ID"); v != "" {
authArgs.AccountID = v
}
if v := env.Get(ctx, "DATABRICKS_WORKSPACE_ID"); v != "" {
authArgs.WorkspaceID = v
}
if ok, _ := env.GetBool(ctx, "DATABRICKS_EXPERIMENTAL_IS_UNIFIED_HOST"); ok {
authArgs.IsUnifiedHost = true
}
return "", nil, nil
}

// Step 2: Try DATABRICKS_CONFIG_PROFILE env var.
if envProfile := env.Get(ctx, "DATABRICKS_CONFIG_PROFILE"); envProfile != "" {
p, err := loadProfileByName(ctx, envProfile, profiler)
if err != nil {
return "", nil, err
}
return envProfile, p, nil
}

// Step 3: No env vars resolved. Load all profiles for interactive selection
// or non-interactive error.
allProfiles, err := profiler.LoadProfiles(ctx, profile.MatchAllProfiles)
if err != nil && !errors.Is(err, profile.ErrNoConfiguration) {
return "", nil, err
}

if !cmdio.IsPromptSupported(ctx) {
if len(allProfiles) > 0 {
return "", nil, errors.New("no profile specified. Use --profile <name> to specify which profile to use")
}
return "", nil, errors.New("no profiles configured. Run 'databricks auth login' to create a profile")
}

// Interactive: show profile picker.
result, selectedName, err := promptForProfileSelection(ctx, allProfiles)
if err != nil {
return "", nil, err
}
switch result {
case enterHostSelected:
// Fall through — setHostAndAccountId will prompt for the host.
return "", nil, nil
case createNewSelected:
return runInlineLogin(ctx, profiler)
default:
p, err := loadProfileByName(ctx, selectedName, profiler)
if err != nil {
return "", nil, err
}
return selectedName, p, nil
}
}

// profileSelectItem is used by promptForProfileSelection to render both
// regular profiles and special action options in the same select list.
type profileSelectItem struct {
Name string
Host string
}

// promptForProfileSelection shows a promptui select list with all configured
// profiles plus "Enter a host URL" and "Create a new profile" options.
// Returns the selection type and, when a profile is selected, its name.
func promptForProfileSelection(ctx context.Context, profiles profile.Profiles) (profileSelectionResult, string, error) {
items := make([]profileSelectItem, 0, len(profiles)+2)
for _, p := range profiles {
items = append(items, profileSelectItem{Name: p.Name, Host: p.Host})
}
createProfileIdx := len(items)
items = append(items, profileSelectItem{Name: "Create a new profile"})
enterHostIdx := len(items)
items = append(items, profileSelectItem{Name: "Enter a host URL manually"})

i, _, err := cmdio.RunSelect(ctx, &promptui.Select{
Label: "Select a profile",
Items: items,
StartInSearchMode: len(profiles) > 5,
Searcher: func(input string, index int) bool {
input = strings.ToLower(input)
name := strings.ToLower(items[index].Name)
host := strings.ToLower(items[index].Host)
return strings.Contains(name, input) || strings.Contains(host, input)
},
Templates: &promptui.SelectTemplates{
Label: "{{ . | faint }}",
Active: `{{.Name | bold}}{{if .Host}} ({{.Host|faint}}){{end}}`,
Inactive: `{{.Name}}{{if .Host}} ({{.Host}}){{end}}`,
Selected: `{{ "Using profile" | faint }}: {{ .Name | bold }}`,
},
})
if err != nil {
return 0, "", err
}

switch i {
case enterHostIdx:
return enterHostSelected, "", nil
case createProfileIdx:
return createNewSelected, "", nil
default:
return profileSelected, profiles[i].Name, nil
}
}

// runInlineLogin runs a minimal interactive login flow: prompts for a profile
// name and host, performs the OAuth challenge, saves the profile to
// .databrickscfg, and returns the new profile name and profile.
func runInlineLogin(ctx context.Context, profiler profile.Profiler) (string, *profile.Profile, error) {
profileName, err := promptForProfile(ctx, "DEFAULT")
if err != nil {
return "", nil, err
}

existingProfile, err := loadProfileByName(ctx, profileName, profiler)
if err != nil {
return "", nil, err
}

loginArgs := &auth.AuthArguments{}
applyUnifiedHostFlags(existingProfile, loginArgs)

err = setHostAndAccountId(ctx, existingProfile, loginArgs, nil)
if err != nil {
return "", nil, err
}

loginArgs.Profile = profileName

oauthArgument, err := loginArgs.ToOAuthArgument()
if err != nil {
return "", nil, err
}
persistentAuth, err := u2m.NewPersistentAuth(ctx,
u2m.WithOAuthArgument(oauthArgument),
u2m.WithBrowser(openURLSuppressingStderr),
)
if err != nil {
return "", nil, err
}
defer persistentAuth.Close()

ctx, cancel := context.WithTimeout(ctx, defaultTimeout)
defer cancel()

if err = persistentAuth.Challenge(); err != nil {
return "", nil, err
}

err = databrickscfg.SaveToProfile(ctx, &config.Config{
Profile: profileName,
Host: loginArgs.Host,
AuthType: authTypeDatabricksCLI,
AccountID: loginArgs.AccountID,
WorkspaceID: loginArgs.WorkspaceID,
Experimental_IsUnifiedHost: loginArgs.IsUnifiedHost,
ConfigFile: os.Getenv("DATABRICKS_CONFIG_FILE"),
})
if err != nil {
return "", nil, err
}

cmdio.LogString(ctx, fmt.Sprintf("Profile %s was successfully saved", profileName))

p, err := loadProfileByName(ctx, profileName, profiler)
if err != nil {
return "", nil, err
}
return profileName, p, nil
}
Loading