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 @@ -6,6 +6,7 @@

* `databricks auth describe` now reports where U2M (`databricks-cli`) tokens are stored: `plaintext` (`~/.databricks/token-cache.json`) or `secure` (OS keyring), and the source of the choice (env var, config setting, or default).
* Marked the default profile in the interactive pickers shown by `databricks auth switch`, `databricks auth logout`, `databricks auth token`, and `databricks auth login`, and moved it to the top of the list. `databricks auth login` and `databricks auth logout` now offer the same selectors as `databricks auth token` and `databricks auth switch` respectively.
* `[__settings__].default_profile` is now consulted as a fallback by `databricks api`, `databricks auth token`, and bundle commands when neither `--profile` nor `DATABRICKS_CONFIG_PROFILE` is set. `databricks auth token` continues to give precedence to `DATABRICKS_HOST` over `default_profile`. For bundle commands, `default_profile` only applies when the bundle does not pin its own `workspace.host`.

### Bundles

Expand Down
5 changes: 5 additions & 0 deletions acceptance/auth/bundle_default_profile/databricks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
bundle:
name: test-default-profile

# No workspace.host on purpose: this is the surface where
# [__settings__].default_profile should be applied.
3 changes: 3 additions & 0 deletions acceptance/auth/bundle_default_profile/out.test.toml

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

45 changes: 45 additions & 0 deletions acceptance/auth/bundle_default_profile/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@

=== Bundle without workspace.host: default_profile is honored

>>> [CLI] bundle validate -o json
{
"host": null,
"profile": "default-target"
}

=== --profile overrides default_profile (negative case)

>>> errcode [CLI] bundle validate -p other -o json
Warn: [hostmetadata] failed to fetch host metadata for https://other.test, will skip for 1m0s
Error: Get "https://other.test/api/2.0/preview/scim/v2/Me": (redacted)


Exit code: 1
{
"host": null,
"profile": "other"
}

=== Bundle with workspace.host: default_profile is NOT applied

>>> errcode [CLI] bundle validate -o json
Error: failed during request visitor: default auth: cannot configure default credentials, please check https://docs.databricks.com/en/dev-tools/auth.html#databricks-client-unified-authentication to configure credentials for your preferred authentication method. Config: host=[DATABRICKS_URL], workspace_id=[NUMID], databricks_cli_path=[CLI]. Env: DATABRICKS_CLI_PATH


Exit code: 1
{
"host": "[DATABRICKS_URL]",
"profile": null
}

=== Bundle with workspace.profile: pinned profile wins over default_profile

>>> errcode [CLI] bundle validate -o json
Error: failed during request visitor: default auth: cannot configure default credentials, please check https://docs.databricks.com/en/dev-tools/auth.html#databricks-client-unified-authentication to configure credentials for your preferred authentication method. Config: profile=other, databricks_cli_path=[CLI]. Env: DATABRICKS_CLI_PATH


Exit code: 1
{
"host": null,
"profile": "other"
}
58 changes: 58 additions & 0 deletions acceptance/auth/bundle_default_profile/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
sethome "./home"

# Save the test server host so we can pin a bundle-with-host variant below.
host_value="$DATABRICKS_HOST"

cat > "./home/.databrickscfg" <<EOF
[default-target]
host = $DATABRICKS_HOST
token = $DATABRICKS_TOKEN

[other]
host = https://other.test
token = other-token

[__settings__]
default_profile = default-target
EOF

unset DATABRICKS_HOST
unset DATABRICKS_TOKEN
unset DATABRICKS_CONFIG_PROFILE

title "Bundle without workspace.host: default_profile is honored\n"
trace $CLI bundle validate -o json | jq '{host: .workspace.host, profile: .workspace.profile}'

title "--profile overrides default_profile (negative case)\n"
trace errcode $CLI bundle validate -p other -o json | jq '{host: .workspace.host, profile: .workspace.profile}'

# Switch to a bundle that pins workspace.host. The default_profile guard in
# configureProfile must NOT apply default_profile here, because that would
# silently route the user to a profile pointing at a different host than the
# bundle declares.
mkdir -p ./bundle-with-host
cat > ./bundle-with-host/databricks.yml <<EOF
bundle:
name: bundle-with-host

workspace:
host: $host_value
EOF

title "Bundle with workspace.host: default_profile is NOT applied\n"
(cd ./bundle-with-host && trace errcode $CLI bundle validate -o json | jq '{host: .workspace.host, profile: .workspace.profile}')

# Switch to a bundle that pins workspace.profile but no host. The pinned
# profile must win over default_profile — configureProfile's guard skips
# default_profile when workspace.profile is already set.
mkdir -p ./bundle-with-profile
cat > ./bundle-with-profile/databricks.yml <<EOF
bundle:
name: bundle-with-profile

