Skip to content
Open
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
1 change: 1 addition & 0 deletions NEXT_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ To disable this, set the environment variable DATABRICKS_CACHE_ENABLED to false.

### CLI
* Add commands to pipelines command group ([#4275](https://github.com/databricks/cli/pull/4275))
* Add support for unified host with experimental flag ([#4260](https://github.com/databricks/cli/pull/4260))

### Bundles
* Add support for configuring app.yaml options for apps via bundle config ([#4271](https://github.com/databricks/cli/pull/4271))
Expand Down
8 changes: 8 additions & 0 deletions bundle/config/workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ type Workspace struct {
AzureEnvironment string `json:"azure_environment,omitempty"`
AzureLoginAppID string `json:"azure_login_app_id,omitempty"`

// Unified host specific attributes.
ExperimentalIsUnifiedHost bool `json:"experimental_is_unified_host,omitempty"`
WorkspaceID string `json:"workspace_id,omitempty"`

// CurrentUser holds the current user.
// This is set after configuration initialization.
CurrentUser *User `json:"current_user,omitempty" bundle:"readonly"`
Expand Down Expand Up @@ -117,6 +121,10 @@ func (w *Workspace) Config() *config.Config {
AzureTenantID: w.AzureTenantID,
AzureEnvironment: w.AzureEnvironment,
AzureLoginAppID: w.AzureLoginAppID,

// Unified host
Experimental_IsUnifiedHost: w.ExperimentalIsUnifiedHost,
WorkspaceId: w.WorkspaceID,
}

for k := range config.ConfigAttributes {
Expand Down
6 changes: 6 additions & 0 deletions bundle/internal/schema/annotations.yml
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,9 @@ github.com/databricks/cli/bundle/config.Workspace:
"client_id":
"description": |-
The client ID for the workspace
"experimental_is_unified_host":
"description": |-
Flag to indicate if the host is a unified host
"file_path":
"description": |-
The file path to use within the workspace for both deployments and workflow runs
Expand All @@ -445,6 +448,9 @@ github.com/databricks/cli/bundle/config.Workspace:
"state_path":
"description": |-
The workspace state path
"workspace_id":
"description": |-
The Databricks workspace ID
github.com/databricks/cli/bundle/config/resources.Alert:
"create_time":
"description": |-
Expand Down
8 changes: 8 additions & 0 deletions bundle/schema/jsonschema.json

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

14 changes: 14 additions & 0 deletions cmd/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ GCP: https://docs.gcp.databricks.com/dev-tools/auth/index.html`,
var authArguments auth.AuthArguments
cmd.PersistentFlags().StringVar(&authArguments.Host, "host", "", "Databricks Host")
cmd.PersistentFlags().StringVar(&authArguments.AccountID, "account-id", "", "Databricks Account ID")
cmd.PersistentFlags().BoolVar(&authArguments.IsUnifiedHost, "experimental-is-unified-host", false, "Flag to indicate if the host is a unified host")
cmd.PersistentFlags().StringVar(&authArguments.WorkspaceID, "workspace-id", "", "Databricks Workspace ID")

cmd.AddCommand(newEnvCommand())
cmd.AddCommand(newLoginCommand(&authArguments))
Expand Down Expand Up @@ -55,3 +57,15 @@ func promptForAccountID(ctx context.Context) (string, error) {
prompt.AllowEdit = true
return prompt.Run()
}

func promptForWorkspaceID(ctx context.Context) (string, error) {
if !cmdio.IsPromptSupported(ctx) {
return "", errors.New("the command is being run in a non-interactive environment, please specify a workspace ID using --workspace-id")
}

prompt := cmdio.Prompt(ctx)
prompt.Label = "Databricks workspace ID"
prompt.Default = ""
prompt.AllowEdit = true
return prompt.Run()
}
96 changes: 74 additions & 22 deletions cmd/auth/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,15 @@ depends on the existing profiles you have set in your configuration file
if err != nil {
return err
}

// Load unified host flags from the profile if not explicitly set via CLI flag
if !cmd.Flag("experimental-is-unified-host").Changed && existingProfile != nil {
authArguments.IsUnifiedHost = existingProfile.IsUnifiedHost
}
if !cmd.Flag("workspace-id").Changed && existingProfile != nil {
authArguments.WorkspaceID = existingProfile.WorkspaceID
}

err = setHostAndAccountId(ctx, existingProfile, authArguments, args)
if err != nil {
return err
Expand Down Expand Up @@ -202,13 +211,15 @@ depends on the existing profiles you have set in your configuration file

if profileName != "" {
err = databrickscfg.SaveToProfile(ctx, &config.Config{
Profile: profileName,
Host: cfg.Host,
AuthType: cfg.AuthType,
AccountID: cfg.AccountID,
ClusterID: cfg.ClusterID,
ConfigFile: cfg.ConfigFile,
ServerlessComputeID: cfg.ServerlessComputeID,
Profile: profileName,
Host: cfg.Host,
AuthType: cfg.AuthType,
AccountID: cfg.AccountID,
WorkspaceId: authArguments.WorkspaceID,
Experimental_IsUnifiedHost: authArguments.IsUnifiedHost,
ClusterID: cfg.ClusterID,
ConfigFile: cfg.ConfigFile,
ServerlessComputeID: cfg.ServerlessComputeID,
})
if err != nil {
return err
Expand Down Expand Up @@ -260,24 +271,65 @@ func setHostAndAccountId(ctx context.Context, existingProfile *profile.Profile,
}
}

// If the account-id was not provided as a cmd line flag, try to read it from
// the specified profile.
//nolint:staticcheck // SA1019: IsAccountClient is deprecated but is still used here to avoid breaking changes
isAccountClient := (&config.Config{Host: authArguments.Host}).IsAccountClient()
accountID := authArguments.AccountID
if isAccountClient && accountID == "" {
if existingProfile != nil && existingProfile.AccountID != "" {
authArguments.AccountID = existingProfile.AccountID
} else {
// Prompt user for the account-id if it we could not get it from a
// profile.
accountId, err := promptForAccountID(ctx)
if err != nil {
return err
// Determine the host type and handle account ID / workspace ID accordingly
cfg := &config.Config{
Host: authArguments.Host,
AccountID: authArguments.AccountID,
WorkspaceId: authArguments.WorkspaceID,
Experimental_IsUnifiedHost: authArguments.IsUnifiedHost,
}

switch cfg.HostType() {
case config.AccountHost:
// Account host - prompt for account ID if not provided
if authArguments.AccountID == "" {
if existingProfile != nil && existingProfile.AccountID != "" {
authArguments.AccountID = existingProfile.AccountID
} else {
accountId, err := promptForAccountID(ctx)
if err != nil {
return err
}
authArguments.AccountID = accountId
}
}
case config.UnifiedHost:
// Unified host requires an account ID for OAuth URL construction
if authArguments.AccountID == "" {
if existingProfile != nil && existingProfile.AccountID != "" {
authArguments.AccountID = existingProfile.AccountID
} else {
accountId, err := promptForAccountID(ctx)
if err != nil {
return err
}
authArguments.AccountID = accountId
}
}

// Workspace ID is optional and determines API access level:
// - With workspace ID: workspace-level APIs
// - Without workspace ID: account-level APIs
// If neither is provided via flags, prompt for workspace ID (most common case)
hasWorkspaceID := authArguments.WorkspaceID != ""
if !hasWorkspaceID {
if existingProfile != nil && existingProfile.WorkspaceID != "" {
authArguments.WorkspaceID = existingProfile.WorkspaceID
} else {
// Prompt for workspace ID for workspace-level access
workspaceId, err := promptForWorkspaceID(ctx)
if err != nil {
return err
}
authArguments.WorkspaceID = workspaceId
}
authArguments.AccountID = accountId
}
case config.WorkspaceHost:
// Workspace host - no additional prompts needed
default:
return fmt.Errorf("unknown host type: %v", cfg.HostType())
}

return nil
}

Expand Down
9 changes: 6 additions & 3 deletions cmd/auth/profiles.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ func (c *profileMetadata) Load(ctx context.Context, configFilePath string, skipV
return
}

//nolint:staticcheck // SA1019: IsAccountClient is deprecated but is still used here to avoid breaking changes
if cfg.IsAccountClient() {
switch cfg.ConfigType() {
case config.AccountConfig:
a, err := databricks.NewAccountClient((*databricks.Config)(cfg))
if err != nil {
return
Expand All @@ -64,7 +64,7 @@ func (c *profileMetadata) Load(ctx context.Context, configFilePath string, skipV
return
}
c.Valid = true
} else {
case config.WorkspaceConfig:
w, err := databricks.NewWorkspaceClient((*databricks.Config)(cfg))
if err != nil {
return
Expand All @@ -76,6 +76,9 @@ func (c *profileMetadata) Load(ctx context.Context, configFilePath string, skipV
return
}
c.Valid = true
case config.InvalidConfig:
// Invalid configuration, skip validation
return
}
}

Expand Down
11 changes: 11 additions & 0 deletions cmd/auth/testdata/.databrickscfg
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,14 @@ cluster_id = cluster-from-config
[invalid-profile]
# This profile is missing the required 'host' field
cluster_id = some-cluster-id

[unified-workspace]
host = https://unified.databricks.com
account_id = test-unified-account
workspace_id = 123456789
experimental_is_unified_host = true

[unified-account]
host = https://unified.databricks.com
account_id = test-unified-account
experimental_is_unified_host = true
10 changes: 10 additions & 0 deletions cmd/auth/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,16 @@ 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
}
}

err = setHostAndAccountId(ctx, existingProfile, args.authArguments, args.args)
if err != nil {
return nil, err
Expand Down
3 changes: 1 addition & 2 deletions cmd/labs/project/entrypoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,8 +248,7 @@ func (e *Entrypoint) validLogin(cmd *cobra.Command) (*config.Config, error) {
// an account profile during installation (anymore) and just prompt for it, when context
// does require it. This also means that we always prompt for account-level commands, unless
// users specify a `--profile` flag.
//nolint:staticcheck // SA1019: IsAccountClient is deprecated but is still used here to avoid breaking changes
isACC := cfg.IsAccountClient()
isACC := cfg.ConfigType() == config.AccountConfig
if e.IsAccountLevel && cfg.Profile == "" {
if !cmdio.IsPromptSupported(ctx) {
return nil, config.ErrCannotConfigureDefault
Expand Down
4 changes: 2 additions & 2 deletions cmd/labs/project/installer.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/databricks/cli/libs/log"
"github.com/databricks/cli/libs/process"
"github.com/databricks/databricks-sdk-go"
"github.com/databricks/databricks-sdk-go/config"
"github.com/databricks/databricks-sdk-go/service/compute"
"github.com/databricks/databricks-sdk-go/service/sql"
"github.com/fatih/color"
Expand Down Expand Up @@ -177,8 +178,7 @@ func (i *installer) login(ctx context.Context) (*databricks.WorkspaceClient, err
} else if err != nil {
return nil, fmt.Errorf("valid: %w", err)
}
//nolint:staticcheck // SA1019: IsAccountClient is deprecated but is still used here to avoid breaking changes
if !i.HasAccountLevelCommands() && cfg.IsAccountClient() {
if !i.HasAccountLevelCommands() && cfg.ConfigType() == config.AccountConfig {
return nil, errors.New("got account-level client, but no account-level commands")
}
lc := &loginConfig{Entrypoint: i.Installer.Entrypoint}
Expand Down
3 changes: 1 addition & 2 deletions cmd/labs/project/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ type loginConfig struct {
}

func (lc *loginConfig) askWorkspace(ctx context.Context, cfg *config.Config) (*databricks.WorkspaceClient, error) {
//nolint:staticcheck // SA1019: IsAccountClient is deprecated but is still used here to avoid breaking changes
if cfg.IsAccountClient() {
if cfg.ConfigType() == config.AccountConfig {
return nil, nil
}
err := lc.askWorkspaceProfile(ctx, cfg)
Expand Down
28 changes: 21 additions & 7 deletions libs/auth/arguments.go
Original file line number Diff line number Diff line change
@@ -1,27 +1,41 @@
package auth

import (
"fmt"

"github.com/databricks/databricks-sdk-go/config"
"github.com/databricks/databricks-sdk-go/credentials/u2m"
)

// AuthArguments is a struct that contains the common arguments passed to
// `databricks auth` commands.
type AuthArguments struct {
Host string
AccountID string
Host string
AccountID string
WorkspaceID string
IsUnifiedHost bool
}

// ToOAuthArgument converts the AuthArguments to an OAuthArgument from the Go SDK.
func (a AuthArguments) ToOAuthArgument() (u2m.OAuthArgument, error) {
cfg := &config.Config{
Host: a.Host,
AccountID: a.AccountID,
Host: a.Host,
AccountID: a.AccountID,
WorkspaceId: a.WorkspaceID,
Experimental_IsUnifiedHost: a.IsUnifiedHost,
}
host := cfg.CanonicalHostName()
//nolint:staticcheck // SA1019: IsAccountClient is deprecated but is still used here to avoid breaking changes
if cfg.IsAccountClient() {

switch cfg.HostType() {
case config.AccountHost:
return u2m.NewBasicAccountOAuthArgument(host, cfg.AccountID)
case config.WorkspaceHost:
return u2m.NewBasicWorkspaceOAuthArgument(host)
case config.UnifiedHost:
// For unified hosts, always use the unified OAuth argument with account ID.
// The workspace ID is stored in the config for API routing, not OAuth.
return u2m.NewBasicUnifiedOAuthArgument(host, cfg.AccountID)
default:
return nil, fmt.Errorf("unknown host type: %v", cfg.HostType())
}
return u2m.NewBasicWorkspaceOAuthArgument(host)
}
Loading