Skip to content

Commit 0dd48d1

Browse files
Refactor CliTokenSource to use an ordered attempt chain
1 parent 1c68e85 commit 0dd48d1

File tree

5 files changed

+305
-72
lines changed

5 files changed

+305
-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+
* Generalized CLI token source into a progressive command attempt list, replacing the fixed three-field approach with an extensible chain.
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: 114 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616
import java.time.format.DateTimeFormatter;
1717
import java.time.format.DateTimeParseException;
1818
import java.util.Arrays;
19+
import java.util.Collections;
1920
import java.util.List;
21+
import java.util.concurrent.atomic.AtomicInteger;
22+
import java.util.stream.Collectors;
2023
import org.apache.commons.io.IOUtils;
2124
import org.slf4j.Logger;
2225
import org.slf4j.LoggerFactory;
@@ -25,13 +28,25 @@
2528
public class CliTokenSource implements TokenSource {
2629
private static final Logger LOG = LoggerFactory.getLogger(CliTokenSource.class);
2730

28-
private List<String> cmd;
29-
private List<String> fallbackCmd;
30-
private List<String> secondFallbackCmd;
31-
private String tokenTypeField;
32-
private String accessTokenField;
33-
private String expiryField;
34-
private Environment env;
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;
41+
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+
}
3550

3651
/**
3752
* Internal exception that carries the clean stderr message but exposes full output for checks.
@@ -49,34 +64,72 @@ String getFullOutput() {
4964
}
5065
}
5166

67+
private final List<CliCommand> commands;
68+
69+
// Index of the CLI command known to work, or -1 if not yet resolved. Once
70+
// resolved it never changes — older CLIs don't gain new flags. We use
71+
// AtomicInteger instead of synchronization because probing must be retryable
72+
// on transient errors: concurrent callers may redundantly probe, but all
73+
// converge to the same index.
74+
private final AtomicInteger activeCommandIndex = new AtomicInteger(-1);
75+
76+
private final String tokenTypeField;
77+
private final String accessTokenField;
78+
private final String expiryField;
79+
private final Environment env;
80+
81+
/** Constructs a single-attempt source. Used by Azure CLI and simple callers. */
5282
public CliTokenSource(
5383
List<String> cmd,
5484
String tokenTypeField,
5585
String accessTokenField,
5686
String expiryField,
5787
Environment env) {
58-
this(cmd, tokenTypeField, accessTokenField, expiryField, env, null, null);
88+
this(cmd, null, tokenTypeField, accessTokenField, expiryField, env);
5989
}
6090