workspace:
profile: other
EOF

title "Bundle with workspace.profile: pinned profile wins over default_profile\n"
(cd ./bundle-with-profile && trace errcode $CLI bundle validate -o json | jq '{host: .workspace.host, profile: .workspace.profile}')
12 changes: 12 additions & 0 deletions acceptance/auth/bundle_default_profile/test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Ignore = [
"home",
".databricks",
"bundle-with-host",
"bundle-with-profile",
]

# Negative case: -p other tries to reach a non-existing host. Redact the
# OS-/network-dependent suffix so the test is stable across runners.
[[Repls]]
Old = 'Get "https://other.test/api/2.0/preview/scim/v2/Me": .*'
New = 'Get "https://other.test/api/2.0/preview/scim/v2/Me": (redacted)'
3 changes: 3 additions & 0 deletions acceptance/cmd/api/default-profile/out.test.toml

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

28 changes: 28 additions & 0 deletions acceptance/cmd/api/default-profile/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@

=== default_profile is used when no --profile flag and no DATABRICKS_CONFIG_PROFILE

>>> [CLI] api get /api/2.0/clusters/list
{}

>>> print_requests.py --get //api/2.0/clusters/list
{
"headers": {
"Authorization": [
"Bearer [DATABRICKS_TOKEN]"
],
"User-Agent": [
"cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat"
],
"X-Databricks-Org-Id": [
"[NUMID]"
]
},
"method": "GET",
"path": "/api/2.0/clusters/list"
}

=== --profile overrides default_profile (negative case)
Warn: [hostmetadata] failed to fetch host metadata for https://other.test, will skip for 1m0s
Error: Get "https://other.test/api/2.0/clusters/list": (redacted)

Exit code: 1
28 changes: 28 additions & 0 deletions acceptance/cmd/api/default-profile/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
sethome "./home"

# Two profiles plus an explicit default_profile pointing at the test server.
# The 'other' profile points at an RFC 2606 reserved host so we can assert
# that --profile overrides default_profile without making a real request.
cat > "./home/.databrickscfg" <<EOF
[default-target]
host = $DATABRICKS_HOST
token = $DATABRICKS_TOKEN

[other]
host = https://other.test
token = other-token

[__settings__]
default_profile = default-target
EOF

unset DATABRICKS_HOST
unset DATABRICKS_TOKEN
unset DATABRICKS_CONFIG_PROFILE

title "default_profile is used when no --profile flag and no DATABRICKS_CONFIG_PROFILE\n"
MSYS_NO_PATHCONV=1 trace $CLI api get /api/2.0/clusters/list
trace print_requests.py --get //api/2.0/clusters/list | contains.py "X-Databricks-Org-Id"

title "--profile overrides default_profile (negative case)\n"
MSYS_NO_PATHCONV=1 errcode $CLI api get /api/2.0/clusters/list -p other 2>&1 | contains.py "other.test"
10 changes: 10 additions & 0 deletions acceptance/cmd/api/default-profile/test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Ignore = [
"home",
]

# Redact the OS- and network-dependent suffix on the failed lookup so the
# negative case (--profile overrides default_profile) is stable across
# runners. We still assert the requested host appears in output.
[[Repls]]
Old = 'Get "https://other.test/api/2.0/clusters/list": .*'
New = 'Get "https://other.test/api/2.0/clusters/list": (redacted)'
3 changes: 3 additions & 0 deletions acceptance/cmd/auth/token/default-profile/out.test.toml

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

15 changes: 15 additions & 0 deletions acceptance/cmd/auth/token/default-profile/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@

=== default_profile is honored when no args, --profile, or env var

>>> errcode [CLI] auth token
Warn: [hostmetadata] failed to fetch host metadata for https://myworkspace.test, will skip for 1m0s
Error: cache: databricks OAuth is not configured for this host. Try logging in again with `databricks auth login --profile myprofile` before retrying. If this fails, please report this issue to the Databricks CLI maintainers at https://github.com/databricks/cli/issues/new

Exit code: 1

=== default_profile pointing at a missing profile falls through to picker

>>> errcode [CLI] auth token
Error: no profile specified. Use --profile <name> to specify which profile to use

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

unset DATABRICKS_HOST
unset DATABRICKS_TOKEN
unset DATABRICKS_CONFIG_PROFILE

# default_profile points at "myprofile". Without it, `auth token` would fall
# through to the non-interactive error "no profile specified".
cat > "./home/.databrickscfg" <<'ENDCFG'
[myprofile]
host = https://myworkspace.test
auth_type = databricks-cli

[other]
host = https://other.test
auth_type = databricks-cli

[__settings__]
default_profile = myprofile
ENDCFG

title "default_profile is honored when no args, --profile, or env var\n"
trace errcode $CLI auth token

