Skip to content

Commit 67f79d8

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 67f79d8

3 files changed

Lines changed: 190 additions & 67 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: 59 additions & 28 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,37 @@ 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, "") {
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.")
111+
}
112+
113+
if c.hostCmd != nil {
87114
return c.execCliCommand(ctx, c.hostCmd)
88115
}
89-
return tok, err
116+
117+
return nil, fmt.Errorf("no CLI command configured")
90118
}
91119

92120
func (c *CliTokenSource) execCliCommand(ctx context.Context, args []string) (*oauth2.Token, error) {
@@ -115,10 +143,13 @@ func (c *CliTokenSource) execCliCommand(ctx context.Context, args []string) (*oa
115143
}
116144

117145
// 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")
146+
// recognize a flag. Pass a specific flag (e.g. "--profile") to check for that
147+
// flag, or pass "" to match any "unknown flag:" error.
148+
func isUnknownFlagError(err error, flag string) bool {
149+
if flag == "" {
150+
return strings.Contains(err.Error(), "unknown flag:")
151+
}
152+
return strings.Contains(err.Error(), "unknown flag: "+flag)
122153
}
123154

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

config/cli_token_source_test.go

Lines changed: 130 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -136,20 +136,21 @@ func TestBuildCliCommands(t *testing.T) {
136136
)
137137

138138
testCases := []struct {
139-
name string
140-
cfg *Config
141-
wantCmd []string
142-
wantHostCmd []string
139+
name string
140+
cfg *Config
141+
wantForceCmd []string
142+
wantProfileCmd []string
143+
wantHostCmd []string
143144
}{
144145
{
145-
name: "workspace host",
146-
cfg: &Config{Host: host},
147-
wantCmd: []string{cliPath, "auth", "token", "--host", host},
146+
name: "workspace host only",
147+
cfg: &Config{Host: host},
148+
wantHostCmd: []string{cliPath, "auth", "token", "--host", host},
148149
},
149150
{
150-
name: "account host",
151-
cfg: &Config{Host: accountHost, AccountID: accountID},
152-
wantCmd: []string{cliPath, "auth", "token", "--host", accountHost, "--account-id", accountID},
151+
name: "account host only",
152+
cfg: &Config{Host: accountHost, AccountID: accountID},
153+
wantHostCmd: []string{cliPath, "auth", "token", "--host", accountHost, "--account-id", accountID},
153154
},
154155
{
155156
name: "former unified host treated as workspace",
@@ -158,26 +159,31 @@ func TestBuildCliCommands(t *testing.T) {
158159
AccountID: accountID,
159160
WorkspaceID: workspaceID,
160161
},
161-
wantCmd: []string{cliPath, "auth", "token", "--host", unifiedHost},
162+
wantHostCmd: []string{cliPath, "auth", "token", "--host", unifiedHost},
162163
},
163164
{
164-
name: "profile uses --profile with --host fallback",
165-
cfg: &Config{Profile: "my-profile", Host: host},
166-
wantCmd: []string{cliPath, "auth", "token", "--profile", "my-profile"},
167-
wantHostCmd: []string{cliPath, "auth", "token", "--host", host},
165+
name: "profile with host — all three commands",
166+
cfg: &Config{Profile: "my-profile", Host: host},
167+
wantForceCmd: []string{cliPath, "auth", "token", "--profile", "my-profile", "--force-refresh"},
168+
wantProfileCmd: []string{cliPath, "auth", "token", "--profile", "my-profile"},
169+
wantHostCmd: []string{cliPath, "auth", "token", "--host", host},
168170
},
169171
{
170-
name: "profile without host — no fallback",
171-
cfg: &Config{Profile: "my-profile"},
172-
wantCmd: []string{cliPath, "auth", "token", "--profile", "my-profile"},
172+
name: "profile without host — no host fallback",
173+
cfg: &Config{Profile: "my-profile"},
174+
wantForceCmd: []string{cliPath, "auth", "token", "--profile", "my-profile", "--force-refresh"},
175+
wantProfileCmd: []string{cliPath, "auth", "token", "--profile", "my-profile"},
173176
},
174177
}
175178

176179
for _, tc := range testCases {
177180
t.Run(tc.name, func(t *testing.T) {
178-
gotCmd, gotHostCmd := buildCliCommands(cliPath, tc.cfg)
179-
if !slices.Equal(gotCmd, tc.wantCmd) {
180-
t.Errorf("primary cmd = %v, want %v", gotCmd, tc.wantCmd)
181+
gotForceCmd, gotProfileCmd, gotHostCmd := buildCliCommands(cliPath, tc.cfg)
182+
if !slices.Equal(gotForceCmd, tc.wantForceCmd) {
183+
t.Errorf("force cmd = %v, want %v", gotForceCmd, tc.wantForceCmd)
184+
}
185+
if !slices.Equal(gotProfileCmd, tc.wantProfileCmd) {
186+
t.Errorf("profile cmd = %v, want %v", gotProfileCmd, tc.wantProfileCmd)
181187
}
182188
if !slices.Equal(gotHostCmd, tc.wantHostCmd) {
183189
t.Errorf("host cmd = %v, want %v", gotHostCmd, tc.wantHostCmd)
@@ -204,9 +210,8 @@ func TestNewCliTokenSource(t *testing.T) {
204210
if err != nil {
205211
t.Fatalf("NewCliTokenSource() unexpected error: %v", err)
206212
}
207-
// Verify CLI path was resolved and used
208-
if ts.cmd[0] != validCliPath {
209-
t.Errorf("cmd[0] = %q, want %q", ts.cmd[0], validCliPath)
213+
if ts.hostCmd[0] != validCliPath {
214+
t.Errorf("hostCmd[0] = %q, want %q", ts.hostCmd[0], validCliPath)
210215
}
211216
})
212217

@@ -262,7 +267,7 @@ func TestCliTokenSource_Token(t *testing.T) {
262267
t.Fatalf("failed to create mock script: %v", err)
263268
}
264269

265-
ts := &CliTokenSource{cmd: []string{mockScript}}
270+
ts := &CliTokenSource{hostCmd: []string{mockScript}}
266271
token, err := ts.Token(context.Background())
267272

268273
if tc.wantErrMsg != "" {
@@ -281,42 +286,122 @@ func TestCliTokenSource_Token(t *testing.T) {
281286
}
282287
}
283288

284-
func TestCliTokenSource_Token_FallbackOnUnknownFlag(t *testing.T) {
289+
func TestCliTokenSource_Token_ForceRefreshFallbackToProfile(t *testing.T) {
290+
if runtime.GOOS == "windows" {
291+
t.Skip("Skipping shell script test on Windows")
292+
}
293+
294+
expiry := time.Now().Add(1 * time.Hour).Format(time.RFC3339)
295+
validResponse, _ := json.Marshal(cliTokenResponse{
296+
AccessToken: "profile-token",
297+
TokenType: "Bearer",
298+
Expiry: expiry,
299+
})
300+
301+
tempDir := t.TempDir()
302+
303+
forceScript := filepath.Join(tempDir, "force_cli.sh")
304+
if err := os.WriteFile(forceScript, []byte("#!/bin/sh\necho 'Error: unknown flag: --force-refresh' >&2\nexit 1"), 0755); err != nil {
305+
t.Fatalf("failed to create force script: %v", err)
306+
}
307+
308+
profileScript := filepath.Join(tempDir, "profile_cli.sh")
309+
if err := os.WriteFile(profileScript, []byte("#!/bin/sh\necho '"+string(validResponse)+"'"), 0755); err != nil {
310+
t.Fatalf("failed to create profile script: %v", err)
311+
}
312+
313+
ts := &CliTokenSource{
314+
forceCmd: []string{forceScript},
315+
profileCmd: []string{profileScript},
316+
}
317+
token, err := ts.Token(context.Background())
318+
if err != nil {
319+
t.Fatalf("Token() error = %v, want fallback to profileCmd to succeed", err)
320+
}
321+
if token.AccessToken != "profile-token" {
322+
t.Errorf("AccessToken = %q, want %q", token.AccessToken, "profile-token")
323+
}
324+
}
325+
326+
func TestCliTokenSource_Token_ProfileFallbackToHost(t *testing.T) {
285327
if runtime.GOOS == "windows" {
286328
t.Skip("Skipping shell script test on Windows")
287329
}
288330

289331
expiry := time.Now().Add(1 * time.Hour).Format(time.RFC3339)
290332
validResponse, _ := json.Marshal(cliTokenResponse{
291-
AccessToken: "fallback-token",
333+
AccessToken: "host-token",
292334
TokenType: "Bearer",
293335
Expiry: expiry,
294336
})
295337

296338
tempDir := t.TempDir()
297339

298-
// Primary script simulates an old CLI that doesn't know --profile.
299340
profileScript := filepath.Join(tempDir, "profile_cli.sh")
300341
if err := os.WriteFile(profileScript, []byte("#!/bin/sh\necho 'Error: unknown flag: --profile' >&2\nexit 1"), 0755); err != nil {
301342
t.Fatalf("failed to create profile script: %v", err)
302343
}
303344

304-
// Fallback script succeeds with --host.
305345
hostScript := filepath.Join(tempDir, "host_cli.sh")
306346
if err := os.WriteFile(hostScript, []byte("#!/bin/sh\necho '"+string(validResponse)+"'"), 0755); err != nil {
307347
t.Fatalf("failed to create host script: %v", err)
308348
}
309349

310350
ts := &CliTokenSource{
311-
cmd: []string{profileScript},
312-
hostCmd: []string{hostScript},
351+
profileCmd: []string{profileScript},
352+
hostCmd: []string{hostScript},
313353
}
314354
token, err := ts.Token(context.Background())
315355
if err != nil {
316-
t.Fatalf("Token() error = %v, want fallback to succeed", err)
356+
t.Fatalf("Token() error = %v, want fallback to hostCmd to succeed", err)
317357
}
318-
if token.AccessToken != "fallback-token" {
319-
t.Errorf("AccessToken = %q, want %q", token.AccessToken, "fallback-token")
358+
if token.AccessToken != "host-token" {
359+
t.Errorf("AccessToken = %q, want %q", token.AccessToken, "host-token")
360+
}
361+
}
362+
363+
func TestCliTokenSource_Token_ForceRefreshFallbackToHostOnProfileError(t *testing.T) {
364+
if runtime.GOOS == "windows" {
365+
t.Skip("Skipping shell script test on Windows")
366+
}
367+
368+
expiry := time.Now().Add(1 * time.Hour).Format(time.RFC3339)
369+
validResponse, _ := json.Marshal(cliTokenResponse{
370+
AccessToken: "host-token",
371+
TokenType: "Bearer",
372+
Expiry: expiry,
373+
})
374+
375+
tempDir := t.TempDir()
376+
377+
// forceCmd fails with --profile unknown (very old CLI).
378+
forceScript := filepath.Join(tempDir, "force_cli.sh")
379+
if err := os.WriteFile(forceScript, []byte("#!/bin/sh\necho 'Error: unknown flag: --profile' >&2\nexit 1"), 0755); err != nil {
380+
t.Fatalf("failed to create force script: %v", err)
381+
}
382+
383+
// profileCmd also fails with --profile unknown.
384+
profileScript := filepath.Join(tempDir, "profile_cli.sh")
385+
if err := os.WriteFile(profileScript, []byte("#!/bin/sh\necho 'Error: unknown flag: --profile' >&2\nexit 1"), 0755); err != nil {
386+
t.Fatalf("failed to create profile script: %v", err)
387+
}
388+
389+
hostScript := filepath.Join(tempDir, "host_cli.sh")
390+
if err := os.WriteFile(hostScript, []byte("#!/bin/sh\necho '"+string(validResponse)+"'"), 0755); err != nil {
391+
t.Fatalf("failed to create host script: %v", err)
392+
}
393+
394+
ts := &CliTokenSource{
395+
forceCmd: []string{forceScript},
396+
profileCmd: []string{profileScript},
397+
hostCmd: []string{hostScript},
398+
}
399+
token, err := ts.Token(context.Background())
400+
if err != nil {
401+
t.Fatalf("Token() error = %v, want fallback through to hostCmd to succeed", err)
402+
}
403+
if token.AccessToken != "host-token" {
404+
t.Errorf("AccessToken = %q, want %q", token.AccessToken, "host-token")
320405
}
321406
}
322407

@@ -327,21 +412,27 @@ func TestCliTokenSource_Token_NoFallbackOnRealError(t *testing.T) {
327412

328413
tempDir := t.TempDir()
329414

330-
// Primary script fails with a real auth error (not unknown flag).
415+
// forceCmd fails with a real auth error (not unknown flag).
416+
forceScript := filepath.Join(tempDir, "force_cli.sh")
417+
if err := os.WriteFile(forceScript, []byte("#!/bin/sh\necho 'cache: databricks OAuth is not configured for this host' >&2\nexit 1"), 0755); err != nil {
418+
t.Fatalf("failed to create force script: %v", err)
419+
}
420+
421+
// profileCmd and hostCmd should not be called.
331422
profileScript := filepath.Join(tempDir, "profile_cli.sh")
332-
if err := os.WriteFile(profileScript, []byte("#!/bin/sh\necho 'cache: databricks OAuth is not configured for this host' >&2\nexit 1"), 0755); err != nil {
423+
if err := os.WriteFile(profileScript, []byte("#!/bin/sh\necho 'should not reach here' >&2\nexit 1"), 0755); err != nil {
333424
t.Fatalf("failed to create profile script: %v", err)
334425
}
335426

336-
// Fallback script would succeed, but should not be called.
337427
hostScript := filepath.Join(tempDir, "host_cli.sh")
338428
if err := os.WriteFile(hostScript, []byte("#!/bin/sh\necho 'should not reach here' >&2\nexit 1"), 0755); err != nil {
339429
t.Fatalf("failed to create host script: %v", err)
340430
}
341431

342432
ts := &CliTokenSource{
343-
cmd: []string{profileScript},
344-
hostCmd: []string{hostScript},
433+
forceCmd: []string{forceScript},
434+
profileCmd: []string{profileScript},
435+
hostCmd: []string{hostScript},
345436
}
346437
_, err := ts.Token(context.Background())
347438
if err == nil {

0 commit comments

Comments
 (0)