Skip to content

Commit 50b9f08

Browse files
authored
[Feature] Add support for Unified Host with experimental flag (#4260)
## Changes <!-- Brief summary of your changes that is easy to understand --> - Add support for unified host with experimental flag. - Prompt for workspace id as it's not part of the host - Write the IDs and flag in the created profile. - Depends on databricks/databricks-sdk-go#1307 ## Why <!-- Why are these changes needed? Provide the context that the reviewer might be missing. --> This support is required for enabling unified hosts which Databricks free edition uses. ## Tests <!-- How have you tested the changes? --> - Unit tests - Manual E2E tests -- CUJ for CLI U2M `databricks auth login --host <spog-host> --experimental-is-unified-host` prompts for account and workspace id followed by opening web browser. The IDs and flag is stores in the config. The subsequent cli operations eg: databricks clusters list --profile "above-profile" works.
1 parent 3579182 commit 50b9f08

23 files changed

Lines changed: 327 additions & 47 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"headers": {
3+
"Authorization": [
4+
"Bearer dapi-unified-token"
5+
],
6+
"User-Agent": [
7+
"cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/current-user_me cmd-exec-id/[UUID] auth/pat"
8+
],
9+
"X-Databricks-Org-Id": [
10+
"[NUMID]"
11+
]
12+
},
13+
"method": "GET",
14+
"path": "/api/2.0/preview/scim/v2/Me"
15+
}

acceptance/auth/credentials/unified-host/out.test.toml

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
2+
=== With workspace_id
3+
{
4+
"id":"[USERID]",
5+
"userName":"[USERNAME]"
6+
}
7+
8+
=== Without workspace_id (should error)
9+
Error: WorkspaceId must be set when using WorkspaceClient with unified host
10+
11+
Exit code: 1
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Test unified host authentication with PAT token
2+
export DATABRICKS_TOKEN=dapi-unified-token
3+
export DATABRICKS_ACCOUNT_ID=test-account-123
4+
export DATABRICKS_WORKSPACE_ID=1234567890
5+
export DATABRICKS_EXPERIMENTAL_IS_UNIFIED_HOST=true
6+
7+
title "With workspace_id\n"
8+
$CLI current-user me
9+
10+
title "Without workspace_id (should error)\n"
11+
unset DATABRICKS_WORKSPACE_ID
12+
errcode $CLI current-user me
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Test unified host authentication with PAT tokens
2+
# Include X-Databricks-Org-Id header to verify workspace_id is sent
3+
IncludeRequestHeaders = ["Authorization", "User-Agent", "X-Databricks-Org-Id"]

