Skip to content

Add --force-refresh support for Databricks CLI token fetching#751

Open
mihaimitrea-db wants to merge 1 commit intomainfrom
mihaimitrea-db/stack/cli-force-refresh
Open

Add --force-refresh support for Databricks CLI token fetching#751
mihaimitrea-db wants to merge 1 commit intomainfrom
mihaimitrea-db/stack/cli-force-refresh

Conversation

@mihaimitrea-db
Copy link
Copy Markdown
Contributor

@mihaimitrea-db mihaimitrea-db commented Mar 31, 2026

🥞 Stacked PR

Use this link to review incremental changes.


Summary

Pass --force-refresh to the Databricks CLI auth token command so the SDK always receives a fresh token instead of a potentially stale one from the CLI's internal cache.

Why

The SDK manages its own token caching via CachedTokenSource. When the SDK decides it needs a new token and shells out to databricks auth token, the CLI may return a cached token that is about to expire (or has already expired from the SDK's perspective). This creates unnecessary refresh failures and retry loops.

The CLI recently added a --force-refresh flag (databricks/cli#4767) that bypasses its internal cache. By using this flag, the SDK is guaranteed a freshly minted token every time it asks for one, eliminating the stale-token problem.

What changed

Interface changes

None. CliTokenSource is not part of the public API surface.

Behavioral changes

  • The SDK now passes --force-refresh when invoking databricks auth token. If the CLI is too old to support this flag, the SDK falls back to the plain --profile command (and then to --host if --profile is also unsupported).
  • New warning message logged when falling back:
    • "Databricks CLI does not support --force-refresh flag. Falling back to regular token fetch. Please upgrade your CLI to the latest version."
    • The existing --profile fallback warning is unchanged.

Internal changes

  • CliTokenSource now holds three command variants: forceCmd (--profile + --force-refresh), profileCmd (--profile), and fallbackCmd (--host). Any of these can be null depending on the config.
  • getDatabricksCliTokenSource in DatabricksCliCredentialsProvider builds all three commands. forceCmd is always built as profileCmd + --force-refresh.
  • getToken() tries commands in order (forceCmd -> profileCmd -> fallbackCmd), falling through on "unknown flag:" errors and logging the appropriate warning at each step.
  • isUnknownFlagError takes a bare flag name (e.g. "--profile") and prepends "unknown flag: " internally for matching.
  • Azure CLI callers are unchanged; they use constructors that leave forceCmd null.

How is this tested?

Unit tests in CliTokenSourceTest:

  • testForceCmdSucceedsAndProfileCmdNotRun -- forceCmd succeeds, no further commands are tried.
  • testForceCmdFailsWithUnknownForceRefreshFallsBackToProfileCmd -- forceCmd fails with "unknown flag: --force-refresh", verifies fallback to profileCmd.
  • testForceCmdFailsWithUnknownProfileFallsThroughToFallbackCmd -- forceCmd fails with "unknown flag: --profile" (very old CLI), verifies fallback cascades through profileCmd to fallbackCmd.
  • testForceCmdRealAuthErrorDoesNotFallBack -- non-flag error from forceCmd is returned immediately without fallback.
  • testNullForceCmdPreservesExistingBehavior -- forceCmd is null, existing profileCmd -> fallbackCmd chain is preserved.

Unit tests in DatabricksCliCredentialsProviderTest:

  • testBuildProfileArgs -- verifies profile command construction.

@mihaimitrea-db
Copy link
Copy Markdown
Contributor Author

Range-diff: main (048a903 -> 5e8f476)
NEXT_CHANGELOG.md
@@ -0,0 +1,10 @@
+diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md
+--- a/NEXT_CHANGELOG.md
++++ b/NEXT_CHANGELOG.md
+ 
+ ### New Features and Improvements
+ * Added automatic detection of AI coding agents (Antigravity, Claude Code, Cline, Codex, Copilot CLI, Cursor, Gemini CLI, OpenCode) in the user-agent string. The SDK now appends `agent/<name>` to HTTP request headers when running inside a known AI agent environment.
++* Pass `--force-refresh` to Databricks CLI `auth token` command so the SDK always receives a fresh token instead of a potentially stale one from the CLI's internal cache. Falls back gracefully on older CLIs that do not support this flag.
+ 
+ ### Bug Fixes
+ * Fixed Databricks CLI authentication to detect when the cached token's scopes don't match the SDK's configured scopes. Previously, a scope mismatch was silently ignored, causing requests to use wrong permissions. The SDK now raises an error with instructions to re-authenticate.
\ No newline at end of file
databricks-sdk-java/src/main/java/com/databricks/sdk/core/CliTokenSource.java
@@ -57,8 +57,7 @@
      this.env = env;
      this.fallbackCmd =
          fallbackCmd != null ? OSUtils.get(env).getCliExecutableCommand(fallbackCmd) : null;
-+    this.forceCmd =
-+        forceCmd != null ? OSUtils.get(env).getCliExecutableCommand(forceCmd) : null;
++    this.forceCmd = forceCmd != null ? OSUtils.get(env).getCliExecutableCommand(forceCmd) : null;
    }
  
    /**
databricks-sdk-java/src/test/java/com/databricks/sdk/core/CliTokenSourceTest.java
@@ -50,8 +50,7 @@
 +            (pb, context) -> {
 +              Process successProcess = mock(Process.class);
 +              when(successProcess.getInputStream())
-+                  .thenReturn(
-+                      new ByteArrayInputStream(validTokenJson("forced-token").getBytes()));
++                  .thenReturn(new ByteArrayInputStream(validTokenJson("forced-token").getBytes()));
 +              when(successProcess.getErrorStream())
 +                  .thenReturn(new ByteArrayInputStream(new byte[0]));
 +              when(successProcess.waitFor()).thenReturn(0);
@@ -136,15 +135,13 @@
 +                    .thenReturn(new ByteArrayInputStream(new byte[0]));
 +                when(failProcess.getErrorStream())
 +                    .thenReturn(
-+                        new ByteArrayInputStream(
-+                            "Error: unknown flag: --profile".getBytes()));
++                        new ByteArrayInputStream("Error: unknown flag: --profile".getBytes()));
 +                when(failProcess.waitFor()).thenReturn(1);
 +                when(pb.start()).thenReturn(failProcess);
 +              } else {
 +                Process successProcess = mock(Process.class);
 +                when(successProcess.getInputStream())
-+                    .thenReturn(
-+                        new ByteArrayInputStream(validTokenJson("host-token").getBytes()));
++                    .thenReturn(new ByteArrayInputStream(validTokenJson("host-token").getBytes()));
 +                when(successProcess.getErrorStream())
 +                    .thenReturn(new ByteArrayInputStream(new byte[0]));
 +                when(successProcess.waitFor()).thenReturn(0);
@@ -164,8 +161,7 @@
 +
 +    List<String> forceCmd =
 +        Arrays.asList("databricks", "auth", "token", "--host", "https://h", "--force-refresh");
-+    List<String> profileCmd =
-+        Arrays.asList("databricks", "auth", "token", "--host", "https://h");
++    List<String> profileCmd = Arrays.asList("databricks", "auth", "token", "--host", "https://h");
 +
 +    CliTokenSource tokenSource = makeTokenSource(env, profileCmd, null, forceCmd);
 +

Reproduce locally: git range-diff f28430b..048a903 f28430b..5e8f476 | Disable: git config gitstack.push-range-diff false

@mihaimitrea-db mihaimitrea-db force-pushed the mihaimitrea-db/stack/cli-force-refresh branch from 5e8f476 to 6b8a57f Compare March 31, 2026 14:04
@mihaimitrea-db
Copy link
Copy Markdown
Contributor Author

Range-diff: main (5e8f476 -> 6b8a57f)
databricks-sdk-java/src/main/java/com/databricks/sdk/core/CliTokenSource.java
@@ -5,9 +5,6 @@
    private static final Logger LOG = LoggerFactory.getLogger(CliTokenSource.class);
  
 -  private List<String> cmd;
-+  private static final String UNKNOWN_PROFILE_FLAG = "unknown flag: --profile";
-+  private static final String UNKNOWN_FORCE_REFRESH_FLAG = "unknown flag: --force-refresh";
-+
 +  // forceCmd is tried before profileCmd when non-null. If the CLI rejects
 +  // --force-refresh or --profile, execution falls through to profileCmd.
 +  private List<String> forceCmd;
@@ -73,7 +70,7 @@
 +  }
 +
 +  private boolean isUnknownFlagError(String errorText, String flag) {
-+    return errorText != null && errorText.contains(flag);
++    return errorText != null && errorText.contains("unknown flag: " + flag);
 +  }
 +
 +  private Token execProfileCmdWithFallback() {
@@ -89,7 +86,7 @@
 -          && textToCheck != null
 -          && textToCheck.contains("unknown flag: --profile")) {
 +      String textToCheck = getErrorText(e);
-+      if (fallbackCmd != null && isUnknownFlagError(textToCheck, UNKNOWN_PROFILE_FLAG)) {
++      if (fallbackCmd != null && isUnknownFlagError(textToCheck, "--profile")) {
          LOG.warn(
              "Databricks CLI does not support --profile flag. Falling back to --host. "
                  + "Please upgrade your CLI to the latest version.");
@@ -107,8 +104,8 @@
 +      return execCliCommand(this.forceCmd);
 +    } catch (IOException e) {
 +      String textToCheck = getErrorText(e);
-+      if (isUnknownFlagError(textToCheck, UNKNOWN_FORCE_REFRESH_FLAG)
-+          || isUnknownFlagError(textToCheck, UNKNOWN_PROFILE_FLAG)) {
++      if (isUnknownFlagError(textToCheck, "--force-refresh")
++          || isUnknownFlagError(textToCheck, "--profile")) {
 +        LOG.warn(
 +            "Databricks CLI does not support --force-refresh flag. "
 +                + "Falling back to regular token fetch. "

Reproduce locally: git range-diff f28430b..5e8f476 f28430b..6b8a57f | Disable: git config gitstack.push-range-diff false

@mihaimitrea-db mihaimitrea-db force-pushed the mihaimitrea-db/stack/cli-force-refresh branch from 6b8a57f to 61686da Compare March 31, 2026 16:03
@mihaimitrea-db
Copy link
Copy Markdown
Contributor Author

Range-diff: main (6b8a57f -> 61686da)
databricks-sdk-java/src/main/java/com/databricks/sdk/core/CliTokenSource.java
@@ -1,15 +1,11 @@
 diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/CliTokenSource.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/CliTokenSource.java
 --- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/CliTokenSource.java
 +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/CliTokenSource.java
- public class CliTokenSource implements TokenSource {
    private static final Logger LOG = LoggerFactory.getLogger(CliTokenSource.class);
  
--  private List<String> cmd;
-+  // forceCmd is tried before profileCmd when non-null. If the CLI rejects
-+  // --force-refresh or --profile, execution falls through to profileCmd.
-+  private List<String> forceCmd;
-+
-+  private List<String> profileCmd;
+   private List<String> cmd;
++  private List<String> fallbackCmd;
++  private List<String> secondFallbackCmd;
    private String tokenTypeField;
    private String accessTokenField;
    private String expiryField;
@@ -18,11 +14,10 @@
 -  // indicating the CLI is too old to support --profile. Can be removed once support
 -  // for CLI versions predating --profile is dropped.
 -  // See: https://github.com/databricks/databricks-sdk-go/pull/1497
-+  // fallbackCmd is tried when profileCmd fails with "unknown flag: --profile",
-+  // indicating the CLI is too old to support --profile.
-   private List<String> fallbackCmd;
+-  private List<String> fallbackCmd;
  
    /**
+    * Internal exception that carries the clean stderr message but exposes full output for checks.
        String accessTokenField,
        String expiryField,
        Environment env) {
@@ -31,52 +26,43 @@
    }
  
    public CliTokenSource(
+       String accessTokenField,
        String expiryField,
        Environment env,
-       List<String> fallbackCmd) {
-+    this(cmd, tokenTypeField, accessTokenField, expiryField, env, fallbackCmd, null);
-+  }
-+
-+  public CliTokenSource(
-+      List<String> cmd,
-+      String tokenTypeField,
-+      String accessTokenField,
-+      String expiryField,
-+      Environment env,
+-      List<String> fallbackCmd) {
+-    super();
 +      List<String> fallbackCmd,
-+      List<String> forceCmd) {
-     super();
--    this.cmd = OSUtils.get(env).getCliExecutableCommand(cmd);
-+    this.profileCmd = OSUtils.get(env).getCliExecutableCommand(cmd);
++      List<String> secondFallbackCmd) {
+     this.cmd = OSUtils.get(env).getCliExecutableCommand(cmd);
      this.tokenTypeField = tokenTypeField;
      this.accessTokenField = accessTokenField;
-     this.expiryField = expiryField;
      this.env = env;
      this.fallbackCmd =
          fallbackCmd != null ? OSUtils.get(env).getCliExecutableCommand(fallbackCmd) : null;
-+    this.forceCmd = forceCmd != null ? OSUtils.get(env).getCliExecutableCommand(forceCmd) : null;
++    this.secondFallbackCmd =
++        secondFallbackCmd != null
++            ? OSUtils.get(env).getCliExecutableCommand(secondFallbackCmd)
++            : null;
    }
  
    /**
      }
    }
  
--  @Override
--  public Token getToken() {
 +  private String getErrorText(IOException e) {
 +    return e instanceof CliCommandException
 +        ? ((CliCommandException) e).getFullOutput()
 +        : e.getMessage();
 +  }
 +
-+  private boolean isUnknownFlagError(String errorText, String flag) {
-+    return errorText != null && errorText.contains("unknown flag: " + flag);
++  private boolean isUnknownFlagError(String errorText) {
++    return errorText != null && errorText.contains("unknown flag:");
 +  }
 +
-+  private Token execProfileCmdWithFallback() {
+   @Override
+   public Token getToken() {
      try {
--      return execCliCommand(this.cmd);
-+      return execCliCommand(this.profileCmd);
+       return execCliCommand(this.cmd);
      } catch (IOException e) {
 -      String textToCheck =
 -          e instanceof CliCommandException
@@ -85,34 +71,38 @@
 -      if (fallbackCmd != null
 -          && textToCheck != null
 -          && textToCheck.contains("unknown flag: --profile")) {
-+      String textToCheck = getErrorText(e);
-+      if (fallbackCmd != null && isUnknownFlagError(textToCheck, "--profile")) {
++      if (fallbackCmd != null && isUnknownFlagError(getErrorText(e))) {
          LOG.warn(
-             "Databricks CLI does not support --profile flag. Falling back to --host. "
+-            "Databricks CLI does not support --profile flag. Falling back to --host. "
++            "CLI does not support some flags used by this SDK. "
++                + "Falling back to a compatible command. "
                  + "Please upgrade your CLI to the latest version.");
-       throw new DatabricksException(e.getMessage(), e);
-     }
-   }
-+
-+  @Override
-+  public Token getToken() {
-+    if (forceCmd == null) {
-+      return execProfileCmdWithFallback();
+-        try {
+-          return execCliCommand(this.fallbackCmd);
+-        } catch (IOException fallbackException) {
+-          throw new DatabricksException(fallbackException.getMessage(), fallbackException);
+-        }
++      } else {
++        throw new DatabricksException(e.getMessage(), e);
++      }
 +    }
 +
 +    try {
-+      return execCliCommand(this.forceCmd);
++      return execCliCommand(this.fallbackCmd);
 +    } catch (IOException e) {
-+      String textToCheck = getErrorText(e);
-+      if (isUnknownFlagError(textToCheck, "--force-refresh")
-+          || isUnknownFlagError(textToCheck, "--profile")) {
++      if (secondFallbackCmd != null && isUnknownFlagError(getErrorText(e))) {
 +        LOG.warn(
-+            "Databricks CLI does not support --force-refresh flag. "
-+                + "Falling back to regular token fetch. "
++            "CLI does not support some flags used by this SDK. "
++                + "Falling back to a compatible command. "
 +                + "Please upgrade your CLI to the latest version.");
-+        return execProfileCmdWithFallback();
-+      }
-+      throw new DatabricksException(e.getMessage(), e);
++      } else {
++        throw new DatabricksException(e.getMessage(), e);
+       }
 +    }
-+  }
- }
\ No newline at end of file
++
++    try {
++      return execCliCommand(this.secondFallbackCmd);
++    } catch (IOException e) {
+       throw new DatabricksException(e.getMessage(), e);
+     }
+   }
\ No newline at end of file
databricks-sdk-java/src/main/java/com/databricks/sdk/core/DatabricksCliCredentialsProvider.java
@@ -18,13 +18,10 @@
    private CliTokenSource getDatabricksCliTokenSource(DatabricksConfig config) {
      String cliPath = config.getDatabricksCliPath();
      if (cliPath == null) {
-       return null;
-     }
  
--    List<String> cmd;
-+    List<String> profileCmd;
+     List<String> cmd;
      List<String> fallbackCmd = null;
-+    List<String> forceCmd;
++    List<String> secondFallbackCmd = null;
  
      if (config.getProfile() != null) {
 -      // When profile is set, use --profile as the primary command.
@@ -33,20 +30,26 @@
 -          new ArrayList<>(
 -              Arrays.asList(cliPath, "auth", "token", "--profile", config.getProfile()));
 -      // Build a --host fallback for older CLIs that don't support --profile.
-+      profileCmd = buildProfileArgs(cliPath, config);
-+      forceCmd = withForceRefresh(profileCmd);
++      List<String> profileArgs = buildProfileArgs(cliPath, config);
++      cmd = withForceRefresh(profileArgs);
++      fallbackCmd = profileArgs;
        if (config.getHost() != null) {
-         fallbackCmd = buildHostArgs(cliPath, config);
+-        fallbackCmd = buildHostArgs(cliPath, config);
++        secondFallbackCmd = buildHostArgs(cliPath, config);
        }
      } else {
--      cmd = buildHostArgs(cliPath, config);
-+      profileCmd = buildHostArgs(cliPath, config);
-+      forceCmd = withForceRefresh(profileCmd);
+       cmd = buildHostArgs(cliPath, config);
      }
  
      return new CliTokenSource(
 -        cmd, "token_type", "access_token", "expiry", config.getEnv(), fallbackCmd);
-+        profileCmd, "token_type", "access_token", "expiry", config.getEnv(), fallbackCmd, forceCmd);
++        cmd,
++        "token_type",
++        "access_token",
++        "expiry",
++        config.getEnv(),
++        fallbackCmd,
++        secondFallbackCmd);
    }
  
    @Override
\ No newline at end of file
databricks-sdk-java/src/test/java/com/databricks/sdk/core/CliTokenSourceTest.java
@@ -8,19 +8,20 @@
 +  // ---- Fallback tests for --profile and --force-refresh flag handling ----
  
    private CliTokenSource makeTokenSource(
-       Environment env, List<String> primaryCmd, List<String> fallbackCmd) {
-+    return makeTokenSource(env, primaryCmd, fallbackCmd, null);
+-      Environment env, List<String> primaryCmd, List<String> fallbackCmd) {
++      Environment env, List<String> cmd, List<String> fallbackCmd) {
++    return makeTokenSource(env, cmd, fallbackCmd, null);
 +  }
 +
 +  private CliTokenSource makeTokenSource(
-+      Environment env, List<String> primaryCmd, List<String> fallbackCmd, List<String> forceCmd) {
++      Environment env, List<String> cmd, List<String> fallbackCmd, List<String> secondFallbackCmd) {
      OSUtilities osUtils = mock(OSUtilities.class);
      when(osUtils.getCliExecutableCommand(any())).thenAnswer(inv -> inv.getArgument(0));
      try (MockedStatic<OSUtils> mockedOSUtils = mockStatic(OSUtils.class)) {
        mockedOSUtils.when(() -> OSUtils.get(any())).thenReturn(osUtils);
        return new CliTokenSource(
 -          primaryCmd, "token_type", "access_token", "expiry", env, fallbackCmd);
-+          primaryCmd, "token_type", "access_token", "expiry", env, fallbackCmd, forceCmd);
++          cmd, "token_type", "access_token", "expiry", env, fallbackCmd, secondFallbackCmd);
      }
    }
  
@@ -31,18 +32,18 @@
 +  // ---- Force-refresh tests ----
 +
 +  @Test
-+  public void testForceCmdSucceedsAndProfileCmdNotRun() {
++  public void testForceCmdSucceedsAndFallbacksNotRun() {
 +    Environment env = mock(Environment.class);
 +    when(env.getEnv()).thenReturn(new HashMap<>());
 +
-+    List<String> forceCmd =
++    List<String> cmd =
 +        Arrays.asList("databricks", "auth", "token", "--profile", "my-profile", "--force-refresh");
-+    List<String> profileCmd =
++    List<String> fallbackCmdList =
 +        Arrays.asList("databricks", "auth", "token", "--profile", "my-profile");
-+    List<String> fallbackCmd =
++    List<String> secondFallbackCmdList =
 +        Arrays.asList("databricks", "auth", "token", "--host", "https://workspace.databricks.com");
 +
-+    CliTokenSource tokenSource = makeTokenSource(env, profileCmd, fallbackCmd, forceCmd);
++    CliTokenSource tokenSource = makeTokenSource(env, cmd, fallbackCmdList, secondFallbackCmdList);
 +
 +    try (MockedConstruction<ProcessBuilder> mocked =
 +        mockConstruction(
@@ -63,16 +64,16 @@
 +  }
 +
 +  @Test
-+  public void testForceCmdFailsWithUnknownForceRefreshFallsBackToProfileCmd() {
++  public void testCmdFailsWithUnknownFlagFallsBackToFallbackCmd() {
 +    Environment env = mock(Environment.class);
 +    when(env.getEnv()).thenReturn(new HashMap<>());
 +
-+    List<String> forceCmd =
++    List<String> cmd =
 +        Arrays.asList("databricks", "auth", "token", "--profile", "my-profile", "--force-refresh");
-+    List<String> profileCmd =
++    List<String> fallbackCmdList =
 +        Arrays.asList("databricks", "auth", "token", "--profile", "my-profile");
 +
-+    CliTokenSource tokenSource = makeTokenSource(env, profileCmd, null, forceCmd);
++    CliTokenSource tokenSource = makeTokenSource(env, cmd, fallbackCmdList);
 +
 +    AtomicInteger callCount = new AtomicInteger(0);
 +    try (MockedConstruction<ProcessBuilder> mocked =
@@ -107,20 +108,20 @@
 +  }
 +
 +  @Test
-+  public void testForceCmdFailsWithUnknownProfileFallsThroughToFallbackCmd() {
-+    // Very old CLI: forceCmd has --profile --force-refresh but CLI doesn't know --profile.
-+    // Should fall through to profileCmd (which also fails), then to fallbackCmd (--host).
++  public void testCmdAndFallbackBothFailFallsThroughToSecondFallback() {
++    // Very old CLI: cmd has --profile --force-refresh but CLI doesn't know --profile.
++    // Should fall through to fallbackCmd (which also fails), then to secondFallbackCmd (--host).
 +    Environment env = mock(Environment.class);
 +    when(env.getEnv()).thenReturn(new HashMap<>());
 +
-+    List<String> forceCmd =
++    List<String> cmd =
 +        Arrays.asList("databricks", "auth", "token", "--profile", "my-profile", "--force-refresh");
-+    List<String> profileCmd =
++    List<String> fallbackCmdList =
 +        Arrays.asList("databricks", "auth", "token", "--profile", "my-profile");
-+    List<String> fallbackCmd =
++    List<String> secondFallbackCmdList =
 +        Arrays.asList("databricks", "auth", "token", "--host", "https://workspace.databricks.com");
 +
-+    CliTokenSource tokenSource = makeTokenSource(env, profileCmd, fallbackCmd, forceCmd);
++    CliTokenSource tokenSource = makeTokenSource(env, cmd, fallbackCmdList, secondFallbackCmdList);
 +
 +    AtomicInteger callCount = new AtomicInteger(0);
 +    try (MockedConstruction<ProcessBuilder> mocked =
@@ -155,15 +156,16 @@
 +  }
 +
 +  @Test
-+  public void testForceCmdRealAuthErrorDoesNotFallBack() {
++  public void testRealAuthErrorDoesNotFallBack() {
 +    Environment env = mock(Environment.class);
 +    when(env.getEnv()).thenReturn(new HashMap<>());
 +
-+    List<String> forceCmd =
-+        Arrays.asList("databricks", "auth", "token", "--host", "https://h", "--force-refresh");
-+    List<String> profileCmd = Arrays.asList("databricks", "auth", "token", "--host", "https://h");
++    List<String> cmd =
++        Arrays.asList("databricks", "auth", "token", "--profile", "my-profile", "--force-refresh");
++    List<String> fallbackCmdList =
++        Arrays.asList("databricks", "auth", "token", "--profile", "my-profile");
 +
-+    CliTokenSource tokenSource = makeTokenSource(env, profileCmd, null, forceCmd);
++    CliTokenSource tokenSource = makeTokenSource(env, cmd, fallbackCmdList);
 +
 +    try (MockedConstruction<ProcessBuilder> mocked =
 +        mockConstruction(
@@ -185,17 +187,16 @@
 +  }
 +
 +  @Test
-+  public void testNullForceCmdPreservesExistingBehavior() {
-+    // When forceCmd is null, behaves exactly like before: profileCmd -> fallbackCmd.
++  public void testTwoLevelFallbackWithNoSecondFallback() {
++    // cmd -> fallbackCmd chain with no secondFallbackCmd.
 +    Environment env = mock(Environment.class);
 +    when(env.getEnv()).thenReturn(new HashMap<>());
 +
-+    List<String> profileCmd =
-+        Arrays.asList("databricks", "auth", "token", "--profile", "my-profile");
-+    List<String> fallbackCmd =
++    List<String> cmd = Arrays.asList("databricks", "auth", "token", "--profile", "my-profile");
++    List<String> fallbackCmdList =
 +        Arrays.asList("databricks", "auth", "token", "--host", "https://workspace.databricks.com");
 +
-+    CliTokenSource tokenSource = makeTokenSource(env, profileCmd, fallbackCmd, null);
++    CliTokenSource tokenSource = makeTokenSource(env, cmd, fallbackCmdList);
 +
 +    AtomicInteger callCount = new AtomicInteger(0);
 +    try (MockedConstruction<ProcessBuilder> mocked =

Reproduce locally: git range-diff f28430b..6b8a57f f28430b..61686da | Disable: git config gitstack.push-range-diff false

Try `--force-refresh` before the regular CLI command so the SDK can
bypass the CLI's own token cache when the SDK considers its token stale.
If the CLI is too old to recognise `--force-refresh` (or `--profile`),
gracefully fall back to the next command in the chain.

Chain order:
- with profile: forceCmd (--profile --force-refresh) -> profileCmd (--profile) -> fallbackCmd (--host)
- without profile: forceCmd (--host --force-refresh) -> profileCmd (--host)

Azure CLI callers are unchanged; they use constructors that leave
forceCmd null, preserving existing behavior.

Signed-off-by: Mihai Mitrea <mihai.mitrea@databricks.com>
@mihaimitrea-db mihaimitrea-db force-pushed the mihaimitrea-db/stack/cli-force-refresh branch from 61686da to 1c68e85 Compare April 1, 2026 07:59
@mihaimitrea-db
Copy link
Copy Markdown
Contributor Author

Range-diff: main (61686da -> 1c68e85)
databricks-sdk-java/src/test/java/com/databricks/sdk/core/CliTokenSourceTest.java
@@ -1,6 +1,19 @@
 diff --git a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/CliTokenSourceTest.java b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/CliTokenSourceTest.java
 --- a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/CliTokenSourceTest.java
 +++ b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/CliTokenSourceTest.java
+ import org.mockito.MockedStatic;
+ 
+ public class CliTokenSourceTest {
++  private static final List<String> FORCE_CMD =
++      Arrays.asList("databricks", "auth", "token", "--profile", "my-profile", "--force-refresh");
++  private static final List<String> PROFILE_CMD =
++      Arrays.asList("databricks", "auth", "token", "--profile", "my-profile");
++  private static final List<String> HOST_CMD =
++      Arrays.asList("databricks", "auth", "token", "--host", "https://workspace.databricks.com");
++
+   String getExpiryStr(String dateFormat, Duration offset) {
+     ZonedDateTime futureExpiry = ZonedDateTime.now().plus(offset);
+     return futureExpiry.format(DateTimeFormatter.ofPattern(dateFormat));
      }
    }
  
@@ -25,6 +38,68 @@
      }
    }
  
+     Environment env = mock(Environment.class);
+     when(env.getEnv()).thenReturn(new HashMap<>());
+ 
+-    List<String> primaryCmd =
+-        Arrays.asList("databricks", "auth", "token", "--profile", "my-profile");
+-    List<String> fallbackCmdList =
+-        Arrays.asList("databricks", "auth", "token", "--host", "https://workspace.databricks.com");
+-
+-    CliTokenSource tokenSource = makeTokenSource(env, primaryCmd, fallbackCmdList);
++    CliTokenSource tokenSource = makeTokenSource(env, PROFILE_CMD, HOST_CMD);
+ 
+     AtomicInteger callCount = new AtomicInteger(0);
+     try (MockedConstruction<ProcessBuilder> mocked =
+ 
+   @Test
+   public void testFallbackTriggeredWhenUnknownFlagInStdout() {
+-    // Fallback triggers even when "unknown flag" appears in stdout rather than stderr.
+     Environment env = mock(Environment.class);
+     when(env.getEnv()).thenReturn(new HashMap<>());
+ 
+-    List<String> primaryCmd =
+-        Arrays.asList("databricks", "auth", "token", "--profile", "my-profile");
+-    List<String> fallbackCmdList =
+-        Arrays.asList("databricks", "auth", "token", "--host", "https://workspace.databricks.com");
+-
+-    CliTokenSource tokenSource = makeTokenSource(env, primaryCmd, fallbackCmdList);
++    CliTokenSource tokenSource = makeTokenSource(env, PROFILE_CMD, HOST_CMD);
+ 
+     AtomicInteger callCount = new AtomicInteger(0);
+     try (MockedConstruction<ProcessBuilder> mocked =
+ 
+   @Test
+   public void testNoFallbackOnRealAuthError() {
+-    // When the primary fails with a real error (not unknown flag), no fallback is attempted.
+     Environment env = mock(Environment.class);
+     when(env.getEnv()).thenReturn(new HashMap<>());
+ 
+-    List<String> primaryCmd =
+-        Arrays.asList("databricks", "auth", "token", "--profile", "my-profile");
+-    List<String> fallbackCmdList =
+-        Arrays.asList("databricks", "auth", "token", "--host", "https://workspace.databricks.com");
+-
+-    CliTokenSource tokenSource = makeTokenSource(env, primaryCmd, fallbackCmdList);
++    CliTokenSource tokenSource = makeTokenSource(env, PROFILE_CMD, HOST_CMD);
+ 
+     try (MockedConstruction<ProcessBuilder> mocked =
+         mockConstruction(
+ 
+   @Test
+   public void testNoFallbackWhenFallbackCmdNotSet() {
+-    // When fallbackCmd is null and the primary fails with unknown flag, original error propagates.
+     Environment env = mock(Environment.class);
+     when(env.getEnv()).thenReturn(new HashMap<>());
+ 
+-    List<String> primaryCmd =
+-        Arrays.asList("databricks", "auth", "token", "--profile", "my-profile");
+-
+-    CliTokenSource tokenSource = makeTokenSource(env, primaryCmd, null);
++    CliTokenSource tokenSource = makeTokenSource(env, PROFILE_CMD, null);
+ 
+     try (MockedConstruction<ProcessBuilder> mocked =
+         mockConstruction(
        assertEquals(1, mocked.constructed().size());
      }
    }
@@ -36,14 +111,7 @@
 +    Environment env = mock(Environment.class);
 +    when(env.getEnv()).thenReturn(new HashMap<>());
 +
-+    List<String> cmd =
-+        Arrays.asList("databricks", "auth", "token", "--profile", "my-profile", "--force-refresh");
-+    List<String> fallbackCmdList =
-+        Arrays.asList("databricks", "auth", "token", "--profile", "my-profile");
-+    List<String> secondFallbackCmdList =
-+        Arrays.asList("databricks", "auth", "token", "--host", "https://workspace.databricks.com");
-+
-+    CliTokenSource tokenSource = makeTokenSource(env, cmd, fallbackCmdList, secondFallbackCmdList);
++    CliTokenSource tokenSource = makeTokenSource(env, FORCE_CMD, PROFILE_CMD, HOST_CMD);
 +
 +    try (MockedConstruction<ProcessBuilder> mocked =
 +        mockConstruction(
@@ -68,12 +136,7 @@
 +    Environment env = mock(Environment.class);
 +    when(env.getEnv()).thenReturn(new HashMap<>());
 +
-+    List<String> cmd =
-+        Arrays.asList("databricks", "auth", "token", "--profile", "my-profile", "--force-refresh");
-+    List<String> fallbackCmdList =
-+        Arrays.asList("databricks", "auth", "token", "--profile", "my-profile");
-+
-+    CliTokenSource tokenSource = makeTokenSource(env, cmd, fallbackCmdList);
++    CliTokenSource tokenSource = makeTokenSource(env, FORCE_CMD, PROFILE_CMD);
 +
 +    AtomicInteger callCount = new AtomicInteger(0);
 +    try (MockedConstruction<ProcessBuilder> mocked =
@@ -109,19 +172,10 @@
 +
 +  @Test
 +  public void testCmdAndFallbackBothFailFallsThroughToSecondFallback() {
-+    // Very old CLI: cmd has --profile --force-refresh but CLI doesn't know --profile.
-+    // Should fall through to fallbackCmd (which also fails), then to secondFallbackCmd (--host).
 +    Environment env = mock(Environment.class);
 +    when(env.getEnv()).thenReturn(new HashMap<>());
 +
-+    List<String> cmd =
-+        Arrays.asList("databricks", "auth", "token", "--profile", "my-profile", "--force-refresh");
-+    List<String> fallbackCmdList =
-+        Arrays.asList("databricks", "auth", "token", "--profile", "my-profile");
-+    List<String> secondFallbackCmdList =
-+        Arrays.asList("databricks", "auth", "token", "--host", "https://workspace.databricks.com");
-+
-+    CliTokenSource tokenSource = makeTokenSource(env, cmd, fallbackCmdList, secondFallbackCmdList);
++    CliTokenSource tokenSource = makeTokenSource(env, FORCE_CMD, PROFILE_CMD, HOST_CMD);
 +
 +    AtomicInteger callCount = new AtomicInteger(0);
 +    try (MockedConstruction<ProcessBuilder> mocked =
@@ -160,12 +214,7 @@
 +    Environment env = mock(Environment.class);
 +    when(env.getEnv()).thenReturn(new HashMap<>());
 +
-+    List<String> cmd =
-+        Arrays.asList("databricks", "auth", "token", "--profile", "my-profile", "--force-refresh");
-+    List<String> fallbackCmdList =
-+        Arrays.asList("databricks", "auth", "token", "--profile", "my-profile");
-+
-+    CliTokenSource tokenSource = makeTokenSource(env, cmd, fallbackCmdList);
++    CliTokenSource tokenSource = makeTokenSource(env, FORCE_CMD, PROFILE_CMD);
 +
 +    try (MockedConstruction<ProcessBuilder> mocked =
 +        mockConstruction(
@@ -188,15 +237,10 @@
 +
 +  @Test
 +  public void testTwoLevelFallbackWithNoSecondFallback() {
-+    // cmd -> fallbackCmd chain with no secondFallbackCmd.
 +    Environment env = mock(Environment.class);
 +    when(env.getEnv()).thenReturn(new HashMap<>());
 +
-+    List<String> cmd = Arrays.asList("databricks", "auth", "token", "--profile", "my-profile");
-+    List<String> fallbackCmdList =
-+        Arrays.asList("databricks", "auth", "token", "--host", "https://workspace.databricks.com");
-+
-+    CliTokenSource tokenSource = makeTokenSource(env, cmd, fallbackCmdList);
++    CliTokenSource tokenSource = makeTokenSource(env, PROFILE_CMD, HOST_CMD);
 +
 +    AtomicInteger callCount = new AtomicInteger(0);
 +    try (MockedConstruction<ProcessBuilder> mocked =

Reproduce locally: git range-diff f28430b..61686da f28430b..1c68e85 | Disable: git config gitstack.push-range-diff false

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 1, 2026

If integration tests don't run automatically, an authorized user can run them manually by following the instructions below:

Trigger:
go/deco-tests-run/sdk-java

Inputs:

  • PR number: 751
  • Commit SHA: 1c68e85d843476f27b1b152501028359b19a0c94

Checks will be approved automatically on success.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant