From 44602e71c243c3c4a987ef3bcf83720cbe652672 Mon Sep 17 00:00:00 2001 From: Bart Koelman <104792814+bart-vmware@users.noreply.github.com> Date: Thu, 12 Sep 2024 17:24:00 +0200 Subject: [PATCH] Placeholder/encryption configuration refactorings (#1354) * Placeholder/encryption configuration refactorings # Placeholder/encryption configuration providers - Extract duplicate code from configuration sources/providers - Fix reloading and change notification when binding to options - Remove special-cased code paths for `ConfigurationManager` (no longer needed) - Add trace-level logging, using source generator - Refactor internal methods to recursively search through configuration sources/providers and update call sites - Wire `Configuration.Encryption` from `AddSteeltoe` in Bootstrap ## Placeholder - Fixes in placeholder substitution when using '.' (Spring-style) key separator with default value ## Encryption - Rename various types and methods containing Encrypt* that actually decrypt. - Remove `IConfiguration` extension methods, lazily create decryptor from loaded configuration - Fix regex used in decryption, tweak its options, and use source generator ## Config Server - Change: Do not implicitly add placeholder anymore; nesting of placeholder/encryption depends on what the user needs - Remove duplicate tests # Env actuator (Management) - Fix host configuration from not being included in the response - Include an empty placeholder/encryption property source in the response when used * Review feedback: rename variables * Update src/Configuration/src/Placeholder/PropertyPlaceHolderHelper.cs Co-authored-by: Tim Hess --------- Co-authored-by: Tim Hess --- .../src/AutoConfiguration/BootstrapScanner.cs | 9 + .../AutoConfiguration/PublicAPI.Unshipped.txt | 1 + ...teeltoe.Bootstrap.AutoConfiguration.csproj | 1 + .../SteeltoeAssemblyNames.cs | 1 + .../HostBuilderExtensionsTest.cs | 34 +- ...oe.Bootstrap.AutoConfiguration.Test.csproj | 2 + .../test/TestResources}/MemoryFileProvider.cs | 4 +- src/Configuration/README.md | 3 +- .../CompositeConfigurationProvider.cs | 146 +++++++ .../Abstractions/ConfigurationExtensions.cs | 67 --- ...figurationProviderEnumerationExtensions.cs | 91 ++++ ...onfigurationSourceEnumerationExtensions.cs | 74 ++++ .../ICompositeConfigurationSource.cs | 18 + .../IPlaceholderResolverProvider.cs | 13 - .../Abstractions/Properties/AssemblyInfo.cs | 6 + .../src/Abstractions/PublicAPI.Unshipped.txt | 5 +- .../ConfigurationBuilderExtensions.cs | 2 +- ...udFoundryConfigurationBuilderExtensions.cs | 2 +- ...figServerConfigurationBuilderExtensions.cs | 4 +- .../ConfigServerConfigurationSource.cs | 13 +- .../ConfigServerHealthContributor.cs | 2 +- .../ConfigServer/ConfigServerHostedService.cs | 4 +- ...Steeltoe.Configuration.ConfigServer.csproj | 1 - .../src/Encryption/ConfigurationSchema.json | 8 +- .../src/Encryption/ConfigurationView.cs | 119 ----- .../AesTextDecryptor.cs | 6 +- .../ConfigServerDecryptionSettings.cs} | 22 +- .../ConfigurationSettingsBinder.cs} | 6 +- .../CryptoKeyStoreSettings.cs} | 6 +- .../{ => Cryptography}/DecryptionException.cs | 2 +- .../IKeyProvider.cs | 2 +- .../{ => Cryptography}/ITextDecryptor.cs | 2 +- .../KeyProvider.cs | 2 +- .../NoneDecryptor.cs} | 6 +- .../RsaCryptoSettings.cs} | 10 +- .../RsaKeyStoreDecryptor.cs | 4 +- .../TextDecryptorFactory.cs} | 14 +- ...ecryptionConfigurationBuilderExtensions.cs | 92 ++++ .../DecryptionConfigurationProvider.cs | 56 +++ .../DecryptionConfigurationSource.cs | 39 ++ .../EncryptionConfigurationExtensions.cs | 205 --------- .../Encryption/EncryptionResolverProvider.cs | 219 ---------- .../Encryption/EncryptionResolverSource.cs | 103 ----- .../EncryptionServiceCollectionExtensions.cs | 146 ------- .../src/Encryption/Properties/AssemblyInfo.cs | 4 +- .../src/Encryption/PublicAPI.Unshipped.txt | 30 +- src/Configuration/src/Encryption/README.md | 4 +- .../Steeltoe.Configuration.Encryption.csproj | 5 +- .../ConfigurationBuilderExtensions.cs | 2 +- .../ConfigurationBuilderExtensions.cs | 87 ---- .../src/Placeholder/ConfigurationView.cs | 119 ----- ...aceholderConfigurationBuilderExtensions.cs | 66 +++ .../PlaceholderConfigurationExtensions.cs | 160 ------- .../PlaceholderConfigurationProvider.cs | 44 ++ .../PlaceholderConfigurationSource.cs | 36 ++ .../PlaceholderResolverProvider.cs | 206 --------- .../Placeholder/PlaceholderResolverSource.cs | 92 ---- .../PlaceholderServiceCollectionExtensions.cs | 62 --- .../Placeholder}/PropertyPlaceHolderHelper.cs | 158 +++---- .../src/Placeholder/PublicAPI.Unshipped.txt | 13 +- ...ndomValueConfigurationBuilderExtensions.cs | 2 +- .../src/RandomValue/RandomValueProvider.cs | 4 +- .../src/RandomValue/RandomValueSource.cs | 6 +- .../Steeltoe.Configuration.RandomValue.csproj | 1 + ...pringBootConfigurationBuilderExtensions.cs | 4 +- .../Steeltoe.Configuration.SpringBoot.csproj | 1 + ...undryConfigurationBuilderExtensionsTest.cs | 15 +- .../CloudFoundryHostBuilderExtensionsTest.cs | 9 +- .../EncryptionBeforePlaceholderStartup.cs | 33 -- .../PlaceholderBeforeEncryptionStartup.cs | 33 -- ...> PlaceholderEncryptionIntegrationTest.cs} | 51 ++- ...ation.ConfigServer.Integration.Test.csproj | 1 + ...rConfigurationBuilderExtensionsCoreTest.cs | 24 +- ...erverConfigurationBuilderExtensionsTest.cs | 258 +---------- .../ConfigServerConfigurationSourceTest.cs | 9 +- .../ConfigServerHealthContributorTest.cs | 2 +- .../ConfigServerHostBuilderExtensionsTest.cs | 30 +- .../ConfigServerHostedServiceTest.cs | 58 +-- ...igServerServiceCollectionExtensionsTest.cs | 21 + ...toe.Configuration.ConfigServer.Test.csproj | 5 +- .../AesTextDecryptorTest.cs | 9 +- .../Cryptography/NoneDecryptorTest.cs | 18 + .../RsaKeyStoreDecryptorTest.cs | 6 +- .../Cryptography/TextDecryptorFactoryTest.cs | 123 ++++++ .../{Decryption => Cryptography}/server.jks | Bin .../Decryption/EncryptionFactoryTest.cs | 123 ------ ...CollectionForConfigServerExtensionsTest.cs | 49 --- .../Decryption/NoopDecryptorTest.cs | 18 - ...ConfigureConfigServerEncryptionResolver.cs | 30 -- .../DecryptionConfigurationTest.cs | 107 +++++ .../EncryptionConfigurationExtensionsTest.cs | 151 ------- .../EncryptionResolverProviderTest.cs | 139 ------ .../EncryptionResolverSourceTest.cs | 43 -- ...cryptionServiceCollectionExtensionsTest.cs | 40 -- .../StartupForConfigureEncryptionResolver.cs | 44 -- ...eltoe.Configuration.Encryption.Test.csproj | 4 +- .../test/Encryption.Test/xunit.runner.json | 4 - .../Placeholder.Test/ConfigureTestOptions.cs | 25 ++ .../PlaceholderConfigurationExtensionsTest.cs | 286 ------------ .../PlaceholderConfigurationTest.cs | 411 ++++++++++++++++++ .../PlaceholderResolverExtensionsTest.cs | 204 --------- .../PlaceholderResolverProviderTest.cs | 349 --------------- .../PlaceholderResolverSourceTest.cs | 40 -- .../PlaceholderWebApplicationTest.cs | 194 +++++++++ .../PropertyPlaceholderHelperTest.cs | 153 ++++--- .../TestConfigurationProvider.cs | 38 ++ .../TestConfigurationSource.cs | 29 ++ .../{TestServerStartup.cs => TestOptions.cs} | 13 +- .../test/Placeholder.Test/xunit.runner.json | 4 - .../RandomValue.Test/RandomValueSourceTest.cs | 9 +- .../src/Connectors/ConnectorConfigurer.cs | 3 +- .../KubernetesMemoryServiceBindingsReader.cs | 1 + .../Environment/EnvironmentEndpointHandler.cs | 20 +- .../Environment/EndpointMiddlewareTest.cs | 107 ++++- .../Steeltoe.Management.Endpoint.Test.csproj | 1 + 115 files changed, 2157 insertions(+), 3865 deletions(-) rename src/{Connectors/test/Connectors.Test => Common/test/TestResources}/MemoryFileProvider.cs (98%) create mode 100644 src/Configuration/src/Abstractions/CompositeConfigurationProvider.cs delete mode 100644 src/Configuration/src/Abstractions/ConfigurationExtensions.cs create mode 100644 src/Configuration/src/Abstractions/ConfigurationProviderEnumerationExtensions.cs create mode 100644 src/Configuration/src/Abstractions/ConfigurationSourceEnumerationExtensions.cs create mode 100644 src/Configuration/src/Abstractions/ICompositeConfigurationSource.cs delete mode 100644 src/Configuration/src/Abstractions/IPlaceholderResolverProvider.cs delete mode 100644 src/Configuration/src/Encryption/ConfigurationView.cs rename src/Configuration/src/Encryption/{Decryption => Cryptography}/AesTextDecryptor.cs (94%) rename src/Configuration/src/Encryption/{Decryption/ConfigServerEncryptionSettings.cs => Cryptography/ConfigServerDecryptionSettings.cs} (54%) rename src/Configuration/src/Encryption/{Decryption/ConfigurationSettingsHelper.cs => Cryptography/ConfigurationSettingsBinder.cs} (78%) rename src/Configuration/src/Encryption/{Decryption/EncryptionKeyStoreSettings.cs => Cryptography/CryptoKeyStoreSettings.cs} (79%) rename src/Configuration/src/Encryption/{ => Cryptography}/DecryptionException.cs (89%) rename src/Configuration/src/Encryption/{Decryption => Cryptography}/IKeyProvider.cs (84%) rename src/Configuration/src/Encryption/{ => Cryptography}/ITextDecryptor.cs (89%) rename src/Configuration/src/Encryption/{Decryption => Cryptography}/KeyProvider.cs (95%) rename src/Configuration/src/Encryption/{Decryption/NoopDecryptor.cs => Cryptography/NoneDecryptor.cs} (65%) rename src/Configuration/src/Encryption/{Decryption/RsaEncryptionSettings.cs => Cryptography/RsaCryptoSettings.cs} (82%) rename src/Configuration/src/Encryption/{Decryption => Cryptography}/RsaKeyStoreDecryptor.cs (95%) rename src/Configuration/src/Encryption/{Decryption/EncryptionFactory.cs => Cryptography/TextDecryptorFactory.cs} (72%) create mode 100644 src/Configuration/src/Encryption/DecryptionConfigurationBuilderExtensions.cs create mode 100644 src/Configuration/src/Encryption/DecryptionConfigurationProvider.cs create mode 100644 src/Configuration/src/Encryption/DecryptionConfigurationSource.cs delete mode 100644 src/Configuration/src/Encryption/EncryptionConfigurationExtensions.cs delete mode 100644 src/Configuration/src/Encryption/EncryptionResolverProvider.cs delete mode 100644 src/Configuration/src/Encryption/EncryptionResolverSource.cs delete mode 100644 src/Configuration/src/Encryption/EncryptionServiceCollectionExtensions.cs delete mode 100644 src/Configuration/src/Placeholder/ConfigurationBuilderExtensions.cs delete mode 100644 src/Configuration/src/Placeholder/ConfigurationView.cs create mode 100644 src/Configuration/src/Placeholder/PlaceholderConfigurationBuilderExtensions.cs delete mode 100644 src/Configuration/src/Placeholder/PlaceholderConfigurationExtensions.cs create mode 100644 src/Configuration/src/Placeholder/PlaceholderConfigurationProvider.cs create mode 100644 src/Configuration/src/Placeholder/PlaceholderConfigurationSource.cs delete mode 100644 src/Configuration/src/Placeholder/PlaceholderResolverProvider.cs delete mode 100644 src/Configuration/src/Placeholder/PlaceholderResolverSource.cs delete mode 100644 src/Configuration/src/Placeholder/PlaceholderServiceCollectionExtensions.cs rename src/{Common/src/Common/Configuration => Configuration/src/Placeholder}/PropertyPlaceHolderHelper.cs (50%) delete mode 100644 src/Configuration/test/ConfigServer.Integration.Test/EncryptionBeforePlaceholderStartup.cs delete mode 100644 src/Configuration/test/ConfigServer.Integration.Test/PlaceholderBeforeEncryptionStartup.cs rename src/Configuration/test/ConfigServer.Integration.Test/{NestedPlaceholderEncryptionIntegrationTest.cs => PlaceholderEncryptionIntegrationTest.cs} (62%) rename src/Configuration/test/Encryption.Test/{Decryption => Cryptography}/AesTextDecryptorTest.cs (85%) create mode 100644 src/Configuration/test/Encryption.Test/Cryptography/NoneDecryptorTest.cs rename src/Configuration/test/Encryption.Test/{Decryption => Cryptography}/RsaKeyStoreDecryptorTest.cs (96%) create mode 100644 src/Configuration/test/Encryption.Test/Cryptography/TextDecryptorFactoryTest.cs rename src/Configuration/test/Encryption.Test/{Decryption => Cryptography}/server.jks (100%) delete mode 100644 src/Configuration/test/Encryption.Test/Decryption/EncryptionFactoryTest.cs delete mode 100644 src/Configuration/test/Encryption.Test/Decryption/EncryptionServiceCollectionForConfigServerExtensionsTest.cs delete mode 100644 src/Configuration/test/Encryption.Test/Decryption/NoopDecryptorTest.cs delete mode 100644 src/Configuration/test/Encryption.Test/Decryption/StartupForConfigureConfigServerEncryptionResolver.cs create mode 100644 src/Configuration/test/Encryption.Test/DecryptionConfigurationTest.cs delete mode 100644 src/Configuration/test/Encryption.Test/EncryptionConfigurationExtensionsTest.cs delete mode 100644 src/Configuration/test/Encryption.Test/EncryptionResolverProviderTest.cs delete mode 100644 src/Configuration/test/Encryption.Test/EncryptionResolverSourceTest.cs delete mode 100644 src/Configuration/test/Encryption.Test/EncryptionServiceCollectionExtensionsTest.cs delete mode 100644 src/Configuration/test/Encryption.Test/StartupForConfigureEncryptionResolver.cs delete mode 100644 src/Configuration/test/Encryption.Test/xunit.runner.json create mode 100644 src/Configuration/test/Placeholder.Test/ConfigureTestOptions.cs delete mode 100644 src/Configuration/test/Placeholder.Test/PlaceholderConfigurationExtensionsTest.cs create mode 100644 src/Configuration/test/Placeholder.Test/PlaceholderConfigurationTest.cs delete mode 100644 src/Configuration/test/Placeholder.Test/PlaceholderResolverExtensionsTest.cs delete mode 100644 src/Configuration/test/Placeholder.Test/PlaceholderResolverProviderTest.cs delete mode 100644 src/Configuration/test/Placeholder.Test/PlaceholderResolverSourceTest.cs create mode 100644 src/Configuration/test/Placeholder.Test/PlaceholderWebApplicationTest.cs rename src/{Common/test/Common.Test/Configuration => Configuration/test/Placeholder.Test}/PropertyPlaceholderHelperTest.cs (73%) create mode 100644 src/Configuration/test/Placeholder.Test/TestConfigurationProvider.cs create mode 100644 src/Configuration/test/Placeholder.Test/TestConfigurationSource.cs rename src/Configuration/test/Placeholder.Test/{TestServerStartup.cs => TestOptions.cs} (50%) delete mode 100644 src/Configuration/test/Placeholder.Test/xunit.runner.json diff --git a/src/Bootstrap/src/AutoConfiguration/BootstrapScanner.cs b/src/Bootstrap/src/AutoConfiguration/BootstrapScanner.cs index 8025fd6629..e695f9fac1 100644 --- a/src/Bootstrap/src/AutoConfiguration/BootstrapScanner.cs +++ b/src/Bootstrap/src/AutoConfiguration/BootstrapScanner.cs @@ -10,6 +10,7 @@ using Steeltoe.Common.Logging; using Steeltoe.Configuration.CloudFoundry; using Steeltoe.Configuration.ConfigServer; +using Steeltoe.Configuration.Encryption; using Steeltoe.Configuration.Placeholder; using Steeltoe.Configuration.RandomValue; using Steeltoe.Connectors.CosmosDb; @@ -71,6 +72,7 @@ public void ConfigureSteeltoe() } WireIfLoaded(WireRandomValueProvider, SteeltoeAssemblyNames.ConfigurationRandomValue); + WireIfLoaded(WireDecryptionProvider, SteeltoeAssemblyNames.ConfigurationEncryption); WireIfLoaded(WirePlaceholderResolver, SteeltoeAssemblyNames.ConfigurationPlaceholder); WireIfLoaded(WireConnectors, SteeltoeAssemblyNames.Connectors); WireIfLoaded(WireDynamicSerilog, SteeltoeAssemblyNames.LoggingDynamicSerilog); @@ -104,6 +106,13 @@ private void WireRandomValueProvider() _logger.LogInformation("Configured random value configuration provider"); } + private void WireDecryptionProvider() + { + _wrapper.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.AddDecryption(_loggerFactory)); + + _logger.LogInformation("Configured decryption configuration provider"); + } + private void WirePlaceholderResolver() { _wrapper.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.AddPlaceholderResolver(_loggerFactory)); diff --git a/src/Bootstrap/src/AutoConfiguration/PublicAPI.Unshipped.txt b/src/Bootstrap/src/AutoConfiguration/PublicAPI.Unshipped.txt index 2b61e7f678..104bc0dcfe 100644 --- a/src/Bootstrap/src/AutoConfiguration/PublicAPI.Unshipped.txt +++ b/src/Bootstrap/src/AutoConfiguration/PublicAPI.Unshipped.txt @@ -1,6 +1,7 @@ #nullable enable const Steeltoe.Bootstrap.AutoConfiguration.SteeltoeAssemblyNames.ConfigurationCloudFoundry = "Steeltoe.Configuration.CloudFoundry" -> string! const Steeltoe.Bootstrap.AutoConfiguration.SteeltoeAssemblyNames.ConfigurationConfigServer = "Steeltoe.Configuration.ConfigServer" -> string! +const Steeltoe.Bootstrap.AutoConfiguration.SteeltoeAssemblyNames.ConfigurationEncryption = "Steeltoe.Configuration.Encryption" -> string! const Steeltoe.Bootstrap.AutoConfiguration.SteeltoeAssemblyNames.ConfigurationPlaceholder = "Steeltoe.Configuration.Placeholder" -> string! const Steeltoe.Bootstrap.AutoConfiguration.SteeltoeAssemblyNames.ConfigurationRandomValue = "Steeltoe.Configuration.RandomValue" -> string! const Steeltoe.Bootstrap.AutoConfiguration.SteeltoeAssemblyNames.Connectors = "Steeltoe.Connectors" -> string! diff --git a/src/Bootstrap/src/AutoConfiguration/Steeltoe.Bootstrap.AutoConfiguration.csproj b/src/Bootstrap/src/AutoConfiguration/Steeltoe.Bootstrap.AutoConfiguration.csproj index 79a4123e03..c5641289b5 100644 --- a/src/Bootstrap/src/AutoConfiguration/Steeltoe.Bootstrap.AutoConfiguration.csproj +++ b/src/Bootstrap/src/AutoConfiguration/Steeltoe.Bootstrap.AutoConfiguration.csproj @@ -16,6 +16,7 @@ + diff --git a/src/Bootstrap/src/AutoConfiguration/SteeltoeAssemblyNames.cs b/src/Bootstrap/src/AutoConfiguration/SteeltoeAssemblyNames.cs index 639d571361..322f112129 100644 --- a/src/Bootstrap/src/AutoConfiguration/SteeltoeAssemblyNames.cs +++ b/src/Bootstrap/src/AutoConfiguration/SteeltoeAssemblyNames.cs @@ -13,6 +13,7 @@ public static class SteeltoeAssemblyNames public const string ConfigurationConfigServer = "Steeltoe.Configuration.ConfigServer"; public const string ConfigurationRandomValue = "Steeltoe.Configuration.RandomValue"; public const string ConfigurationPlaceholder = "Steeltoe.Configuration.Placeholder"; + public const string ConfigurationEncryption = "Steeltoe.Configuration.Encryption"; public const string Connectors = "Steeltoe.Connectors"; public const string DiscoveryConfiguration = "Steeltoe.Discovery.Configuration"; public const string DiscoveryConsul = "Steeltoe.Discovery.Consul"; diff --git a/src/Bootstrap/test/AutoConfiguration.Test/HostBuilderExtensionsTest.cs b/src/Bootstrap/test/AutoConfiguration.Test/HostBuilderExtensionsTest.cs index 814166cf7a..638ff854d7 100644 --- a/src/Bootstrap/test/AutoConfiguration.Test/HostBuilderExtensionsTest.cs +++ b/src/Bootstrap/test/AutoConfiguration.Test/HostBuilderExtensionsTest.cs @@ -27,6 +27,7 @@ using Steeltoe.Configuration.CloudFoundry; using Steeltoe.Configuration.CloudFoundry.ServiceBinding; using Steeltoe.Configuration.ConfigServer; +using Steeltoe.Configuration.Encryption; using Steeltoe.Configuration.Kubernetes.ServiceBinding; using Steeltoe.Configuration.Placeholder; using Steeltoe.Configuration.RandomValue; @@ -85,6 +86,17 @@ public async Task RandomValueConfiguration_IsAutowired(HostBuilderType hostBuild AssertRandomValueConfigurationIsAutowired(hostWrapper); } + [Theory] + [InlineData(HostBuilderType.Host)] + [InlineData(HostBuilderType.WebHost)] + [InlineData(HostBuilderType.WebApplication)] + public async Task EncryptionConfiguration_IsAutowired(HostBuilderType hostBuilderType) + { + await using HostWrapper hostWrapper = HostWrapperFactory.GetForOnly(SteeltoeAssemblyNames.ConfigurationEncryption, hostBuilderType); + + AssertEncryptionConfigurationIsAutowired(hostWrapper); + } + [Theory] [InlineData(HostBuilderType.Host)] [InlineData(HostBuilderType.WebHost)] @@ -220,6 +232,7 @@ public async Task Everything_IsAutowired(HostBuilderType hostBuilderType) AssertConfigServerConfigurationIsAutowired(hostWrapper); AssertCloudFoundryConfigurationIsAutowired(hostWrapper); AssertRandomValueConfigurationIsAutowired(hostWrapper); + AssertEncryptionConfigurationIsAutowired(hostWrapper); AssertPlaceholderResolverIsAutowired(hostWrapper); AssertConnectorsAreAutowired(hostWrapper); AssertDynamicSerilogIsAutowired(hostWrapper); @@ -238,37 +251,44 @@ private static void AssertConfigServerConfigurationIsAutowired(HostWrapper hostW { var configuration = hostWrapper.Services.GetRequiredService(); - configuration.FindConfigurationProvider().Should().NotBeNull(); - configuration.FindConfigurationProvider().Should().NotBeNull(); + configuration.EnumerateProviders().Should().HaveCount(1); + configuration.EnumerateProviders().Should().HaveCount(1); } private static void AssertCloudFoundryConfigurationIsAutowired(HostWrapper hostWrapper) { var configuration = hostWrapper.Services.GetRequiredService(); - configuration.FindConfigurationProvider().Should().NotBeNull(); + configuration.EnumerateProviders().Should().HaveCount(1); } private static void AssertRandomValueConfigurationIsAutowired(HostWrapper hostWrapper) { var configuration = hostWrapper.Services.GetRequiredService(); - configuration.FindConfigurationProvider().Should().NotBeNull(); + configuration.EnumerateProviders().Should().HaveCount(1); + } + + private static void AssertEncryptionConfigurationIsAutowired(HostWrapper hostWrapper) + { + var configurationRoot = (IConfigurationRoot)hostWrapper.Services.GetRequiredService(); + + configurationRoot.EnumerateProviders().Should().HaveCount(1); } private static void AssertPlaceholderResolverIsAutowired(HostWrapper hostWrapper) { var configurationRoot = (IConfigurationRoot)hostWrapper.Services.GetRequiredService(); - configurationRoot.Providers.OfType().Should().HaveCount(1); + configurationRoot.EnumerateProviders().Should().HaveCount(1); } private static void AssertConnectorsAreAutowired(HostWrapper hostWrapper) { var configuration = hostWrapper.Services.GetRequiredService(); - configuration.FindConfigurationProvider().Should().NotBeNull(); - configuration.FindConfigurationProvider().Should().NotBeNull(); + configuration.EnumerateProviders().Should().NotBeEmpty(); + configuration.EnumerateProviders().Should().HaveCount(1); hostWrapper.Services.GetService>().Should().NotBeNull(); hostWrapper.Services.GetService>().Should().NotBeNull(); diff --git a/src/Bootstrap/test/AutoConfiguration.Test/Steeltoe.Bootstrap.AutoConfiguration.Test.csproj b/src/Bootstrap/test/AutoConfiguration.Test/Steeltoe.Bootstrap.AutoConfiguration.Test.csproj index 5e7dea4d6e..d1ffa3ea9b 100644 --- a/src/Bootstrap/test/AutoConfiguration.Test/Steeltoe.Bootstrap.AutoConfiguration.Test.csproj +++ b/src/Bootstrap/test/AutoConfiguration.Test/Steeltoe.Bootstrap.AutoConfiguration.Test.csproj @@ -17,6 +17,8 @@ + + diff --git a/src/Connectors/test/Connectors.Test/MemoryFileProvider.cs b/src/Common/test/TestResources/MemoryFileProvider.cs similarity index 98% rename from src/Connectors/test/Connectors.Test/MemoryFileProvider.cs rename to src/Common/test/TestResources/MemoryFileProvider.cs index 901b0d86ff..8f9d4cdd2b 100644 --- a/src/Connectors/test/Connectors.Test/MemoryFileProvider.cs +++ b/src/Common/test/TestResources/MemoryFileProvider.cs @@ -8,9 +8,9 @@ using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Primitives; -namespace Steeltoe.Connectors.Test; +namespace Steeltoe.Common.TestResources; -internal sealed class MemoryFileProvider : IFileProvider +public sealed class MemoryFileProvider : IFileProvider { private static readonly char[] DirectorySeparators = [ diff --git a/src/Configuration/README.md b/src/Configuration/README.md index 584480be8c..e9d993f03e 100644 --- a/src/Configuration/README.md +++ b/src/Configuration/README.md @@ -7,6 +7,7 @@ Steeltoe configuration providers can: - Interact with [Spring Cloud Config](https://spring.io/projects/spring-cloud-config) - Read [Cloud Foundry environment variables](https://docs.cloudfoundry.org/devguide/deploy-apps/environment-variable.html) - Replace property placeholders +- Decrypt encrypted values - Provide random values For more information on how to use these components see the [Steeltoe documentation](https://steeltoe.io/). @@ -19,7 +20,7 @@ See the `Configuration` directory inside the [Samples](https://github.com/Steelt ### Unstructured data files -Unlike the Java version of the configuration server client, the Steeltoe client currently only supports property and yaml files; not plain text. +Unlike the Java version of the Config Server client, the Steeltoe client currently only supports property and yaml files; not plain text. ### Server initiated reload diff --git a/src/Configuration/src/Abstractions/CompositeConfigurationProvider.cs b/src/Configuration/src/Abstractions/CompositeConfigurationProvider.cs new file mode 100644 index 0000000000..b9523cf738 --- /dev/null +++ b/src/Configuration/src/Abstractions/CompositeConfigurationProvider.cs @@ -0,0 +1,146 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; + +namespace Steeltoe.Configuration; + +internal abstract partial class CompositeConfigurationProvider : IConfigurationProvider, IDisposable +{ + private readonly IList _providers; + private readonly ILogger _logger; + private bool _isDisposed; + + protected internal IConfigurationRoot? ConfigurationRoot { get; private set; } + + protected CompositeConfigurationProvider(IList providers, ILoggerFactory loggerFactory) + { + ArgumentNullException.ThrowIfNull(providers); + ArgumentNullException.ThrowIfNull(loggerFactory); + + _providers = providers; + _logger = loggerFactory.CreateLogger(); + } + + public IChangeToken GetReloadToken() + { + LogGetReloadToken(GetType().Name); + return ConfigurationRoot?.GetReloadToken()!; + } + + public void Load() + { + Load(ConfigurationRoot != null); + } + + private void Load(bool isReload) + { + LogLoad(GetType().Name, isReload); + + if (isReload) + { + ConfigurationRoot!.Reload(); + } + else + { + LogCreateConfigurationRoot(GetType().Name, _providers.Count); + ConfigurationRoot = new ConfigurationRoot(_providers); + } + } + + public IEnumerable GetChildKeys(IEnumerable earlierKeys, string? parentPath) + { + string[] earlierKeysArray = earlierKeys as string[] ?? earlierKeys.ToArray(); +#pragma warning disable S3236 // Caller information arguments should not be provided explicitly + ArgumentNullException.ThrowIfNull(earlierKeysArray, nameof(earlierKeys)); +#pragma warning restore S3236 // Caller information arguments should not be provided explicitly + + LogGetChildKeys(GetType().Name, earlierKeysArray, parentPath); + + IConfiguration? section = parentPath == null ? ConfigurationRoot : ConfigurationRoot?.GetSection(parentPath); + + if (section == null) + { + return earlierKeysArray; + } + + List keys = []; + keys.AddRange(section.GetChildren().Select(child => child.Key)); + keys.AddRange(earlierKeysArray); + keys.Sort(ConfigurationKeyComparer.Instance); + return keys; + } + + public virtual bool TryGet(string key, out string? value) + { + ArgumentNullException.ThrowIfNull(key); + + LogTryGet(GetType().Name, key); + + value = ConfigurationRoot?.GetValue(key); + bool found = value != null; + + if (found) + { + LogTryGetFound(GetType().Name, key, value!); + } + + return found; + } + + public void Set(string key, string? value) + { + ArgumentNullException.ThrowIfNull(key); + + LogSet(GetType().Name, key, value); + + if (ConfigurationRoot != null) + { + ConfigurationRoot[key] = value; + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing && !_isDisposed && ConfigurationRoot is IDisposable disposable) + { + _isDisposed = true; + + LogDispose(GetType().Name); + disposable.Dispose(); + } + } + + [LoggerMessage(Level = LogLevel.Trace, Message = "GetReloadToken from {Type}.")] + private partial void LogGetReloadToken(string type); + + [LoggerMessage(Level = LogLevel.Trace, Message = "Load from {Type} with isReload {IsReload}.")] + private partial void LogLoad(string type, bool isReload); + + [LoggerMessage(Level = LogLevel.Trace, Message = "CreateConfigurationRoot from {Type} with {ProviderCount} providers.")] + private partial void LogCreateConfigurationRoot(string type, int providerCount); + + [LoggerMessage(Level = LogLevel.Trace, Message = "GetChildKeys from {Type} with earlierKeys [{EarlierKeys}] and parentPath '{ParentPath}'.")] + private partial void LogGetChildKeys(string type, string[] earlierKeys, string? parentPath); + + [LoggerMessage(Level = LogLevel.Trace, Message = "TryGet from {Type} with key '{Key}'.")] + private partial void LogTryGet(string type, string key); + + [LoggerMessage(Level = LogLevel.Trace, Message = "TryGet from {Type} with key '{Key}' found value '{Value}'.")] + private partial void LogTryGetFound(string type, string key, string value); + + [LoggerMessage(Level = LogLevel.Trace, Message = "Set from {Type} with key '{Key}' and value '{Value}'.")] + private partial void LogSet(string type, string key, string? value); + + [LoggerMessage(Level = LogLevel.Trace, Message = "Dispose from {Type}.")] + private partial void LogDispose(string type); +} diff --git a/src/Configuration/src/Abstractions/ConfigurationExtensions.cs b/src/Configuration/src/Abstractions/ConfigurationExtensions.cs deleted file mode 100644 index 10510c936a..0000000000 --- a/src/Configuration/src/Abstractions/ConfigurationExtensions.cs +++ /dev/null @@ -1,67 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information. - -using Microsoft.Extensions.Configuration; - -namespace Steeltoe.Configuration; - -internal static class ConfigurationExtensions -{ - /// - /// Finds the first configuration provider of the specified type, scanning through any and - /// composite providers. - /// - /// - /// The type of to find. - /// - /// - /// The configuration to search in. - /// - public static TProvider? FindConfigurationProvider(this IConfiguration configuration) - where TProvider : class, IConfigurationProvider - { - ArgumentNullException.ThrowIfNull(configuration); - - if (configuration is IConfigurationRoot root) - { - return FindConfigurationProvider(root.Providers); - } - - return null; - } - - private static TProvider? FindConfigurationProvider(IEnumerable providers) - where TProvider : class, IConfigurationProvider - { - foreach (IConfigurationProvider provider in providers) - { - if (provider is TProvider matchingProvider) - { - return matchingProvider; - } - - if (provider is IPlaceholderResolverProvider placeholder) - { - var nextProvider = FindConfigurationProvider(placeholder.Providers); - - if (nextProvider != null) - { - return nextProvider; - } - } - - if (provider is ChainedConfigurationProvider chained) - { - var nextProvider = FindConfigurationProvider(chained.Configuration); - - if (nextProvider != null) - { - return nextProvider; - } - } - } - - return null; - } -} diff --git a/src/Configuration/src/Abstractions/ConfigurationProviderEnumerationExtensions.cs b/src/Configuration/src/Abstractions/ConfigurationProviderEnumerationExtensions.cs new file mode 100644 index 0000000000..67c74bc86e --- /dev/null +++ b/src/Configuration/src/Abstractions/ConfigurationProviderEnumerationExtensions.cs @@ -0,0 +1,91 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. + +using Microsoft.Extensions.Configuration; + +namespace Steeltoe.Configuration; + +internal static class ConfigurationProviderEnumerationExtensions +{ + private static readonly Predicate MatchAlways = _ => true; + + /// + /// Enumerates all configuration providers, scanning through any and + /// providers. + /// + /// + /// The configuration to search in. + /// + public static IEnumerable EnumerateProviders(this IConfiguration configuration) + { + return EnumerateProviders(configuration, MatchAlways); + } + + /// + /// Enumerates all configuration providers of the specified type, scanning through any and + /// providers. + /// + /// + /// The type of configuration provider to enumerate. + /// + /// + /// The configuration to search in. + /// + public static IEnumerable EnumerateProviders(this IConfiguration configuration) + where TProvider : IConfigurationProvider + { + return EnumerateProviders(configuration, MatchAlways).OfType(); + } + + /// + /// Enumerates all configuration providers that match the specified predicate, scanning through any and + /// providers. + /// + /// + /// The configuration to search in. + /// + /// + /// The provider filter condition. + /// + public static IEnumerable EnumerateProviders(this IConfiguration configuration, Predicate predicate) + { + ArgumentNullException.ThrowIfNull(configuration); + ArgumentNullException.ThrowIfNull(predicate); + + if (configuration is IConfigurationRoot root) + { + return FilterProviders(root.Providers, predicate); + } + + return Array.Empty(); + } + + private static IEnumerable FilterProviders(IEnumerable providers, + Predicate predicate) + { + foreach (IConfigurationProvider provider in providers) + { + if (predicate(provider)) + { + yield return provider; + } + + if (provider is ChainedConfigurationProvider chainedConfigurationProvider) + { + foreach (IConfigurationProvider match in EnumerateProviders(chainedConfigurationProvider.Configuration, predicate)) + { + yield return match; + } + } + + if (provider is CompositeConfigurationProvider { ConfigurationRoot: not null } compositeConfigurationProvider) + { + foreach (IConfigurationProvider match in EnumerateProviders(compositeConfigurationProvider.ConfigurationRoot, predicate)) + { + yield return match; + } + } + } + } +} diff --git a/src/Configuration/src/Abstractions/ConfigurationSourceEnumerationExtensions.cs b/src/Configuration/src/Abstractions/ConfigurationSourceEnumerationExtensions.cs new file mode 100644 index 0000000000..f527cef4d9 --- /dev/null +++ b/src/Configuration/src/Abstractions/ConfigurationSourceEnumerationExtensions.cs @@ -0,0 +1,74 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. + +using Microsoft.Extensions.Configuration; + +namespace Steeltoe.Configuration; + +internal static class ConfigurationSourceEnumerationExtensions +{ + private static readonly Predicate MatchAlways = _ => true; + + /// + /// Enumerates all configuration sources, scanning through any sources. + /// + /// + /// The configuration builder to search in. + /// + public static IEnumerable EnumerateSources(this IConfigurationBuilder builder) + { + return EnumerateSources(builder, MatchAlways); + } + + /// + /// Enumerates all configuration sources of the specified type, scanning through any sources. + /// + /// + /// The type of configuration source to enumerate. + /// + /// + /// The configuration builder to search in. + /// + public static IEnumerable EnumerateSources(this IConfigurationBuilder builder) + where TSource : IConfigurationSource + { + return EnumerateSources(builder, MatchAlways).OfType(); + } + + /// + /// Enumerates all configuration sources that match the specified predicate, scanning through any sources. + /// + /// + /// The configuration builder to search in. + /// + /// + /// The source filter condition. + /// + public static IEnumerable EnumerateSources(this IConfigurationBuilder builder, Predicate predicate) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(predicate); + + return FilterSources(builder.Sources, predicate); + } + + private static IEnumerable FilterSources(IEnumerable sources, Predicate predicate) + { + foreach (IConfigurationSource source in sources) + { + if (predicate(source)) + { + yield return source; + } + + if (source is ICompositeConfigurationSource compositeConfigurationSource) + { + foreach (IConfigurationSource match in FilterSources(compositeConfigurationSource.Sources, predicate)) + { + yield return match; + } + } + } + } +} diff --git a/src/Configuration/src/Abstractions/ICompositeConfigurationSource.cs b/src/Configuration/src/Abstractions/ICompositeConfigurationSource.cs new file mode 100644 index 0000000000..a592b2e445 --- /dev/null +++ b/src/Configuration/src/Abstractions/ICompositeConfigurationSource.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. + +using Microsoft.Extensions.Configuration; + +namespace Steeltoe.Configuration; + +/// +/// Represents a configuration source that is composed of multiple other configuration sources. +/// +public interface ICompositeConfigurationSource : IConfigurationSource +{ + /// + /// Gets the sources that are owned by this composite source. + /// + IList Sources { get; } +} diff --git a/src/Configuration/src/Abstractions/IPlaceholderResolverProvider.cs b/src/Configuration/src/Abstractions/IPlaceholderResolverProvider.cs deleted file mode 100644 index 7bb8d94eb7..0000000000 --- a/src/Configuration/src/Abstractions/IPlaceholderResolverProvider.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information. - -using Microsoft.Extensions.Configuration; - -namespace Steeltoe.Configuration; - -public interface IPlaceholderResolverProvider : IConfigurationProvider -{ - IList Providers { get; } - IList ResolvedKeys { get; } -} diff --git a/src/Configuration/src/Abstractions/Properties/AssemblyInfo.cs b/src/Configuration/src/Abstractions/Properties/AssemblyInfo.cs index 2ba44b9ece..bfce931fbe 100644 --- a/src/Configuration/src/Abstractions/Properties/AssemblyInfo.cs +++ b/src/Configuration/src/Abstractions/Properties/AssemblyInfo.cs @@ -5,11 +5,17 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Steeltoe.Bootstrap.AutoConfiguration.Test")] +[assembly: InternalsVisibleTo("Steeltoe.Configuration.CloudFoundry")] +[assembly: InternalsVisibleTo("Steeltoe.Configuration.CloudFoundry.Test")] [assembly: InternalsVisibleTo("Steeltoe.Configuration.CloudFoundry.ServiceBinding")] [assembly: InternalsVisibleTo("Steeltoe.Configuration.CloudFoundry.ServiceBinding.Test")] [assembly: InternalsVisibleTo("Steeltoe.Configuration.ConfigServer")] [assembly: InternalsVisibleTo("Steeltoe.Configuration.ConfigServer.Test")] +[assembly: InternalsVisibleTo("Steeltoe.Configuration.Encryption")] [assembly: InternalsVisibleTo("Steeltoe.Configuration.Kubernetes.ServiceBinding")] [assembly: InternalsVisibleTo("Steeltoe.Configuration.Kubernetes.ServiceBinding.Test")] +[assembly: InternalsVisibleTo("Steeltoe.Configuration.Placeholder")] +[assembly: InternalsVisibleTo("Steeltoe.Configuration.RandomValue")] +[assembly: InternalsVisibleTo("Steeltoe.Configuration.SpringBoot")] [assembly: InternalsVisibleTo("Steeltoe.Connectors")] [assembly: InternalsVisibleTo("Steeltoe.Management.Endpoint")] diff --git a/src/Configuration/src/Abstractions/PublicAPI.Unshipped.txt b/src/Configuration/src/Abstractions/PublicAPI.Unshipped.txt index c425ecad5f..82d73f49ab 100644 --- a/src/Configuration/src/Abstractions/PublicAPI.Unshipped.txt +++ b/src/Configuration/src/Abstractions/PublicAPI.Unshipped.txt @@ -1,4 +1,3 @@ #nullable enable -Steeltoe.Configuration.IPlaceholderResolverProvider -Steeltoe.Configuration.IPlaceholderResolverProvider.Providers.get -> System.Collections.Generic.IList! -Steeltoe.Configuration.IPlaceholderResolverProvider.ResolvedKeys.get -> System.Collections.Generic.IList! +Steeltoe.Configuration.ICompositeConfigurationSource +Steeltoe.Configuration.ICompositeConfigurationSource.Sources.get -> System.Collections.Generic.IList! diff --git a/src/Configuration/src/CloudFoundry.ServiceBinding/ConfigurationBuilderExtensions.cs b/src/Configuration/src/CloudFoundry.ServiceBinding/ConfigurationBuilderExtensions.cs index 5c615382ea..2d35df937b 100644 --- a/src/Configuration/src/CloudFoundry.ServiceBinding/ConfigurationBuilderExtensions.cs +++ b/src/Configuration/src/CloudFoundry.ServiceBinding/ConfigurationBuilderExtensions.cs @@ -75,7 +75,7 @@ public static IConfigurationBuilder AddCloudFoundryServiceBindings(this IConfigu ArgumentNullException.ThrowIfNull(serviceBindingsReader); ArgumentNullException.ThrowIfNull(loggerFactory); - if (!builder.Sources.OfType().Any()) + if (!builder.EnumerateSources().Any()) { var source = new CloudFoundryServiceBindingConfigurationSource(serviceBindingsReader) { diff --git a/src/Configuration/src/CloudFoundry/CloudFoundryConfigurationBuilderExtensions.cs b/src/Configuration/src/CloudFoundry/CloudFoundryConfigurationBuilderExtensions.cs index 2220f34c9d..30c9f0a28d 100644 --- a/src/Configuration/src/CloudFoundry/CloudFoundryConfigurationBuilderExtensions.cs +++ b/src/Configuration/src/CloudFoundry/CloudFoundryConfigurationBuilderExtensions.cs @@ -62,7 +62,7 @@ public static IConfigurationBuilder AddCloudFoundry(this IConfigurationBuilder b ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(loggerFactory); - if (!builder.Sources.OfType().Any()) + if (!builder.EnumerateSources().Any()) { var source = new CloudFoundryConfigurationSource(settingsReader); builder.Add(source); diff --git a/src/Configuration/src/ConfigServer/ConfigServerConfigurationBuilderExtensions.cs b/src/Configuration/src/ConfigServer/ConfigServerConfigurationBuilderExtensions.cs index 75cbb1ef33..0a4c340669 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerConfigurationBuilderExtensions.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerConfigurationBuilderExtensions.cs @@ -7,7 +7,6 @@ using Microsoft.Extensions.Logging.Abstractions; using Steeltoe.Configuration.CloudFoundry; using Steeltoe.Configuration.Kubernetes.ServiceBinding; -using Steeltoe.Configuration.Placeholder; namespace Steeltoe.Configuration.ConfigServer; @@ -70,11 +69,10 @@ public static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder b ArgumentNullException.ThrowIfNull(options); ArgumentNullException.ThrowIfNull(loggerFactory); - if (!builder.Sources.OfType().Any()) + if (!builder.EnumerateSources().Any()) { builder.AddCloudFoundry(); builder.AddKubernetesServiceBindings(); - builder.AddPlaceholderResolver(loggerFactory); ConfigServerConfigurationSource source = builder is IConfiguration configuration ? new ConfigServerConfigurationSource(options, configuration, loggerFactory) diff --git a/src/Configuration/src/ConfigServer/ConfigServerConfigurationSource.cs b/src/Configuration/src/ConfigServer/ConfigServerConfigurationSource.cs index b7332879f2..02d2d15f0a 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerConfigurationSource.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerConfigurationSource.cs @@ -11,6 +11,8 @@ namespace Steeltoe.Configuration.ConfigServer; internal sealed class ConfigServerConfigurationSource : IConfigurationSource { + private readonly ILoggerFactory _loggerFactory; + internal List Sources { get; } = []; internal Dictionary Properties { get; } = []; @@ -25,11 +27,6 @@ internal sealed class ConfigServerConfigurationSource : IConfigurationSource /// internal IConfiguration? Configuration { get; private set; } - /// - /// Gets the logger factory used by the Config Server client. - /// - internal ILoggerFactory LoggerFactory { get; } - /// /// Initializes a new instance of the class. /// @@ -50,7 +47,7 @@ public ConfigServerConfigurationSource(ConfigServerClientOptions defaultOptions, Configuration = configuration; DefaultOptions = defaultOptions; - LoggerFactory = loggerFactory; + _loggerFactory = loggerFactory; } /// @@ -84,7 +81,7 @@ public ConfigServerConfigurationSource(ConfigServerClientOptions defaultOptions, } DefaultOptions = defaultOptions; - LoggerFactory = loggerFactory; + _loggerFactory = loggerFactory; } /// @@ -129,6 +126,6 @@ public IConfigurationProvider Build(IConfigurationBuilder builder) DefaultOptions.ClientCertificate.Certificate = options.Certificate; } - return new ConfigServerConfigurationProvider(this, LoggerFactory); + return new ConfigServerConfigurationProvider(this, _loggerFactory); } } diff --git a/src/Configuration/src/ConfigServer/ConfigServerHealthContributor.cs b/src/Configuration/src/ConfigServer/ConfigServerHealthContributor.cs index 75a00bef13..e2d6eb22ad 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerHealthContributor.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerHealthContributor.cs @@ -23,7 +23,7 @@ public ConfigServerHealthContributor(IConfiguration configuration, ILogger(); + Provider = configuration.EnumerateProviders().FirstOrDefault(); if (Provider == null) { diff --git a/src/Configuration/src/ConfigServer/ConfigServerHostedService.cs b/src/Configuration/src/ConfigServer/ConfigServerHostedService.cs index a81ef6ff48..c444b52c53 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerHostedService.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerHostedService.cs @@ -30,8 +30,8 @@ public ConfigServerHostedService(IConfigurationRoot configuration, IEnumerable() ?? - throw new ArgumentException("ConfigServerConfigurationProvider was not found in configuration.", nameof(configuration)); + _configurationProvider = configuration.EnumerateProviders().FirstOrDefault() ?? + throw new InvalidOperationException("ConfigServerConfigurationProvider was not found in configuration."); _discoveryClients = discoveryClientArray; } diff --git a/src/Configuration/src/ConfigServer/Steeltoe.Configuration.ConfigServer.csproj b/src/Configuration/src/ConfigServer/Steeltoe.Configuration.ConfigServer.csproj index 6be853cf61..c71705815b 100644 --- a/src/Configuration/src/ConfigServer/Steeltoe.Configuration.ConfigServer.csproj +++ b/src/Configuration/src/ConfigServer/Steeltoe.Configuration.ConfigServer.csproj @@ -16,7 +16,6 @@ - diff --git a/src/Configuration/src/Encryption/ConfigurationSchema.json b/src/Configuration/src/Encryption/ConfigurationSchema.json index 633cfb7cf6..2979cc4334 100644 --- a/src/Configuration/src/Encryption/ConfigurationSchema.json +++ b/src/Configuration/src/Encryption/ConfigurationSchema.json @@ -21,11 +21,11 @@ "properties": { "Enabled": { "type": "boolean", - "description": "Gets or sets a value indicating whether encryption is enabled. Default value: false." + "description": "Gets or sets a value indicating whether decryption is enabled. Default value: false." }, "Key": { "type": "string", - "description": "Gets or sets the key of the simple encryption." + "description": "Gets or sets the cryptographic key." }, "KeyStore": { "type": "object", @@ -61,10 +61,10 @@ "description": "Gets or sets a value indicating whether strong encryption is enabled. Default value: false." } }, - "description": "Gets the settings related to RSA encryption." + "description": "Gets the settings related to RSA cryptography." } }, - "description": "Holds settings used to configure encryption for the Spring Cloud Config Server provider." + "description": "Holds settings used to configure decryption for the Spring Cloud Config Server provider." } } } diff --git a/src/Configuration/src/Encryption/ConfigurationView.cs b/src/Configuration/src/Encryption/ConfigurationView.cs deleted file mode 100644 index 0ef6fa2ecd..0000000000 --- a/src/Configuration/src/Encryption/ConfigurationView.cs +++ /dev/null @@ -1,119 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information. - -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Primitives; - -namespace Steeltoe.Configuration.Encryption; - -/// -/// Specialized implementation of that does not call on providers. -/// -internal sealed class ConfigurationView : IConfigurationRoot -{ - private readonly IList _providers; - private readonly ConfigurationReloadToken _changeToken = new(); - - /// - /// Gets the s for this configuration. - /// - public IEnumerable Providers => _providers; - - /// - /// Gets or sets the value corresponding to a configuration key. - /// - /// - /// The configuration key. - /// - /// - /// The configuration value. - /// - public string? this[string key] - { - get => GetConfiguration(_providers, key); - set => SetConfiguration(_providers, key, value); - } - - /// - /// Initializes a new instance of the class from a list of providers. - /// - /// - /// The s for this configuration. - /// - public ConfigurationView(IList providers) - { - ArgumentNullException.ThrowIfNull(providers); - - _providers = providers; - } - - /// - /// Gets the immediate child subsections. - /// - /// - /// The children. - /// - public IEnumerable GetChildren() - { - return _providers.Aggregate(Enumerable.Empty(), (seed, source) => source.GetChildKeys(seed, null)).Distinct(StringComparer.OrdinalIgnoreCase) - .Select(GetSection); - } - - /// - /// Returns a that can be used to observe when this configuration is reloaded. - /// - /// - /// The . - /// - public IChangeToken GetReloadToken() - { - return _changeToken; - } - - /// - /// Gets a configuration subsection with the specified key. - /// - /// - /// This method will never return null. If no matching subsection is found with the specified key, an empty - /// will be returned. - /// - /// - /// The key of the configuration section. - /// - /// - /// The . - /// - public IConfigurationSection GetSection(string key) - { - return new ConfigurationSection(this, key); - } - - public void Reload() - { - // Intentionally left empty - this provider is readonly. - } - - private static string? GetConfiguration(IList providers, string key) - { - for (int index = providers.Count - 1; index >= 0; index--) - { - IConfigurationProvider provider = providers[index]; - - if (provider.TryGet(key, out string? value)) - { - return value; - } - } - - return null; - } - - private static void SetConfiguration(IList providers, string key, string? value) - { - foreach (IConfigurationProvider provider in providers) - { - provider.Set(key, value); - } - } -} diff --git a/src/Configuration/src/Encryption/Decryption/AesTextDecryptor.cs b/src/Configuration/src/Encryption/Cryptography/AesTextDecryptor.cs similarity index 94% rename from src/Configuration/src/Encryption/Decryption/AesTextDecryptor.cs rename to src/Configuration/src/Encryption/Cryptography/AesTextDecryptor.cs index 2079f84e53..22f9200c5f 100644 --- a/src/Configuration/src/Encryption/Decryption/AesTextDecryptor.cs +++ b/src/Configuration/src/Encryption/Cryptography/AesTextDecryptor.cs @@ -8,7 +8,7 @@ using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Security; -namespace Steeltoe.Configuration.Encryption.Decryption; +namespace Steeltoe.Configuration.Encryption.Cryptography; internal sealed class AesTextDecryptor : ITextDecryptor { @@ -18,7 +18,7 @@ internal sealed class AesTextDecryptor : ITextDecryptor private readonly IBufferedCipher _cipher; public AesTextDecryptor(string key) - : this(key, RsaEncryptionSettings.DefaultSalt, false) + : this(key, RsaCryptoSettings.DefaultSalt, false) { } @@ -59,7 +59,7 @@ public string Decrypt(string fullCipher) public string Decrypt(string fullCipher, string alias) { - throw new NotSupportedException("Key alias is not supported for symmetric encryption"); + throw new NotSupportedException("Key alias is not supported for symmetric cryptography."); } public string Decrypt(byte[] fullCipher) diff --git a/src/Configuration/src/Encryption/Decryption/ConfigServerEncryptionSettings.cs b/src/Configuration/src/Encryption/Cryptography/ConfigServerDecryptionSettings.cs similarity index 54% rename from src/Configuration/src/Encryption/Decryption/ConfigServerEncryptionSettings.cs rename to src/Configuration/src/Encryption/Cryptography/ConfigServerDecryptionSettings.cs index ae63b02f56..0635907515 100644 --- a/src/Configuration/src/Encryption/Decryption/ConfigServerEncryptionSettings.cs +++ b/src/Configuration/src/Encryption/Cryptography/ConfigServerDecryptionSettings.cs @@ -4,39 +4,39 @@ using Microsoft.Extensions.Configuration; -namespace Steeltoe.Configuration.Encryption.Decryption; +namespace Steeltoe.Configuration.Encryption.Cryptography; /// -/// Holds settings used to configure encryption for the Spring Cloud Config Server provider. +/// Holds settings used to configure decryption for the Spring Cloud Config Server provider. /// -internal sealed class ConfigServerEncryptionSettings +internal sealed class ConfigServerDecryptionSettings { /// - /// Gets or sets a value indicating whether encryption is enabled. Default value: false. + /// Gets or sets a value indicating whether decryption is enabled. Default value: false. /// public bool Enabled { get; set; } /// - /// Gets the settings related to RSA encryption. + /// Gets the settings related to RSA cryptography. /// [ConfigurationKeyName("RSA")] - public RsaEncryptionSettings Rsa { get; } = new(); + public RsaCryptoSettings Rsa { get; } = new(); /// /// Gets the settings related to the key store. /// - public EncryptionKeyStoreSettings KeyStore { get; } = new(); + public CryptoKeyStoreSettings KeyStore { get; } = new(); /// - /// Gets or sets the key of the simple encryption. + /// Gets or sets the cryptographic key. /// public string? Key { get; set; } public static ITextDecryptor CreateTextDecryptor(IConfiguration configuration) { - var settings = new ConfigServerEncryptionSettings(); - ConfigurationSettingsHelper.Initialize(settings, configuration); + var settings = new ConfigServerDecryptionSettings(); + ConfigurationSettingsBinder.Initialize(settings, configuration); - return EncryptionFactory.CreateEncryptor(settings); + return TextDecryptorFactory.CreateDecryptor(settings); } } diff --git a/src/Configuration/src/Encryption/Decryption/ConfigurationSettingsHelper.cs b/src/Configuration/src/Encryption/Cryptography/ConfigurationSettingsBinder.cs similarity index 78% rename from src/Configuration/src/Encryption/Decryption/ConfigurationSettingsHelper.cs rename to src/Configuration/src/Encryption/Cryptography/ConfigurationSettingsBinder.cs index b250436c09..0abae661fb 100644 --- a/src/Configuration/src/Encryption/Decryption/ConfigurationSettingsHelper.cs +++ b/src/Configuration/src/Encryption/Cryptography/ConfigurationSettingsBinder.cs @@ -4,13 +4,13 @@ using Microsoft.Extensions.Configuration; -namespace Steeltoe.Configuration.Encryption.Decryption; +namespace Steeltoe.Configuration.Encryption.Cryptography; -internal static class ConfigurationSettingsHelper +internal static class ConfigurationSettingsBinder { private const string ConfigurationPrefix = "encrypt"; - public static void Initialize(ConfigServerEncryptionSettings settings, IConfiguration configuration) + public static void Initialize(ConfigServerDecryptionSettings settings, IConfiguration configuration) { ArgumentNullException.ThrowIfNull(settings); ArgumentNullException.ThrowIfNull(configuration); diff --git a/src/Configuration/src/Encryption/Decryption/EncryptionKeyStoreSettings.cs b/src/Configuration/src/Encryption/Cryptography/CryptoKeyStoreSettings.cs similarity index 79% rename from src/Configuration/src/Encryption/Decryption/EncryptionKeyStoreSettings.cs rename to src/Configuration/src/Encryption/Cryptography/CryptoKeyStoreSettings.cs index 04d020f43e..fc603eaefb 100644 --- a/src/Configuration/src/Encryption/Decryption/EncryptionKeyStoreSettings.cs +++ b/src/Configuration/src/Encryption/Cryptography/CryptoKeyStoreSettings.cs @@ -2,12 +2,12 @@ // The .NET Foundation licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information. -namespace Steeltoe.Configuration.Encryption.Decryption; +namespace Steeltoe.Configuration.Encryption.Cryptography; /// -/// Holds settings related to an encryption key store. +/// Holds settings related to a cryptographic key store. /// -internal sealed class EncryptionKeyStoreSettings +internal sealed class CryptoKeyStoreSettings { /// /// Gets or sets the location of the keystore. diff --git a/src/Configuration/src/Encryption/DecryptionException.cs b/src/Configuration/src/Encryption/Cryptography/DecryptionException.cs similarity index 89% rename from src/Configuration/src/Encryption/DecryptionException.cs rename to src/Configuration/src/Encryption/Cryptography/DecryptionException.cs index fcf948030e..46afd34d85 100644 --- a/src/Configuration/src/Encryption/DecryptionException.cs +++ b/src/Configuration/src/Encryption/Cryptography/DecryptionException.cs @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information. -namespace Steeltoe.Configuration.Encryption; +namespace Steeltoe.Configuration.Encryption.Cryptography; public sealed class DecryptionException : Exception { diff --git a/src/Configuration/src/Encryption/Decryption/IKeyProvider.cs b/src/Configuration/src/Encryption/Cryptography/IKeyProvider.cs similarity index 84% rename from src/Configuration/src/Encryption/Decryption/IKeyProvider.cs rename to src/Configuration/src/Encryption/Cryptography/IKeyProvider.cs index 995668b915..2128b3b860 100644 --- a/src/Configuration/src/Encryption/Decryption/IKeyProvider.cs +++ b/src/Configuration/src/Encryption/Cryptography/IKeyProvider.cs @@ -4,7 +4,7 @@ using Org.BouncyCastle.Crypto; -namespace Steeltoe.Configuration.Encryption.Decryption; +namespace Steeltoe.Configuration.Encryption.Cryptography; internal interface IKeyProvider { diff --git a/src/Configuration/src/Encryption/ITextDecryptor.cs b/src/Configuration/src/Encryption/Cryptography/ITextDecryptor.cs similarity index 89% rename from src/Configuration/src/Encryption/ITextDecryptor.cs rename to src/Configuration/src/Encryption/Cryptography/ITextDecryptor.cs index 84a238368e..320498feb5 100644 --- a/src/Configuration/src/Encryption/ITextDecryptor.cs +++ b/src/Configuration/src/Encryption/Cryptography/ITextDecryptor.cs @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information. -namespace Steeltoe.Configuration.Encryption; +namespace Steeltoe.Configuration.Encryption.Cryptography; public interface ITextDecryptor { diff --git a/src/Configuration/src/Encryption/Decryption/KeyProvider.cs b/src/Configuration/src/Encryption/Cryptography/KeyProvider.cs similarity index 95% rename from src/Configuration/src/Encryption/Decryption/KeyProvider.cs rename to src/Configuration/src/Encryption/Cryptography/KeyProvider.cs index 8af1d3cf25..19022c577d 100644 --- a/src/Configuration/src/Encryption/Decryption/KeyProvider.cs +++ b/src/Configuration/src/Encryption/Cryptography/KeyProvider.cs @@ -5,7 +5,7 @@ using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Pkcs; -namespace Steeltoe.Configuration.Encryption.Decryption; +namespace Steeltoe.Configuration.Encryption.Cryptography; internal sealed class KeyProvider : IKeyProvider { diff --git a/src/Configuration/src/Encryption/Decryption/NoopDecryptor.cs b/src/Configuration/src/Encryption/Cryptography/NoneDecryptor.cs similarity index 65% rename from src/Configuration/src/Encryption/Decryption/NoopDecryptor.cs rename to src/Configuration/src/Encryption/Cryptography/NoneDecryptor.cs index 9f4a511fe7..8a8bf335a9 100644 --- a/src/Configuration/src/Encryption/Decryption/NoopDecryptor.cs +++ b/src/Configuration/src/Encryption/Cryptography/NoneDecryptor.cs @@ -1,10 +1,10 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information. -namespace Steeltoe.Configuration.Encryption.Decryption; +namespace Steeltoe.Configuration.Encryption.Cryptography; -internal sealed class NoopDecryptor : ITextDecryptor +internal sealed class NoneDecryptor : ITextDecryptor { public string Decrypt(string fullCipher) { diff --git a/src/Configuration/src/Encryption/Decryption/RsaEncryptionSettings.cs b/src/Configuration/src/Encryption/Cryptography/RsaCryptoSettings.cs similarity index 82% rename from src/Configuration/src/Encryption/Decryption/RsaEncryptionSettings.cs rename to src/Configuration/src/Encryption/Cryptography/RsaCryptoSettings.cs index 12eaf6f4d7..d5b066c093 100644 --- a/src/Configuration/src/Encryption/Decryption/RsaEncryptionSettings.cs +++ b/src/Configuration/src/Encryption/Cryptography/RsaCryptoSettings.cs @@ -2,20 +2,20 @@ // The .NET Foundation licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information. -namespace Steeltoe.Configuration.Encryption.Decryption; +namespace Steeltoe.Configuration.Encryption.Cryptography; /// -/// Holds settings related to RSA encryption. +/// Holds settings related to RSA cryptography. /// -internal sealed class RsaEncryptionSettings +internal sealed class RsaCryptoSettings { /// - /// Default Encryption method. + /// Default RSA algorithm. /// internal const string DefaultAlgorithm = "DEFAULT"; /// - /// Default salt. + /// Default salt value. /// internal const string DefaultSalt = "deadbeef"; diff --git a/src/Configuration/src/Encryption/Decryption/RsaKeyStoreDecryptor.cs b/src/Configuration/src/Encryption/Cryptography/RsaKeyStoreDecryptor.cs similarity index 95% rename from src/Configuration/src/Encryption/Decryption/RsaKeyStoreDecryptor.cs rename to src/Configuration/src/Encryption/Cryptography/RsaKeyStoreDecryptor.cs index 36329f39b7..a92bdcabbc 100644 --- a/src/Configuration/src/Encryption/Decryption/RsaKeyStoreDecryptor.cs +++ b/src/Configuration/src/Encryption/Cryptography/RsaKeyStoreDecryptor.cs @@ -6,7 +6,7 @@ using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Security; -namespace Steeltoe.Configuration.Encryption.Decryption; +namespace Steeltoe.Configuration.Encryption.Cryptography; internal sealed class RsaKeyStoreDecryptor : ITextDecryptor { @@ -17,7 +17,7 @@ internal sealed class RsaKeyStoreDecryptor : ITextDecryptor private readonly string _defaultKeyAlias; public RsaKeyStoreDecryptor(IKeyProvider keyProvider, string alias) - : this(keyProvider, alias, RsaEncryptionSettings.DefaultSalt, false, RsaEncryptionSettings.DefaultAlgorithm) + : this(keyProvider, alias, RsaCryptoSettings.DefaultSalt, false, RsaCryptoSettings.DefaultAlgorithm) { } diff --git a/src/Configuration/src/Encryption/Decryption/EncryptionFactory.cs b/src/Configuration/src/Encryption/Cryptography/TextDecryptorFactory.cs similarity index 72% rename from src/Configuration/src/Encryption/Decryption/EncryptionFactory.cs rename to src/Configuration/src/Encryption/Cryptography/TextDecryptorFactory.cs index 29a282b63a..341f406208 100644 --- a/src/Configuration/src/Encryption/Decryption/EncryptionFactory.cs +++ b/src/Configuration/src/Encryption/Cryptography/TextDecryptorFactory.cs @@ -2,17 +2,17 @@ // The .NET Foundation licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information. -namespace Steeltoe.Configuration.Encryption.Decryption; +namespace Steeltoe.Configuration.Encryption.Cryptography; -internal static class EncryptionFactory +internal static class TextDecryptorFactory { - public static ITextDecryptor CreateEncryptor(ConfigServerEncryptionSettings settings) + public static ITextDecryptor CreateDecryptor(ConfigServerDecryptionSettings settings) { - ITextDecryptor decryptor = new NoopDecryptor(); + ITextDecryptor decryptor = new NoneDecryptor(); if (settings.Enabled) { - EnsureValidEncryptionSettings(settings); + EnsureValidSettings(settings); if (!string.IsNullOrEmpty(settings.Key)) { @@ -31,7 +31,7 @@ public static ITextDecryptor CreateEncryptor(ConfigServerEncryptionSettings sett return decryptor; } - private static void EnsureValidEncryptionSettings(ConfigServerEncryptionSettings settings) + private static void EnsureValidSettings(ConfigServerDecryptionSettings settings) { bool hasKeySettings = !string.IsNullOrEmpty(settings.Key); @@ -41,7 +41,7 @@ private static void EnsureValidEncryptionSettings(ConfigServerEncryptionSettings if ((hasKeySettings && hasKeyStoreSettings) || (!hasKeySettings && !hasKeyStoreSettings)) { throw new DecryptionException( - "No valid configuration for encryption key or key store. Either 'encrypt.key' or the 'encrypt.keyStore' properties (location, password, alias) must be set. Not both."); + "No valid configuration for cryptographic key or key store. Either 'encrypt.key' or the 'encrypt.keyStore' properties (location, password, alias) must be set. Not both."); } } } diff --git a/src/Configuration/src/Encryption/DecryptionConfigurationBuilderExtensions.cs b/src/Configuration/src/Encryption/DecryptionConfigurationBuilderExtensions.cs new file mode 100644 index 0000000000..b5c9f12054 --- /dev/null +++ b/src/Configuration/src/Encryption/DecryptionConfigurationBuilderExtensions.cs @@ -0,0 +1,92 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Steeltoe.Configuration.Encryption.Cryptography; + +namespace Steeltoe.Configuration.Encryption; + +public static class DecryptionConfigurationBuilderExtensions +{ + /// + /// Adds a configuration source to the that decrypts encrypted values. + /// + /// This method replaces all the existing s contained in the builder, taking ownership of them. The newly added source + /// then provides decryption of values stored in its inner configuration (built from the pre-existing sources). Typically, you will want to add this + /// configuration source as the last one, so that decryption is applied to all configuration values. + /// + /// + /// + /// The to add configuration to. + /// + /// + /// The incoming so that additional calls can be chained. + /// + public static IConfigurationBuilder AddDecryption(this IConfigurationBuilder builder) + { + return AddDecryption(builder, null, NullLoggerFactory.Instance); + } + + /// + /// Adds a configuration source to the that decrypts encrypted values. + /// + /// This method replaces all the existing s contained in the builder, taking ownership of them. The newly added source + /// then provides decryption of values stored in its inner configuration (built from the pre-existing sources). Typically, you will want to add this + /// configuration source as the last one, so that decryption is applied to all configuration values. + /// + /// + /// + /// The to add configuration to. + /// + /// + /// Used for internal logging. Pass to disable logging. + /// + /// + /// The incoming so that additional calls can be chained. + /// + public static IConfigurationBuilder AddDecryption(this IConfigurationBuilder builder, ILoggerFactory loggerFactory) + { + return AddDecryption(builder, null, loggerFactory); + } + + /// + /// Adds a configuration source to the that decrypts encrypted values. + /// + /// This method replaces all the existing s contained in the builder, taking ownership of them. The newly added source + /// then provides decryption of values stored in its inner configuration (built from the pre-existing sources). Typically, you will want to add this + /// configuration source as the last one, so that decryption is applied to all configuration values. + /// + /// + /// + /// The to add configuration to. + /// + /// + /// The decryptor to use, or null to load settings from configuration. + /// + /// + /// Used for internal logging. Pass to disable logging. + /// + /// + /// The incoming so that additional calls can be chained. + /// + public static IConfigurationBuilder AddDecryption(this IConfigurationBuilder builder, ITextDecryptor? textDecryptor, ILoggerFactory loggerFactory) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(loggerFactory); + + var decryptionSource = new DecryptionConfigurationSource(textDecryptor, loggerFactory); + + foreach (IConfigurationSource source in builder.Sources) + { + decryptionSource.Sources.Add(source); + } + + builder.Sources.Clear(); + builder.Sources.Add(decryptionSource); + + return builder; + } +} diff --git a/src/Configuration/src/Encryption/DecryptionConfigurationProvider.cs b/src/Configuration/src/Encryption/DecryptionConfigurationProvider.cs new file mode 100644 index 0000000000..21c78681fa --- /dev/null +++ b/src/Configuration/src/Encryption/DecryptionConfigurationProvider.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. + +using System.Text.RegularExpressions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Steeltoe.Configuration.Encryption.Cryptography; + +namespace Steeltoe.Configuration.Encryption; + +internal sealed partial class DecryptionConfigurationProvider( + IList providers, ITextDecryptor? textDecryptor, ILoggerFactory loggerFactory) + : CompositeConfigurationProvider(providers, loggerFactory) +{ + private readonly ILogger _logger = loggerFactory.CreateLogger(); + + [GeneratedRegex("^{cipher}({key:(?.*)})?(?.*)$", RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture, 1000)] + private static partial Regex CipherRegex(); + + public override bool TryGet(string key, out string? value) + { + bool found = base.TryGet(key, out value); + + if (found && ConfigurationRoot != null && !string.IsNullOrEmpty(value)) + { + Match match = CipherRegex().Match(value); + + if (match.Success) + { + string alias = match.Groups["alias"].Value; + string cipher = match.Groups["cipher"].Value; + + ITextDecryptor decryptor = EnsureDecryptor(ConfigurationRoot); + string decryptedValue = !string.IsNullOrEmpty(alias) ? decryptor.Decrypt(cipher, alias) : decryptor.Decrypt(cipher); + + if (decryptedValue != value) + { + LogDecrypt(key, value, decryptedValue); + value = decryptedValue; + } + } + } + + return value != null; + } + + private ITextDecryptor EnsureDecryptor(IConfigurationRoot configurationRoot) + { + textDecryptor ??= ConfigServerDecryptionSettings.CreateTextDecryptor(configurationRoot); + return textDecryptor; + } + + [LoggerMessage(Level = LogLevel.Trace, Message = "Decrypted value '{CipherValue}' at key '{Key}' to '{PlainTextValue}'.")] + private partial void LogDecrypt(string key, string? cipherValue, string? plainTextValue); +} diff --git a/src/Configuration/src/Encryption/DecryptionConfigurationSource.cs b/src/Configuration/src/Encryption/DecryptionConfigurationSource.cs new file mode 100644 index 0000000000..a55cbc8aee --- /dev/null +++ b/src/Configuration/src/Encryption/DecryptionConfigurationSource.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Steeltoe.Configuration.Encryption.Cryptography; + +namespace Steeltoe.Configuration.Encryption; + +internal sealed partial class DecryptionConfigurationSource : ICompositeConfigurationSource +{ + private readonly ITextDecryptor? _textDecryptor; + private readonly ILoggerFactory _loggerFactory; + private readonly ILogger _logger; + + public IList Sources { get; } = new List(); + + public DecryptionConfigurationSource(ITextDecryptor? textDecryptor, ILoggerFactory loggerFactory) + { + ArgumentNullException.ThrowIfNull(loggerFactory); + + _textDecryptor = textDecryptor; + _loggerFactory = loggerFactory; + _logger = loggerFactory.CreateLogger(); + } + + public IConfigurationProvider Build(IConfigurationBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); + + LogBuild(_logger, Sources.Count, builder.Properties.Count); + List providers = Sources.Select(source => source.Build(builder)).ToList(); + return new DecryptionConfigurationProvider(providers, _textDecryptor, _loggerFactory); + } + + [LoggerMessage(Level = LogLevel.Trace, Message = "Build for {SourceCount} sources and {PropertyCount} properties.")] + private static partial void LogBuild(ILogger logger, int sourceCount, int propertyCount); +} diff --git a/src/Configuration/src/Encryption/EncryptionConfigurationExtensions.cs b/src/Configuration/src/Encryption/EncryptionConfigurationExtensions.cs deleted file mode 100644 index 38731b292c..0000000000 --- a/src/Configuration/src/Encryption/EncryptionConfigurationExtensions.cs +++ /dev/null @@ -1,205 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information. - -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Steeltoe.Configuration.Encryption.Decryption; - -namespace Steeltoe.Configuration.Encryption; - -public static class EncryptionConfigurationExtensions -{ - /// - /// Adds an encryption resolver configuration source to the . The encryption resolver source will capture and wrap all - /// the existing sources contained in the builder. The newly created source will then replace the existing sources - /// and provide encryption resolution for the configuration. Typically, you will want to add this configuration source as the last one so that you wrap - /// all applications' configuration sources with encryption resolution. - /// - /// - /// The to add configuration to. - /// - /// - /// The incoming so that additional calls can be chained. - /// - public static IConfigurationBuilder AddEncryptionResolver(this IConfigurationBuilder builder) - { - return AddEncryptionResolver(builder, NullLoggerFactory.Instance); - } - - /// - /// Adds an encryption resolver configuration source to the . The encryption resolver source will capture and wrap all - /// the existing sources contained in the builder. The newly created source will then replace the existing sources - /// and provide encryption resolution for the configuration. Typically, you will want to add this configuration source as the last one so that you wrap - /// all applications' configuration sources with encryption resolution. - /// - /// - /// The to add configuration to. - /// - /// - /// Used for internal logging. Pass to disable logging. - /// - /// - /// The incoming so that additional calls can be chained. - /// - public static IConfigurationBuilder AddEncryptionResolver(this IConfigurationBuilder builder, ILoggerFactory loggerFactory) - { - ArgumentNullException.ThrowIfNull(builder); - - IConfiguration configuration = builder.Build(); - ITextDecryptor textDecryptor = ConfigServerEncryptionSettings.CreateTextDecryptor(configuration); - - return AddEncryptionResolver(builder, textDecryptor, loggerFactory); - } - - /// - /// Adds an encryption resolver configuration source to the . The encryption resolver source will capture and wrap all - /// the existing sources contained in the builder. The newly created source will then replace the existing sources - /// and provide encryption resolution for the configuration. Typically, you will want to add this configuration source as the last one so that you wrap - /// all applications' configuration sources with encryption resolution. - /// - /// - /// The to add configuration to. - /// - /// - /// The decryptor to use. - /// - /// - /// Used for internal logging. Pass to disable logging. - /// - /// - /// The incoming so that additional calls can be chained. - /// - public static IConfigurationBuilder AddEncryptionResolver(this IConfigurationBuilder builder, ITextDecryptor textDecryptor, ILoggerFactory loggerFactory) - { - ArgumentNullException.ThrowIfNull(builder); - ArgumentNullException.ThrowIfNull(textDecryptor); - ArgumentNullException.ThrowIfNull(loggerFactory); - - if (!builder.Sources.OfType().Any()) - { - if (builder is IConfigurationRoot configuration) - { - var source = new EncryptionResolverSource(configuration, textDecryptor, loggerFactory); - builder.Add(source); - } - else - { - var source = new EncryptionResolverSource(builder.Sources, textDecryptor, loggerFactory); - builder.Sources.Clear(); - builder.Add(source); - } - } - - return builder; - } - - /// - /// Creates a new from a . The encryption resolver will be created using the - /// existing configuration providers contained in the incoming configuration. This results in providing encryption resolution for those configuration - /// sources. - /// - /// - /// The to wrap. - /// - /// - /// The decryptor to use. - /// - /// - /// A new configuration. - /// - public static IConfiguration AddEncryptionResolver(this IConfiguration configuration, ITextDecryptor textDecryptor) - { - return AddEncryptionResolver(configuration, textDecryptor, NullLoggerFactory.Instance); - } - - /// - /// Creates a new from a . The encryption resolver will be created using the - /// existing configuration providers contained in the incoming configuration. This results in providing encryption resolution for those configuration - /// sources. - /// - /// - /// The to wrap. - /// - /// - /// The decryptor to use. - /// - /// - /// Used for internal logging. Pass to disable logging. - /// - /// - /// A new configuration. - /// - public static IConfiguration AddEncryptionResolver(this IConfiguration configuration, ITextDecryptor textDecryptor, ILoggerFactory loggerFactory) - { - ArgumentNullException.ThrowIfNull(configuration); - ArgumentNullException.ThrowIfNull(textDecryptor); - ArgumentNullException.ThrowIfNull(loggerFactory); - - if (configuration is not IConfigurationRoot root) - { - throw new InvalidOperationException($"Configuration must implement '{typeof(IConfigurationRoot)}'."); - } - - if (root.Providers.Any(provider => provider is EncryptionResolverProvider)) - { - return configuration; - } - - return new ConfigurationRoot(new List - { - new EncryptionResolverProvider(new List(root.Providers), textDecryptor, loggerFactory) - }); - } - - /// - /// Adds an encryption resolver configuration source to the . The encryption resolver source will capture and wrap all - /// the existing sources contained in the builder. The newly created source will then replace the existing sources - /// and provide encryption resolution for the configuration. Typically, you will want to add this configuration source as the last one so that you wrap - /// all applications' configuration sources with encryption resolution. - /// - /// - /// The to configure. - /// - /// - /// The decryptor to use. - /// - /// - /// The incoming so that additional calls can be chained. - /// - public static ConfigurationManager AddEncryptionResolver(this ConfigurationManager configurationManager, ITextDecryptor textDecryptor) - { - return AddEncryptionResolver(configurationManager, textDecryptor, NullLoggerFactory.Instance); - } - - /// - /// Adds an encryption resolver configuration source to the . The encryption resolver source will capture and wrap all - /// the existing sources contained in the builder. The newly created source will then replace the existing sources - /// and provide encryption resolution for the configuration. Typically, you will want to add this configuration source as the last one so that you wrap - /// all applications' configuration sources with encryption resolution. - /// - /// - /// The to configure. - /// - /// - /// The decryptor to use. - /// - /// - /// Used for internal logging. Pass to disable logging. - /// - /// - /// The incoming so that additional calls can be chained. - /// - public static ConfigurationManager AddEncryptionResolver(this ConfigurationManager configurationManager, ITextDecryptor textDecryptor, - ILoggerFactory loggerFactory) - { - ArgumentNullException.ThrowIfNull(configurationManager); - ArgumentNullException.ThrowIfNull(textDecryptor); - ArgumentNullException.ThrowIfNull(loggerFactory); - - ((IConfigurationBuilder)configurationManager).AddEncryptionResolver(textDecryptor, loggerFactory); - - return configurationManager; - } -} diff --git a/src/Configuration/src/Encryption/EncryptionResolverProvider.cs b/src/Configuration/src/Encryption/EncryptionResolverProvider.cs deleted file mode 100644 index 9120a31b99..0000000000 --- a/src/Configuration/src/Encryption/EncryptionResolverProvider.cs +++ /dev/null @@ -1,219 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information. - -using System.Text.RegularExpressions; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Primitives; - -namespace Steeltoe.Configuration.Encryption; - -/// -/// Configuration provider that resolves encryptions. An encryption takes the form of: -/// -/// -internal sealed class EncryptionResolverProvider : IConfigurationProvider, IDisposable -{ - // regex for matching {cipher}{key:keyAlias} at the start of the string - private readonly Regex _cipherRegex = new("^{cipher}({key:(?.*)})?(?.*)", RegexOptions.None, TimeSpan.FromSeconds(1)); - private bool _isDisposed; - - /// - /// Gets the configuration this encryption resolver wraps. - /// - internal IConfigurationRoot? Configuration { get; private set; } - - public IList Providers { get; } = new List(); - - public ITextDecryptor Decryptor { get; set; } - - /// - /// Initializes a new instance of the class. The new encryption resolver wraps the provided configuration root. - /// - /// - /// The configuration the provider uses when resolving encryptions. - /// - /// - /// The decryptor to use. - /// - /// - /// Used for internal logging. Pass to disable logging. - /// - public EncryptionResolverProvider(IConfigurationRoot root, ITextDecryptor textDecryptor, ILoggerFactory loggerFactory) - { - ArgumentNullException.ThrowIfNull(root); - ArgumentNullException.ThrowIfNull(textDecryptor); - ArgumentNullException.ThrowIfNull(loggerFactory); - - Configuration = root; - Decryptor = textDecryptor; - } - - /// - /// Initializes a new instance of the class. The new encryption resolver wraps the provided configuration - /// providers. The will be created from these providers. - /// - /// - /// The configuration providers the resolver uses when resolving encryptions. - /// - /// - /// The decryptor to use. - /// - /// - /// Used for internal logging. Pass to disable logging. - /// - public EncryptionResolverProvider(IList providers, ITextDecryptor textDecryptor, ILoggerFactory loggerFactory) - { - ArgumentNullException.ThrowIfNull(providers); - ArgumentNullException.ThrowIfNull(textDecryptor); - ArgumentNullException.ThrowIfNull(loggerFactory); - - Providers = providers; - Decryptor = textDecryptor; - } - - /// - /// Tries to get a configuration value for the specified key. If the value is an encryption, it will try to resolve the encryption before returning it. - /// - /// - /// The configuration key. - /// - /// - /// When this method returns, contains the resolved configuration value, if a value for the specified key was found. - /// - /// - /// true if a value for the specified key was found, otherwise false. - /// - public bool TryGet(string key, out string? value) - { - ArgumentNullException.ThrowIfNull(key); - EnsureInitialized(); - - string? originalValue = Configuration![key]; - value = originalValue; - - if (!string.IsNullOrEmpty(originalValue)) - { - Match match = _cipherRegex.Match(originalValue); - - if (match.Success) - { - string alias = match.Groups["alias"].Value; - string cipher = match.Groups["cipher"].Value; - - value = !string.IsNullOrEmpty(alias) ? Decryptor.Decrypt(cipher, alias) : Decryptor.Decrypt(cipher); - } - } - - return !string.IsNullOrEmpty(value); - } - - /// - /// Sets a configuration value for the specified key. No encryption resolution is performed. - /// - /// - /// The configuration key whose value to set. - /// - /// - /// The configuration value to set at the specified key. - /// - public void Set(string key, string? value) - { - ArgumentException.ThrowIfNullOrEmpty(key); - EnsureInitialized(); - - Configuration![key] = value; - } - - /// - /// Returns a change token that can be used to observe when this configuration is reloaded. - /// - /// - /// The change token. - /// - public IChangeToken GetReloadToken() - { - EnsureInitialized(); - - return Configuration!.GetReloadToken(); - } - - /// - /// Creates the from the providers, if it has not done so already, and calls on the - /// underlying configuration. - /// - public void Load() - { - EnsureInitialized(); - - Configuration!.Reload(); - } - - /// - /// Returns the immediate descendant configuration keys for a given parent path, based on this 's data and the set of keys - /// returned by all the preceding providers. - /// - /// - /// The child keys returned by the preceding providers for the same parent path. - /// - /// - /// The parent path. - /// - /// - /// The child keys. - /// - public IEnumerable GetChildKeys(IEnumerable earlierKeys, string? parentPath) - { - EnsureInitialized(); - - IConfiguration section = parentPath == null ? Configuration! : Configuration!.GetSection(parentPath); - IEnumerable children = section.GetChildren(); - - return children.Select(childSection => childSection.Key).Concat(earlierKeys).OrderBy(key => key, ConfigurationKeyComparer.Instance); - } - - private void EnsureInitialized() - { - ObjectDisposedException.ThrowIf(_isDisposed, this); - - Configuration ??= new ConfigurationRoot(Providers); - } - - public void Dispose() - { - if (!_isDisposed) - { - HashSet disposables = []; - - foreach (IConfigurationProvider provider in Providers) - { - if (provider is IDisposable disposable) - { - disposables.Add(disposable); - } - } - - if (Configuration != null) - { - foreach (IConfigurationProvider provider in Configuration.Providers) - { - if (provider is IDisposable disposable) - { - disposables.Add(disposable); - } - } - } - - foreach (IDisposable disposable in disposables) - { - disposable.Dispose(); - } - - _isDisposed = true; - } - } -} diff --git a/src/Configuration/src/Encryption/EncryptionResolverSource.cs b/src/Configuration/src/Encryption/EncryptionResolverSource.cs deleted file mode 100644 index 0fadfb4669..0000000000 --- a/src/Configuration/src/Encryption/EncryptionResolverSource.cs +++ /dev/null @@ -1,103 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information. - -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; - -namespace Steeltoe.Configuration.Encryption; - -/// -/// Configuration source used in creating a that resolves encryptions. An encryption takes the form of: -/// -/// -internal sealed class EncryptionResolverSource : IConfigurationSource -{ - private readonly IConfigurationRoot? _configuration; - private readonly ITextDecryptor _textDecryptor; - - internal IList? Sources { get; } - internal ILoggerFactory LoggerFactory { get; } - - /// - /// Initializes a new instance of the class. - /// - /// - /// The configuration sources to use. - /// - /// - /// The decryptor to use. - /// - /// - /// Used for internal logging. Pass to disable logging. - /// - public EncryptionResolverSource(IList sources, ITextDecryptor textDecryptor, ILoggerFactory loggerFactory) - { - ArgumentNullException.ThrowIfNull(sources); - ArgumentNullException.ThrowIfNull(textDecryptor); - ArgumentNullException.ThrowIfNull(loggerFactory); - - _textDecryptor = textDecryptor; - Sources = sources.ToList(); - LoggerFactory = loggerFactory; - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// The root configuration to use. - /// - /// - /// Decryptor to use. - /// - /// - /// Used for internal logging. Pass to disable logging. - /// - public EncryptionResolverSource(IConfigurationRoot root, ITextDecryptor textDecryptor, ILoggerFactory loggerFactory) - { - ArgumentNullException.ThrowIfNull(root); - ArgumentNullException.ThrowIfNull(textDecryptor); - ArgumentNullException.ThrowIfNull(loggerFactory); - - _configuration = root; - _textDecryptor = textDecryptor; - LoggerFactory = loggerFactory; - } - - /// - /// Builds a from the specified builder. - /// - /// - /// Used to build providers from sources, in case a list of sources was provided instead of a configuration root. - /// - /// - /// The encryption resolver provider. - /// - public IConfigurationProvider Build(IConfigurationBuilder builder) - { - ArgumentNullException.ThrowIfNull(builder); - - if (_configuration != null) - { - var configurationView = new ConfigurationView(_configuration.Providers.ToArray()); - return new EncryptionResolverProvider(configurationView, _textDecryptor, LoggerFactory); - } - - List providers = []; - - if (Sources != null) - { - foreach (IConfigurationSource source in Sources) - { - IConfigurationProvider provider = source.Build(builder); - providers.Add(provider); - } - } - - return new EncryptionResolverProvider(providers, _textDecryptor, LoggerFactory); - } -} diff --git a/src/Configuration/src/Encryption/EncryptionServiceCollectionExtensions.cs b/src/Configuration/src/Encryption/EncryptionServiceCollectionExtensions.cs deleted file mode 100644 index 4d63771694..0000000000 --- a/src/Configuration/src/Encryption/EncryptionServiceCollectionExtensions.cs +++ /dev/null @@ -1,146 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information. - -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Steeltoe.Configuration.Encryption.Decryption; - -namespace Steeltoe.Configuration.Encryption; - -public static class EncryptionServiceCollectionExtensions -{ - /// - /// Creates a new using a which wraps the provided - /// . The new configuration will then be used to replace the current in the service container. All subsequent requests for - /// a will return the newly created providing encryption resolution. - /// - /// - /// The to add services to. - /// - /// - /// The configuration the encryption resolver will wrap. - /// - /// - /// The new configuration. - /// - public static IConfiguration ConfigureEncryptionResolver(this IServiceCollection services, IConfiguration configuration) - { - ArgumentNullException.ThrowIfNull(services); - ArgumentNullException.ThrowIfNull(configuration); - - ITextDecryptor textDecryptor = ConfigServerEncryptionSettings.CreateTextDecryptor(configuration); - return ConfigureEncryptionResolver(services, configuration, textDecryptor, NullLoggerFactory.Instance); - } - - /// - /// Creates a new using a which wraps the provided - /// . The new configuration will then be used to replace the current in the service container. All subsequent requests for - /// a will return the newly created providing encryption resolution. - /// - /// - /// The to add services to. - /// - /// - /// The configuration the encryption resolver will wrap. - /// - /// - /// The decryptor to use. - /// - /// - /// The new configuration. - /// - public static IConfiguration ConfigureEncryptionResolver(this IServiceCollection services, IConfiguration configuration, ITextDecryptor textDecryptor) - { - return ConfigureEncryptionResolver(services, configuration, textDecryptor, NullLoggerFactory.Instance); - } - - /// - /// Creates a new using a which wraps the provided - /// . The new configuration will then be used to replace the current in the service container. All subsequent requests for - /// a will return the newly created providing encryption resolution. - /// - /// - /// The to add services to. - /// - /// - /// The configuration the encryption resolver will wrap. - /// - /// - /// The decryptor to use. - /// - /// - /// Used for internal logging. Pass to disable logging. - /// - /// - /// The new configuration. - /// - public static IConfiguration ConfigureEncryptionResolver(this IServiceCollection services, IConfiguration configuration, ITextDecryptor textDecryptor, - ILoggerFactory loggerFactory) - { - ArgumentNullException.ThrowIfNull(services); - ArgumentNullException.ThrowIfNull(configuration); - ArgumentNullException.ThrowIfNull(textDecryptor); - ArgumentNullException.ThrowIfNull(loggerFactory); - - IConfiguration newConfiguration = configuration.AddEncryptionResolver(textDecryptor, loggerFactory); - services.Replace(ServiceDescriptor.Singleton(typeof(IConfiguration), newConfiguration)); - services.Replace(ServiceDescriptor.Singleton(typeof(ITextDecryptor), textDecryptor)); - - return newConfiguration; - } - - /// - /// Creates a new using a which wraps the provided - /// . The new configuration will then be used to replace the current in the service container. All subsequent requests for - /// a will return the newly created providing encryption resolution. - /// - /// - /// The to add services to. - /// - /// - /// The configuration the encryption resolver will wrap. - /// - /// - /// The new configuration. - /// - public static IConfiguration ConfigureConfigServerEncryptionResolver(this IServiceCollection services, IConfiguration configuration) - { - return ConfigureConfigServerEncryptionResolver(services, configuration, NullLoggerFactory.Instance); - } - - /// - /// Creates a new using a which wraps the provided - /// . The new configuration will then be used to replace the current in the service container. All subsequent requests for - /// a will return the newly created providing encryption resolution. - /// - /// - /// The to add services to. - /// - /// - /// The configuration the encryption resolver will wrap. - /// - /// - /// Used for internal logging. Pass to disable logging. - /// - /// - /// The new configuration. - /// - public static IConfiguration ConfigureConfigServerEncryptionResolver(this IServiceCollection services, IConfiguration configuration, - ILoggerFactory loggerFactory) - { - ArgumentNullException.ThrowIfNull(services); - ArgumentNullException.ThrowIfNull(configuration); - ArgumentNullException.ThrowIfNull(loggerFactory); - - ITextDecryptor textDecryptor = ConfigServerEncryptionSettings.CreateTextDecryptor(configuration); - - IConfiguration newConfiguration = configuration.AddEncryptionResolver(textDecryptor, loggerFactory); - services.Replace(ServiceDescriptor.Singleton(typeof(IConfiguration), newConfiguration)); - - return newConfiguration; - } -} diff --git a/src/Configuration/src/Encryption/Properties/AssemblyInfo.cs b/src/Configuration/src/Encryption/Properties/AssemblyInfo.cs index 866ec739ed..4b98fd7181 100644 --- a/src/Configuration/src/Encryption/Properties/AssemblyInfo.cs +++ b/src/Configuration/src/Encryption/Properties/AssemblyInfo.cs @@ -4,9 +4,9 @@ using System.Runtime.CompilerServices; using Aspire; -using Steeltoe.Configuration.Encryption.Decryption; +using Steeltoe.Configuration.Encryption.Cryptography; -[assembly: ConfigurationSchema("Encrypt", typeof(ConfigServerEncryptionSettings))] +[assembly: ConfigurationSchema("Encrypt", typeof(ConfigServerDecryptionSettings))] [assembly: LoggingCategories("Steeltoe", "Steeltoe.Configuration", "Steeltoe.Configuration.Encryption")] [assembly: InternalsVisibleTo("Steeltoe.Bootstrap.AutoConfiguration.Test")] diff --git a/src/Configuration/src/Encryption/PublicAPI.Unshipped.txt b/src/Configuration/src/Encryption/PublicAPI.Unshipped.txt index 9c56fbb791..3f2c3ff797 100644 --- a/src/Configuration/src/Encryption/PublicAPI.Unshipped.txt +++ b/src/Configuration/src/Encryption/PublicAPI.Unshipped.txt @@ -1,21 +1,11 @@ #nullable enable -static Steeltoe.Configuration.Encryption.EncryptionConfigurationExtensions.AddEncryptionResolver(this Microsoft.Extensions.Configuration.ConfigurationManager! configurationManager, Steeltoe.Configuration.Encryption.ITextDecryptor! textDecryptor) -> Microsoft.Extensions.Configuration.ConfigurationManager! -static Steeltoe.Configuration.Encryption.EncryptionConfigurationExtensions.AddEncryptionResolver(this Microsoft.Extensions.Configuration.ConfigurationManager! configurationManager, Steeltoe.Configuration.Encryption.ITextDecryptor! textDecryptor, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> Microsoft.Extensions.Configuration.ConfigurationManager! -static Steeltoe.Configuration.Encryption.EncryptionConfigurationExtensions.AddEncryptionResolver(this Microsoft.Extensions.Configuration.IConfiguration! configuration, Steeltoe.Configuration.Encryption.ITextDecryptor! textDecryptor) -> Microsoft.Extensions.Configuration.IConfiguration! -static Steeltoe.Configuration.Encryption.EncryptionConfigurationExtensions.AddEncryptionResolver(this Microsoft.Extensions.Configuration.IConfiguration! configuration, Steeltoe.Configuration.Encryption.ITextDecryptor! textDecryptor, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> Microsoft.Extensions.Configuration.IConfiguration! -static Steeltoe.Configuration.Encryption.EncryptionConfigurationExtensions.AddEncryptionResolver(this Microsoft.Extensions.Configuration.IConfigurationBuilder! builder) -> Microsoft.Extensions.Configuration.IConfigurationBuilder! -static Steeltoe.Configuration.Encryption.EncryptionConfigurationExtensions.AddEncryptionResolver(this Microsoft.Extensions.Configuration.IConfigurationBuilder! builder, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> Microsoft.Extensions.Configuration.IConfigurationBuilder! -static Steeltoe.Configuration.Encryption.EncryptionConfigurationExtensions.AddEncryptionResolver(this Microsoft.Extensions.Configuration.IConfigurationBuilder! builder, Steeltoe.Configuration.Encryption.ITextDecryptor! textDecryptor, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> Microsoft.Extensions.Configuration.IConfigurationBuilder! -static Steeltoe.Configuration.Encryption.EncryptionServiceCollectionExtensions.ConfigureConfigServerEncryptionResolver(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfiguration! configuration) -> Microsoft.Extensions.Configuration.IConfiguration! -static Steeltoe.Configuration.Encryption.EncryptionServiceCollectionExtensions.ConfigureConfigServerEncryptionResolver(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfiguration! configuration, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> Microsoft.Extensions.Configuration.IConfiguration! -static Steeltoe.Configuration.Encryption.EncryptionServiceCollectionExtensions.ConfigureEncryptionResolver(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfiguration! configuration) -> Microsoft.Extensions.Configuration.IConfiguration! -static Steeltoe.Configuration.Encryption.EncryptionServiceCollectionExtensions.ConfigureEncryptionResolver(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfiguration! configuration, Steeltoe.Configuration.Encryption.ITextDecryptor! textDecryptor) -> Microsoft.Extensions.Configuration.IConfiguration! -static Steeltoe.Configuration.Encryption.EncryptionServiceCollectionExtensions.ConfigureEncryptionResolver(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfiguration! configuration, Steeltoe.Configuration.Encryption.ITextDecryptor! textDecryptor, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> Microsoft.Extensions.Configuration.IConfiguration! -Steeltoe.Configuration.Encryption.DecryptionException -Steeltoe.Configuration.Encryption.DecryptionException.DecryptionException(string? message) -> void -Steeltoe.Configuration.Encryption.DecryptionException.DecryptionException(string? message, System.Exception? innerException) -> void -Steeltoe.Configuration.Encryption.EncryptionConfigurationExtensions -Steeltoe.Configuration.Encryption.EncryptionServiceCollectionExtensions -Steeltoe.Configuration.Encryption.ITextDecryptor -Steeltoe.Configuration.Encryption.ITextDecryptor.Decrypt(string! fullCipher) -> string! -Steeltoe.Configuration.Encryption.ITextDecryptor.Decrypt(string! fullCipher, string! alias) -> string! +static Steeltoe.Configuration.Encryption.DecryptionConfigurationBuilderExtensions.AddDecryption(this Microsoft.Extensions.Configuration.IConfigurationBuilder! builder) -> Microsoft.Extensions.Configuration.IConfigurationBuilder! +static Steeltoe.Configuration.Encryption.DecryptionConfigurationBuilderExtensions.AddDecryption(this Microsoft.Extensions.Configuration.IConfigurationBuilder! builder, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> Microsoft.Extensions.Configuration.IConfigurationBuilder! +static Steeltoe.Configuration.Encryption.DecryptionConfigurationBuilderExtensions.AddDecryption(this Microsoft.Extensions.Configuration.IConfigurationBuilder! builder, Steeltoe.Configuration.Encryption.Cryptography.ITextDecryptor? textDecryptor, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> Microsoft.Extensions.Configuration.IConfigurationBuilder! +Steeltoe.Configuration.Encryption.Cryptography.DecryptionException +Steeltoe.Configuration.Encryption.Cryptography.DecryptionException.DecryptionException(string? message) -> void +Steeltoe.Configuration.Encryption.Cryptography.DecryptionException.DecryptionException(string? message, System.Exception? innerException) -> void +Steeltoe.Configuration.Encryption.Cryptography.ITextDecryptor +Steeltoe.Configuration.Encryption.Cryptography.ITextDecryptor.Decrypt(string! fullCipher) -> string! +Steeltoe.Configuration.Encryption.Cryptography.ITextDecryptor.Decrypt(string! fullCipher, string! alias) -> string! +Steeltoe.Configuration.Encryption.DecryptionConfigurationBuilderExtensions diff --git a/src/Configuration/src/Encryption/README.md b/src/Configuration/src/Encryption/README.md index 13b5519bb0..e6a76813fa 100644 --- a/src/Configuration/src/Encryption/README.md +++ b/src/Configuration/src/Encryption/README.md @@ -1,4 +1,4 @@ -# Configuration Encryption Resolver .NET Configuration Provider +# Encryption .NET Configuration Provider -This project contains an Encryption resolver configuration provider. +This project contains a configuration provider that decrypts encrypted values in configuration. For more information on how to use this component see the online [Steeltoe documentation](https://steeltoe.io/). diff --git a/src/Configuration/src/Encryption/Steeltoe.Configuration.Encryption.csproj b/src/Configuration/src/Encryption/Steeltoe.Configuration.Encryption.csproj index 3694589a84..0882656aca 100644 --- a/src/Configuration/src/Encryption/Steeltoe.Configuration.Encryption.csproj +++ b/src/Configuration/src/Encryption/Steeltoe.Configuration.Encryption.csproj @@ -1,8 +1,8 @@ net8.0 - Configuration provider for resolving property encryptions in configuration values - configuration;encryptions;spring boot + Configuration provider for decrypting encrypted configuration values + configuration;cryptography;decryption;spring boot true @@ -14,5 +14,6 @@ + diff --git a/src/Configuration/src/Kubernetes.ServiceBinding/ConfigurationBuilderExtensions.cs b/src/Configuration/src/Kubernetes.ServiceBinding/ConfigurationBuilderExtensions.cs index 6ba71ec777..904a608ead 100644 --- a/src/Configuration/src/Kubernetes.ServiceBinding/ConfigurationBuilderExtensions.cs +++ b/src/Configuration/src/Kubernetes.ServiceBinding/ConfigurationBuilderExtensions.cs @@ -87,7 +87,7 @@ public static IConfigurationBuilder AddKubernetesServiceBindings(this IConfigura ArgumentNullException.ThrowIfNull(serviceBindingsReader); ArgumentNullException.ThrowIfNull(loggerFactory); - if (!builder.Sources.OfType().Any()) + if (!builder.EnumerateSources().Any()) { var source = new KubernetesServiceBindingConfigurationSource(serviceBindingsReader) { diff --git a/src/Configuration/src/Placeholder/ConfigurationBuilderExtensions.cs b/src/Configuration/src/Placeholder/ConfigurationBuilderExtensions.cs deleted file mode 100644 index b107fa1482..0000000000 --- a/src/Configuration/src/Placeholder/ConfigurationBuilderExtensions.cs +++ /dev/null @@ -1,87 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information. - -using Microsoft.Extensions.Configuration; - -namespace Steeltoe.Configuration.Placeholder; - -internal static class ConfigurationBuilderExtensions -{ - /// - /// Finds the first configuration source of the specified type, scanning through any composite sources. - /// - /// - /// The type of to find. - /// - /// - /// The configuration builder to search in. - /// - public static TSource? FindConfigurationSource(this IConfigurationBuilder builder) - where TSource : class, IConfigurationSource - { - ArgumentNullException.ThrowIfNull(builder); - - return FindConfigurationSource(builder.Sources); - } - - public static TSource? FindConfigurationSource(this IEnumerable sources) - where TSource : class, IConfigurationSource - { - foreach (IConfigurationSource source in sources) - { - if (source is TSource matchingSource) - { - return matchingSource; - } - - if (source is PlaceholderResolverSource { Sources: not null } placeholder) - { - var nextSource = FindConfigurationSource(placeholder.Sources); - - if (nextSource != null) - { - return nextSource; - } - } - } - - return null; - } - - /// - /// Gets all configuration sources of the specified type, scanning through any composite sources. - /// - /// - /// The type of to find. - /// - /// - /// The configuration builder to search in. - /// - public static IEnumerable GetConfigurationSources(this IConfigurationBuilder builder) - where TSource : class, IConfigurationSource - { - ArgumentNullException.ThrowIfNull(builder); - - List sources = []; - AddConfigurationSources(builder.Sources, sources); - return sources; - } - - private static void AddConfigurationSources(IEnumerable sourcesToScan, List foundSources) - where TSource : class, IConfigurationSource - { - foreach (IConfigurationSource source in sourcesToScan) - { - if (source is TSource matchingSource) - { - foundSources.Add(matchingSource); - } - - if (source is PlaceholderResolverSource { Sources: not null } placeholder) - { - AddConfigurationSources(placeholder.Sources, foundSources); - } - } - } -} diff --git a/src/Configuration/src/Placeholder/ConfigurationView.cs b/src/Configuration/src/Placeholder/ConfigurationView.cs deleted file mode 100644 index a9f8dfec6a..0000000000 --- a/src/Configuration/src/Placeholder/ConfigurationView.cs +++ /dev/null @@ -1,119 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information. - -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Primitives; - -namespace Steeltoe.Configuration.Placeholder; - -/// -/// Specialized implementation of that does not call on providers. -/// -internal sealed class ConfigurationView : IConfigurationRoot -{ - private readonly IList _providers; - private readonly ConfigurationReloadToken _changeToken = new(); - - /// - /// Gets the s for this configuration. - /// - public IEnumerable Providers => _providers; - - /// - /// Gets or sets the value corresponding to a configuration key. - /// - /// - /// The configuration key. - /// - /// - /// The configuration value. - /// - public string? this[string key] - { - get => GetConfiguration(_providers, key); - set => SetConfiguration(_providers, key, value); - } - - /// - /// Initializes a new instance of the class from a list of providers. - /// - /// - /// The s for this configuration. - /// - public ConfigurationView(IList providers) - { - ArgumentNullException.ThrowIfNull(providers); - - _providers = providers; - } - - /// - /// Gets the immediate child subsections. - /// - /// - /// The children. - /// - public IEnumerable GetChildren() - { - return _providers.Aggregate(Enumerable.Empty(), (seed, source) => source.GetChildKeys(seed, null)).Distinct(StringComparer.OrdinalIgnoreCase) - .Select(GetSection); - } - - /// - /// Returns a that can be used to observe when this configuration is reloaded. - /// - /// - /// The . - /// - public IChangeToken GetReloadToken() - { - return _changeToken; - } - - /// - /// Gets a configuration subsection with the specified key. - /// - /// - /// This method will never return null. If no matching subsection is found with the specified key, an empty - /// will be returned. - /// - /// - /// The key of the configuration section. - /// - /// - /// The . - /// - public IConfigurationSection GetSection(string key) - { - return new ConfigurationSection(this, key); - } - - public void Reload() - { - // Intentionally left empty - this provider is readonly. - } - - private static string? GetConfiguration(IList providers, string key) - { - for (int index = providers.Count - 1; index >= 0; index--) - { - IConfigurationProvider provider = providers[index]; - - if (provider.TryGet(key, out string? value)) - { - return value; - } - } - - return null; - } - - private static void SetConfiguration(IList providers, string key, string? value) - { - foreach (IConfigurationProvider provider in providers) - { - provider.Set(key, value); - } - } -} diff --git a/src/Configuration/src/Placeholder/PlaceholderConfigurationBuilderExtensions.cs b/src/Configuration/src/Placeholder/PlaceholderConfigurationBuilderExtensions.cs new file mode 100644 index 0000000000..d111aad0a5 --- /dev/null +++ b/src/Configuration/src/Placeholder/PlaceholderConfigurationBuilderExtensions.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Steeltoe.Configuration.Placeholder; + +public static class PlaceholderConfigurationBuilderExtensions +{ + /// + /// Adds a configuration source to the that substitutes placeholders in configuration values. + /// + /// This method replaces all the existing s contained in the builder, taking ownership of them. The newly added source + /// then provides placeholder substitution of values stored in its inner configuration (built from the pre-existing sources). Typically, you will want to + /// add this configuration source as the last one, so that placeholder substitution is applied to all configuration values. + /// + /// + /// + /// The to add configuration to. + /// + /// + /// The incoming so that additional calls can be chained. + /// + public static IConfigurationBuilder AddPlaceholderResolver(this IConfigurationBuilder builder) + { + return AddPlaceholderResolver(builder, NullLoggerFactory.Instance); + } + + /// + /// Adds a configuration source to the that substitutes placeholders in configuration values. + /// + /// This method replaces all the existing s contained in the builder, taking ownership of them. The newly added source + /// then provides placeholder substitution of values stored in its inner configuration (built from the pre-existing sources). Typically, you will want to + /// add this configuration source as the last one, so that placeholder substitution is applied to all configuration values. + /// + /// + /// + /// The to add configuration to. + /// + /// + /// Used for internal logging. Pass to disable logging. + /// + /// + /// The incoming so that additional calls can be chained. + /// + public static IConfigurationBuilder AddPlaceholderResolver(this IConfigurationBuilder builder, ILoggerFactory loggerFactory) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(loggerFactory); + + var placeholderSource = new PlaceholderConfigurationSource(loggerFactory); + + foreach (IConfigurationSource source in builder.Sources) + { + placeholderSource.Sources.Add(source); + } + + builder.Sources.Clear(); + builder.Sources.Add(placeholderSource); + + return builder; + } +} diff --git a/src/Configuration/src/Placeholder/PlaceholderConfigurationExtensions.cs b/src/Configuration/src/Placeholder/PlaceholderConfigurationExtensions.cs deleted file mode 100644 index f919d8ccc3..0000000000 --- a/src/Configuration/src/Placeholder/PlaceholderConfigurationExtensions.cs +++ /dev/null @@ -1,160 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information. - -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; - -namespace Steeltoe.Configuration.Placeholder; - -public static class PlaceholderConfigurationExtensions -{ - /// - /// Adds a placeholder resolver configuration source to the . The placeholder resolver source will capture and wrap - /// all the existing sources contained in the builder. The newly created source will then replace the existing - /// sources and provide placeholder resolution for the configuration. Typically, you will want to add this configuration source as the last one so that - /// you wrap all applications' configuration sources with placeholder resolution. - /// - /// - /// The to add configuration to. - /// - /// - /// The incoming so that additional calls can be chained. - /// - public static IConfigurationBuilder AddPlaceholderResolver(this IConfigurationBuilder builder) - { - return AddPlaceholderResolver(builder, NullLoggerFactory.Instance); - } - - /// - /// Adds a placeholder resolver configuration source to the . The placeholder resolver source will capture and wrap - /// all the existing sources contained in the builder. The newly created source will then replace the existing - /// sources and provide placeholder resolution for the configuration. Typically, you will want to add this configuration source as the last one so that - /// you wrap all applications' configuration sources with placeholder resolution. - /// - /// - /// The to add configuration to. - /// - /// - /// Used for internal logging. Pass to disable logging. - /// - /// - /// The incoming so that additional calls can be chained. - /// - public static IConfigurationBuilder AddPlaceholderResolver(this IConfigurationBuilder builder, ILoggerFactory loggerFactory) - { - ArgumentNullException.ThrowIfNull(builder); - ArgumentNullException.ThrowIfNull(loggerFactory); - - if (!builder.Sources.OfType().Any()) - { - if (builder is IConfigurationRoot configuration) - { - var source = new PlaceholderResolverSource(configuration, loggerFactory); - builder.Add(source); - } - else - { - var source = new PlaceholderResolverSource(builder.Sources, loggerFactory); - builder.Sources.Clear(); - builder.Add(source); - } - } - - return builder; - } - - /// - /// Creates a new from a . The placeholder resolver will be created using the - /// existing configuration providers contained in the incoming configuration. This results in providing placeholder resolution for those configuration - /// sources. - /// - /// - /// The configuration to wrap. - /// - /// - /// A new configuration. - /// - public static IConfiguration AddPlaceholderResolver(this IConfiguration configuration) - { - return AddPlaceholderResolver(configuration, NullLoggerFactory.Instance); - } - - /// - /// Creates a new from a . The placeholder resolver will be created using the - /// existing configuration providers contained in the incoming configuration. This results in providing placeholder resolution for those configuration - /// sources. - /// - /// - /// The configuration to wrap. - /// - /// - /// Used for internal logging. Pass to disable logging. - /// - /// - /// A new configuration. - /// - public static IConfiguration AddPlaceholderResolver(this IConfiguration configuration, ILoggerFactory loggerFactory) - { - ArgumentNullException.ThrowIfNull(configuration); - ArgumentNullException.ThrowIfNull(loggerFactory); - - if (configuration is not IConfigurationRoot root) - { - throw new InvalidOperationException($"Configuration must implement '{typeof(IConfigurationRoot)}'."); - } - - if (root.Providers.Any(provider => provider is IPlaceholderResolverProvider)) - { - return configuration; - } - - return new ConfigurationRoot(new List - { - new PlaceholderResolverProvider(new List(root.Providers), loggerFactory) - }); - } - - /// - /// Adds a placeholder resolver configuration source to the . The placeholder resolver source will capture and wrap - /// all the existing sources contained in the builder. The newly created source will then replace the existing - /// sources and provide placeholder resolution for the configuration. Typically, you will want to add this configuration source as the last one so that - /// you wrap all applications' configuration sources with placeholder resolution. - /// - /// - /// The to configure. - /// - /// - /// The incoming so that additional calls can be chained. - /// - public static ConfigurationManager AddPlaceholderResolver(this ConfigurationManager configurationManager) - { - return AddPlaceholderResolver(configurationManager, NullLoggerFactory.Instance); - } - - /// - /// Adds a placeholder resolver configuration source to the . The placeholder resolver source will capture and wrap - /// all the existing sources contained in the builder. The newly created source will then replace the existing - /// sources and provide placeholder resolution for the configuration. Typically, you will want to add this configuration source as the last one so that - /// you wrap all applications' configuration sources with placeholder resolution. - /// - /// - /// The to configure. - /// - /// - /// Used for internal logging. Pass to disable logging. - /// - /// - /// The incoming so that additional calls can be chained. - /// - public static ConfigurationManager AddPlaceholderResolver(this ConfigurationManager configurationManager, ILoggerFactory loggerFactory) - { - ArgumentNullException.ThrowIfNull(configurationManager); - ArgumentNullException.ThrowIfNull(loggerFactory); - - ((IConfigurationBuilder)configurationManager).AddPlaceholderResolver(loggerFactory); - - return configurationManager; - } -} diff --git a/src/Configuration/src/Placeholder/PlaceholderConfigurationProvider.cs b/src/Configuration/src/Placeholder/PlaceholderConfigurationProvider.cs new file mode 100644 index 0000000000..1c4c341578 --- /dev/null +++ b/src/Configuration/src/Placeholder/PlaceholderConfigurationProvider.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Steeltoe.Configuration.Placeholder; + +internal sealed partial class PlaceholderConfigurationProvider : CompositeConfigurationProvider +{ + private readonly ILogger _logger; + private readonly PropertyPlaceholderHelper _propertyPlaceholderHelper; + + public PlaceholderConfigurationProvider(IList providers, ILoggerFactory loggerFactory) + : base(providers, loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + + ILogger placeholderHelperLogger = loggerFactory.CreateLogger(); + _propertyPlaceholderHelper = new PropertyPlaceholderHelper(placeholderHelperLogger); + } + + public override bool TryGet(string key, out string? value) + { + bool found = base.TryGet(key, out value); + + if (found) + { + string? replacementValue = _propertyPlaceholderHelper.ResolvePlaceholders(value, ConfigurationRoot); + + if (replacementValue != value) + { + LogReplacement(key, value, replacementValue); + value = replacementValue; + } + } + + return value != null; + } + + [LoggerMessage(Level = LogLevel.Trace, Message = "Replaced value '{OriginalValue}' at key '{Key}' with '{ReplacementValue}'.")] + private partial void LogReplacement(string key, string? originalValue, string? replacementValue); +} diff --git a/src/Configuration/src/Placeholder/PlaceholderConfigurationSource.cs b/src/Configuration/src/Placeholder/PlaceholderConfigurationSource.cs new file mode 100644 index 0000000000..0b6a35b2f9 --- /dev/null +++ b/src/Configuration/src/Placeholder/PlaceholderConfigurationSource.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Steeltoe.Configuration.Placeholder; + +internal sealed partial class PlaceholderConfigurationSource : ICompositeConfigurationSource +{ + private readonly ILoggerFactory _loggerFactory; + private readonly ILogger _logger; + + public IList Sources { get; } = new List(); + + public PlaceholderConfigurationSource(ILoggerFactory loggerFactory) + { + ArgumentNullException.ThrowIfNull(loggerFactory); + + _loggerFactory = loggerFactory; + _logger = loggerFactory.CreateLogger(); + } + + public IConfigurationProvider Build(IConfigurationBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); + + LogBuild(_logger, Sources.Count, builder.Properties.Count); + List providers = Sources.Select(source => source.Build(builder)).ToList(); + return new PlaceholderConfigurationProvider(providers, _loggerFactory); + } + + [LoggerMessage(Level = LogLevel.Trace, Message = "Build for {SourceCount} sources and {PropertyCount} properties.")] + private static partial void LogBuild(ILogger logger, int sourceCount, int propertyCount); +} diff --git a/src/Configuration/src/Placeholder/PlaceholderResolverProvider.cs b/src/Configuration/src/Placeholder/PlaceholderResolverProvider.cs deleted file mode 100644 index 6ad80875f1..0000000000 --- a/src/Configuration/src/Placeholder/PlaceholderResolverProvider.cs +++ /dev/null @@ -1,206 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information. - -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Primitives; -using Steeltoe.Common.Configuration; - -namespace Steeltoe.Configuration.Placeholder; - -/// -/// Configuration provider that resolves placeholders. A placeholder takes the form of: -/// -/// -internal sealed class PlaceholderResolverProvider : IPlaceholderResolverProvider, IDisposable -{ - private readonly PropertyPlaceholderHelper _propertyPlaceholderHelper; - private bool _isDisposed; - - public IList Providers { get; } = new List(); - public IList ResolvedKeys { get; } = new List(); - - /// - /// Gets the configuration this placeholder resolver wraps. - /// - public IConfigurationRoot? Configuration { get; private set; } - - /// - /// Initializes a new instance of the class. The new placeholder resolver wraps the provided configuration - /// root. - /// - /// - /// The configuration the provider uses when resolving placeholders. - /// - /// - /// Used for internal logging. Pass to disable logging. - /// - public PlaceholderResolverProvider(IConfigurationRoot root, ILoggerFactory loggerFactory) - { - ArgumentNullException.ThrowIfNull(root); - ArgumentNullException.ThrowIfNull(loggerFactory); - - Configuration = root; - - ILogger placeholderHelperLogger = loggerFactory.CreateLogger(); - _propertyPlaceholderHelper = new PropertyPlaceholderHelper(placeholderHelperLogger); - } - - /// - /// Initializes a new instance of the class. The new placeholder resolver wraps the provided configuration - /// providers. The will be created from these providers. - /// - /// - /// The configuration providers the resolver uses when resolving placeholders. - /// - /// - /// Used for internal logging. Pass to disable logging. - /// - public PlaceholderResolverProvider(IList providers, ILoggerFactory loggerFactory) - { - ArgumentNullException.ThrowIfNull(providers); - ArgumentNullException.ThrowIfNull(loggerFactory); - - Providers = providers; - - ILogger placeholderHelperLogger = loggerFactory.CreateLogger(); - _propertyPlaceholderHelper = new PropertyPlaceholderHelper(placeholderHelperLogger); - } - - /// - /// Tries to get a configuration value for the specified key. If the value is a placeholder, it will try to resolve the placeholder before returning it. - /// - /// - /// The configuration key. - /// - /// - /// When this method returns, contains the resolved configuration value, if a value for the specified key was found. - /// - /// - /// true if a value for the specified key was found, otherwise false. - /// - public bool TryGet(string key, out string? value) - { - ArgumentNullException.ThrowIfNull(key); - EnsureInitialized(); - - string? originalValue = Configuration![key]; - value = _propertyPlaceholderHelper.ResolvePlaceholders(originalValue, Configuration); - - if (value != originalValue && !ResolvedKeys.Contains(key)) - { - ResolvedKeys.Add(key); - } - - return value != null; - } - - /// - /// Sets a configuration value for the specified key. No placeholder resolution is performed. - /// - /// - /// The configuration key whose value to set. - /// - /// - /// The configuration value to set at the specified key. - /// - public void Set(string key, string? value) - { - ArgumentException.ThrowIfNullOrEmpty(key); - EnsureInitialized(); - - Configuration![key] = value; - } - - /// - /// Returns a change token that can be used to observe when this configuration is reloaded. - /// - /// - /// The change token. - /// - public IChangeToken GetReloadToken() - { - EnsureInitialized(); - - return Configuration!.GetReloadToken(); - } - - /// - /// Creates the from the providers, if it has not done so already, and calls on the - /// underlying configuration. - /// - public void Load() - { - EnsureInitialized(); - - Configuration!.Reload(); - } - - /// - /// Returns the immediate descendant configuration keys for a given parent path, based on this 's data and the set of keys - /// returned by all the preceding providers. - /// - /// - /// The child keys returned by the preceding providers for the same parent path. - /// - /// - /// The parent path. - /// - /// - /// The child keys. - /// - public IEnumerable GetChildKeys(IEnumerable earlierKeys, string? parentPath) - { - EnsureInitialized(); - - IConfiguration section = parentPath == null ? Configuration! : Configuration!.GetSection(parentPath); - IEnumerable children = section.GetChildren(); - - return children.Select(childSection => childSection.Key).Concat(earlierKeys).OrderBy(key => key, ConfigurationKeyComparer.Instance); - } - - private void EnsureInitialized() - { - ObjectDisposedException.ThrowIf(_isDisposed, this); - - Configuration ??= new ConfigurationRoot(Providers); - } - - public void Dispose() - { - if (!_isDisposed) - { - HashSet disposables = []; - - foreach (IConfigurationProvider provider in Providers) - { - if (provider is IDisposable disposable) - { - disposables.Add(disposable); - } - } - - if (Configuration != null) - { - foreach (IConfigurationProvider provider in Configuration.Providers) - { - if (provider is IDisposable disposable) - { - disposables.Add(disposable); - } - } - } - - foreach (IDisposable disposable in disposables) - { - disposable.Dispose(); - } - - _isDisposed = true; - } - } -} diff --git a/src/Configuration/src/Placeholder/PlaceholderResolverSource.cs b/src/Configuration/src/Placeholder/PlaceholderResolverSource.cs deleted file mode 100644 index 6061279738..0000000000 --- a/src/Configuration/src/Placeholder/PlaceholderResolverSource.cs +++ /dev/null @@ -1,92 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information. - -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; - -namespace Steeltoe.Configuration.Placeholder; - -/// -/// Configuration source used in creating a that resolves placeholders. A placeholder takes the form of: -/// -/// -internal sealed class PlaceholderResolverSource : IConfigurationSource -{ - private readonly IConfigurationRoot? _configuration; - - internal IList? Sources { get; } - internal ILoggerFactory LoggerFactory { get; } - - /// - /// Initializes a new instance of the class. - /// - /// - /// The configuration sources to use. - /// - /// - /// Used for internal logging. Pass to disable logging. - /// - public PlaceholderResolverSource(IList sources, ILoggerFactory loggerFactory) - { - ArgumentNullException.ThrowIfNull(sources); - ArgumentNullException.ThrowIfNull(loggerFactory); - - Sources = sources.ToList(); - LoggerFactory = loggerFactory; - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// The root configuration to use. - /// - /// - /// Used for internal logging. Pass to disable logging. - /// - public PlaceholderResolverSource(IConfigurationRoot root, ILoggerFactory loggerFactory) - { - ArgumentNullException.ThrowIfNull(root); - ArgumentNullException.ThrowIfNull(loggerFactory); - - _configuration = root; - LoggerFactory = loggerFactory; - } - - /// - /// Builds a from the specified builder. - /// - /// - /// Used to build providers from sources, in case a list of sources was provided instead of a configuration root. - /// - /// - /// The placeholder resolver provider. - /// - public IConfigurationProvider Build(IConfigurationBuilder builder) - { - ArgumentNullException.ThrowIfNull(builder); - - if (_configuration != null) - { - var configurationView = new ConfigurationView(_configuration.Providers.ToArray()); - return new PlaceholderResolverProvider(configurationView, LoggerFactory); - } - - List providers = []; - - if (Sources != null) - { - foreach (IConfigurationSource source in Sources) - { - IConfigurationProvider provider = source.Build(builder); - providers.Add(provider); - } - } - - return new PlaceholderResolverProvider(providers, LoggerFactory); - } -} diff --git a/src/Configuration/src/Placeholder/PlaceholderServiceCollectionExtensions.cs b/src/Configuration/src/Placeholder/PlaceholderServiceCollectionExtensions.cs deleted file mode 100644 index 9c00a4529b..0000000000 --- a/src/Configuration/src/Placeholder/PlaceholderServiceCollectionExtensions.cs +++ /dev/null @@ -1,62 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information. - -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; - -namespace Steeltoe.Configuration.Placeholder; - -public static class PlaceholderServiceCollectionExtensions -{ - /// - /// Creates a new using a which wraps the provided - /// . The new configuration will then be used to replace the current in the service container. All subsequent requests for - /// a will return the newly created providing placeholder resolution. - /// - /// - /// The to add services to. - /// - /// - /// The configuration the placeholder resolver will wrap. - /// - /// - /// The new configuration. - /// - public static IConfiguration ConfigurePlaceholderResolver(this IServiceCollection services, IConfiguration configuration) - { - return ConfigurePlaceholderResolver(services, configuration, NullLoggerFactory.Instance); - } - - /// - /// Creates a new using a which wraps the provided - /// . The new configuration will then be used to replace the current in the service container. All subsequent requests for - /// a will return the newly created providing placeholder resolution. - /// - /// - /// The to add services to. - /// - /// - /// The configuration the placeholder resolver will wrap. - /// - /// - /// Used for internal logging. Pass to disable logging. - /// - /// - /// The new configuration. - /// - public static IConfiguration ConfigurePlaceholderResolver(this IServiceCollection services, IConfiguration configuration, ILoggerFactory loggerFactory) - { - ArgumentNullException.ThrowIfNull(services); - ArgumentNullException.ThrowIfNull(configuration); - ArgumentNullException.ThrowIfNull(loggerFactory); - - IConfiguration newConfiguration = configuration.AddPlaceholderResolver(loggerFactory); - services.Replace(ServiceDescriptor.Singleton(typeof(IConfiguration), newConfiguration)); - - return newConfiguration; - } -} diff --git a/src/Common/src/Common/Configuration/PropertyPlaceHolderHelper.cs b/src/Configuration/src/Placeholder/PropertyPlaceHolderHelper.cs similarity index 50% rename from src/Common/src/Common/Configuration/PropertyPlaceHolderHelper.cs rename to src/Configuration/src/Placeholder/PropertyPlaceHolderHelper.cs index ff25db92ea..4cca8c32f1 100644 --- a/src/Common/src/Common/Configuration/PropertyPlaceHolderHelper.cs +++ b/src/Configuration/src/Placeholder/PropertyPlaceHolderHelper.cs @@ -7,12 +7,12 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; -namespace Steeltoe.Common.Configuration; +namespace Steeltoe.Configuration.Placeholder; /// /// Utility class for working with configuration values that have placeholders in them. A placeholder takes the form of: /// /// /// Note: This was "inspired" by the Spring class @@ -39,76 +39,40 @@ public PropertyPlaceholderHelper(ILogger logger) /// /// Replaces all placeholders of the form: with the corresponding value from - /// the supplied . + /// ${path:to:key?default_if_not_present} + /// ]]> with the corresponding value from the + /// supplied . /// - /// - /// The string containing one or more placeholders. + /// + /// A string that can contain a mix of literal text and (recursive) placeholder references. /// /// /// The configuration used for finding replacement values. /// /// - /// The supplied value, with the placeholders replaced inline. + /// The incoming text, with all placeholders replaced inline. Unknown placeholders remain as-is. /// - public string? ResolvePlaceholders(string? property, IConfiguration? configuration) + public string? ResolvePlaceholders(string? text, IConfiguration? configuration) { - return ParseStringValue(property, configuration, false, new HashSet()); + return ParseStringValue(text, configuration, new HashSet()); } - /// - /// Finds all placeholders of the form: , resolves them from other values in - /// the configuration, and returns a new dictionary to add to your configuration. - /// - /// - /// The configuration to use as both source and target for placeholder resolution. - /// - /// - /// A list of keys with resolved values. Add them to your with method 'AddInMemoryCollection'. - /// - public IDictionary GetResolvedConfigurationPlaceholders(IConfiguration configuration) - { - ArgumentNullException.ThrowIfNull(configuration); - - // setup a holding tank for resolved values - var resolvedValues = new Dictionary(); - var visitedPlaceholders = new HashSet(); - - // iterate all configuration entries where the value isn't null and contains both the prefix and suffix that identify placeholders - foreach ((string key, string? value) in configuration.AsEnumerable().Where(pair => - pair.Value != null && pair.Value.Contains(Prefix, StringComparison.Ordinal) && pair.Value.Contains(Suffix, StringComparison.Ordinal))) - { - _logger.LogTrace("Found a property placeholder '{Placeholder}' to resolve for key '{Key}", value, key); - resolvedValues.Add(key, ParseStringValue(value, configuration, true, visitedPlaceholders)); - } - - return resolvedValues; - } - - [return: NotNullIfNotNull(nameof(property))] - private string? ParseStringValue(string? property, IConfiguration? configuration, bool useEmptyStringIfNotFound, ISet visitedPlaceHolders) + [return: NotNullIfNotNull(nameof(text))] + private string? ParseStringValue(string? text, IConfiguration? configuration, ISet visitedPlaceholders) { - if (configuration == null) - { - return property; - } - - if (string.IsNullOrEmpty(property)) + if (configuration == null || string.IsNullOrEmpty(text)) { - return property; + return text; } - int startIndex = property.IndexOf(Prefix, StringComparison.Ordinal); + int startIndex = text.IndexOf(Prefix, StringComparison.Ordinal); if (startIndex == -1) { - return property; + return text; } - var result = new StringBuilder(property); + var result = new StringBuilder(text); while (startIndex != -1) { @@ -116,54 +80,37 @@ public PropertyPlaceholderHelper(ILogger logger) if (endIndex != -1) { - string placeholder = Substring(result, startIndex + Prefix.Length, endIndex); + string outerPlaceholder = Substring(result, startIndex + Prefix.Length, endIndex); - string originalPlaceholder = placeholder; - - if (!visitedPlaceHolders.Add(originalPlaceholder)) + if (!visitedPlaceholders.Add(outerPlaceholder)) { - throw new InvalidOperationException($"Found circular placeholder reference '{originalPlaceholder}' in property definitions."); + throw new InvalidOperationException($"Found circular placeholder reference '{outerPlaceholder}' in configuration."); } // Recursive invocation, parsing placeholders contained in the placeholder key. - placeholder = ParseStringValue(placeholder, configuration, useEmptyStringIfNotFound, visitedPlaceHolders); - - // Handle array references foo:bar[1]:baz format -> foo:bar:1:baz - string lookup = placeholder.Replace('[', ':').Replace("]", string.Empty, StringComparison.Ordinal); + string innerPlaceholder = ParseStringValue(outerPlaceholder, configuration, visitedPlaceholders); + PlaceholderExpression expression = PlaceholderExpression.Parse(innerPlaceholder); // Now obtain the value for the fully resolved key... - string? propertyValue = configuration[lookup]; + string? propertyValue = configuration[expression.Key]; + // Attempt to resolve as a Spring-compatible placeholder. if (propertyValue == null) { - int separatorIndex = placeholder.IndexOf(Separator, StringComparison.Ordinal); - - if (separatorIndex != -1) - { - string actualPlaceholder = placeholder[..separatorIndex]; - string defaultValue = placeholder[(separatorIndex + Separator.Length)..]; - propertyValue = configuration[actualPlaceholder] ?? defaultValue; - } - else if (useEmptyStringIfNotFound) - { - propertyValue = string.Empty; - } + // Replace Spring delimiters ('.') with dotnet-friendly delimiters (':') so Spring placeholders can also be resolved. + string springKey = expression.Key.Replace('.', ':'); + propertyValue = configuration[springKey]; } - // Attempt to resolve as a spring-compatible placeholder - if (propertyValue == null) - { - // Replace Spring delimiters ('.') with MS-friendly delimiters (':') so Spring placeholders can also be resolved - lookup = placeholder.Replace('.', ':'); - propertyValue = configuration[lookup]; - } + propertyValue ??= expression.DefaultValue; if (propertyValue != null) { - // Recursive invocation, parsing placeholders contained in these previously resolved placeholder value. - propertyValue = ParseStringValue(propertyValue, configuration, useEmptyStringIfNotFound, visitedPlaceHolders); + // Recursive invocation, parsing placeholders contained in this previously resolved placeholder value. + propertyValue = ParseStringValue(propertyValue, configuration, visitedPlaceholders); Replace(result, startIndex, endIndex + Suffix.Length, propertyValue); - _logger.LogDebug("Resolved placeholder '{Placeholder}'", placeholder); + + _logger.LogDebug("Resolved placeholder '{Placeholder}' to '{Value}'", innerPlaceholder, propertyValue); startIndex = IndexOf(result, Prefix, startIndex + propertyValue.Length); } else @@ -172,7 +119,7 @@ public PropertyPlaceholderHelper(ILogger logger) startIndex = IndexOf(result, Prefix, endIndex + Prefix.Length); } - visitedPlaceHolders.Remove(originalPlaceholder); + visitedPlaceholders.Remove(outerPlaceholder); } else { @@ -254,4 +201,43 @@ private static string Substring(StringBuilder builder, int start, int end) { return builder.ToString()[start..end]; } + + private readonly struct PlaceholderExpression(string key, string? defaultValue) : IEquatable + { + public string Key { get; } = key; + public string? DefaultValue { get; } = defaultValue; + + public static PlaceholderExpression Parse(string text) + { + ArgumentNullException.ThrowIfNull(text); + + // Convert array references, for example: foo:bar[1]:baz -> foo:bar:1:baz + string normalizedText = text.Replace('[', ':').Replace("]", string.Empty, StringComparison.Ordinal); + + // Split into key and optional default value, for example: path:to:key?default -> path:to:key and default + string[] parts = normalizedText.Split(Separator, 2); + + if (parts.Length == 2) + { + return new PlaceholderExpression(parts[0], parts[1]); + } + + return new PlaceholderExpression(parts[0], null); + } + + public bool Equals(PlaceholderExpression other) + { + return Key == other.Key && DefaultValue == other.DefaultValue; + } + + public override bool Equals(object? obj) + { + return obj is PlaceholderExpression other && Equals(other); + } + + public override int GetHashCode() + { + return HashCode.Combine(Key, DefaultValue); + } + } } diff --git a/src/Configuration/src/Placeholder/PublicAPI.Unshipped.txt b/src/Configuration/src/Placeholder/PublicAPI.Unshipped.txt index ca1430c18a..441cd66991 100644 --- a/src/Configuration/src/Placeholder/PublicAPI.Unshipped.txt +++ b/src/Configuration/src/Placeholder/PublicAPI.Unshipped.txt @@ -1,11 +1,4 @@ #nullable enable -static Steeltoe.Configuration.Placeholder.PlaceholderConfigurationExtensions.AddPlaceholderResolver(this Microsoft.Extensions.Configuration.ConfigurationManager! configurationManager) -> Microsoft.Extensions.Configuration.ConfigurationManager! -static Steeltoe.Configuration.Placeholder.PlaceholderConfigurationExtensions.AddPlaceholderResolver(this Microsoft.Extensions.Configuration.ConfigurationManager! configurationManager, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> Microsoft.Extensions.Configuration.ConfigurationManager! -static Steeltoe.Configuration.Placeholder.PlaceholderConfigurationExtensions.AddPlaceholderResolver(this Microsoft.Extensions.Configuration.IConfiguration! configuration) -> Microsoft.Extensions.Configuration.IConfiguration! -static Steeltoe.Configuration.Placeholder.PlaceholderConfigurationExtensions.AddPlaceholderResolver(this Microsoft.Extensions.Configuration.IConfiguration! configuration, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> Microsoft.Extensions.Configuration.IConfiguration! -static Steeltoe.Configuration.Placeholder.PlaceholderConfigurationExtensions.AddPlaceholderResolver(this Microsoft.Extensions.Configuration.IConfigurationBuilder! builder) -> Microsoft.Extensions.Configuration.IConfigurationBuilder! -static Steeltoe.Configuration.Placeholder.PlaceholderConfigurationExtensions.AddPlaceholderResolver(this Microsoft.Extensions.Configuration.IConfigurationBuilder! builder, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> Microsoft.Extensions.Configuration.IConfigurationBuilder! -static Steeltoe.Configuration.Placeholder.PlaceholderServiceCollectionExtensions.ConfigurePlaceholderResolver(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfiguration! configuration) -> Microsoft.Extensions.Configuration.IConfiguration! -static Steeltoe.Configuration.Placeholder.PlaceholderServiceCollectionExtensions.ConfigurePlaceholderResolver(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfiguration! configuration, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> Microsoft.Extensions.Configuration.IConfiguration! -Steeltoe.Configuration.Placeholder.PlaceholderConfigurationExtensions -Steeltoe.Configuration.Placeholder.PlaceholderServiceCollectionExtensions +static Steeltoe.Configuration.Placeholder.PlaceholderConfigurationBuilderExtensions.AddPlaceholderResolver(this Microsoft.Extensions.Configuration.IConfigurationBuilder! builder) -> Microsoft.Extensions.Configuration.IConfigurationBuilder! +static Steeltoe.Configuration.Placeholder.PlaceholderConfigurationBuilderExtensions.AddPlaceholderResolver(this Microsoft.Extensions.Configuration.IConfigurationBuilder! builder, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> Microsoft.Extensions.Configuration.IConfigurationBuilder! +Steeltoe.Configuration.Placeholder.PlaceholderConfigurationBuilderExtensions diff --git a/src/Configuration/src/RandomValue/RandomValueConfigurationBuilderExtensions.cs b/src/Configuration/src/RandomValue/RandomValueConfigurationBuilderExtensions.cs index c5e29d3d1a..873a36d471 100644 --- a/src/Configuration/src/RandomValue/RandomValueConfigurationBuilderExtensions.cs +++ b/src/Configuration/src/RandomValue/RandomValueConfigurationBuilderExtensions.cs @@ -78,7 +78,7 @@ public static IConfigurationBuilder AddRandomValueSource(this IConfigurationBuil ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(loggerFactory); - if (!builder.Sources.OfType().Any()) + if (!builder.EnumerateSources().Any()) { if (prefix != null && !prefix.EndsWith(':')) { diff --git a/src/Configuration/src/RandomValue/RandomValueProvider.cs b/src/Configuration/src/RandomValue/RandomValueProvider.cs index fea3c98f1d..ec19f1332f 100644 --- a/src/Configuration/src/RandomValue/RandomValueProvider.cs +++ b/src/Configuration/src/RandomValue/RandomValueProvider.cs @@ -18,7 +18,7 @@ internal sealed class RandomValueProvider : ConfigurationProvider private readonly string _prefix; /// - /// Initializes a new instance of the class. The new placeholder resolver wraps the provided configuration. + /// Initializes a new instance of the class. /// /// /// Prefix to use to match random number keys. @@ -75,7 +75,7 @@ public override bool TryGet(string key, out string? value) /// public override void Set(string key, string? value) { - // for future use + // Intentionally left empty. } /// diff --git a/src/Configuration/src/RandomValue/RandomValueSource.cs b/src/Configuration/src/RandomValue/RandomValueSource.cs index be23d9f9b7..6b0810fabe 100644 --- a/src/Configuration/src/RandomValue/RandomValueSource.cs +++ b/src/Configuration/src/RandomValue/RandomValueSource.cs @@ -14,9 +14,9 @@ namespace Steeltoe.Configuration.RandomValue; internal sealed class RandomValueSource : IConfigurationSource { private const string DefaultPrefix = "random:"; + private readonly ILoggerFactory _loggerFactory; internal string Prefix { get; } - internal ILoggerFactory LoggerFactory { get; } /// /// Initializes a new instance of the class. @@ -43,7 +43,7 @@ public RandomValueSource(string? prefix, ILoggerFactory loggerFactory) ArgumentNullException.ThrowIfNull(loggerFactory); Prefix = prefix ?? DefaultPrefix; - LoggerFactory = loggerFactory; + _loggerFactory = loggerFactory; } /// @@ -57,6 +57,6 @@ public RandomValueSource(string? prefix, ILoggerFactory loggerFactory) /// public IConfigurationProvider Build(IConfigurationBuilder builder) { - return new RandomValueProvider(Prefix, LoggerFactory); + return new RandomValueProvider(Prefix, _loggerFactory); } } diff --git a/src/Configuration/src/RandomValue/Steeltoe.Configuration.RandomValue.csproj b/src/Configuration/src/RandomValue/Steeltoe.Configuration.RandomValue.csproj index 734ffb4173..88735c5401 100644 --- a/src/Configuration/src/RandomValue/Steeltoe.Configuration.RandomValue.csproj +++ b/src/Configuration/src/RandomValue/Steeltoe.Configuration.RandomValue.csproj @@ -10,5 +10,6 @@ + diff --git a/src/Configuration/src/SpringBoot/SpringBootConfigurationBuilderExtensions.cs b/src/Configuration/src/SpringBoot/SpringBootConfigurationBuilderExtensions.cs index 85b5c770f1..664c8093da 100644 --- a/src/Configuration/src/SpringBoot/SpringBootConfigurationBuilderExtensions.cs +++ b/src/Configuration/src/SpringBoot/SpringBootConfigurationBuilderExtensions.cs @@ -43,7 +43,7 @@ public static IConfigurationBuilder AddSpringBootFromEnvironmentVariable(this IC ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(loggerFactory); - if (!builder.Sources.OfType().Any()) + if (!builder.EnumerateSources().Any()) { builder.Add(new SpringBootEnvironmentVariableSource()); } @@ -92,7 +92,7 @@ public static IConfigurationBuilder AddSpringBootFromCommandLine(this IConfigura ArgumentNullException.ThrowIfNull(configuration); ArgumentNullException.ThrowIfNull(loggerFactory); - if (!builder.Sources.OfType().Any()) + if (!builder.EnumerateSources().Any()) { builder.Add(new SpringBootCommandLineSource(configuration)); } diff --git a/src/Configuration/src/SpringBoot/Steeltoe.Configuration.SpringBoot.csproj b/src/Configuration/src/SpringBoot/Steeltoe.Configuration.SpringBoot.csproj index ce8345bb10..89fc961a14 100644 --- a/src/Configuration/src/SpringBoot/Steeltoe.Configuration.SpringBoot.csproj +++ b/src/Configuration/src/SpringBoot/Steeltoe.Configuration.SpringBoot.csproj @@ -15,5 +15,6 @@ + diff --git a/src/Configuration/test/CloudFoundry.Test/CloudFoundryConfigurationBuilderExtensionsTest.cs b/src/Configuration/test/CloudFoundry.Test/CloudFoundryConfigurationBuilderExtensionsTest.cs index 0830707502..57164a7cd6 100644 --- a/src/Configuration/test/CloudFoundry.Test/CloudFoundryConfigurationBuilderExtensionsTest.cs +++ b/src/Configuration/test/CloudFoundry.Test/CloudFoundryConfigurationBuilderExtensionsTest.cs @@ -12,22 +12,9 @@ public sealed class CloudFoundryConfigurationBuilderExtensionsTest public void AddCloudFoundry_AddsCloudFoundrySourceToSourcesList() { var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddCloudFoundry(); - CloudFoundryConfigurationSource? cloudSource = null; - - foreach (IConfigurationSource source in configurationBuilder.Sources) - { - cloudSource = source as CloudFoundryConfigurationSource; - - if (cloudSource != null) - { - break; - } - } - - Assert.NotNull(cloudSource); + configurationBuilder.EnumerateSources().Should().HaveCount(1); } [Fact] diff --git a/src/Configuration/test/CloudFoundry.Test/CloudFoundryHostBuilderExtensionsTest.cs b/src/Configuration/test/CloudFoundry.Test/CloudFoundryHostBuilderExtensionsTest.cs index 978ce9f27c..606d431a43 100644 --- a/src/Configuration/test/CloudFoundry.Test/CloudFoundryHostBuilderExtensionsTest.cs +++ b/src/Configuration/test/CloudFoundry.Test/CloudFoundryHostBuilderExtensionsTest.cs @@ -24,7 +24,8 @@ public void HostAddCloudFoundryConfiguration_Adds() var instanceInfo = host.Services.GetRequiredService(); Assert.IsAssignableFrom(instanceInfo); var configurationRoot = (IConfigurationRoot)host.Services.GetRequiredService(); - Assert.Contains(configurationRoot.Providers, provider => provider is CloudFoundryConfigurationProvider); + + Assert.Single(configurationRoot.EnumerateProviders()); } [Fact] @@ -35,7 +36,8 @@ public void WebHostAddCloudFoundryConfiguration_Adds() using IWebHost host = hostbuilder.Build(); var configurationRoot = (IConfigurationRoot)host.Services.GetRequiredService(); - Assert.Contains(configurationRoot.Providers, provider => provider is CloudFoundryConfigurationProvider); + + Assert.Single(configurationRoot.EnumerateProviders()); } [Fact] @@ -46,6 +48,7 @@ public async Task WebApplicationAddCloudFoundryConfiguration_Adds() await using WebApplication host = hostbuilder.Build(); var configurationRoot = (IConfigurationRoot)host.Services.GetRequiredService(); - Assert.Contains(configurationRoot.Providers, provider => provider is CloudFoundryConfigurationProvider); + + Assert.Single(configurationRoot.EnumerateProviders()); } } diff --git a/src/Configuration/test/ConfigServer.Integration.Test/EncryptionBeforePlaceholderStartup.cs b/src/Configuration/test/ConfigServer.Integration.Test/EncryptionBeforePlaceholderStartup.cs deleted file mode 100644 index 8e4fd42027..0000000000 --- a/src/Configuration/test/ConfigServer.Integration.Test/EncryptionBeforePlaceholderStartup.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information. - -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Steeltoe.Configuration.Encryption; -using Steeltoe.Configuration.Placeholder; - -namespace Steeltoe.Configuration.ConfigServer.Integration.Test; - -public sealed class EncryptionBeforePlaceholderStartup -{ - private readonly IConfiguration _configuration; - - public EncryptionBeforePlaceholderStartup(IConfiguration configuration) - { - ArgumentNullException.ThrowIfNull(configuration); - - _configuration = configuration; - } - - public void ConfigureServices(IServiceCollection services) - { - IConfiguration configuration = services.ConfigureEncryptionResolver(_configuration); - services.ConfigurePlaceholderResolver(configuration); - } - - public void Configure(IApplicationBuilder app) - { - } -} diff --git a/src/Configuration/test/ConfigServer.Integration.Test/PlaceholderBeforeEncryptionStartup.cs b/src/Configuration/test/ConfigServer.Integration.Test/PlaceholderBeforeEncryptionStartup.cs deleted file mode 100644 index 25167efd3b..0000000000 --- a/src/Configuration/test/ConfigServer.Integration.Test/PlaceholderBeforeEncryptionStartup.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information. - -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Steeltoe.Configuration.Encryption; -using Steeltoe.Configuration.Placeholder; - -namespace Steeltoe.Configuration.ConfigServer.Integration.Test; - -public sealed class PlaceholderBeforeEncryptionStartup -{ - private readonly IConfiguration _configuration; - - public PlaceholderBeforeEncryptionStartup(IConfiguration configuration) - { - ArgumentNullException.ThrowIfNull(configuration); - - _configuration = configuration; - } - - public void ConfigureServices(IServiceCollection services) - { - IConfiguration configuration = services.ConfigurePlaceholderResolver(_configuration); - services.ConfigureEncryptionResolver(configuration); - } - - public void Configure(IApplicationBuilder app) - { - } -} diff --git a/src/Configuration/test/ConfigServer.Integration.Test/NestedPlaceholderEncryptionIntegrationTest.cs b/src/Configuration/test/ConfigServer.Integration.Test/PlaceholderEncryptionIntegrationTest.cs similarity index 62% rename from src/Configuration/test/ConfigServer.Integration.Test/NestedPlaceholderEncryptionIntegrationTest.cs rename to src/Configuration/test/ConfigServer.Integration.Test/PlaceholderEncryptionIntegrationTest.cs index b4c207ba30..23bcfa01fe 100644 --- a/src/Configuration/test/ConfigServer.Integration.Test/NestedPlaceholderEncryptionIntegrationTest.cs +++ b/src/Configuration/test/ConfigServer.Integration.Test/PlaceholderEncryptionIntegrationTest.cs @@ -3,18 +3,18 @@ // See the LICENSE file in the project root for more information. using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Steeltoe.Common.TestResources; +using Steeltoe.Configuration.Encryption; +using Steeltoe.Configuration.Placeholder; namespace Steeltoe.Configuration.ConfigServer.Integration.Test; -public sealed class NestedPlaceholderEncryptionIntegrationTest +public sealed class PlaceholderEncryptionIntegrationTest { [Fact] - [Trait("Category", "Integration")] - public void ResolveOuterEncryptionInnerPlaceholder_returnsDecryptedValuesInPlaceholder() + public void PlaceholderInsideDecryptionProvider_ReturnsDecryptedValuesInPlaceholder() { var settings = new Dictionary { @@ -32,23 +32,24 @@ public void ResolveOuterEncryptionInnerPlaceholder_returnsDecryptedValuesInPlace { "placeholder", "${encrypted}" } }; - var builder = new ConfigurationBuilder(); - builder.AddInMemoryCollection(settings); - IConfigurationRoot configuration1 = builder.Build(); + IWebHostBuilder hostBuilder = TestWebHostBuilderFactory.Create(); - IWebHostBuilder hostBuilder = TestWebHostBuilderFactory.Create().UseStartup().UseConfiguration(configuration1); + hostBuilder.ConfigureAppConfiguration(configurationBuilder => + { + configurationBuilder.AddInMemoryCollection(settings); + configurationBuilder.AddPlaceholderResolver(); + configurationBuilder.AddDecryption(); + }); - using var server = new TestServer(hostBuilder); - var configuration2 = server.Services.GetRequiredService(); - Assert.NotSame(configuration1, configuration2); + using IWebHost host = hostBuilder.Build(); + var configuration = host.Services.GetRequiredService(); - Assert.Equal("encrypt the world", configuration2["encrypted"]); - Assert.Equal("encrypt the world", configuration2["placeholder"]); + configuration["encrypted"].Should().Be("encrypt the world"); + configuration["placeholder"].Should().Be("encrypt the world"); } [Fact] - [Trait("Category", "Integration")] - public void ResolveOuterPlaceholderInnerEncryption_returnsDecryptedValuesInPlaceholder() + public void DecryptionInsidePlaceholderProvider_ReturnsDecryptedValuesInPlaceholder() { var settings = new Dictionary { @@ -66,17 +67,19 @@ public void ResolveOuterPlaceholderInnerEncryption_returnsDecryptedValuesInPlace { "placeholder", "${encrypted}" } }; - var builder = new ConfigurationBuilder(); - builder.AddInMemoryCollection(settings); - IConfigurationRoot configuration1 = builder.Build(); + IWebHostBuilder hostBuilder = TestWebHostBuilderFactory.Create(); - IWebHostBuilder hostBuilder = TestWebHostBuilderFactory.Create().UseStartup().UseConfiguration(configuration1); + hostBuilder.ConfigureAppConfiguration(configurationBuilder => + { + configurationBuilder.AddInMemoryCollection(settings); + configurationBuilder.AddDecryption(); + configurationBuilder.AddPlaceholderResolver(); + }); - using var server = new TestServer(hostBuilder); - var configuration2 = server.Services.GetRequiredService(); - Assert.NotSame(configuration1, configuration2); + using IWebHost host = hostBuilder.Build(); + var configuration = host.Services.GetRequiredService(); - Assert.Equal("encrypt the world", configuration2["encrypted"]); - Assert.Equal("encrypt the world", configuration2["placeholder"]); + configuration["encrypted"].Should().Be("encrypt the world"); + configuration["placeholder"].Should().Be("encrypt the world"); } } diff --git a/src/Configuration/test/ConfigServer.Integration.Test/Steeltoe.Configuration.ConfigServer.Integration.Test.csproj b/src/Configuration/test/ConfigServer.Integration.Test/Steeltoe.Configuration.ConfigServer.Integration.Test.csproj index 49ef8666bd..4a18628987 100644 --- a/src/Configuration/test/ConfigServer.Integration.Test/Steeltoe.Configuration.ConfigServer.Integration.Test.csproj +++ b/src/Configuration/test/ConfigServer.Integration.Test/Steeltoe.Configuration.ConfigServer.Integration.Test.csproj @@ -15,5 +15,6 @@ + diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsCoreTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsCoreTest.cs index 83df55adfc..25a41fda54 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsCoreTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsCoreTest.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging; using Steeltoe.Common.TestResources; using Steeltoe.Common.TestResources.IO; +using Steeltoe.Configuration.Placeholder; namespace Steeltoe.Configuration.ConfigServer.Test; @@ -24,7 +25,7 @@ public void AddConfigServer_AddsConfigServerProviderToProvidersList() configurationBuilder.AddConfigServer(); IConfigurationRoot configurationRoot = configurationBuilder.Build(); - ConfigServerConfigurationProvider? configServerProvider = configurationRoot.Providers.OfType().SingleOrDefault(); + ConfigServerConfigurationProvider? configServerProvider = configurationRoot.EnumerateProviders().SingleOrDefault(); Assert.NotNull(configServerProvider); } @@ -92,7 +93,7 @@ public void AddConfigServer_JsonAppSettingsConfiguresClient() configurationBuilder.AddConfigServer(); IConfigurationRoot configurationRoot = configurationBuilder.Build(); - ConfigServerConfigurationProvider? configServerProvider = configurationRoot.Providers.OfType().SingleOrDefault(); + ConfigServerConfigurationProvider? configServerProvider = configurationRoot.EnumerateProviders().SingleOrDefault(); Assert.NotNull(configServerProvider); ConfigServerClientOptions options = configServerProvider.ClientOptions; @@ -146,7 +147,7 @@ public void AddConfigServer_ValidateCertificates_DisablesCertValidation() configurationBuilder.AddConfigServer(); IConfigurationRoot configurationRoot = configurationBuilder.Build(); - ConfigServerConfigurationProvider? configServerProvider = configurationRoot.Providers.OfType().SingleOrDefault(); + ConfigServerConfigurationProvider? configServerProvider = configurationRoot.EnumerateProviders().SingleOrDefault(); Assert.NotNull(configServerProvider); ConfigServerClientOptions options = configServerProvider.ClientOptions; @@ -180,7 +181,7 @@ public void AddConfigServer_Validate_Certificates_DisablesCertValidation() configurationBuilder.AddConfigServer(); IConfigurationRoot configurationRoot = configurationBuilder.Build(); - ConfigServerConfigurationProvider? configServerProvider = configurationRoot.Providers.OfType().SingleOrDefault(); + ConfigServerConfigurationProvider? configServerProvider = configurationRoot.EnumerateProviders().SingleOrDefault(); Assert.NotNull(configServerProvider); ConfigServerClientOptions options = configServerProvider.ClientOptions; @@ -219,7 +220,7 @@ public void AddConfigServer_XmlAppSettingsConfiguresClient() configurationBuilder.AddConfigServer(); IConfigurationRoot configurationRoot = configurationBuilder.Build(); - ConfigServerConfigurationProvider? configServerProvider = configurationRoot.Providers.OfType().SingleOrDefault(); + ConfigServerConfigurationProvider? configServerProvider = configurationRoot.EnumerateProviders().SingleOrDefault(); Assert.NotNull(configServerProvider); ConfigServerClientOptions options = configServerProvider.ClientOptions; @@ -261,7 +262,7 @@ public void AddConfigServer_IniAppSettingsConfiguresClient() configurationBuilder.AddConfigServer(); IConfigurationRoot configurationRoot = configurationBuilder.Build(); - ConfigServerConfigurationProvider? configServerProvider = configurationRoot.Providers.OfType().SingleOrDefault(); + ConfigServerConfigurationProvider? configServerProvider = configurationRoot.EnumerateProviders().SingleOrDefault(); Assert.NotNull(configServerProvider); ConfigServerClientOptions options = configServerProvider.ClientOptions; @@ -301,7 +302,7 @@ public void AddConfigServer_CommandLineAppSettingsConfiguresClient() configurationBuilder.AddConfigServer(); IConfigurationRoot configurationRoot = configurationBuilder.Build(); - ConfigServerConfigurationProvider? configServerProvider = configurationRoot.Providers.OfType().SingleOrDefault(); + ConfigServerConfigurationProvider? configServerProvider = configurationRoot.EnumerateProviders().SingleOrDefault(); Assert.NotNull(configServerProvider); ConfigServerClientOptions options = configServerProvider.ClientOptions; @@ -319,7 +320,7 @@ public void AddConfigServer_CommandLineAppSettingsConfiguresClient() } [Fact] - public void AddConfigServer_HandlesPlaceHolders() + public void AddConfigServer_SubstitutesPlaceholders() { const string appsettings = """ { @@ -356,10 +357,11 @@ public void AddConfigServer_HandlesPlaceHolders() var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.SetBasePath(directory); configurationBuilder.AddJsonFile(fileName); + configurationBuilder.AddPlaceholderResolver(); configurationBuilder.AddConfigServer(); IConfigurationRoot configurationRoot = configurationBuilder.Build(); - ConfigServerConfigurationProvider? configServerProvider = configurationRoot.Providers.OfType().SingleOrDefault(); + ConfigServerConfigurationProvider? configServerProvider = configurationRoot.EnumerateProviders().SingleOrDefault(); Assert.NotNull(configServerProvider); ConfigServerClientOptions options = configServerProvider.ClientOptions; @@ -459,7 +461,7 @@ public void AddConfigServer_WithCloudfoundryEnvironment_ConfiguresClientCorrectl configurationBuilder.AddConfigServer(); IConfigurationRoot configurationRoot = configurationBuilder.Build(); - ConfigServerConfigurationProvider? configServerProvider = configurationRoot.Providers.OfType().SingleOrDefault(); + ConfigServerConfigurationProvider? configServerProvider = configurationRoot.EnumerateProviders().SingleOrDefault(); Assert.NotNull(configServerProvider); ConfigServerClientOptions options = configServerProvider.ClientOptions; @@ -567,7 +569,7 @@ public void AddConfigServer_WithCloudfoundryEnvironmentSCS3_ConfiguresClientCorr configurationBuilder.AddConfigServer(); IConfigurationRoot configurationRoot = configurationBuilder.Build(); - ConfigServerConfigurationProvider? configServerProvider = configurationRoot.Providers.OfType().SingleOrDefault(); + ConfigServerConfigurationProvider? configServerProvider = configurationRoot.EnumerateProviders().SingleOrDefault(); Assert.NotNull(configServerProvider); ConfigServerClientOptions options = configServerProvider.ClientOptions; diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsTest.cs index d21374a0e2..1fc178c4c8 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsTest.cs @@ -6,9 +6,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Steeltoe.Common.TestResources; -using Steeltoe.Common.TestResources.IO; using Steeltoe.Configuration.CloudFoundry; -using Steeltoe.Configuration.Placeholder; namespace Steeltoe.Configuration.ConfigServer.Test; @@ -135,7 +133,7 @@ public void AddConfigServer_WithConfigServerCertificate_AddsConfigServerSourceWi configurationBuilder.AddConfigServer(options, NullLoggerFactory.Instance); _ = configurationBuilder.Build(); - var source = configurationBuilder.FindConfigurationSource(); + ConfigServerConfigurationSource? source = configurationBuilder.EnumerateSources().SingleOrDefault(); Assert.NotNull(source); Assert.NotNull(source.DefaultOptions.ClientCertificate); } @@ -159,7 +157,7 @@ public void AddConfigServer_WithGlobalCertificate_AddsConfigServerSourceWithCert configurationBuilder.AddConfigServer(options, NullLoggerFactory.Instance); _ = configurationBuilder.Build(); - var source = configurationBuilder.FindConfigurationSource(); + ConfigServerConfigurationSource? source = configurationBuilder.EnumerateSources().SingleOrDefault(); Assert.NotNull(source); Assert.NotNull(source.DefaultOptions.ClientCertificate); } @@ -170,7 +168,7 @@ public void AddConfigServer_AddsConfigServerSourceToList() var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.AddConfigServer(); - ConfigServerConfigurationSource? configServerSource = configurationBuilder.Sources.OfType().SingleOrDefault(); + ConfigServerConfigurationSource? configServerSource = configurationBuilder.EnumerateSources().SingleOrDefault(); Assert.NotNull(configServerSource); } @@ -190,247 +188,6 @@ public void AddConfigServer_WithLoggerFactorySucceeds() "DBUG Steeltoe.Configuration.ConfigServer.ConfigServerConfigurationProvider: Fetching configuration from server at: http://localhost:8888/"); } - [Fact] - public void AddConfigServer_JsonAppSettingsConfiguresClient() - { - const string appsettings = """ - { - "spring": { - "application": { - "name": "myName" - }, - "cloud": { - "config": { - "uri": "https://user:password@foo.com:9999", - "enabled": false, - "failFast": false, - "label": "myLabel", - "username": "myUsername", - "password": "myPassword", - "timeout": 10000, - "token" : "vaulttoken", - "retry": { - "enabled":"false", - "initialInterval":55555, - "maxInterval": 55555, - "multiplier": 5.5, - "maxAttempts": 55555 - } - } - } - } - } - """; - - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile("appsettings.json", appsettings); - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); - - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.SetBasePath(directory); - configurationBuilder.AddJsonFile(fileName); - configurationBuilder.AddConfigServer(); - IConfigurationRoot configurationRoot = configurationBuilder.Build(); - - ConfigServerConfigurationProvider? configServerProvider = configurationRoot.Providers.OfType().SingleOrDefault(); - Assert.NotNull(configServerProvider); - - ConfigServerClientOptions options = configServerProvider.ClientOptions; - Assert.False(options.Enabled); - Assert.False(options.FailFast); - Assert.Equal("https://user:password@foo.com:9999", options.Uri); - Assert.Equal("Production", options.Environment); - Assert.Equal("myName", options.Name); - Assert.Equal("myLabel", options.Label); - Assert.Equal("myUsername", options.Username); - Assert.Equal("myPassword", options.Password); - Assert.False(options.Retry.Enabled); - Assert.Equal(55555, options.Retry.MaxAttempts); - Assert.Equal(55555, options.Retry.InitialInterval); - Assert.Equal(55555, options.Retry.MaxInterval); - Assert.Equal(5.5, options.Retry.Multiplier); - Assert.Equal(10000, options.Timeout); - Assert.Equal("vaulttoken", options.Token); - } - - [Fact] - public void AddConfigServer_XmlAppSettingsConfiguresClient() - { - const string appsettings = """ - - - - - https://foo.com:9999 - false - false - - myName - myUsername - myPassword - - - - - """; - - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile("appsettings.json", appsettings); - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); - - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.SetBasePath(directory); - configurationBuilder.AddXmlFile(fileName); - configurationBuilder.AddConfigServer(); - IConfigurationRoot configurationRoot = configurationBuilder.Build(); - - ConfigServerConfigurationProvider? configServerProvider = configurationRoot.Providers.OfType().FirstOrDefault(); - Assert.NotNull(configServerProvider); - - ConfigServerClientOptions options = configServerProvider.ClientOptions; - Assert.False(options.Enabled); - Assert.False(options.FailFast); - Assert.Equal("https://foo.com:9999", options.Uri); - Assert.Equal("Production", options.Environment); - Assert.Equal("myName", options.Name); - Assert.Equal("myLabel", options.Label); - Assert.Equal("myUsername", options.Username); - Assert.Equal("myPassword", options.Password); - } - - [Fact] - public void AddConfigServer_IniAppSettingsConfiguresClient() - { - const string appsettings = """ - [spring:cloud:config] - uri=https://foo.com:9999 - enabled=false - failFast=false - label=myLabel - name=myName - username=myUsername - password=myPassword - """; - - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile("appsettings.json", appsettings); - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); - - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.SetBasePath(directory); - configurationBuilder.AddIniFile(fileName); - configurationBuilder.AddConfigServer(); - IConfigurationRoot configurationRoot = configurationBuilder.Build(); - - ConfigServerConfigurationProvider? configServerProvider = configurationRoot.Providers.OfType().SingleOrDefault(); - Assert.NotNull(configServerProvider); - - ConfigServerClientOptions options = configServerProvider.ClientOptions; - Assert.False(options.Enabled); - Assert.False(options.FailFast); - Assert.Equal("https://foo.com:9999", options.Uri); - Assert.Equal("Production", options.Environment); - Assert.Equal("myName", options.Name); - Assert.Equal("myLabel", options.Label); - Assert.Equal("myUsername", options.Username); - Assert.Equal("myPassword", options.Password); - } - - [Fact] - public void AddConfigServer_CommandLineAppSettingsConfiguresClient() - { - string[] appsettings = - [ - "spring:cloud:config:enabled=false", - "--spring:cloud:config:failFast=false", - "/spring:cloud:config:uri=https://foo.com:9999", - "--spring:cloud:config:name", - "myName", - "/spring:cloud:config:label", - "myLabel", - "--spring:cloud:config:username", - "myUsername", - "--spring:cloud:config:password", - "myPassword" - ]; - - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddCommandLine(appsettings); - configurationBuilder.AddConfigServer(); - IConfigurationRoot configurationRoot = configurationBuilder.Build(); - - ConfigServerConfigurationProvider? configServerProvider = configurationRoot.Providers.OfType().SingleOrDefault(); - Assert.NotNull(configServerProvider); - - ConfigServerClientOptions options = configServerProvider.ClientOptions; - Assert.False(options.Enabled); - Assert.False(options.FailFast); - Assert.Equal("https://foo.com:9999", options.Uri); - Assert.Equal("Production", options.Environment); - Assert.Equal("myName", options.Name); - Assert.Equal("myLabel", options.Label); - Assert.Equal("myUsername", options.Username); - Assert.Equal("myPassword", options.Password); - } - - [Fact] - public void AddConfigServer_HandlesPlaceHolders() - { - const string appsettings = """ - { - "foo": { - "bar": { - "name": "testName" - }, - }, - "spring": { - "application": { - "name": "myName" - }, - "cloud": { - "config": { - "uri": "https://user:password@foo.com:9999", - "enabled": false, - "failFast": false, - "name": "${foo:bar:name?foobar}", - "label": "myLabel", - "username": "myUsername", - "password": "myPassword" - } - } - } - } - """; - - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile("appsettings.json", appsettings); - - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); - - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.SetBasePath(directory); - configurationBuilder.AddJsonFile(fileName); - configurationBuilder.AddConfigServer(); - IConfigurationRoot configurationRoot = configurationBuilder.Build(); - - ConfigServerConfigurationProvider? configServerProvider = configurationRoot.Providers.OfType().SingleOrDefault(); - Assert.NotNull(configServerProvider); - - ConfigServerClientOptions options = configServerProvider.ClientOptions; - Assert.False(options.Enabled); - Assert.False(options.FailFast); - Assert.Equal("https://user:password@foo.com:9999", options.Uri); - Assert.Equal("Production", options.Environment); - Assert.Equal("testName", options.Name); - Assert.Equal("myLabel", options.Label); - Assert.Equal("myUsername", options.Username); - Assert.Equal("myPassword", options.Password); - } - [Theory] [InlineData(VcapServicesV2)] [InlineData(VcapServicesV3)] @@ -456,7 +213,7 @@ public void AddConfigServer_VCAP_SERVICES_Override_Defaults(string vcapServices) configurationBuilder.AddConfigServer(options, NullLoggerFactory.Instance); IConfigurationRoot configurationRoot = configurationBuilder.Build(); - ConfigServerConfigurationProvider? provider = configurationRoot.Providers.OfType().FirstOrDefault(); + ConfigServerConfigurationProvider? provider = configurationRoot.EnumerateProviders().FirstOrDefault(); Assert.NotNull(provider); Assert.IsType(provider); @@ -485,7 +242,7 @@ public void AddConfigServer_PaysAttentionToSettings() configurationBuilder.AddConfigServer(options, NullLoggerFactory.Instance); IConfigurationRoot configurationRoot = configurationBuilder.Build(); - ConfigServerConfigurationProvider? provider = configurationRoot.Providers.OfType().FirstOrDefault(); + ConfigServerConfigurationProvider? provider = configurationRoot.EnumerateProviders().FirstOrDefault(); Assert.NotNull(provider); Assert.Equal("testConfigLabel", provider.ClientOptions.Label); @@ -501,8 +258,7 @@ public void AddConfigServer_AddsCloudFoundryConfigurationSource() var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.AddConfigServer(); - var source = configurationBuilder.FindConfigurationSource(); - Assert.NotNull(source); + configurationBuilder.EnumerateSources().Should().HaveCount(1); } [Fact] @@ -512,7 +268,7 @@ public void AddConfigServer_Only_AddsOneCloudFoundryConfigurationSource() configurationBuilder.AddCloudFoundry(new OtherCloudFoundrySettingsReader()); configurationBuilder.AddConfigServer(); - Assert.Single(configurationBuilder.GetConfigurationSources()); + configurationBuilder.EnumerateSources().Should().HaveCount(1); } private sealed class OtherCloudFoundrySettingsReader : ICloudFoundrySettingsReader diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationSourceTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationSourceTest.cs index db413f98cf..2077d152fa 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationSourceTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationSourceTest.cs @@ -4,7 +4,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration.Memory; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; namespace Steeltoe.Configuration.ConfigServer.Test; @@ -19,15 +18,12 @@ public void Constructors_InitializesProperties() List sources = [memSource]; - using var factory = new LoggerFactory(); - var source = new ConfigServerConfigurationSource(options, sources, new Dictionary { { "foo", "bar" } - }, factory); + }, NullLoggerFactory.Instance); Assert.Equal(options, source.DefaultOptions); - Assert.Equal(factory, source.LoggerFactory); Assert.Null(source.Configuration); Assert.NotSame(sources, source.Sources); Assert.Single(source.Sources); @@ -36,9 +32,8 @@ public void Constructors_InitializesProperties() Assert.Equal("bar", source.Properties["foo"]); IConfigurationRoot configurationRoot = new ConfigurationBuilder().AddInMemoryCollection().Build(); - source = new ConfigServerConfigurationSource(options, configurationRoot, factory); + source = new ConfigServerConfigurationSource(options, configurationRoot, NullLoggerFactory.Instance); Assert.Equal(options, source.DefaultOptions); - Assert.Equal(factory, source.LoggerFactory); Assert.NotNull(source.Configuration); var root = source.Configuration as IConfigurationRoot; Assert.NotNull(root); diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerHealthContributorTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerHealthContributorTest.cs index c695a7c998..175d85af59 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerHealthContributorTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerHealthContributorTest.cs @@ -13,7 +13,7 @@ namespace Steeltoe.Configuration.ConfigServer.Test; public sealed class ConfigServerHealthContributorTest { [Fact] - public void Constructor_FindsConfigServerProvider() + public void Constructor_FindsConfigServerProviderInsidePlaceholderProvider() { var values = new Dictionary(TestSettingsFactory.Get(FastTestConfigurations.ConfigServer)) { diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerHostBuilderExtensionsTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerHostBuilderExtensionsTest.cs index 1486ce7290..35f49a386b 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerHostBuilderExtensionsTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerHostBuilderExtensionsTest.cs @@ -26,8 +26,8 @@ public void AddConfigServer_WebHost_AddsConfigServer() using IWebHost host = hostBuilder.Build(); var configuration = host.Services.GetRequiredService(); - Assert.NotNull(configuration.FindConfigurationProvider()); - Assert.NotNull(configuration.FindConfigurationProvider()); + Assert.Single(configuration.EnumerateProviders()); + Assert.Single(configuration.EnumerateProviders()); } [Fact] @@ -40,8 +40,8 @@ public void AddConfigServer_IHostBuilder_AddsConfigServer() using IHost host = hostBuilder.Build(); var configuration = host.Services.GetRequiredService(); - Assert.NotNull(configuration.FindConfigurationProvider()); - Assert.NotNull(configuration.FindConfigurationProvider()); + Assert.Single(configuration.EnumerateProviders()); + Assert.Single(configuration.EnumerateProviders()); } [Fact] @@ -59,8 +59,8 @@ public async Task AddConfigServer_WebApplicationBuilder_AddsConfigServer() await using WebApplication host = hostBuilder.Build(); var configuration = host.Services.GetRequiredService(); - Assert.NotNull(configuration.FindConfigurationProvider()); - Assert.NotNull(configuration.FindConfigurationProvider()); + Assert.Single(configuration.EnumerateProviders()); + Assert.Single(configuration.EnumerateProviders()); } [Fact] @@ -80,7 +80,7 @@ public void AddConfigServer_HostBuilder_DisposesTimer() using (IHost host = hostBuilder.Build()) { var configurationRoot = (IConfigurationRoot)host.Services.GetRequiredService(); - provider = configurationRoot.Providers.OfType().Single(); + provider = configurationRoot.EnumerateProviders().Single(); } FieldInfo refreshTimerField = provider.GetType().GetField("_refreshTimer", BindingFlags.NonPublic | BindingFlags.Instance)!; @@ -104,7 +104,7 @@ public void AddConfigServer_WebHostBuilder_DisposesTimer() using (IWebHost webHost = webHostBuilder.Build()) { var configurationRoot = (IConfigurationRoot)webHost.Services.GetRequiredService(); - provider = configurationRoot.Providers.OfType().Single(); + provider = configurationRoot.EnumerateProviders().Single(); } FieldInfo refreshTimerField = provider.GetType().GetField("_refreshTimer", BindingFlags.NonPublic | BindingFlags.Instance)!; @@ -124,7 +124,7 @@ public async Task AddConfigServer_WebApplicationBuilder_DisposesTimer() hostBuilder.AddConfigServer(); var configurationRoot = (IConfigurationRoot)hostBuilder.Configuration; - ConfigServerConfigurationProvider provider = configurationRoot.Providers.OfType().Single(); + ConfigServerConfigurationProvider provider = configurationRoot.EnumerateProviders().Single(); await using (WebApplication host = hostBuilder.Build()) { @@ -151,7 +151,7 @@ public async Task AddConfigServer_WebApplicationBuilder_TakesAppNameFromConfigSe await using WebApplication host = hostBuilder.Build(); var configuration = host.Services.GetRequiredService(); - var provider = configuration.FindConfigurationProvider(); + ConfigServerConfigurationProvider? provider = configuration.EnumerateProviders().SingleOrDefault(); provider.Should().NotBeNull(); provider!.ClientOptions.Name.Should().Be("myApp"); @@ -173,7 +173,7 @@ public async Task AddConfigServer_WebApplicationBuilder_TakesAppNameFromSpringCo await using WebApplication host = hostBuilder.Build(); var configuration = host.Services.GetRequiredService(); - var provider = configuration.FindConfigurationProvider(); + ConfigServerConfigurationProvider? provider = configuration.EnumerateProviders().SingleOrDefault(); provider.Should().NotBeNull(); provider!.ClientOptions.Name.Should().Be("myApp"); @@ -195,7 +195,7 @@ public async Task AddConfigServer_WebApplicationBuilder_TakesAppNameFromVcapAppl await using WebApplication host = hostBuilder.Build(); var configuration = host.Services.GetRequiredService(); - var provider = configuration.FindConfigurationProvider(); + ConfigServerConfigurationProvider? provider = configuration.EnumerateProviders().SingleOrDefault(); provider.Should().NotBeNull(); provider!.ClientOptions.Name.Should().Be("myApp"); @@ -219,7 +219,7 @@ public async Task AddConfigServer_WebApplicationBuilder_TakesAppNameFromHostingE await using WebApplication host = hostBuilder.Build(); var configuration = host.Services.GetRequiredService(); - var provider = configuration.FindConfigurationProvider(); + ConfigServerConfigurationProvider? provider = configuration.EnumerateProviders().SingleOrDefault(); provider.Should().NotBeNull(); provider!.ClientOptions.Name.Should().Be("myApp"); @@ -241,7 +241,7 @@ public async Task AddConfigServer_WebApplicationBuilder_TakesEnvironmentNameFrom await using WebApplication host = hostBuilder.Build(); var configuration = host.Services.GetRequiredService(); - var provider = configuration.FindConfigurationProvider(); + ConfigServerConfigurationProvider? provider = configuration.EnumerateProviders().SingleOrDefault(); provider.Should().NotBeNull(); provider!.ClientOptions.Environment.Should().Be("TestEnv"); @@ -263,7 +263,7 @@ public async Task AddConfigServer_WebApplicationBuilder_TakesEnvironmentNameFrom await using WebApplication host = hostBuilder.Build(); var configuration = host.Services.GetRequiredService(); - var provider = configuration.FindConfigurationProvider(); + ConfigServerConfigurationProvider? provider = configuration.EnumerateProviders().SingleOrDefault(); provider.Should().NotBeNull(); provider!.ClientOptions.Environment.Should().Be("TestEnv"); diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerHostedServiceTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerHostedServiceTest.cs index 3ce2732e27..2d26de7e81 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerHostedServiceTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerHostedServiceTest.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; using Steeltoe.Common.TestResources; +using Steeltoe.Configuration.Encryption; using Steeltoe.Configuration.Placeholder; namespace Steeltoe.Configuration.ConfigServer.Test; @@ -14,36 +15,12 @@ public sealed class ConfigServerHostedServiceTest [Fact] public async Task ServiceConstructsAndOperatesWithConfigurationRoot() { - var configurationRoot = new ConfigurationRoot([ - new ConfigServerConfigurationProvider(new ConfigServerClientOptions - { - Enabled = false - }, null, null, NullLoggerFactory.Instance) - ]); - - var service = new ConfigServerHostedService(configurationRoot, []); - - Func startStopAction = async () => + var provider = new ConfigServerConfigurationProvider(new ConfigServerClientOptions { - await service.StartAsync(default); - await service.StopAsync(default); - }; - - await startStopAction.Should().NotThrowAsync("ConfigServerHostedService should start"); - } - - [Fact] - public async Task FindsConfigServerProviderInPlaceholderProvider() - { - var placeholder = new PlaceholderResolverProvider([ - new ConfigServerConfigurationProvider(new ConfigServerClientOptions - { - Enabled = false - }, null, null, NullLoggerFactory.Instance) - ], NullLoggerFactory.Instance); - - var configurationRoot = new ConfigurationRoot([placeholder]); + Enabled = false + }, null, null, NullLoggerFactory.Instance); + var configurationRoot = new ConfigurationRoot([provider]); var service = new ConfigServerHostedService(configurationRoot, []); Func startStopAction = async () => @@ -71,4 +48,29 @@ public async Task ServiceConstructsAndOperatesWithConfigurationManager() await startStopAction.Should().NotThrowAsync("ConfigServerHostedService should start"); } + + [Fact] + public void ThrowsWhenConfigServerProviderNotFound() + { + var builder = new ConfigurationBuilder(); + IConfigurationRoot configurationRoot = builder.Build(); + + Action action = () => _ = new ConfigServerHostedService(configurationRoot, []); + + action.Should().ThrowExactly().WithMessage("ConfigServerConfigurationProvider was not found in configuration."); + } + + [Fact] + public void FindsConfigServerProviderInPlaceholderProviderInDecryptionProvider() + { + var builder = new ConfigurationBuilder(); + builder.AddConfigServer(); + builder.AddPlaceholderResolver(); + builder.AddDecryption(); + IConfigurationRoot configurationRoot = builder.Build(); + + Action action = () => _ = new ConfigServerHostedService(configurationRoot, []); + + action.Should().NotThrow(); + } } diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerServiceCollectionExtensionsTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerServiceCollectionExtensionsTest.cs index 2921853dc4..42ececd205 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerServiceCollectionExtensionsTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerServiceCollectionExtensionsTest.cs @@ -6,6 +6,8 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; +using Steeltoe.Configuration.Encryption; +using Steeltoe.Configuration.Placeholder; namespace Steeltoe.Configuration.ConfigServer.Test; @@ -28,4 +30,23 @@ public async Task ConfigureConfigServerClientOptions_ConfiguresConfigServerClien string? expectedAppName = Assembly.GetEntryAssembly()!.GetName().Name; TestHelper.VerifyDefaults(optionsMonitor.CurrentValue, expectedAppName); } + + [Fact] + public void DoesNotAddConfigServerMultipleTimes() + { + var builder = new ConfigurationBuilder(); + builder.AddConfigServer(); + builder.AddPlaceholderResolver(); + builder.AddDecryption(); + builder.AddConfigServer(); + builder.AddPlaceholderResolver(); + builder.AddDecryption(); + builder.AddConfigServer(); + + builder.EnumerateSources().Should().HaveCount(1); + + IConfigurationRoot configurationRoot = builder.Build(); + + configurationRoot.EnumerateProviders().Should().HaveCount(1); + } } diff --git a/src/Configuration/test/ConfigServer.Test/Steeltoe.Configuration.ConfigServer.Test.csproj b/src/Configuration/test/ConfigServer.Test/Steeltoe.Configuration.ConfigServer.Test.csproj index d9e3110273..ed660ed6a1 100644 --- a/src/Configuration/test/ConfigServer.Test/Steeltoe.Configuration.ConfigServer.Test.csproj +++ b/src/Configuration/test/ConfigServer.Test/Steeltoe.Configuration.ConfigServer.Test.csproj @@ -9,12 +9,11 @@ PreserveNewest - - Always - + + diff --git a/src/Configuration/test/Encryption.Test/Decryption/AesTextDecryptorTest.cs b/src/Configuration/test/Encryption.Test/Cryptography/AesTextDecryptorTest.cs similarity index 85% rename from src/Configuration/test/Encryption.Test/Decryption/AesTextDecryptorTest.cs rename to src/Configuration/test/Encryption.Test/Cryptography/AesTextDecryptorTest.cs index f807dfdc25..87cd0e1dc0 100644 --- a/src/Configuration/test/Encryption.Test/Decryption/AesTextDecryptorTest.cs +++ b/src/Configuration/test/Encryption.Test/Cryptography/AesTextDecryptorTest.cs @@ -2,19 +2,20 @@ // The .NET Foundation licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information. -using Steeltoe.Configuration.Encryption.Decryption; +using Steeltoe.Configuration.Encryption.Cryptography; -namespace Steeltoe.Configuration.Encryption.Test.Decryption; +namespace Steeltoe.Configuration.Encryption.Test.Cryptography; public sealed class AesTextDecryptorTest { [Theory] [MemberData(nameof(GetTestVector))] - public void DecodeTestForSpringConfigCipher(string salt, string key, string cipher, string plainText) + public void CanDecryptSpringConfigCipher(string salt, string key, string cipher, string plainText) { var textDecryptor = new AesTextDecryptor(key, salt); string decrypted = textDecryptor.Decrypt(cipher); - Assert.Equal(plainText, decrypted); + + decrypted.Should().Be(plainText); } public static TheoryData GetTestVector() diff --git a/src/Configuration/test/Encryption.Test/Cryptography/NoneDecryptorTest.cs b/src/Configuration/test/Encryption.Test/Cryptography/NoneDecryptorTest.cs new file mode 100644 index 0000000000..5ef7af0abd --- /dev/null +++ b/src/Configuration/test/Encryption.Test/Cryptography/NoneDecryptorTest.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. + +using Steeltoe.Configuration.Encryption.Cryptography; + +namespace Steeltoe.Configuration.Encryption.Test.Cryptography; + +public sealed class NoneDecryptorTest +{ + [Fact] + public void TestNoneEncryptor_ReturnsInput() + { + var noneDecryptor = new NoneDecryptor(); + Assert.Equal("something", noneDecryptor.Decrypt("something")); + Assert.Equal("something", noneDecryptor.Decrypt("something", "anything")); + } +} diff --git a/src/Configuration/test/Encryption.Test/Decryption/RsaKeyStoreDecryptorTest.cs b/src/Configuration/test/Encryption.Test/Cryptography/RsaKeyStoreDecryptorTest.cs similarity index 96% rename from src/Configuration/test/Encryption.Test/Decryption/RsaKeyStoreDecryptorTest.cs rename to src/Configuration/test/Encryption.Test/Cryptography/RsaKeyStoreDecryptorTest.cs index 7c7f8b042a..31ef779659 100644 --- a/src/Configuration/test/Encryption.Test/Decryption/RsaKeyStoreDecryptorTest.cs +++ b/src/Configuration/test/Encryption.Test/Cryptography/RsaKeyStoreDecryptorTest.cs @@ -2,13 +2,13 @@ // The .NET Foundation licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information. -using Steeltoe.Configuration.Encryption.Decryption; +using Steeltoe.Configuration.Encryption.Cryptography; -namespace Steeltoe.Configuration.Encryption.Test.Decryption; +namespace Steeltoe.Configuration.Encryption.Test.Cryptography; public sealed class RsaKeyStoreDecryptorTest { - private readonly KeyProvider _keyProvider = new("./Decryption/server.jks", "letmein"); + private readonly KeyProvider _keyProvider = new("./Cryptography/server.jks", "letmein"); [Fact] public void Decrypt_WithGarbageBase64Throws() diff --git a/src/Configuration/test/Encryption.Test/Cryptography/TextDecryptorFactoryTest.cs b/src/Configuration/test/Encryption.Test/Cryptography/TextDecryptorFactoryTest.cs new file mode 100644 index 0000000000..7d0bcaebe7 --- /dev/null +++ b/src/Configuration/test/Encryption.Test/Cryptography/TextDecryptorFactoryTest.cs @@ -0,0 +1,123 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. + +using Steeltoe.Configuration.Encryption.Cryptography; + +namespace Steeltoe.Configuration.Encryption.Test.Cryptography; + +public sealed class TextDecryptorFactoryTest +{ + [Fact] + public void Create_WhenDisabled_CreateNoneDecryptor() + { + var settings = new ConfigServerDecryptionSettings(); + Assert.IsType(TextDecryptorFactory.CreateDecryptor(settings)); + } + + [Fact] + public void Create_WhenEnabledWithKey_CreateAesDecryptor() + { + var settings = new ConfigServerDecryptionSettings + { + Enabled = true, + Key = "something" + }; + + Assert.IsType(TextDecryptorFactory.CreateDecryptor(settings)); + } + + [Fact] + public void Create_WhenEnabledKeyStoreLocation_CreateRsaDecryptor() + { + var settings = new ConfigServerDecryptionSettings + { + Enabled = true, + KeyStore = + { + Location = "./Cryptography/server.jks", + Password = "letmein", + Alias = "mytestkey" + } + }; + + Assert.IsType(TextDecryptorFactory.CreateDecryptor(settings)); + } + + [Fact] + public void Create_WhenEnabledInvalidStoreLocation_Throws() + { + var settings = new ConfigServerDecryptionSettings + { + Enabled = true, + KeyStore = + { + Password = "letmein", + Alias = "mytestkey" + } + }; + + Assert.Throws(() => TextDecryptorFactory.CreateDecryptor(settings)); + } + + [Fact] + public void Create_WhenEnabledInvalidStorePassword_Throws() + { + var settings = new ConfigServerDecryptionSettings + { + Enabled = true, + KeyStore = + { + Location = "./Cryptography/server.jks", + Alias = "mytestkey" + } + }; + + Assert.Throws(() => TextDecryptorFactory.CreateDecryptor(settings)); + } + + [Fact] + public void Create_WhenEnabledInvalidStoreAlias_Throws() + { + var settings = new ConfigServerDecryptionSettings + { + Enabled = true, + KeyStore = + { + Location = "./Cryptography/server.jks", + Password = "letmein" + } + }; + + Assert.Throws(() => TextDecryptorFactory.CreateDecryptor(settings)); + } + + [Fact] + public void Create_WhenEnabledValidKeyAndStore_Throws() + { + var settings = new ConfigServerDecryptionSettings + { + Enabled = true, + Key = "something", + KeyStore = + { + Location = "./Cryptography/server.jks", + Password = "letmein", + Alias = "mytestkey" + } + }; + + Assert.Throws(() => TextDecryptorFactory.CreateDecryptor(settings)); + } + + [Fact] + public void Create_WhenEnabledNothingConfigured_Throws() + { + var settings = new ConfigServerDecryptionSettings + { + Enabled = true + }; + + Assert.Throws(() => TextDecryptorFactory.CreateDecryptor(settings)); + } +} diff --git a/src/Configuration/test/Encryption.Test/Decryption/server.jks b/src/Configuration/test/Encryption.Test/Cryptography/server.jks similarity index 100% rename from src/Configuration/test/Encryption.Test/Decryption/server.jks rename to src/Configuration/test/Encryption.Test/Cryptography/server.jks diff --git a/src/Configuration/test/Encryption.Test/Decryption/EncryptionFactoryTest.cs b/src/Configuration/test/Encryption.Test/Decryption/EncryptionFactoryTest.cs deleted file mode 100644 index 4a054d5750..0000000000 --- a/src/Configuration/test/Encryption.Test/Decryption/EncryptionFactoryTest.cs +++ /dev/null @@ -1,123 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information. - -using Steeltoe.Configuration.Encryption.Decryption; - -namespace Steeltoe.Configuration.Encryption.Test.Decryption; - -public sealed class EncryptionFactoryTest -{ - [Fact] - public void Create_WhenDisabled_CreateNoopDecryptor() - { - var configServerEncryptionSettings = new ConfigServerEncryptionSettings(); - Assert.IsType(EncryptionFactory.CreateEncryptor(configServerEncryptionSettings)); - } - - [Fact] - public void Create_WhenEnabledWithKey_CreateAesDecryptor() - { - var configServerEncryptionSettings = new ConfigServerEncryptionSettings - { - Enabled = true, - Key = "something" - }; - - Assert.IsType(EncryptionFactory.CreateEncryptor(configServerEncryptionSettings)); - } - - [Fact] - public void Create_WhenEnabledKeyStoreLocation_CreateRsaDecryptor() - { - var configServerEncryptionSettings = new ConfigServerEncryptionSettings - { - Enabled = true, - KeyStore = - { - Location = "./Decryption/server.jks", - Password = "letmein", - Alias = "mytestkey" - } - }; - - Assert.IsType(EncryptionFactory.CreateEncryptor(configServerEncryptionSettings)); - } - - [Fact] - public void Create_WhenEnabledInvalidStoreLocation_Throws() - { - var configServerEncryptionSettings = new ConfigServerEncryptionSettings - { - Enabled = true, - KeyStore = - { - Password = "letmein", - Alias = "mytestkey" - } - }; - - Assert.Throws(() => EncryptionFactory.CreateEncryptor(configServerEncryptionSettings)); - } - - [Fact] - public void Create_WhenEnabledInvalidStorePassword_Throws() - { - var configServerEncryptionSettings = new ConfigServerEncryptionSettings - { - Enabled = true, - KeyStore = - { - Location = "./Decryption/server.jks", - Alias = "mytestkey" - } - }; - - Assert.Throws(() => EncryptionFactory.CreateEncryptor(configServerEncryptionSettings)); - } - - [Fact] - public void Create_WhenEnabledInvalidStoreAlias_Throws() - { - var configServerEncryptionSettings = new ConfigServerEncryptionSettings - { - Enabled = true, - KeyStore = - { - Location = "./Decryption/server.jks", - Password = "letmein" - } - }; - - Assert.Throws(() => EncryptionFactory.CreateEncryptor(configServerEncryptionSettings)); - } - - [Fact] - public void Create_WhenEnabledValidKeyAndStore_Throws() - { - var configServerEncryptionSettings = new ConfigServerEncryptionSettings - { - Enabled = true, - Key = "something", - KeyStore = - { - Location = "./Decryption/server.jks", - Password = "letmein", - Alias = "mytestkey" - } - }; - - Assert.Throws(() => EncryptionFactory.CreateEncryptor(configServerEncryptionSettings)); - } - - [Fact] - public void Create_WhenEnabledNothingConfigured_Throws() - { - var configServerEncryptionSettings = new ConfigServerEncryptionSettings - { - Enabled = true - }; - - Assert.Throws(() => EncryptionFactory.CreateEncryptor(configServerEncryptionSettings)); - } -} diff --git a/src/Configuration/test/Encryption.Test/Decryption/EncryptionServiceCollectionForConfigServerExtensionsTest.cs b/src/Configuration/test/Encryption.Test/Decryption/EncryptionServiceCollectionForConfigServerExtensionsTest.cs deleted file mode 100644 index bb9d8ddfdd..0000000000 --- a/src/Configuration/test/Encryption.Test/Decryption/EncryptionServiceCollectionForConfigServerExtensionsTest.cs +++ /dev/null @@ -1,49 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information. - -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Steeltoe.Common.TestResources; - -namespace Steeltoe.Configuration.Encryption.Test.Decryption; - -public sealed class EncryptionServiceCollectionForConfigServerExtensionsTest -{ - [Fact] - public void ConfigureEncryptionResolver_ConfiguresIConfiguration_ReplacesExisting() - { - var settings = new Dictionary - { - { "encrypt:enabled", "true" }, - { "encrypt:keyStore:location", "./Decryption/server.jks" }, - { "encrypt:keyStore:password", "letmein" }, - { "encrypt:keyStore:alias", "mytestkey" }, - { "encrypt:rsa:strong", "false" }, - { "encrypt:rsa:algorithm", "OAEP" }, - { "encrypt:rsa:salt", "deadbeef" }, - { "key1", "value1" }, - { - "key2", - "{cipher}AQATBPXCmri0MCEoCam0noXJgKGlFfE/chVN7XhH1V23MqJ8sI3lI61PyvsryJP3LlfNn38gUuulMeslAs/gUCoPFPV/zD7M8x527wQUbmWD6bR0ZMJ4hu3DisK6Diw2YAOxXSsm3Zh46cPFQcowfOG1x2OXj+5uL4T+VBGdt3Nr6dHCOumkTJ1KAtaJMfASf3J8G4M27v6m4Y2EdBqP1zWwDhAZ3R0u9uTP9xYUqQiKsUeOixrhOaCvtb1Q+Zg6A41CxM4cjL3Ty6miNYLx3QkxRvfkdo0iqo7jTrWWAT1aeRV6t5U5iMlWnD4eXzad60E3ZSINhvDiB03xPPPuHKC6qUTRJEEbQFegmn/KIPMMn9WaH/JLLZNvQYMuaFszZ84AE3aQcH0be+sNFDSjHNHL" - } - }; - - var builder = new ConfigurationBuilder(); - builder.AddInMemoryCollection(settings); - IConfigurationRoot configuration1 = builder.Build(); - - IWebHostBuilder hostBuilder = TestWebHostBuilderFactory.Create().UseStartup() - .UseConfiguration(configuration1); - - using var server = new TestServer(hostBuilder); - var configuration2 = server.Services.GetRequiredService(); - Assert.NotSame(configuration1, configuration2); - - Assert.Null(configuration2["nokey"]); - Assert.Equal("value1", configuration2["key1"]); - Assert.Equal("encrypt the world", configuration2["key2"]); - } -} diff --git a/src/Configuration/test/Encryption.Test/Decryption/NoopDecryptorTest.cs b/src/Configuration/test/Encryption.Test/Decryption/NoopDecryptorTest.cs deleted file mode 100644 index 35c6d11680..0000000000 --- a/src/Configuration/test/Encryption.Test/Decryption/NoopDecryptorTest.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information. - -using Steeltoe.Configuration.Encryption.Decryption; - -namespace Steeltoe.Configuration.Encryption.Test.Decryption; - -public sealed class NoopDecryptorTest -{ - [Fact] - public void TestNoopEncryptor_ReturnsInput() - { - var noopDecryptor = new NoopDecryptor(); - Assert.Equal("something", noopDecryptor.Decrypt("something")); - Assert.Equal("something", noopDecryptor.Decrypt("something", "anything")); - } -} diff --git a/src/Configuration/test/Encryption.Test/Decryption/StartupForConfigureConfigServerEncryptionResolver.cs b/src/Configuration/test/Encryption.Test/Decryption/StartupForConfigureConfigServerEncryptionResolver.cs deleted file mode 100644 index 0338d7630a..0000000000 --- a/src/Configuration/test/Encryption.Test/Decryption/StartupForConfigureConfigServerEncryptionResolver.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information. - -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; - -namespace Steeltoe.Configuration.Encryption.Test.Decryption; - -public sealed class StartupForConfigureConfigServerEncryptionResolver -{ - private readonly IConfiguration _configuration; - - public StartupForConfigureConfigServerEncryptionResolver(IConfiguration configuration) - { - ArgumentNullException.ThrowIfNull(configuration); - - _configuration = configuration; - } - - public void ConfigureServices(IServiceCollection services) - { - services.ConfigureConfigServerEncryptionResolver(_configuration); - } - - public void Configure(IApplicationBuilder app) - { - } -} diff --git a/src/Configuration/test/Encryption.Test/DecryptionConfigurationTest.cs b/src/Configuration/test/Encryption.Test/DecryptionConfigurationTest.cs new file mode 100644 index 0000000000..3b1e44ee0f --- /dev/null +++ b/src/Configuration/test/Encryption.Test/DecryptionConfigurationTest.cs @@ -0,0 +1,107 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Memory; +using Microsoft.Extensions.Logging; +using Steeltoe.Common.TestResources; +using Steeltoe.Configuration.Encryption.Cryptography; +using Steeltoe.Configuration.Placeholder; +using Xunit.Abstractions; + +namespace Steeltoe.Configuration.Encryption.Test; + +public sealed class DecryptionConfigurationTest : IDisposable +{ + private readonly LoggerFactory _loggerFactory; + + public DecryptionConfigurationTest(ITestOutputHelper testOutputHelper) + { + var loggerProvider = new XunitLoggerProvider(testOutputHelper); + _loggerFactory = new LoggerFactory([loggerProvider]); + } + + [Fact] + public void Takes_ownership_of_existing_sources() + { + var builder = new ConfigurationBuilder(); + builder.AddInMemoryCollection(); + builder.AddInMemoryCollection(); + builder.AddDecryption(_loggerFactory); + + builder.Sources.Should().HaveCount(1); + DecryptionConfigurationSource decryptionSource = builder.Sources[0].Should().BeOfType().Subject; + + decryptionSource.Sources.Should().HaveCount(2); + decryptionSource.Sources.Should().AllBeOfType(); + } + + [Fact] + public void Decrypts_encrypted_values() + { + var decryptor = new ToUpperCaseDecryptor(); + + var appSettings = new Dictionary + { + ["key1"] = "value1", + ["key2"] = "{cipher}example-cipher-without-alias", + ["key3"] = "{cipher}{key:key-alias}example-cipher-with-alias" + }; + + var builder = new ConfigurationBuilder(); + builder.AddInMemoryCollection(appSettings); + builder.AddDecryption(decryptor, _loggerFactory); + IConfiguration configuration = builder.Build(); + + configuration["no-key"].Should().BeNull(); + configuration["key1"].Should().Be("value1"); + configuration["key2"].Should().Be("EXAMPLE-CIPHER-WITHOUT-ALIAS"); + configuration["key3"].Should().Be("EXAMPLE-CIPHER-WITH-ALIAS|KEY-ALIAS"); + + configuration["key2"] = "{cipher}{key:other-alias}other-cipher-with-alias"; + configuration["key2"].Should().Be("OTHER-CIPHER-WITH-ALIAS|OTHER-ALIAS"); + + configuration["key2"] = "no-cipher"; + configuration["key2"].Should().Be("no-cipher"); + } + + [Fact] + public void Can_resolve_placeholder_to_decrypted_value() + { + var decryptor = new ToUpperCaseDecryptor(); + + var appSettings = new Dictionary + { + ["result"] = "start-${test-placeholder}-end", + ["test-placeholder"] = "{cipher}secret" + }; + + var builder = new ConfigurationBuilder(); + builder.Sources.Clear(); + builder.AddInMemoryCollection(appSettings); + builder.AddDecryption(decryptor, _loggerFactory); + builder.AddPlaceholderResolver(_loggerFactory); + IConfiguration configuration = builder.Build(); + + configuration["result"].Should().Be("start-SECRET-end"); + } + + public void Dispose() + { + _loggerFactory.Dispose(); + } + + private sealed class ToUpperCaseDecryptor : ITextDecryptor + { + public string Decrypt(string fullCipher) + { + return fullCipher.ToUpperInvariant(); + } + + public string Decrypt(string fullCipher, string alias) + { + return $"{fullCipher.ToUpperInvariant()}|{alias.ToUpperInvariant()}"; + } + } +} diff --git a/src/Configuration/test/Encryption.Test/EncryptionConfigurationExtensionsTest.cs b/src/Configuration/test/Encryption.Test/EncryptionConfigurationExtensionsTest.cs deleted file mode 100644 index 89921baec1..0000000000 --- a/src/Configuration/test/Encryption.Test/EncryptionConfigurationExtensionsTest.cs +++ /dev/null @@ -1,151 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information. - -using Microsoft.Extensions.Configuration; -using Moq; - -namespace Steeltoe.Configuration.Encryption.Test; - -public sealed class EncryptionConfigurationExtensionsTest -{ - [Fact] - public void AddEncryptionResolver_AddsEncryptionResolverSourceToList() - { - var configurationBuilder = new ConfigurationBuilder(); - - configurationBuilder.AddEncryptionResolver(); - - EncryptionResolverSource? encryptionSource = configurationBuilder.Sources.OfType().SingleOrDefault(); - Assert.NotNull(encryptionSource); - } - - [Fact] - public void AddEncryptionResolver_NoDuplicates() - { - var configurationBuilder = new ConfigurationBuilder(); - - configurationBuilder.AddEncryptionResolver(); - configurationBuilder.AddEncryptionResolver(); - configurationBuilder.AddEncryptionResolver(); - - EncryptionResolverSource? source = configurationBuilder.Sources.OfType().SingleOrDefault(); - Assert.NotNull(source); - Assert.NotNull(source.Sources); - Assert.Empty(source.Sources); - } - - [Fact] - public void AddEncryptionResolver_CreatesProvider() - { - var configurationBuilder = new ConfigurationBuilder(); - - configurationBuilder.AddEncryptionResolver(); - IConfigurationRoot configurationRoot = configurationBuilder.Build(); - - EncryptionResolverProvider? provider = configurationRoot.Providers.OfType().SingleOrDefault(); - - Assert.NotNull(provider); - } - - [Fact] - public void AddEncryptionResolver_ClearsSources() - { - var settings = new Dictionary - { - { "key1", "value1" }, - { "key2", "{cypher}something" } - }; - - var builder = new ConfigurationBuilder(); - builder.AddInMemoryCollection(settings); - builder.AddEncryptionResolver(); - - Assert.Single(builder.Sources); - IConfigurationRoot configurationRoot = builder.Build(); - - Assert.Single(configurationRoot.Providers); - IConfigurationProvider provider = configurationRoot.Providers.ToList()[0]; - Assert.IsType(provider); - } - - [Fact] - public void AddEncryptionResolver_WithConfiguration_ReturnsNewConfigurationWithDecryption() - { - Mock decryptorMock = new(); - decryptorMock.Setup(x => x.Decrypt(It.IsAny())).Returns((string _) => "DECRYPTED"); - - var settings = new Dictionary - { - { "key1", "value1" }, - { "key2", "{cipher}something" } - }; - - var builder = new ConfigurationBuilder(); - builder.AddInMemoryCollection(settings); - IConfigurationRoot configuration1 = builder.Build(); - - IConfiguration configuration2 = configuration1.AddEncryptionResolver(decryptorMock.Object); - Assert.NotSame(configuration1, configuration2); - - var root2 = (IConfigurationRoot)configuration2; - Assert.Single(root2.Providers); - IConfigurationProvider provider = root2.Providers.ToList()[0]; - Assert.IsType(provider); - - Assert.Null(configuration2["nokey"]); - Assert.Equal("value1", configuration2["key1"]); - Assert.Equal("DECRYPTED", configuration2["key2"]); - - decryptorMock.Verify(x => x.Decrypt("something")); - decryptorMock.VerifyNoOtherCalls(); - } - - [Fact] - public void AddEncryptionResolver_WithConfiguration_ReturnsNewConfigurationWithWithKeyAliasDecryption() - { - Mock decryptorMock = new(); - decryptorMock.Setup(x => x.Decrypt(It.IsAny(), It.IsAny())).Returns((string _, string _) => "DECRYPTED"); - - var settings = new Dictionary - { - { "key1", "value1" }, - { "key2", "{cipher}{key:keyalias}something" } - }; - - var builder = new ConfigurationBuilder(); - builder.AddInMemoryCollection(settings); - IConfigurationRoot configuration1 = builder.Build(); - - IConfiguration configuration2 = configuration1.AddEncryptionResolver(decryptorMock.Object); - Assert.NotSame(configuration1, configuration2); - - var root2 = (IConfigurationRoot)configuration2; - Assert.Single(root2.Providers); - IConfigurationProvider provider = root2.Providers.ToList()[0]; - Assert.IsType(provider); - - Assert.Null(configuration2["nokey"]); - Assert.Equal("value1", configuration2["key1"]); - Assert.Equal("DECRYPTED", configuration2["key2"]); - - decryptorMock.Verify(x => x.Decrypt("something", "keyalias")); - decryptorMock.VerifyNoOtherCalls(); - } - - [Fact] - public void AddEncryptionResolver_WithConfiguration_NoDuplicates() - { - Mock decryptorMock = new(); - IConfigurationRoot configurationRoot = new ConfigurationBuilder().Build(); - - IConfiguration newConfiguration = configurationRoot.AddEncryptionResolver(decryptorMock.Object).AddEncryptionResolver(decryptorMock.Object) - .AddEncryptionResolver(decryptorMock.Object); - - ConfigurationRoot newConfigurationRoot = newConfiguration.Should().BeOfType().Which; - newConfigurationRoot.Providers.Should().HaveCount(1); - - EncryptionResolverProvider? provider = newConfigurationRoot.Providers.Single().Should().BeOfType().Which; - provider.Providers.Should().BeEmpty(); - } -} diff --git a/src/Configuration/test/Encryption.Test/EncryptionResolverProviderTest.cs b/src/Configuration/test/Encryption.Test/EncryptionResolverProviderTest.cs deleted file mode 100644 index 98ddaf3b54..0000000000 --- a/src/Configuration/test/Encryption.Test/EncryptionResolverProviderTest.cs +++ /dev/null @@ -1,139 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information. - -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; - -namespace Steeltoe.Configuration.Encryption.Test; - -public sealed class EncryptionResolverProviderTest -{ - private readonly Mock _decryptorMock = new(); - - [Fact] - public void Constructor_WithConfiguration() - { - IConfigurationRoot configurationRoot = new ConfigurationBuilder().Build(); - - var provider = new EncryptionResolverProvider(configurationRoot, _decryptorMock.Object, NullLoggerFactory.Instance); - - Assert.NotNull(provider.Configuration); - Assert.Empty(provider.Providers); - } - - [Fact] - public void Constructor_WithProviders() - { - List providers = []; - var provider = new EncryptionResolverProvider(providers, _decryptorMock.Object, NullLoggerFactory.Instance); - - Assert.Null(provider.Configuration); - Assert.Same(providers, provider.Providers); - } - - [Fact] - public void TryGet_ReturnsResolvedDecryptedValues() - { - _decryptorMock.Setup(x => x.Decrypt("something")).Returns("DECRYPTED"); - - var settings = new Dictionary - { - { "key1", "value1" }, - { "key2", "{cipher}something" } - }; - - var builder = new ConfigurationBuilder(); - builder.AddInMemoryCollection(settings); - List providers = builder.Build().Providers.ToList(); - - var holder = new EncryptionResolverProvider(providers, _decryptorMock.Object, NullLoggerFactory.Instance); - - Assert.False(holder.TryGet("nokey", out string? val)); - Assert.True(holder.TryGet("key1", out val)); - Assert.Equal("value1", val); - Assert.True(holder.TryGet("key2", out val)); - Assert.Equal("DECRYPTED", val); - } - - [Fact] - public void Set_SetsValues_ReturnsResolvedDecryptedValues() - { - _decryptorMock.Setup(x => x.Decrypt("something")).Returns("DECRYPTED"); - _decryptorMock.Setup(x => x.Decrypt("something2")).Returns("DECRYPTED2"); - - var settings = new Dictionary - { - { "key1", "value1" }, - { "key2", "{cipher}something" } - }; - - var builder = new ConfigurationBuilder(); - builder.AddInMemoryCollection(settings); - List providers = builder.Build().Providers.ToList(); - - var holder = new EncryptionResolverProvider(providers, _decryptorMock.Object, NullLoggerFactory.Instance); - - Assert.False(holder.TryGet("nokey", out string? val)); - Assert.True(holder.TryGet("key1", out val)); - Assert.Equal("value1", val); - Assert.True(holder.TryGet("key2", out val)); - Assert.Equal("DECRYPTED", val); - - holder.Set("key2", "{cipher}something2"); - Assert.True(holder.TryGet("key2", out val)); - Assert.Equal("DECRYPTED2", val); - - holder.Set("key2", "nocipher"); - Assert.True(holder.TryGet("key2", out val)); - Assert.Equal("nocipher", val); - } - - [Fact] - public void Load_CreatesConfiguration() - { - var settings = new Dictionary - { - { "key1", "value1" }, - { "key2", "{cipher}encrypted" } - }; - - var builder = new ConfigurationBuilder(); - builder.AddInMemoryCollection(settings); - List providers = builder.Build().Providers.ToList(); - - var holder = new EncryptionResolverProvider(providers, _decryptorMock.Object, NullLoggerFactory.Instance); - Assert.Null(holder.Configuration); - holder.Load(); - Assert.NotNull(holder.Configuration); - Assert.Equal("value1", holder.Configuration["key1"]); - } - - [Fact] - public void AdjustConfigManagerBuilder_CorrectlyReflectNewValues() - { - _decryptorMock.Setup(x => x.Decrypt("encrypted")).Returns("DECRYPTED"); - - var manager = new ConfigurationManager(); - - var valueProviderA = new Dictionary - { - { "value", "a" } - }; - - var encryption = new Dictionary - { - { "value", "{cipher}encrypted" } - }; - - manager.AddInMemoryCollection(valueProviderA); - manager.AddInMemoryCollection(encryption); - manager.AddEncryptionResolver(_decryptorMock.Object); - string? result = manager.GetValue("value"); - Assert.Equal("DECRYPTED", result); - - _decryptorMock.Verify(x => x.Decrypt("encrypted")); - _decryptorMock.VerifyNoOtherCalls(); - } -} diff --git a/src/Configuration/test/Encryption.Test/EncryptionResolverSourceTest.cs b/src/Configuration/test/Encryption.Test/EncryptionResolverSourceTest.cs deleted file mode 100644 index 1fd17c02cb..0000000000 --- a/src/Configuration/test/Encryption.Test/EncryptionResolverSourceTest.cs +++ /dev/null @@ -1,43 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information. - -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Configuration.Memory; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; - -namespace Steeltoe.Configuration.Encryption.Test; - -public sealed class EncryptionResolverSourceTest -{ - private readonly Mock _decryptorMock = new(); - - [Fact] - public void Constructors_InitializesProperties() - { - var memorySource = new MemoryConfigurationSource(); - List sources = [memorySource]; - - using var factory = new LoggerFactory(); - - var encryptionSource = new EncryptionResolverSource(sources, _decryptorMock.Object, factory); - Assert.Equal(factory, encryptionSource.LoggerFactory); - Assert.NotNull(encryptionSource.Sources); - Assert.Single(encryptionSource.Sources); - Assert.NotSame(sources, encryptionSource.Sources); - Assert.Contains(memorySource, encryptionSource.Sources); - } - - [Fact] - public void Build_ReturnsProvider() - { - var memorySource = new MemoryConfigurationSource(); - List sources = [memorySource]; - - var encryptionSource = new EncryptionResolverSource(sources, _decryptorMock.Object, NullLoggerFactory.Instance); - IConfigurationProvider provider = encryptionSource.Build(new ConfigurationBuilder()); - Assert.IsType(provider); - } -} diff --git a/src/Configuration/test/Encryption.Test/EncryptionServiceCollectionExtensionsTest.cs b/src/Configuration/test/Encryption.Test/EncryptionServiceCollectionExtensionsTest.cs deleted file mode 100644 index b47c1f1468..0000000000 --- a/src/Configuration/test/Encryption.Test/EncryptionServiceCollectionExtensionsTest.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information. - -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Steeltoe.Common.TestResources; - -namespace Steeltoe.Configuration.Encryption.Test; - -public sealed class EncryptionServiceCollectionExtensionsTest -{ - [Fact] - public void ConfigureEncryptionResolver_ConfiguresIConfiguration_ReplacesExisting() - { - var settings = new Dictionary - { - { "key1", "value1" }, - { "key2", "{cipher}somecipher" }, - { "key3", "{cipher}{key:keyalias}somekeyaliascipher" } - }; - - var builder = new ConfigurationBuilder(); - builder.AddInMemoryCollection(settings); - IConfigurationRoot configuration1 = builder.Build(); - - IWebHostBuilder hostBuilder = TestWebHostBuilderFactory.Create().UseStartup().UseConfiguration(configuration1); - - using var server = new TestServer(hostBuilder); - var configuration2 = server.Services.GetRequiredService(); - Assert.NotSame(configuration1, configuration2); - - Assert.Null(configuration2["nokey"]); - Assert.Equal("value1", configuration2["key1"]); - Assert.Equal("DECRYPTED", configuration2["key2"]); - Assert.Equal("DECRYPTEDWITHALIAS", configuration2["key3"]); - } -} diff --git a/src/Configuration/test/Encryption.Test/StartupForConfigureEncryptionResolver.cs b/src/Configuration/test/Encryption.Test/StartupForConfigureEncryptionResolver.cs deleted file mode 100644 index 857fdff83b..0000000000 --- a/src/Configuration/test/Encryption.Test/StartupForConfigureEncryptionResolver.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information. - -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; - -namespace Steeltoe.Configuration.Encryption.Test; - -public sealed class StartupForConfigureEncryptionResolver -{ - private readonly IConfiguration _configuration; - private readonly TextDecryptorForTest _textDecryptor = new(); - - public StartupForConfigureEncryptionResolver(IConfiguration configuration) - { - ArgumentNullException.ThrowIfNull(configuration); - - _configuration = configuration; - } - - public void ConfigureServices(IServiceCollection services) - { - services.ConfigureEncryptionResolver(_configuration, _textDecryptor); - } - - public void Configure(IApplicationBuilder app) - { - } - - private sealed class TextDecryptorForTest : ITextDecryptor - { - public string Decrypt(string fullCipher) - { - return "DECRYPTED"; - } - - public string Decrypt(string fullCipher, string alias) - { - return "DECRYPTEDWITHALIAS"; - } - } -} diff --git a/src/Configuration/test/Encryption.Test/Steeltoe.Configuration.Encryption.Test.csproj b/src/Configuration/test/Encryption.Test/Steeltoe.Configuration.Encryption.Test.csproj index 977006b03f..804bc9f1ff 100644 --- a/src/Configuration/test/Encryption.Test/Steeltoe.Configuration.Encryption.Test.csproj +++ b/src/Configuration/test/Encryption.Test/Steeltoe.Configuration.Encryption.Test.csproj @@ -6,12 +6,14 @@ - + Always + + diff --git a/src/Configuration/test/Encryption.Test/xunit.runner.json b/src/Configuration/test/Encryption.Test/xunit.runner.json deleted file mode 100644 index fdeefaa456..0000000000 --- a/src/Configuration/test/Encryption.Test/xunit.runner.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "maxParallelThreads": 1, - "parallelizeTestCollections": false -} diff --git a/src/Configuration/test/Placeholder.Test/ConfigureTestOptions.cs b/src/Configuration/test/Placeholder.Test/ConfigureTestOptions.cs new file mode 100644 index 0000000000..0a0f8d2454 --- /dev/null +++ b/src/Configuration/test/Placeholder.Test/ConfigureTestOptions.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Steeltoe.Configuration.Placeholder.Test; + +internal sealed partial class ConfigureTestOptions(ILogger logger) : IConfigureOptions +{ + private readonly ILogger _logger = logger; + private long _configureCount; + + public long ConfigureCount => Interlocked.Read(ref _configureCount); + + public void Configure(TestOptions options) + { + LogConfigure(options.Value); + Interlocked.Increment(ref _configureCount); + } + + [LoggerMessage(Level = LogLevel.Trace, Message = "Configure with value '{Value}'.")] + private partial void LogConfigure(string? value); +} diff --git a/src/Configuration/test/Placeholder.Test/PlaceholderConfigurationExtensionsTest.cs b/src/Configuration/test/Placeholder.Test/PlaceholderConfigurationExtensionsTest.cs deleted file mode 100644 index de49ea1aab..0000000000 --- a/src/Configuration/test/Placeholder.Test/PlaceholderConfigurationExtensionsTest.cs +++ /dev/null @@ -1,286 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information. - -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using Steeltoe.Common.TestResources.IO; - -namespace Steeltoe.Configuration.Placeholder.Test; - -public sealed class PlaceholderConfigurationExtensionsTest -{ - [Fact] - public void AddPlaceholderResolver_AddsPlaceholderResolverSourceToList() - { - var configurationBuilder = new ConfigurationBuilder(); - - configurationBuilder.AddPlaceholderResolver(); - - PlaceholderResolverSource? placeholderSource = configurationBuilder.Sources.OfType().SingleOrDefault(); - Assert.NotNull(placeholderSource); - } - - [Fact] - public void AddPlaceholderResolver_NoDuplicates() - { - var configurationBuilder = new ConfigurationBuilder(); - - configurationBuilder.AddPlaceholderResolver(); - configurationBuilder.AddPlaceholderResolver(); - configurationBuilder.AddPlaceholderResolver(); - - PlaceholderResolverSource? source = configurationBuilder.Sources.OfType().SingleOrDefault(); - Assert.NotNull(source); - Assert.NotNull(source.Sources); - Assert.Empty(source.Sources); - } - - [Fact] - public void AddPlaceholderResolver_CreatesProvider() - { - var configurationBuilder = new ConfigurationBuilder(); - using var loggerFactory = new LoggerFactory(); - - configurationBuilder.AddPlaceholderResolver(loggerFactory); - IConfigurationRoot configurationRoot = configurationBuilder.Build(); - - PlaceholderResolverProvider? provider = configurationRoot.Providers.OfType().SingleOrDefault(); - - Assert.NotNull(provider); - } - - [Fact] - public void AddPlaceholderResolver_JsonAppSettingsResolvesPlaceholders() - { - const string appsettings = """ - { - "spring": { - "bar": { - "name": "myName" - }, - "cloud": { - "config": { - "name": "${spring:bar:name?noname}" - } - } - } - } - """; - - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile("appsettings.json", appsettings); - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.SetBasePath(directory); - - configurationBuilder.AddJsonFile(fileName, false, false); - - configurationBuilder.AddPlaceholderResolver(); - IConfigurationRoot configurationRoot = configurationBuilder.Build(); - - Assert.Equal("myName", configurationRoot["spring:cloud:config:name"]); - } - - [Fact] - public void AddPlaceholderResolver_XmlAppSettingsResolvesPlaceholders() - { - const string appsettings = """ - - - - myName - - - - ${spring:bar:name?noName} - - - - - """; - - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile("appsettings.json", appsettings); - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.SetBasePath(directory); - - configurationBuilder.AddXmlFile(fileName, false, false); - - configurationBuilder.AddPlaceholderResolver(); - IConfigurationRoot configurationRoot = configurationBuilder.Build(); - - Assert.Equal("myName", configurationRoot["spring:cloud:config:name"]); - } - - [Fact] - public void AddPlaceholderResolver_IniAppSettingsResolvesPlaceholders() - { - const string appsettings = """ - [spring:bar] - name=myName - [spring:cloud:config] - name=${spring:bar:name?noName} - """; - - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile("appsettings.json", appsettings); - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.SetBasePath(directory); - - configurationBuilder.AddIniFile(fileName, false, false); - - configurationBuilder.AddPlaceholderResolver(); - IConfigurationRoot configurationRoot = configurationBuilder.Build(); - - Assert.Equal("myName", configurationRoot["spring:cloud:config:name"]); - } - - [Fact] - public void AddPlaceholderResolver_CommandLineAppSettingsResolvesPlaceholders() - { - string[] appsettings = - [ - "spring:bar:name=myName", - "--spring:cloud:config:name=${spring:bar:name?noName}" - ]; - - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddCommandLine(appsettings); - - configurationBuilder.AddPlaceholderResolver(); - IConfigurationRoot configurationRoot = configurationBuilder.Build(); - - Assert.Equal("myName", configurationRoot["spring:cloud:config:name"]); - } - - [Fact] - public void AddPlaceholderResolver_HandlesRecursivePlaceHolders() - { - const string appsettingsJson = """ - { - "spring": { - "json": { - "name": "myName" - }, - "cloud": { - "config": { - "name": "${spring:xml:name?noname}" - } - } - } - } - """; - - const string appsettingsXml = """ - - - - ${spring:ini:name?noName} - - - - """; - - const string appsettingsIni = """ - [spring:ini] - name=${spring:line:name?noName} - """; - - string[] appsettingsLine = ["--spring:line:name=${spring:json:name?noName}"]; - - using var sandbox = new Sandbox(); - string jsonPath = sandbox.CreateFile("appsettings.json", appsettingsJson); - string jsonFileName = Path.GetFileName(jsonPath); - string xmlPath = sandbox.CreateFile("appsettings.xml", appsettingsXml); - string xmlFileName = Path.GetFileName(xmlPath); - string iniPath = sandbox.CreateFile("appsettings.ini", appsettingsIni); - string iniFileName = Path.GetFileName(iniPath); - - string directory = Path.GetDirectoryName(jsonPath)!; - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.SetBasePath(directory); - - configurationBuilder.AddJsonFile(jsonFileName, false, false); - configurationBuilder.AddXmlFile(xmlFileName, false, false); - configurationBuilder.AddIniFile(iniFileName, false, false); - configurationBuilder.AddCommandLine(appsettingsLine); - - configurationBuilder.AddPlaceholderResolver(); - IConfigurationRoot configurationRoot = configurationBuilder.Build(); - - Assert.Equal("myName", configurationRoot["spring:cloud:config:name"]); - } - - [Fact] - public void AddPlaceholderResolver_ClearsSources() - { - var settings = new Dictionary - { - { "key1", "value1" }, - { "key2", "${key1?notfound}" }, - { "key3", "${nokey?notfound}" }, - { "key4", "${nokey}" } - }; - - var builder = new ConfigurationBuilder(); - builder.AddInMemoryCollection(settings); - builder.AddPlaceholderResolver(); - - Assert.Single(builder.Sources); - IConfigurationRoot configurationRoot = builder.Build(); - - Assert.Single(configurationRoot.Providers); - IConfigurationProvider provider = configurationRoot.Providers.ToList()[0]; - Assert.IsType(provider); - } - - [Fact] - public void AddPlaceholderResolver_WithConfiguration_ReturnsNewConfiguration() - { - var settings = new Dictionary - { - { "key1", "value1" }, - { "key2", "${key1?notfound}" }, - { "key3", "${nokey?notfound}" }, - { "key4", "${nokey}" } - }; - - var builder = new ConfigurationBuilder(); - builder.AddInMemoryCollection(settings); - IConfigurationRoot configuration1 = builder.Build(); - - IConfiguration configuration2 = configuration1.AddPlaceholderResolver(); - Assert.NotSame(configuration1, configuration2); - - var root2 = (IConfigurationRoot)configuration2; - Assert.Single(root2.Providers); - IConfigurationProvider provider = root2.Providers.ToList()[0]; - Assert.IsType(provider); - - Assert.Null(configuration2["nokey"]); - Assert.Equal("value1", configuration2["key1"]); - Assert.Equal("value1", configuration2["key2"]); - Assert.Equal("notfound", configuration2["key3"]); - Assert.Equal("${nokey}", configuration2["key4"]); - } - - [Fact] - public void AddPlaceholderResolver_WithConfiguration_NoDuplicates() - { - IConfigurationRoot configurationRoot = new ConfigurationBuilder().Build(); - - IConfiguration newConfiguration = configurationRoot.AddPlaceholderResolver().AddPlaceholderResolver().AddPlaceholderResolver(); - - ConfigurationRoot newConfigurationRoot = newConfiguration.Should().BeOfType().Which; - newConfigurationRoot.Providers.Should().HaveCount(1); - - PlaceholderResolverProvider? provider = newConfigurationRoot.Providers.Single().Should().BeOfType().Which; - provider.Providers.Should().BeEmpty(); - } -} diff --git a/src/Configuration/test/Placeholder.Test/PlaceholderConfigurationTest.cs b/src/Configuration/test/Placeholder.Test/PlaceholderConfigurationTest.cs new file mode 100644 index 0000000000..7cb9749da7 --- /dev/null +++ b/src/Configuration/test/Placeholder.Test/PlaceholderConfigurationTest.cs @@ -0,0 +1,411 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Json; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Steeltoe.Common.TestResources; +using Xunit.Abstractions; + +namespace Steeltoe.Configuration.Placeholder.Test; + +public sealed class PlaceholderConfigurationTest : IDisposable +{ + private readonly LoggerFactory _loggerFactory; + + public PlaceholderConfigurationTest(ITestOutputHelper testOutputHelper) + { + var loggerProvider = new XunitLoggerProvider(testOutputHelper); + _loggerFactory = new LoggerFactory([loggerProvider]); + } + + [Fact] + public void Takes_ownership_of_existing_sources() + { + var testSourceA = new TestConfigurationSource("A", _loggerFactory); + var testSourceB = new TestConfigurationSource("B", _loggerFactory); + + var builder = new ConfigurationBuilder(); + builder.Add(testSourceA); + builder.Add(testSourceB); + builder.AddPlaceholderResolver(_loggerFactory); + + builder.Sources.Should().HaveCount(1); + PlaceholderConfigurationSource placeholderSource = builder.Sources[0].Should().BeOfType().Subject; + + placeholderSource.Sources.Should().HaveCount(2); + placeholderSource.Sources.Should().Contain(testSourceA); + placeholderSource.Sources.Should().Contain(testSourceB); + } + + [Fact] + public void Reloads_owned_providers() + { + var testSourceA = new TestConfigurationSource("A", _loggerFactory); + Guid testSourceIdA = testSourceA.Id; + var testSourceB = new TestConfigurationSource("B", _loggerFactory); + Guid testSourceIdB = testSourceB.Id; + + var builder = new ConfigurationBuilder(); + builder.Add(testSourceA); + builder.Add(testSourceB); + builder.AddPlaceholderResolver(_loggerFactory); + + testSourceA.LastProvider.Should().BeNull(); + testSourceB.LastProvider.Should().BeNull(); + + IConfigurationRoot configurationRoot = builder.Build(); + + testSourceA.LastProvider.Should().NotBeNull().And.BeOfType(); + testSourceB.LastProvider.Should().NotBeNull().And.BeOfType(); + + testSourceA.LastProvider!.LoadCount.Should().Be(1); + testSourceB.LastProvider!.LoadCount.Should().Be(1); + + Guid lastProviderIdA = testSourceA.LastProvider.Id; + Guid lastProviderIdB = testSourceB.LastProvider.Id; + + configurationRoot.Reload(); + + testSourceA.LastProvider.LoadCount.Should().Be(2); + testSourceB.LastProvider.LoadCount.Should().Be(2); + + configurationRoot.Reload(); + + testSourceA.LastProvider.LoadCount.Should().Be(3); + testSourceB.LastProvider.LoadCount.Should().Be(3); + + testSourceA.Id.Should().Be(testSourceIdA); + testSourceB.Id.Should().Be(testSourceIdB); + + lastProviderIdA.Should().Be(testSourceA.LastProvider.Id); + lastProviderIdB.Should().Be(testSourceB.LastProvider.Id); + } + + [Fact] + public void Disposes_owned_providers() + { + var testSourceA = new TestConfigurationSource("A", _loggerFactory); + var testSourceB = new TestConfigurationSource("B", _loggerFactory); + + var builder = new ConfigurationBuilder(); + builder.Add(testSourceA); + builder.Add(testSourceB); + builder.AddPlaceholderResolver(_loggerFactory); + + var configurationRoot = (ConfigurationRoot)builder.Build(); + + testSourceA.LastProvider.Should().NotBeNull(); + testSourceB.LastProvider.Should().NotBeNull(); + + testSourceA.LastProvider!.DisposeCount.Should().Be(0); + testSourceB.LastProvider!.DisposeCount.Should().Be(0); + + configurationRoot.Dispose(); + + testSourceA.LastProvider.DisposeCount.Should().Be(1); + testSourceB.LastProvider.DisposeCount.Should().Be(1); + +#pragma warning disable S3966 // Objects should not be disposed more than once + configurationRoot.Dispose(); +#pragma warning restore S3966 // Objects should not be disposed more than once + + testSourceA.LastProvider.DisposeCount.Should().Be(1); + testSourceB.LastProvider.DisposeCount.Should().Be(1); + } + + [Fact] + public void Configuration_rebuild_creates_new_providers() + { + var testSourceA = new TestConfigurationSource("A", _loggerFactory); + Guid testSourceIdA = testSourceA.Id; + var testSourceB = new TestConfigurationSource("B", _loggerFactory); + Guid testSourceIdB = testSourceB.Id; + + var builder = new ConfigurationBuilder(); + builder.Add(testSourceA); + builder.Add(testSourceB); + builder.AddPlaceholderResolver(_loggerFactory); + + TestConfigurationProvider previousProviderA; + TestConfigurationProvider previousProviderB; + + using ((ConfigurationRoot)builder.Build()) + { + previousProviderA = testSourceA.LastProvider!; + previousProviderB = testSourceA.LastProvider!; + } + + _ = builder.Build(); + + TestConfigurationProvider nextProviderA = testSourceA.LastProvider!; + TestConfigurationProvider nextProviderB = testSourceA.LastProvider!; + + previousProviderA.Id.Should().NotBe(nextProviderA.Id); + previousProviderB.Id.Should().NotBe(nextProviderB.Id); + + previousProviderA.LoadCount.Should().Be(1); + previousProviderB.LoadCount.Should().Be(1); + + previousProviderA.DisposeCount.Should().Be(1); + previousProviderB.DisposeCount.Should().Be(1); + + nextProviderA.LoadCount.Should().Be(1); + nextProviderB.LoadCount.Should().Be(1); + + nextProviderA.DisposeCount.Should().Be(0); + nextProviderB.DisposeCount.Should().Be(0); + + testSourceA.Id.Should().Be(testSourceIdA); + testSourceB.Id.Should().Be(testSourceIdB); + } + + [Fact] + public void Forwards_key_lookups_to_owned_providers() + { + var testSourceA = new TestConfigurationSource("A", _loggerFactory); + var testSourceB = new TestConfigurationSource("B", _loggerFactory); + + var builder = new ConfigurationBuilder(); + builder.Add(testSourceA); + builder.Add(testSourceB); + builder.AddPlaceholderResolver(_loggerFactory); + IConfigurationRoot configurationRoot = builder.Build(); + + testSourceA.LastProvider.Should().NotBeNull(); + testSourceB.LastProvider.Should().NotBeNull(); + + testSourceA.LastProvider!.Set("testRoot:keyA", "valueA"); + testSourceB.LastProvider!.Set("testRoot:keyB", "valueB"); + + configurationRoot["testRoot:keyA"].Should().Be("valueA"); + configurationRoot["testRoot:keyB"].Should().Be("valueB"); + + testSourceA.LastProvider.Set("testRoot:keyA", "alt-valueA"); + testSourceB.LastProvider.Set("testRoot:keyB", null); + + configurationRoot["testRoot:keyA"].Should().Be("alt-valueA"); + configurationRoot["testRoot:keyB"].Should().BeNull(); + } + + [Fact] + public void Substitutes_placeholders_in_key_lookups() + { + var appSettings = new Dictionary + { + ["key1"] = "value1", + ["key2"] = "${key1?not-found}", + ["key3"] = "${no-key?not-found}", + ["key4"] = "${no-key}", + ["key5"] = string.Empty + }; + + var builder = new ConfigurationBuilder(); + builder.AddInMemoryCollection(appSettings); + builder.AddPlaceholderResolver(_loggerFactory); + IConfiguration configuration = builder.Build(); + + configuration["no-key"].Should().BeNull(); + configuration["key1"].Should().Be("value1"); + configuration["key2"].Should().Be("value1"); + configuration["key3"].Should().Be("not-found"); + configuration["key4"].Should().Be("${no-key}"); + configuration["key5"].Should().Be(string.Empty); + + configuration["no-key"] = "new-key-value"; + + configuration["key3"].Should().Be("new-key-value"); + configuration["key4"].Should().Be("new-key-value"); + } + + [Fact] + public void Substitutes_placeholders_in_section_lookups() + { + var appSettings = new Dictionary + { + ["appName"] = "test", + ["one"] = "value1-${appName}", + ["one:two"] = "value2-${appName}", + ["one:two:three"] = "value3-${appName}", + ["one:two:three:four"] = "value4-${appName}" + }; + + var builder = new ConfigurationBuilder(); + builder.AddInMemoryCollection(appSettings); + builder.AddPlaceholderResolver(_loggerFactory); + IConfiguration configuration = builder.Build(); + + IConfigurationSection oneSection = configuration.GetSection("one"); + oneSection.Path.Should().Be("one"); + oneSection.Key.Should().Be("one"); + oneSection.Value.Should().Be("value1-test"); + oneSection.GetChildren().Should().HaveCount(1); + + IConfigurationSection twoSection = oneSection.GetSection("two"); + twoSection.Path.Should().Be("one:two"); + twoSection.Key.Should().Be("two"); + twoSection.Value.Should().Be("value2-test"); + twoSection.GetChildren().Should().HaveCount(1); + + IConfigurationSection threeSection = twoSection.GetSection("three"); + threeSection.Path.Should().Be("one:two:three"); + threeSection.Value.Should().Be("value3-test"); + threeSection.GetChildren().Should().HaveCount(1); + + IConfigurationSection fourSection = threeSection.GetSection("four"); + fourSection.Path.Should().Be("one:two:three:four"); + fourSection.Value.Should().Be("value4-test"); + fourSection.GetChildren().Should().BeEmpty(); + } + + [Fact] + public void Substitutes_recursive_placeholders() + { + var appSettings = new Dictionary + { + ["TestRoot:Key1"] = "1:FinalValue", + ["TestRoot:Key2"] = "2:${TestRoot:Key1}", + ["TestRoot:Key3"] = "3:${TestRoot:Key2}", + ["TestRoot:Key4"] = "4:${TestRoot:Key3}", + ["TestRoot:Key5"] = "5:${TestRoot:Key4}" + }; + + var builder = new ConfigurationBuilder(); + builder.AddInMemoryCollection(appSettings); + builder.AddPlaceholderResolver(_loggerFactory); + IConfiguration configuration = builder.Build(); + + configuration["TestRoot:Key5"].Should().Be("5:4:3:2:1:FinalValue"); + } + + [Fact] + public void Throws_on_self_reference() + { + var appSettings = new Dictionary + { + ["TestRoot:Key1"] = "${TestRoot:Key1}" + }; + + var builder = new ConfigurationBuilder(); + builder.AddInMemoryCollection(appSettings); + builder.AddPlaceholderResolver(_loggerFactory); + IConfiguration configuration = builder.Build(); + + Action action = () => _ = configuration["TestRoot:Key1"]; + + action.Should().ThrowExactly().WithMessage("Found circular placeholder reference 'TestRoot:Key1' in configuration."); + } + + [Fact] + public void Throws_on_circular_reference() + { + var appSettings = new Dictionary + { + ["TestRoot:Key1"] = "1:${TestRoot:Key5}", + ["TestRoot:Key2"] = "2:${TestRoot:Key1}", + ["TestRoot:Key3"] = "3:${TestRoot:Key2}", + ["TestRoot:Key4"] = "4:${TestRoot:Key3}", + ["TestRoot:Key5"] = "5:${TestRoot:Key4}" + }; + + var builder = new ConfigurationBuilder(); + builder.AddInMemoryCollection(appSettings); + builder.AddPlaceholderResolver(_loggerFactory); + IConfiguration configuration = builder.Build(); + + Action action = () => _ = configuration["TestRoot:Key4"]; + + action.Should().ThrowExactly().WithMessage("Found circular placeholder reference 'TestRoot:Key3' in configuration."); + } + + [Theory] + [InlineData(1)] + [InlineData(3)] + public void Reloads_options_on_change(int placeholderCount) + { + const string fileName = "appsettings.json"; + MemoryFileProvider fileProvider = new(); + + fileProvider.IncludeFile(fileName, """ + { + "TestRoot": { + "Value": "valueA" + } + } + """); + + var builder = new ConfigurationBuilder(); + builder.AddJsonFile(fileProvider, fileName, false, true); + +#pragma warning disable SA1312 // Variable names should begin with lower-case letter + foreach (int _ in Enumerable.Repeat(0, placeholderCount)) +#pragma warning restore SA1312 // Variable names should begin with lower-case letter + { + builder.AddPlaceholderResolver(_loggerFactory); + } + + AssertTypesInSourceTree(builder.Sources, placeholderCount); + + IConfiguration configuration = builder.Build(); + + var services = new ServiceCollection(); + services.AddSingleton(_loggerFactory); + services.AddLogging(); + services.AddSingleton(configuration); + services.Configure(configuration.GetSection("TestRoot")); + services.AddSingleton, ConfigureTestOptions>(); + + ServiceProvider serviceProvider = services.BuildServiceProvider(); + + var configurer = (ConfigureTestOptions)serviceProvider.GetRequiredService>(); + var optionsMonitor = serviceProvider.GetRequiredService>(); + + configurer.ConfigureCount.Should().Be(0); + optionsMonitor.CurrentValue.Value.Should().Be("valueA"); + _ = optionsMonitor.CurrentValue; + configurer.ConfigureCount.Should().Be(1); + + fileProvider.ReplaceFile(fileName, """ + { + "TestRoot": { + "Value": "valueB" + } + } + """); + + fileProvider.NotifyChanged(); + + configurer.ConfigureCount.Should().Be(2); + optionsMonitor.CurrentValue.Value.Should().Be("valueB"); + _ = optionsMonitor.CurrentValue; + configurer.ConfigureCount.Should().Be(2); + + static void AssertTypesInSourceTree(IList sources, int index) + { + while (true) + { + sources.Should().HaveCount(1); + + if (index > 0) + { + PlaceholderConfigurationSource placeholderSource = sources[0].Should().BeOfType().Subject; + sources = placeholderSource.Sources; + index -= 1; + } + else + { + sources[0].Should().BeOfType(); + break; + } + } + } + } + + public void Dispose() + { + _loggerFactory.Dispose(); + } +} diff --git a/src/Configuration/test/Placeholder.Test/PlaceholderResolverExtensionsTest.cs b/src/Configuration/test/Placeholder.Test/PlaceholderResolverExtensionsTest.cs deleted file mode 100644 index 1b32cfde28..0000000000 --- a/src/Configuration/test/Placeholder.Test/PlaceholderResolverExtensionsTest.cs +++ /dev/null @@ -1,204 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information. - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Steeltoe.Common.TestResources; -using Steeltoe.Common.TestResources.IO; - -namespace Steeltoe.Configuration.Placeholder.Test; - -public sealed class PlaceholderResolverExtensionsTest -{ - [Fact] - public void ConfigurePlaceholderResolver_ConfiguresIConfiguration_ReplacesExisting() - { - var settings = new Dictionary - { - { "key1", "value1" }, - { "key2", "${key1?notfound}" }, - { "key3", "${nokey?notfound}" }, - { "key4", "${nokey}" } - }; - - var builder = new ConfigurationBuilder(); - builder.AddInMemoryCollection(settings); - IConfigurationRoot configuration1 = builder.Build(); - - IWebHostBuilder hostBuilder = TestWebHostBuilderFactory.Create(); - hostBuilder.UseStartup(); - hostBuilder.UseConfiguration(configuration1); - hostBuilder.ConfigureServices((context, services) => services.ConfigurePlaceholderResolver(context.Configuration)); - - using var server = new TestServer(hostBuilder); - var configuration2 = server.Services.GetRequiredService(); - Assert.NotSame(configuration1, configuration2); - - Assert.Null(configuration2["nokey"]); - Assert.Equal("value1", configuration2["key1"]); - Assert.Equal("value1", configuration2["key2"]); - Assert.Equal("notfound", configuration2["key3"]); - Assert.Equal("${nokey}", configuration2["key4"]); - } - - [Fact] - public void AddPlaceholderResolver_WebHostBuilder_WrapsApplicationsConfiguration() - { - const string appsettingsJson = """ - { - "spring": { - "json": { - "name": "myName" - }, - "cloud": { - "config": { - "name": "${spring:xml:name?noname}" - } - } - } - } - """; - - const string appsettingsXml = """ - - - - ${spring:ini:name?noName} - - - - """; - - const string appsettingsIni = """ - [spring:ini] - name=${spring:line:name?noName} - """; - - string[] appsettingsLine = ["--spring:line:name=${spring:json:name?noName}"]; - - using var sandbox = new Sandbox(); - string jsonPath = sandbox.CreateFile("appsettings.json", appsettingsJson); - string jsonFileName = Path.GetFileName(jsonPath); - string xmlPath = sandbox.CreateFile("appsettings.xml", appsettingsXml); - string xmlFileName = Path.GetFileName(xmlPath); - string iniPath = sandbox.CreateFile("appsettings.ini", appsettingsIni); - string iniFileName = Path.GetFileName(iniPath); - - string directory = Path.GetDirectoryName(jsonPath)!; - - IWebHostBuilder hostBuilder = TestWebHostBuilderFactory.Create().UseStartup().ConfigureAppConfiguration(configurationBuilder => - { - configurationBuilder.SetBasePath(directory); - configurationBuilder.AddJsonFile(jsonFileName); - configurationBuilder.AddXmlFile(xmlFileName); - configurationBuilder.AddIniFile(iniFileName); - configurationBuilder.AddCommandLine(appsettingsLine); - configurationBuilder.AddPlaceholderResolver(); - }); - - using var server = new TestServer(hostBuilder); - var configuration = server.Services.GetRequiredService(); - Assert.Equal("myName", configuration["spring:cloud:config:name"]); - } - - [Fact] - public void AddPlaceholderResolver_HostBuilder_WrapsApplicationsConfiguration() - { - const string appsettingsJson = """ - { - "spring": { - "json": { - "name": "myName" - }, - "cloud": { - "config": { - "name": "${spring:xml:name?noname}" - } - } - } - } - """; - - const string appsettingsXml = """ - - - - ${spring:json:name?noName} - - - - """; - - using var sandbox = new Sandbox(); - string jsonPath = sandbox.CreateFile("appsettings.json", appsettingsJson); - string jsonFileName = Path.GetFileName(jsonPath); - string xmlPath = sandbox.CreateFile("appsettings.xml", appsettingsXml); - string xmlFileName = Path.GetFileName(xmlPath); - string directory = Path.GetDirectoryName(jsonPath)!; - - IHostBuilder hostBuilder = TestHostBuilderFactory.Create().ConfigureWebHost(configure => configure.UseTestServer()).ConfigureAppConfiguration( - configurationBuilder => - { - configurationBuilder.SetBasePath(directory); - configurationBuilder.AddJsonFile(jsonFileName); - configurationBuilder.AddXmlFile(xmlFileName); - configurationBuilder.AddPlaceholderResolver(); - }); - - using IHost host = hostBuilder.Build(); - using TestServer server = host.GetTestServer(); - var configuration = server.Services.GetRequiredService(); - Assert.Equal("myName", configuration["spring:cloud:config:name"]); - } - - [Fact] - public async Task AddPlaceholderResolverViaWebApplicationBuilderWorks() - { - const string appsettingsJson = """ - { - "spring": { - "json": { - "name": "myName" - }, - "cloud": { - "config": { - "name": "${spring:xml:name?noname}" - } - } - } - } - """; - - const string appsettingsXml = """ - - - - ${spring:json:name?noName} - - - - """; - - using var sandbox = new Sandbox(); - string jsonPath = sandbox.CreateFile("appsettings.json", appsettingsJson); - string jsonFileName = Path.GetFileName(jsonPath); - string xmlPath = sandbox.CreateFile("appsettings.xml", appsettingsXml); - string xmlFileName = Path.GetFileName(xmlPath); - string directory = Path.GetDirectoryName(jsonPath)!; - - WebApplicationBuilder hostBuilder = TestWebApplicationBuilderFactory.Create(); - hostBuilder.Configuration.SetBasePath(directory); - hostBuilder.Configuration.AddJsonFile(jsonFileName); - hostBuilder.Configuration.AddXmlFile(xmlFileName); - hostBuilder.Configuration.AddPlaceholderResolver(); - - await using WebApplication server = hostBuilder.Build(); - var configuration = server.Services.GetRequiredService(); - Assert.Equal("myName", configuration["spring:cloud:config:name"]); - } -} diff --git a/src/Configuration/test/Placeholder.Test/PlaceholderResolverProviderTest.cs b/src/Configuration/test/Placeholder.Test/PlaceholderResolverProviderTest.cs deleted file mode 100644 index b4a6f809e3..0000000000 --- a/src/Configuration/test/Placeholder.Test/PlaceholderResolverProviderTest.cs +++ /dev/null @@ -1,349 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information. - -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Primitives; -using Steeltoe.Common.TestResources.IO; - -namespace Steeltoe.Configuration.Placeholder.Test; - -public sealed class PlaceholderResolverProviderTest -{ - [Fact] - public void Constructor_WithConfiguration() - { - IConfigurationRoot configurationRoot = new ConfigurationBuilder().Build(); - - var provider = new PlaceholderResolverProvider(configurationRoot, NullLoggerFactory.Instance); - - Assert.NotNull(provider.Configuration); - Assert.Empty(provider.Providers); - } - - [Fact] - public void Constructor_WithProviders() - { - List providers = []; - var provider = new PlaceholderResolverProvider(providers, NullLoggerFactory.Instance); - - Assert.Null(provider.Configuration); - Assert.Same(providers, provider.Providers); - } - - [Fact] - public void TryGet_ReturnsResolvedValues() - { - var settings = new Dictionary - { - { "key1", "value1" }, - { "key2", "${key1?notfound}" }, - { "key3", "${nokey?notfound}" }, - { "key4", "${nokey}" } - }; - - var builder = new ConfigurationBuilder(); - builder.AddInMemoryCollection(settings); - List providers = builder.Build().Providers.ToList(); - - var holder = new PlaceholderResolverProvider(providers, NullLoggerFactory.Instance); - - Assert.False(holder.TryGet("nokey", out _)); - Assert.True(holder.TryGet("key1", out string? value)); - Assert.Equal("value1", value); - Assert.True(holder.TryGet("key2", out value)); - Assert.Equal("value1", value); - Assert.True(holder.TryGet("key3", out value)); - Assert.Equal("notfound", value); - Assert.True(holder.TryGet("key4", out value)); - Assert.Equal("${nokey}", value); - } - - [Fact] - public void Set_SetsValues_ReturnsResolvedValues() - { - var settings = new Dictionary - { - { "key1", "value1" }, - { "key2", "${key1?notfound}" }, - { "key3", "${nokey?notfound}" }, - { "key4", "${nokey}" } - }; - - var builder = new ConfigurationBuilder(); - builder.AddInMemoryCollection(settings); - List providers = builder.Build().Providers.ToList(); - - var holder = new PlaceholderResolverProvider(providers, NullLoggerFactory.Instance); - - Assert.False(holder.TryGet("nokey", out _)); - Assert.True(holder.TryGet("key1", out string? value)); - Assert.Equal("value1", value); - Assert.True(holder.TryGet("key2", out value)); - Assert.Equal("value1", value); - Assert.True(holder.TryGet("key3", out value)); - Assert.Equal("notfound", value); - Assert.True(holder.TryGet("key4", out value)); - Assert.Equal("${nokey}", value); - - holder.Set("nokey", "nokeyvalue"); - Assert.True(holder.TryGet("key3", out value)); - Assert.Equal("nokeyvalue", value); - Assert.True(holder.TryGet("key4", out value)); - Assert.Equal("nokeyvalue", value); - } - - [Fact] - public async Task GetReloadToken_ReturnsExpected_NotifyChanges() - { - const string appsettings1 = """ - { - "spring": { - "bar": { - "name": "myName" - }, - "cloud": { - "config": { - "name": "${spring:bar:name?noname}" - } - } - } - } - """; - - const string appsettings2 = """ - { - "spring": { - "bar": { - "name": "newMyName" - }, - "cloud": { - "config": { - "name": "${spring:bar:name?noname}" - } - } - } - } - """; - - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile("appsettings.json", appsettings1); - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.SetBasePath(directory); - - configurationBuilder.AddJsonFile(fileName, false, true); - - IConfigurationRoot configurationRoot = configurationBuilder.Build(); - - var holder = new PlaceholderResolverProvider(new List(configurationRoot.Providers), NullLoggerFactory.Instance); - IChangeToken token = holder.GetReloadToken(); - Assert.NotNull(token); - Assert.False(token.HasChanged); - - Assert.True(holder.TryGet("spring:cloud:config:name", out string? value)); - Assert.Equal("myName", value); - - await File.WriteAllTextAsync(path, appsettings2); - - // There is a 250ms delay to detect change - // ASP.NET Core tests use 2000 Sleep for this kind of test - await Task.Delay(2000); - - Assert.True(token.HasChanged); - Assert.True(holder.TryGet("spring:cloud:config:name", out value)); - Assert.Equal("newMyName", value); - } - - [Fact] - public void Load_CreatesConfiguration() - { - var settings = new Dictionary - { - { "key1", "value1" }, - { "key2", "${key1?notfound}" }, - { "key3", "${nokey?notfound}" }, - { "key4", "${nokey}" } - }; - - var builder = new ConfigurationBuilder(); - builder.AddInMemoryCollection(settings); - List providers = builder.Build().Providers.ToList(); - - var holder = new PlaceholderResolverProvider(providers, NullLoggerFactory.Instance); - Assert.Null(holder.Configuration); - holder.Load(); - Assert.NotNull(holder.Configuration); - Assert.Equal("value1", holder.Configuration["key1"]); - } - - [Fact] - public async Task Load_ReloadsConfiguration() - { - const string appsettings1 = """ - { - "spring": { - "bar": { - "name": "myName" - }, - "cloud": { - "config": { - "name": "${spring:bar:name?noname}" - } - } - } - } - """; - - const string appsettings2 = """ - { - "spring": { - "bar": { - "name": "newMyName" - }, - "cloud": { - "config": { - "name": "${spring:bar:name?noname}" - } - } - } - } - """; - - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile("appsettings.json", appsettings1); - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.SetBasePath(directory); - - configurationBuilder.AddJsonFile(fileName, false, true); - - IConfigurationRoot configurationRoot = configurationBuilder.Build(); - - var holder = new PlaceholderResolverProvider(configurationRoot, NullLoggerFactory.Instance); - Assert.True(holder.TryGet("spring:cloud:config:name", out string? value)); - Assert.Equal("myName", value); - - await File.WriteAllTextAsync(path, appsettings2); - await Task.Delay(1000); // There is a 250ms delay - - holder.Load(); - - Assert.True(holder.TryGet("spring:cloud:config:name", out value)); - Assert.Equal("newMyName", value); - } - - [Fact] - public void GetChildKeys_ReturnsResolvableSection() - { - var settings = new Dictionary - { - { "spring:bar:name", "myName" }, - { "spring:cloud:name", "${spring:bar:name?noname}" } - }; - - var builder = new ConfigurationBuilder(); - builder.AddInMemoryCollection(settings); - List providers = builder.Build().Providers.ToList(); - - var holder = new PlaceholderResolverProvider(providers, NullLoggerFactory.Instance); - IEnumerable result = holder.GetChildKeys(Array.Empty(), "spring"); - - Assert.NotNull(result); - List list = result.ToList(); - - Assert.Equal(2, list.Count); - Assert.Contains("bar", list); - Assert.Contains("cloud", list); - } - - [Fact] - public void AdjustConfigManagerBuilder_CorrectlyReflectNewValues() - { - var manager = new ConfigurationManager(); - - var template = new Dictionary - { - { "placeholder", "${value}" } - }; - - var valueProviderA = new Dictionary - { - { "value", "a" } - }; - - var valueProviderB = new Dictionary - { - { "value", "b" } - }; - - manager.AddInMemoryCollection(template); - manager.AddInMemoryCollection(valueProviderA); - manager.AddInMemoryCollection(valueProviderB); - manager.AddPlaceholderResolver(); - string? result = manager.GetValue("placeholder"); - Assert.Equal("b", result); - } - - [Fact] - public void EmptyValuesHandledAsExpected() - { - IConfigurationBuilder builder = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary - { - { "valueIsEmptyString", string.Empty } - }).AddPlaceholderResolver(); - - IConfigurationRoot config = builder.Build(); - - config.AsEnumerable().Should().ContainSingle(); - config["valueIsEmptyString"].Should().BeEmpty(); - - // for comparison, keys not defined return null values - config["undefinedKey"].Should().BeNull(); - } - - [Fact] - public void ConstructorWithConfiguration_Dispose_DisposesChildren() - { - IConfigurationRoot configurationRoot = new ConfigurationBuilder().Add(new DisposableConfigurationSource()).Build(); - DisposableConfigurationProvider disposableConfigurationProvider = configurationRoot.Providers.OfType().Single(); - - var placeholderResolverProvider = new PlaceholderResolverProvider(configurationRoot, NullLoggerFactory.Instance); - - placeholderResolverProvider.Dispose(); - - disposableConfigurationProvider.IsDisposed.Should().BeTrue(); - } - - [Fact] - public void ConstructorWithProviders_Dispose_DisposesChildren() - { - var disposableConfigurationProvider = new DisposableConfigurationProvider(); - var placeholderResolverProvider = new PlaceholderResolverProvider([disposableConfigurationProvider], NullLoggerFactory.Instance); - - placeholderResolverProvider.Dispose(); - - disposableConfigurationProvider.IsDisposed.Should().BeTrue(); - } - - private sealed class DisposableConfigurationSource : IConfigurationSource - { - public IConfigurationProvider Build(IConfigurationBuilder builder) - { - return new DisposableConfigurationProvider(); - } - } - - private sealed class DisposableConfigurationProvider : ConfigurationProvider, IDisposable - { - public bool IsDisposed { get; private set; } - - public void Dispose() - { - IsDisposed = true; - } - } -} diff --git a/src/Configuration/test/Placeholder.Test/PlaceholderResolverSourceTest.cs b/src/Configuration/test/Placeholder.Test/PlaceholderResolverSourceTest.cs deleted file mode 100644 index cc901bba7e..0000000000 --- a/src/Configuration/test/Placeholder.Test/PlaceholderResolverSourceTest.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information. - -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Configuration.Memory; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; - -namespace Steeltoe.Configuration.Placeholder.Test; - -public sealed class PlaceholderResolverSourceTest -{ - [Fact] - public void Constructors_InitializesProperties() - { - var memorySource = new MemoryConfigurationSource(); - List sources = [memorySource]; - - using var factory = new LoggerFactory(); - - var placeholderSource = new PlaceholderResolverSource(sources, factory); - Assert.Equal(factory, placeholderSource.LoggerFactory); - Assert.NotNull(placeholderSource.Sources); - Assert.Single(placeholderSource.Sources); - Assert.NotSame(sources, placeholderSource.Sources); - Assert.Contains(memorySource, placeholderSource.Sources); - } - - [Fact] - public void Build_ReturnsProvider() - { - var memorySource = new MemoryConfigurationSource(); - List sources = [memorySource]; - - var placeholderSource = new PlaceholderResolverSource(sources, NullLoggerFactory.Instance); - IConfigurationProvider provider = placeholderSource.Build(new ConfigurationBuilder()); - Assert.IsType(provider); - } -} diff --git a/src/Configuration/test/Placeholder.Test/PlaceholderWebApplicationTest.cs b/src/Configuration/test/Placeholder.Test/PlaceholderWebApplicationTest.cs new file mode 100644 index 0000000000..89bf59ade2 --- /dev/null +++ b/src/Configuration/test/Placeholder.Test/PlaceholderWebApplicationTest.cs @@ -0,0 +1,194 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. + +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Steeltoe.Common.TestResources; +using Steeltoe.Common.TestResources.IO; +using Xunit.Abstractions; + +namespace Steeltoe.Configuration.Placeholder.Test; + +public sealed class PlaceholderWebApplicationTest : IDisposable +{ + private readonly LoggerFactory _loggerFactory; + + public PlaceholderWebApplicationTest(ITestOutputHelper testOutputHelper) + { + var loggerProvider = new XunitLoggerProvider(testOutputHelper, category => category.StartsWith("Steeltoe", StringComparison.Ordinal)); + _loggerFactory = new LoggerFactory([loggerProvider]); + } + + [Fact] + public async Task Reloads_options_on_change() + { + const string appsettings = """ + { + "TestRoot": { + "AppName": "AppOne" + } + } + """; + + const string fileName = "appsettings.json"; + using var sandbox = new Sandbox(); + string path = sandbox.CreateFile(fileName, appsettings); + + var memorySettings = new Dictionary + { + ["TestRoot:Value"] = "${testRoot:appName}" + }; + + WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); + builder.Services.AddSingleton(_loggerFactory); + builder.Configuration.SetBasePath(sandbox.FullPath); + builder.Configuration.AddInMemoryCollection(memorySettings); + builder.Configuration.AddJsonFile(fileName, false, true); + builder.Configuration.AddPlaceholderResolver(_loggerFactory); + builder.Services.Configure(builder.Configuration.GetSection("TestRoot")); + builder.Services.AddSingleton, ConfigureTestOptions>(); + await using WebApplication app = builder.Build(); + + var optionsMonitor = app.Services.GetRequiredService>(); + optionsMonitor.CurrentValue.Value.Should().Be("AppOne"); + + await File.WriteAllTextAsync(path, """ + { + "TestRoot": { + "AppName": "AppTwo" + } + } + """); + + await Task.Delay(TimeSpan.FromSeconds(2)); + + optionsMonitor.CurrentValue.Value.Should().Be("AppTwo"); + + await File.WriteAllTextAsync(path, """ + { + "TestRoot": { + "AppName": "AppThree" + } + } + """); + + await Task.Delay(TimeSpan.FromSeconds(2)); + + optionsMonitor.CurrentValue.Value.Should().Be("AppThree"); + } + + [Fact] + public void Can_rebuild_sources() + { + var template = new Dictionary + { + { "placeholder", "${value}" } + }; + + var valueProviderA = new Dictionary + { + { "value", "A" } + }; + + var valueProviderB = new Dictionary + { + { "value", "B" } + }; + + WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); + builder.Configuration.Sources.Clear(); + builder.Configuration.AddInMemoryCollection(template); + builder.Configuration.AddInMemoryCollection(valueProviderA); + builder.Configuration.AddInMemoryCollection(valueProviderB); + builder.Configuration.AddPlaceholderResolver(_loggerFactory); + + builder.Configuration["placeholder"].Should().Be("B"); + + var configurationBuilder = (IConfigurationBuilder)builder.Configuration; + + PlaceholderConfigurationSource placeholderSource = configurationBuilder.Sources.OfType().Single(); + placeholderSource.Sources.RemoveAt(2); + + // Trigger a rebuild of all configuration sources in ConfigurationManager, which creates new providers and disposes the existing ones. + configurationBuilder.Properties.Remove(string.Empty); + + builder.Configuration["placeholder"].Should().Be("A"); + } + + [Fact] + public async Task Can_substitute_across_multiple_sources() + { + const string appsettingsJsonFileName = "appsettings.json"; + const string appsettingsXmlFileName = "appsettings.xml"; + const string appsettingsIniFileName = "appsettings.ini"; + + const string appsettingsJsonContent = """ + { + "JsonTestRoot": { + "JsonSubLevel": { + "JsonKey": "JsonValue", + "XmlSource": "JsonTo${XmlTestRoot:XmlSubLevel:XmlKey}" + } + } + } + """; + + const string appsettingsXmlContent = """ + + + + XmlValue + XmlTo${IniTestRoot:IniSubLevel:IniKey} + + + + """; + + const string appsettingsIniContent = """ + [IniTestRoot:IniSubLevel] + IniKey=IniValue + CmdSource=IniTo${CmdTestRoot:CmdSubLevel:CmdKey} + """; + + string[] appsettingsCommandLine = + [ + "--CmdTestRoot:CmdSubLevel:CmdKey=CmdValue", + "--CmdTestRoot:CmdSubLevel:JsonSource=CmdTo${JsonTestRoot:JsonSubLevel:JsonKey}" + ]; + + using var sandbox = new Sandbox(); + sandbox.CreateFile(appsettingsJsonFileName, appsettingsJsonContent); + sandbox.CreateFile(appsettingsXmlFileName, appsettingsXmlContent); + sandbox.CreateFile(appsettingsIniFileName, appsettingsIniContent); + + WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); + builder.Configuration.SetBasePath(sandbox.FullPath); + builder.Configuration.AddJsonFile(appsettingsJsonFileName); + builder.Configuration.AddXmlFile(appsettingsXmlFileName); + builder.Configuration.AddIniFile(appsettingsIniFileName); + builder.Configuration.AddCommandLine(appsettingsCommandLine); + builder.Configuration.AddPlaceholderResolver(_loggerFactory); + + await using WebApplication app = builder.Build(); + var configuration = app.Services.GetRequiredService(); + + configuration["JsonTestRoot:JsonSubLevel:JsonKey"].Should().Be("JsonValue"); + configuration["XmlTestRoot:XmlSubLevel:XmlKey"].Should().Be("XmlValue"); + configuration["IniTestRoot:IniSubLevel:IniKey"].Should().Be("IniValue"); + configuration["CmdTestRoot:CmdSubLevel:CmdKey"].Should().Be("CmdValue"); + + configuration["JsonTestRoot:JsonSubLevel:XmlSource"].Should().Be("JsonToXmlValue"); + configuration["XmlTestRoot:XmlSubLevel:IniSource"].Should().Be("XmlToIniValue"); + configuration["IniTestRoot:IniSubLevel:CmdSource"].Should().Be("IniToCmdValue"); + configuration["CmdTestRoot:CmdSubLevel:JsonSource"].Should().Be("CmdToJsonValue"); + } + + public void Dispose() + { + _loggerFactory.Dispose(); + } +} diff --git a/src/Common/test/Common.Test/Configuration/PropertyPlaceholderHelperTest.cs b/src/Configuration/test/Placeholder.Test/PropertyPlaceholderHelperTest.cs similarity index 73% rename from src/Common/test/Common.Test/Configuration/PropertyPlaceholderHelperTest.cs rename to src/Configuration/test/Placeholder.Test/PropertyPlaceholderHelperTest.cs index e2c793088a..f1b68b888a 100644 --- a/src/Common/test/Common.Test/Configuration/PropertyPlaceholderHelperTest.cs +++ b/src/Configuration/test/Placeholder.Test/PropertyPlaceholderHelperTest.cs @@ -4,10 +4,8 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; -using Steeltoe.Common.Configuration; -using Steeltoe.Common.TestResources.IO; -namespace Steeltoe.Common.Test.Configuration; +namespace Steeltoe.Configuration.Placeholder.Test; public sealed class PropertyPlaceholderHelperTest { @@ -31,6 +29,40 @@ public void ResolvePlaceholders_ResolvesSinglePlaceholder() Assert.Equal("foo=bar", result); } + [Fact] + public void ResolvePlaceholders_ResolvesSinglePlaceholder_WithDefault() + { + const string text = "foo=${foo?empty}"; + + var appSettings = new Dictionary + { + { "foo", "bar" } + }; + + var builder = new ConfigurationBuilder(); + builder.AddInMemoryCollection(appSettings); + IConfiguration configuration = builder.Build(); + + var helper = new PropertyPlaceholderHelper(NullLogger.Instance); + + string? result = helper.ResolvePlaceholders(text, configuration); + Assert.Equal("foo=bar", result); + } + + [Fact] + public void ResolvePlaceholders_ResolvesSinglePlaceholder_UsesDefault() + { + const string text = "foo=${foo?empty}"; + + var builder = new ConfigurationBuilder(); + IConfiguration configuration = builder.Build(); + + var helper = new PropertyPlaceholderHelper(NullLogger.Instance); + + string? result = helper.ResolvePlaceholders(text, configuration); + Assert.Equal("foo=empty", result); + } + [Fact] public void ResolvePlaceholders_ResolvesSingleSpringPlaceholder() { @@ -51,6 +83,40 @@ public void ResolvePlaceholders_ResolvesSingleSpringPlaceholder() Assert.Equal("foo=bar", result); } + [Fact] + public void ResolvePlaceholders_ResolvesSingleSpringPlaceholder_WithDefault() + { + const string text = "foo=${foo.bar?empty}"; + + var appSettings = new Dictionary + { + { "foo:bar", "bar" } + }; + + var builder = new ConfigurationBuilder(); + builder.AddInMemoryCollection(appSettings); + IConfiguration configuration = builder.Build(); + + var helper = new PropertyPlaceholderHelper(NullLogger.Instance); + + string? result = helper.ResolvePlaceholders(text, configuration); + Assert.Equal("foo=bar", result); + } + + [Fact] + public void ResolvePlaceholders_ResolvesSingleSpringPlaceholder_UsesDefault() + { + const string text = "foo=${foo.bar?empty}"; + + var builder = new ConfigurationBuilder(); + IConfiguration configuration = builder.Build(); + + var helper = new PropertyPlaceholderHelper(NullLogger.Instance); + + string? result = helper.ResolvePlaceholders(text, configuration); + Assert.Equal("foo=empty", result); + } + [Fact] public void ResolvePlaceholders_ResolvesMultiplePlaceholders() { @@ -234,93 +300,58 @@ public void ResolvePlaceholders_UnresolvedPlaceholderIsIgnored() [Fact] public void ResolvePlaceholders_ResolvesArrayRefPlaceholder() { - const string json = """ - { - "vcap": { - "application": { - "application_id": "fa05c1a9-0fc1-4fbd-bae1-139850dec7a3", - "application_name": "my-app", - "application_uris": [ - "my-app.10.244.0.34.xip.io" - ], - "application_version": "fb8fbcc6-8d58-479e-bcc7-3b4ce5a7f0ca", - "limits": { - "disk": 1024, - "fds": 16384, - "mem": 256 - }, - "name": "my-app", - "space_id": "06450c72-4669-4dc6-8096-45f9777db68a", - "space_name": "my-space", - "uris": [ - "my-app.10.244.0.34.xip.io", - "my-app2.10.244.0.34.xip.io" - ], - "users": null, - "version": "fb8fbcc6-8d58-479e-bcc7-3b4ce5a7f0ca" - } - } - } - """; - - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile("json", json); - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); - var builder = new ConfigurationBuilder(); - builder.SetBasePath(directory); + const string text = "line=${root:sub:lines[2]}"; - builder.AddJsonFile(fileName); - IConfiguration configuration = builder.Build(); + var appSettings = new Dictionary + { + ["root:sub:lines:0"] = "zero", + ["root:sub:lines:1"] = "one", + ["root:sub:lines:2"] = "two" + }; - const string text = "foo=${vcap:application:uris[1]}"; + var builder = new ConfigurationBuilder(); + builder.AddInMemoryCollection(appSettings); + IConfiguration configuration = builder.Build(); var helper = new PropertyPlaceholderHelper(NullLogger.Instance); string? result = helper.ResolvePlaceholders(text, configuration); - Assert.Equal("foo=my-app2.10.244.0.34.xip.io", result); + Assert.Equal("line=two", result); } [Fact] - public void GetResolvedConfigurationPlaceholders_ReturnsValues_WhenResolved() + public void ResolvePlaceholders_ResolvesArrayRefPlaceholder_WithDefault() { - var builder = new ConfigurationBuilder(); + const string text = "line=${root:sub:lines[2]?empty}"; var appSettings = new Dictionary { - { "foo", "${bar}" }, - { "bar", "baz" } + ["root:sub:lines:0"] = "zero", + ["root:sub:lines:1"] = "one", + ["root:sub:lines:2"] = "two" }; + var builder = new ConfigurationBuilder(); builder.AddInMemoryCollection(appSettings); IConfiguration configuration = builder.Build(); var helper = new PropertyPlaceholderHelper(NullLogger.Instance); - IDictionary resolved = helper.GetResolvedConfigurationPlaceholders(configuration); - - Assert.Contains(resolved, pair => pair.Key == "foo"); - Assert.DoesNotContain(resolved, pair => pair.Key == "bar"); - Assert.Equal("baz", resolved.First(pair => pair.Key == "foo").Value); + string? result = helper.ResolvePlaceholders(text, configuration); + Assert.Equal("line=two", result); } [Fact] - public void GetResolvedConfigurationPlaceholders_ReturnsEmpty_WhenUnResolved() + public void ResolvePlaceholders_ResolvesArrayRefPlaceholder_UsesDefault() { - var appSettings = new Dictionary - { - { "foo", "${bar}" } - }; + const string text = "line=${root:sub:lines[2]?empty}"; var builder = new ConfigurationBuilder(); - builder.AddInMemoryCollection(appSettings); IConfiguration configuration = builder.Build(); var helper = new PropertyPlaceholderHelper(NullLogger.Instance); - IDictionary resolved = helper.GetResolvedConfigurationPlaceholders(configuration); - - Assert.Contains(resolved, pair => pair.Key == "foo"); - Assert.Equal(string.Empty, resolved.First(k => k.Key == "foo").Value); + string? result = helper.ResolvePlaceholders(text, configuration); + Assert.Equal("line=empty", result); } } diff --git a/src/Configuration/test/Placeholder.Test/TestConfigurationProvider.cs b/src/Configuration/test/Placeholder.Test/TestConfigurationProvider.cs new file mode 100644 index 0000000000..3fe7c260fd --- /dev/null +++ b/src/Configuration/test/Placeholder.Test/TestConfigurationProvider.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Steeltoe.Configuration.Placeholder.Test; + +internal sealed partial class TestConfigurationProvider(string name, ILogger logger) : ConfigurationProvider, IDisposable +{ + private readonly string _name = name; + private readonly ILogger _logger = logger; + private long _loadCount; + private long _disposeCount; + + public Guid Id { get; } = Guid.NewGuid(); + public long LoadCount => Interlocked.Read(ref _loadCount); + public long DisposeCount => Interlocked.Read(ref _disposeCount); + + public override void Load() + { + LogLoad(_name); + Interlocked.Increment(ref _loadCount); + } + + public void Dispose() + { + LogDispose(_name); + Interlocked.Increment(ref _disposeCount); + } + + [LoggerMessage(Level = LogLevel.Trace, Message = "Load ({Name}).")] + private partial void LogLoad(string name); + + [LoggerMessage(Level = LogLevel.Trace, Message = "Dispose ({Name}).")] + private partial void LogDispose(string name); +} diff --git a/src/Configuration/test/Placeholder.Test/TestConfigurationSource.cs b/src/Configuration/test/Placeholder.Test/TestConfigurationSource.cs new file mode 100644 index 0000000000..ca6ce53fb3 --- /dev/null +++ b/src/Configuration/test/Placeholder.Test/TestConfigurationSource.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Steeltoe.Configuration.Placeholder.Test; + +internal sealed partial class TestConfigurationSource(string name, ILoggerFactory loggerFactory) : IConfigurationSource +{ + private readonly string _name = name; + private readonly ILogger _logger = loggerFactory.CreateLogger(); + private readonly ILogger _providerLogger = loggerFactory.CreateLogger(); + + public Guid Id { get; } = Guid.NewGuid(); + public TestConfigurationProvider? LastProvider { get; private set; } + + public IConfigurationProvider Build(IConfigurationBuilder builder) + { + LogBuild(_logger, _name); + + LastProvider = new TestConfigurationProvider(_name, _providerLogger); + return LastProvider; + } + + [LoggerMessage(Level = LogLevel.Trace, Message = "Build ({Name}).")] + private static partial void LogBuild(ILogger logger, string name); +} diff --git a/src/Configuration/test/Placeholder.Test/TestServerStartup.cs b/src/Configuration/test/Placeholder.Test/TestOptions.cs similarity index 50% rename from src/Configuration/test/Placeholder.Test/TestServerStartup.cs rename to src/Configuration/test/Placeholder.Test/TestOptions.cs index b3809c9a2b..bbbeca4572 100644 --- a/src/Configuration/test/Placeholder.Test/TestServerStartup.cs +++ b/src/Configuration/test/Placeholder.Test/TestOptions.cs @@ -2,18 +2,9 @@ // The .NET Foundation licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information. -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; - namespace Steeltoe.Configuration.Placeholder.Test; -public sealed class TestServerStartup +internal sealed class TestOptions { - public void ConfigureServices(IServiceCollection services) - { - } - - public void Configure(IApplicationBuilder app) - { - } + public string? Value { get; set; } } diff --git a/src/Configuration/test/Placeholder.Test/xunit.runner.json b/src/Configuration/test/Placeholder.Test/xunit.runner.json deleted file mode 100644 index fdeefaa456..0000000000 --- a/src/Configuration/test/Placeholder.Test/xunit.runner.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "maxParallelThreads": 1, - "parallelizeTestCollections": false -} diff --git a/src/Configuration/test/RandomValue.Test/RandomValueSourceTest.cs b/src/Configuration/test/RandomValue.Test/RandomValueSourceTest.cs index e78c04739a..f07f21378b 100644 --- a/src/Configuration/test/RandomValue.Test/RandomValueSourceTest.cs +++ b/src/Configuration/test/RandomValue.Test/RandomValueSourceTest.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; namespace Steeltoe.Configuration.RandomValue.Test; @@ -13,15 +12,11 @@ public sealed class RandomValueSourceTest [Fact] public void Constructors_InitializesDefaults() { - using var factory = new LoggerFactory(); - - var source = new RandomValueSource(factory); - Assert.Equal(factory, source.LoggerFactory); + var source = new RandomValueSource(NullLoggerFactory.Instance); Assert.NotNull(source.Prefix); Assert.Equal("random:", source.Prefix); - source = new RandomValueSource("foobar:", factory); - Assert.Equal(factory, source.LoggerFactory); + source = new RandomValueSource("foobar:", NullLoggerFactory.Instance); Assert.NotNull(source.Prefix); Assert.Equal("foobar:", source.Prefix); } diff --git a/src/Connectors/src/Connectors/ConnectorConfigurer.cs b/src/Connectors/src/Connectors/ConnectorConfigurer.cs index 2fdd47a77e..9deb873a53 100644 --- a/src/Connectors/src/Connectors/ConnectorConfigurer.cs +++ b/src/Connectors/src/Connectors/ConnectorConfigurer.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using Microsoft.Extensions.Configuration; +using Steeltoe.Configuration; using Steeltoe.Configuration.CloudFoundry.ServiceBinding; using Steeltoe.Configuration.Kubernetes.ServiceBinding; @@ -29,7 +30,7 @@ public static void Configure(IConfigurationBuilder builder, Acti private static bool IsConfigured(IConfigurationBuilder builder) where TPostProcessor : ConnectionStringPostProcessor { - return builder.Sources.OfType().Any(connectionStringSource => + return builder.EnumerateSources().Any(connectionStringSource => connectionStringSource.PostProcessors.Any(postProcessor => postProcessor is TPostProcessor)); } diff --git a/src/Connectors/test/Connectors.Test/KubernetesMemoryServiceBindingsReader.cs b/src/Connectors/test/Connectors.Test/KubernetesMemoryServiceBindingsReader.cs index aeda44220e..dcb0bb108f 100644 --- a/src/Connectors/test/Connectors.Test/KubernetesMemoryServiceBindingsReader.cs +++ b/src/Connectors/test/Connectors.Test/KubernetesMemoryServiceBindingsReader.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using Microsoft.Extensions.FileProviders; +using Steeltoe.Common.TestResources; using Steeltoe.Configuration.Kubernetes.ServiceBinding; namespace Steeltoe.Connectors.Test; diff --git a/src/Management/src/Endpoint/Actuators/Environment/EnvironmentEndpointHandler.cs b/src/Management/src/Endpoint/Actuators/Environment/EnvironmentEndpointHandler.cs index 9511114658..26a38a1e78 100644 --- a/src/Management/src/Endpoint/Actuators/Environment/EnvironmentEndpointHandler.cs +++ b/src/Management/src/Endpoint/Actuators/Environment/EnvironmentEndpointHandler.cs @@ -51,21 +51,10 @@ internal IList GetPropertySources() { List results = []; - if (_configuration is IConfigurationRoot root) + foreach (IConfigurationProvider provider in _configuration.EnumerateProviders()) { - List providers = root.Providers.ToList(); - IPlaceholderResolverProvider? placeholderProvider = providers.OfType().FirstOrDefault(); - - if (placeholderProvider != null) - { - providers.InsertRange(0, placeholderProvider.Providers); - } - - foreach (IConfigurationProvider provider in providers) - { - PropertySourceDescriptor descriptor = GetPropertySourceDescriptor(provider); - results.Add(descriptor); - } + PropertySourceDescriptor descriptor = GetPropertySourceDescriptor(provider); + results.Add(descriptor); } return results; @@ -82,8 +71,9 @@ public PropertySourceDescriptor GetPropertySourceDescriptor(IConfigurationProvid { if (provider.TryGet(key, out string? value)) { - if (provider is IPlaceholderResolverProvider placeholderProvider && !placeholderProvider.ResolvedKeys.Contains(key)) + if (provider is CompositeConfigurationProvider) { + // Wraps other providers, but has no key/value storage of its own. continue; } diff --git a/src/Management/test/Endpoint.Test/Actuators/Environment/EndpointMiddlewareTest.cs b/src/Management/test/Endpoint.Test/Actuators/Environment/EndpointMiddlewareTest.cs index c088b87227..0f3701264e 100644 --- a/src/Management/test/Endpoint.Test/Actuators/Environment/EndpointMiddlewareTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/Environment/EndpointMiddlewareTest.cs @@ -9,11 +9,11 @@ using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Steeltoe.Common.TestResources; -using Steeltoe.Logging.DynamicLogger; +using Steeltoe.Configuration.Encryption; +using Steeltoe.Configuration.Placeholder; using Steeltoe.Management.Endpoint.Actuators.Environment; using Steeltoe.Management.Endpoint.Configuration; @@ -91,22 +91,89 @@ public async Task HandleEnvironmentRequestAsync_ReturnsExpected() [Fact] public async Task EnvironmentActuator_ReturnsExpectedData() { - // Some developers set ASPNETCORE_ENVIRONMENT in their environment, which will break this test if we don't un-set it - using var scope = new EnvironmentVariableScope("ASPNETCORE_ENVIRONMENT", null); + IWebHostBuilder builder = TestWebHostBuilderFactory.Create(); + builder.UseStartup(); + builder.ConfigureAppConfiguration((_, configuration) => configuration.AddInMemoryCollection(AppSettings)); - IWebHostBuilder builder = TestWebHostBuilderFactory.Create().UseStartup() - .ConfigureAppConfiguration((_, configuration) => configuration.AddInMemoryCollection(AppSettings)).ConfigureLogging( - (webHostContext, loggingBuilder) => + using var server = new TestServer(builder); + using HttpClient client = server.CreateClient(); + + HttpResponseMessage response = await client.GetAsync(new Uri("http://localhost/actuator/env")); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + string json = await response.Content.ReadAsStringAsync(); + + json.Should().BeJson(""" + { + "activeProfiles": [ + "Production" + ], + "propertySources": [ + { + "name": "ChainedConfigurationProvider", + "properties": { + "applicationName": { + "value": "Steeltoe.Management.Endpoint.Test" + } + } + }, + { + "name": "EnvironmentVariablesConfigurationProvider", + "properties": { + "applicationName": { + "value": "Steeltoe.Management.Endpoint.Test" + }, + "environment": {}, + "urls": {} + } + }, { - loggingBuilder.AddConfiguration(webHostContext.Configuration); - loggingBuilder.AddDynamicConsole(); - }); + "name": "MemoryConfigurationProvider", + "properties": { + "Logging:Console:IncludeScopes": { + "value": "false" + }, + "Logging:LogLevel:Default": { + "value": "Warning" + }, + "Logging:LogLevel:Pivotal": { + "value": "Information" + }, + "Logging:LogLevel:Steeltoe": { + "value": "Information" + }, + "management:endpoints:actuator:exposure:include:0": { + "value": "*" + }, + "management:endpoints:enabled": { + "value": "true" + } + } + } + ] + } + """); + } + + [Fact] + public async Task EnvironmentActuator_withPlaceholderDecryption_ReturnsExpectedData() + { + IWebHostBuilder builder = TestWebHostBuilderFactory.Create(); + builder.UseStartup(); + + builder.ConfigureAppConfiguration((_, configuration) => + { + configuration.AddInMemoryCollection(AppSettings); + configuration.AddPlaceholderResolver(); + configuration.AddDecryption(); + }); using var server = new TestServer(builder); + using HttpClient client = server.CreateClient(); - HttpClient client = server.CreateClient(); HttpResponseMessage response = await client.GetAsync(new Uri("http://localhost/actuator/env")); Assert.Equal(HttpStatusCode.OK, response.StatusCode); + string json = await response.Content.ReadAsStringAsync(); json.Should().BeJson(""" @@ -115,6 +182,14 @@ public async Task EnvironmentActuator_ReturnsExpectedData() "Production" ], "propertySources": [ + { + "name": "DecryptionConfigurationProvider", + "properties": {} + }, + { + "name": "PlaceholderConfigurationProvider", + "properties": {} + }, { "name": "ChainedConfigurationProvider", "properties": { @@ -123,6 +198,16 @@ public async Task EnvironmentActuator_ReturnsExpectedData() } } }, + { + "name": "EnvironmentVariablesConfigurationProvider", + "properties": { + "applicationName": { + "value": "Steeltoe.Management.Endpoint.Test" + }, + "environment": {}, + "urls": {} + } + }, { "name": "MemoryConfigurationProvider", "properties": { diff --git a/src/Management/test/Endpoint.Test/Steeltoe.Management.Endpoint.Test.csproj b/src/Management/test/Endpoint.Test/Steeltoe.Management.Endpoint.Test.csproj index be61bb63a1..e0b5175ebb 100644 --- a/src/Management/test/Endpoint.Test/Steeltoe.Management.Endpoint.Test.csproj +++ b/src/Management/test/Endpoint.Test/Steeltoe.Management.Endpoint.Test.csproj @@ -26,6 +26,7 @@ +