diff --git a/src/KubernetesClient/KubeConfigModels/K8SConfiguration.cs b/src/KubernetesClient/KubeConfigModels/K8SConfiguration.cs
index b74fc54..980d93a 100644
--- a/src/KubernetesClient/KubeConfigModels/K8SConfiguration.cs
+++ b/src/KubernetesClient/KubeConfigModels/K8SConfiguration.cs
@@ -9,6 +9,7 @@ namespace k8s.KubeConfigModels
///
///
/// Should be kept in sync with https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/client-go/tools/clientcmd/api/v1/types.go
+ /// Should update MergeKubeConfig in KubernetesClientConfiguration.ConfigFile.cs if updated.
///
public class K8SConfiguration
{
diff --git a/src/KubernetesClient/KubernetesClientConfiguration.ConfigFile.cs b/src/KubernetesClient/KubernetesClientConfiguration.ConfigFile.cs
index bf6b7af..47e174d 100644
--- a/src/KubernetesClient/KubernetesClientConfiguration.ConfigFile.cs
+++ b/src/KubernetesClient/KubernetesClientConfiguration.ConfigFile.cs
@@ -1,7 +1,7 @@
using System;
+using System.Collections.Generic;
#if NETSTANDARD2_0
using Newtonsoft.Json;
-using System.Collections.Generic;
using System.Diagnostics;
#endif
using System.IO;
@@ -30,6 +30,9 @@ namespace k8s
///
public string CurrentContext { get; private set; }
+ // For testing
+ internal static string KubeConfigEnvironmentVariable { get; set; } = "KUBECONFIG";
+
///
/// Initializes a new instance of the from default locations
/// If the KUBECONFIG environment variable is set, then that will be used.
@@ -37,27 +40,35 @@ namespace k8s
/// Then, it checks whether it is executing inside a cluster and will use .
/// Finally, if nothing else exists, it creates a default config with localhost:8080 as host.
///
+ ///
+ /// If multiple kubeconfig files are specified in the KUBECONFIG environment variable,
+ /// merges the files, where first occurence wins. See https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#merging-kubeconfig-files.
+ ///
public static KubernetesClientConfiguration BuildDefaultConfig()
{
- var kubeconfig = Environment.GetEnvironmentVariable("KUBECONFIG");
+ var kubeconfig = Environment.GetEnvironmentVariable(KubeConfigEnvironmentVariable);
if (kubeconfig != null)
{
- return BuildConfigFromConfigFile(kubeconfigPath: kubeconfig);
+ var configList = kubeconfig.Split(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ';' : ':').Select((s) => new FileInfo(s));
+ var k8sConfig = LoadKubeConfig(configList.ToArray());
+ return BuildConfigFromConfigObject(k8sConfig);
}
+
if (File.Exists(KubeConfigDefaultLocation))
{
return BuildConfigFromConfigFile(kubeconfigPath: KubeConfigDefaultLocation);
}
+
if (IsInCluster())
{
return InClusterConfig();
}
+
var config = new KubernetesClientConfiguration();
config.Host = "http://localhost:8080";
return config;
}
-
///
/// Initializes a new instance of the from config file
///
@@ -564,6 +575,46 @@ namespace k8s
return LoadKubeConfigAsync(kubeconfigStream).GetAwaiter().GetResult();
}
+ ///
+ /// Loads Kube Config
+ ///
+ /// List of kube config file contents
+ /// When , the paths in the kubeconfig file will be considered to be relative to the directory in which the kubeconfig
+ /// file is located. When , the paths will be considered to be relative to the current working directory.
+ /// Instance of the class
+ ///
+ /// The kube config files will be merges into a single , where first occurence wins.
+ /// See https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#merging-kubeconfig-files.
+ ///
+ internal static K8SConfiguration LoadKubeConfig(FileInfo[] kubeConfigs, bool useRelativePaths = true)
+ {
+ return LoadKubeConfigAsync(kubeConfigs, useRelativePaths).GetAwaiter().GetResult();
+ }
+
+ ///
+ /// Loads Kube Config
+ ///
+ /// List of kube config file contents
+ /// When , the paths in the kubeconfig file will be considered to be relative to the directory in which the kubeconfig
+ /// file is located. When , the paths will be considered to be relative to the current working directory.
+ /// Instance of the class
+ ///
+ /// The kube config files will be merges into a single , where first occurence wins.
+ /// See https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#merging-kubeconfig-files.
+ ///
+ internal static async Task LoadKubeConfigAsync(FileInfo[] kubeConfigs, bool useRelativePaths = true)
+ {
+ var basek8SConfig = await LoadKubeConfigAsync(kubeConfigs[0], useRelativePaths).ConfigureAwait(false);
+
+ for (var i = 1; i < kubeConfigs.Length; i++)
+ {
+ var mergek8SConfig = await LoadKubeConfigAsync(kubeConfigs[i], useRelativePaths).ConfigureAwait(false);
+ MergeKubeConfig(basek8SConfig, mergek8SConfig);
+ }
+
+ return basek8SConfig;
+ }
+
///
/// Tries to get the full path to a file referenced from the Kubernetes configuration.
///
@@ -593,5 +644,76 @@ namespace k8s
return Path.Combine(Path.GetDirectoryName(configuration.FileName), path);
}
}
+
+ ///
+ /// Merges kube config files together, preferring configuration present in the base config over the merge config.
+ ///
+ /// The to merge into
+ /// The to merge from
+ private static void MergeKubeConfig(K8SConfiguration basek8SConfig, K8SConfiguration mergek8SConfig)
+ {
+ // For scalar values, prefer local values
+ basek8SConfig.CurrentContext = basek8SConfig.CurrentContext ?? mergek8SConfig.CurrentContext;
+ basek8SConfig.FileName = basek8SConfig.FileName ?? mergek8SConfig.FileName;
+
+ // Kinds must match in kube config, otherwise throw.
+ if (basek8SConfig.Kind != mergek8SConfig.Kind)
+ {
+ throw new KubeConfigException($"kubeconfig \"kind\" are different between {basek8SConfig.FileName} and {mergek8SConfig.FileName}");
+ }
+
+ if (mergek8SConfig.Preferences != null)
+ {
+ foreach (var preference in mergek8SConfig.Preferences)
+ {
+ if (basek8SConfig.Preferences?.ContainsKey(preference.Key) == false)
+ {
+ basek8SConfig.Preferences[preference.Key] = preference.Value;
+ }
+ }
+ }
+
+ if (mergek8SConfig.Extensions != null)
+ {
+ foreach (var extension in mergek8SConfig.Extensions)
+ {
+ if (basek8SConfig.Extensions?.ContainsKey(extension.Key) == false)
+ {
+ basek8SConfig.Extensions[extension.Key] = extension.Value;
+ }
+ }
+ }
+
+ // Note, Clusters, Contexts, and Extensions are map-like in config despite being represented as a list here:
+ // https://github.com/kubernetes/client-go/blob/ede92e0fe62deed512d9ceb8bf4186db9f3776ff/tools/clientcmd/api/types.go#L238
+ basek8SConfig.Clusters = MergeLists(basek8SConfig.Clusters, mergek8SConfig.Clusters, (s) => s.Name);
+ basek8SConfig.Users = MergeLists(basek8SConfig.Users, mergek8SConfig.Users, (s) => s.Name);
+ basek8SConfig.Contexts = MergeLists(basek8SConfig.Contexts, mergek8SConfig.Contexts, (s) => s.Name);
+ }
+
+ private static IEnumerable MergeLists(IEnumerable baseList, IEnumerable mergeList, Func getNameFunc)
+ {
+ if (mergeList != null && mergeList.Count() > 0)
+ {
+ var mapping = new Dictionary();
+ foreach (var item in baseList)
+ {
+ mapping[getNameFunc(item)] = item;
+ }
+
+ foreach (var item in mergeList)
+ {
+ var name = getNameFunc(item);
+ if (!mapping.ContainsKey(name))
+ {
+ mapping[name] = item;
+ }
+ }
+
+ return mapping.Values;
+ }
+
+ return baseList;
+ }
}
}
diff --git a/tests/KubernetesClient.Tests/KubernetesClientConfigurationTests.cs b/tests/KubernetesClient.Tests/KubernetesClientConfigurationTests.cs
index 341a252..a5f23d8 100755
--- a/tests/KubernetesClient.Tests/KubernetesClientConfigurationTests.cs
+++ b/tests/KubernetesClient.Tests/KubernetesClientConfigurationTests.cs
@@ -1,5 +1,7 @@
+using System;
using System.IO;
using System.Linq;
+using System.Runtime.InteropServices;
using System.Threading;
using k8s.Exceptions;
using k8s.KubeConfigModels;
@@ -407,6 +409,102 @@ namespace k8s.Tests
AssertConfigEqual(expectedCfg, cfg);
}
+ [Fact]
+ public void LoadKubeConfigFromEnvironmentVariable()
+ {
+ // BuildDefaultConfig assumes UseRelativePaths: true, which isn't
+ // done by any tests.
+ var filePath = Path.GetFullPath("assets/kubeconfig.relative.yml");
+ var environmentVariable = "KUBECONFIG_LoadKubeConfigFromEnvironmentVariable";
+
+ Environment.SetEnvironmentVariable(environmentVariable, filePath);
+ KubernetesClientConfiguration.KubeConfigEnvironmentVariable = environmentVariable;
+
+ var cfg = KubernetesClientConfiguration.BuildDefaultConfig();
+
+ Assert.NotNull(cfg);
+ }
+
+ [Fact]
+ public void LoadKubeConfigFromEnvironmentVariable_MultipleConfigs()
+ {
+ // This test makes sure that a list of environment variables works (no exceptions),
+ // doesn't check validity of configuration, which is done in other tests.
+
+ var filePath = Path.GetFullPath("assets/kubeconfig.relative.yml");
+ var environmentVariable = "KUBECONFIG_LoadKubeConfigFromEnvironmentVariable_MultipleConfigs";
+
+ Environment.SetEnvironmentVariable(environmentVariable, string.Concat(filePath, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ';' : ':', filePath));
+ KubernetesClientConfiguration.KubeConfigEnvironmentVariable = environmentVariable;
+
+ var cfg = KubernetesClientConfiguration.BuildDefaultConfig();
+
+ Assert.NotNull(cfg);
+ }
+
+ [Fact]
+ public void LoadSameKubeConfigFromEnvironmentVariableUnmodified()
+ {
+ var txt = File.ReadAllText("assets/kubeconfig.yml");
+ var expectedCfg = Yaml.LoadFromString(txt);
+
+ var fileInfo = new FileInfo(Path.GetFullPath("assets/kubeconfig.yml"));
+
+ var cfg = KubernetesClientConfiguration.LoadKubeConfig(new FileInfo[] { fileInfo, fileInfo });
+
+ AssertConfigEqual(expectedCfg, cfg);
+ }
+
+ [Fact]
+ public void MergeKubeConfigNoDuplicates()
+ {
+ var firstPath = Path.GetFullPath("assets/kubeconfig.as-user-extra.yml");
+ var secondPath = Path.GetFullPath("assets/kubeconfig.yml");
+
+ var cfg = KubernetesClientConfiguration.LoadKubeConfig(new FileInfo[] { new FileInfo(firstPath), new FileInfo(secondPath) });
+
+ // Merged file has 6 users now.
+ Assert.Equal(6, cfg.Users.Count());
+ Assert.Equal(5, cfg.Clusters.Count());
+ Assert.Equal(5, cfg.Contexts.Count());
+ }
+
+ [Fact]
+ public void AlwaysPicksFirstOccurence()
+ {
+ var firstPath = Path.GetFullPath("assets/kubeconfig.no-cluster.yml");
+ var secondPath = Path.GetFullPath("assets/kubeconfig.no-context.yml");
+
+ var cfg = KubernetesClientConfiguration.LoadKubeConfig(new FileInfo[] { new FileInfo(firstPath), new FileInfo(secondPath) });
+
+ var user = cfg.Users.Where(u => u.Name == "green-user").Single();
+ Assert.NotNull(user.UserCredentials.Password);
+ Assert.Null(user.UserCredentials.ClientCertificate);
+ }
+
+ [Fact]
+ public void ContextFromSecondWorks()
+ {
+ var firstPath = Path.GetFullPath("assets/kubeconfig.no-current-context.yml");
+ var secondPath = Path.GetFullPath("assets/kubeconfig.no-user.yml");
+
+ var cfg = KubernetesClientConfiguration.LoadKubeConfig(new FileInfo[] { new FileInfo(firstPath), new FileInfo(secondPath) });
+
+ // green-user
+ Assert.NotNull(cfg.CurrentContext);
+ }
+
+ [Fact]
+ public void ContextPreferencesExtensionsMergeWithDuplicates()
+ {
+ var path = Path.GetFullPath("assets/kubeconfig.preferences-extensions.yml");
+
+ var cfg = KubernetesClientConfiguration.LoadKubeConfig(new FileInfo[] { new FileInfo(path), new FileInfo(path) });
+
+ Assert.Equal(1, cfg.Extensions.Count);
+ Assert.Equal(1, cfg.Preferences.Count);
+ }
+
///
/// Ensures Kube config file can be loaded from within a non-default .
/// The use of ensures the test is run from within a UI-like .
diff --git a/tests/KubernetesClient.Tests/assets/kubeconfig.preferences-extensions.yml b/tests/KubernetesClient.Tests/assets/kubeconfig.preferences-extensions.yml
new file mode 100644
index 0000000..7e8a6d2
--- /dev/null
+++ b/tests/KubernetesClient.Tests/assets/kubeconfig.preferences-extensions.yml
@@ -0,0 +1,6 @@
+apiVersion: v1
+kind: Config
+preferences:
+ colors: true
+extensions:
+ foo: bar
diff --git a/tests/KubernetesClient.Tests/assets/kubeconfig.user-pass.yml b/tests/KubernetesClient.Tests/assets/kubeconfig.user-pass.yml
index 2f621c3..7ef9af2 100644
--- a/tests/KubernetesClient.Tests/assets/kubeconfig.user-pass.yml
+++ b/tests/KubernetesClient.Tests/assets/kubeconfig.user-pass.yml
@@ -19,4 +19,4 @@ users:
- name: green-user
user:
password: secret
- username: admin
\ No newline at end of file
+ username: admin