title "default_profile pointing at a missing profile falls through to picker\n"
cat > "./home/.databrickscfg" <<'ENDCFG'
[myprofile]
host = https://myworkspace.test
auth_type = databricks-cli

[__settings__]
default_profile = does-not-exist
ENDCFG
trace errcode $CLI auth token
17 changes: 14 additions & 3 deletions cmd/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
"github.com/databricks/cli/cmd/root"
"github.com/databricks/cli/libs/auth"
"github.com/databricks/cli/libs/cmdio"
"github.com/databricks/cli/libs/databrickscfg"
"github.com/databricks/cli/libs/env"
"github.com/databricks/cli/libs/flags"
"github.com/databricks/databricks-sdk-go/client"
"github.com/databricks/databricks-sdk-go/config"
Expand Down Expand Up @@ -77,11 +79,20 @@ func makeCommand(method string) *cobra.Command {

cfg := &config.Config{}

// command-line flag can specify the profile in use
profileFlag := cmd.Flag("profile")
if profileFlag != nil {
// Resolve the profile mirroring MustWorkspaceClient precedence:
// 1. --profile flag, 2. DATABRICKS_CONFIG_PROFILE env var (the SDK
// also reads it, but setting cfg.Profile here keeps any error
// messages we render referring to the same name), 3.
// [__settings__].default_profile in the config file.
if profileFlag := cmd.Flag("profile"); profileFlag != nil {
cfg.Profile = profileFlag.Value.String()
}
if cfg.Profile == "" {
cfg.Profile = env.Get(cmd.Context(), "DATABRICKS_CONFIG_PROFILE")
}
if cfg.Profile == "" {
cfg.Profile = databrickscfg.ResolveDefaultProfile(cmd.Context())
}

api, err := client.New(cfg)
if err != nil {
Expand Down
13 changes: 13 additions & 0 deletions cmd/auth/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/databricks/cli/libs/databrickscfg/profile"
"github.com/databricks/cli/libs/env"
"github.com/databricks/cli/libs/flags"
"github.com/databricks/cli/libs/log"
"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 Down Expand Up @@ -327,6 +328,18 @@ func resolveNoArgsToken(ctx context.Context, profiler profile.Profiler, authArgs
return envProfile, p, nil
}

// Step 2.5: Try [__settings__].default_profile from the config file.
// default_profile is advisory: if it points at a profile that no longer
// exists, fall through to the interactive picker rather than erroring.
if defaultProfile := databrickscfg.ResolveDefaultProfile(ctx); defaultProfile != "" {
p, err := loadProfileByName(ctx, defaultProfile, profiler)
if err != nil {
log.Warnf(ctx, "default_profile %q not loadable: %v", defaultProfile, err)
} else if p != nil {
return defaultProfile, 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)
Expand Down
11 changes: 2 additions & 9 deletions cmd/root/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import (
"github.com/databricks/cli/libs/databrickscfg"
"github.com/databricks/cli/libs/databrickscfg/profile"
envlib "github.com/databricks/cli/libs/env"
"github.com/databricks/cli/libs/log"
"github.com/databricks/cli/libs/logdiag"
"github.com/databricks/databricks-sdk-go"
"github.com/databricks/databricks-sdk-go/config"
Expand Down Expand Up @@ -299,14 +298,8 @@ func resolveDefaultProfile(ctx context.Context, cfg *config.Config) {
if cfg.Profile != "" || envlib.Get(ctx, "DATABRICKS_CONFIG_PROFILE") != "" {
return
}
configFilePath := envlib.Get(ctx, "DATABRICKS_CONFIG_FILE")
resolvedProfile, err := databrickscfg.GetConfiguredDefaultProfile(ctx, configFilePath)
if err != nil {
log.Warnf(ctx, "Failed to load default profile: %v", err)
return
}
if resolvedProfile != "" {
cfg.Profile = resolvedProfile
if resolved := databrickscfg.ResolveDefaultProfile(ctx); resolved != "" {
cfg.Profile = resolved
}
}

Expand Down
10 changes: 10 additions & 0 deletions cmd/root/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,16 @@ func getProfile(cmd *cobra.Command) (value string) {
// configureProfile applies the profile flag to the bundle.
func configureProfile(cmd *cobra.Command, b *bundle.Bundle) {
profile := getProfile(cmd)

// Fall back to [__settings__].default_profile only when the bundle does
// not pin its own host. If the bundle declares workspace.host, applying
// default_profile here could route the user to a profile that points at
// a different host than the bundle expects — let the SDK resolve auth
// from the host instead.
if profile == "" && b.Config.Workspace.Host == "" && b.Config.Workspace.Profile == "" {
profile = databrickscfg.ResolveDefaultProfile(cmd.Context())
}

if profile == "" {
return
}
Expand Down
Loading
Loading