bundle/config/workspace.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ type Workspace struct {
4141
AzureEnvironment string `json:"azure_environment,omitempty"`
4242
AzureLoginAppID string `json:"azure_login_app_id,omitempty"`
4343

44+
// Unified host specific attributes.
45+
ExperimentalIsUnifiedHost bool `json:"experimental_is_unified_host,omitempty"`
46+
WorkspaceId string `json:"workspace_id,omitempty"`
47+
4448
// CurrentUser holds the current user.
4549
// This is set after configuration initialization.
4650
CurrentUser *User `json:"current_user,omitempty" bundle:"readonly"`
@@ -117,6 +121,10 @@ func (w *Workspace) Config() *config.Config {
117121
AzureTenantID: w.AzureTenantID,
118122
AzureEnvironment: w.AzureEnvironment,
119123
AzureLoginAppID: w.AzureLoginAppID,
124+
125+
// Unified host
126+
Experimental_IsUnifiedHost: w.ExperimentalIsUnifiedHost,
127+
WorkspaceId: w.WorkspaceId,
120128
}
121129

122130
for k := range config.ConfigAttributes {

bundle/internal/schema/annotations.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,9 @@ github.com/databricks/cli/bundle/config.Workspace:
424424
"client_id":
425425
"description": |-
426426
The client ID for the workspace
427+
"experimental_is_unified_host":
428+
"description": |-
429+
Experimental feature flag to indicate if the host is a unified host
427430
"file_path":
428431
"description": |-
429432
The file path to use within the workspace for both deployments and workflow runs
@@ -445,6 +448,9 @@ github.com/databricks/cli/bundle/config.Workspace:
445448
"state_path":
446449
"description": |-
447450
The workspace state path
451+
"workspace_id":
452+
"description": |-
453+
The Databricks workspace ID
448454
github.com/databricks/cli/bundle/config/resources.Alert:
449455
"create_time":
450456
"description": |-

bundle/schema/jsonschema.json

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/auth/auth.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ 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().BoolVar(&authArguments.IsUnifiedHost, "experimental-is-unified-host", false, "Flag to indicate if the host is a unified host")
29+
cmd.PersistentFlags().StringVar(&authArguments.WorkspaceId, "workspace-id", "", "Databricks Workspace ID")
2830

2931
cmd.AddCommand(newEnvCommand())
3032
cmd.AddCommand(newLoginCommand(&authArguments))
@@ -55,3 +57,16 @@ func promptForAccountID(ctx context.Context) (string, error) {
5557
prompt.AllowEdit = true
5658
return prompt.Run()
5759
}
60+
61+
func promptForWorkspaceID(ctx context.Context) (string, error) {
62+
if !cmdio.IsPromptSupported(ctx) {
63+
// Workspace ID is optional for unified hosts, so return empty string in non-interactive mode
64+
return "", nil
65+
}
66+
67+
prompt := cmdio.Prompt(ctx)
68+
prompt.Label = "Databricks workspace ID (optional - provide only if using this profile for workspace operations, leave empty for account operations)"
69+
prompt.Default = ""
70+
prompt.AllowEdit = true
71+
return prompt.Run()
72+
}

cmd/auth/login.go

Lines changed: 79 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,15 @@ depends on the existing profiles you have set in your configuration file
133133
if err != nil {
134134
return err
135135
}
136+
137+
// Load unified host flags from the profile if not explicitly set via CLI flag
138+
if !cmd.Flag("experimental-is-unified-host").Changed && existingProfile != nil {
139+
authArguments.IsUnifiedHost = existingProfile.IsUnifiedHost
140+
}
141+
if !cmd.Flag("workspace-id").Changed && existingProfile != nil {
142+
authArguments.WorkspaceId = existingProfile.WorkspaceId
143+
}
144+
136145
err = setHostAndAccountId(ctx, existingProfile, authArguments, args)
137146
if err != nil {
138147
return err
@@ -155,9 +164,11 @@ depends on the existing profiles you have set in your configuration file
155164
// We need the config without the profile before it's used to initialise new workspace client below.
156165
// Otherwise it will complain about non existing profile because it was not yet saved.
157166
cfg := config.Config{
158-
Host: authArguments.Host,
159-
AccountID: authArguments.AccountID,
160-
AuthType: "databricks-cli",
167+
Host: authArguments.Host,
168+
AccountID: authArguments.AccountID,
169+
WorkspaceId: authArguments.WorkspaceId,
170+
Experimental_IsUnifiedHost: authArguments.IsUnifiedHost,
171+
AuthType: "databricks-cli",
161172
}
162173
databricksCfgFile := os.Getenv("DATABRICKS_CONFIG_FILE")
163174
if databricksCfgFile != "" {
@@ -202,13 +213,15 @@ depends on the existing profiles you have set in your configuration file
202213

203214
if profileName != "" {
204215
err = databrickscfg.SaveToProfile(ctx, &config.Config{
205-
Profile: profileName,
206-
Host: cfg.Host,
207-
AuthType: cfg.AuthType,
208-
AccountID: cfg.AccountID,
209-
ClusterID: cfg.ClusterID,
210-
ConfigFile: cfg.ConfigFile,
211-
ServerlessComputeID: cfg.ServerlessComputeID,
216+
Profile: profileName,
217+
Host: cfg.Host,
218+
AuthType: cfg.AuthType,
219+
AccountID: cfg.AccountID,
220+
WorkspaceId: authArguments.WorkspaceId,
221+
Experimental_IsUnifiedHost: authArguments.IsUnifiedHost,
222+
ClusterID: cfg.ClusterID,
223+
ConfigFile: cfg.ConfigFile,
224+
ServerlessComputeID: cfg.ServerlessComputeID,
212225
})
213226
if err != nil {
214227
return err
@@ -260,24 +273,65 @@ func setHostAndAccountId(ctx context.Context, existingProfile *profile.Profile,
260273
}
261274
}
262275

263-
// If the account-id was not provided as a cmd line flag, try to read it from
264-
// the specified profile.
265-
//nolint:staticcheck // SA1019: IsAccountClient is deprecated but is still used here to avoid breaking changes
266-
isAccountClient := (&config.Config{Host: authArguments.Host}).IsAccountClient()
267-
accountID := authArguments.AccountID
268-
if isAccountClient && accountID == "" {
269-
if existingProfile != nil && existingProfile.AccountID != "" {
270-
authArguments.AccountID = existingProfile.AccountID
271-
} else {
272-
// Prompt user for the account-id if it we could not get it from a
273-
// profile.
274-
accountId, err := promptForAccountID(ctx)
275-
if err != nil {
276-
return err
276+
// Determine the host type and handle account ID / workspace ID accordingly
277+
cfg := &config.Config{
278+
Host: authArguments.Host,
279+
AccountID: authArguments.AccountID,
280+
WorkspaceId: authArguments.WorkspaceId,
281+
Experimental_IsUnifiedHost: authArguments.IsUnifiedHost,
282+
}
283+
284+
switch cfg.HostType() {
285+
case config.AccountHost:
286+
// Account host - prompt for account ID if not provided
287+
if authArguments.AccountID == "" {
288+
if existingProfile != nil && existingProfile.AccountID != "" {
289+
authArguments.AccountID = existingProfile.AccountID
290+
} else {
291+
accountId, err := promptForAccountID(ctx)
292+
if err != nil {
293+
return err
294+
}
295+
authArguments.AccountID = accountId
296+
}
297+
}
298+
case config.UnifiedHost:
299+
// Unified host requires an account ID for OAuth URL construction
300+
if authArguments.AccountID == "" {
301+
if existingProfile != nil && existingProfile.AccountID != "" {
302+
authArguments.AccountID = existingProfile.AccountID
303+
} else {
304+
accountId, err := promptForAccountID(ctx)
305+
if err != nil {
306+
return err
307+
}
308+
authArguments.AccountID = accountId
309+
}
310+
}
311+
312+
// Workspace ID is optional and determines API access level:
313+
// - With workspace ID: workspace-level APIs
314+
// - Without workspace ID: account-level APIs
315+
// If neither is provided via flags, prompt for workspace ID (most common case)
316+
hasWorkspaceID := authArguments.WorkspaceId != ""
317+
if !hasWorkspaceID {
318+
if existingProfile != nil && existingProfile.WorkspaceId != "" {
319+
authArguments.WorkspaceId = existingProfile.WorkspaceId
320+
} else {
321+
// Prompt for workspace ID for workspace-level access
322+
workspaceId, err := promptForWorkspaceID(ctx)
323+
if err != nil {
324+
return err
325+
}
326+
authArguments.WorkspaceId = workspaceId
277327
}
278-
authArguments.AccountID = accountId
279328
}
329+
case config.WorkspaceHost:
330+
// Workspace host - no additional prompts needed
331+
default:
332+
return fmt.Errorf("unknown host type: %v", cfg.HostType())
280333
}
334+
281335
return nil
282336
}
283337

0 commit comments

Comments
 (0)