61-
public CliTokenSource(
91+
/** Creates a CliTokenSource from a pre-built command chain. */
92+
static CliTokenSource fromCommands(
93+
List<CliCommand> commands,
94+
String tokenTypeField,
95+
String accessTokenField,
96+
String expiryField,
97+
Environment env) {
98+
return new CliTokenSource(null, commands, tokenTypeField, accessTokenField, expiryField, env);
99+
}
100+
101+
private CliTokenSource(
62102
List<String> cmd,
103+
List<CliCommand> commands,
63104
String tokenTypeField,
64105
String accessTokenField,
65106
String expiryField,
66-
Environment env,
67-
List<String> fallbackCmd,
68-
List<String> secondFallbackCmd) {
69-
this.cmd = OSUtils.get(env).getCliExecutableCommand(cmd);
107+
Environment env) {
108+
if (commands != null && !commands.isEmpty()) {
109+
this.commands =
110+
commands.stream()
111+
.map(
112+
a ->
113+
new CliCommand(
114+
OSUtils.get(env).getCliExecutableCommand(a.cmd),
115+
a.usedFlags,
116+
a.fallbackMessage))
117+
.collect(Collectors.toList());
118+
} else if (cmd != null) {
119+
if (commands != null && commands.isEmpty()) {
120+
LOG.warn("No CLI commands configured. Falling back to the default command.");
121+
}
122+
this.commands =
123+
Collections.singletonList(
124+
new CliCommand(
125+
OSUtils.get(env).getCliExecutableCommand(cmd), Collections.emptyList(), null));
126+
} else {
127+
throw new DatabricksException("cannot get access token: no CLI commands configured");
128+
}
70129
this.tokenTypeField = tokenTypeField;
71130
this.accessTokenField = accessTokenField;
72131
this.expiryField = expiryField;
73132
this.env = env;
74-
this.fallbackCmd =
75-
fallbackCmd != null ? OSUtils.get(env).getCliExecutableCommand(fallbackCmd) : null;
76-
this.secondFallbackCmd =
77-
secondFallbackCmd != null
78-
? OSUtils.get(env).getCliExecutableCommand(secondFallbackCmd)
79-
: null;
80133
}
81134

82135
/**
@@ -137,8 +190,9 @@ private Token execCliCommand(List<String> cmdToRun) throws IOException {
137190
if (stderr.contains("not found")) {
138191
throw new DatabricksException(stderr);
139192
}
140-
// getMessage() returns the clean stderr-based message; getFullOutput() exposes
141-
// both streams so the caller can check for "unknown flag: --profile" in either.
193+
// getMessage() carries the clean stderr message for user-facing errors;
194+
// getFullOutput() includes both streams so isUnknownFlagError can detect
195+
// "unknown flag:" regardless of which stream the CLI wrote it to.
142196
throw new CliCommandException("cannot get access token: " + stderr, stdout + "\n" + stderr);
143197
}
144198
JsonNode jsonNode = new ObjectMapper().readTree(stdout);
@@ -154,48 +208,61 @@ private Token execCliCommand(List<String> cmdToRun) throws IOException {
154208
}
155209
}
156210

157-
private String getErrorText(IOException e) {
211+
private static String getErrorText(IOException e) {
158212
return e instanceof CliCommandException
159213
? ((CliCommandException) e).getFullOutput()
160214
: e.getMessage();
161215
}
162216

163-
private boolean isUnknownFlagError(String errorText) {
164-
return errorText != null && errorText.contains("unknown flag:");
217+
private static boolean isUnknownFlagError(String errorText, List<String> flags) {
218+
if (errorText == null) {
219+
return false;
220+
}
221+
for (String flag : flags) {
222+
if (errorText.contains("unknown flag: " + flag)) {
223+
return true;
224+
}
225+
}
226+
return false;
165227
}
166228

167229
@Override
168230
public Token getToken() {
169-
try {
170-
return execCliCommand(this.cmd);
171-
} catch (IOException e) {
172-
if (fallbackCmd != null && isUnknownFlagError(getErrorText(e))) {
173-
LOG.warn(
174-
"CLI does not support some flags used by this SDK. "
175-
+ "Falling back to a compatible command. "
176-
+ "Please upgrade your CLI to the latest version.");
177-
} else {
231+
int idx = activeCommandIndex.get();
232+
if (idx >= 0) {
233+
try {
234+
return execCliCommand(commands.get(idx).cmd);
235+
} catch (IOException e) {
178236
throw new DatabricksException(e.getMessage(), e);
179237
}
180238
}
239+
return probeAndExec();
240+
}
181241

182-
try {
183-
return execCliCommand(this.fallbackCmd);
184-
} catch (IOException e) {
185-
if (secondFallbackCmd != null && isUnknownFlagError(getErrorText(e))) {
186-
LOG.warn(
187-
"CLI does not support some flags used by this SDK. "
188-
+ "Falling back to a compatible command. "
189-
+ "Please upgrade your CLI to the latest version.");
190-
} else {
242+
/**
243+
* Walks the command list from most-featured to simplest, looking for a CLI command that succeeds.
244+
* When a command fails with "unknown flag" for one of its {@link CliCommand#usedFlags}, it logs a
245+
* warning and tries the next. On success, {@link #activeCommandIndex} is stored so future calls
246+
* skip probing.
247+
*/
248+
private Token probeAndExec() {
249+
for (int i = 0; i < commands.size(); i++) {
250+
CliCommand command = commands.get(i);
251+
try {
252+
Token token = execCliCommand(command.cmd);
253+
activeCommandIndex.set(i);
254+
return token;
255+
} catch (IOException e) {
256+
if (i + 1 < commands.size() && isUnknownFlagError(getErrorText(e), command.usedFlags)) {
257+
if (command.fallbackMessage != null) {
258+
LOG.warn(command.fallbackMessage);
259+
}
260+
continue;
261+
}
191262
throw new DatabricksException(e.getMessage(), e);
192263
}
193264
}
194265

195-
try {
196-
return execCliCommand(this.secondFallbackCmd);
197-
} catch (IOException e) {
198-
throw new DatabricksException(e.getMessage(), e);
199-
}
266+
throw new DatabricksException("cannot get access token: all CLI commands failed");
200267
}
201268
}

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

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,40 @@ private static List<String> withForceRefresh(List<String> cmd) {
8080
return forceCmd;
8181
}
8282

