Skip to content

Commit b8242ca

Browse files
committed
[WIP] support unified hosts
1 parent 3772d53 commit b8242ca

File tree

13 files changed

+154
-42
lines changed

13 files changed

+154
-42
lines changed

cmd/auth/auth.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ GCP: https://docs.gcp.databricks.com/dev-tools/auth/index.html`,
2525
var authArguments auth.AuthArguments
2626
cmd.PersistentFlags().StringVar(&authArguments.Host, "host", "", "Databricks Host")
2727
cmd.PersistentFlags().StringVar(&authArguments.AccountID, "account-id", "", "Databricks Account ID")
28+
cmd.PersistentFlags().BoolVarP(&authArguments.IsUnifiedHost, "experimental-is-unified-host", "u", false, "Whether the host is a unified host")
2829

2930
cmd.AddCommand(newEnvCommand())
3031
cmd.AddCommand(newLoginCommand(&authArguments))
@@ -55,3 +56,19 @@ func promptForAccountID(ctx context.Context) (string, error) {
5556
prompt.AllowEdit = true
5657
return prompt.Run()
5758
}
59+
60+
func promptForWorkspaceId(ctx context.Context) (string, error) {
61+
if !cmdio.IsInTTY(ctx) {
62+
return "", nil
63+
}
64+
65+
prompt := cmdio.Prompt(ctx)
66+
prompt.Label = "Databricks workspace ID (optional - provide only if using this profile for workspace operations, leave empty for account operations)"
67+
prompt.Default = ""
68+
prompt.AllowEdit = true
69+
result, err := prompt.Run()
70+
if err != nil {
71+
return "", err
72+
}
73+
return result, nil
74+
}

cmd/auth/login.go

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ depends on the existing profiles you have set in your configuration file
132132
if err != nil {
133133
return err
134134
}
135-
err = setHostAndAccountId(ctx, existingProfile, authArguments, args)
135+
err = setHostAndAccountId(ctx, cmd, existingProfile, authArguments, args)
136136
if err != nil {
137137
return err
138138
}
@@ -170,6 +170,12 @@ depends on the existing profiles you have set in your configuration file
170170
return err
171171
}
172172

