Skip to content

Fix CLI token source --profile fallback with version detection#751

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

Fix CLI token source --profile fallback with version detection#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

Replace the broken error-based --profile fallback in CliTokenSource with version-based CLI detection at init time. Mirrors databricks/databricks-sdk-go#1605 and databricks/databricks-sdk-py#1377.

Why

--profile on databricks auth token is a global flag, so old CLIs (< v0.207.1) silently accept it and then fail with "cannot fetch credentials" instead of "unknown flag: --profile". The existing retry check was matching on the latter and never fired — the --host fallback it gated was effectively dead code. Switching to databricks version + a minimum-version constant makes the fallback reliable and sets up future capability-gated flags (e.g. --force-refresh in #752) without additional subprocess calls.

What changed

Interface changes

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

Behavioral changes

  • cfg.profile + CLI < v0.207.1 now correctly falls back to --host (previously broken).
  • databricks version failures log a WARNING and fall back to the most conservative command. Successful detections are cached per CLI path; failures are not cached and will be retried on the next call.
  • A default dev build (v0.0.0-dev) logs an INFO explaining why feature gates are conservative.

AzureCliCredentialsProvider is untouched.

Internal changes

  • New DatabricksCliVersion class with a (major, minor, patch) triple, an UNKNOWN sentinel, an atLeast() comparator, and an isDefaultDevBuild() helper.
  • CliTokenSource simplified to a single cmd; the fallbackCmd parameter and its retry logic are removed.
  • DatabricksCliCredentialsProvider gains getCliVersion, probeCliVersion, parseCliVersion, resolveCliCommand, and buildCliCommand helpers.

How is this tested?

Unit tests in DatabricksCliVersionTest cover version comparison (across patch/minor/major), the UNKNOWN sentinel, dev-build detection, and toString formatting.

Unit tests in DatabricksCliCredentialsProviderTest cover JSON parsing of databricks version --output json (standard, dev build, missing fields, malformed JSON, empty string) and command assembly for every profile/host/version combination (host-only, account host, profile + new CLI, profile + old CLI, unknown version, dev build).

CliTokenSourceTest retains its parsing and timezone tests; the obsolete fallback tests are dropped.

@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

@mihaimitrea-db mihaimitrea-db self-assigned this Apr 1, 2026
@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

@mihaimitrea-db mihaimitrea-db removed the request for review from parthban-db April 14, 2026 13:59
@mihaimitrea-db mihaimitrea-db force-pushed the mihaimitrea-db/stack/cli-force-refresh branch from 1c68e85 to 0de2614 Compare April 30, 2026 13:28
@mihaimitrea-db mihaimitrea-db force-pushed the mihaimitrea-db/stack/cli-force-refresh branch from 0de2614 to 6d775fc Compare April 30, 2026 13:37
@mihaimitrea-db mihaimitrea-db changed the title Add --force-refresh support for Databricks CLI token fetching Fix CLI token source --profile fallback with version detection Apr 30, 2026
`--profile` on `databricks auth token` is a global Cobra flag, so old
CLIs (< v0.207.1) silently accept it and fail later with `cannot fetch
credentials` instead of `unknown flag: --profile`. The previous
error-based fallback never matched, leaving the `--host` fallback as
dead code.

This commit replaces the runtime fallback chain with version-based
capability detection:

* `CliVersion` carries a (major, minor, patch) triple plus an
  `UNKNOWN` sentinel and a default-dev-build (0,0,0) check.
* `DatabricksCliCredentialsProvider` runs `databricks version --output
  json` once per CLI path (cached on success only, with a 5s timeout)
  and gates `--profile` on >= v0.207.1; everything else falls back to
  `--host` with a precise warning.
* `CliTokenSource` is simplified to a single `cmd`; the
  `fallbackCmd` parameter and the runtime "unknown flag" retry loop are
  removed.

Mirrors the equivalent refactors in the Go and Python SDKs:
* databricks/databricks-sdk-go#1605
* databricks/databricks-sdk-py#1377

Co-authored-by: Isaac
@mihaimitrea-db mihaimitrea-db force-pushed the mihaimitrea-db/stack/cli-force-refresh branch from 6d775fc to c4e12c1 Compare April 30, 2026 14:51
@github-actions
Copy link
Copy Markdown
Contributor

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: c4e12c185de5b8decc8aa5e228cdd5094b80fe5d

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