|
1 | 1 | using System;
|
| 2 | +using System.Collections.Generic; |
2 | 3 | #if NETSTANDARD2_0
|
3 | 4 | using Newtonsoft.Json;
|
4 |
| -using System.Collections.Generic; |
5 | 5 | using System.Diagnostics;
|
6 | 6 | #endif
|
7 | 7 | using System.IO;
|
@@ -30,34 +30,45 @@ public partial class KubernetesClientConfiguration
|
30 | 30 | /// </summary>
|
31 | 31 | public string CurrentContext { get; private set; }
|
32 | 32 |
|
| 33 | + // For testing |
| 34 | + internal static string KubeConfigEnvironmentVariable { get; set; } = "KUBECONFIG"; |
| 35 | + |
33 | 36 | /// <summary>
|
34 | 37 | /// Initializes a new instance of the <see cref="KubernetesClientConfiguration" /> from default locations
|
35 | 38 | /// If the KUBECONFIG environment variable is set, then that will be used.
|
36 | 39 | /// Next, it looks for a config file at <see cref="KubeConfigDefaultLocation"/>.
|
37 | 40 | /// Then, it checks whether it is executing inside a cluster and will use <see cref="InClusterConfig()" />.
|
38 | 41 | /// Finally, if nothing else exists, it creates a default config with localhost:8080 as host.
|
39 | 42 | /// </summary>
|
| 43 | + /// <remarks> |
| 44 | + /// If multiple kubeconfig files are specified in the KUBECONFIG environment variable, |
| 45 | + /// merges the files, where first occurence wins. See https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#merging-kubeconfig-files. |
| 46 | + /// </remarks> |
40 | 47 | public static KubernetesClientConfiguration BuildDefaultConfig()
|
41 | 48 | {
|
42 |
| - var kubeconfig = Environment.GetEnvironmentVariable("KUBECONFIG"); |
| 49 | + var kubeconfig = Environment.GetEnvironmentVariable(KubeConfigEnvironmentVariable); |
43 | 50 | if (kubeconfig != null)
|
44 | 51 | {
|
45 |
| - return BuildConfigFromConfigFile(kubeconfigPath: kubeconfig); |
| 52 | + var configList = kubeconfig.Split(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ';' : ':').Select((s) => new FileInfo(s)); |
| 53 | + var k8sConfig = LoadKubeConfig(configList.ToArray()); |
| 54 | + return BuildConfigFromConfigObject(k8sConfig); |
46 | 55 | }
|
| 56 | + |
47 | 57 | if (File.Exists(KubeConfigDefaultLocation))
|
48 | 58 | {
|
49 | 59 | return BuildConfigFromConfigFile(kubeconfigPath: KubeConfigDefaultLocation);
|
50 | 60 | }
|
| 61 | + |
51 | 62 | if (IsInCluster())
|
52 | 63 | {
|
53 | 64 | return InClusterConfig();
|
54 | 65 | }
|
| 66 | + |
55 | 67 | var config = new KubernetesClientConfiguration();
|
56 | 68 | config.Host = "http://localhost:8080";
|
57 | 69 | return config;
|
58 | 70 | }
|
59 | 71 |
|
60 |
| - |
61 | 72 | /// <summary>
|
62 | 73 | /// Initializes a new instance of the <see cref="KubernetesClientConfiguration" /> from config file
|
63 | 74 | /// </summary>
|
@@ -564,6 +575,46 @@ public static K8SConfiguration LoadKubeConfig(Stream kubeconfigStream)
|
564 | 575 | return LoadKubeConfigAsync(kubeconfigStream).GetAwaiter().GetResult();
|
565 | 576 | }
|
566 | 577 |
|
| 578 | + /// <summary> |
| 579 | + /// Loads Kube Config |
| 580 | + /// </summary> |
| 581 | + /// <param name="kubeconfigs">List of kube config file contents</param> |
| 582 | + /// <param name="useRelativePaths">When <see langword="true"/>, the paths in the kubeconfig file will be considered to be relative to the directory in which the kubeconfig |
| 583 | + /// file is located. When <see langword="false"/>, the paths will be considered to be relative to the current working directory.</param> |
| 584 | + /// <returns>Instance of the <see cref="K8SConfiguration"/> class</returns> |
| 585 | + /// <remarks> |
| 586 | + /// The kube config files will be merges into a single <see cref="K8SConfiguration"/>, where first occurence wins. |
| 587 | + /// See https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#merging-kubeconfig-files. |
| 588 | + /// </remarks> |
| 589 | + internal static K8SConfiguration LoadKubeConfig(FileInfo[] kubeConfigs, bool useRelativePaths = true) |
| 590 | + { |
| 591 | + return LoadKubeConfigAsync(kubeConfigs, useRelativePaths).GetAwaiter().GetResult(); |
| 592 | + } |
| 593 | + |
| 594 | + /// <summary> |
| 595 | + /// Loads Kube Config |
| 596 | + /// </summary> |
| 597 | + /// <param name="kubeconfigs">List of kube config file contents</param> |
| 598 | + /// <param name="useRelativePaths">When <see langword="true"/>, the paths in the kubeconfig file will be considered to be relative to the directory in which the kubeconfig |
| 599 | + /// file is located. When <see langword="false"/>, the paths will be considered to be relative to the current working directory.</param> |
| 600 | + /// <returns>Instance of the <see cref="K8SConfiguration"/> class</returns> |
| 601 | + /// <remarks> |
| 602 | + /// The kube config files will be merges into a single <see cref="K8SConfiguration"/>, where first occurence wins. |
| 603 | + /// See https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#merging-kubeconfig-files. |
| 604 | + /// </remarks> |
| 605 | + internal static async Task<K8SConfiguration> LoadKubeConfigAsync(FileInfo[] kubeConfigs, bool useRelativePaths = true) |
| 606 | + { |
| 607 | + var basek8SConfig = await LoadKubeConfigAsync(kubeConfigs[0], useRelativePaths).ConfigureAwait(false); |
| 608 | + |
| 609 | + for (var i = 1; i < kubeConfigs.Length; i++) |
| 610 | + { |
| 611 | + var mergek8SConfig = await LoadKubeConfigAsync(kubeConfigs[i], useRelativePaths).ConfigureAwait(false); |
| 612 | + MergeKubeConfig(basek8SConfig, mergek8SConfig); |
| 613 | + } |
| 614 | + |
| 615 | + return basek8SConfig; |
| 616 | + } |
| 617 | + |
567 | 618 | /// <summary>
|
568 | 619 | /// Tries to get the full path to a file referenced from the Kubernetes configuration.
|
569 | 620 | /// </summary>
|
@@ -593,5 +644,76 @@ private static string GetFullPath(K8SConfiguration configuration, string path)
|
593 | 644 | return Path.Combine(Path.GetDirectoryName(configuration.FileName), path);
|
594 | 645 | }
|
595 | 646 | }
|
| 647 | + |
| 648 | + /// <summary> |
| 649 | + /// Merges kube config files together, preferring configuration present in the base config over the merge config. |
| 650 | + /// </summary> |
| 651 | + /// <param name="basek8SConfig">The <see cref="K8SConfiguration"/> to merge into</param> |
| 652 | + /// <param name="mergek8SConfig">The <see cref="K8SConfiguration"/> to merge from</param> |
| 653 | + private static void MergeKubeConfig(K8SConfiguration basek8SConfig, K8SConfiguration mergek8SConfig) |
| 654 | + { |
| 655 | + // For scalar values, prefer local values |
| 656 | + basek8SConfig.CurrentContext = basek8SConfig.CurrentContext ?? mergek8SConfig.CurrentContext; |
| 657 | + basek8SConfig.FileName = basek8SConfig.FileName ?? mergek8SConfig.FileName; |
| 658 | + |
| 659 | + // Kinds must match in kube config, otherwise throw. |
| 660 | + if (basek8SConfig.Kind != mergek8SConfig.Kind) |
| 661 | + { |
| 662 | + throw new KubeConfigException($"kubeconfig \"kind\" are different between {basek8SConfig.FileName} and {mergek8SConfig.FileName}"); |
| 663 | + } |
| 664 | + |
| 665 | + if (mergek8SConfig.Preferences != null) |
| 666 | + { |
| 667 | + foreach (var preference in mergek8SConfig.Preferences) |
| 668 | + { |
| 669 | + if (basek8SConfig.Preferences?.ContainsKey(preference.Key) == false) |
| 670 | + { |
| 671 | + basek8SConfig.Preferences[preference.Key] = preference.Value; |
| 672 | + } |
| 673 | + } |
| 674 | + } |
| 675 | + |
| 676 | + if (mergek8SConfig.Extensions != null) |
| 677 | + { |
| 678 | + foreach (var extension in mergek8SConfig.Extensions) |
| 679 | + { |
| 680 | + if (basek8SConfig.Extensions?.ContainsKey(extension.Key) == false) |
| 681 | + { |
| 682 | + basek8SConfig.Extensions[extension.Key] = extension.Value; |
| 683 | + } |
| 684 | + } |
| 685 | + } |
| 686 | + |
| 687 | + // Note, Clusters, Contexts, and Extensions are map-like in config despite being represented as a list here: |
| 688 | + // https://github.com/kubernetes/client-go/blob/ede92e0fe62deed512d9ceb8bf4186db9f3776ff/tools/clientcmd/api/types.go#L238 |
| 689 | + basek8SConfig.Clusters = MergeLists(basek8SConfig.Clusters, mergek8SConfig.Clusters, (s) => s.Name); |
| 690 | + basek8SConfig.Users = MergeLists(basek8SConfig.Users, mergek8SConfig.Users, (s) => s.Name); |
| 691 | + basek8SConfig.Contexts = MergeLists(basek8SConfig.Contexts, mergek8SConfig.Contexts, (s) => s.Name); |
| 692 | + } |
| 693 | + |
| 694 | + private static IEnumerable<T> MergeLists<T>(IEnumerable<T> baseList, IEnumerable<T> mergeList, Func<T, string> getNameFunc) |
| 695 | + { |
| 696 | + if (mergeList != null && mergeList.Count() > 0) |
| 697 | + { |
| 698 | + var mapping = new Dictionary<string, T>(); |
| 699 | + foreach (var item in baseList) |
| 700 | + { |
| 701 | + mapping[getNameFunc(item)] = item; |
| 702 | + } |
| 703 | + |
| 704 | + foreach (var item in mergeList) |
| 705 | + { |
| 706 | + var name = getNameFunc(item); |
| 707 | + if (!mapping.ContainsKey(name)) |
| 708 | + { |
| 709 | + mapping[name] = item; |
| 710 | + } |
| 711 | + } |
| 712 | + |
| 713 | + return mapping.Values; |
| 714 | + } |
| 715 | + |
| 716 | + return baseList; |
| 717 | + } |
596 | 718 | }
|
597 | 719 | }
|
0 commit comments