173+
// Prompt for workspace_id if the host is unified
174+
workspaceId, err := promptForWorkspaceIdIfUnified(ctx, authArguments, existingProfile)
175+
if err != nil {
176+
return err
177+
}
178+
173179
switch {
174180
case configureCluster:
175181
w, err := databricks.NewWorkspaceClient((*databricks.Config)(&cfg))
@@ -201,13 +207,15 @@ depends on the existing profiles you have set in your configuration file
201207

202208
if profileName != "" {
203209
err = databrickscfg.SaveToProfile(ctx, &config.Config{
204-
Profile: profileName,
205-
Host: cfg.Host,
206-
AuthType: cfg.AuthType,
207-
AccountID: cfg.AccountID,
208-
ClusterID: cfg.ClusterID,
209-
ConfigFile: cfg.ConfigFile,
210-
ServerlessComputeID: cfg.ServerlessComputeID,
210+
Profile: profileName,
211+
Host: cfg.Host,
212+
AuthType: cfg.AuthType,
213+
AccountID: cfg.AccountID,
214+
WorkspaceId: workspaceId,
215+
ClusterID: cfg.ClusterID,
216+
ConfigFile: cfg.ConfigFile,
217+
ServerlessComputeID: cfg.ServerlessComputeID,
218+
Experimental_IsUnifiedHost: authArguments.IsUnifiedHost,
211219
})
212220
if err != nil {
213221
return err
@@ -233,7 +241,7 @@ depends on the existing profiles you have set in your configuration file
233241
// 1. --account-id flag.
234242
// 2. account-id from the specified profile, if available.
235243
// 3. Prompt the user for the account-id.
236-
func setHostAndAccountId(ctx context.Context, existingProfile *profile.Profile, authArguments *auth.AuthArguments, args []string) error {
244+
func setHostAndAccountId(ctx context.Context, cmd *cobra.Command, existingProfile *profile.Profile, authArguments *auth.AuthArguments, args []string) error {
237245
// If both [HOST] and --host are provided, return an error.
238246
host := authArguments.Host
239247
if len(args) > 0 && host != "" {
@@ -259,11 +267,21 @@ func setHostAndAccountId(ctx context.Context, existingProfile *profile.Profile,
259267
}
260268
}
261269

270+
// Determine if the host is a unified host in the following order of precedence:
271+
// 1. --experimental-is-unified-host flag (if explicitly set)
272+
// 2. experimental_is_unified_host from the specified profile, if available
273+
// 3. default to false if neither is provided.
274+
if !cmd.Flag("experimental-is-unified-host").Changed {
275+
if existingProfile != nil {
276+
authArguments.IsUnifiedHost = existingProfile.Experimental_IsUnifiedHost
277+
}
278+
}
279+
262280
// If the account-id was not provided as a cmd line flag, try to read it from
263281
// the specified profile.
264-
isAccountClient := (&config.Config{Host: authArguments.Host}).IsAccountClient()
282+
isAccountHost := (&config.Config{Host: authArguments.Host, Experimental_IsUnifiedHost: authArguments.IsUnifiedHost}).GetHostType() != config.WorkspaceHost
265283
accountID := authArguments.AccountID
266-
if isAccountClient && accountID == "" {
284+
if isAccountHost && accountID == "" {
267285
if existingProfile != nil && existingProfile.AccountID != "" {
268286
authArguments.AccountID = existingProfile.AccountID
269287
} else {
@@ -279,6 +297,28 @@ func setHostAndAccountId(ctx context.Context, existingProfile *profile.Profile,
279297
return nil
280298
}
281299

300+
// promptForWorkspaceIdIfUnified prompts for workspace ID if the host is unified.
301+
// Returns the workspace ID from the profile if available, otherwise prompts the user.
302+
func promptForWorkspaceIdIfUnified(ctx context.Context, authArguments *auth.AuthArguments, existingProfile *profile.Profile) (string, error) {
303+
// Only prompt for workspace_id if the host is a unified host
304+
if !authArguments.IsUnifiedHost {
305+
return "", nil
306+
}
307+
308+
// If the existing profile has a workspace_id, use it
309+
if existingProfile != nil && existingProfile.WorkspaceId != "" {
310+
return existingProfile.WorkspaceId, nil
311+
}
312+
313+
// Prompt the user for workspace_id
314+
workspaceId, err := promptForWorkspaceId(ctx)
315+
if err != nil {
316+
return "", err
317+
}
318+
319+
return workspaceId, nil
320+
}
321+
282322
// getProfileName returns the default profile name for a given host/account ID.
283323
// If the account ID is provided, the profile name is "ACCOUNT-<account-id>".
284324
// Otherwise, the profile name is the first part of the host URL.

cmd/auth/login_test.go

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ func TestSetHostDoesNotFailWithNoDatabrickscfg(t *testing.T) {
2626
existingProfile, err := loadProfileByName(ctx, "foo", profile.DefaultProfiler)
2727
assert.NoError(t, err)
2828

29-
err = setHostAndAccountId(ctx, existingProfile, &auth.AuthArguments{Host: "test"}, []string{})
29+
cmd := newLoginCommand(&auth.AuthArguments{})
30+
err = setHostAndAccountId(ctx, cmd, existingProfile, &auth.AuthArguments{Host: "test"}, []string{})
3031
assert.NoError(t, err)
3132
}
3233

@@ -38,38 +39,40 @@ func TestSetHost(t *testing.T) {
3839
profile1 := loadTestProfile(t, ctx, "profile-1")
3940
profile2 := loadTestProfile(t, ctx, "profile-2")
4041

42+
cmd := newLoginCommand(&auth.AuthArguments{})
43+
4144
// Test error when both flag and argument are provided
4245
authArguments.Host = "val from --host"
43-
err := setHostAndAccountId(ctx, profile1, &authArguments, []string{"val from [HOST]"})
46+
err := setHostAndAccountId(ctx, cmd, profile1, &authArguments, []string{"val from [HOST]"})
4447
assert.EqualError(t, err, "please only provide a host as an argument or a flag, not both")
4548

4649
// Test setting host from flag
4750
authArguments.Host = "val from --host"
48-
err = setHostAndAccountId(ctx, profile1, &authArguments, []string{})
51+
err = setHostAndAccountId(ctx, cmd, profile1, &authArguments, []string{})
4952
assert.NoError(t, err)
5053
assert.Equal(t, "val from --host", authArguments.Host)
5154

5255
// Test setting host from argument
5356
authArguments.Host = ""
54-
err = setHostAndAccountId(ctx, profile1, &authArguments, []string{"val from [HOST]"})
57+
err = setHostAndAccountId(ctx, cmd, profile1, &authArguments, []string{"val from [HOST]"})
5558
assert.NoError(t, err)
5659
assert.Equal(t, "val from [HOST]", authArguments.Host)
5760

5861
// Test setting host from profile
5962
authArguments.Host = ""
60-
err = setHostAndAccountId(ctx, profile1, &authArguments, []string{})
63+
err = setHostAndAccountId(ctx, cmd, profile1, &authArguments, []string{})
6164
assert.NoError(t, err)
6265
assert.Equal(t, "https://www.host1.com", authArguments.Host)
6366

6467
// Test setting host from profile
6568
authArguments.Host = ""
66-
err = setHostAndAccountId(ctx, profile2, &authArguments, []string{})
69+
err = setHostAndAccountId(ctx, cmd, profile2, &authArguments, []string{})
6770
assert.NoError(t, err)
6871
assert.Equal(t, "https://www.host2.com", authArguments.Host)
6972

7073
// Test host is not set. Should prompt.
7174
authArguments.Host = ""
72-
err = setHostAndAccountId(ctx, nil, &authArguments, []string{})
75+
err = setHostAndAccountId(ctx, cmd, nil, &authArguments, []string{})
7376
assert.EqualError(t, err, "the command is being run in a non-interactive environment, please specify a host using --host")
7477
}
7578

@@ -80,24 +83,26 @@ func TestSetAccountId(t *testing.T) {
8083

8184
accountProfile := loadTestProfile(t, ctx, "account-profile")
8285

86+
cmd := newLoginCommand(&auth.AuthArguments{})
87+
8388
// Test setting account-id from flag
8489
authArguments.AccountID = "val from --account-id"
85-
err := setHostAndAccountId(ctx, accountProfile, &authArguments, []string{})
90+
err := setHostAndAccountId(ctx, cmd, accountProfile, &authArguments, []string{})
8691
assert.NoError(t, err)
8792
assert.Equal(t, "https://accounts.cloud.databricks.com", authArguments.Host)
8893
assert.Equal(t, "val from --account-id", authArguments.AccountID)
8994

9095
// Test setting account_id from profile
9196
authArguments.AccountID = ""
92-
err = setHostAndAccountId(ctx, accountProfile, &authArguments, []string{})
97+
err = setHostAndAccountId(ctx, cmd, accountProfile, &authArguments, []string{})
9398
require.NoError(t, err)
9499
assert.Equal(t, "https://accounts.cloud.databricks.com", authArguments.Host)
95100
assert.Equal(t, "id-from-profile", authArguments.AccountID)
96101

97102
// Neither flag nor profile account-id is set, should prompt
98103
authArguments.AccountID = ""
99104
authArguments.Host = "https://accounts.cloud.databricks.com"
100-
err = setHostAndAccountId(ctx, nil, &authArguments, []string{})
105+
err = setHostAndAccountId(ctx, cmd, nil, &authArguments, []string{})
101106
assert.EqualError(t, err, "the command is being run in a non-interactive environment, please specify an account ID using --account-id")
102107
}
103108

cmd/auth/profiles.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ func (c *profileMetadata) Load(ctx context.Context, configFilePath string, skipV
5151
return
5252
}
5353

54-
if cfg.IsAccountClient() {
54+
if cfg.GetHostType() != config.WorkspaceHost {
5555
a, err := databricks.NewAccountClient((*databricks.Config)(cfg))
5656
if err != nil {
5757
return

cmd/auth/token.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ using a client ID and secret is not supported.`,
4343
}
4444

4545
t, err := loadToken(ctx, loadTokenArgs{
46+
cmd: cmd,
4647
authArguments: authArguments,
4748
profileName: profileName,
4849
args: args,
@@ -65,6 +66,9 @@ using a client ID and secret is not supported.`,
6566
}
6667

6768
type loadTokenArgs struct {
69+
// cmd is the cobra command, used to access flags like experimental-is-unified-host.
70+
cmd *cobra.Command
71+
6872
// authArguments is the parsed auth arguments, including the host and optionally the account ID.
6973
authArguments *auth.AuthArguments
7074

@@ -98,7 +102,7 @@ func loadToken(ctx context.Context, args loadTokenArgs) (*oauth2.Token, error) {
98102
return nil, err
99103
}
100104

101-
err = setHostAndAccountId(ctx, existingProfile, args.authArguments, args.args)
105+
err = setHostAndAccountId(ctx, args.cmd, existingProfile, args.authArguments, args.args)
102106
if err != nil {
103107
return nil, err
104108
}

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,5 @@ require (
7575
google.golang.org/grpc v1.75.1 // indirect
7676
google.golang.org/protobuf v1.36.9 // indirect
7777
)
78+
79+
replace github.com/databricks/databricks-sdk-go => ../databricks-sdk-go

go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@ github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZ
2929
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
3030
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
3131
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
32-
github.com/databricks/databricks-sdk-go v0.86.0 h1:Di1+NBQlfzBMUhY6w6gS2mtmNXIWycowoCsLCGFQPyU=
33-
github.com/databricks/databricks-sdk-go v0.86.0/go.mod h1:hWoHnHbNLjPKiTm5K/7bcIv3J3Pkgo5x9pPzh8K3RVE=
3432
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3533
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
3634
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

libs/auth/arguments.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,23 @@ import (
88
// AuthArguments is a struct that contains the common arguments passed to
99
// `databricks auth` commands.
1010
type AuthArguments struct {
11-
Host string
12-
AccountID string
11+
Host string
12+
AccountID string
13+
IsUnifiedHost bool
1314
}
1415

1516
// ToOAuthArgument converts the AuthArguments to an OAuthArgument from the Go SDK.
1617
func (a AuthArguments) ToOAuthArgument() (u2m.OAuthArgument, error) {
1718
cfg := &config.Config{
18-
Host: a.Host,
19-
AccountID: a.AccountID,
19+
Host: a.Host,
20+
AccountID: a.AccountID,
21+
Experimental_IsUnifiedHost: a.IsUnifiedHost,
2022
}
2123
host := cfg.CanonicalHostName()
22-
if cfg.IsAccountClient() {
24+
if cfg.GetHostType() == config.AccountHost {
2325
return u2m.NewBasicAccountOAuthArgument(host, cfg.AccountID)
26+
} else if cfg.GetHostType() == config.UnifiedHost {
27+
return u2m.NewBasicUnifiedOAuthArgument(host, cfg.AccountID)
2428
}
2529
return u2m.NewBasicWorkspaceOAuthArgument(host)
2630
}

libs/auth/error.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313
func RewriteAuthError(ctx context.Context, host, accountId, profile string, err error) (bool, error) {
1414
target := &u2m.InvalidRefreshTokenError{}
1515
if errors.As(err, &target) {
16-
oauthArgument, err := AuthArguments{host, accountId}.ToOAuthArgument()
16+
oauthArgument, err := AuthArguments{host, accountId, false}.ToOAuthArgument() // TODO: pass the IsUnifiedHost flag
1717
if err != nil {
1818
return false, err
1919
}

libs/databrickscfg/profile/file.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,13 @@ func (f FileProfilerImpl) LoadProfiles(ctx context.Context, fn ProfileMatchFunct
7979
continue
8080
}
8181
profile := Profile{
82-
Name: v.Name(),
83-
Host: host,
84-
AccountID: all["account_id"],
85-
ClusterID: all["cluster_id"],
86-
ServerlessComputeID: all["serverless_compute_id"],
82+
Name: v.Name(),
83+
Host: host,
84+
AccountID: all["account_id"],
85+
WorkspaceId: all["workspace_id"],
86+
ClusterID: all["cluster_id"],
87+
ServerlessComputeID: all["serverless_compute_id"],
88+
Experimental_IsUnifiedHost: all["experimental_is_unified_host"] == "true",
8789
}
8890
if fn(profile) {
8991
profiles = append(profiles, profile)

0 commit comments

Comments
 (0)