83+
List<CliTokenSource.CliCommand> buildCommands(String cliPath, DatabricksConfig config) {
84+
List<CliTokenSource.CliCommand> commands = new ArrayList<>();
85+
86+
boolean hasProfile = config.getProfile() != null;
87+
boolean hasHost = config.getHost() != null;
88+
89+
if (hasProfile) {
90+
List<String> profileCmd = buildProfileArgs(cliPath, config);
91+
92+
commands.add(
93+
new CliTokenSource.CliCommand(
94+
withForceRefresh(profileCmd),
95+
Arrays.asList("--force-refresh", "--profile"),
96+
"Databricks CLI does not support --force-refresh flag. "
97+
+ "Falling back to regular token fetch. "
98+
+ "Please upgrade your CLI to the latest version."));
99+
100+
commands.add(
101+
new CliTokenSource.CliCommand(
102+
profileCmd,
103+
Collections.singletonList("--profile"),
104+
"Databricks CLI does not support --profile flag. Falling back to --host. "
105+
+ "Please upgrade your CLI to the latest version."));
106+
}
107+
108+
if (hasHost) {
109+
commands.add(
110+
new CliTokenSource.CliCommand(
111+
buildHostArgs(cliPath, config), Collections.emptyList(), null));
112+
}
113+
114+
return commands;
115+
}
116+
83117
private CliTokenSource getDatabricksCliTokenSource(DatabricksConfig config) {
84118
String cliPath = config.getDatabricksCliPath();
85119
if (cliPath == null) {
@@ -90,29 +124,8 @@ private CliTokenSource getDatabricksCliTokenSource(DatabricksConfig config) {
90124
return null;
91125
}
92126

93-
List<String> cmd;
94-
List<String> fallbackCmd = null;
95-
List<String> secondFallbackCmd = null;
96-
97-
if (config.getProfile() != null) {
98-
List<String> profileArgs = buildProfileArgs(cliPath, config);
99-
cmd = withForceRefresh(profileArgs);
100-
fallbackCmd = profileArgs;
101-
if (config.getHost() != null) {
102-
secondFallbackCmd = buildHostArgs(cliPath, config);
103-
}
104-
} else {
105-
cmd = buildHostArgs(cliPath, config);
106-
}
107-
108-
return new CliTokenSource(
109-
cmd,
110-
"token_type",
111-
"access_token",
112-
"expiry",
113-
config.getEnv(),
114-
fallbackCmd,
115-
secondFallbackCmd);
127+
return CliTokenSource.fromCommands(
128+
buildCommands(cliPath, config), "token_type", "access_token", "expiry", config.getEnv());
116129
}
117130

118131
@Override

0 commit comments

Comments
 (0)