Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 93 additions & 13 deletions src/KubernetesClient.Aot/KubernetesClientConfiguration.ConfigFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Net;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
using System.Text.Json.Nodes;

namespace k8s
{
Expand Down Expand Up @@ -306,26 +307,25 @@ private void SetClusterDetails(K8SConfiguration k8SConfig, Context activeContext
{
if (!string.IsNullOrEmpty(clusterDetails.ClusterEndpoint.CertificateAuthorityData))
{
var data = clusterDetails.ClusterEndpoint.CertificateAuthorityData;
CaData = clusterDetails.ClusterEndpoint.CertificateAuthorityData;
#if NET9_0_OR_GREATER
SslCaCerts = new X509Certificate2Collection(X509CertificateLoader.LoadCertificate(Convert.FromBase64String(data)));
SslCaCerts = new X509Certificate2Collection(X509CertificateLoader.LoadCertificate(Convert.FromBase64String(CaData)));
#else
string nullPassword = null;
// This null password is to change the constructor to fix this KB:
// https://support.microsoft.com/en-us/topic/kb5025823-change-in-how-net-applications-import-x-509-certificates-bf81c936-af2b-446e-9f7a-016f4713b46b
SslCaCerts = new X509Certificate2Collection(new X509Certificate2(Convert.FromBase64String(data), nullPassword));
SslCaCerts = new X509Certificate2Collection(new X509Certificate2(Convert.FromBase64String(CaData), nullPassword));
#endif
}
else if (!string.IsNullOrEmpty(clusterDetails.ClusterEndpoint.CertificateAuthority))
{
Comment thread
mutsaddi-deshaw marked this conversation as resolved.
var caBytes = File.ReadAllBytes(GetFullPath(k8SConfig, clusterDetails.ClusterEndpoint.CertificateAuthority));
CaData = Convert.ToBase64String(caBytes);
#if NET9_0_OR_GREATER
SslCaCerts = new X509Certificate2Collection(X509CertificateLoader.LoadCertificateFromFile(GetFullPath(
k8SConfig,
clusterDetails.ClusterEndpoint.CertificateAuthority)));
SslCaCerts = new X509Certificate2Collection(X509CertificateLoader.LoadCertificate(caBytes));
#else
SslCaCerts = new X509Certificate2Collection(new X509Certificate2(GetFullPath(
k8SConfig,
clusterDetails.ClusterEndpoint.CertificateAuthority)));
string nullPassword = null;
SslCaCerts = new X509Certificate2Collection(new X509Certificate2(caBytes, nullPassword));
#endif
}
}
Expand Down Expand Up @@ -416,7 +416,19 @@ private void SetUserDetails(K8SConfiguration k8SConfig, Context activeContext)
throw new KubeConfigException("External command execution missing ApiVersion key");
}

var response = ExecuteExternalCommand(userDetails.UserCredentials.ExternalExecution);
ClusterEndpoint clusterEndpoint = null;
if (userDetails.UserCredentials.ExternalExecution.ProvideClusterInfo)
{
clusterEndpoint = new ClusterEndpoint
{
Server = this.Host,
SkipTlsVerify = this.SkipTlsVerify,
TlsServerName = this.TlsServerName,
CertificateAuthorityData = this.CaData,
};
}

var response = ExecuteExternalCommand(userDetails.UserCredentials.ExternalExecution, clusterEndpoint);
AccessToken = response.Status.Token;
// When reading ClientCertificateData from a config file it will be base64 encoded, and code later in the system (see CertUtils.GeneratePfx)
// expects ClientCertificateData and ClientCertificateKeyData to be base64 encoded because of this. However the string returned by external
Expand All @@ -429,7 +441,7 @@ private void SetUserDetails(K8SConfiguration k8SConfig, Context activeContext)
// TODO: support client certificates here too.
if (AccessToken != null)
{
TokenProvider = new ExecTokenProvider(userDetails.UserCredentials.ExternalExecution);
TokenProvider = new ExecTokenProvider(userDetails.UserCredentials.ExternalExecution, clusterEndpoint);
}
}

Expand All @@ -440,16 +452,79 @@ private void SetUserDetails(K8SConfiguration k8SConfig, Context activeContext)
}
}

