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));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(config.AccessToken))
|
||||
if (config.TokenProvider != null)
|
||||
{
|
||||
return new TokenCredentials(config.TokenProvider);
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(config.AccessToken))
|
||||
{
|
||||
return new TokenCredentials(config.AccessToken);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Threading.Tasks;
|
||||
using k8s.Authentication;
|
||||
using k8s.Exceptions;
|
||||
using k8s.KubeConfigModels;
|
||||
|
||||
@@ -367,23 +368,9 @@ namespace k8s
|
||||
}
|
||||
case "gcp":
|
||||
{
|
||||
// config
|
||||
var config = userDetails.UserCredentials.AuthProvider.Config;
|
||||
const string keyExpire = "expiry";
|
||||
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"];
|
||||
TokenProvider = new GcpTokenProvider(config["cmd-path"]);
|
||||
userCredentialsFound = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Microsoft.Rest;
|
||||
|
||||
namespace k8s
|
||||
{
|
||||
@@ -76,5 +77,7 @@ namespace k8s
|
||||
/// </summary>
|
||||
/// <value>The access token.</value>
|
||||
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