1212*/
1313package io .kubernetes .client .util ;
1414
15+ import com .google .gson .JsonElement ;
16+ import com .google .gson .JsonObject ;
17+ import com .google .gson .JsonParseException ;
18+ import com .google .gson .JsonParser ;
1519import io .kubernetes .client .util .authenticators .Authenticator ;
1620import io .kubernetes .client .util .authenticators .AzureActiveDirectoryAuthenticator ;
1721import io .kubernetes .client .util .authenticators .GCPAuthenticator ;
22+ import java .io .File ;
1823import java .io .IOException ;
24+ import java .io .InputStream ;
25+ import java .io .InputStreamReader ;
1926import java .io .Reader ;
2027import java .nio .charset .StandardCharsets ;
2128import java .nio .file .FileSystems ;
2229import java .nio .file .Files ;
30+ import java .nio .file .Path ;
2331import java .nio .file .Paths ;
2432import java .util .ArrayList ;
2533import java .util .HashMap ;
34+ import java .util .List ;
2635import java .util .Map ;
2736import org .apache .commons .codec .binary .Base64 ;
2837import org .slf4j .Logger ;
@@ -54,6 +63,7 @@ public class KubeConfig {
5463 String currentNamespace ;
5564 Object preferences ;
5665 ConfigPersister persister ;
66+ private File file ;
5767
5868 public static void registerAuthenticator (Authenticator auth ) {
5969 synchronized (authenticators ) {
@@ -185,6 +195,7 @@ public String getPassword() {
185195 return getData (currentUser , "password" );
186196 }
187197
198+ @ SuppressWarnings ("unchecked" )
188199 public String getAccessToken () {
189200 if (currentUser == null ) {
190201 return null ;
@@ -214,6 +225,11 @@ public String getAccessToken() {
214225 }
215226 }
216227 }
228+ String tokenViaExecCredential =
229+ tokenViaExecCredential ((Map <String , Object >) currentUser .get ("exec" ));
230+ if (tokenViaExecCredential != null ) {
231+ return tokenViaExecCredential ;
232+ }
217233 if (currentUser .containsKey ("token" )) {
218234 return (String ) currentUser .get ("token" );
219235 }
@@ -229,6 +245,102 @@ public String getAccessToken() {
229245 return null ;
230246 }
231247
248+ /**
249+ * Attempt to create an access token by running a configured external program.
250+ *
251+ * @see <a
252+ * href="https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins">
253+ * Authenticating » client-go credential plugins</a>
254+ */
255+ @ SuppressWarnings ("unchecked" )
256+ private String tokenViaExecCredential (Map <String , Object > execMap ) {
257+ if (execMap == null ) {
258+ return null ;
259+ }
260+ String apiVersion = (String ) execMap .get ("apiVersion" );
261+ if (!"client.authentication.k8s.io/v1beta1" .equals (apiVersion )
262+ && !"client.authentication.k8s.io/v1alpha1" .equals (apiVersion )) {
263+ log .error ("Unrecognized user.exec.apiVersion: {}" , apiVersion );
264+ return null ;
265+ }
266+ String command = (String ) execMap .get ("command" );
267+ JsonElement root = runExec (command , (List ) execMap .get ("args" ), (List ) execMap .get ("env" ));
268+ if (root == null ) {
269+ return null ;
270+ }
271+ if (!"ExecCredential" .equals (root .getAsJsonObject ().get ("kind" ).getAsString ())) {
272+ log .error ("Unrecognized kind in response" );
273+ return null ;
274+ }
275+ if (!apiVersion .equals (root .getAsJsonObject ().get ("apiVersion" ).getAsString ())) {
276+ log .error ("Mismatched apiVersion in response" );
277+ return null ;
278+ }
279+ JsonObject status = root .getAsJsonObject ().get ("status" ).getAsJsonObject ();
280+ JsonElement token = status .get ("token" );
281+ if (token == null ) {
282+ // TODO handle clientCertificateData/clientKeyData
283+ // (KubeconfigAuthentication is not yet set up for that to be dynamic)
284+ log .warn ("No token produced by {}" , command );
285+ return null ;
286+ }
287+ log .debug ("Obtained a token from {}" , command );
288+ return token .getAsString ();
289+ // TODO cache tokens between calls, up to .status.expirationTimestamp
290+ // TODO a 401 is supposed to force a refresh,
291+ // but KubeconfigAuthentication hardcodes AccessTokenAuthentication which does not support that
292+ // and anyway ClientBuilder only calls Authenticator.provide once per ApiClient;
293+ // we would need to do it on every request
294+ }
295+
296+ private JsonElement runExec (String command , List <String > args , List <Map <String , String >> env ) {
297+ List <String > argv = new ArrayList <>();
298+ if (command .contains ("/" ) || command .contains ("\\ " )) {
299+ // Spec is unclear on what should be treated as a “relative command path”.
300+ // This clause should cover anything not resolved from $PATH / %Path%.
301+ Path resolvedCommand = file .toPath ().getParent ().resolve (command ).normalize ();
302+ if (!Files .exists (resolvedCommand )) {
303+ log .error ("No such file: {}" , resolvedCommand );
304+ return null ;
305+ }
306+ // Not checking isRegularFile or isExecutable here; leave that to ProcessBuilder.start.
307+ log .debug ("Resolved {} to {}" , command , resolvedCommand );
308+ argv .add (resolvedCommand .toString ());
309+ } else {
310+ argv .add (command );
311+ }
312+ if (args != null ) {
313+ argv .addAll (args );
314+ }
315+ ProcessBuilder pb = new ProcessBuilder (argv );
316+ if (env != null ) {
317+ for (Map <String , String > entry : env ) {
318+ pb .environment ().put (entry .get ("name" ), entry .get ("value" ));
319+ }
320+ }
321+ pb .redirectError (ProcessBuilder .Redirect .INHERIT );
322+ try {
323+ Process proc = pb .start ();
324+ JsonElement root ;
325+ try (InputStream is = proc .getInputStream ();
326+ Reader r = new InputStreamReader (is , StandardCharsets .UTF_8 )) {
327+ root = new JsonParser ().parse (r );
328+ } catch (JsonParseException x ) {
329+ log .error ("Failed to parse output of " + command , x );
330+ return null ;
331+ }
332+ int r = proc .waitFor ();
333+ if (r != 0 ) {
334+ log .error ("{} failed with exit code {}" , command , r );
335+ return null ;
336+ }
337+ return root ;
338+ } catch (IOException | InterruptedException x ) {
339+ log .error ("Failed to run " + command , x );
340+ return null ;
341+ }
342+ }
343+
232344 public boolean verifySSL () {
233345 if (currentCluster == null ) {
234346 return false ;
@@ -248,6 +360,15 @@ public void setPersistConfig(ConfigPersister persister) {
248360 this .persister = persister ;
249361 }
250362
363+ /**
364+ * Indicates a file from which this configuration was loaded.
365+ *
366+ * @param file a file path, available for use when resolving relative file paths
367+ */
368+ public void setFile (File file ) {
369+ this .file = file ;
370+ }
371+
251372 public void setPreferences (Object preferences ) {
252373 this .preferences = preferences ;
253374 }
0 commit comments