55 "errors"
66 "fmt"
77 "io"
8- "net/http"
98 "runtime"
109 "strings"
1110 "time"
@@ -66,13 +65,27 @@ type discoveryPersistentAuth interface {
6665 Close () error
6766}
6867
69- var newDiscoveryOAuthArgument = u2m .NewBasicDiscoveryOAuthArgument
68+ // discoveryClient abstracts the external dependencies of discoveryLogin so
69+ // they can be replaced in tests without package-level variable mutation.
70+ type discoveryClient interface {
71+ NewOAuthArgument (profileName string ) (* u2m.BasicDiscoveryOAuthArgument , error )
72+ NewPersistentAuth (ctx context.Context , opts ... u2m.PersistentAuthOption ) (discoveryPersistentAuth , error )
73+ IntrospectToken (ctx context.Context , host , accessToken string ) (* auth.IntrospectionResult , error )
74+ }
75+
76+ type defaultDiscoveryClient struct {}
77+
78+ func (d * defaultDiscoveryClient ) NewOAuthArgument (profileName string ) (* u2m.BasicDiscoveryOAuthArgument , error ) {
79+ return u2m .NewBasicDiscoveryOAuthArgument (profileName )
80+ }
7081
71- var newDiscoveryPersistentAuth = func (ctx context.Context , opts ... u2m.PersistentAuthOption ) (discoveryPersistentAuth , error ) {
82+ func ( d * defaultDiscoveryClient ) NewPersistentAuth (ctx context.Context , opts ... u2m.PersistentAuthOption ) (discoveryPersistentAuth , error ) {
7283 return u2m .NewPersistentAuth (ctx , opts ... )
7384}
7485
75- var introspectToken = auth .IntrospectToken
86+ func (d * defaultDiscoveryClient ) IntrospectToken (ctx context.Context , host , accessToken string ) (* auth.IntrospectionResult , error ) {
87+ return auth .IntrospectToken (ctx , host , accessToken , nil )
88+ }
7689
7790func newLoginCommand (authArguments * auth.AuthArguments ) * cobra.Command {
7891 defaultConfigPath := "~/.databrickscfg"
@@ -170,16 +183,10 @@ depends on the existing profiles you have set in your configuration file
170183 // If no host is available from any source, use the discovery flow
171184 // via login.databricks.com.
172185 if shouldUseDiscovery (authArguments .Host , args , existingProfile ) {
173- if configureCluster {
174- return errors .New ("--configure-cluster requires --host to be specified" )
175- }
176- if configureServerless {
177- return errors .New ("--configure-serverless requires --host to be specified" )
178- }
179186 if err := validateDiscoveryFlagCompatibility (cmd ); err != nil {
180187 return err
181188 }
182- return discoveryLogin (ctx , profileName , loginTimeout , scopes , existingProfile , getBrowserFunc (cmd ))
189+ return discoveryLogin (ctx , & defaultDiscoveryClient {}, profileName , loginTimeout , scopes , existingProfile , getBrowserFunc (cmd ))
183190 }
184191
185192 // Load unified host flags from the profile if not explicitly set via CLI flag
@@ -461,6 +468,8 @@ var discoveryIncompatibleFlags = []string{
461468 "account-id" ,
462469 "workspace-id" ,
463470 "experimental-is-unified-host" ,
471+ "configure-cluster" ,
472+ "configure-serverless" ,
464473}
465474
466475// validateDiscoveryFlagCompatibility returns an error if any flags that require
@@ -494,8 +503,8 @@ func openURLSuppressingStderr(url string) error {
494503// discoveryLogin runs the login.databricks.com discovery flow. The user
495504// authenticates in the browser, selects a workspace, and the CLI receives
496505// the workspace host from the OAuth callback's iss parameter.
497- func discoveryLogin (ctx context.Context , profileName string , timeout time.Duration , scopes string , existingProfile * profile.Profile , browserFunc func (string ) error ) error {
498- arg , err := newDiscoveryOAuthArgument (profileName )
506+ func discoveryLogin (ctx context.Context , dc discoveryClient , profileName string , timeout time.Duration , scopes string , existingProfile * profile.Profile , browserFunc func (string ) error ) error {
507+ arg , err := dc . NewOAuthArgument (profileName )
499508 if err != nil {
500509 return discoveryErr ("setting up login.databricks.com" , err )
501510 }
@@ -518,7 +527,7 @@ func discoveryLogin(ctx context.Context, profileName string, timeout time.Durati
518527 ctx , cancel := context .WithTimeout (ctx , timeout )
519528 defer cancel ()
520529
521- persistentAuth , err := newDiscoveryPersistentAuth (ctx , opts ... )
530+ persistentAuth , err := dc . NewPersistentAuth (ctx , opts ... )
522531 if err != nil {
523532 return discoveryErr ("setting up login.databricks.com" , err )
524533 }
@@ -541,9 +550,8 @@ func discoveryLogin(ctx context.Context, profileName string, timeout time.Durati
541550 }
542551
543552 // Best-effort introspection for metadata.
544- httpClient := httpClientForIntrospection ()
545553 var workspaceID string
546- introspection , err := introspectToken (ctx , discoveredHost , tok .AccessToken , httpClient )
554+ introspection , err := dc . IntrospectToken (ctx , discoveredHost , tok .AccessToken )
547555 if err != nil {
548556 log .Debugf (ctx , "token introspection failed (non-fatal): %v" , err )
549557 } else {
@@ -562,10 +570,11 @@ func discoveryLogin(ctx context.Context, profileName string, timeout time.Durati
562570
563571 configFile := env .Get (ctx , "DATABRICKS_CONFIG_FILE" )
564572 clearKeys := oauthLoginClearKeys ()
565- // Clear stale routing fields that may exist from a previous login to a
566- // different host type. Discovery always produces a workspace-level profile.
567- // workspace_id is cleared and re-written only if introspection returned a
568- // fresh value, preventing stale IDs from surviving failed introspection.
573+ // Discovery login always produces a workspace-level profile pointing at the
574+ // discovered host. Any previous routing metadata (account_id, workspace_id,
575+ // is_unified_host, cluster_id, serverless_compute_id) from a prior login to
576+ // a different host type must be cleared so they don't leak into the new
577+ // profile. workspace_id is re-added only when introspection succeeds.
569578 clearKeys = append (clearKeys ,
570579 "account_id" ,
571580 "workspace_id" ,
@@ -594,9 +603,6 @@ func discoveryLogin(ctx context.Context, profileName string, timeout time.Durati
594603
595604// splitScopes splits a comma-separated scopes string into a trimmed slice.
596605func splitScopes (scopes string ) []string {
597- if scopes == "" {
598- return nil
599- }
600606 var result []string
601607 for _ , s := range strings .Split (scopes , "," ) {
602608 scope := strings .TrimSpace (s )
@@ -652,11 +658,3 @@ func getBrowserFunc(cmd *cobra.Command) func(url string) error {
652658 }
653659 }
654660}
655-
656- // httpClientForIntrospection returns an *http.Client suitable for the
657- // best-effort token introspection call. It clones http.DefaultTransport
658- // to inherit system CA certs, proxy settings, and timeouts.
659- func httpClientForIntrospection () * http.Client {
660- transport := http .DefaultTransport .(* http.Transport ).Clone ()
661- return & http.Client {Transport : transport }
662- }
0 commit comments