Skip to content

Commit f88c52a

Browse files
Refactor CliTokenSource to use an ordered attempt chain
Replace ad-hoc forceCmd/profileCmd/fallbackCmd fields with a generic ordered attempt model: each CliCommandAttempt describes its command, the error substrings that trigger fallback to the next attempt, and an optional log 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 5e8f476 commit f88c52a

File tree

4 files changed

+241
-75
lines changed

4 files changed

+241
-75
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: 126 additions & 55 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,21 +28,21 @@
2528
public class CliTokenSource implements TokenSource {
2629
private static final Logger LOG = LoggerFactory.getLogger(CliTokenSource.class);
2730

28-
private static final String UNKNOWN_PROFILE_FLAG = "unknown flag: --profile";
29-
private static final String UNKNOWN_FORCE_REFRESH_FLAG = "unknown flag: --force-refresh";
30-
31-
// forceCmd is tried before profileCmd when non-null. If the CLI rejects
32-
// --force-refresh or --profile, execution falls through to profileCmd.
33-
private List<String> forceCmd;
31+
/**
32+
* Describes a CLI command with the error substrings that allow falling through to the next
33+
* command in the chain and an optional log message emitted on fallback.
34+
*/
35+
static class CliCommand {
36+
final List<String> cmd;
37+
final List<String> fallbackTriggers;
38+
final String fallbackMessage;
3439

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

4447
/**
4548
* Internal exception that carries the clean stderr message but exposes full output for checks.
@@ -57,6 +60,13 @@ String getFullOutput() {
5760
}
5861
}
5962

63+
private final List<CliCommand> attempts;
64+
private final String tokenTypeField;
65+
private final String accessTokenField;
66+
private final String expiryField;
67+
private final Environment env;
68+
69+
/** Constructs a single-attempt source. Used by Azure CLI and simple callers. */
6070
public CliTokenSource(
6171
List<String> cmd,
6272
String tokenTypeField,
@@ -66,6 +76,7 @@ public CliTokenSource(
6676
this(cmd, tokenTypeField, accessTokenField, expiryField, env, null, null);
6777
}
6878

79+
/** Constructs a two-attempt source with --profile to --host fallback. */
6980
public CliTokenSource(
7081
List<String> cmd,
7182
String tokenTypeField,
@@ -76,6 +87,7 @@ public CliTokenSource(
7687
this(cmd, tokenTypeField, accessTokenField, expiryField, env, fallbackCmd, null);
7788
}
7889

90+
/** Constructs a source with optional force-refresh, profile, and host fallback chain. */
7991
public CliTokenSource(
8092
List<String> cmd,
8193
String tokenTypeField,
@@ -84,15 +96,89 @@ public CliTokenSource(
8496
Environment env,
8597
List<String> fallbackCmd,
8698
List<String> forceCmd) {
87-
super();
88-
this.profileCmd = OSUtils.get(env).getCliExecutableCommand(cmd);
99+
this(
100+
buildAttempts(forceCmd, cmd, fallbackCmd).stream()
101+
.map(
102+
a ->
103+
new CliCommand(
104+
OSUtils.get(env).getCliExecutableCommand(a.cmd),
105+
a.fallbackTriggers,
106+
a.fallbackMessage))
107+
.collect(Collectors.toList()),
108+
tokenTypeField,
109+
accessTokenField,
110+
expiryField,
111+
env,
112+
true);
113+
}
114+
115+
/** Creates a CliTokenSource from a pre-built attempt chain. */
116+
static CliTokenSource fromAttempts(
117+
List<CliCommand> attempts,
118+
String tokenTypeField,
119+
String accessTokenField,
120+
String expiryField,
121+
Environment env) {
122+
return new CliTokenSource(
123+
attempts.stream()
124+
.map(
125+
a ->
126+
new CliCommand(
127+
OSUtils.get(env).getCliExecutableCommand(a.cmd),
128+
a.fallbackTriggers,
129+
a.fallbackMessage))
130+
.collect(Collectors.toList()),
131+
tokenTypeField,
132+
accessTokenField,
133+
expiryField,
134+
env,
135+
true);
136+
}
137+
138+
private CliTokenSource(
139+
List<CliCommand> attempts,
140+
String tokenTypeField,
141+
String accessTokenField,
142+
String expiryField,
143+
Environment env,
144+
boolean alreadyResolved) {
145+
this.attempts = attempts;
89146
this.tokenTypeField = tokenTypeField;
90147
this.accessTokenField = accessTokenField;
91148
this.expiryField = expiryField;
92149
this.env = env;
93-
this.fallbackCmd =
94-
fallbackCmd != null ? OSUtils.get(env).getCliExecutableCommand(fallbackCmd) : null;
95-
this.forceCmd = forceCmd != null ? OSUtils.get(env).getCliExecutableCommand(forceCmd) : null;
150+
}
151+
152+
private static final String UNKNOWN_PROFILE_FLAG = "unknown flag: --profile";
153+
private static final String UNKNOWN_FORCE_REFRESH_FLAG = "unknown flag: --force-refresh";
154+
155+
private static List<CliCommand> buildAttempts(
156+
List<String> forceCmd, List<String> profileCmd, List<String> fallbackCmd) {
157+
List<CliCommand> attempts = new ArrayList<>();
158+
159+
if (forceCmd != null) {
160+
attempts.add(
161+
new CliCommand(
162+
forceCmd,
163+
Arrays.asList(UNKNOWN_FORCE_REFRESH_FLAG, UNKNOWN_PROFILE_FLAG),
164+
"Databricks CLI does not support --force-refresh flag. "
165+
+ "Falling back to regular token fetch. "
166+
+ "Please upgrade your CLI to the latest version."));
167+
}
168+
169+
if (fallbackCmd != null) {
170+
attempts.add(
171+
new CliCommand(
172+
profileCmd,
173+
Collections.singletonList(UNKNOWN_PROFILE_FLAG),
174+
"Databricks CLI does not support --profile flag. Falling back to --host. "
175+
+ "Please upgrade your CLI to the latest version."));
176+
attempts.add(new CliCommand(fallbackCmd, Collections.emptyList(), null));
177+
} else {
178+
attempts.add(new CliCommand(profileCmd, Collections.emptyList(), null));
179+
}
180+
181+
return attempts;
96182
}
97183

98184
/**
@@ -170,54 +256,39 @@ private Token execCliCommand(List<String> cmdToRun) throws IOException {
170256
}
171257
}
172258

173-
private String getErrorText(IOException e) {
259+
private static String getErrorText(IOException e) {
174260
return e instanceof CliCommandException
175261
? ((CliCommandException) e).getFullOutput()
176262
: e.getMessage();
177263
}
178264

179-
private boolean isUnknownFlagError(String errorText, String flag) {
180-
return errorText != null && errorText.contains(flag);
181-
}
182-
183-
private Token execProfileCmdWithFallback() {
184-
try {
185-
return execCliCommand(this.profileCmd);
186-
} catch (IOException e) {
187-
String textToCheck = getErrorText(e);
188-
if (fallbackCmd != null && isUnknownFlagError(textToCheck, UNKNOWN_PROFILE_FLAG)) {
189-
LOG.warn(
190-
"Databricks CLI does not support --profile flag. Falling back to --host. "
191-
+ "Please upgrade your CLI to the latest version.");
192-
try {
193-
return execCliCommand(this.fallbackCmd);
194-
} catch (IOException fallbackException) {
195-
throw new DatabricksException(fallbackException.getMessage(), fallbackException);
196-
}
197-
}
198-
throw new DatabricksException(e.getMessage(), e);
265+
private static boolean shouldFallback(CliCommand attempt, String errorText) {
266+
if (errorText == null) {
267+
return false;
199268
}
269+
return attempt.fallbackTriggers.stream().anyMatch(errorText::contains);
200270
}
201271

202272
@Override
203273
public Token getToken() {
204-
if (forceCmd == null) {
205-
return execProfileCmdWithFallback();
206-
}
274+
IOException lastException = null;
207275

208-
try {
209-
return execCliCommand(this.forceCmd);
210-
} catch (IOException e) {
211-
String textToCheck = getErrorText(e);
212-
if (isUnknownFlagError(textToCheck, UNKNOWN_FORCE_REFRESH_FLAG)
213-
|| isUnknownFlagError(textToCheck, UNKNOWN_PROFILE_FLAG)) {
214-
LOG.warn(
215-
"Databricks CLI does not support --force-refresh flag. "
216-
+ "Falling back to regular token fetch. "
217-
+ "Please upgrade your CLI to the latest version.");
218-
return execProfileCmdWithFallback();
276+
for (int i = 0; i < attempts.size(); i++) {
277+
CliCommand attempt = attempts.get(i);
278+
try {
279+
return execCliCommand(attempt.cmd);
280+
} catch (IOException e) {
281+
if (i + 1 < attempts.size() && shouldFallback(attempt, getErrorText(e))) {
282+
if (attempt.fallbackMessage != null) {
283+
LOG.warn(attempt.fallbackMessage);
284+
}
285+
lastException = e;
286+
continue;
287+
}
288+
throw new DatabricksException(e.getMessage(), e);
219289
}
220-
throw new DatabricksException(e.getMessage(), e);
221290
}
291+
292+
throw new DatabricksException(lastException.getMessage(), lastException);
222293
}
223294
}

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

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -69,15 +69,56 @@ List<String> buildHostArgs(String cliPath, DatabricksConfig config) {
6969
return cmd;
7070
}
7171

72+
private static final String UNKNOWN_PROFILE_FLAG = "unknown flag: --profile";
73+
private static final String UNKNOWN_FORCE_REFRESH_FLAG = "unknown flag: --force-refresh";
74+
7275
List<String> buildProfileArgs(String cliPath, DatabricksConfig config) {
7376
return new ArrayList<>(
7477
Arrays.asList(cliPath, "auth", "token", "--profile", config.getProfile()));
7578
}
7679

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

83124
private CliTokenSource getDatabricksCliTokenSource(DatabricksConfig config) {
@@ -90,23 +131,8 @@ private CliTokenSource getDatabricksCliTokenSource(DatabricksConfig config) {
90131
return null;
91132
}
92133

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);
134+
return CliTokenSource.fromAttempts(
135+
buildAttempts(cliPath, config), "token_type", "access_token", "expiry", config.getEnv());
110136
}
111137

112138
@Override

0 commit comments

Comments
 (0)