Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/KubeConfigModels/ClusterEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ public class ClusterEndpoint
/// <summary>
/// Gets or sets the path to a cert file for the certificate authority.
/// </summary>
[YamlMember(Alias = "certificate-authority")]
[YamlMember(Alias = "certificate-authority", ApplyNamingConventions = false)]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

ApplyNamingConventions = false is need when using naming conventions like CamelCaseNamingConvention

public string CertificateAuthority {get; set; }

/// <summary>
/// Gets or sets =PEM-encoded certificate authority certificates. Overrides <see cref="CertificateAuthority"/>.
/// </summary>
[YamlMember(Alias = "certificate-authority-data")]
[YamlMember(Alias = "certificate-authority-data", ApplyNamingConventions = false)]
public string CertificateAuthorityData { get; set; }

/// <summary>
Expand All @@ -30,7 +30,7 @@ public class ClusterEndpoint
/// Gets or sets a value indicating whether to skip the validity check for the server's certificate.
/// This will make your HTTPS connections insecure.
/// </summary>
[YamlMember(Alias = "insecure-skip-tls-verify")]
[YamlMember(Alias = "insecure-skip-tls-verify", ApplyNamingConventions = false)]
public bool SkipTlsVerify { get; set; }

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion src/KubeConfigModels/K8SConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public class K8SConfiguration
/// <summary>
/// Gets or sets the name of the context that you would like to use by default.
/// </summary>
[YamlMember(Alias = "current-context")]
[YamlMember(Alias = "current-context", ApplyNamingConventions = false)]
public string CurrentContext { get; set; }

/// <summary>
Expand Down
14 changes: 7 additions & 7 deletions src/KubeConfigModels/UserCredentials.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,25 @@ public class UserCredentials
/// <summary>
/// Gets or sets PEM-encoded data from a client cert file for TLS. Overrides <see cref="ClientCertificate"/>.
/// </summary>
[YamlMember(Alias = "client-certificate-data")]
[YamlMember(Alias = "client-certificate-data", ApplyNamingConventions = false)]
public string ClientCertificateData { get; set; }

/// <summary>
/// Gets or sets the path to a client cert file for TLS.
/// </summary>
[YamlMember(Alias = "client-certificate")]
[YamlMember(Alias = "client-certificate", ApplyNamingConventions = false)]
public string ClientCertificate { get; set; }

/// <summary>
/// Gets or sets PEM-encoded data from a client key file for TLS. Overrides <see cref="ClientKey"/>.
/// </summary>
[YamlMember(Alias = "client-key-data")]
[YamlMember(Alias = "client-key-data", ApplyNamingConventions = false)]
public string ClientKeyData { get; set; }

/// <summary>
/// Gets or sets the path to a client key file for TLS.
/// </summary>
[YamlMember(Alias = "client-key")]
[YamlMember(Alias = "client-key", ApplyNamingConventions = false)]
public string ClientKey { get; set; }

/// <summary>
Expand All @@ -48,13 +48,13 @@ public class UserCredentials
/// <summary>
/// Gets or sets the groups to imperonate.
/// </summary>
[YamlMember(Alias = "as-groups")]
[YamlMember(Alias = "as-groups", ApplyNamingConventions = false)]
public IEnumerable<string> ImpersonateGroups { get; set; } = new string[0];

/// <summary>
/// Gets or sets additional information for impersonated user.
/// </summary>
[YamlMember(Alias = "as-user-extra")]
[YamlMember(Alias = "as-user-extra", ApplyNamingConventions = false)]
public Dictionary<string, string> ImpersonateUserExtra { get; set; } = new Dictionary<string, string>();

/// <summary>
Expand All @@ -72,7 +72,7 @@ public class UserCredentials
/// <summary>
/// Gets or sets custom authentication plugin for the kubernetes cluster.
/// </summary>
[YamlMember(Alias = "auth-provider")]
[YamlMember(Alias = "auth-provider", ApplyNamingConventions = false)]
public Dictionary<string, dynamic> AuthProvider { get; set; }

