1515import java .time .ZoneId ;
1616import java .time .format .DateTimeFormatter ;
1717import java .time .format .DateTimeParseException ;
18+ import java .util .ArrayList ;
1819import java .util .Arrays ;
20+ import java .util .Collections ;
1921import java .util .List ;
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- // 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}
0 commit comments