-
Notifications
You must be signed in to change notification settings - Fork 168
Improve auth token error formatting for easier copy-paste #5115
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,13 +2,12 @@ | |
|
|
||
| ## Release v0.300.0 | ||
|
|
||
| ### Notable Changes | ||
|
|
||
| ### CLI | ||
|
|
||
| * Added `databricks aitools` command group for installing Databricks skills into your coding agents (Claude Code, Cursor, Codex CLI, OpenCode, GitHub Copilot, Antigravity). Skills are fetched from [github.com/databricks/databricks-agent-skills](https://github.com/databricks/databricks-agent-skills) and either symlinked into each agent's skills directory or copied into the current project. Use `databricks aitools install` to set up, `update` to pull newer versions, `list` to see what's available, and `uninstall` to remove them. | ||
| * `databricks api` now works against unified hosts. Adds `--account` to scope a call to the account API and `--workspace-id` to override the workspace routing identifier per call. A `?o=<workspace-id>` query parameter on the path (the SPOG URL convention used by the Databricks UI) is also recognized as a per-call workspace override, so URLs pasted from the browser route correctly. | ||
|
|
||
| * Improve `auth token` error formatting for easier copy-paste of login commands ([#4602](https://github.com/databricks/cli/pull/4602)) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: this links the superseded |
||
|
|
||
| ### Bundles | ||
| * Make sure warnings asking for approval are understood by agents ([#5239](https://github.com/databricks/cli/pull/5239)) | ||
|
|
||
| ### Dependency updates | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,7 @@ | ||
| Error: A new access token could not be retrieved because the refresh token is invalid. To reauthenticate, run the following command: | ||
| $ databricks auth login --profile test-profile | ||
| Error: A new access token could not be retrieved because the refresh token is invalid. | ||
|
|
||
| To reauthenticate, run the following command: | ||
|
|
||
| $ databricks auth login --profile test-profile --workspace-id [NUMID] | ||
|
|
||
| Exit code: 1 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,9 @@ | ||
| Error: cache: databricks OAuth is not configured for this host. Try logging in again with `databricks auth login --profile test-profile` before retrying. If this fails, please report this issue to the Databricks CLI maintainers at https://github.com/databricks/cli/issues/new | ||
| Error: cache: databricks OAuth is not configured for this host. | ||
|
|
||
| To reauthenticate, run the following command: | ||
|
|
||
| $ databricks auth login --profile test-profile --workspace-id [NUMID] | ||
|
|
||
| If this fails, please report this issue to the Databricks CLI maintainers at https://github.com/databricks/cli/issues/new | ||
|
|
||
| Exit code: 1 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,11 @@ | ||
|
|
||
| >>> [CLI] auth token --host [DATABRICKS_URL] | ||
| Error: cache: databricks OAuth is not configured for this host. Try logging in again with `databricks auth login --host [DATABRICKS_URL]` before retrying. If this fails, please report this issue to the Databricks CLI maintainers at https://github.com/databricks/cli/issues/new | ||
| Error: cache: databricks OAuth is not configured for this host. | ||
|
|
||
| To reauthenticate, run the following command: | ||
|
|
||
| $ databricks auth login --host [DATABRICKS_URL] | ||
|
|
||
| If this fails, please report this issue to the Databricks CLI maintainers at https://github.com/databricks/cli/issues/new | ||
|
|
||
| Exit code: 1 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,9 +25,9 @@ import ( | |
| "golang.org/x/oauth2" | ||
| ) | ||
|
|
||
| func helpfulError(ctx context.Context, profile string, persistentAuth u2m.OAuthArgument) string { | ||
| loginMsg := auth.BuildLoginCommand(ctx, profile, persistentAuth) | ||
| return fmt.Sprintf("Try logging in again with `%s` before retrying. If this fails, please report this issue to the Databricks CLI maintainers at https://github.com/databricks/cli/issues/new", loginMsg) | ||
| func helpfulError(ctx context.Context, profile, workspaceID string, persistentAuth u2m.OAuthArgument) string { | ||
| loginMsg := auth.BuildLoginCommand(ctx, profile, workspaceID, persistentAuth) | ||
| return fmt.Sprintf("To reauthenticate, run the following command:\n\n $ %s\n\nIf this fails, please report this issue to the Databricks CLI maintainers at https://github.com/databricks/cli/issues/new", loginMsg) | ||
| } | ||
|
|
||
| func newTokenCommand(authArguments *auth.AuthArguments) *cobra.Command { | ||
|
|
@@ -267,8 +267,8 @@ func loadToken(ctx context.Context, args loadTokenArgs) (*oauth2.Token, error) { | |
| allArgs = append(allArgs, u2m.WithOAuthArgument(oauthArgument)) | ||
| persistentAuth, err := u2m.NewPersistentAuth(ctx, allArgs...) | ||
| if err != nil { | ||
| helpMsg := helpfulError(ctx, args.profileName, oauthArgument) | ||
| return nil, fmt.Errorf("%w. %s", err, helpMsg) | ||
| helpMsg := helpfulError(ctx, args.profileName, args.authArguments.WorkspaceID, oauthArgument) | ||
|
Comment on lines
267
to
+270
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Question: why include When the user invokes Including it is meaningful only if (a) the profile's persisted workspace_id has drifted from the failing call's workspace_id, or (b) the user typed Not asking for a code change — just want to confirm this was deliberate. If yes, a one-line addendum to the doc-comment (e.g. "…also re-emitted on the profile path so the suggested reauth pins the same workspace_id the failing call resolved against") would close the loop. |
||
| return nil, fmt.Errorf("%w.\n\n%s", err, helpMsg) | ||
| } | ||
| var t *oauth2.Token | ||
| if args.forceRefresh { | ||
|
|
@@ -289,11 +289,11 @@ func loadToken(ctx context.Context, args loadTokenArgs) (*oauth2.Token, error) { | |
| // This is captured in an acceptance test under "cmd/auth/token". | ||
| err = errors.New("cache: databricks OAuth is not configured for this host") | ||
| } | ||
| if rewritten, rewrittenErr := auth.RewriteAuthError(ctx, args.authArguments.Host, args.authArguments.AccountID, args.profileName, err); rewritten { | ||
| if rewritten, rewrittenErr := auth.RewriteAuthError(ctx, args.authArguments.Host, args.authArguments.AccountID, args.authArguments.WorkspaceID, args.profileName, err); rewritten { | ||
| return nil, rewrittenErr | ||
| } | ||
| helpMsg := helpfulError(ctx, args.profileName, oauthArgument) | ||
| return nil, fmt.Errorf("%w. %s", err, helpMsg) | ||
| helpMsg := helpfulError(ctx, args.profileName, args.authArguments.WorkspaceID, oauthArgument) | ||
| return nil, fmt.Errorf("%w.\n\n%s", err, helpMsg) | ||
| } | ||
| return t, nil | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ import ( | |
| "net/http" | ||
| "strings" | ||
|
|
||
| "github.com/databricks/cli/libs/shellquote" | ||
| "github.com/databricks/databricks-sdk-go/apierr" | ||
| "github.com/databricks/databricks-sdk-go/config" | ||
| "github.com/databricks/databricks-sdk-go/credentials/u2m" | ||
|
|
@@ -53,18 +54,20 @@ func AuthTypeDisplayName(authType string) string { | |
|
|
||
| // RewriteAuthError rewrites the error message for invalid refresh token error. | ||
| // It returns whether the error was rewritten and the rewritten error. | ||
| func RewriteAuthError(ctx context.Context, host, accountId, profile string, err error) (bool, error) { | ||
| func RewriteAuthError(ctx context.Context, host, accountId, workspaceId, profile string, err error) (bool, error) { | ||
| target := &u2m.InvalidRefreshTokenError{} | ||
| if errors.As(err, &target) { | ||
| oauthArgument, err := AuthArguments{ | ||
| Host: host, | ||
| AccountID: accountId, | ||
| Host: host, | ||
| AccountID: accountId, | ||
| WorkspaceID: workspaceId, | ||
| }.ToOAuthArgument() | ||
| if err != nil { | ||
| return false, err | ||
| } | ||
| msg := `A new access token could not be retrieved because the refresh token is invalid. To reauthenticate, run the following command: | ||
| $ ` + BuildLoginCommand(ctx, profile, oauthArgument) | ||
| msg := "A new access token could not be retrieved because the refresh token is invalid.\n\n" + | ||
| "To reauthenticate, run the following command:\n\n" + | ||
| " $ " + BuildLoginCommand(ctx, profile, workspaceId, oauthArgument) | ||
| return true, errors.New(msg) | ||
| } | ||
| return false, err | ||
|
|
@@ -118,9 +121,11 @@ func writeReauthSteps(ctx context.Context, cfg *config.Config, b *strings.Builde | |
| switch strings.ToLower(cfg.AuthType) { | ||
| case AuthTypeDatabricksCli: | ||
| // When profile is set, BuildLoginCommand uses --profile and ignores | ||
| // the OAuthArgument, so skip the conversion entirely. | ||
| // the OAuthArgument, so skip the conversion entirely. Route through | ||
| // the helper so this branch picks up shell-quoting on the profile name | ||
| // and emits --workspace-id consistently with the host-based branch. | ||
| if cfg.Profile != "" { | ||
| fmt.Fprintf(b, "\n - Re-authenticate: databricks auth login --profile %s", cfg.Profile) | ||
| fmt.Fprintf(b, "\n - Re-authenticate: %s", BuildLoginCommand(ctx, cfg.Profile, cfg.WorkspaceID, nil)) | ||
| return | ||
| } | ||
| oauthArg, argErr := AuthArguments{ | ||
|
|
@@ -133,7 +138,7 @@ func writeReauthSteps(ctx context.Context, cfg *config.Config, b *strings.Builde | |
| fmt.Fprint(b, "\n - Re-authenticate: databricks auth login") | ||
| return | ||
| } | ||
| fmt.Fprintf(b, "\n - Re-authenticate: %s", BuildLoginCommand(ctx, "", oauthArg)) | ||
| fmt.Fprintf(b, "\n - Re-authenticate: %s", BuildLoginCommand(ctx, "", cfg.WorkspaceID, oauthArg)) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This non-profile branch now gets the safer command construction, but the
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Important: the sibling profile branch (line 125) wasn't updated. This branch correctly routes through Result: a 401 on a profile-based databricks-cli call (the most common case) renders without Fix: route the profile branch through |
||
|
|
||
| case AuthTypePat: | ||
| if cfg.Profile != "" { | ||
|
|
@@ -160,28 +165,60 @@ func writeReauthSteps(ctx context.Context, cfg *config.Config, b *strings.Builde | |
| } | ||
| } | ||
|
|
||
| // BuildLoginCommand builds the login command for the given OAuth argument or profile. | ||
| func BuildLoginCommand(ctx context.Context, profile string, arg u2m.OAuthArgument) string { | ||
| // BuildLoginCommand builds the login command for the given OAuth argument or | ||
| // profile. Each argument is shell-quoted so the rendered command is safe to | ||
| // copy-paste even when host, profile, or account-id values contain spaces or | ||
| // shell metacharacters. | ||
| // | ||
| // workspaceID, when non-empty and not the WorkspaceIDNone sentinel, is | ||
| // emitted as --workspace-id for unified hosts so the suggested reauth | ||
| // targets the same workspace the failing call resolved against (the | ||
| // information the OAuthArgument otherwise drops). It is also re-emitted on | ||
| // the profile path so the suggested reauth pins the same workspace_id the | ||
| // failing call resolved against, even if the profile's persisted value has | ||
| // drifted. | ||
| // | ||
| // For AccountOAuthArgument and WorkspaceOAuthArgument, workspaceID is | ||
| // intentionally ignored: account args target the account API, and workspace | ||
| // args already identify a workspace via --host. | ||
| func BuildLoginCommand(ctx context.Context, profile, workspaceID string, arg u2m.OAuthArgument) string { | ||
| cmd := []string{ | ||
| "databricks", | ||
| "auth", | ||
| "login", | ||
| } | ||
| if profile != "" { | ||
| cmd = append(cmd, "--profile", profile) | ||
| if isExplicitWorkspaceID(workspaceID) { | ||
| cmd = append(cmd, "--workspace-id", workspaceID) | ||
| } | ||
| } else { | ||
| switch arg := arg.(type) { | ||
| case u2m.UnifiedOAuthArgument: | ||
| // Discovery handles unified-host routing from --host + --account-id, | ||
| // so we no longer suggest --experimental-is-unified-host here. | ||
| cmd = append(cmd, "--host", arg.GetHost(), "--account-id", arg.GetAccountId()) | ||
| if isExplicitWorkspaceID(workspaceID) { | ||
| cmd = append(cmd, "--workspace-id", workspaceID) | ||
| } | ||
| case u2m.AccountOAuthArgument: | ||
| cmd = append(cmd, "--host", arg.GetAccountHost(), "--account-id", arg.GetAccountId()) | ||
| case u2m.WorkspaceOAuthArgument: | ||
| cmd = append(cmd, "--host", arg.GetWorkspaceHost()) | ||
| } | ||
| } | ||
| return strings.Join(cmd, " ") | ||
| quoted := make([]string, len(cmd)) | ||
| for i, c := range cmd { | ||
| quoted[i] = shellquote.BashArg(c) | ||
| } | ||
| return strings.Join(quoted, " ") | ||
| } | ||
|
|
||
| // isExplicitWorkspaceID reports whether workspaceID names a real workspace | ||
| // (not the "none" sentinel persisted to .databrickscfg when the user | ||
| // explicitly skipped workspace selection). | ||
| func isExplicitWorkspaceID(workspaceID string) bool { | ||
| return workspaceID != "" && workspaceID != WorkspaceIDNone | ||
| } | ||
|
|
||
| // BuildDescribeCommand builds the describe command for the given config. | ||
|
|
@@ -190,7 +227,7 @@ func BuildLoginCommand(ctx context.Context, profile string, arg u2m.OAuthArgumen | |
| // automatically. | ||
| func BuildDescribeCommand(cfg *config.Config) string { | ||
| if cfg.Profile != "" { | ||
| return "databricks auth describe --profile " + cfg.Profile | ||
| return "databricks auth describe --profile " + shellquote.BashArg(cfg.Profile) | ||
| } | ||
| return "databricks auth describe" | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,6 +25,66 @@ func TestBuildDescribeCommand(t *testing.T) { | |
| ) | ||
| } | ||
|
|
||
| func TestBuildLoginCommand_AppendsWorkspaceID(t *testing.T) { | ||
| ctx := t.Context() | ||
|
|
||
| t.Run("profile path emits --workspace-id when set", func(t *testing.T) { | ||
| cmd := BuildLoginCommand(ctx, "dev", "12345", nil) | ||
| assert.Equal(t, "databricks auth login --profile dev --workspace-id 12345", cmd) | ||
| }) | ||
|
|
||
| t.Run("profile path omits --workspace-id when empty", func(t *testing.T) { | ||
| cmd := BuildLoginCommand(ctx, "dev", "", nil) | ||
| assert.Equal(t, "databricks auth login --profile dev", cmd) | ||
| }) | ||
|
|
||
| t.Run("profile path omits --workspace-id for the 'none' sentinel", func(t *testing.T) { | ||
| cmd := BuildLoginCommand(ctx, "dev", WorkspaceIDNone, nil) | ||
| assert.Equal(t, "databricks auth login --profile dev", cmd) | ||
| }) | ||
|
|
||
| t.Run("unified host path emits --workspace-id when set", func(t *testing.T) { | ||
| oauthArg, err := AuthArguments{ | ||
| Host: "https://unified.cloud.databricks.com", | ||
| AccountID: "acc-123", | ||
| DiscoveryURL: "https://unified.cloud.databricks.com/oidc/accounts/acc-123/.well-known/oauth-authorization-server", | ||
| }.ToOAuthArgument() | ||
| require.NoError(t, err) | ||
|
|
||
| cmd := BuildLoginCommand(ctx, "", "ws-456", oauthArg) | ||
| assert.Contains(t, cmd, "--account-id acc-123") | ||
| assert.Contains(t, cmd, "--workspace-id ws-456") | ||
| }) | ||
|
|
||
| t.Run("workspace host path omits --workspace-id even when provided", func(t *testing.T) { | ||
| oauthArg, err := AuthArguments{ | ||
| Host: "https://adb-123.azuredatabricks.net", | ||
| }.ToOAuthArgument() | ||
| require.NoError(t, err) | ||
|
|
||
| cmd := BuildLoginCommand(ctx, "", "ws-456", oauthArg) | ||
| assert.Contains(t, cmd, "--host https://adb-123.azuredatabricks.net") | ||
| assert.NotContains(t, cmd, "--workspace-id") | ||
| }) | ||
|
|
||
| t.Run("account host path omits --workspace-id even when provided", func(t *testing.T) { | ||
| oauthArg, err := AuthArguments{ | ||
| Host: "https://accounts.cloud.databricks.com", | ||
| AccountID: "acc-123", | ||
| }.ToOAuthArgument() | ||
| require.NoError(t, err) | ||
|
|
||
| cmd := BuildLoginCommand(ctx, "", "ws-456", oauthArg) | ||
| assert.Contains(t, cmd, "--account-id acc-123") | ||
| assert.NotContains(t, cmd, "--workspace-id") | ||
| }) | ||
|
|
||
| t.Run("profile path shell-quotes profile names with spaces", func(t *testing.T) { | ||
| cmd := BuildLoginCommand(ctx, "weird name", "", nil) | ||
| assert.Equal(t, "databricks auth login --profile 'weird name'", cmd) | ||
| }) | ||
| } | ||
|
Comment on lines
+28
to
+86
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: two coverage gaps in
Fix: two short table cases — one with |
||
|
|
||
| func TestAuthTypeDisplayName(t *testing.T) { | ||
| assert.Equal(t, "Personal Access Token (pat)", AuthTypeDisplayName("pat")) | ||
| assert.Equal(t, "OAuth (databricks-cli)", AuthTypeDisplayName("databricks-cli")) | ||
|
|
@@ -241,7 +301,7 @@ func TestEnrichAuthError(t *testing.T) { | |
| "\nHost: https://unified.cloud.databricks.com" + | ||
| "\nAuth type: OAuth (databricks-cli)" + | ||
| "\n\nNext steps:" + | ||
| "\n - Re-authenticate: databricks auth login --host https://unified.cloud.databricks.com --account-id acc-123" + | ||
| "\n - Re-authenticate: databricks auth login --host https://unified.cloud.databricks.com --account-id acc-123 --workspace-id ws-456" + | ||
| "\n - Check your identity: databricks auth describe" + | ||
| "\n - Consider setting up a profile: databricks auth login --profile <name>", | ||
| }, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This link still points to the replaced PR (
#4602). Since this PR is the one that will merge, the release note should reference#5115; otherwise the shipped changelog sends readers to the abandoned PR.