Skip to content

Commit 25a3779

Browse files
Refactor CliTokenSource to use an ordered attempt chain
Replace ad-hoc forceCmd/profileCmd/fallbackCmd fields with a generic ordered attempt model: each CliCommand describes its command, the flags it uses (for detecting "unknown flag" errors), and an optional warning message. getToken() is now a simple loop over the attempt chain instead of nested if-else branches. This makes adding future CLI flag fallbacks straightforward without introducing more fields. DatabricksCliCredentialsProvider builds the attempt chain explicitly: - with profile: force+profile -> profile -> host - without profile: force+host -> host Old constructors delegate to the new model. Azure CLI callers use the 5-arg constructor which creates a single-attempt chain, so their behavior is unchanged. Signed-off-by: Mihai Mitrea <mihai.mitrea@databricks.com>
1 parent 6b8a57f commit 25a3779

File tree

4 files changed

+246
-72
lines changed

4 files changed

+246
-72
lines changed

NEXT_CHANGELOG.md

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

1616
### Internal Changes
17+
* Generalize CLI token source into a progressive command list for forward-compatible flag support.
1718

1819
### API Changes
1920
* Add `createCatalog()`, `createSyncedTable()`, `deleteCatalog()`, `deleteSyncedTable()`, `getCatalog()` and `getSyncedTable()` methods for `workspaceClient.postgres()` service.

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