/// <summary>
/// Converts a resolved <see cref="ClusterEndpoint"/> into the
/// <c>spec.cluster</c> JSON representation defined by the exec credential plugin
/// protocol (client.authentication.k8s.io/v1). Returns <c>null</c> if
/// <paramref name="cluster"/> is <c>null</c>.
/// </summary>
/// <remarks>
/// The AOT <see cref="ClusterEndpoint"/> does not include Extensions (dynamic types
/// are incompatible with AOT), so <c>spec.cluster.config</c> is not populated.
/// </remarks>
/// <seealso href="https://kubernetes.io/docs/reference/config-api/client-authentication.v1/#Cluster"/>
internal static JsonNode ToExecClusterInfo(ClusterEndpoint cluster)
{
if (cluster == null)
{
return null;
}

var node = new JsonObject
{
["server"] = cluster.Server,
};

if (cluster.SkipTlsVerify)
{
node["insecure-skip-tls-verify"] = true;
}

if (!string.IsNullOrEmpty(cluster.CertificateAuthorityData))
{
node["certificate-authority-data"] = cluster.CertificateAuthorityData;
}

if (!string.IsNullOrEmpty(cluster.TlsServerName))
{
node["tls-server-name"] = cluster.TlsServerName;
}

return node;
}

public static Process CreateRunnableExternalProcess(ExternalExecution config, EventHandler<DataReceivedEventArgs> captureStdError = null)
{
return CreateRunnableExternalProcess(config, captureStdError, null);
}