/// <summary>
Expand Down
75 changes: 39 additions & 36 deletions src/KubernetesClientConfiguration.ConfigFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using k8s.Exceptions;
using k8s.KubeConfigModels;
using YamlDotNet.Serialization;


namespace k8s
{
public partial class KubernetesClientConfiguration
Expand All @@ -28,7 +28,7 @@ public partial class KubernetesClientConfiguration
/// Initializes a new instance of the <see cref="KubernetesClientConfiguration" /> from config file
/// </summary>
/// <param name="masterUrl">kube api server endpoint</param>
/// <param name="kubeconfigPath">kubeconfig filepath</param>
/// <param name="kubeconfigPath">Explicit file path to kubeconfig. Set to null to use the default file path</param>
public static KubernetesClientConfiguration BuildConfigFromConfigFile(string masterUrl = null,
string kubeconfigPath = null)
{
Expand Down Expand Up @@ -57,7 +57,7 @@ public static KubernetesClientConfiguration BuildConfigFromConfigFile(FileInfo k

/// <summary>
/// </summary>
/// <param name="kubeconfig">Fileinfo of the kubeconfig, cannot be null, whitespaced or empty</param>
/// <param name="kubeconfig">String representation of kubeconfig contents, cannot be null, whitespaced or empty</param>
/// <param name="currentContext">override the context in config file, set null if do not want to override</param>
/// <param name="masterUrl">overrider kube api server endpoint, set null if do not want to override</param>
public static KubernetesClientConfiguration BuildConfigFromConfigFile(string kubeconfig,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I couldn't find any references to this method. Can we deprecate?

Copy link
Contributor

Choose a reason for hiding this comment

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

Sure, seems reasonable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Mark as [Obsolete] or remove?

Expand All @@ -68,7 +68,7 @@ public static KubernetesClientConfiguration BuildConfigFromConfigFile(string kub
throw new NullReferenceException(nameof(kubeconfig));
}

var k8SConfig = LoadKubeConfig(kubeconfig);
var k8SConfig = Yaml.LoadFromString<K8SConfiguration>(kubeconfig);
var k8SConfiguration = GetKubernetesClientConfiguration(currentContext, masterUrl, k8SConfig);

return k8SConfiguration;
Expand All @@ -94,7 +94,7 @@ public static KubernetesClientConfiguration BuildConfigFromConfigFile(Stream kub

kubeconfig.Position = 0;

var k8SConfig = LoadKubeConfig(kubeconfig);
var k8SConfig = Yaml.LoadFromStreamAsync<K8SConfiguration>(kubeconfig).GetAwaiter().GetResult();
var k8SConfiguration = GetKubernetesClientConfiguration(currentContext, masterUrl, k8SConfig);

return k8SConfiguration;
Expand Down Expand Up @@ -259,53 +259,56 @@ private void SetUserDetails(K8SConfiguration k8SConfig, Context activeContext)
throw new KubeConfigException(
$"User: {userDetails.Name} does not have appropriate auth credentials in kubeconfig");
}
}
}

/// <summary>
/// Loads entire Kube Config from default or explicit file path
/// </summary>
/// <param name="kubeconfigPath">Explicit file path to kubeconfig. Set to null to use the default file path</param>
/// <returns></returns>
public static async Task<K8SConfiguration> LoadKubeConfigAsync(string kubeconfigPath = null)
{
var fileInfo = new FileInfo(kubeconfigPath ?? KubeConfigDefaultLocation);

return await LoadKubeConfigAsync(fileInfo);
}

/// <summary>
/// Loads entire Kube Config from default or explicit file path
/// </summary>
/// <param name="kubeconfigPath">Explicit file path to kubeconfig. Set to null to use the default file path</param>
/// <returns></returns>
public static K8SConfiguration LoadKubeConfig(string kubeconfigPath = null)
{
return LoadKubeConfigAsync(kubeconfigPath).GetAwaiter().GetResult();
}

// <summary>
/// Loads Kube Config
/// </summary>
/// <param name="kubeconfig">Kube config file contents</param>
/// <returns>Instance of the <see cref="K8SConfiguration"/> class</returns>
private static K8SConfiguration LoadKubeConfig(FileInfo kubeconfig)
public static async Task<K8SConfiguration> LoadKubeConfigAsync(FileInfo kubeconfig)
{
if (!kubeconfig.Exists)
{
throw new KubeConfigException($"kubeconfig file not found at {kubeconfig.FullName}");
}

using (var stream = kubeconfig.OpenRead())
{
return await Yaml.LoadFromStreamAsync<K8SConfiguration>(stream);
}

var deserializeBuilder = new DeserializerBuilder();
var deserializer = deserializeBuilder.Build();
using (var kubeConfigTextStream = kubeconfig.OpenText())
{
return deserializer.Deserialize<K8SConfiguration>(kubeConfigTextStream);
}
}

/// <summary>
/// Loads Kube Config from string
/// </summary>
/// <param name="kubeconfig">Kube config file contents</param>
/// <returns>Instance of the <see cref="K8SConfiguration"/> class</returns>
private static K8SConfiguration LoadKubeConfig(string kubeconfig)
{

var deserializeBuilder = new DeserializerBuilder();
var deserializer = deserializeBuilder.Build();
return deserializer.Deserialize<K8SConfiguration>(kubeconfig);
}

/// <summary>
/// Loads Kube Config from stream.
/// Loads Kube Config
/// </summary>
/// <param name="kubeconfig">Kube config file contents</param>
/// <returns>Instance of the <see cref="K8SConfiguration"/> class</returns>
private static K8SConfiguration LoadKubeConfig(Stream kubeconfig)
public static K8SConfiguration LoadKubeConfig(FileInfo kubeconfig)
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's keep this as a stream, since we want to support loading from network streams as well as files.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch. I'll add it back in and use Yaml.LoadFromStreamAsync. Should we keep the method with FileInfo kubeconfig?

Essentially, we would have these method signatures.

public static async Task<K8SConfiguration> LoadKubeConfigAsync(string kubeconfigPath = null) public static K8SConfiguration LoadKubeConfig(string kubeconfigPath = null) public static async Task<K8SConfiguration> LoadKubeConfigAsync(FileInfo kubeconfig) public static K8SConfiguration LoadKubeConfig(FileInfo kubeconfig) public static async Task<K8SConfiguration> LoadKubeConfigAsync(Stream kubeconfigStream) public static K8SConfiguration LoadKubeConfig(Stream kubeconfigStream) 
Copy link
Contributor

Choose a reason for hiding this comment

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

Your call about FileInfo. it's easy enough to turn a file name into a Stream

{
using (var sr = new StreamReader(kubeconfig))
{
var strKubeConfig = sr.ReadToEnd();
return LoadKubeConfig(strKubeConfig);
}
return LoadKubeConfigAsync(kubeconfig).GetAwaiter().GetResult();
}
}
}
76 changes: 74 additions & 2 deletions tests/KubernetesClientConfigurationTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System.IO;
using System.Linq;
using k8s.Exceptions;
using k8s.KubeConfigModels;
using Xunit;

