12
12
*/
13
13
package io .kubernetes .client .util ;
14
14
15
+ import com .google .gson .JsonElement ;
16
+ import com .google .gson .JsonObject ;
17
+ import com .google .gson .JsonParseException ;
18
+ import com .google .gson .JsonParser ;
15
19
import io .kubernetes .client .util .authenticators .Authenticator ;
16
20
import io .kubernetes .client .util .authenticators .AzureActiveDirectoryAuthenticator ;
17
21
import io .kubernetes .client .util .authenticators .GCPAuthenticator ;
22
+ import java .io .File ;
18
23
import java .io .IOException ;
24
+ import java .io .InputStream ;
25
+ import java .io .InputStreamReader ;
19
26
import java .io .Reader ;
20
27
import java .nio .charset .StandardCharsets ;
21
28
import java .nio .file .FileSystems ;
22
29
import java .nio .file .Files ;
30
+ import java .nio .file .Path ;
23
31
import java .nio .file .Paths ;
24
32
import java .util .ArrayList ;
25
33
import java .util .HashMap ;
34
+ import java .util .List ;
26
35
import java .util .Map ;
27
36
import org .apache .commons .codec .binary .Base64 ;
28
37
import org .slf4j .Logger ;
@@ -54,6 +63,7 @@ public class KubeConfig {
54
63
String currentNamespace ;
55
64
Object preferences ;
56
65
ConfigPersister persister ;
66
+ private File file ;
57
67
58
68
public static void registerAuthenticator (Authenticator auth ) {
59
69
synchronized (authenticators ) {
@@ -185,6 +195,7 @@ public String getPassword() {
185
195
return getData (currentUser , "password" );
186
196
}
187
197
198
+ @ SuppressWarnings ("unchecked" )
188
199
public String getAccessToken () {
189
200
if (currentUser == null ) {
190
201
return null ;
@@ -214,6 +225,11 @@ public String getAccessToken() {
214
225
}
215
226
}
216
227
}
228
+ String tokenViaExecCredential =
229
+ tokenViaExecCredential ((Map <String , Object >) currentUser .get ("exec" ));
230
+ if (tokenViaExecCredential != null ) {
231
+ return tokenViaExecCredential ;
232
+ }
217
233
if (currentUser .containsKey ("token" )) {
218
234
return (String ) currentUser .get ("token" );
219
235
}
@@ -229,6 +245,102 @@ public String getAccessToken() {
229
245
return null ;
230
246
}
231
247
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
+
232
344
public boolean verifySSL () {
233
345
if (currentCluster == null ) {
234
346
return false ;
@@ -248,6 +360,15 @@ public void setPersistConfig(ConfigPersister persister) {
248
360
this .persister = persister ;
249
361
}
250
362
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
+
251
372
public void setPreferences (Object preferences ) {
252
373
this .preferences = preferences ;
253
374
}
0 commit comments