diff --git a/.editorconfig b/.editorconfig index bb3bbcd..8843b3c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -83,3 +83,5 @@ csharp_new_line_before_catch = true csharp_new_line_before_finally = true csharp_new_line_before_members_in_object_initializers = true csharp_new_line_before_members_in_anonymous_types = true +csharp_indent_case_contents=true +csharp_indent_case_contents_when_block=false diff --git a/src/KubeClient.Extensions.KubeConfig/K8sConfig.cs b/src/KubeClient.Extensions.KubeConfig/K8sConfig.cs index b30beb4..ed5f244 100644 --- a/src/KubeClient.Extensions.KubeConfig/K8sConfig.cs +++ b/src/KubeClient.Extensions.KubeConfig/K8sConfig.cs @@ -1,16 +1,17 @@ +using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Linq; using System.Runtime.InteropServices; +using System.Security.Cryptography.X509Certificates; using YamlDotNet.Serialization; namespace KubeClient { using Extensions.KubeConfig.Models; using KubeClient.Authentication; - using Microsoft.Extensions.Logging; - using System.Security.Cryptography.X509Certificates; /// /// Kubernetes client configuration. @@ -65,16 +66,16 @@ public static string Locate() //Mirror the logic that kubectl and other Kubernetes tools use for homedir resolution: https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/client-go/util/homedir/homedir.go if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - var home = Environment.GetEnvironmentVariable("HOME"); + string home = Environment.GetEnvironmentVariable("HOME"); if (!String.IsNullOrEmpty(home)) return Path.Combine(home, ".kube", "config"); - var homeDrive = Environment.GetEnvironmentVariable("HOMEDRIVE"); - var homePath = Environment.GetEnvironmentVariable("HOMEPATH"); + string homeDrive = Environment.GetEnvironmentVariable("HOMEDRIVE"); + string homePath = Environment.GetEnvironmentVariable("HOMEPATH"); if (!String.IsNullOrEmpty(homeDrive) && !String.IsNullOrEmpty(homePath)) return Path.Combine(homeDrive + homePath, ".kube", "config"); - var userProfile = Environment.GetEnvironmentVariable("USERPROFILE"); + string userProfile = Environment.GetEnvironmentVariable("USERPROFILE"); if (!String.IsNullOrEmpty(userProfile)) return Path.Combine(userProfile, ".kube", "config"); } @@ -192,7 +193,6 @@ public KubeClientOptions ConfigureKubeClientOptions(KubeClientOptions kubeClient kubeClientOptions.ApiEndPoint = new Uri(targetCluster.Config.Server); kubeClientOptions.KubeNamespace = defaultKubeNamespace; - kubeClientOptions.ClientCertificate = targetUser.Config.GetClientCertificate(); kubeClientOptions.AllowInsecure = targetCluster.Config.AllowInsecure; kubeClientOptions.CertificationAuthorityCertificate = targetCluster.Config.GetCACertificate(); @@ -237,6 +237,17 @@ public KubeClientOptions ConfigureKubeClientOptions(KubeClientOptions kubeClient kubeClientOptions.AuthStrategy = authStrategy; } + else if (targetUser.Config.Exec != null) + { + CredentialPluginConfig credentialPluginConfig = targetUser.Config.Exec; + + kubeClientOptions.AuthStrategy = KubeAuthStrategy.CredentialPlugin( + pluginApiVersion: credentialPluginConfig.ApiVersion, + command: credentialPluginConfig.Command, + arguments: credentialPluginConfig.Arguments, + environmentVariables: credentialPluginConfig.EnvironmentVariables.ToDictionary(variable => variable.Name, variable => variable.Value) + ); + } else kubeClientOptions.AuthStrategy = KubeAuthStrategy.None; } diff --git a/src/KubeClient.Extensions.WebSockets/K8sWebSocketOptions.cs b/src/KubeClient.Extensions.WebSockets/K8sWebSocketOptions.cs index 04fee85..189a802 100644 --- a/src/KubeClient.Extensions.WebSockets/K8sWebSocketOptions.cs +++ b/src/KubeClient.Extensions.WebSockets/K8sWebSocketOptions.cs @@ -1,3 +1,4 @@ +using KubeClient.Authentication; using System; using System.Collections.Generic; using System.Diagnostics; @@ -61,7 +62,7 @@ public K8sWebSocketOptions() /// Default is , which lets the platform select the most appropriate protocol. /// public SslProtocols EnabledSslProtocols { get; set; } = SslProtocols.None; - + /// /// The WebSocket keep-alive interval. /// @@ -80,44 +81,75 @@ public static K8sWebSocketOptions FromClientOptions(IKubeApiClient client) { if (client == null) throw new ArgumentNullException(nameof(client)); - - var socketOptions = new K8sWebSocketOptions(); KubeClientOptions clientOptions = client.GetClientOptions(); - - if (!String.IsNullOrWhiteSpace(clientOptions.AccessToken)) - socketOptions.RequestHeaders["Authorization"] = $"Bearer {clientOptions.AccessToken}"; - if (clientOptions.ClientCertificate != null) - socketOptions.ClientCertificates.Add(clientOptions.ClientCertificate); + return FromClientOptions(clientOptions); + } + + /// + /// Create using the client's authentication settings. + /// + /// + /// The . + /// + /// + /// The configured . + /// + public static K8sWebSocketOptions FromClientOptions(KubeClientOptions clientOptions) + { + if (clientOptions == null) + throw new ArgumentNullException(nameof(clientOptions)); + + var socketOptions = new K8sWebSocketOptions(); + + // TODO: Expose functionality for obtaining access token via KubeAuthStrategy and call it from here, rather than using the special-case credential configuration logic below. + + switch (clientOptions.AuthStrategy) + { + case CertificateAuthStrategy certificateAuthStrategy: + { + if (certificateAuthStrategy.Certificate != null) + socketOptions.ClientCertificates.Add(certificateAuthStrategy.Certificate); + + break; + } + case BearerTokenAuthStrategy bearerTokenAuthStrategy: + { + if (!String.IsNullOrWhiteSpace(bearerTokenAuthStrategy.Token)) + socketOptions.RequestHeaders["Authorization"] = $"Bearer {bearerTokenAuthStrategy.Token}"; + + break; + } + } if (clientOptions.CertificationAuthorityCertificate != null) { socketOptions.ServerCertificateCustomValidationCallback = (sender, certificate, chain, sslPolicyErrors) => - { - if (sslPolicyErrors != SslPolicyErrors.RemoteCertificateChainErrors) - return false; - - try - { - using (X509Chain certificateChain = new X509Chain()) - { - certificateChain.ChainPolicy.ExtraStore.Add(clientOptions.CertificationAuthorityCertificate); - certificateChain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority; - certificateChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; - - return certificateChain.Build( + { + if (sslPolicyErrors != SslPolicyErrors.RemoteCertificateChainErrors) + return false; + + try + { + using (X509Chain certificateChain = new X509Chain()) + { + certificateChain.ChainPolicy.ExtraStore.Add(clientOptions.CertificationAuthorityCertificate); + certificateChain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority; + certificateChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + + return certificateChain.Build( (X509Certificate2)certificate ); - } - } - catch (Exception chainException) - { + } + } + catch (Exception chainException) + { Debug.WriteLine(chainException); - return false; - } - }; + return false; + } + }; } else if (clientOptions.AllowInsecure) socketOptions.ServerCertificateCustomValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true; diff --git a/src/KubeClient/Authentication/BearerTokenProviderAuthStrategy.cs b/src/KubeClient/Authentication/BearerTokenProviderAuthStrategy.cs index 0c9ea67..4739cc7 100644 --- a/src/KubeClient/Authentication/BearerTokenProviderAuthStrategy.cs +++ b/src/KubeClient/Authentication/BearerTokenProviderAuthStrategy.cs @@ -7,7 +7,7 @@ namespace KubeClient.Authentication using MessageHandlers; /// - /// A Kubernetes API authentication strategy that uses an access token provided by a legacy authentication provider ("auth-provider" in the K8s config). + /// A Kubernetes API authentication strategy that uses an access token provided by a authentication provider ("auth-provider" in the K8s config). /// public class BearerTokenProviderAuthStrategy : KubeAuthStrategy @@ -84,7 +84,7 @@ public override ClientBuilder Configure(ClientBuilder clientBuilder) Validate(); return clientBuilder.AddHandler( - () => new CommandBearerTokenHandler(Command, Arguments, Selector, ExpirySelector, InitialToken, InitialTokenExpiryUtc) + () => new ExecCommandBearerTokenHandler(Command, Arguments, Selector, ExpirySelector, InitialToken, InitialTokenExpiryUtc) ); } diff --git a/src/KubeClient/Authentication/CredentialPluginAuthStrategy.cs b/src/KubeClient/Authentication/CredentialPluginAuthStrategy.cs index 4e4b634..6678a0f 100644 --- a/src/KubeClient/Authentication/CredentialPluginAuthStrategy.cs +++ b/src/KubeClient/Authentication/CredentialPluginAuthStrategy.cs @@ -105,13 +105,9 @@ public override KubeAuthStrategy Clone() clonedStrategy.Arguments.AddRange(Arguments); - foreach ( string variableName in EnvironmentVariables.Keys ) - { - clonedStrategy.EnvironmentVariables.Add(variableName, - value: EnvironmentVariables[variableName] - ); - } - + foreach ((string variableName, string variableValue) in EnvironmentVariables) + clonedStrategy.EnvironmentVariables.Add(variableName, variableValue); + return clonedStrategy; } } diff --git a/src/KubeClient/KubeApiClient.cs b/src/KubeClient/KubeApiClient.cs index 74987c9..433fa27 100644 --- a/src/KubeClient/KubeApiClient.cs +++ b/src/KubeClient/KubeApiClient.cs @@ -219,7 +219,6 @@ public static KubeApiClient Create(string apiEndPoint, string accessToken, X509C { ApiEndPoint = new Uri(apiEndPoint), AuthStrategy = KubeAuthStrategy.BearerToken(accessToken), - AccessToken = accessToken, CertificationAuthorityCertificate = expectServerCertificate, LoggerFactory = loggerFactory }); @@ -249,7 +248,6 @@ public static KubeApiClient Create(string apiEndPoint, X509Certificate2 clientCe { ApiEndPoint = new Uri(apiEndPoint), AuthStrategy = KubeAuthStrategy.ClientCertificate(clientCertificate), - ClientCertificate = clientCertificate, CertificationAuthorityCertificate = expectServerCertificate, LoggerFactory = loggerFactory }); diff --git a/src/KubeClient/KubeAuthStrategy.cs b/src/KubeClient/KubeAuthStrategy.cs index 41305a2..1d4ec79 100644 --- a/src/KubeClient/KubeAuthStrategy.cs +++ b/src/KubeClient/KubeAuthStrategy.cs @@ -6,6 +6,7 @@ namespace KubeClient { using Authentication; + using System.Collections.Generic; /// /// The base class for implementations of authentication to the Kubernetes API. @@ -123,5 +124,51 @@ public abstract class KubeAuthStrategy InitialToken = initialToken, InitialTokenExpiryUtc = initialTokenExpiryUtc }; + + /// + /// Use a client-go credential plugin ("exec" in the K8s config). + /// + /// + /// The plugin API version. + /// + /// + /// The API version returned by the plugin MUST match the version specified here. + /// + /// + /// + /// The command used to generate an access token for authenticating to the Kubernetes API. + /// + /// + /// The arguments (if any) for the command used to generate an access token for authenticating to the Kubernetes API. + /// + /// + /// The environment variables arguments (if any) for the command used to generate an access token for authenticating to the Kubernetes API. + /// + /// + /// The configured . + /// + public static CredentialPluginAuthStrategy CredentialPlugin(string pluginApiVersion, string command, IReadOnlyList arguments = null, IReadOnlyDictionary environmentVariables = null) + { + if (arguments == null) + throw new ArgumentNullException(nameof(arguments)); + + var authStrategy = new CredentialPluginAuthStrategy + { + PluginApiVersion = pluginApiVersion, + Command = command, + }; + + if (arguments != null) + authStrategy.Arguments.AddRange(arguments); + + if (environmentVariables != null) + { + foreach ((string variableName, string variableValue) in authStrategy.EnvironmentVariables) + authStrategy.EnvironmentVariables.Add(variableName, variableValue); + } + + return authStrategy; + } + } } diff --git a/src/KubeClient/KubeClientOptions.cs b/src/KubeClient/KubeClientOptions.cs index c8609ec..e567838 100644 --- a/src/KubeClient/KubeClientOptions.cs +++ b/src/KubeClient/KubeClientOptions.cs @@ -43,61 +43,11 @@ public KubeClientOptions(string apiEndPoint) /// public Uri ApiEndPoint { get; set; } - /// - /// The access token used to authenticate to the Kubernetes API. - /// - public string AccessToken { get; set; } - - /// - /// The username used to authenticate to the Kubernetes API. - /// - public string Username { get; set; } - - /// - /// The password used to authenticate to the Kubernetes API. - /// - public string Password { get; set; } - - /// - /// The command used to generate an access token for authenticating to the Kubernetes API. - /// - public string AccessTokenCommand { get; set; } - - /// - /// The command arguments used to generate an access token for authenticating to the Kubernetes API. - /// - public string AccessTokenCommandArguments { get; set; } - - /// - /// The Go-style selector used to retrieve the access token from the command output. - /// - public string AccessTokenSelector { get; set; } - - /// - /// The Go-style selector used to retrieve the access token's expiry date/time from the command output. - /// - public string AccessTokenExpirySelector { get; set; } - - /// - /// The initial access token used to authenticate to the Kubernetes API. - /// - public string InitialAccessToken { get; set; } - - /// - /// The initial token expiry used to authenticate to the Kubernetes API. - /// - public DateTime? InitialTokenExpiryUtc { get; set; } - /// /// The strategy used for authenticating to the Kubernetes API. /// public KubeAuthStrategy AuthStrategy { get; set; } - /// - /// The client certificate used to authenticate to the Kubernetes API. - /// - public X509Certificate2 ClientCertificate { get; set; } - /// /// The expected CA certificate used by the Kubernetes API. /// @@ -141,18 +91,10 @@ public KubeClientOptions Clone() { var clonedOptions = new KubeClientOptions { - AccessToken = AccessToken, - AccessTokenCommand = AccessTokenCommand, - AccessTokenCommandArguments = AccessTokenCommandArguments, - AccessTokenExpirySelector = AccessTokenExpirySelector, - AccessTokenSelector = AccessTokenSelector, AllowInsecure = AllowInsecure, ApiEndPoint = ApiEndPoint, AuthStrategy = AuthStrategy, CertificationAuthorityCertificate = CertificationAuthorityCertificate, - ClientCertificate = ClientCertificate, - InitialAccessToken = InitialAccessToken, - InitialTokenExpiryUtc = InitialTokenExpiryUtc, KubeNamespace = KubeNamespace, LoggerFactory = LoggerFactory, LogHeaders = LogHeaders, @@ -175,9 +117,6 @@ public KubeClientOptions EnsureValid() if (ApiEndPoint == null || !ApiEndPoint.IsAbsoluteUri) throw new KubeClientException("Invalid KubeClientOptions: must specify a valid API end-point."); - if (ClientCertificate != null && !ClientCertificate.HasPrivateKey) - throw new KubeClientException("Invalid KubeClientOptions: the private key for the supplied client certificate is not available."); - if (String.IsNullOrWhiteSpace(KubeNamespace)) throw new KubeClientException("Invalid KubeClientOptions: must specify a valid default namespace."); @@ -216,7 +155,6 @@ public static KubeClientOptions FromPodServiceAccount(string serviceAccountPath { ApiEndPoint = new Uri(apiEndPoint), AuthStrategy = KubeAuthStrategy.BearerToken(accessToken), - AccessToken = accessToken, CertificationAuthorityCertificate = kubeCACertificate, KubeNamespace = defaultNamespace }; diff --git a/src/KubeClient/MessageHandlers/CredentialPluginBearerTokenHandler.cs b/src/KubeClient/MessageHandlers/CredentialPluginBearerTokenHandler.cs index e832569..16e3344 100644 --- a/src/KubeClient/MessageHandlers/CredentialPluginBearerTokenHandler.cs +++ b/src/KubeClient/MessageHandlers/CredentialPluginBearerTokenHandler.cs @@ -81,7 +81,7 @@ public CredentialPluginBearerTokenHandler(string expectedApiVersion, string plug _expectedApiVersion = expectedApiVersion; _pluginCommand = pluginCommand; _pluginCommandArguments = pluginCommandArguments?.ToArray() ?? new string[0]; - _pluginCommandEnvironment = pluginCommandEnvironment?.ToDictionary(entry => entry.Key, entry => entry.Value) ?? new Dictionary(); + _pluginCommandEnvironment = pluginCommandEnvironment?.ToDictionary(entry => entry.Key, entry => entry.Value, StringComparer.OrdinalIgnoreCase) ?? new Dictionary(StringComparer.OrdinalIgnoreCase); } /// diff --git a/src/KubeClient/MessageHandlers/CommandBearerTokenHandler.cs b/src/KubeClient/MessageHandlers/ExecCommandBearerTokenHandler.cs similarity index 92% rename from src/KubeClient/MessageHandlers/CommandBearerTokenHandler.cs rename to src/KubeClient/MessageHandlers/ExecCommandBearerTokenHandler.cs index c9e89c0..119ea64 100644 --- a/src/KubeClient/MessageHandlers/CommandBearerTokenHandler.cs +++ b/src/KubeClient/MessageHandlers/ExecCommandBearerTokenHandler.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Globalization; +using System.Linq; using System.Threading; using System.Threading.Tasks; using KubeClient.Utilities; @@ -13,7 +14,7 @@ namespace KubeClient.MessageHandlers /// /// HTTP message handler that runs a command to obtain a bearer token and adds it to outgoing requests. /// - public class CommandBearerTokenHandler + public class ExecCommandBearerTokenHandler : BearerTokenHandler { /// @@ -54,10 +55,10 @@ public class CommandBearerTokenHandler /// /// Environment variables assigned to the executed command /// - private readonly Dictionary _environmentVariables; + private readonly IReadOnlyDictionary _environmentVariables; /// - /// Create a new . + /// Create a new . /// /// /// The command to execute in order to obtain the access token for outgoing requests. @@ -80,7 +81,7 @@ public class CommandBearerTokenHandler /// /// Environment variables assigned to the executed command /// - public CommandBearerTokenHandler(string accessTokenCommand, string accessTokenCommandArguments, string accessTokenSelector, string accessTokenExpirySelector, string initialAccessToken = null, DateTime? initialTokenExpiryUtc = null, Dictionary environmentVariables = null) + public ExecCommandBearerTokenHandler(string accessTokenCommand, string accessTokenCommandArguments, string accessTokenSelector, string accessTokenExpirySelector, string initialAccessToken = null, DateTime? initialTokenExpiryUtc = null, IReadOnlyDictionary environmentVariables = null) { if (String.IsNullOrWhiteSpace(accessTokenCommand)) throw new ArgumentException("Argument cannot be null, empty, or entirely composed of whitespace: 'accessTokenCommand'.", nameof(accessTokenCommand)); @@ -100,7 +101,7 @@ public CommandBearerTokenHandler(string accessTokenCommand, string accessTokenCo _accessToken = initialAccessToken; _accessTokenExpiresUtc = initialTokenExpiryUtc; - _environmentVariables = environmentVariables; + _environmentVariables = environmentVariables?.ToDictionary(entry => entry.Key, entry => entry.Value, StringComparer.OrdinalIgnoreCase) ?? new Dictionary(StringComparer.OrdinalIgnoreCase); } ///