namespace k8s.Tests
Expand Down Expand Up @@ -270,8 +272,8 @@ public void NoCurrentContext()
public void DeletedConfigurationFile()
{
var assetFileInfo = new FileInfo("assets/kubeconfig.yml");
var tempFileInfo = new FileInfo(Path.GetTempFileName());
var tempFileInfo = new FileInfo(Path.GetTempFileName());

File.Copy(assetFileInfo.FullName, tempFileInfo.FullName, /* overwrite: */ true);

KubernetesClientConfiguration config;
Expand Down Expand Up @@ -322,6 +324,76 @@ public void AsUserExtra()

var cfg = KubernetesClientConfiguration.BuildConfigFromConfigFile(txt, null, null);
Assert.NotNull(cfg.Host);
}

/// <summary>
/// Ensures Kube config file is loaded from explicit file
/// </summary>
[Fact]
public void LoadKubeConfigExplicitFilePath()
{
var txt = File.ReadAllText("assets/kubeconfig.yml");
var expectedCfg = Yaml.LoadFromString<K8SConfiguration>(txt);

var cfg = KubernetesClientConfiguration.LoadKubeConfig("assets/kubeconfig.yml");

Assert.NotNull(cfg);

ConfigsEqual(expectedCfg, cfg);
}

private void ConfigsEqual(K8SConfiguration expected, K8SConfiguration actual)
Copy link
Contributor