Lines changed: 134 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@
1515
import java.time.ZoneId;
1616
import java.time.format.DateTimeFormatter;
1717
import java.time.format.DateTimeParseException;
18+
import java.util.ArrayList;
1819
import java.util.Arrays;
20+
import java.util.Collections;
1921
import java.util.List;
22+
import java.util.stream.Collectors;
2023
import org.apache.commons.io.IOUtils;
2124
import org.slf4j.Logger;
2225
import org.slf4j.LoggerFactory;
@@ -25,18 +28,25 @@
2528
public class CliTokenSource implements TokenSource {
2629
private static final Logger LOG = LoggerFactory.getLogger(CliTokenSource.class);
2730

28-
// forceCmd is tried before profileCmd when non-null. If the CLI rejects
29-
// --force-refresh or --profile, execution falls through to profileCmd.
30-
private List<String> forceCmd;
31+
/**
32+
* Describes a CLI command with an optional warning message emitted when falling through to the
33+
* next command in the chain.
34+
*/
35+
static class CliCommand {
36+
final List<String> cmd;
37+
38+
// Flags used by this command (e.g. "--force-refresh", "--profile"). Used to distinguish
39+
// "unknown flag" errors (which trigger fallback) from real auth errors (which propagate).
40+
final List<String> usedFlags;
3141

32-
private List<String> profileCmd;
33-
private String tokenTypeField;
34-
private String accessTokenField;
35-
private String expiryField;
36-
private Environment env;
37-
// fallbackCmd is tried when profileCmd fails with "unknown flag: --profile",
38-
// indicating the CLI is too old to support --profile.
39-
private List<String> fallbackCmd;
42+
final String fallbackMessage;
43+
44+
CliCommand(List<String> cmd, List<String> usedFlags, String fallbackMessage) {
45+
this.cmd = cmd;
46+
this.usedFlags = usedFlags != null ? usedFlags : Collections.emptyList();
47+
this.fallbackMessage = fallbackMessage;
48+
}
49+
}
4050

4151
/**
4252
* Internal exception that carries the clean stderr message but exposes full output for checks.
@@ -54,6 +64,13 @@ String getFullOutput() {
5464
}
5565
}
5666

67+
private final List<CliCommand> attempts;
68+
private final String tokenTypeField;
69+
private final String accessTokenField;
70+
private final String expiryField;
71+
private final Environment env;
72+
73+
/** Constructs a single-attempt source. Used by Azure CLI and simple callers. */
5774
public CliTokenSource(
5875
List<String> cmd,
5976
String tokenTypeField,
@@ -63,6 +80,7 @@ public CliTokenSource(
6380
this(cmd, tokenTypeField, accessTokenField, expiryField, env, null, null);
6481
}
6582

83+
/** Constructs a two-attempt source with --profile to --host fallback. */
6684
public CliTokenSource(
6785
List<String> cmd,
6886
String tokenTypeField,
@@ -73,6 +91,7 @@ public CliTokenSource(
7391
this(cmd, tokenTypeField, accessTokenField, expiryField, env, fallbackCmd, null);
7492
}
7593

94+
/** Constructs a source with optional force-refresh, profile, and host fallback chain. */
7695
public CliTokenSource(
7796
List<String> cmd,
7897
String tokenTypeField,
@@ -81,15 +100,86 @@ public CliTokenSource(
81100
Environment env,
82101
List<String> fallbackCmd,
83102
List<String> forceCmd) {
84-
super();
85-
this.profileCmd = OSUtils.get(env).getCliExecutableCommand(cmd);
103+
this(
104+
buildAttempts(forceCmd, cmd, fallbackCmd).stream()
105+
.map(
106+
a ->
107+
new CliCommand(
108+
OSUtils.get(env).getCliExecutableCommand(a.cmd),
109+
a.usedFlags,
110+
a.fallbackMessage))
111+
.collect(Collectors.toList()),
112+
tokenTypeField,
113+
accessTokenField,
114+
expiryField,
115+
env,
116+
true);
117+
}
118+
119+
/** Creates a CliTokenSource from a pre-built attempt chain. */
120+
static CliTokenSource fromAttempts(
121+
List<CliCommand> attempts,
122+
String tokenTypeField,
123+
String accessTokenField,
124+
String expiryField,
125+
Environment env) {
126+
return new CliTokenSource(
127+
attempts.stream()
128+
.map(
129+
a ->
130+
new CliCommand(
131+
OSUtils.get(env).getCliExecutableCommand(a.cmd),
132+
a.usedFlags,
133+
a.fallbackMessage))
134+
.collect(Collectors.toList()),
135+
tokenTypeField,
136+
accessTokenField,
137+
expiryField,
138+
env,
139+
true);
140+
}
141+
142+
private CliTokenSource(
143+
List<CliCommand> attempts,
144+
String tokenTypeField,
145+
String accessTokenField,
146+
String expiryField,
147+
Environment env,
148+
boolean alreadyResolved) {
149+
this.attempts = attempts;
86150
this.tokenTypeField = tokenTypeField;
87151
this.accessTokenField = accessTokenField;
88152
this.expiryField = expiryField;
89153
this.env = env;
90-
this.fallbackCmd =
91-
fallbackCmd != null ? OSUtils.get(env).getCliExecutableCommand(fallbackCmd) : null;
92-
this.forceCmd = forceCmd != null ? OSUtils.get(env).getCliExecutableCommand(forceCmd) : null;
154+
}
155+
156+
private static List<CliCommand> buildAttempts(
157+
List<String> forceCmd, List<String> profileCmd, List<String> fallbackCmd) {
158+
List<CliCommand> attempts = new ArrayList<>();
159+
160+
if (forceCmd != null) {
161+
attempts.add(
162+
new CliCommand(
163+
forceCmd,
164+
Arrays.asList("--force-refresh", "--profile"),
165+
"Databricks CLI does not support --force-refresh flag. "
166+
+ "Falling back to regular token fetch. "
167+
+ "Please upgrade your CLI to the latest version."));
168+
}
169+
170+
if (fallbackCmd != null) {
171+
attempts.add(
172+
new CliCommand(
173+
profileCmd,
174+
Collections.singletonList("--profile"),
175+
"Databricks CLI does not support --profile flag. Falling back to --host. "
176+
+ "Please upgrade your CLI to the latest version."));
177+
attempts.add(new CliCommand(fallbackCmd, Collections.emptyList(), null));
178+
} else {
179+
attempts.add(new CliCommand(profileCmd, Collections.emptyList(), null));
180+
}
181+
182+
return attempts;
93183
}
94184

95185
/**
@@ -150,8 +240,6 @@ private Token execCliCommand(List<String> cmdToRun) throws IOException {
150240
if (stderr.contains("not found")) {
151241
throw new DatabricksException(stderr);
152242
}
153-
// getMessage() returns the clean stderr-based message; getFullOutput() exposes
154-
// both streams so the caller can check for "unknown flag: --profile" in either.
155243
throw new CliCommandException("cannot get access token: " + stderr, stdout + "\n" + stderr);
156244
}
157245
JsonNode jsonNode = new ObjectMapper().readTree(stdout);
@@ -167,54 +255,48 @@ private Token execCliCommand(List<String> cmdToRun) throws IOException {
167255
}
168256
}
169257

170-
private String getErrorText(IOException e) {
258+
private static String getErrorText(IOException e) {
171259
return e instanceof CliCommandException
172260
? ((CliCommandException) e).getFullOutput()
173261
: e.getMessage();
174262
}
175263

176-
private boolean isUnknownFlagError(String errorText, String flag) {
177-
return errorText != null && errorText.contains("unknown flag: " + flag);
178-
}
179-
180-
private Token execProfileCmdWithFallback() {
181-
try {
182-
return execCliCommand(this.profileCmd);
183-
} catch (IOException e) {
184-
String textToCheck = getErrorText(e);
185-
if (fallbackCmd != null && isUnknownFlagError(textToCheck, "--profile")) {
186-
LOG.warn(
187-
"Databricks CLI does not support --profile flag. Falling back to --host. "
188-
+ "Please upgrade your CLI to the latest version.");
189-
try {
190-
return execCliCommand(this.fallbackCmd);
191-
} catch (IOException fallbackException) {
192-
throw new DatabricksException(fallbackException.getMessage(), fallbackException);
193-
}
264+
private static boolean isUnknownFlagError(String errorText, List<String> flags) {
265+
if (errorText == null) {
266+
return false;
267+
}
268+
for (String flag : flags) {
269+
if (errorText.contains("unknown flag: " + flag)) {
270+
return true;
194271
}
195-
throw new DatabricksException(e.getMessage(), e);
196272
}
273+
return false;
197274
}
198275

199276
@Override
200277
public Token getToken() {
201-
if (forceCmd == null) {
202-
return execProfileCmdWithFallback();
278+
if (attempts.isEmpty()) {
279+
throw new DatabricksException("cannot get access token: no CLI commands configured");
203280
}
204281

205-
try {
206-
return execCliCommand(this.forceCmd);
207-
} catch (IOException e) {
208-
String textToCheck = getErrorText(e);
209-
if (isUnknownFlagError(textToCheck, "--force-refresh")
210-
|| isUnknownFlagError(textToCheck, "--profile")) {
211-
LOG.warn(
212-
"Databricks CLI does not support --force-refresh flag. "
213-
+ "Falling back to regular token fetch. "
214-
+ "Please upgrade your CLI to the latest version.");
215-
return execProfileCmdWithFallback();
282+
IOException lastException = null;
283+
284+
for (int i = 0; i < attempts.size(); i++) {
285+
CliCommand attempt = attempts.get(i);
286+
try {
287+
return execCliCommand(attempt.cmd);
288+
} catch (IOException e) {
289+
if (i + 1 < attempts.size() && isUnknownFlagError(getErrorText(e), attempt.usedFlags)) {
290+
if (attempt.fallbackMessage != null) {
291+
LOG.warn(attempt.fallbackMessage);
292+
}
293+
lastException = e;
294+
continue;
295+
}
296+
throw new DatabricksException(e.getMessage(), e);
216297
}
217-
throw new DatabricksException(e.getMessage(), e);
218298
}
299+
300+
throw new DatabricksException(lastException.getMessage(), lastException);
219301
}
220302
}

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

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,47 @@ List<String> buildProfileArgs(String cliPath, DatabricksConfig config) {
7575
}
7676

7777
private static List<String> withForceRefresh(List<String> cmd) {
78-
List<String> forceCmd = new ArrayList<>(cmd);
79-
forceCmd.add("--force-refresh");
80-
return forceCmd;
78+
List<String> result = new ArrayList<>(cmd);
79+
result.add("--force-refresh");
80+
return result;
81+
}
82+
83+
List<CliTokenSource.CliCommand> buildAttempts(String cliPath, DatabricksConfig config) {
84+
List<CliTokenSource.CliCommand> attempts = new ArrayList<>();
85+
86+
List<String> profileCmd;
87+
boolean hasHostFallback = false;
88+
89+
if (config.getProfile() != null) {
90+
profileCmd = buildProfileArgs(cliPath, config);
91+
hasHostFallback = config.getHost() != null;
92+
} else {
93+
profileCmd = buildHostArgs(cliPath, config);
94+
}
95+
96+
attempts.add(
97+
new CliTokenSource.CliCommand(
98+
withForceRefresh(profileCmd),
99+
Arrays.asList("--force-refresh", "--profile"),
100+
"Databricks CLI does not support --force-refresh flag. "
101+
+ "Falling back to regular token fetch. "
102+
+ "Please upgrade your CLI to the latest version."));
103+
104+
if (hasHostFallback) {
105+
attempts.add(
106+
new CliTokenSource.CliCommand(
107+
profileCmd,
108+
Collections.singletonList("--profile"),
109+
"Databricks CLI does not support --profile flag. Falling back to --host. "
110+
+ "Please upgrade your CLI to the latest version."));
111+
attempts.add(
112+
new CliTokenSource.CliCommand(
113+
buildHostArgs(cliPath, config), Collections.emptyList(), null));
114+
} else {
115+
attempts.add(new CliTokenSource.CliCommand(profileCmd, Collections.emptyList(), null));
116+
}
117+
118+
return attempts;
81119
}
82120

83121
private CliTokenSource getDatabricksCliTokenSource(DatabricksConfig config) {
@@ -90,23 +128,8 @@ private CliTokenSource getDatabricksCliTokenSource(DatabricksConfig config) {
90128
return null;
91129
}
92130

93-
List<String> profileCmd;
94-
List<String> fallbackCmd = null;
95-
List<String> forceCmd;
96-
97-
if (config.getProfile() != null) {
98-
profileCmd = buildProfileArgs(cliPath, config);
99-
forceCmd = withForceRefresh(profileCmd);
100-
if (config.getHost() != null) {
101-
fallbackCmd = buildHostArgs(cliPath, config);
102-
}
103-
} else {
104-
profileCmd = buildHostArgs(cliPath, config);
105-
forceCmd = withForceRefresh(profileCmd);
106-
}
107-
108-
return new CliTokenSource(
109-
profileCmd, "token_type", "access_token", "expiry", config.getEnv(), fallbackCmd, forceCmd);
131+
return CliTokenSource.fromAttempts(
132+
buildAttempts(cliPath, config), "token_type", "access_token", "expiry", config.getEnv());
110133
}
111134

112135
@Override

0 commit comments

Comments
 (0)