Skip to content

Commit 69a7c95

Browse files
Add --force-refresh flag to CLI token source
Pass --force-refresh to the Databricks CLI auth token command to bypass the CLI's internal token cache. The SDK manages its own token caching, so the CLI serving stale tokens from its cache causes unnecessary refresh failures. Commands are now tried in order: forceCmd (--profile + --force-refresh), profileCmd (--profile), hostCmd (--host), falling back progressively for older CLI versions that don't support newer flags. Signed-off-by: Mihai Mitrea <mihai.mitrea@databricks.com>
1 parent 78469e6 commit 69a7c95

3 files changed

Lines changed: 221 additions & 69 deletions

File tree

NEXT_CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
### New Features and Improvements
1010

11+
* Pass `--force-refresh` to Databricks CLI `auth token` command to bypass the CLI's internal token cache.
1112
* Added `HostMetadataResolver` hook to allow callers to customize host metadata resolution, e.g. with caching ([#1572](https://github.com/databricks/databricks-sdk-go/pull/1572)).
1213
* Added `NewLimitIterator` to `listing` package for lazy iteration with a cap on output items ([#1555](https://github.com/databricks/databricks-sdk-go/pull/1555)).
1314

config/cli_token_source.go

Lines changed: 60 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,19 @@ type cliTokenResponse struct {
3131
Expiry string `json:"expiry"`
3232
}
3333

34+
// CliTokenSource fetches OAuth tokens by shelling out to the Databricks CLI.
35+
// Commands are tried in order: forceCmd -> profileCmd -> hostCmd, progressively
36+
// falling back to simpler invocations for older CLI versions.
3437
type CliTokenSource struct {
35-
// cmd is the primary command to execute (--profile when available, --host otherwise).
36-
cmd []string
38+
// forceCmd uses --profile with --force-refresh to bypass the CLI's token cache.
39+
// Nil when cfg.Profile is empty (--force-refresh requires --profile support).
40+
forceCmd []string
3741

38-
// hostCmd is a fallback command using --host, used when the primary --profile
39-
// command fails because the CLI is too old to support --profile.
42+
// profileCmd uses --profile for token lookup. Nil when cfg.Profile is empty.
43+
profileCmd []string
44+
45+
// hostCmd uses --host as a fallback for CLIs that predate --profile support.
46+
// Nil when cfg.Host is empty.
4047
hostCmd []string
4148
}
4249

@@ -45,25 +52,25 @@ func NewCliTokenSource(cfg *Config) (*CliTokenSource, error) {
4552
if err != nil {
4653
return nil, err
4754
}
48-
profileCmd, hostCmd := buildCliCommands(cliPath, cfg)
49-
return &CliTokenSource{cmd: profileCmd, hostCmd: hostCmd}, nil
55+
forceCmd, profileCmd, hostCmd := buildCliCommands(cliPath, cfg)
56+
return &CliTokenSource{forceCmd: forceCmd, profileCmd: profileCmd, hostCmd: hostCmd}, nil
5057
}
5158

5259
// buildCliCommands constructs the CLI commands for fetching an auth token.
53-
// When cfg.Profile is set, the primary command uses --profile and a fallback
54-
// --host command is also returned for compatibility with older CLIs.
55-
// When cfg.Profile is empty, the primary command uses --host and no fallback
56-
// is needed.
57-
func buildCliCommands(cliPath string, cfg *Config) (primaryCmd []string, hostCmd []string) {
60+
// When cfg.Profile is set, three commands are built: a --force-refresh variant,
61+
// a plain --profile variant, and (when host is available) a --host fallback.
62+
// When cfg.Profile is empty, only --host is returned — the CLI must support
63+
// --profile before --force-refresh can be used (monotonic feature assumption).
64+
func buildCliCommands(cliPath string, cfg *Config) ([]string, []string, []string) {
65+
var forceCmd, profileCmd, hostCmd []string
5866
if cfg.Profile != "" {
59-
primary := []string{cliPath, "auth", "token", "--profile", cfg.Profile}
60-
if cfg.Host != "" {
61-
// Build a --host fallback for old CLIs that don't support --profile.
62-
return primary, buildHostCommand(cliPath, cfg)
63-
}
64-
return primary, nil
67+
profileCmd = []string{cliPath, "auth", "token", "--profile", cfg.Profile}
68+
forceCmd = append(profileCmd, "--force-refresh")
6569
}
66-
return buildHostCommand(cliPath, cfg), nil
70+
if cfg.Host != "" {
71+
hostCmd = buildHostCommand(cliPath, cfg)
72+
}
73+
return forceCmd, profileCmd, hostCmd
6774
}
6875

6976
// buildHostCommand constructs the legacy --host based CLI command.
@@ -77,16 +84,36 @@ func buildHostCommand(cliPath string, cfg *Config) []string {
7784
}
7885

7986
// Token fetches an OAuth token by shelling out to the Databricks CLI.
80-
// When a --profile command is configured, it is tried first. If the CLI
81-
// returns "unknown flag: --profile" (indicating an older CLI version),
82-
// the fallback --host command is used instead.
87+
// Commands are tried in order: forceCmd -> profileCmd -> hostCmd, skipping nil
88+
// entries. Each command falls through to the next on "unknown flag" errors,
89+
// logging a warning about the unsupported feature.
8390
func (c *CliTokenSource) Token(ctx context.Context) (*oauth2.Token, error) {
84-
tok, err := c.execCliCommand(ctx, c.cmd)
85-
if err != nil && c.hostCmd != nil && isUnknownFlagError(err) {
91+
if c.forceCmd != nil {
92+
tok, err := c.execCliCommand(ctx, c.forceCmd)
93+
if err == nil {
94+
return tok, nil
95+
}
96+
if !isUnknownFlagError(err, "--force-refresh") && !isUnknownFlagError(err, "--profile") {
97+
return nil, err
98+
}
99+
logger.Warnf(ctx, "Databricks CLI does not support --force-refresh flag. The CLI's token cache may provide stale tokens. Please upgrade your CLI to the latest version.")
100+
}
101+
102+
if c.profileCmd != nil {
103+
tok, err := c.execCliCommand(ctx, c.profileCmd)
104+
if err == nil {
105+
return tok, nil
106+
}
107+
if !isUnknownFlagError(err, "--profile") {
108+
return nil, err
109+
}
86110
logger.Warnf(ctx, "Databricks CLI does not support --profile flag. Falling back to --host. Please upgrade your CLI to the latest version.")
87-
return c.execCliCommand(ctx, c.hostCmd)
88111
}
89-
return tok, err
112+
113+
if c.hostCmd == nil {
114+
return nil, fmt.Errorf("cannot get access token: no CLI commands available")
115+
}
116+
return c.execCliCommand(ctx, c.hostCmd)
90117
}
91118

92119
func (c *CliTokenSource) execCliCommand(ctx context.Context, args []string) (*oauth2.Token, error) {
@@ -95,7 +122,7 @@ func (c *CliTokenSource) execCliCommand(ctx context.Context, args []string) (*oa
95122
if err != nil {
96123
var exitErr *exec.ExitError
97124
if errors.As(err, &exitErr) {
98-
return nil, fmt.Errorf("cannot get access token: %s", strings.TrimSpace(string(exitErr.Stderr)))
125+
return nil, fmt.Errorf("cannot get access token: %q", strings.TrimSpace(string(exitErr.Stderr)))
99126
}
100127
return nil, fmt.Errorf("cannot get access token: %w", err)
101128
}
@@ -115,10 +142,13 @@ func (c *CliTokenSource) execCliCommand(ctx context.Context, args []string) (*oa
115142
}
116143

117144
// isUnknownFlagError returns true if the error indicates the CLI does not
118-
// recognize the --profile flag. This happens with older CLI versions that
119-
// predate profile-based token lookup.
120-
func isUnknownFlagError(err error) bool {
121-
return strings.Contains(err.Error(), "unknown flag: --profile")
145+
// recognize a flag. Pass a specific flag (e.g. "--profile") to check for that
146+
// flag, or pass "" to match any "unknown flag:" error.
147+
func isUnknownFlagError(err error, flag string) bool {
148+
if flag == "" {
149+
return strings.Contains(err.Error(), "unknown flag:")
150+
}
151+
return strings.Contains(err.Error(), "unknown flag: "+flag)
122152
}
123153

124154
// parseExpiry parses an expiry time string in multiple formats for cross-SDK compatibility.

0 commit comments

Comments
 (0)