From 8da722708fc44449cae1a7848a8b31fdc9a5c18a Mon Sep 17 00:00:00 2001 From: Adam Friedman Date: Sat, 21 Sep 2019 14:11:08 +1000 Subject: [PATCH 1/2] Add configuration-source support for multiple ConfigMaps and Secrets tintoy/dotnet-kube-client#96 --- .../ConfigMapBuilderPropertyConstants.cs | 17 ---- .../ConfigMapConfigurationProvider.cs | 44 ++++------- .../ConfigMapConfigurationSource.cs | 30 ++++--- .../KubeClientConfigurationExtensions.cs | 37 ++++----- .../SecretConfigurationProvider.cs | 44 ++++------- .../SecretConfigurationSource.cs | 28 ++++--- .../ConfigMapConfigurationSettings.cs | 79 +++++++++++++++++++ .../Settings/SecretConfigurationSettings.cs | 79 +++++++++++++++++++ 8 files changed, 235 insertions(+), 123 deletions(-) delete mode 100644 src/KubeClient.Extensions.Configuration/ConfigMapBuilderPropertyConstants.cs create mode 100644 src/KubeClient.Extensions.Configuration/Settings/ConfigMapConfigurationSettings.cs create mode 100644 src/KubeClient.Extensions.Configuration/Settings/SecretConfigurationSettings.cs diff --git a/src/KubeClient.Extensions.Configuration/ConfigMapBuilderPropertyConstants.cs b/src/KubeClient.Extensions.Configuration/ConfigMapBuilderPropertyConstants.cs deleted file mode 100644 index 448dd7b8..00000000 --- a/src/KubeClient.Extensions.Configuration/ConfigMapBuilderPropertyConstants.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace KubeClient.Extensions.Configuration -{ - /// - /// Constants for ConfigMap builder properties. - /// - static class ConfigMapBuilderPropertyConstants - { - private const string Prefix = "KubeClient_ConfigMap_"; - - public const string Client = Prefix + "Client"; - public const string Name = Prefix + "Name"; - public const string Namespace = Prefix + "Namespace"; - public const string SectionName = Prefix + "SectionName"; - public const string Watch = Prefix + "Watch"; - public const string ThrowOnNotFound = Prefix + "ThrowOnNotFound"; - } -} \ No newline at end of file diff --git a/src/KubeClient.Extensions.Configuration/ConfigMapConfigurationProvider.cs b/src/KubeClient.Extensions.Configuration/ConfigMapConfigurationProvider.cs index fed1015a..79cfb6be 100644 --- a/src/KubeClient.Extensions.Configuration/ConfigMapConfigurationProvider.cs +++ b/src/KubeClient.Extensions.Configuration/ConfigMapConfigurationProvider.cs @@ -7,6 +7,7 @@ namespace KubeClient.Extensions.Configuration { using Models; + using Settings; /// /// Provider for configuration that comes from a Kubernetes ConfigMap. @@ -17,7 +18,7 @@ sealed class ConfigMapConfigurationProvider /// /// The used to communicate with the Kubernetes API. /// - readonly KubeApiClient _client; + readonly IKubeApiClient _client; /// /// The name of the target ConfigMap. @@ -52,39 +53,22 @@ sealed class ConfigMapConfigurationProvider /// /// Create a new . /// - /// - /// The used to communicate with the Kubernetes API. - /// - /// - /// The name of the target ConfigMap. + /// + /// The used to configure the provider. /// - /// - /// The Kubernetes namespace that contains the target ConfigMap. - /// - /// - /// The name of the target configuration section (if any). - /// - /// - /// Watch the ConfigMap for changes? - /// - /// - /// Throw an exception if the ConfigMap was not found? - /// - public ConfigMapConfigurationProvider(KubeApiClient client, string configMapName, string kubeNamespace, string sectionName, bool watch, bool throwOnNotFound) + public ConfigMapConfigurationProvider(ConfigMapConfigurationSettings providerSettings) { - if (client == null) - throw new ArgumentNullException(nameof(client)); + if ( providerSettings == null ) + throw new ArgumentNullException(nameof(providerSettings)); + + _client = providerSettings.Client; + _configMapName = providerSettings.ConfigMapName; + _kubeNamespace = providerSettings.KubeNamespace; + _sectionName = providerSettings.SectionName; + _watch = providerSettings.Watch; + _throwOnNotFound = providerSettings.ThrowOnNotFound; - if (String.IsNullOrWhiteSpace(configMapName)) - throw new ArgumentException("Argument cannot be null, empty, or entirely composed of whitespace: 'configMapName'.", nameof(configMapName)); - - _client = client; Log = _client.LoggerFactory.CreateLogger(); - _configMapName = configMapName; - _kubeNamespace = kubeNamespace; - _sectionName = sectionName; - _watch = watch; - _throwOnNotFound = throwOnNotFound; } /// diff --git a/src/KubeClient.Extensions.Configuration/ConfigMapConfigurationSource.cs b/src/KubeClient.Extensions.Configuration/ConfigMapConfigurationSource.cs index b0084c22..6f664c4a 100644 --- a/src/KubeClient.Extensions.Configuration/ConfigMapConfigurationSource.cs +++ b/src/KubeClient.Extensions.Configuration/ConfigMapConfigurationSource.cs @@ -1,21 +1,35 @@ using Microsoft.Extensions.Configuration; +using System; namespace KubeClient.Extensions.Configuration { + using Settings; + /// /// Source for configuration that comes from a Kubernetes ConfigMap. /// sealed class ConfigMapConfigurationSource : IConfigurationSource { - /// /// Create a new . /// - public ConfigMapConfigurationSource() + /// + /// The used to create configuration providers. + /// + public ConfigMapConfigurationSource(ConfigMapConfigurationSettings settings) { + if ( settings == null ) + throw new ArgumentNullException(nameof(settings)); + + Settings = settings; } + /// + /// The used to create configuration providers. + /// + public ConfigMapConfigurationSettings Settings { get; } + /// /// Build a configuration provider with configured options. /// @@ -25,16 +39,6 @@ public ConfigMapConfigurationSource() /// /// The new . /// - public IConfigurationProvider Build(IConfigurationBuilder configurationBuilder) - { - return new ConfigMapConfigurationProvider( - client: (KubeApiClient)configurationBuilder.Properties[ConfigMapBuilderPropertyConstants.Client], - configMapName: (string)configurationBuilder.Properties[ConfigMapBuilderPropertyConstants.Name], - kubeNamespace: (string)configurationBuilder.Properties[ConfigMapBuilderPropertyConstants.Namespace], - sectionName: (string)configurationBuilder.Properties[ConfigMapBuilderPropertyConstants.SectionName], - watch: (bool)configurationBuilder.Properties[ConfigMapBuilderPropertyConstants.Watch], - throwOnNotFound: (bool) configurationBuilder.Properties[ConfigMapBuilderPropertyConstants.ThrowOnNotFound] - ); - } + public IConfigurationProvider Build(IConfigurationBuilder configurationBuilder) => new ConfigMapConfigurationProvider(Settings); } } diff --git a/src/KubeClient.Extensions.Configuration/KubeClientConfigurationExtensions.cs b/src/KubeClient.Extensions.Configuration/KubeClientConfigurationExtensions.cs index 68e84447..317f777e 100644 --- a/src/KubeClient.Extensions.Configuration/KubeClientConfigurationExtensions.cs +++ b/src/KubeClient.Extensions.Configuration/KubeClientConfigurationExtensions.cs @@ -1,8 +1,10 @@ -using System; using Microsoft.Extensions.Configuration; +using System; namespace KubeClient.Extensions.Configuration { + using Settings; + /// /// extension methods for Kubernetes ConfigMaps and Secrets. /// @@ -29,6 +31,9 @@ public static class KubeClientConfigurationExtensions /// /// Reload the configuration if the ConfigMap changes? /// + /// + /// Throw an exception if the ConfigMap was not found when the configuration is first loaded? + /// /// /// The configured . /// @@ -41,8 +46,7 @@ public static IConfigurationBuilder AddKubeConfigMap(this IConfigurationBuilder KubeApiClient client = KubeApiClient.Create(clientOptions); - return configurationBuilder.AddKubeConfigMap(client, configMapName, kubeNamespace, sectionName, - reloadOnChange, throwOnNotFound); + return configurationBuilder.AddKubeConfigMap(client, configMapName, kubeNamespace, sectionName, reloadOnChange, throwOnNotFound); } /// @@ -67,7 +71,7 @@ public static IConfigurationBuilder AddKubeConfigMap(this IConfigurationBuilder /// Reload the configuration if the ConfigMap changes? /// /// - /// Throw an exception if the ConfigMap was not found. + /// Throw an exception if the ConfigMap was not found when the configuration is first loaded? /// /// /// The configured . @@ -79,16 +83,9 @@ public static IConfigurationBuilder AddKubeConfigMap(this IConfigurationBuilder if (configurationBuilder == null) throw new ArgumentNullException(nameof(configurationBuilder)); - configurationBuilder.Properties[ConfigMapBuilderPropertyConstants.Client] = client; - configurationBuilder.Properties[ConfigMapBuilderPropertyConstants.Name] = configMapName; - configurationBuilder.Properties[ConfigMapBuilderPropertyConstants.Namespace] = kubeNamespace; - configurationBuilder.Properties[ConfigMapBuilderPropertyConstants.SectionName] = sectionName; - configurationBuilder.Properties[ConfigMapBuilderPropertyConstants.Watch] = reloadOnChange; - configurationBuilder.Properties[ConfigMapBuilderPropertyConstants.ThrowOnNotFound] = throwOnNotFound; - - return configurationBuilder.Add( - new ConfigMapConfigurationSource() - ); + return configurationBuilder.Add(new ConfigMapConfigurationSource( + new ConfigMapConfigurationSettings(client, configMapName, kubeNamespace, sectionName, reloadOnChange, throwOnNotFound) + )); } /// @@ -158,15 +155,9 @@ public static IConfigurationBuilder AddKubeSecret(this IConfigurationBuilder con if (configurationBuilder == null) throw new ArgumentNullException(nameof(configurationBuilder)); - configurationBuilder.Properties["KubeClient_Secret_Client"] = client; - configurationBuilder.Properties["KubeClient_Secret_Name"] = secretName; - configurationBuilder.Properties["KubeClient_Secret_Namespace"] = kubeNamespace; - configurationBuilder.Properties["KubeClient_Secret_SectionName"] = sectionName; - configurationBuilder.Properties["KubeClient_Secret_Watch"] = reloadOnChange; - - return configurationBuilder.Add( - new SecretConfigurationSource() - ); + return configurationBuilder.Add(new SecretConfigurationSource( + new SecretConfigurationSettings(client, secretName, kubeNamespace, sectionName, reloadOnChange, throwOnNotFound: false /* not implemented yet */) + )); } } } \ No newline at end of file diff --git a/src/KubeClient.Extensions.Configuration/SecretConfigurationProvider.cs b/src/KubeClient.Extensions.Configuration/SecretConfigurationProvider.cs index c69dcc8a..7d875fb1 100644 --- a/src/KubeClient.Extensions.Configuration/SecretConfigurationProvider.cs +++ b/src/KubeClient.Extensions.Configuration/SecretConfigurationProvider.cs @@ -2,13 +2,13 @@ using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; -using System.Reactive; +using System.Linq; +using System.Text; namespace KubeClient.Extensions.Configuration { - using System.Linq; - using System.Text; using Models; + using Settings; /// /// Provider for configuration that comes from a Kubernetes Secret. @@ -19,7 +19,7 @@ sealed class SecretConfigurationProvider /// /// The used to communicate with the Kubernetes API. /// - readonly KubeApiClient _client; + readonly IKubeApiClient _client; /// /// The name of the target Secret. @@ -49,35 +49,21 @@ sealed class SecretConfigurationProvider /// /// Create a new . /// - /// - /// The used to communicate with the Kubernetes API. - /// - /// - /// The name of the target Secret. + /// + /// The used to configure the provider. /// - /// - /// The Kubernetes namespace that contains the target Secret. - /// - /// - /// The name of the target configuration section (if any). - /// - /// - /// Watch the Secret for changes? - /// - public SecretConfigurationProvider(KubeApiClient client, string secretName, string kubeNamespace, string sectionName, bool watch) + public SecretConfigurationProvider(SecretConfigurationSettings providerSettings) { - if (client == null) - throw new ArgumentNullException(nameof(client)); + if ( providerSettings == null ) + throw new ArgumentNullException(nameof(providerSettings)); + + _client = providerSettings.Client; + _secretName = providerSettings.SecretName; + _kubeNamespace = providerSettings.KubeNamespace; + _sectionName = providerSettings.SectionName; + _watch = providerSettings.Watch; - if (String.IsNullOrWhiteSpace(secretName)) - throw new ArgumentException("Argument cannot be null, empty, or entirely composed of whitespace: 'secretName'.", nameof(secretName)); - - _client = client; Log = _client.LoggerFactory.CreateLogger(); - _secretName = secretName; - _kubeNamespace = kubeNamespace; - _sectionName = sectionName; - _watch = watch; } /// diff --git a/src/KubeClient.Extensions.Configuration/SecretConfigurationSource.cs b/src/KubeClient.Extensions.Configuration/SecretConfigurationSource.cs index 69590d0d..f2c86049 100644 --- a/src/KubeClient.Extensions.Configuration/SecretConfigurationSource.cs +++ b/src/KubeClient.Extensions.Configuration/SecretConfigurationSource.cs @@ -2,6 +2,9 @@ namespace KubeClient.Extensions.Configuration { + using Settings; + using System; + /// /// Source for configuration that comes from a Kubernetes Secret. /// @@ -11,10 +14,22 @@ sealed class SecretConfigurationSource /// /// Create a new . /// - public SecretConfigurationSource() + /// + /// The used to create configuration providers. + /// + public SecretConfigurationSource(SecretConfigurationSettings settings) { + if ( settings == null ) + throw new ArgumentNullException(nameof(settings)); + + Settings = settings; } + /// + /// The used to create configuration providers. + /// + public SecretConfigurationSettings Settings { get; } + /// /// Build a configuration provider with configured options. /// @@ -24,15 +39,6 @@ public SecretConfigurationSource() /// /// The new . /// - public IConfigurationProvider Build(IConfigurationBuilder configurationBuilder) - { - return new SecretConfigurationProvider( - client: (KubeApiClient)configurationBuilder.Properties["KubeClient_Secret_Client"], - secretName: (string)configurationBuilder.Properties["KubeClient_Secret_Name"], - kubeNamespace: (string)configurationBuilder.Properties["KubeClient_Secret_Namespace"], - sectionName: (string)configurationBuilder.Properties["KubeClient_Secret_SectionName"], - watch: (bool)configurationBuilder.Properties["KubeClient_Secret_Watch"] - ); - } + public IConfigurationProvider Build(IConfigurationBuilder configurationBuilder) => new SecretConfigurationProvider(Settings); } } diff --git a/src/KubeClient.Extensions.Configuration/Settings/ConfigMapConfigurationSettings.cs b/src/KubeClient.Extensions.Configuration/Settings/ConfigMapConfigurationSettings.cs new file mode 100644 index 00000000..6b9c9caa --- /dev/null +++ b/src/KubeClient.Extensions.Configuration/Settings/ConfigMapConfigurationSettings.cs @@ -0,0 +1,79 @@ +using System; + +namespace KubeClient.Extensions.Configuration.Settings +{ + /// + /// Settings for a single instance of the . + /// + public class ConfigMapConfigurationSettings + { + /// + /// Create new . + /// + /// + /// The used to communicate with the Kubernetes API. + /// + /// Note: this client will be disposed by the provider. + /// + /// + /// The name of the target ConfigMap. + /// + /// + /// The Kubernetes namespace that contains the target ConfigMap. + /// + /// + /// The name of the target configuration section (if any). + /// + /// + /// Watch the ConfigMap for changes? + /// + /// + /// Throw an exception if the ConfigMap was not found? + /// + public ConfigMapConfigurationSettings(IKubeApiClient client, string configMapName, string kubeNamespace, string sectionName, bool watch, bool throwOnNotFound) + { + if ( client == null ) + throw new ArgumentNullException(nameof(client)); + + if ( String.IsNullOrWhiteSpace(configMapName) ) + throw new ArgumentException("Argument cannot be null, empty, or entirely composed of whitespace: 'configMapName'.", nameof(configMapName)); + + Client = client; + ConfigMapName = configMapName; + KubeNamespace = kubeNamespace; + SectionName = sectionName; + Watch = watch; + ThrowOnNotFound = throwOnNotFound; + } + + /// + /// The used to communicate with the Kubernetes API. + /// + public IKubeApiClient Client { get; } + + /// + /// The name of the target ConfigMap. + /// + public string ConfigMapName { get; } + + /// + /// The Kubernetes namespace that contains the target ConfigMap. + /// + public string KubeNamespace { get; } + + /// + /// The name of the target configuration section (if any). + /// + public string SectionName { get; } + + /// + /// Watch the ConfigMap for changes? + /// + public bool Watch { get; } + + /// + /// Throw an exception if the ConfigMap was not found? + /// + public bool ThrowOnNotFound { get; } + } +} diff --git a/src/KubeClient.Extensions.Configuration/Settings/SecretConfigurationSettings.cs b/src/KubeClient.Extensions.Configuration/Settings/SecretConfigurationSettings.cs new file mode 100644 index 00000000..30fef663 --- /dev/null +++ b/src/KubeClient.Extensions.Configuration/Settings/SecretConfigurationSettings.cs @@ -0,0 +1,79 @@ +using System; + +namespace KubeClient.Extensions.Configuration.Settings +{ + /// + /// Settings for a single instance of the . + /// + public class SecretConfigurationSettings + { + /// + /// Create new . + /// + /// + /// The used to communicate with the Kubernetes API. + /// + /// Note: this client will be disposed by the provider. + /// + /// + /// The name of the target Secret. + /// + /// + /// The Kubernetes namespace that contains the target Secret. + /// + /// + /// The name of the target configuration section (if any). + /// + /// + /// Watch the Secret for changes? + /// + /// + /// Throw an exception if the Secret was not found? + /// + public SecretConfigurationSettings(IKubeApiClient client, string secretName, string kubeNamespace, string sectionName, bool watch, bool throwOnNotFound) + { + if ( client == null ) + throw new ArgumentNullException(nameof(client)); + + if ( String.IsNullOrWhiteSpace(secretName) ) + throw new ArgumentException("Argument cannot be null, empty, or entirely composed of whitespace: 'secretName'.", nameof(secretName)); + + Client = client; + SecretName = secretName; + KubeNamespace = kubeNamespace; + SectionName = sectionName; + Watch = watch; + ThrowOnNotFound = throwOnNotFound; + } + + /// + /// The used to communicate with the Kubernetes API. + /// + public IKubeApiClient Client { get; } + + /// + /// The name of the target Secret. + /// + public string SecretName { get; } + + /// + /// The Kubernetes namespace that contains the target Secret. + /// + public string KubeNamespace { get; } + + /// + /// The name of the target configuration section (if any). + /// + public string SectionName { get; } + + /// + /// Watch the Secret for changes? + /// + public bool Watch { get; } + + /// + /// Throw an exception if the Secret was not found? + /// + public bool ThrowOnNotFound { get; } + } +} From 1e1fc5af532e9d8cda2d9fef7c28a99b5758b770 Mon Sep 17 00:00:00 2001 From: Adam Friedman Date: Sat, 5 Oct 2019 16:48:11 +1000 Subject: [PATCH 2/2] Update ConfigFromConfigMap sample to demonstrate configuration provider semantics (in terms of overriding configuration items) Also, add test to demonstrate general Microsoft.Extensions.Configuration provider semantics (taking K8s out of the picture entirely). tintoy/dotnet-kube-client#101 --- KubeClient.sln | 15 ++ samples/ConfigFromConfigMap/Program.cs | 103 ++++++++++--- ...ient.Extensions.Configuration.Tests.csproj | 37 +++++ .../ProviderSemanticsTests.cs | 137 ++++++++++++++++++ 4 files changed, 268 insertions(+), 24 deletions(-) create mode 100644 test/KubeClient.Extensions.Configuration.Tests/KubeClient.Extensions.Configuration.Tests.csproj create mode 100644 test/KubeClient.Extensions.Configuration.Tests/ProviderSemanticsTests.cs diff --git a/KubeClient.sln b/KubeClient.sln index 5da43663..15b5871a 100644 --- a/KubeClient.sln +++ b/KubeClient.sln @@ -38,6 +38,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeploymentWithRollback", "s EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WatchEvents", "samples\WatchEvents\WatchEvents.csproj", "{EA1B1086-1813-478A-96B8-D54ABAEA77BE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KubeClient.Extensions.Configuration.Tests", "test\KubeClient.Extensions.Configuration.Tests\KubeClient.Extensions.Configuration.Tests.csproj", "{F712BCDC-21FB-4598-A8D3-88746948496E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -232,6 +234,18 @@ Global {EA1B1086-1813-478A-96B8-D54ABAEA77BE}.Release|x64.Build.0 = Release|Any CPU {EA1B1086-1813-478A-96B8-D54ABAEA77BE}.Release|x86.ActiveCfg = Release|Any CPU {EA1B1086-1813-478A-96B8-D54ABAEA77BE}.Release|x86.Build.0 = Release|Any CPU + {F712BCDC-21FB-4598-A8D3-88746948496E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F712BCDC-21FB-4598-A8D3-88746948496E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F712BCDC-21FB-4598-A8D3-88746948496E}.Debug|x64.ActiveCfg = Debug|Any CPU + {F712BCDC-21FB-4598-A8D3-88746948496E}.Debug|x64.Build.0 = Debug|Any CPU + {F712BCDC-21FB-4598-A8D3-88746948496E}.Debug|x86.ActiveCfg = Debug|Any CPU + {F712BCDC-21FB-4598-A8D3-88746948496E}.Debug|x86.Build.0 = Debug|Any CPU + {F712BCDC-21FB-4598-A8D3-88746948496E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F712BCDC-21FB-4598-A8D3-88746948496E}.Release|Any CPU.Build.0 = Release|Any CPU + {F712BCDC-21FB-4598-A8D3-88746948496E}.Release|x64.ActiveCfg = Release|Any CPU + {F712BCDC-21FB-4598-A8D3-88746948496E}.Release|x64.Build.0 = Release|Any CPU + {F712BCDC-21FB-4598-A8D3-88746948496E}.Release|x86.ActiveCfg = Release|Any CPU + {F712BCDC-21FB-4598-A8D3-88746948496E}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -252,6 +266,7 @@ Global {4DCB98B3-4B9D-4A80-A819-60281A1B0739} = {619D7194-2A3C-4C5C-A5A3-EBAD28C1D65F} {2DEC9BCC-AA1C-4A1A-B0EA-FC5930297568} = {619D7194-2A3C-4C5C-A5A3-EBAD28C1D65F} {EA1B1086-1813-478A-96B8-D54ABAEA77BE} = {619D7194-2A3C-4C5C-A5A3-EBAD28C1D65F} + {F712BCDC-21FB-4598-A8D3-88746948496E} = {0F92C90F-A489-4059-AE24-36361B883E81} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {191B89E7-F944-4315-9E98-F6F736D27296} diff --git a/samples/ConfigFromConfigMap/Program.cs b/samples/ConfigFromConfigMap/Program.cs index 99bbbc4e..c18d5935 100644 --- a/samples/ConfigFromConfigMap/Program.cs +++ b/samples/ConfigFromConfigMap/Program.cs @@ -37,7 +37,8 @@ static async Task Main(string[] commandLineArguments) try { - const string configMapName = "config-from-configmap"; + const string configMap1Name = "config-from-configmap-1"; + const string configMap2Name = "config-from-configmap-2"; const string configMapNamespace = "default"; KubeClientOptions clientOptions = K8sConfig.Load().ToKubeClientOptions(defaultKubeNamespace: configMapNamespace, loggerFactory: loggerFactory); @@ -47,35 +48,68 @@ static async Task Main(string[] commandLineArguments) KubeApiClient client = KubeApiClient.Create(clientOptions); - Log.Information("Checking for existing ConfigMap..."); - ConfigMapV1 configMap = await client.ConfigMapsV1().Get(configMapName, configMapNamespace); - if (configMap != null) + Log.Information("Checking for existing ConfigMaps..."); + + ConfigMapV1 configMap1 = await client.ConfigMapsV1().Get(configMap1Name, configMapNamespace); + if (configMap1 != null) + { + Log.Information("Deleting existing ConfigMap {ConfigMapName}...", configMap1Name); + await client.ConfigMapsV1().Delete(configMap1Name); + Log.Information("Deleted existing ConfigMap {ConfigMapName}.", configMap1Name); + } + + ConfigMapV1 configMap2 = await client.ConfigMapsV1().Get(configMap2Name, configMapNamespace); + if ( configMap2 != null ) { - Log.Information("Deleting existing ConfigMap..."); - await client.ConfigMapsV1().Delete(configMapName); - Log.Information("Existing ConfigMap deleted."); + Log.Information("Deleting existing ConfigMap {ConfigMapName}...", configMap2Name); + await client.ConfigMapsV1().Delete(configMap2Name); + Log.Information("Deleted existing ConfigMap {ConfigMapName}.", configMap2Name); } - Log.Information("Creating new ConfigMap..."); - configMap = await client.ConfigMapsV1().Create(new ConfigMapV1 + Log.Information("Creating ConfigMaps..."); + + Log.Information("Creating ConfigMap {ConfigMapName}...", configMap1Name); + configMap1 = await client.ConfigMapsV1().Create(new ConfigMapV1 + { + Metadata = new ObjectMetaV1 + { + Name = configMap1Name, + Namespace = configMapNamespace + }, + Data = + { + ["Key1"] = "OneA", + ["Key2"] = "TwoA", + ["Key3"] = "ThreeA" + } + }); + + Log.Information("Creating ConfigMap {ConfigMapName}...", configMap2Name); + configMap2 = await client.ConfigMapsV1().Create(new ConfigMapV1 { Metadata = new ObjectMetaV1 { - Name = configMapName, + Name = configMap2Name, Namespace = configMapNamespace }, Data = { - ["Key1"] = "One", - ["Key2"] = "Two" + ["Key1"] = "OneB", + ["Key2"] = "TwoB", + ["Key4"] = "FourB" } }); - Log.Information("New ConfigMap created."); + + Log.Information("ConfigMaps created."); Log.Information("Building configuration..."); IConfiguration configuration = new ConfigurationBuilder() .AddKubeConfigMap(clientOptions, - configMapName: "config-from-configmap", + configMapName: configMap1Name, + reloadOnChange: true + ) + .AddKubeConfigMap(clientOptions, + configMapName: configMap2Name, reloadOnChange: true ) .Build(); @@ -84,7 +118,7 @@ static async Task Main(string[] commandLineArguments) Log.Information("Got configuration:"); Dump(configuration); - Log.Information("Press enter to update ConfigMap..."); + Log.Information("Press enter to update ConfigMaps {ConfigMap1Name} and {ConfigMap2Name}:", configMap1Name, configMap2Name); Console.ReadLine(); @@ -105,26 +139,47 @@ static async Task Main(string[] commandLineArguments) using (configurationChanged) using (reloadNotifications) { - Log.Information("Updating ConfigMap..."); + Log.Information("Updating ConfigMap {ConfigMapName}...", configMap1Name); - configMap.Data["One"] = "1"; - configMap.Data["Two"] = "2"; + configMap1.Data["key5"] = "FiveA"; + configMap1.Data["key6"] = "SixA"; // Replace the entire Data dictionary (to modify only some of the data, you'll need to use an untyped JsonPatchDocument). - await client.ConfigMapsV1().Update(configMapName, patch => + await client.ConfigMapsV1().Update(configMap1Name, patch => { patch.Replace(patchConfigMap => patchConfigMap.Data, - value: configMap.Data + value: configMap1.Data ); }); - Log.Information("Updated ConfigMap."); + Log.Information("Updated ConfigMap {ConfigMapName}.", configMap1Name); Log.Information("Waiting for configuration change..."); configurationChanged.WaitOne(); - Log.Information("Configuration changed."); + Log.Information("Configuration changed via ConfigMap {ConfigMapName}.", configMap1Name); + + configurationChanged.Reset(); + + Log.Information("Updating ConfigMap {ConfigMapName}...", configMap2Name); + + configMap2.Data["key5"] = "FiveB"; + configMap2.Data["key6"] = "SixB"; + + // Replace the entire Data dictionary (to modify only some of the data, you'll need to use an untyped JsonPatchDocument). + await client.ConfigMapsV1().Update(configMap2Name, patch => + { + patch.Replace(patchConfigMap => patchConfigMap.Data, + value: configMap2.Data + ); + }); + + Log.Information("Updated ConfigMap {ConfigMapName}.", configMap2Name); + + configurationChanged.WaitOne(); + + Log.Information("Configuration changed via ConfigMap {ConfigMapName}.", configMap2Name); } return ExitCodes.Success; @@ -152,8 +207,8 @@ static void Dump(IConfiguration configuration) if (configuration == null) throw new ArgumentNullException(nameof(configuration)); - foreach (var item in configuration.AsEnumerable()) - Log.Information("\t'{Key}' = '{Value}'", item.Key, item.Value); + foreach ((string key, string value) in configuration.AsEnumerable().OrderBy(item => item.Key)) + Log.Information("\t'{Key}' = '{Value}'", key, value); } /// diff --git a/test/KubeClient.Extensions.Configuration.Tests/KubeClient.Extensions.Configuration.Tests.csproj b/test/KubeClient.Extensions.Configuration.Tests/KubeClient.Extensions.Configuration.Tests.csproj new file mode 100644 index 00000000..02c90804 --- /dev/null +++ b/test/KubeClient.Extensions.Configuration.Tests/KubeClient.Extensions.Configuration.Tests.csproj @@ -0,0 +1,37 @@ + + + + netcoreapp2.0 + latest + + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/KubeClient.Extensions.Configuration.Tests/ProviderSemanticsTests.cs b/test/KubeClient.Extensions.Configuration.Tests/ProviderSemanticsTests.cs new file mode 100644 index 00000000..1c1fac49 --- /dev/null +++ b/test/KubeClient.Extensions.Configuration.Tests/ProviderSemanticsTests.cs @@ -0,0 +1,137 @@ +using Microsoft.Extensions.Configuration; +using System; +using System.Collections.Generic; +using Xunit; + +namespace KubeClient.Extensions.Configuration.Tests +{ + /// + /// Tests to validate the semantics of , , and . + /// + public class ProviderSemanticsTests + { + /// + /// An delegate used to trigger reload of configuration. + /// + static Action TriggerReload; + + /// + /// A second will override values provided by the first source. + /// + [Fact] + public void Can_Override_Configuration() + { + var source1 = new DummyConfigSource + { + ProviderData = + { + ["Key1"] = "Value1", + ["Key2"] = "Value2", + } + }; + + var source2 = new DummyConfigSource + { + ProviderData = + { + ["Key1"] = "Value1a", + ["Key3"] = "Value3", + } + }; + + IConfiguration configuration = new ConfigurationBuilder() + .Add(source1) + .Add(source2) + .Build(); + + Assert.Equal("Value2", configuration["Key2"]); + Assert.Equal("Value3", configuration["Key3"]); + + Assert.Equal("Value1a", configuration["Key1"]); + + source2.ProviderData["Key1"] = "Value1b"; + TriggerReload(); + + Assert.Equal("Value2", configuration["Key2"]); + Assert.Equal("Value3", configuration["Key3"]); + + Assert.Equal("Value1a", configuration["Key1"]); + } + + /// + /// A dummy configuration source that gets its data from a dictionary. + /// + class DummyConfigSource + : IConfigurationSource + { + /// + /// Create a new . + /// + public DummyConfigSource() + { + } + + /// + /// The provider data. + /// + public Dictionary ProviderData { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// Build a configuration provider for the source's configured values. + /// + /// + /// The for which configuration is being built. + /// + /// + /// The configuration provider. + /// + public IConfigurationProvider Build(IConfigurationBuilder builder) => new DummyConfigProvider(ProviderData); + } + + /// + /// A dummy configuration provider that gets its data from a dictionary. + /// + class DummyConfigProvider + : ConfigurationProvider + { + /// + /// Create a new . + /// + /// + /// A containing the provider data. + /// + public DummyConfigProvider(Dictionary providerData) + { + if ( providerData == null ) + throw new ArgumentNullException(nameof(providerData)); + + ProviderData = providerData; + + // Hacky mechanism to register for reload. + Action oldReload = TriggerReload; + TriggerReload = () => + { + if (oldReload != null) + oldReload(); + + OnReload(); + }; + } + + /// + /// A containing the provider data. + /// + public Dictionary ProviderData { get; } + + /// + /// Load the provider data's. + /// + public override void Load() + { + base.Load(); + + Data = new Dictionary(ProviderData, StringComparer.OrdinalIgnoreCase); + } + } + } +}