Skip to content

Commit ae9dd04

Browse files
authored
Allow token refresh for GCP (#402)
1 parent cfc4306 commit ae9dd04

10 files changed

+186
-18
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
using System;
2+
using System.Diagnostics;
3+
using System.Net.Http.Headers;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using k8s.Exceptions;
7+
using Microsoft.Rest;
8+
using Newtonsoft.Json.Linq;
9+
10+
namespace k8s.Authentication
11+
{
12+
public class GcpTokenProvider : ITokenProvider
13+
{
14+
private readonly string _gcloudCli;
15+
private string _token;
16+
private DateTime _expiry;
17+
18+
public GcpTokenProvider(string gcloudCli)
19+
{
20+
_gcloudCli = gcloudCli;
21+
}
22+
23+
public async Task<AuthenticationHeaderValue> GetAuthenticationHeaderAsync(CancellationToken cancellationToken)
24+
{
25+
if (DateTime.UtcNow.AddSeconds(30) > _expiry)
26+
{
27+
await RefreshToken();
28+
}
29+
return new AuthenticationHeaderValue("Bearer", _token);
30+
}
31+
32+
private async Task RefreshToken()
33+
{
34+
var process = new Process
35+
{
36+
StartInfo =
37+
{
38+
FileName = _gcloudCli,
39+
Arguments = "config config-helper --format=json",
40+
UseShellExecute = false,
41+
CreateNoWindow = true,
42+
RedirectStandardOutput = true,
43+
RedirectStandardError = true
44+
},
45+
EnableRaisingEvents = true
46+
};
47+
var tcs = new TaskCompletionSource<bool>();
48+
process.Exited += (sender, arg) =>
49+
{
50+
tcs.SetResult(true);
51+
};
52+
process.Start();
53+
var output = process.StandardOutput.ReadToEndAsync();
54+
var err = process.StandardError.ReadToEndAsync();
55+
56+
await Task.WhenAll(tcs.Task, output, err);
57+
58+
if (process.ExitCode != 0)
59+
{
60+
throw new KubernetesClientException($"Unable to obtain a token via gcloud command. Error code {process.ExitCode}. \n {err}");
61+
}
62+
63+
var json = JToken.Parse(await output);
64+
_token = json["credential"]["access_token"].Value<string>();
65+
_expiry = json["credential"]["token_expiry"].Value<DateTime>();
66+
}
67+
}
68+
}

src/KubernetesClient/Kubernetes.ConfigInit.cs

+5-2
Original file line numberDiff line numberDiff line change
@@ -306,8 +306,11 @@ public static ServiceClientCredentials CreateCredentials(KubernetesClientConfigu
306306
{
307307
throw new ArgumentNullException(nameof(config));
308308
}
309-
310-
if (!string.IsNullOrEmpty(config.AccessToken))
309+
if (config.TokenProvider != null)
310+
{
311+
return new TokenCredentials(config.TokenProvider);
312+
}
313+
else if (!string.IsNullOrEmpty(config.AccessToken))
311314
{
312315
return new TokenCredentials(config.AccessToken);
313316
}

src/KubernetesClient/KubernetesClientConfiguration.ConfigFile.cs

+3-16
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.Runtime.InteropServices;
1010
using System.Security.Cryptography.X509Certificates;
1111
using System.Threading.Tasks;
12+
using k8s.Authentication;
1213
using k8s.Exceptions;
1314
using k8s.KubeConfigModels;
1415

@@ -367,23 +368,9 @@ private void SetUserDetails(K8SConfiguration k8SConfig, Context activeContext)
367368
}
368369
case "gcp":
369370
{
371+
// config
370372
var config = userDetails.UserCredentials.AuthProvider.Config;
371-
const string keyExpire = "expiry";
372-
if (config.ContainsKey(keyExpire))
373-
{
374-
if (DateTimeOffset.TryParse(config[keyExpire]
375-
, out DateTimeOffset expires))
376-
{
377-
if (DateTimeOffset.Compare(expires
378-
, DateTimeOffset.Now)
379-
<= 0)
380-
{
381-
throw new KubeConfigException("Refresh not supported.");
382-
}
383-
}
384-
}
385-
386-
AccessToken = config["access-token"];
373+
TokenProvider = new GcpTokenProvider(config["cmd-path"]);
387374
userCredentialsFound = true;
388375
break;
389376
}

src/KubernetesClient/KubernetesClientConfiguration.cs

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Collections.Generic;
22
using System.Security.Cryptography.X509Certificates;
3+
using Microsoft.Rest;
34

45
namespace k8s
56
{
@@ -76,5 +77,7 @@ public partial class KubernetesClientConfiguration
7677
/// </summary>
7778
/// <value>The access token.</value>
7879
public string AccessToken { get; set; }
80+
81+
public ITokenProvider TokenProvider { get; set; }
7982
}
8083
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using System;
2+
using System.IO;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
using FluentAssertions;
6+
using k8s.Authentication;
7+
using Xunit;
8+
9+
namespace k8s.Tests
10+
{
11+
public class GcpTokenProviderTests
12+
{
13+
[OperatingSystemDependentFact(Exclude = OperatingSystem.OSX)]
14+
public async Task GetToken()
15+
{
16+
var isWindows = Environment.OSVersion.Platform == PlatformID.Win32NT;
17+
var cmd = Path.Combine(Directory.GetCurrentDirectory(), "assets", isWindows ? "mock-gcloud.cmd" : "mock-gcloud.sh");
18+
if (!isWindows)
19+
{
20+
System.Diagnostics.Process.Start("chmod", $"+x {cmd}").WaitForExit();
21+
}
22+
var sut = new GcpTokenProvider(cmd);
23+
var result = await sut.GetAuthenticationHeaderAsync(CancellationToken.None);
24+
result.Scheme.Should().Be("Bearer");
25+
result.Parameter.Should().Be("ACCESS-TOKEN");
26+
}
27+
}
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using System;
2+
3+
namespace k8s.Tests
4+
{
5+
[Flags]
6+
public enum OperatingSystem
7+
{
8+
Windows = 1,
9+
Linux = 2,
10+
OSX = 4
11+
}
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using System.Runtime.InteropServices;
2+
using Xunit;
3+
4+
namespace k8s.Tests
5+
{
6+
public class OperatingSystemDependentFactAttribute : FactAttribute
7+
{
8+
public OperatingSystem Include { get; set; } = OperatingSystem.Linux | OperatingSystem.Windows | OperatingSystem.OSX;
9+
public OperatingSystem Exclude { get; set; }
10+
11+
public override string Skip
12+
{
13+
get => IsOS(Include) && !IsOS(Exclude) ? null : "Not compatible with current OS";
14+
set { }
15+
}
16+
17+
private bool IsOS(OperatingSystem operatingSystem)
18+
{
19+
if (operatingSystem.HasFlag(OperatingSystem.Linux) && RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
20+
{
21+
return true;
22+
}
23+
24+
if (operatingSystem.HasFlag(OperatingSystem.Windows) && RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
25+
{
26+
return true;
27+
}
28+
29+
if (operatingSystem.HasFlag(OperatingSystem.OSX) && RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
30+
{
31+
return true;
32+
}
33+
34+
return false;
35+
}
36+
}
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"configuration": {
3+
"active_configuration": "default",
4+
"properties": {
5+
"compute": {
6+
"region": "us-east1",
7+
"zone": "us-east1-b"
8+
},
9+
"core": {
10+
"account": "some@account.io",
11+
"disable_usage_reporting": "True",
12+
"project": "fe-astakhov"
13+
}
14+
}
15+
},
16+
"credential": {
17+
"access_token": "ACCESS-TOKEN",
18+
"token_expiry": "2020-03-20T07:09:20Z"
19+
},
20+
"sentinels": {
21+
"config_sentinel": "C:\\Users\\Andrew\\AppData\\Roaming\\gcloud\\config_sentinel"
22+
}
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@echo off
2+
type %~dp0\gcloud-config-helper.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/bin/bash
2+
SCRIPT=$(readlink -f "$0")
3+
SCRIPTPATH=$(dirname "$SCRIPT")
4+
OUTPUT_JSON=$SCRIPTPATH/gcloud-config-helper.json
5+
cat $OUTPUT_JSON

0 commit comments

Comments
 (0)