1
1
using System ;
2
+ #if NETSTANDARD2_0
3
+ using Newtonsoft . Json ;
2
4
using System . Collections . Generic ;
5
+ using System . Diagnostics ;
6
+ #endif
3
7
using System . IO ;
4
8
using System . Linq ;
5
9
using System . Runtime . InteropServices ;
8
12
using k8s . Exceptions ;
9
13
using k8s . KubeConfigModels ;
10
14
15
+
11
16
namespace k8s
12
17
{
13
18
public partial class KubernetesClientConfiguration
@@ -28,15 +33,19 @@ public partial class KubernetesClientConfiguration
28
33
/// <summary>
29
34
/// Initializes a new instance of the <see cref="KubernetesClientConfiguration" /> from config file
30
35
/// </summary>
31
- public static KubernetesClientConfiguration BuildDefaultConfig ( ) {
36
+ public static KubernetesClientConfiguration BuildDefaultConfig ( )
37
+ {
32
38
var kubeconfig = Environment . GetEnvironmentVariable ( "KUBECONFIG" ) ;
33
- if ( kubeconfig != null ) {
39
+ if ( kubeconfig != null )
40
+ {
34
41
return BuildConfigFromConfigFile ( kubeconfigPath : kubeconfig ) ;
35
42
}
36
- if ( File . Exists ( KubeConfigDefaultLocation ) ) {
43
+ if ( File . Exists ( KubeConfigDefaultLocation ) )
44
+ {
37
45
return BuildConfigFromConfigFile ( kubeconfigPath : KubeConfigDefaultLocation ) ;
38
46
}
39
- if ( IsInCluster ( ) ) {
47
+ if ( IsInCluster ( ) )
48
+ {
40
49
return InClusterConfig ( ) ;
41
50
}
42
51
var config = new KubernetesClientConfiguration ( ) ;
@@ -150,7 +159,7 @@ private static KubernetesClientConfiguration GetKubernetesClientConfiguration(st
150
159
var k8SConfiguration = new KubernetesClientConfiguration ( ) ;
151
160
152
161
currentContext = currentContext ?? k8SConfig . CurrentContext ;
153
- // only init context if context if set
162
+ // only init context if context is set
154
163
if ( currentContext != null )
155
164
{
156
165
k8SConfiguration . InitializeContext ( k8SConfig , currentContext ) ;
@@ -214,7 +223,7 @@ private void SetClusterDetails(K8SConfiguration k8SConfig, Context activeContext
214
223
Host = clusterDetails . ClusterEndpoint . Server ;
215
224
SkipTlsVerify = clusterDetails . ClusterEndpoint . SkipTlsVerify ;
216
225
217
- if ( ! Uri . TryCreate ( Host , UriKind . Absolute , out Uri uri ) )
226
+ if ( ! Uri . TryCreate ( Host , UriKind . Absolute , out Uri uri ) )
218
227
{
219
228
throw new KubeConfigException ( $ "Bad server host URL `{ Host } ` (cannot be parsed)") ;
220
229
}
@@ -294,65 +303,81 @@ private void SetUserDetails(K8SConfiguration k8SConfig, Context activeContext)
294
303
switch ( userDetails . UserCredentials . AuthProvider . Name )
295
304
{
296
305
case "azure" :
297
- {
298
- var config = userDetails . UserCredentials . AuthProvider . Config ;
299
- if ( config . ContainsKey ( "expires-on" ) )
300
306
{
301
- var expiresOn = Int32 . Parse ( config [ "expires-on" ] ) ;
302
- DateTimeOffset expires ;
303
- #if NET452
307
+ var config = userDetails . UserCredentials . AuthProvider . Config ;
308
+ if ( config . ContainsKey ( "expires-on" ) )
309
+ {
310
+ var expiresOn = Int32 . Parse ( config [ "expires-on" ] ) ;
311
+ DateTimeOffset expires ;
312
+ #if NET452
304
313
var epoch = new DateTimeOffset ( 1970 , 1 , 1 , 0 , 0 , 0 , TimeSpan . Zero ) ;
305
314
expires = epoch . AddSeconds ( expiresOn ) ;
306
- #else
307
- expires = DateTimeOffset . FromUnixTimeSeconds ( expiresOn ) ;
308
- #endif
315
+ #else
316
+ expires = DateTimeOffset . FromUnixTimeSeconds ( expiresOn ) ;
317
+ #endif
309
318
310
- if ( DateTimeOffset . Compare ( expires
311
- , DateTimeOffset . Now )
312
- <= 0 )
313
- {
314
- var tenantId = config [ "tenant-id" ] ;
315
- var clientId = config [ "client-id" ] ;
316
- var apiServerId = config [ "apiserver-id" ] ;
317
- var refresh = config [ "refresh-token" ] ;
318
- var newToken = RenewAzureToken ( tenantId
319
- , clientId
320
- , apiServerId
321
- , refresh ) ;
322
- config [ "access-token" ] = newToken ;
319
+ if ( DateTimeOffset . Compare ( expires
320
+ , DateTimeOffset . Now )
321
+ <= 0 )
322
+ {
323
+ var tenantId = config [ "tenant-id" ] ;
324
+ var clientId = config [ "client-id" ] ;
325
+ var apiServerId = config [ "apiserver-id" ] ;
326
+ var refresh = config [ "refresh-token" ] ;
327
+ var newToken = RenewAzureToken ( tenantId
328
+ , clientId
329
+ , apiServerId
330
+ , refresh ) ;
331
+ config [ "access-token" ] = newToken ;
332
+ }
323
333
}
324
- }
325
334
326
- AccessToken = config [ "access-token" ] ;
327
- userCredentialsFound = true ;
328
- break ;
329
- }
335
+ AccessToken = config [ "access-token" ] ;
336
+ userCredentialsFound = true ;
337
+ break ;
338
+ }
330
339
case "gcp" :
331
- {
332
- var config = userDetails . UserCredentials . AuthProvider . Config ;
333
- const string keyExpire = "expiry" ;
334
- if ( config . ContainsKey ( keyExpire ) )
335
340
{
336
- if ( DateTimeOffset . TryParse ( config [ keyExpire ]
337
- , out DateTimeOffset expires ) )
341
+ var config = userDetails . UserCredentials . AuthProvider . Config ;
342
+ const string keyExpire = "expiry" ;
343
+ if ( config . ContainsKey ( keyExpire ) )
338
344
{
339
- if ( DateTimeOffset . Compare ( expires
340
- , DateTimeOffset . Now )
341
- <= 0 )
345
+ if ( DateTimeOffset . TryParse ( config [ keyExpire ]
346
+ , out DateTimeOffset expires ) )
342
347
{
343
- throw new KubeConfigException ( "Refresh not supported." ) ;
348
+ if ( DateTimeOffset . Compare ( expires
349
+ , DateTimeOffset . Now )
350
+ <= 0 )
351
+ {
352
+ throw new KubeConfigException ( "Refresh not supported." ) ;
353
+ }
344
354
}
345
355
}
346
- }
347
356
348
- AccessToken = config [ "access-token" ] ;
349
- userCredentialsFound = true ;
350
- break ;
351
- }
357
+ AccessToken = config [ "access-token" ] ;
358
+ userCredentialsFound = true ;
359
+ break ;
360
+ }
352
361
}
353
362
}
354
363
}
355
364
365
+ #if NETSTANDARD2_0
366
+ if ( userDetails . UserCredentials . ExternalExecution != null )
367
+ {
368
+ if ( string . IsNullOrWhiteSpace ( userDetails . UserCredentials . ExternalExecution . Command ) )
369
+ throw new KubeConfigException (
370
+ "External command execution to receive user credentials must include a command to execute" ) ;
371
+ if ( string . IsNullOrWhiteSpace ( userDetails . UserCredentials . ExternalExecution . ApiVersion ) )
372
+ throw new KubeConfigException ( "External command execution missing ApiVersion key" ) ;
373
+
374
+ var token = ExecuteExternalCommand ( userDetails . UserCredentials . ExternalExecution ) ;
375
+ AccessToken = token ;
376
+
377
+ userCredentialsFound = true ;
378
+ }
379
+ #endif
380
+
356
381
if ( ! userCredentialsFound )
357
382
{
358
383
throw new KubeConfigException (
@@ -365,6 +390,84 @@ public static string RenewAzureToken(string tenantId, string clientId, string ap
365
390
throw new KubeConfigException ( "Refresh not supported." ) ;
366
391
}
367
392
393
+ #if NETSTANDARD2_0
394
+ /// <summary>
395
+ /// Implementation of the proposal for out-of-tree client
396
+ /// authentication providers as described here --
397
+ /// https://github.com/kubernetes/community/blob/master/contributors/design-proposals/auth/kubectl-exec-plugins.md
398
+ /// Took inspiration from python exec_provider.py --
399
+ /// https://github.com/kubernetes-client/python-base/blob/master/config/exec_provider.py
400
+ /// </summary>
401
+ /// <param name="config">The external command execution configuration</param>
402
+ /// <returns>The token received from the external commmand execution</returns>
403
+ public static string ExecuteExternalCommand ( ExternalExecution config )
404
+ {
405
+ var execInfo = new Dictionary < string , dynamic >
406
+ {
407
+ { "apiVersion" , config . ApiVersion } ,
408
+ { "kind" , "ExecCredentials" } ,
409
+ { "spec" , new Dictionary < string , bool >
410
+ {
411
+ { "interactive" , Environment . UserInteractive }
412
+ } }
413
+ } ;
414
+
415
+ var process = new Process ( ) ;
416
+
417
+ process . StartInfo . Environment . Add ( "KUBERNETES_EXEC_INFO" ,
418
+ JsonConvert . SerializeObject ( execInfo ) ) ;
419
+
420
+ if ( config . EnvironmentVariables != null )
421
+ foreach ( var configEnvironmentVariableKey in config . EnvironmentVariables . Keys )
422
+ process . StartInfo . Environment . Add ( key : configEnvironmentVariableKey ,
423
+ value : config . EnvironmentVariables [ configEnvironmentVariableKey ] ) ;
424
+
425
+ process . StartInfo . FileName = config . Command ;
426
+ if ( config . Arguments != null )
427
+ process . StartInfo . Arguments = string . Join ( " " , config . Arguments ) ;
428
+ process . StartInfo . RedirectStandardOutput = true ;
429
+ process . StartInfo . RedirectStandardError = true ;
430
+ process . StartInfo . UseShellExecute = false ;
431
+
432
+ try
433
+ {
434
+ process . Start ( ) ;
435
+ }
436
+ catch ( Exception ex )
437
+ {
438
+ throw new KubeConfigException ( $ "external exec failed due to: { ex . Message } ") ;
439
+ }
440
+
441
+ var stdout = process . StandardOutput . ReadToEnd ( ) ;
442
+ var stderr = process . StandardOutput . ReadToEnd ( ) ;
443
+ if ( string . IsNullOrWhiteSpace ( stderr ) == false )
444
+ throw new KubeConfigException ( $ "external exec failed due to: { stderr } ") ;
445
+
446
+ // Wait for a maximum of 5 seconds, if a response takes longer probably something went wrong...
447
+ process . WaitForExit ( 5 ) ;
448
+
449
+ try
450
+ {
451
+ var responseObject = JsonConvert . DeserializeObject < ExecCredentialResponse > ( stdout ) ;
452
+ if ( responseObject == null || responseObject . ApiVersion != config . ApiVersion )
453
+ throw new KubeConfigException (
454
+ $ "external exec failed because api version { responseObject . ApiVersion } does not match { config . ApiVersion } ") ;
455
+ return responseObject . Status [ "token" ] ;
456
+ }
457
+ catch ( JsonSerializationException ex )
458
+ {
459
+ throw new KubeConfigException ( $ "external exec failed due to failed deserialization process: { ex } ") ;
460
+ }
461
+ catch ( Exception ex )
462
+ {
463
+ throw new KubeConfigException ( $ "external exec failed due to uncaught exception: { ex } ") ;
464
+ }
465
+
466
+
467
+
468
+ }
469
+ #endif
470
+
368
471
/// <summary>
369
472
/// Loads entire Kube Config from default or explicit file path
370
473
/// </summary>
0 commit comments