Allow token refresh for GCP (#402)
This commit is contained in:
68
src/KubernetesClient/Authentication/GcpTokenProvider.cs
Normal file
68
src/KubernetesClient/Authentication/GcpTokenProvider.cs
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using k8s.Exceptions;
|
||||||
|
using Microsoft.Rest;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace k8s.Authentication
|
||||||
|
{
|
||||||
|
public class GcpTokenProvider : ITokenProvider
|
||||||
|
{
|
||||||
|
private readonly string _gcloudCli;
|
||||||
|
private string _token;
|
||||||
|
private DateTime _expiry;
|
||||||
|
|
||||||
|
public GcpTokenProvider(string gcloudCli)
|
||||||
|
{
|
||||||
|
_gcloudCli = gcloudCli;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<AuthenticationHeaderValue> GetAuthenticationHeaderAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (DateTime.UtcNow.AddSeconds(30) > _expiry)
|
||||||
|
{
|
||||||
|
await RefreshToken();
|
||||||
|
}
|
||||||
|
return new AuthenticationHeaderValue("Bearer", _token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RefreshToken()
|
||||||
|
{
|
||||||
|
var process = new Process
|
||||||
|
{
|
||||||
|
StartInfo =
|
||||||
|
{
|
||||||
|
FileName = _gcloudCli,
|
||||||
|
Arguments = "config config-helper --format=json",
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true
|
||||||
|
},
|
||||||
|
EnableRaisingEvents = true
|
||||||
|
};
|
||||||
|
var tcs = new TaskCompletionSource<bool>();
|
||||||
|
process.Exited += (sender, arg) =>
|
||||||
|
{
|
||||||
|
tcs.SetResult(true);
|
||||||
|
};
|
||||||
|
process.Start();
|
||||||
|
var output = process.StandardOutput.ReadToEndAsync();
|
||||||
|
var err = process.StandardError.ReadToEndAsync();
|
||||||
|
|
||||||
|
await Task.WhenAll(tcs.Task, output, err);
|
||||||
|
|
||||||
|
if (process.ExitCode != 0)
|
||||||
|
{
|
||||||
|
throw new KubernetesClientException($"Unable to obtain a token via gcloud command. Error code {process.ExitCode}. \n {err}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var json = JToken.Parse(await output);
|
||||||
|
_token = json["credential"]["access_token"].Value<string>();
|
||||||
|
_expiry = json["credential"]["token_expiry"].Value<DateTime>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -306,8 +306,11 @@ namespace k8s
|
|||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(config));
|
throw new ArgumentNullException(nameof(config));
|
||||||
}
|
}
|
||||||
|
if (config.TokenProvider != null)
|
||||||
if (!string.IsNullOrEmpty(config.AccessToken))
|
{
|
||||||
|
return new TokenCredentials(config.TokenProvider);
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrEmpty(config.AccessToken))
|
||||||
{
|
{
|
||||||
return new TokenCredentials(config.AccessToken);
|
return new TokenCredentials(config.AccessToken);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ using System.Linq;
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Security.Cryptography.X509Certificates;
|
using System.Security.Cryptography.X509Certificates;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using k8s.Authentication;
|
||||||
using k8s.Exceptions;
|
using k8s.Exceptions;
|
||||||
using k8s.KubeConfigModels;
|
using k8s.KubeConfigModels;
|
||||||
|
|
||||||
@@ -367,23 +368,9 @@ namespace k8s
|
|||||||
}
|
}
|
||||||
case "gcp":
|
case "gcp":
|
||||||
{
|
{
|
||||||
|
// config
|
||||||
var config = userDetails.UserCredentials.AuthProvider.Config;
|
var config = userDetails.UserCredentials.AuthProvider.Config;
|
||||||
const string keyExpire = "expiry";
|
TokenProvider = new GcpTokenProvider(config["cmd-path"]);
|
||||||
if (config.ContainsKey(keyExpire))
|
|
||||||
{
|
|
||||||
if (DateTimeOffset.TryParse(config[keyExpire]
|
|
||||||
, out DateTimeOffset expires))
|
|
||||||
{
|
|
||||||
if (DateTimeOffset.Compare(expires
|
|
||||||
, DateTimeOffset.Now)
|
|
||||||
<= 0)
|
|
||||||
{
|
|
||||||
throw new KubeConfigException("Refresh not supported.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AccessToken = config["access-token"];
|
|
||||||
userCredentialsFound = true;
|
userCredentialsFound = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Security.Cryptography.X509Certificates;
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using Microsoft.Rest;
|
||||||
|
|
||||||
namespace k8s
|
namespace k8s
|
||||||
{
|
{
|
||||||
@@ -76,5 +77,7 @@ namespace k8s
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The access token.</value>
|
/// <value>The access token.</value>
|
||||||
public string AccessToken { get; set; }
|
public string AccessToken { get; set; }
|
||||||
|
|
||||||
|
public ITokenProvider TokenProvider { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
28
tests/KubernetesClient.Tests/GcpTokenProviderTests.cs
Normal file
28
tests/KubernetesClient.Tests/GcpTokenProviderTests.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using FluentAssertions;
|
||||||
|
using k8s.Authentication;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace k8s.Tests
|
||||||
|
{
|
||||||
|
public class GcpTokenProviderTests
|
||||||
|
{
|
||||||
|
[OperatingSystemDependentFact(Exclude = OperatingSystem.OSX)]
|
||||||
|
public async Task GetToken()
|
||||||
|
{
|
||||||
|
var isWindows = Environment.OSVersion.Platform == PlatformID.Win32NT;
|
||||||
|
var cmd = Path.Combine(Directory.GetCurrentDirectory(), "assets", isWindows ? "mock-gcloud.cmd" : "mock-gcloud.sh");
|
||||||
|
if (!isWindows)
|
||||||
|
{
|
||||||
|
System.Diagnostics.Process.Start("chmod", $"+x {cmd}").WaitForExit();
|
||||||
|
}
|
||||||
|
var sut = new GcpTokenProvider(cmd);
|
||||||
|
var result = await sut.GetAuthenticationHeaderAsync(CancellationToken.None);
|
||||||
|
result.Scheme.Should().Be("Bearer");
|
||||||
|
result.Parameter.Should().Be("ACCESS-TOKEN");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
tests/KubernetesClient.Tests/OperatingSystem.cs
Normal file
12
tests/KubernetesClient.Tests/OperatingSystem.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace k8s.Tests
|
||||||
|
{
|
||||||
|
[Flags]
|
||||||
|
public enum OperatingSystem
|
||||||
|
{
|
||||||
|
Windows = 1,
|
||||||
|
Linux = 2,
|
||||||
|
OSX = 4
|
||||||
|
}
|
||||||
|
}
|
||||||
37
tests/KubernetesClient.Tests/OperatingSystemFact.cs
Normal file
37
tests/KubernetesClient.Tests/OperatingSystemFact.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace k8s.Tests
|
||||||
|
{
|
||||||
|
public class OperatingSystemDependentFactAttribute : FactAttribute
|
||||||
|
{
|
||||||
|
public OperatingSystem Include { get; set; } = OperatingSystem.Linux | OperatingSystem.Windows | OperatingSystem.OSX;
|
||||||
|
public OperatingSystem Exclude { get; set; }
|
||||||
|
|
||||||
|
public override string Skip
|
||||||
|
{
|
||||||
|
get => IsOS(Include) && !IsOS(Exclude) ? null : "Not compatible with current OS";
|
||||||
|
set { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsOS(OperatingSystem operatingSystem)
|
||||||
|
{
|
||||||
|
if (operatingSystem.HasFlag(OperatingSystem.Linux) && RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (operatingSystem.HasFlag(OperatingSystem.Windows) && RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (operatingSystem.HasFlag(OperatingSystem.OSX) && RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"configuration": {
|
||||||
|
"active_configuration": "default",
|
||||||
|
"properties": {
|
||||||
|
"compute": {
|
||||||
|
"region": "us-east1",
|
||||||
|
"zone": "us-east1-b"
|
||||||
|
},
|
||||||
|
"core": {
|
||||||
|
"account": "some@account.io",
|
||||||
|
"disable_usage_reporting": "True",
|
||||||
|
"project": "fe-astakhov"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"credential": {
|
||||||
|
"access_token": "ACCESS-TOKEN",
|
||||||
|
"token_expiry": "2020-03-20T07:09:20Z"
|
||||||
|
},
|
||||||
|
"sentinels": {
|
||||||
|
"config_sentinel": "C:\\Users\\Andrew\\AppData\\Roaming\\gcloud\\config_sentinel"
|
||||||
|
}
|
||||||
|
}
|
||||||
2
tests/KubernetesClient.Tests/assets/mock-gcloud.cmd
Normal file
2
tests/KubernetesClient.Tests/assets/mock-gcloud.cmd
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
@echo off
|
||||||
|
type %~dp0\gcloud-config-helper.json
|
||||||
5
tests/KubernetesClient.Tests/assets/mock-gcloud.sh
Normal file
5
tests/KubernetesClient.Tests/assets/mock-gcloud.sh
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
SCRIPT=$(readlink -f "$0")
|
||||||
|
SCRIPTPATH=$(dirname "$SCRIPT")
|
||||||
|
OUTPUT_JSON=$SCRIPTPATH/gcloud-config-helper.json
|
||||||
|
cat $OUTPUT_JSON
|
||||||
Reference in New Issue
Block a user