-
Notifications
You must be signed in to change notification settings - Fork 167
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
152a1b7
368593e
412952e
8a47c90
3e1ef3f
cb5d408
f0f67fd
395ca7d
45602a6
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 |
|---|---|---|
|
|
@@ -6,6 +6,8 @@ | |
|
|
||
| * `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 | ||
|
|
||
| ### Dependency updates | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| 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 | ||
| $ 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 |
|---|---|---|
|
|
@@ -26,9 +26,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) | ||
| } | ||
|
|
||
| // profileSelectionResult represents the user's choice from the interactive | ||
|
|
@@ -278,8 +278,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
278
to
+281
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 { | ||
|
|
@@ -300,11 +300,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,19 @@ 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) | ||
| $ ` + BuildLoginCommand(ctx, profile, workspaceId, oauthArgument) | ||
|
Comment on lines
68
to
+69
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: layout inconsistency.
Compare Fix: unify on the new padded form here too — that's the actual goal of the PR. The acceptance test golden file would need a small update. |
||
| return true, errors.New(msg) | ||
| } | ||
| return false, err | ||
|
|
@@ -133,7 +135,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 +162,53 @@ 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). | ||
|
Comment on lines
+165
to
+173
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: the doc-comment explains the unified-host case for Fix: add one sentence — "For |
||
| 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 +217,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,38 @@ 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") | ||
| }) | ||
| } | ||
|
Comment on lines
+28
to
+58
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 +273,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.