Allow token refresh for GCP (#402)

This commit is contained in:
Andrew Stakhov
2020-04-28 18:34:25 -04:00
committed by GitHub
parent cfc4306528
commit ae9dd04a2e
10 changed files with 186 additions and 18 deletions

View 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>();
}
}
}

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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; }
}
}

View 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");
}
}
}

View File

@@ -0,0 +1,12 @@
using System;
namespace k8s.Tests
{
[Flags]
public enum OperatingSystem
{
Windows = 1,
Linux = 2,
OSX = 4
}
}

View 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;
}
}
}

View File

@@ -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"
}
}

View File

@@ -0,0 +1,2 @@
@echo off
type %~dp0\gcloud-config-helper.json

View File

@@ -0,0 +1,5 @@
#!/bin/bash
SCRIPT=$(readlink -f "$0")
SCRIPTPATH=$(dirname "$SCRIPT")
OUTPUT_JSON=$SCRIPTPATH/gcloud-config-helper.json
cat $OUTPUT_JSON