public static Process CreateRunnableExternalProcess(ExternalExecution config, EventHandler<DataReceivedEventArgs> captureStdError, ClusterEndpoint cluster)
{
if (config == null)
{
throw new ArgumentNullException(nameof(config));
}

var spec = new JsonObject { ["interactive"] = Environment.UserInteractive };
if (config.ProvideClusterInfo)
{
var clusterNode = ToExecClusterInfo(cluster);
if (clusterNode != null)
{
spec["cluster"] = clusterNode;
}
}
Comment thread
mutsaddi-deshaw marked this conversation as resolved.

var execInfo = new JsonObject
{
["apiVersion"] = config.ApiVersion,
["kind"] = "ExecCredentials",
["spec"] = spec,
};

var process = new Process();

process.StartInfo.EnvironmentVariables.Add("KUBERNETES_EXEC_INFO", $"{{ \"apiVersion\":\"{config.ApiVersion}\",\"kind\":\"ExecCredentials\",\"spec\":{{ \"interactive\":{Environment.UserInteractive.ToString().ToLower()} }} }}");
process.StartInfo.EnvironmentVariables.Add("KUBERNETES_EXEC_INFO", execInfo.ToJsonString());
if (config.EnvironmentVariables != null)
{
foreach (var configEnvironmentVariable in config.EnvironmentVariables)
Expand Down Expand Up @@ -493,14 +568,19 @@ public static Process CreateRunnableExternalProcess(ExternalExecution config, Ev
/// The token, client certificate data, and the client key data received from the external command execution
/// </returns>
public static ExecCredentialResponse ExecuteExternalCommand(ExternalExecution config)
{
return ExecuteExternalCommand(config, null);
}

public static ExecCredentialResponse ExecuteExternalCommand(ExternalExecution config, ClusterEndpoint cluster)
{
if (config == null)
{
throw new ArgumentNullException(nameof(config));
}

var captureStdError = ExecStdError;
var process = CreateRunnableExternalProcess(config, captureStdError);
var process = CreateRunnableExternalProcess(config, captureStdError, cluster);

try
{
Expand Down
9 changes: 8 additions & 1 deletion src/KubernetesClient/Authentication/ExecTokenProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,18 @@ namespace k8s.Authentication
public class ExecTokenProvider : ITokenProvider
{
private readonly ExternalExecution exec;
private readonly ClusterEndpoint cluster;
private ExecCredentialResponse response;

public ExecTokenProvider(ExternalExecution exec)
: this(exec, null)
{
}

public ExecTokenProvider(ExternalExecution exec, ClusterEndpoint cluster)
{
this.exec = exec;
this.cluster = cluster;
}

private bool NeedsRefresh()
Expand Down Expand Up @@ -41,7 +48,7 @@ public async Task<AuthenticationHeaderValue> GetAuthenticationHeaderAsync(Cancel
private async Task RefreshToken()
{
response =
await Task.Run(() => KubernetesClientConfiguration.ExecuteExternalCommand(this.exec)).ConfigureAwait(false);
await Task.Run(() => KubernetesClientConfiguration.ExecuteExternalCommand(this.exec, this.cluster)).ConfigureAwait(false);
}
}
}
114 changes: 101 additions & 13 deletions src/KubernetesClient/KubernetesClientConfiguration.ConfigFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Net;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json.Nodes;

namespace k8s
{
Expand Down Expand Up @@ -306,15 +307,16 @@ private void SetClusterDetails(K8SConfiguration k8SConfig, Context activeContext
{
if (!string.IsNullOrEmpty(clusterDetails.ClusterEndpoint.CertificateAuthorityData))
{
var data = clusterDetails.ClusterEndpoint.CertificateAuthorityData;
var pemText = Encoding.UTF8.GetString(Convert.FromBase64String(data));
CaData = clusterDetails.ClusterEndpoint.CertificateAuthorityData;
var pemText = Encoding.UTF8.GetString(Convert.FromBase64String(CaData));
SslCaCerts = CertUtils.LoadFromPemText(pemText);
}
else if (!string.IsNullOrEmpty(clusterDetails.ClusterEndpoint.CertificateAuthority))
{
SslCaCerts = CertUtils.LoadPemFileCert(GetFullPath(
k8SConfig,
clusterDetails.ClusterEndpoint.CertificateAuthority));
var caBytes = File.ReadAllBytes(GetFullPath(k8SConfig, clusterDetails.ClusterEndpoint.CertificateAuthority));
CaData = Convert.ToBase64String(caBytes);
var pemText = Encoding.UTF8.GetString(caBytes);
SslCaCerts = CertUtils.LoadFromPemText(pemText);
}
Comment thread
mutsaddi-deshaw marked this conversation as resolved.
}
}
Expand Down Expand Up @@ -426,7 +428,25 @@ private void SetUserDetails(K8SConfiguration k8SConfig, Context activeContext)
throw new KubeConfigException("External command execution missing ApiVersion key");
}

var response = ExecuteExternalCommand(userDetails.UserCredentials.ExternalExecution);
ClusterEndpoint clusterEndpoint = null;
if (userDetails.UserCredentials.ExternalExecution.ProvideClusterInfo)
{
var clusterDetails = k8SConfig.Clusters.FirstOrDefault(c => c.Name.Equals(
activeContext.ContextDetails.Cluster,
StringComparison.OrdinalIgnoreCase));
var rawCluster = clusterDetails?.ClusterEndpoint;

clusterEndpoint = new ClusterEndpoint
{
Server = this.Host,
SkipTlsVerify = this.SkipTlsVerify,
TlsServerName = this.TlsServerName,
CertificateAuthorityData = this.CaData,
Extensions = rawCluster?.Extensions,
};
}

var response = ExecuteExternalCommand(userDetails.UserCredentials.ExternalExecution, clusterEndpoint);
AccessToken = response.Status.Token;
// When reading ClientCertificateData from a config file it will be base64 encoded, and code later in the system (see CertUtils.GeneratePfx)
// expects ClientCertificateData and ClientCertificateKeyData to be base64 encoded because of this. However the string returned by external
Expand All @@ -439,7 +459,7 @@ private void SetUserDetails(K8SConfiguration k8SConfig, Context activeContext)
// TODO: support client certificates here too.
if (AccessToken != null)
{
TokenProvider = new ExecTokenProvider(userDetails.UserCredentials.ExternalExecution);
TokenProvider = new ExecTokenProvider(userDetails.UserCredentials.ExternalExecution, clusterEndpoint);
}
}

Expand All @@ -450,23 +470,86 @@ private void SetUserDetails(K8SConfiguration k8SConfig, Context activeContext)
}
}

/// <summary>
/// Converts a <see cref="ClusterEndpoint"/> (kubeconfig model) into the
/// <c>spec.cluster</c> JSON representation defined by the exec credential plugin
/// protocol (client.authentication.k8s.io/v1). Returns <c>null</c> if
/// <paramref name="cluster"/> is <c>null</c>.
/// </summary>
/// <seealso href="https://kubernetes.io/docs/reference/config-api/client-authentication.v1/#Cluster"/>
internal static JsonNode ToExecClusterInfo(ClusterEndpoint cluster)
{
if (cluster == null)
{
return null;
}

var node = new JsonObject
{
["server"] = cluster.Server,
};

if (cluster.SkipTlsVerify)
{
node["insecure-skip-tls-verify"] = true;
}

if (!string.IsNullOrEmpty(cluster.CertificateAuthorityData))
{
node["certificate-authority-data"] = cluster.CertificateAuthorityData;
}

if (!string.IsNullOrEmpty(cluster.TlsServerName))
{
node["tls-server-name"] = cluster.TlsServerName;
}

var execExtension = cluster.Extensions?
.FirstOrDefault(e => e.Name == "client.authentication.k8s.io/exec");
Comment thread
mutsaddi-deshaw marked this conversation as resolved.
Outdated
if (execExtension != null)
{
object extConfig = execExtension.Extension;
if (extConfig != null)
{
node["config"] = JsonNode.Parse(JsonSerializer.Serialize(extConfig));
}
Comment on lines +517 to +521

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as #1808 (comment)

}
Comment on lines +513 to +522

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dictionary<object, object> serialization with System.Text.Json is officially supported with default options when the runtime key type is a supported type. See Supported key types

Comment thread
mutsaddi-deshaw marked this conversation as resolved.

return node;
}

public static Process CreateRunnableExternalProcess(ExternalExecution config, EventHandler<DataReceivedEventArgs> captureStdError = null)
{
return CreateRunnableExternalProcess(config, captureStdError, null);
}

public static Process CreateRunnableExternalProcess(ExternalExecution config, EventHandler<DataReceivedEventArgs> captureStdError, ClusterEndpoint cluster)
{
if (config == null)
{
throw new ArgumentNullException(nameof(config));
}

var execInfo = new Dictionary<string, dynamic>
var spec = new JsonObject { ["interactive"] = Environment.UserInteractive };
if (config.ProvideClusterInfo)
{
var clusterNode = ToExecClusterInfo(cluster);
if (clusterNode != null)
{
spec["cluster"] = clusterNode;
}
}
Comment thread
mutsaddi-deshaw marked this conversation as resolved.
Comment thread
mutsaddi-deshaw marked this conversation as resolved.

var execInfo = new JsonObject
{
{ "apiVersion", config.ApiVersion },
{ "kind", "ExecCredentials" },
{ "spec", new Dictionary<string, bool> { { "interactive", Environment.UserInteractive } } },
["apiVersion"] = config.ApiVersion,
["kind"] = "ExecCredentials",
["spec"] = spec,
};

var process = new Process();

process.StartInfo.EnvironmentVariables.Add("KUBERNETES_EXEC_INFO", JsonSerializer.Serialize(execInfo));
process.StartInfo.EnvironmentVariables.Add("KUBERNETES_EXEC_INFO", execInfo.ToJsonString());
if (config.EnvironmentVariables != null)
{
foreach (var configEnvironmentVariable in config.EnvironmentVariables)
Expand Down Expand Up @@ -510,14 +593,19 @@ public static Process CreateRunnableExternalProcess(ExternalExecution config, Ev
/// The token, client certificate data, and the client key data received from the external command execution
/// </returns>
public static ExecCredentialResponse ExecuteExternalCommand(ExternalExecution config)
{
return ExecuteExternalCommand(config, null);
}

public static ExecCredentialResponse ExecuteExternalCommand(ExternalExecution config, ClusterEndpoint cluster)
{
if (config == null)
{
throw new ArgumentNullException(nameof(config));
}

var captureStdError = ExecStdError;
var process = CreateRunnableExternalProcess(config, captureStdError);
var process = CreateRunnableExternalProcess(config, captureStdError, cluster);

try
{
Expand Down
6 changes: 6 additions & 0 deletions src/KubernetesClient/KubernetesClientConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ public partial class KubernetesClientConfiguration
/// </summary>
public string TlsServerName { get; set; }

/// <summary>
/// Gets the base64-encoded PEM certificate authority data, resolved from either
/// inline data or file path during cluster configuration.
/// </summary>
public string CaData { get; set; }
Comment thread
mutsaddi-deshaw marked this conversation as resolved.
Outdated

/// <summary>
/// Gets or sets the HTTP user agent.
/// </summary>
Expand Down
Loading