Skip to content

Commit 56562c5

Browse files
Pass --force-refresh to Databricks CLI auth token command
The SDK manages its own token caching via `CachedTokenSource`. When the SDK shells out to `databricks auth token`, the CLI may return a token from *its* own cache that is about to expire (or has already expired from the SDK's perspective), producing unnecessary refresh failures and retry loops. The CLI added `--force-refresh` in v0.296.0 (databricks/cli#4767) to let callers bypass its cache. With the version-detection infrastructure from the parent PR already in place, opting in is a one-constant, one-branch change: * Introduce `CLI_VERSION_FOR_FORCE_REFRESH = v0.296.0`. * Split `buildCliCommand` into the existing profile/host decision (now `buildCoreCliCommand`) and a thin wrapper that appends `--force-refresh` when supported and otherwise logs a precise warning. Future capability-gated flags slot into the same wrapper. Mirrors: * databricks/databricks-sdk-go#1628 * databricks/databricks-sdk-py#1378 Co-authored-by: Isaac
1 parent 20d0239 commit 56562c5

3 files changed

Lines changed: 82 additions & 25 deletions

File tree

NEXT_CHANGELOG.md

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

1616
### Internal Changes
1717
* Detect Databricks CLI version at init time via `databricks version --output json`, enabling version-gated flag support. Successful detections are cached per CLI path; subprocess failures fall back to the most conservative command and are retried on the next call.
18+
* Pass `--force-refresh` to Databricks CLI `auth token` command (when the installed CLI is >= v0.296.0) so the SDK always receives a freshly minted token instead of a potentially stale one from the CLI's internal cache.
1819

1920
### API Changes
2021
* Add `createExample()`, `deleteExample()`, `getExample()`, `getPermissionLevels()`, `getPermissions()`, `listExamples()`, `setPermissions()`, `updateExample()` and `updatePermissions()` methods for `workspaceClient.supervisorAgents()` service.

databricks-sdk-java/src/main/java/com/databricks/sdk/core/DatabricksCliCredentialsProvider.java

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ public class DatabricksCliCredentialsProvider implements CredentialsProvider {
3535
// --profile support added in CLI v0.207.1: https://github.com/databricks/cli/pull/855
3636
static final DatabricksCliVersion CLI_VERSION_FOR_PROFILE = new DatabricksCliVersion(0, 207, 1);
3737

38+
// --force-refresh support added in CLI v0.296.0: https://github.com/databricks/cli/pull/4767
39+
static final DatabricksCliVersion CLI_VERSION_FOR_FORCE_REFRESH =
40+
new DatabricksCliVersion(0, 296, 0);
41+
3842
// 5-second cap on `databricks version` so a hung CLI (slow first-run scan, antivirus, blocked
3943
// stdin) does not wedge SDK init indefinitely.
4044
private static final long VERSION_PROBE_TIMEOUT_SECONDS = 5;
@@ -161,13 +165,41 @@ List<String> resolveCliCommand(String cliPath, DatabricksConfig config) {
161165
}
162166

163167
/**
164-
* Builds the {@code auth token} command for the given CLI version.
168+
* Builds the full {@code auth token} command, including capability-gated flags.
165169
*
166-
* <p>Falls back to {@code --host} when {@code --profile} is either not configured or not
167-
* supported by the installed CLI.
170+
* <p>Delegates the profile/host decision to {@link #buildCoreCliCommand} and appends {@code
171+
* --force-refresh} when the installed CLI supports it.
168172
*/
169173
List<String> buildCliCommand(
170174
String cliPath, DatabricksConfig config, DatabricksCliVersion version) {
175+
List<String> cmd = buildCoreCliCommand(cliPath, config, version);
176+
if (version.atLeast(CLI_VERSION_FOR_FORCE_REFRESH)) {
177+
cmd.add("--force-refresh");
178+
} else if (version.isDefaultDevBuild()) {
179+
// Dev build — getCliVersion already emitted the dev-build INFO at the probe site.
180+
} else if (version.equals(DatabricksCliVersion.UNKNOWN)) {
181+
// We didn't actually prove the CLI lacks --force-refresh; we just failed to confirm it.
182+
LOG.warn(
183+
"Could not confirm --force-refresh support for Databricks CLI {} (requires >= {}). "
184+
+ "The CLI's token cache may provide stale tokens.",
185+
version,
186+
CLI_VERSION_FOR_FORCE_REFRESH);
187+
} else {
188+
LOG.warn(
189+
"Databricks CLI {} does not support --force-refresh (requires >= {}). "
190+
+ "The CLI's token cache may provide stale tokens.",
191+
version,
192+
CLI_VERSION_FOR_FORCE_REFRESH);
193+
}
194+
return cmd;
195+
}
196+
197+
/**
198+
* Builds the base {@code auth token} command without capability-gated flags. Falls back to {@code
199+
* --host} when {@code --profile} is either not configured or not supported by the installed CLI.
200+
*/
201+
List<String> buildCoreCliCommand(
202+
String cliPath, DatabricksConfig config, DatabricksCliVersion version) {
171203
if (config.getProfile() == null) {
172204
return buildHostArgs(cliPath, config);
173205
}
@@ -176,14 +208,7 @@ List<String> buildCliCommand(
176208
// do not support it. Only use --profile in CLI versions known to support it in `auth token`.
177209
if (!version.atLeast(CLI_VERSION_FOR_PROFILE)) {
178210
if (version.isDefaultDevBuild()) {
179-
// A default-marker dev build has no injected version, so every feature gate fails.
180-
// Surface an informational hint so users know why their feature flags aren't taking
181-
// effect.
182-
LOG.info(
183-
"Databricks CLI {} is a development build; feature detection will use conservative "
184-
+ "fallbacks. Rebuild the CLI with an explicit version to enable capability-based "
185-
+ "flag selection.",
186-
version);
211+
// Dev build — getCliVersion already emitted the dev-build INFO at the probe site.
187212
} else if (version.equals(DatabricksCliVersion.UNKNOWN)) {
188213
LOG.warn(
189214
"Could not confirm --profile support for Databricks CLI {} (requires >= {}). "
@@ -254,11 +279,21 @@ DatabricksCliVersion getCliVersion(String cliPath, Environment env) {
254279
LOG.warn(
255280
"Failed to detect Databricks CLI version: {}. "
256281
+ "Falling back to conservative flag set.",
257-
e.getMessage(),
258-
e);
282+
e.getMessage());
283+
LOG.debug("CLI version probe failure stack:", e);
259284
return DatabricksCliVersion.UNKNOWN;
260285
}
261286
});
287+
if (version.isDefaultDevBuild()) {
288+
// A default-marker dev build has no injected version, so every feature gate falls back to
289+
// the conservative path. Emit once at the probe site so callers see the explanation even
290+
// on the host-only code path that never consults a per-flag gate.
291+
LOG.info(
292+
"Databricks CLI {} is a development build; feature detection will use conservative "
293+
+ "fallbacks. Rebuild the CLI with an explicit version to enable capability-based "
294+
+ "flag selection.",
295+
version);
296+
}
262297
// Don't cache UNKNOWN: a transient probe failure or a parseable-but-malformed payload
263298
// would otherwise pin every later token source to the conservative fallback for the rest
264299
// of the process lifetime. Strip it after computeIfAbsent so the next call re-probes.

databricks-sdk-java/src/test/java/com/databricks/sdk/core/DatabricksCliCredentialsProviderTest.java

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
import static org.junit.jupiter.api.Assertions.*;
44
import static org.mockito.ArgumentMatchers.any;
5-
import static org.mockito.ArgumentMatchers.anyBoolean;
65
import static org.mockito.ArgumentMatchers.anyLong;
6+
import static org.mockito.ArgumentMatchers.eq;
77
import static org.mockito.Mockito.atLeastOnce;
88
import static org.mockito.Mockito.mock;
99
import static org.mockito.Mockito.mockConstruction;
@@ -16,7 +16,6 @@
1616
import com.databricks.sdk.core.utils.OSUtilities;
1717
import com.databricks.sdk.core.utils.OSUtils;
1818
import java.io.ByteArrayInputStream;
19-
import java.io.ByteArrayOutputStream;
2019
import java.io.IOException;
2120
import java.nio.charset.StandardCharsets;
2221
import java.util.ArrayList;
@@ -112,33 +111,56 @@ void testBuildCliCommand_ProfileWithNullHost_ThrowsClearError() {
112111
private static Stream<Arguments> buildCliCommandCases() {
113112
return Stream.of(
114113
Arguments.of(
115-
"host only — old CLI",
114+
"host only — old CLI, no force-refresh",
116115
new DatabricksConfig().setHost(HOST),
117116
new DatabricksCliVersion(0, 200, 0),
118117
Arrays.asList(CLI_PATH, "auth", "token", "--host", HOST)),
119118
Arguments.of(
120-
"account host — old CLI",
119+
"host only — new CLI, with force-refresh",
120+
new DatabricksConfig().setHost(HOST),
121+
DatabricksCliCredentialsProvider.CLI_VERSION_FOR_FORCE_REFRESH,
122+
Arrays.asList(CLI_PATH, "auth", "token", "--host", HOST, "--force-refresh")),
123+
Arguments.of(
124+
"account host — old CLI, no force-refresh",
121125
new DatabricksConfig().setHost(ACCOUNT_HOST).setAccountId(ACCOUNT_ID),
122126
new DatabricksCliVersion(0, 200, 0),
123127
Arrays.asList(
124128
CLI_PATH, "auth", "token", "--host", ACCOUNT_HOST, "--account-id", ACCOUNT_ID)),
125129
Arguments.of(
126-
"profile with new CLI — uses --profile",
130+
"account host — new CLI, with force-refresh",
131+
new DatabricksConfig().setHost(ACCOUNT_HOST).setAccountId(ACCOUNT_ID),
132+
DatabricksCliCredentialsProvider.CLI_VERSION_FOR_FORCE_REFRESH,
133+
Arrays.asList(
134+
CLI_PATH,
135+
"auth",
136+
"token",
137+
"--host",
138+
ACCOUNT_HOST,
139+
"--account-id",
140+
ACCOUNT_ID,
141+
"--force-refresh")),
142+
Arguments.of(
143+
"profile with profile-supporting CLI — uses --profile, no force-refresh",
127144
new DatabricksConfig().setProfile(PROFILE).setHost(HOST),
128145
DatabricksCliCredentialsProvider.CLI_VERSION_FOR_PROFILE,
129146
Arrays.asList(CLI_PATH, "auth", "token", "--profile", PROFILE)),
130147
Arguments.of(
131-
"profile with old CLI — falls back to --host",
148+
"profile with newest CLI — uses --profile and --force-refresh",
149+
new DatabricksConfig().setProfile(PROFILE).setHost(HOST),
150+
DatabricksCliCredentialsProvider.CLI_VERSION_FOR_FORCE_REFRESH,
151+
Arrays.asList(CLI_PATH, "auth", "token", "--profile", PROFILE, "--force-refresh")),
152+
Arguments.of(
153+
"profile with old CLI — falls back to --host, no force-refresh",
132154
new DatabricksConfig().setProfile(PROFILE).setHost(HOST),
133155
new DatabricksCliVersion(0, 207, 0),
134156
Arrays.asList(CLI_PATH, "auth", "token", "--host", HOST)),
135157
Arguments.of(
136-
"unknown version — falls back to --host",
158+
"unknown version — falls back to --host, no force-refresh",
137159
new DatabricksConfig().setProfile(PROFILE).setHost(HOST),
138160
DatabricksCliVersion.UNKNOWN,
139161
Arrays.asList(CLI_PATH, "auth", "token", "--host", HOST)),
140162
Arguments.of(
141-
"dev build — falls back to --host",
163+
"dev build — falls back to --host, no force-refresh",
142164
new DatabricksConfig().setProfile(PROFILE).setHost(HOST),
143165
new DatabricksCliVersion(0, 0, 0),
144166
Arrays.asList(CLI_PATH, "auth", "token", "--host", HOST)));
@@ -366,7 +388,6 @@ private static Process mockProcess(String stdout, int exitCode, boolean exited)
366388
Process process = mock(Process.class);
367389
when(process.getInputStream())
368390
.thenReturn(new ByteArrayInputStream(stdout.getBytes(StandardCharsets.UTF_8)));
369-
when(process.getOutputStream()).thenReturn(new ByteArrayOutputStream());
370391
when(process.waitFor(anyLong(), any(TimeUnit.class))).thenReturn(exited);
371392
when(process.exitValue()).thenReturn(exitCode);
372393
// destroyForcibly() returns the Process so callers can chain .waitFor(...) on it.
@@ -398,7 +419,7 @@ void testProbeCliVersion_SuccessReturnsParsedVersion() throws Exception {
398419
mockConstruction(
399420
ProcessBuilder.class,
400421
(pb, ctx) -> {
401-
when(pb.redirectErrorStream(anyBoolean())).thenReturn(pb);
422+
when(pb.redirectErrorStream(eq(true))).thenReturn(pb);
402423
when(pb.start()).thenReturn(process);
403424
})) {
404425
mockedOSUtils.when(() -> OSUtils.get(any())).thenReturn(osUtils);
@@ -427,7 +448,7 @@ void testProbeCliVersion_TimeoutThrowsAndDestroys() throws Exception {
427448
mockConstruction(
428449
ProcessBuilder.class,
429450
(pb, ctx) -> {
430-
when(pb.redirectErrorStream(anyBoolean())).thenReturn(pb);
451+
when(pb.redirectErrorStream(eq(true))).thenReturn(pb);
431452
when(pb.start()).thenReturn(process);
432453
})) {
433454
mockedOSUtils.when(() -> OSUtils.get(any())).thenReturn(osUtils);
@@ -451,7 +472,7 @@ void testProbeCliVersion_NonZeroExitSurfacesOutput() throws Exception {
451472
mockConstruction(
452473
ProcessBuilder.class,
453474
(pb, ctx) -> {
454-
when(pb.redirectErrorStream(anyBoolean())).thenReturn(pb);
475+
when(pb.redirectErrorStream(eq(true))).thenReturn(pb);
455476
when(pb.start()).thenReturn(process);
456477
})) {
457478
mockedOSUtils.when(() -> OSUtils.get(any())).thenReturn(osUtils);

0 commit comments

Comments
 (0)