Choose a reason for hiding this comment

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

This function is too long, I think. Factor it so there is an

assertContextEqual(...)

assertUserEqual(...)

assertClusterEqual(...)

set of methods and refactor the for loops to just call those methods?

{
Assert.Equal(expected.ApiVersion, actual.ApiVersion);
Assert.Equal(expected.CurrentContext, actual.CurrentContext);

foreach (var expectedContext in expected.Contexts)
{
// Will throw exception if not found
var actualContext = actual.Contexts.First(c => c.Name.Equals(expectedContext.Name));

Assert.Equal(expectedContext.Namespace, actualContext.Namespace);
Assert.Equal(expectedContext.ContextDetails.Cluster, actualContext.ContextDetails.Cluster);
Assert.Equal(expectedContext.ContextDetails.User, actualContext.ContextDetails.User);
Assert.Equal(expectedContext.ContextDetails.Namespace, actualContext.ContextDetails.Namespace);
}

foreach (var expectedCluster in expected.Clusters)
{
// Will throw exception if not found
var actualCluster = actual.Clusters.First(c => c.Name.Equals(expectedCluster.Name));

Assert.Equal(expectedCluster.ClusterEndpoint.CertificateAuthority, actualCluster.ClusterEndpoint.CertificateAuthority);
Assert.Equal(expectedCluster.ClusterEndpoint.CertificateAuthorityData, actualCluster.ClusterEndpoint.CertificateAuthorityData);
Assert.Equal(expectedCluster.ClusterEndpoint.Server, actualCluster.ClusterEndpoint.Server);
Assert.Equal(expectedCluster.ClusterEndpoint.SkipTlsVerify, actualCluster.ClusterEndpoint.SkipTlsVerify);
}

foreach (var expectedUser in expected.Users)
{
// Will throw exception if not found
var actualUser = actual.Users.First(u => u.Name.Equals(expectedUser.Name));

var expectedCreds = expectedUser.UserCredentials;
var actualCreds = expectedUser.UserCredentials;

Assert.Equal(expectedCreds.ClientCertificateData, actualCreds.ClientCertificateData);
Assert.Equal(expectedCreds.ClientCertificate, actualCreds.ClientCertificate);
Assert.Equal(expectedCreds.ClientKeyData, actualCreds.ClientKeyData);
Assert.Equal(expectedCreds.ClientKey, actualCreds.ClientKey);
Assert.Equal(expectedCreds.Token, actualCreds.Token);
Assert.Equal(expectedCreds.Impersonate, actualCreds.Impersonate);
Assert.Equal(expectedCreds.UserName, actualCreds.UserName);
Assert.Equal(expectedCreds.Password, actualCreds.Password);

Assert.True(expectedCreds.ImpersonateGroups.All(x => actualCreds.ImpersonateGroups.Contains(x)));
Assert.True(expectedCreds.ImpersonateUserExtra.All(x => actualCreds.ImpersonateUserExtra.Contains(x)));

if (expectedCreds.AuthProvider != null)
{
Assert.True(expectedCreds.AuthProvider.All(x => actualCreds.AuthProvider.Contains(x)));
}
}
}
}
}