1616import java .time .format .DateTimeFormatter ;
1717import java .time .format .DateTimeParseException ;
1818import java .util .Arrays ;
19+ import java .util .Collections ;
1920import java .util .List ;
21+ import java .util .concurrent .atomic .AtomicInteger ;
22+ import java .util .stream .Collectors ;
2023import org .apache .commons .io .IOUtils ;
2124import org .slf4j .Logger ;
2225import org .slf4j .LoggerFactory ;
2528public 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}
0 commit comments