diff --git a/src/Bootstrap/src/AutoConfiguration/BootstrapScanner.cs b/src/Bootstrap/src/AutoConfiguration/BootstrapScanner.cs index a3c471e64d..5b58a286a8 100644 --- a/src/Bootstrap/src/AutoConfiguration/BootstrapScanner.cs +++ b/src/Bootstrap/src/AutoConfiguration/BootstrapScanner.cs @@ -91,7 +91,7 @@ public void ConfigureSteeltoe() private void WireConfigServer() { - _wrapper.AddConfigServer(_loggerFactory); + _wrapper.AddConfigServer(null, _loggerFactory); LogConfigServerConfigured(); } diff --git a/src/Common/src/Certificates/CertificateOptions.cs b/src/Common/src/Certificates/CertificateOptions.cs index 3b062e0f38..9482b2d792 100644 --- a/src/Common/src/Certificates/CertificateOptions.cs +++ b/src/Common/src/Certificates/CertificateOptions.cs @@ -14,6 +14,20 @@ public sealed class CertificateOptions internal const string ConfigurationKeyPrefix = "Certificates"; public X509Certificate2? Certificate { get; set; } - public IList IssuerChain { get; } = []; + + internal CertificateOptions Clone() + { + var clone = new CertificateOptions + { + Certificate = Certificate + }; + + foreach (X509Certificate2 issuer in IssuerChain) + { + clone.IssuerChain.Add(issuer); + } + + return clone; + } } diff --git a/src/Common/test/Certificates.Test/ConfigureCertificateOptionsTest.cs b/src/Common/test/Certificates.Test/ConfigureCertificateOptionsTest.cs index 2a93ec848b..f14de9eed7 100644 --- a/src/Common/test/Certificates.Test/ConfigureCertificateOptionsTest.cs +++ b/src/Common/test/Certificates.Test/ConfigureCertificateOptionsTest.cs @@ -148,7 +148,7 @@ public async Task CertificateOptions_update_on_changed_contents(string certifica string secondPrivateKeyContent = await File.ReadAllTextAsync("secondInstance.key", TestContext.Current.CancellationToken); using var secondX509 = X509Certificate2.CreateFromPemFile("secondInstance.crt", "secondInstance.key"); string appSettings = BuildAppSettingsJson(certificateName, certificateFilePath, privateKeyFilePath); - string appSettingsPath = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings); + string appSettingsPath = sandbox.CreateFile("appsettings.json", appSettings); var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.AddJsonFile(appSettingsPath, false, true); IConfiguration configuration = configurationBuilder.Build(); @@ -167,7 +167,7 @@ public async Task CertificateOptions_update_on_changed_contents(string certifica await File.WriteAllTextAsync(privateKeyFilePath, secondPrivateKeyContent, TestContext.Current.CancellationToken); using Task pollTask = WaitUntilCertificateChangedToAsync(secondX509, optionsMonitor, certificateName, TestContext.Current.CancellationToken); - await pollTask.WaitAsync(TimeSpan.FromSeconds(1), TestContext.Current.CancellationToken); + await pollTask.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken); optionsMonitor.Get(certificateName).Certificate.Should().Be(secondX509); } @@ -185,7 +185,7 @@ public async Task CertificateOptions_update_on_changed_path(string certificateNa string firstPrivateKeyFilePath = sandbox.CreateFile(Guid.NewGuid() + ".key", firstPrivateKeyContent); using var secondX509 = X509Certificate2.CreateFromPemFile("secondInstance.crt", "secondInstance.key"); string appSettings = BuildAppSettingsJson(certificateName, firstCertificateFilePath, firstPrivateKeyFilePath); - string appSettingsPath = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings); + string appSettingsPath = sandbox.CreateFile("appsettings.json", appSettings); var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.AddJsonFile(appSettingsPath, false, true); IConfiguration configuration = configurationBuilder.Build(); diff --git a/src/Common/test/Common.Test/ApplicationInstanceInfoTest.cs b/src/Common/test/Common.Test/ApplicationInstanceInfoTest.cs index 94f5c04d4b..b38457aaf7 100644 --- a/src/Common/test/Common.Test/ApplicationInstanceInfoTest.cs +++ b/src/Common/test/Common.Test/ApplicationInstanceInfoTest.cs @@ -7,7 +7,6 @@ using Microsoft.Extensions.DependencyInjection; using Steeltoe.Common.Extensions; using Steeltoe.Common.TestResources; -using Steeltoe.Common.TestResources.IO; namespace Steeltoe.Common.Test; @@ -24,7 +23,7 @@ public void ConstructorSetsDefaults() [Fact] public async Task ReadsApplicationConfiguration() { - const string configJson = """ + const string appSettings = """ { "Spring": { "Application": { @@ -34,13 +33,11 @@ public async Task ReadsApplicationConfiguration() } """; - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, configJson); - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsJsonFile(appSettings); + var builder = new ConfigurationBuilder(); - builder.SetBasePath(directory); - builder.AddJsonFile(fileName); + builder.AddInMemoryAppSettingsJsonFile(fileProvider); IConfiguration configuration = builder.Build(); var services = new ServiceCollection(); diff --git a/src/Common/test/TestResources/MemoryFileProvider.cs b/src/Common/test/TestResources/MemoryFileProvider.cs index d8141e3a8a..e28f45a91d 100644 --- a/src/Common/test/TestResources/MemoryFileProvider.cs +++ b/src/Common/test/TestResources/MemoryFileProvider.cs @@ -12,8 +12,6 @@ namespace Steeltoe.Common.TestResources; public sealed class MemoryFileProvider : IFileProvider { - public const string DefaultAppSettingsFileName = "appsettings.json"; - private static readonly char[] DirectorySeparators = [ Path.DirectorySeparatorChar, diff --git a/src/Common/test/TestResources/MemoryFileProviderAppSettingsExtensions.cs b/src/Common/test/TestResources/MemoryFileProviderAppSettingsExtensions.cs new file mode 100644 index 0000000000..de791453a4 --- /dev/null +++ b/src/Common/test/TestResources/MemoryFileProviderAppSettingsExtensions.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. + +namespace Steeltoe.Common.TestResources; + +public static class MemoryFileProviderAppSettingsExtensions +{ + public static void IncludeAppSettingsJsonFile(this MemoryFileProvider fileProvider, string contents) + { + ArgumentNullException.ThrowIfNull(fileProvider); + + fileProvider.IncludeFile(MemoryFileProviderConfigurationBuilderExtensions.AppSettingsJsonFileName, contents); + } + + public static void IncludeAppSettingsXmlFile(this MemoryFileProvider fileProvider, string contents) + { + ArgumentNullException.ThrowIfNull(fileProvider); + + fileProvider.IncludeFile(MemoryFileProviderConfigurationBuilderExtensions.AppSettingsXmlFileName, contents); + } + + public static void IncludeAppSettingsIniFile(this MemoryFileProvider fileProvider, string contents) + { + ArgumentNullException.ThrowIfNull(fileProvider); + + fileProvider.IncludeFile(MemoryFileProviderConfigurationBuilderExtensions.AppSettingsIniFileName, contents); + } + + public static void ReplaceAppSettingsJsonFile(this MemoryFileProvider fileProvider, string contents) + { + ArgumentNullException.ThrowIfNull(fileProvider); + + fileProvider.ReplaceFile(MemoryFileProviderConfigurationBuilderExtensions.AppSettingsJsonFileName, contents); + } +} diff --git a/src/Common/test/TestResources/MemoryFileProviderConfigurationBuilderExtensions.cs b/src/Common/test/TestResources/MemoryFileProviderConfigurationBuilderExtensions.cs new file mode 100644 index 0000000000..fcec5b0ef4 --- /dev/null +++ b/src/Common/test/TestResources/MemoryFileProviderConfigurationBuilderExtensions.cs @@ -0,0 +1,77 @@ +// 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.Ini; +using Microsoft.Extensions.Configuration.Json; +using Microsoft.Extensions.Configuration.Xml; + +namespace Steeltoe.Common.TestResources; + +public static class MemoryFileProviderConfigurationBuilderExtensions +{ + internal const string AppSettingsJsonFileName = "appsettings.json"; + internal const string AppSettingsXmlFileName = "appsettings.xml"; + internal const string AppSettingsIniFileName = "appsettings.ini"; + + public static void AddInMemoryAppSettingsJsonFile(this IConfigurationBuilder builder, MemoryFileProvider fileProvider) + { + AddInMemoryJsonFile(builder, fileProvider, AppSettingsJsonFileName); + } + + public static void AddInMemoryJsonFile(this IConfigurationBuilder builder, MemoryFileProvider fileProvider, string path) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(fileProvider); + ArgumentException.ThrowIfNullOrEmpty(path); + + var source = new JsonConfigurationSource + { + FileProvider = fileProvider, + Path = path, + Optional = false, + ReloadOnChange = true, + // Turn off debounce, so the change token triggers immediately. Then we don't need to sleep in tests. + ReloadDelay = 0 + }; + + builder.Add(source); + } + + public static void AddInMemoryAppSettingsXmlFile(this IConfigurationBuilder builder, MemoryFileProvider fileProvider) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(fileProvider); + + var source = new XmlConfigurationSource + { + FileProvider = fileProvider, + Path = AppSettingsXmlFileName, + Optional = false, + ReloadOnChange = true, + // Turn off debounce, so the change token triggers immediately. Then we don't need to sleep in tests. + ReloadDelay = 0 + }; + + builder.Add(source); + } + + public static void AddInMemoryAppSettingsIniFile(this IConfigurationBuilder builder, MemoryFileProvider fileProvider) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(fileProvider); + + var source = new IniConfigurationSource + { + FileProvider = fileProvider, + Path = AppSettingsIniFileName, + Optional = false, + ReloadOnChange = true, + // Turn off debounce, so the change token triggers immediately. Then we don't need to sleep in tests. + ReloadDelay = 0 + }; + + builder.Add(source); + } +} diff --git a/src/Configuration/src/Abstractions/CompositeConfigurationProvider.cs b/src/Configuration/src/Abstractions/CompositeConfigurationProvider.cs index 0664a92946..dae60f1e3f 100644 --- a/src/Configuration/src/Abstractions/CompositeConfigurationProvider.cs +++ b/src/Configuration/src/Abstractions/CompositeConfigurationProvider.cs @@ -53,10 +53,9 @@ private void Load(bool isReload) public IEnumerable GetChildKeys(IEnumerable earlierKeys, string? parentPath) { + ArgumentNullException.ThrowIfNull(earlierKeys); + 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 if (_logger.IsEnabled(LogLevel.Trace)) { diff --git a/src/Configuration/src/ConfigServer/ConfigServerClientOptions.cs b/src/Configuration/src/ConfigServer/ConfigServerClientOptions.cs index d9ce59f505..f8be0b3b41 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerClientOptions.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerClientOptions.cs @@ -17,10 +17,14 @@ public sealed class ConfigServerClientOptions : IValidateCertificatesOptions private const char CommaDelimiter = ','; internal const string ConfigurationPrefix = "spring:cloud:config"; - internal CertificateOptions ClientCertificate { get; } = new(); internal TimeSpan HttpTimeout => TimeSpan.FromMilliseconds(Timeout); internal bool IsMultiServerConfiguration => Uri != null && Uri.Contains(CommaDelimiter); + /// + /// Gets or sets the client certificate used for mutual TLS authentication with the Config Server. + /// + internal CertificateOptions ClientCertificate { get; set; } = new(); + /// /// Gets or sets a value indicating whether the Config Server provider is enabled. Default value: true. /// @@ -35,7 +39,7 @@ public sealed class ConfigServerClientOptions : IValidateCertificatesOptions /// Gets or sets a comma-separated list of environments used when accessing configuration data. Default value: "Production". /// [ConfigurationKeyName("Env")] - public string? Environment { get; set; } = "Production"; + public string? Environment { get; set; } /// /// Gets or sets a comma-separated list of labels to request from the server. @@ -96,17 +100,17 @@ public bool ValidateCertificatesAlt /// /// Gets retry settings. /// - public ConfigServerRetryOptions Retry { get; } = new(); + public ConfigServerRetryOptions Retry { get; private set; } = new(); /// /// Gets service discovery settings. /// - public ConfigServerDiscoveryOptions Discovery { get; } = new(); + public ConfigServerDiscoveryOptions Discovery { get; private set; } = new(); /// /// Gets health check settings. /// - public ConfigServerHealthOptions Health { get; } = new(); + public ConfigServerHealthOptions Health { get; private set; } = new(); /// /// Gets or sets the address used by the provider to obtain a OAuth Access Token. @@ -129,7 +133,7 @@ public bool ValidateCertificatesAlt public int TokenTtl { get; set; } = 300_000; /// - /// Gets or sets the vault token renew rate (in milliseconds). Default value: 60_000 (1 minute). + /// Gets or sets the Vault token renew rate (in milliseconds). Default value: 60_000 (1 minute). /// public int TokenRenewRate { get; set; } = 60_000; @@ -141,7 +145,52 @@ public bool ValidateCertificatesAlt /// /// Gets headers that will be added to the Config Server request. /// - public IDictionary Headers { get; } = new Dictionary(); + public IDictionary Headers { get; private set; } = new Dictionary(); + + internal ConfigServerClientOptions Clone() + { + return new ConfigServerClientOptions + { + ClientCertificate = ClientCertificate.Clone(), + Enabled = Enabled, + FailFast = FailFast, + Environment = Environment, + Label = Label, + Name = Name, + Uri = Uri, + Username = Username, + Password = Password, + Token = Token, + Timeout = Timeout, + PollingInterval = PollingInterval, + ValidateCertificates = ValidateCertificates, + Retry = new ConfigServerRetryOptions + { + Enabled = Retry.Enabled, + InitialInterval = Retry.InitialInterval, + MaxInterval = Retry.MaxInterval, + Multiplier = Retry.Multiplier, + MaxAttempts = Retry.MaxAttempts + }, + Discovery = new ConfigServerDiscoveryOptions + { + Enabled = Discovery.Enabled, + ServiceId = Discovery.ServiceId + }, + Health = new ConfigServerHealthOptions + { + Enabled = Health.Enabled, + TimeToLive = Health.TimeToLive + }, + AccessTokenUri = AccessTokenUri, + ClientSecret = ClientSecret, + ClientId = ClientId, + TokenTtl = TokenTtl, + TokenRenewRate = TokenRenewRate, + DisableTokenRenewal = DisableTokenRenewal, + Headers = new Dictionary(Headers) + }; + } internal List GetUris() { diff --git a/src/Configuration/src/ConfigServer/ConfigServerConfigurationBuilderExtensions.cs b/src/Configuration/src/ConfigServer/ConfigServerConfigurationBuilderExtensions.cs index 187eff2ff8..19b9ade24e 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerConfigurationBuilderExtensions.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerConfigurationBuilderExtensions.cs @@ -26,7 +26,7 @@ public static class ConfigServerConfigurationBuilderExtensions /// public static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder builder) { - return AddConfigServer(builder, NullLoggerFactory.Instance); + return AddConfigServer(builder, new ConfigServerClientOptions(), null, null, NullLoggerFactory.Instance); } /// @@ -43,9 +43,24 @@ public static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder b /// public static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder builder, ILoggerFactory loggerFactory) { - var options = new ConfigServerClientOptions(); + return AddConfigServer(builder, new ConfigServerClientOptions(), null, null, loggerFactory); + } - return AddConfigServer(builder, options, loggerFactory); + /// + /// Adds a configuration source for Config Server to the . + /// + /// + /// The to add configuration to. + /// + /// + /// The default options, whose values are overridden from . + /// + /// + /// The incoming so that additional calls can be chained. + /// + public static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder builder, ConfigServerClientOptions options) + { + return AddConfigServer(builder, options, null, null, NullLoggerFactory.Instance); } /// @@ -55,7 +70,7 @@ public static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder b /// The to add configuration to. /// /// - /// Enables configuring Config Server from code. + /// The default options, whose values are overridden from . /// /// /// Used for internal logging. Pass to disable logging. @@ -64,6 +79,50 @@ public static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder b /// The incoming so that additional calls can be chained. /// public static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder builder, ConfigServerClientOptions options, ILoggerFactory loggerFactory) + { + return AddConfigServer(builder, options, null, null, loggerFactory); + } + + /// + /// Adds a configuration source for Config Server to the . + /// + /// + /// The to add configuration to. + /// + /// + /// An optional delegate that further configures options from code, after settings from have been applied. + /// + /// + /// The incoming so that additional calls can be chained. + /// + public static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder builder, Action? configure) + { + return AddConfigServer(builder, new ConfigServerClientOptions(), configure, null, NullLoggerFactory.Instance); + } + + /// + /// Adds a configuration source for Config Server to the . + /// + /// + /// The to add configuration to. + /// + /// + /// An optional delegate that further configures options from code, after settings from have been applied. + /// + /// + /// Used for internal logging. Pass to disable logging. + /// + /// + /// The incoming so that additional calls can be chained. + /// + public static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder builder, Action? configure, + ILoggerFactory loggerFactory) + { + return AddConfigServer(builder, new ConfigServerClientOptions(), configure, null, loggerFactory); + } + + internal static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder builder, ConfigServerClientOptions options, + Action? configure, Func? createHttpClientHandler, ILoggerFactory loggerFactory) { ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(options); @@ -74,10 +133,7 @@ public static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder b builder.AddCloudFoundry(); builder.AddKubernetesServiceBindings(); - ConfigServerConfigurationSource source = builder is IConfiguration configuration - ? new ConfigServerConfigurationSource(options, configuration, loggerFactory) - : new ConfigServerConfigurationSource(options, builder.Sources, builder.Properties, loggerFactory); - + var source = new ConfigServerConfigurationSource(options, builder.Sources, builder.Properties, configure, createHttpClientHandler, loggerFactory); builder.Add(source); } diff --git a/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs b/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs index 36be91f368..2246ff3e45 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs @@ -7,6 +7,7 @@ using System.Net.Http.Headers; using System.Net.Http.Json; using System.Net.Sockets; +using System.Runtime.ExceptionServices; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; @@ -14,11 +15,19 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; using Steeltoe.Common.Configuration; using Steeltoe.Common.Discovery; using Steeltoe.Common.Extensions; using Steeltoe.Common.Http; using Steeltoe.Common.Http.HttpClientPooling; +using LockPrimitive = +#if NET10_0_OR_GREATER + System.Threading.Lock +#else + object +#endif + ; namespace Steeltoe.Configuration.ConfigServer; @@ -30,22 +39,27 @@ internal sealed partial class ConfigServerConfigurationProvider : ConfigurationP private const string VaultRenewPath = "vault/v1/auth/token/renew-self"; private const string VaultTokenHeader = "X-Vault-Token"; private const char CommaDelimiter = ','; - internal const string TokenHeader = "X-Config-Token"; - private static readonly string[] EmptyLabels = [string.Empty]; - private readonly ILoggerFactory _loggerFactory; - private readonly ILogger _logger; - private readonly IConfiguration _configuration; - private readonly bool _hasConfiguration; - private readonly bool _ownsHttpClientHandler; + private readonly ILogger _logger; + private readonly Func _createHttpClientHandler; + private readonly bool _disposeHttpClientHandler; private readonly ConfigureConfigServerClientOptions _configurer; - private HttpClientHandler? _httpClientHandler; - - private ConfigServerDiscoveryService? _configServerDiscoveryService; - private Timer? _refreshTimer; - private SemaphoreSlim? _timerTickLock = new(1, 1); + private readonly ConfigServerClientOptions _defaultOptions; + private readonly LockPrimitive _lifecycleLock = new(); + private readonly LockPrimitive _configurationReloadTickLock = new(); + private readonly LockPrimitive _vaultRenewTickLock = new(); + private readonly ConfigServerDiscoveryService _configServerDiscoveryService; + private readonly IDisposable _changeTokenRegistration; + private readonly CancellationTokenSource _shutdownTokenSource = new(); + private readonly CancellationToken _shutdownToken; + + private Timer? _configurationReloadTimer; + private Timer? _vaultRenewTimer; + private volatile DiscoveryLookupResult? _lastDiscoveryLookupResult; + private volatile ConfigServerClientOptions _clientOptions; + private long _isReload; internal static JsonSerializerOptions SerializerOptions { get; } = new() { @@ -54,12 +68,13 @@ internal sealed partial class ConfigServerConfigurationProvider : ConfigurationP PreferredObjectCreationHandling = JsonObjectCreationHandling.Populate }; - internal IDictionary Properties => Data; + internal IDictionary InnerData => Data; /// - /// Gets the configuration settings the provider uses when accessing the server. + /// Gets the settings used to access Config Server, excluding information found during service discovery (so that a provider (re)load properly observes + /// changes and triggers its change token). Returns a cloned snapshot to prevent tearing during reads/writes. /// - public ConfigServerClientOptions ClientOptions { get; } + internal ConfigServerClientOptions ClientOptions => _clientOptions.Clone(); /// /// Initializes a new instance of the class from a . @@ -71,205 +86,309 @@ internal sealed partial class ConfigServerConfigurationProvider : ConfigurationP /// Used for internal logging. Pass to disable logging. /// public ConfigServerConfigurationProvider(ConfigServerConfigurationSource source, ILoggerFactory loggerFactory) - : this(source.DefaultOptions, source.Configuration, null, loggerFactory) + : this(source.DefaultOptions, source.Configuration, source.Configure, source.CreateHttpClientHandler, loggerFactory) { } - internal ConfigServerConfigurationProvider(ConfigServerClientOptions clientOptions, IConfiguration? configuration, HttpClientHandler? httpClientHandler, - ILoggerFactory loggerFactory) + internal ConfigServerConfigurationProvider(ConfigServerClientOptions clientOptions, IConfiguration? configuration, + Action? configure, Func? createHttpClientHandler, ILoggerFactory loggerFactory) { ArgumentNullException.ThrowIfNull(clientOptions); ArgumentNullException.ThrowIfNull(loggerFactory); - _loggerFactory = loggerFactory; - _logger = _loggerFactory.CreateLogger(); + _logger = loggerFactory.CreateLogger(); + _shutdownToken = _shutdownTokenSource.Token; // Don't inline: the token survives disposal, while the source does not. + IConfiguration effectiveConfiguration = configuration ?? new ConfigurationBuilder().Build(); + _configurer = new ConfigureConfigServerClientOptions(effectiveConfiguration, configure); + _configServerDiscoveryService = new ConfigServerDiscoveryService(effectiveConfiguration, loggerFactory); + + _defaultOptions = clientOptions.Clone(); + _clientOptions = _defaultOptions; - if (configuration != null) + if (createHttpClientHandler != null) { - _configuration = configuration; - _hasConfiguration = true; + _createHttpClientHandler = createHttpClientHandler; + _disposeHttpClientHandler = false; } else { - _configuration = new ConfigurationBuilder().Build(); - _hasConfiguration = false; + _createHttpClientHandler = static () => new HttpClientHandler(); + _disposeHttpClientHandler = true; } - _configurer = new ConfigureConfigServerClientOptions(_configuration); + _changeTokenRegistration = ChangeToken.OnChange(effectiveConfiguration.GetReloadToken, () => OnSettingsChanged(true)); + } + + private void OnSettingsChanged(bool skipTimerDueTime) + { + LogEnteringOnSettingsChanged(); - ClientOptions = clientOptions; + ConfigServerClientOptions newOptions = _defaultOptions.Clone(); - if (httpClientHandler == null) + try { - _httpClientHandler = new HttpClientHandler(); - _ownsHttpClientHandler = true; + _configurer.Configure(newOptions); } - else + catch (Exception exception) { - _httpClientHandler = httpClientHandler; + LogBindSettingsFailed(exception); + throw; } - OnSettingsChanged(); - } + lock (_lifecycleLock) + { + if (_shutdownToken.IsCancellationRequested) + { + return; + } - private void OnSettingsChanged() - { - TimeSpan existingPollingInterval = ClientOptions.PollingInterval; + TimeSpan previousPollingInterval = _clientOptions.PollingInterval; + int previousTokenRenewRate = _clientOptions.TokenRenewRate; + + _clientOptions = newOptions; - _configurer.Configure(ClientOptions); + UpdateConfigurationReloadTimer(newOptions, previousPollingInterval, skipTimerDueTime); + UpdateVaultRenewTimer(newOptions, previousTokenRenewRate, skipTimerDueTime); + } + } - if (_hasConfiguration) + private void UpdateConfigurationReloadTimer(ConfigServerClientOptions optionsSnapshot, TimeSpan previousPollingInterval, bool skipDueTime) + { + if (optionsSnapshot.PollingInterval == TimeSpan.Zero || !optionsSnapshot.Enabled) { - _configuration.GetReloadToken().RegisterChangeCallback(_ => OnSettingsChanged(), null); + _configurationReloadTimer?.Dispose(); + _configurationReloadTimer = null; } + else if (_configurationReloadTimer == null) + { + _configurationReloadTimer = new Timer(_ => ConfigurationReloadTimerTick(), null, skipDueTime ? TimeSpan.Zero : optionsSnapshot.PollingInterval, + optionsSnapshot.PollingInterval); + } + else if (previousPollingInterval != optionsSnapshot.PollingInterval) + { + _configurationReloadTimer.Change(skipDueTime ? TimeSpan.Zero : optionsSnapshot.PollingInterval, optionsSnapshot.PollingInterval); + } + } - if (ClientOptions.PollingInterval == TimeSpan.Zero || !ClientOptions.Enabled) + private void UpdateVaultRenewTimer(ConfigServerClientOptions optionsSnapshot, int previousTokenRenewRate, bool skipDueTime) + { + if (string.IsNullOrEmpty(optionsSnapshot.Token) || optionsSnapshot.DisableTokenRenewal || + optionsSnapshot is not { Uri: not null, IsMultiServerConfiguration: false }) { - _refreshTimer?.Dispose(); - _refreshTimer = null; + _vaultRenewTimer?.Dispose(); + _vaultRenewTimer = null; } - else if (ClientOptions.Enabled) + else if (_vaultRenewTimer == null) { - if (_refreshTimer == null) - { -#pragma warning disable S4462 // Calls to "async" methods should not be blocking - // Justification: Configuration sources and providers don't support async. - _refreshTimer = new Timer(_ => DoPolledLoadAsync().GetAwaiter().GetResult(), null, TimeSpan.Zero, ClientOptions.PollingInterval); -#pragma warning restore S4462 // Calls to "async" methods should not be blocking - } - else if (existingPollingInterval != ClientOptions.PollingInterval) - { - _refreshTimer.Change(TimeSpan.Zero, ClientOptions.PollingInterval); - } + TimeSpan refreshInterval = TimeSpan.FromMilliseconds(optionsSnapshot.TokenRenewRate); + _vaultRenewTimer = new Timer(_ => VaultRenewTimerTick(), null, skipDueTime ? TimeSpan.Zero : refreshInterval, refreshInterval); + } + else if (previousTokenRenewRate != optionsSnapshot.TokenRenewRate) + { + TimeSpan refreshInterval = TimeSpan.FromMilliseconds(optionsSnapshot.TokenRenewRate); + _vaultRenewTimer.Change(skipDueTime ? TimeSpan.Zero : refreshInterval, refreshInterval); } } /// - /// DoPolledLoad is called by a Timer callback, so must catch all exceptions. + /// ConfigurationReloadTimerTick is called by a Timer callback, so must catch all exceptions. /// - private async Task DoPolledLoadAsync() + private void ConfigurationReloadTimerTick() { - LogEnteringTimerCycle(); - bool lockTaken = false; + LogEnteringConfigurationReloadCycle(); + +#if NET10_0_OR_GREATER + bool lockTaken = _configurationReloadTickLock.TryEnter(); +#else + bool lockTaken = Monitor.TryEnter(_configurationReloadTickLock); +#endif try { - lockTaken = _timerTickLock != null && await _timerTickLock.WaitAsync(0); + if (!lockTaken || _shutdownToken.IsCancellationRequested) + { + LogSkippingConfigurationReloadCycle(); + return; + } + + LogConfigurationReloadCycleLockObtained(); + ConfigServerClientOptions optionsSnapshot = ClientOptions; + +#pragma warning disable S4462 // Calls to "async" methods should not be blocking + // Justification: Configuration sources and providers don't support async. + UpdateDiscoveryAsync(optionsSnapshot, false, _shutdownToken).GetAwaiter().GetResult(); + DoLoadAsync(optionsSnapshot, true, _shutdownToken).GetAwaiter().GetResult(); +#pragma warning restore S4462 // Calls to "async" methods should not be blocking + + LogConfigurationReloadCycleCompleted(); } - catch (ObjectDisposedException) + catch (Exception exception) { - // Ignore exception originating from potential race condition. + if (!_shutdownToken.IsCancellationRequested) + { + LogConfigurationReloadCycleFailed(exception); + } } - - try + finally { if (lockTaken) { - LogExclusiveLockObtained(); - await DoLoadAsync(true, CancellationToken.None); +#if NET10_0_OR_GREATER + _configurationReloadTickLock.Exit(); +#else + Monitor.Exit(_configurationReloadTickLock); +#endif } - else + } + } + + /// + /// VaultRenewTimerTick is called by a Timer callback, so must catch all exceptions. + /// + private void VaultRenewTimerTick() + { + LogEnteringVaultRenewCycle(); + +#if NET10_0_OR_GREATER + bool lockTaken = _vaultRenewTickLock.TryEnter(); +#else + bool lockTaken = Monitor.TryEnter(_vaultRenewTickLock); +#endif + + try + { + if (!lockTaken || _shutdownToken.IsCancellationRequested) { - LogSkippingCycle(); + LogSkippingVaultRenewCycle(); + return; } + + LogVaultRenewCycleLockObtained(); + +#pragma warning disable S4462 // Calls to "async" methods should not be blocking + // Justification: Configuration sources and providers don't support async. + RefreshVaultTokenAsync(ClientOptions, _shutdownToken).GetAwaiter().GetResult(); +#pragma warning restore S4462 // Calls to "async" methods should not be blocking + + LogVaultRenewCycleCompleted(); } catch (Exception exception) { - LogCouldNotReloadDuringPolling(exception); + if (!_shutdownToken.IsCancellationRequested) + { + LogVaultRenewCycleFailed(exception); + } } finally { if (lockTaken) { - LogTimerCycleCompleted(); - - try - { - _timerTickLock?.Release(); - } - catch (ObjectDisposedException) - { - // Ignore exception originating from potential race condition. - } +#if NET10_0_OR_GREATER + _vaultRenewTickLock.Exit(); +#else + Monitor.Exit(_vaultRenewTickLock); +#endif } } } /// - /// Loads configuration data from the Spring Cloud Configuration Server as specified by the . + /// Loads configuration data from the Spring Cloud Config Server as specified by the . /// public override void Load() { + long previousIsReload = Interlocked.CompareExchange(ref _isReload, 1, 0); + + if (previousIsReload == 0) + { + OnSettingsChanged(false); + } + + ConfigServerClientOptions optionsSnapshot = ClientOptions; + + try + { #pragma warning disable S4462 // Calls to "async" methods should not be blocking - // Justification: Configuration sources and providers don't support async. - LoadInternalAsync(true, CancellationToken.None).GetAwaiter().GetResult(); + // Justification: Configuration sources and providers don't support async. + LoadInternalAsync(optionsSnapshot, true, _shutdownToken).GetAwaiter().GetResult(); #pragma warning restore S4462 // Calls to "async" methods should not be blocking + } + catch (OperationCanceledException) when (_shutdownToken.IsCancellationRequested) + { + // Expected during disposal; silently ignore. + } + catch (ConfigServerException exception) + { + if (optionsSnapshot.FailFast) + { + throw; + } + + LogFetchingRemoteConfigurationFailed(exception); + } } - internal async Task LoadInternalAsync(bool updateDictionary, CancellationToken cancellationToken) + internal async Task LoadInternalAsync(ConfigServerClientOptions optionsSnapshot, bool updateDictionary, + CancellationToken cancellationToken) { - if (!ClientOptions.Enabled) + if (!optionsSnapshot.Enabled) { LogConfigServerClientDisabled(); return null; } - if (IsDiscoveryFirstEnabled()) - { - _configServerDiscoveryService ??= new ConfigServerDiscoveryService(_configuration, ClientOptions, _loggerFactory); - await DiscoverServerInstancesAsync(_configServerDiscoveryService, cancellationToken); - } + await UpdateDiscoveryAsync(optionsSnapshot, optionsSnapshot.FailFast, cancellationToken); - // Adds client settings (e.g. spring:cloud:config:uri, etc.) to the Data dictionary - AddConfigServerClientOptions(); - - if (ClientOptions is { Retry.Enabled: true, FailFast: true }) + if (optionsSnapshot is { Retry.Enabled: true, FailFast: true }) { int attempts = 0; - int backOff = ClientOptions.Retry.InitialInterval; + int backOff = optionsSnapshot.Retry.InitialInterval; + List errors = []; do { - LogFetchingConfiguration(); - try { - return await DoLoadAsync(updateDictionary, cancellationToken); + return await DoLoadAsync(optionsSnapshot, updateDictionary, cancellationToken); } catch (ConfigServerException exception) { - LogFailedFetchingConfiguration(exception); + errors.Add(exception); attempts++; - if (attempts < ClientOptions.Retry.MaxAttempts) + if (attempts < optionsSnapshot.Retry.MaxAttempts) { - Thread.CurrentThread.Join(backOff); - int nextBackOff = (int)(backOff * ClientOptions.Retry.Multiplier); - backOff = Math.Min(nextBackOff, ClientOptions.Retry.MaxInterval); + await Task.Delay(backOff, cancellationToken); + int nextBackOff = (int)(backOff * optionsSnapshot.Retry.Multiplier); + backOff = Math.Min(nextBackOff, optionsSnapshot.Retry.MaxInterval); } else { - throw; + throw new ConfigServerException($"Failed fetching remote configuration from server(s) after {attempts} attempts.", + new AggregateException(null, errors)); } } } while (true); } - LogFetchingConfiguration(); - return await DoLoadAsync(updateDictionary, cancellationToken); + return await DoLoadAsync(optionsSnapshot, updateDictionary, cancellationToken); } - internal async Task DoLoadAsync(bool updateDictionary, CancellationToken cancellationToken) + internal async Task DoLoadAsync(ConfigServerClientOptions optionsSnapshot, bool updateDictionary, CancellationToken cancellationToken) { + LogFetchingRemoteConfiguration(); + + ApplyLastDiscoveryLookupResultToClientOptions(optionsSnapshot); + Exception? error = null; // Get list of Config Server uris to check - List uris = ClientOptions.GetUris(); + List uris = optionsSnapshot.GetUris(); try { - foreach (string label in GetLabels()) + foreach (string label in GetLabels(optionsSnapshot)) { LogProcessingLabel(label); @@ -279,7 +398,7 @@ public override void Load() } // Invoke Config Servers - ConfigEnvironment? env = await RemoteLoadAsync(uris, label, cancellationToken); + ConfigEnvironment? env = await RemoteLoadAsync(optionsSnapshot, uris, label, cancellationToken); // Update configuration Data dictionary with any results if (env != null) @@ -289,6 +408,7 @@ public override void Load() if (updateDictionary) { var data = new Dictionary(StringComparer.OrdinalIgnoreCase); + CopyLastDiscoveryLookupResultToData(data, optionsSnapshot.Discovery.Enabled); if (!string.IsNullOrEmpty(env.State)) { @@ -301,16 +421,12 @@ public override void Load() } IList sources = env.PropertySources; - int index = sources.Count - 1; - for (; index >= 0; index--) + for (int index = sources.Count - 1; index >= 0; index--) { AddPropertySource(sources[index], data); } - // Adds client settings (e.g. spring:cloud:config:uri, etc.) back to the (new) Data dictionary - AddConfigServerClientOptions(data); - if (!AreDictionariesEqual(Data, data)) { LogDataChanged(); @@ -332,15 +448,47 @@ public override void Load() error = exception; } - LogCouldNotLocatePropertySource(error); + throw new ConfigServerException("Failed fetching remote configuration from server(s).", error); + } + + internal void ApplyLastDiscoveryLookupResultToClientOptions(ConfigServerClientOptions optionsSnapshot) + { + DiscoveryLookupResult? lastResult = _lastDiscoveryLookupResult; - if (ClientOptions.FailFast) + if (lastResult != null && optionsSnapshot.Discovery.Enabled) { - LogFailFastEnabled(error); - throw new ConfigServerException("Could not locate PropertySource, fail fast property is set, failing", error); + optionsSnapshot.Uri = lastResult.ConfigServerUri; + + if (lastResult.Username != null) + { + optionsSnapshot.Username = lastResult.Username; + } + + if (lastResult.Password != null) + { + optionsSnapshot.Password = lastResult.Password; + } } + } - return null; + private void CopyLastDiscoveryLookupResultToData(Dictionary data, bool isDiscoveryEnabled) + { + DiscoveryLookupResult? lastResult = _lastDiscoveryLookupResult; + + if (lastResult != null && isDiscoveryEnabled) + { + data["spring:cloud:config:uri"] = lastResult.ConfigServerUri; + + if (lastResult.Username != null) + { + data["spring:cloud:config:username"] = lastResult.Username; + } + + if (lastResult.Password != null) + { + data["spring:cloud:config:password"] = lastResult.Password; + } + } } private static bool AreDictionariesEqual(IDictionary first, Dictionary second) @@ -350,93 +498,92 @@ private static bool AreDictionariesEqual(IDictionary second.ContainsKey(firstKey) && EqualityComparer.Default.Equals(first[firstKey], second[firstKey])); } - internal string[] GetLabels() + internal string[] GetLabels(ConfigServerClientOptions optionsSnapshot) { - if (string.IsNullOrWhiteSpace(ClientOptions.Label)) + if (string.IsNullOrWhiteSpace(optionsSnapshot.Label)) { return EmptyLabels; } - return ClientOptions.Label.Split(CommaDelimiter, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + return optionsSnapshot.Label.Split(CommaDelimiter, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); } - private async Task DiscoverServerInstancesAsync(ConfigServerDiscoveryService configServerDiscoveryService, CancellationToken cancellationToken) + private async Task UpdateDiscoveryAsync(ConfigServerClientOptions optionsSnapshot, bool failFast, CancellationToken cancellationToken) { - IServiceInstance[] instances = (await configServerDiscoveryService.GetConfigServerInstancesAsync(cancellationToken)).ToArray(); - - if (instances.Length == 0) + if (optionsSnapshot.Discovery.Enabled) { - if (ClientOptions.FailFast) + List instances = await _configServerDiscoveryService.GetConfigServerInstancesAsync(optionsSnapshot, cancellationToken); + SetLastDiscoveryLookupResult(instances); + + if (instances.Count == 0 && failFast) { throw new ConfigServerException("Could not locate Config Server via discovery, are you missing a Discovery service assembly?"); } - - return; } - - UpdateSettingsFromDiscovery(instances, ClientOptions); + else + { + SetLastDiscoveryLookupResult([]); + } } - internal void UpdateSettingsFromDiscovery(IEnumerable instances, ConfigServerClientOptions clientOptions) + internal void SetLastDiscoveryLookupResult(IEnumerable instances) { - var endpoints = new StringBuilder(); + var endpointBuilder = new StringBuilder(); + string? username = null; + string? password = null; foreach (IServiceInstance instance in instances) { + if (instance.Metadata.TryGetValue("password", out string? instancePassword)) + { + instance.Metadata.TryGetValue("user", out string? instanceUsername); + username = instanceUsername ?? "user"; + password = instancePassword; + } + string uri = instance.Uri.ToString(); - IReadOnlyDictionary metaData = instance.Metadata; - if (metaData.Count > 0) + if (instance.Metadata.TryGetValue("configPath", out string? path) && path != null) { - if (metaData.TryGetValue("password", out string? password)) + if (uri.EndsWith('/') && path.StartsWith('/')) { - metaData.TryGetValue("user", out string? username); - username ??= "user"; - clientOptions.Username = username; - clientOptions.Password = password; + uri = uri[..^1]; } - if (metaData.TryGetValue("configPath", out string? path) && path != null) - { - if (uri.EndsWith('/') && path.StartsWith('/')) - { - uri = uri[..^1]; - } - - uri += path; - } + uri += path; } - endpoints.Append(uri); - endpoints.Append(','); + endpointBuilder.Append(uri); + endpointBuilder.Append(','); } - if (endpoints.Length > 0) + if (endpointBuilder.Length > 0) { - string uris = endpoints.ToString(0, endpoints.Length - 1); - clientOptions.Uri = uris; + string uris = endpointBuilder.ToString(0, endpointBuilder.Length - 1); + _lastDiscoveryLookupResult = new DiscoveryLookupResult(uris, username, password); + } + else + { + _lastDiscoveryLookupResult = null; } } internal async Task ProvideRuntimeReplacementsAsync(ICollection discoveryClientsFromServiceProvider, CancellationToken cancellationToken) { - if (_configServerDiscoveryService is not null) - { - await _configServerDiscoveryService.ProvideRuntimeReplacementsAsync(discoveryClientsFromServiceProvider, cancellationToken); - } + await _configServerDiscoveryService.ProvideRuntimeReplacementsAsync(discoveryClientsFromServiceProvider, cancellationToken); } internal async Task ShutdownAsync(CancellationToken cancellationToken) { - if (_configServerDiscoveryService is not null) - { - await _configServerDiscoveryService.ShutdownAsync(cancellationToken); - } + await _configServerDiscoveryService.ShutdownAsync(cancellationToken); } /// - /// Creates the that will be used in accessing the Spring Cloud Configuration server. + /// Creates the that will be used in accessing the Spring Cloud Config server. /// + /// + /// A snapshot of the client options to use for this request. + /// /// /// The Uri used when accessing the server. /// @@ -446,7 +593,8 @@ internal async Task ShutdownAsync(CancellationToken cancellationToken) /// /// The HttpRequestMessage built from the path. /// - internal async Task GetRequestMessageAsync(Uri requestUri, CancellationToken cancellationToken) + internal async Task GetRequestMessageAsync(ConfigServerClientOptions optionsSnapshot, Uri requestUri, + CancellationToken cancellationToken) { var uriWithoutUserInfo = new Uri(requestUri.GetComponents(UriComponents.HttpRequestUrl, UriFormat.UriEscaped)); var requestMessage = new HttpRequestMessage(HttpMethod.Get, uriWithoutUserInfo); @@ -460,105 +608,47 @@ internal async Task GetRequestMessageAsync(Uri requestUri, C } else { - if (!string.IsNullOrEmpty(ClientOptions.AccessTokenUri)) + if (!string.IsNullOrEmpty(optionsSnapshot.AccessTokenUri)) { - using HttpClient httpClient = CreateHttpClient(ClientOptions); - var accessTokenUri = new Uri(ClientOptions.AccessTokenUri); + using HttpClient httpClient = CreateHttpClient(optionsSnapshot); + var accessTokenUri = new Uri(optionsSnapshot.AccessTokenUri); string accessToken = - await httpClient.GetAccessTokenAsync(accessTokenUri, ClientOptions.ClientId, ClientOptions.ClientSecret, cancellationToken); + await httpClient.GetAccessTokenAsync(accessTokenUri, optionsSnapshot.ClientId, optionsSnapshot.ClientSecret, cancellationToken); LogAccessTokenFetched(accessTokenUri.ToMaskedString()); requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); } } - if (!string.IsNullOrEmpty(ClientOptions.Token) && ClientOptions is { Uri: not null, IsMultiServerConfiguration: false }) + if (!string.IsNullOrEmpty(optionsSnapshot.Token) && optionsSnapshot is { Uri: not null, IsMultiServerConfiguration: false }) { - if (!ClientOptions.DisableTokenRenewal) - { - RenewToken(); - } - - requestMessage.Headers.Add(TokenHeader, ClientOptions.Token); + requestMessage.Headers.Add(TokenHeader, optionsSnapshot.Token); } return requestMessage; } - /// - /// Adds the client settings for the Configuration Server to the data dictionary. - /// - internal void AddConfigServerClientOptions() - { - Dictionary data = Data.ToDictionary(entry => entry.Key, entry => entry.Value, StringComparer.OrdinalIgnoreCase); - - AddConfigServerClientOptions(data); - - Data = data; - } - - /// - /// Adds the client settings for the Configuration Server to the data dictionary. - /// - /// - /// The client settings to add. - /// - private void AddConfigServerClientOptions(Dictionary data) - { - data["spring:cloud:config:enabled"] = ClientOptions.Enabled.ToString(CultureInfo.InvariantCulture); - data["spring:cloud:config:failFast"] = ClientOptions.FailFast.ToString(CultureInfo.InvariantCulture); - data["spring:cloud:config:env"] = ClientOptions.Environment; - data["spring:cloud:config:label"] = ClientOptions.Label; - data["spring:cloud:config:name"] = ClientOptions.Name; - data["spring:cloud:config:uri"] = ClientOptions.Uri; - data["spring:cloud:config:username"] = ClientOptions.Username; - data["spring:cloud:config:password"] = ClientOptions.Password; - data["spring:cloud:config:token"] = ClientOptions.Token; - data["spring:cloud:config:timeout"] = ClientOptions.Timeout.ToString(CultureInfo.InvariantCulture); - data["spring:cloud:config:pollingInterval"] = ClientOptions.PollingInterval.ToString(null, CultureInfo.InvariantCulture); - data["spring:cloud:config:validateCertificates"] = ClientOptions.ValidateCertificates.ToString(CultureInfo.InvariantCulture); - data["spring:cloud:config:accessTokenUri"] = ClientOptions.AccessTokenUri; - data["spring:cloud:config:clientSecret"] = ClientOptions.ClientSecret; - data["spring:cloud:config:clientId"] = ClientOptions.ClientId; - data["spring:cloud:config:tokenTtl"] = ClientOptions.TokenTtl.ToString(CultureInfo.InvariantCulture); - data["spring:cloud:config:tokenRenewRate"] = ClientOptions.TokenRenewRate.ToString(CultureInfo.InvariantCulture); - data["spring:cloud:config:disableTokenRenewal"] = ClientOptions.DisableTokenRenewal.ToString(CultureInfo.InvariantCulture); - data["spring:cloud:config:retry:enabled"] = ClientOptions.Retry.Enabled.ToString(CultureInfo.InvariantCulture); - data["spring:cloud:config:retry:initialInterval"] = ClientOptions.Retry.InitialInterval.ToString(CultureInfo.InvariantCulture); - data["spring:cloud:config:retry:maxInterval"] = ClientOptions.Retry.MaxInterval.ToString(CultureInfo.InvariantCulture); - data["spring:cloud:config:retry:multiplier"] = ClientOptions.Retry.Multiplier.ToString(CultureInfo.InvariantCulture); - data["spring:cloud:config:retry:maxAttempts"] = ClientOptions.Retry.MaxAttempts.ToString(CultureInfo.InvariantCulture); - data["spring:cloud:config:discovery:enabled"] = ClientOptions.Discovery.Enabled.ToString(CultureInfo.InvariantCulture); - data["spring:cloud:config:discovery:serviceId"] = ClientOptions.Discovery.ServiceId; - data["spring:cloud:config:health:enabled"] = ClientOptions.Health.Enabled.ToString(CultureInfo.InvariantCulture); - data["spring:cloud:config:health:timeToLive"] = ClientOptions.Health.TimeToLive.ToString(CultureInfo.InvariantCulture); - - foreach ((string headerName, string headerValue) in ClientOptions.Headers) - { - data[$"spring:cloud:config:headers:{headerName}"] = headerValue; - } - } - - internal async Task RemoteLoadAsync(List requestUris, string? label, CancellationToken cancellationToken) + internal async Task RemoteLoadAsync(ConfigServerClientOptions optionsSnapshot, List requestUris, string? label, + CancellationToken cancellationToken) { LogRemoteLoadEntered(nameof(RemoteLoadAsync)); // Get client if not already set - using HttpClient httpClient = CreateHttpClient(ClientOptions); + using HttpClient httpClient = CreateHttpClient(optionsSnapshot); Exception? error = null; foreach (Uri requestUri in requestUris) { // Make Config Server URI from settings - Uri uri = BuildConfigServerUri(requestUri, label); + Uri uri = BuildConfigServerUri(optionsSnapshot, requestUri, label); LogTryingToConnect(uri.ToMaskedString()); // Get the request message LogBuildingHttpRequest(); - HttpRequestMessage request = await GetRequestMessageAsync(uri, cancellationToken); + HttpRequestMessage request = await GetRequestMessageAsync(optionsSnapshot, uri, cancellationToken); // Invoke Config Server try @@ -603,7 +693,7 @@ private void AddConfigServerClientOptions(Dictionary data) if (error != null) { - throw error; + ExceptionDispatchInfo.Capture(error).Throw(); } return null; @@ -612,6 +702,9 @@ private void AddConfigServerClientOptions(Dictionary data) /// /// Creates the Uri that will be used in accessing the Configuration Server. /// + /// + /// A snapshot of the client options to use for URI construction. + /// /// /// Base server uri to use. /// @@ -621,23 +714,23 @@ private void AddConfigServerClientOptions(Dictionary data) /// /// The request URI for the Configuration Server. /// - internal Uri BuildConfigServerUri(Uri serverUri, string? label) + internal Uri BuildConfigServerUri(ConfigServerClientOptions optionsSnapshot, Uri serverUri, string? label) { ArgumentNullException.ThrowIfNull(serverUri); var uriBuilder = new UriBuilder(serverUri); - if (!string.IsNullOrEmpty(ClientOptions.Username)) + if (!string.IsNullOrEmpty(optionsSnapshot.Username)) { - uriBuilder.UserName = WebUtility.UrlEncode(ClientOptions.Username); + uriBuilder.UserName = WebUtility.UrlEncode(optionsSnapshot.Username); } - if (!string.IsNullOrEmpty(ClientOptions.Password)) + if (!string.IsNullOrEmpty(optionsSnapshot.Password)) { - uriBuilder.Password = WebUtility.UrlEncode(ClientOptions.Password); + uriBuilder.Password = WebUtility.UrlEncode(optionsSnapshot.Password); } - string pathSuffix = $"{WebUtility.UrlEncode(ClientOptions.Name)}/{WebUtility.UrlEncode(ClientOptions.Environment)}"; + string pathSuffix = $"{WebUtility.UrlEncode(optionsSnapshot.Name)}/{WebUtility.UrlEncode(optionsSnapshot.Environment)}"; if (!string.IsNullOrWhiteSpace(label)) { @@ -698,33 +791,27 @@ private void AddPropertySource(PropertySource? source, Dictionary RefreshVaultTokenAsync(CancellationToken.None).GetAwaiter().GetResult(), null, - TimeSpan.FromMilliseconds(ClientOptions.TokenRenewRate), TimeSpan.FromMilliseconds(ClientOptions.TokenRenewRate)); -#pragma warning restore S4462 // Calls to "async" methods should not be blocking - } - - // fire and forget - internal async Task RefreshVaultTokenAsync(CancellationToken cancellationToken) + /// + /// Extends the lease of the current HashiCorp Vault token; it does not generate a new token. A new token is only picked up when the configuration + /// changes and reconfigures the timer. + /// + internal async Task RefreshVaultTokenAsync(ConfigServerClientOptions optionsSnapshot, CancellationToken cancellationToken) { - if (string.IsNullOrEmpty(ClientOptions.Token)) + if (string.IsNullOrEmpty(optionsSnapshot.Token)) { return; } - string obscuredToken = $"{ClientOptions.Token[..4]}[*]{ClientOptions.Token[^4..]}"; + string obscuredToken = $"{optionsSnapshot.Token[..4]}[*]{optionsSnapshot.Token[^4..]}"; try { - using HttpClient httpClient = CreateHttpClient(ClientOptions); + using HttpClient httpClient = CreateHttpClient(optionsSnapshot); - Uri uri = GetVaultRenewUri(); - HttpRequestMessage message = await GetVaultRenewRequestMessageAsync(uri, cancellationToken); + Uri uri = GetVaultRenewUri(optionsSnapshot); + HttpRequestMessage message = await GetVaultRenewRequestMessageAsync(optionsSnapshot, uri, cancellationToken); - LogRenewingVaultToken(obscuredToken, ClientOptions.TokenTtl, uri.ToMaskedString()); + LogRenewingVaultToken(obscuredToken, optionsSnapshot.TokenTtl, uri.ToMaskedString()); using HttpResponseMessage response = await httpClient.SendAsync(message, cancellationToken); if (response.StatusCode != HttpStatusCode.OK) @@ -738,9 +825,9 @@ internal async Task RefreshVaultTokenAsync(CancellationToken cancellationToken) } } - private Uri GetVaultRenewUri() + private static Uri GetVaultRenewUri(ConfigServerClientOptions optionsSnapshot) { - string baseUri = ClientOptions.Uri!.Split(',')[0].Trim(); + string baseUri = optionsSnapshot.Uri!.Split(',')[0].Trim(); if (!baseUri.EndsWith('/')) { @@ -750,42 +837,38 @@ private Uri GetVaultRenewUri() return new Uri(baseUri + VaultRenewPath, UriKind.RelativeOrAbsolute); } - private async Task GetVaultRenewRequestMessageAsync(Uri requestUri, CancellationToken cancellationToken) + private async Task GetVaultRenewRequestMessageAsync(ConfigServerClientOptions optionsSnapshot, Uri requestUri, + CancellationToken cancellationToken) { var uriWithoutUserInfo = new Uri(requestUri.GetComponents(UriComponents.HttpRequestUrl, UriFormat.UriEscaped)); var requestMessage = new HttpRequestMessage(HttpMethod.Post, uriWithoutUserInfo); - if (!string.IsNullOrEmpty(ClientOptions.AccessTokenUri)) + if (!string.IsNullOrEmpty(optionsSnapshot.AccessTokenUri)) { - using HttpClient httpClient = CreateHttpClient(ClientOptions); - var accessTokenUri = new Uri(ClientOptions.AccessTokenUri); + using HttpClient httpClient = CreateHttpClient(optionsSnapshot); + var accessTokenUri = new Uri(optionsSnapshot.AccessTokenUri); - string accessToken = await httpClient.GetAccessTokenAsync(accessTokenUri, ClientOptions.ClientId, ClientOptions.ClientSecret, cancellationToken); + string accessToken = + await httpClient.GetAccessTokenAsync(accessTokenUri, optionsSnapshot.ClientId, optionsSnapshot.ClientSecret, cancellationToken); LogAccessTokenFetched(accessTokenUri.ToMaskedString()); requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); } - if (!string.IsNullOrEmpty(ClientOptions.Token)) + if (!string.IsNullOrEmpty(optionsSnapshot.Token)) { - requestMessage.Headers.Add(VaultTokenHeader, ClientOptions.Token); + requestMessage.Headers.Add(VaultTokenHeader, optionsSnapshot.Token); } - int renewTtlInSeconds = ClientOptions.TokenTtl / 1000; + int renewTtlInSeconds = optionsSnapshot.TokenTtl / 1000; string json = $"{{\"increment\":{renewTtlInSeconds}}}"; requestMessage.Content = new StringContent(json, Encoding.UTF8, "application/json"); return requestMessage; } - internal bool IsDiscoveryFirstEnabled() - { - IConfigurationSection clientConfigSection = _configuration.GetSection(ConfigServerClientOptions.ConfigurationPrefix); - return clientConfigSection.GetValue("discovery:enabled", ClientOptions.Discovery.Enabled); - } - /// - /// Creates an appropriately configured HttpClient that can be used in communicating with the Spring Cloud Configuration Server. + /// Creates an appropriately configured HttpClient that can be used in communicating with the Spring Cloud Config Server. /// /// /// The settings used to configure the HttpClient. @@ -796,17 +879,11 @@ internal bool IsDiscoveryFirstEnabled() internal HttpClient CreateHttpClient(ConfigServerClientOptions clientOptions) { ArgumentNullException.ThrowIfNull(clientOptions); - ObjectDisposedException.ThrowIf(_httpClientHandler == null, this); - - var clientCertificateConfigurer = new ClientCertificateHttpClientHandlerConfigurer(OptionsMonitorWrapper.Create(clientOptions.ClientCertificate)); - clientCertificateConfigurer.Configure("ConfigServer", _httpClientHandler); - - var validateCertificatesHandler = - new ValidateCertificatesHttpClientHandlerConfigurer(OptionsMonitorWrapper.Create(clientOptions)); - validateCertificatesHandler.Configure(Options.DefaultName, _httpClientHandler); + HttpClientHandler handler = _createHttpClientHandler(); + ConfigureHttpClientHandler(handler, clientOptions); - var httpClient = new HttpClient(_httpClientHandler, false); + var httpClient = new HttpClient(handler, _disposeHttpClientHandler); httpClient.ConfigureForSteeltoe(clientOptions.HttpTimeout); foreach ((string headerName, string headerValue) in clientOptions.Headers) @@ -817,6 +894,19 @@ internal HttpClient CreateHttpClient(ConfigServerClientOptions clientOptions) return httpClient; } + private static void ConfigureHttpClientHandler(HttpClientHandler httpClientHandler, ConfigServerClientOptions optionsSnapshot) + { + httpClientHandler.ClientCertificates.Clear(); + + var clientCertificateConfigurer = new ClientCertificateHttpClientHandlerConfigurer(OptionsMonitorWrapper.Create(optionsSnapshot.ClientCertificate)); + clientCertificateConfigurer.Configure("ConfigServer", httpClientHandler); + + var validateCertificatesHandler = + new ValidateCertificatesHttpClientHandlerConfigurer(OptionsMonitorWrapper.Create(optionsSnapshot)); + + validateCertificatesHandler.Configure(Options.DefaultName, httpClientHandler); + } + private static bool IsSocketError(Exception exception) { return exception is HttpRequestException && exception.InnerException is SocketException; @@ -824,65 +914,107 @@ private static bool IsSocketError(Exception exception) public void Dispose() { - _refreshTimer?.Dispose(); - _refreshTimer = null; + lock (_lifecycleLock) + { + if (_shutdownToken.IsCancellationRequested) + { + return; + } + + LogDisposing(); + _shutdownTokenSource.Cancel(); + _changeTokenRegistration.Dispose(); + ShutdownTimers(); + _shutdownTokenSource.Dispose(); + } + } + + private void ShutdownTimers() + { + // This is fast because in-flight timer callbacks terminate quickly: outstanding HTTP requests are canceled via shutdown token. - _timerTickLock?.Dispose(); - _timerTickLock = null; + using var reloadTimerStopped = new ManualResetEvent(false); + using var vaultTimerStopped = new ManualResetEvent(false); + + if (_configurationReloadTimer == null || !_configurationReloadTimer.Dispose(reloadTimerStopped)) + { + reloadTimerStopped.Set(); + } - if (_ownsHttpClientHandler) + if (_vaultRenewTimer == null || !_vaultRenewTimer.Dispose(vaultTimerStopped)) { - _httpClientHandler?.Dispose(); + vaultTimerStopped.Set(); } - _httpClientHandler = null; + WaitHandle.WaitAll([ + reloadTimerStopped, + vaultTimerStopped + ]); + + _configurationReloadTimer = null; + _vaultRenewTimer = null; } - [LoggerMessage(Level = LogLevel.Trace, Message = "Entering timer cycle.")] - private partial void LogEnteringTimerCycle(); + [LoggerMessage(Level = LogLevel.Trace, Message = "Rebinding options after outer configuration change.")] + private partial void LogEnteringOnSettingsChanged(); - [LoggerMessage(Level = LogLevel.Trace, Message = "Exclusive lock obtained.")] - private partial void LogExclusiveLockObtained(); + [LoggerMessage(Level = LogLevel.Warning, Message = "Failed to bind Config Server options from configuration.")] + private partial void LogBindSettingsFailed(Exception exception); - [LoggerMessage(Level = LogLevel.Trace, Message = "Previous cycle is still running, or already disposed; skipping this cycle.")] - private partial void LogSkippingCycle(); + [LoggerMessage(Level = LogLevel.Debug, Message = "Entering remote configuration reload polling cycle.")] + private partial void LogEnteringConfigurationReloadCycle(); - [LoggerMessage(Level = LogLevel.Warning, Message = "Could not reload configuration during polling.")] - private partial void LogCouldNotReloadDuringPolling(Exception exception); + [LoggerMessage(Level = LogLevel.Trace, Message = "Remote configuration reload polling lock obtained.")] + private partial void LogConfigurationReloadCycleLockObtained(); - [LoggerMessage(Level = LogLevel.Trace, Message = "Timer cycle completed, releasing exclusive lock.")] - private partial void LogTimerCycleCompleted(); + [LoggerMessage(Level = LogLevel.Trace, Message = "Previous remote configuration reload cycle is still running, or already disposed; skipping this cycle.")] + private partial void LogSkippingConfigurationReloadCycle(); - [LoggerMessage(Level = LogLevel.Information, Message = "Config Server client disabled, not fetching configuration.")] - private partial void LogConfigServerClientDisabled(); + [LoggerMessage(Level = LogLevel.Warning, Message = "Failed to reload remote configuration during polling.")] + private partial void LogConfigurationReloadCycleFailed(Exception exception); + + [LoggerMessage(Level = LogLevel.Trace, Message = "Remote configuration reload polling cycle completed, releasing lock.")] + private partial void LogConfigurationReloadCycleCompleted(); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Entering Vault token renewal cycle.")] + private partial void LogEnteringVaultRenewCycle(); + + [LoggerMessage(Level = LogLevel.Trace, Message = "Vault token renewal lock obtained.")] + private partial void LogVaultRenewCycleLockObtained(); + + [LoggerMessage(Level = LogLevel.Trace, Message = "Previous Vault token renewal cycle is still running, or already disposed; skipping this cycle.")] + private partial void LogSkippingVaultRenewCycle(); - [LoggerMessage(Level = LogLevel.Debug, Message = "Fetching configuration from server(s).")] - private partial void LogFetchingConfiguration(); + [LoggerMessage(Level = LogLevel.Warning, Message = "Failed to renew Vault token.")] + private partial void LogVaultRenewCycleFailed(Exception exception); - [LoggerMessage(Level = LogLevel.Warning, Message = "Failed fetching configuration from server(s).")] - private partial void LogFailedFetchingConfiguration(Exception exception); + [LoggerMessage(Level = LogLevel.Trace, Message = "Vault token renewal cycle completed, releasing lock.")] + private partial void LogVaultRenewCycleCompleted(); + + [LoggerMessage(Level = LogLevel.Information, Message = "Config Server client disabled, not fetching remote configuration.")] + private partial void LogConfigServerClientDisabled(); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Fetching remote configuration from server(s).")] + private partial void LogFetchingRemoteConfiguration(); [LoggerMessage(Level = LogLevel.Trace, Message = "Processing label '{Label}'.")] private partial void LogProcessingLabel(string? label); - [LoggerMessage(Level = LogLevel.Debug, Message = "Multiple Config Server Uris listed.")] + [LoggerMessage(Level = LogLevel.Debug, Message = "Multiple Config Server uris listed.")] private partial void LogMultipleConfigServerUris(); [LoggerMessage(Level = LogLevel.Debug, Message = "Located environment with name {Name}, profiles {Profiles}, label {Label}, version {Version} and state {State}.")] private partial void LogEnvironmentLocated(string? name, string profiles, string? label, string? version, string? state); - [LoggerMessage(Level = LogLevel.Trace, Message = "Data has changed, raising configuration reload.")] + [LoggerMessage(Level = LogLevel.Trace, Message = "Remote data has changed, raising configuration reload.")] private partial void LogDataChanged(); - [LoggerMessage(Level = LogLevel.Trace, Message = "Data has not changed.")] + [LoggerMessage(Level = LogLevel.Trace, Message = "Remote data has not changed.")] private partial void LogDataNotChanged(); - [LoggerMessage(Level = LogLevel.Warning, Message = "Could not locate property source.")] - private partial void LogCouldNotLocatePropertySource(Exception? error); - - [LoggerMessage(Level = LogLevel.Trace, Message = "Failure with FailFast enabled, throwing ConfigServerException.")] - private partial void LogFailFastEnabled(Exception? error); + [LoggerMessage(Level = LogLevel.Warning, Message = "Failed fetching remote configuration from server(s).")] + private partial void LogFetchingRemoteConfigurationFailed(Exception? error); [LoggerMessage(Level = LogLevel.Debug, Message = "Adding credentials from '{RequestUri}' to Authorization header.")] private partial void LogAddingCredentials(string requestUri); @@ -922,4 +1054,9 @@ public void Dispose() [LoggerMessage(Level = LogLevel.Error, Message = "Unable to renew Vault token {Token}. The token is likely invalid or has expired.")] private partial void LogUnableToRenewVaultToken(Exception exception, string token); + + [LoggerMessage(Level = LogLevel.Trace, Message = "Disposing Config Server configuration provider.")] + private partial void LogDisposing(); + + private sealed record DiscoveryLookupResult(string ConfigServerUri, string? Username, string? Password); } diff --git a/src/Configuration/src/ConfigServer/ConfigServerConfigurationSource.cs b/src/Configuration/src/ConfigServer/ConfigServerConfigurationSource.cs index 02d2d15f0a..711a9c0796 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerConfigurationSource.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerConfigurationSource.cs @@ -5,7 +5,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using Steeltoe.Common.Certificates; namespace Steeltoe.Configuration.ConfigServer; @@ -13,74 +12,68 @@ internal sealed class ConfigServerConfigurationSource : IConfigurationSource { private readonly ILoggerFactory _loggerFactory; - internal List Sources { get; } = []; - internal Dictionary Properties { get; } = []; + internal List Sources { get; } + internal Dictionary Properties { get; } /// - /// Gets the default settings the Config Server client uses to contact the Config Server. + /// Gets the default options the client uses to contact Config Server. /// - internal ConfigServerClientOptions DefaultOptions { get; } + public ConfigServerClientOptions DefaultOptions { get; } /// - /// Gets the configuration the Config Server client uses to contact the Config Server. Values returned override the default values provided in - /// . + /// Gets the configuration the client uses to contact Config Server. Entries override . /// - internal IConfiguration? Configuration { get; private set; } + public IConfiguration? Configuration { get; private set; } /// - /// Initializes a new instance of the class. + /// Gets an optional delegate that further configures options from code, after settings from have been applied. /// - /// - /// the default settings used by the Config Server client. - /// - /// - /// configuration used by the Config Server client. Values will override those found in default settings. - /// - /// - /// Used for internal logging. Pass to disable logging. - /// - public ConfigServerConfigurationSource(ConfigServerClientOptions defaultOptions, IConfiguration configuration, ILoggerFactory loggerFactory) - { - ArgumentNullException.ThrowIfNull(configuration); - ArgumentNullException.ThrowIfNull(defaultOptions); - ArgumentNullException.ThrowIfNull(loggerFactory); + public Action? Configure { get; } - Configuration = configuration; - DefaultOptions = defaultOptions; - _loggerFactory = loggerFactory; - } + /// + /// Gets an optional factory to create the HTTP client handler, used to mock HTTP requests to Config Server in tests. When provided, the caller is + /// responsible for handler disposal. + /// + public Func? CreateHttpClientHandler { get; } /// /// Initializes a new instance of the class. /// /// - /// the default settings used by the Config Server client. + /// The default options the client uses to contact Config Server. /// /// - /// configuration sources used by the Config Server client. The will be built from these sources and the values will - /// override those found in . + /// Configuration sources the client uses to contact Config Server. The will be built from these, whose entries override + /// . /// /// - /// properties to be used when sources are built. + /// Configuration properties the client uses to contact Config Server. The will be built from these, whose entries override + /// . + /// + /// + /// An optional delegate that further configures options from code, after settings from the built have been applied. + /// + /// + /// An optional factory to create the HTTP client handler, used to mock HTTP requests to Config Server in tests. When provided, the caller is responsible + /// for handler disposal. /// /// /// Used for internal logging. Pass to disable logging. /// public ConfigServerConfigurationSource(ConfigServerClientOptions defaultOptions, IList sources, - IDictionary? properties, ILoggerFactory loggerFactory) + IDictionary? properties, Action? configure, Func? createHttpClientHandler, + ILoggerFactory loggerFactory) { ArgumentNullException.ThrowIfNull(defaultOptions); ArgumentNullException.ThrowIfNull(sources); ArgumentNullException.ThrowIfNull(loggerFactory); Sources = sources.ToList(); - - if (properties != null) - { - Properties = new Dictionary(properties); - } + Properties = properties != null ? new Dictionary(properties) : []; DefaultOptions = defaultOptions; + Configure = configure; + CreateHttpClientHandler = createHttpClientHandler; _loggerFactory = loggerFactory; } @@ -95,37 +88,19 @@ public ConfigServerConfigurationSource(ConfigServerClientOptions defaultOptions, /// public IConfigurationProvider Build(IConfigurationBuilder builder) { - if (Configuration == null) - { - // Create our own builder to build sources - var configurationBuilder = new ConfigurationBuilder(); - - foreach (IConfigurationSource source in Sources) - { - configurationBuilder.Add(source); - } - - // Use properties provided - foreach (KeyValuePair pair in Properties) - { - configurationBuilder.Properties.Add(pair); - } + var configurationBuilder = new ConfigurationBuilder(); - // Create configuration - Configuration = configurationBuilder.Build(); + foreach (IConfigurationSource source in Sources) + { + configurationBuilder.Add(source); } - string? clientCertificatePath = Configuration.GetValue($"{CertificateOptions.ConfigurationKeyPrefix}:ConfigServer:CertificateFilePath"); - - if (!string.IsNullOrEmpty(clientCertificatePath) && DefaultOptions.ClientCertificate.Certificate == null) + foreach (KeyValuePair pair in Properties) { - var certificateConfigurer = new ConfigureCertificateOptions(Configuration); - - var options = new CertificateOptions(); - certificateConfigurer.Configure("ConfigServer", options); - DefaultOptions.ClientCertificate.Certificate = options.Certificate; + configurationBuilder.Properties.Add(pair); } + Configuration = configurationBuilder.Build(); return new ConfigServerConfigurationProvider(this, _loggerFactory); } } diff --git a/src/Configuration/src/ConfigServer/ConfigServerDiscoveryService.cs b/src/Configuration/src/ConfigServer/ConfigServerDiscoveryService.cs index a6b86ee428..595fa993cb 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerDiscoveryService.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerDiscoveryService.cs @@ -12,6 +12,13 @@ using Steeltoe.Discovery.Configuration; using Steeltoe.Discovery.Consul; using Steeltoe.Discovery.Eureka; +using LockPrimitive = +#if NET10_0_OR_GREATER + System.Threading.Lock +#else + object +#endif + ; namespace Steeltoe.Configuration.ConfigServer; @@ -19,29 +26,46 @@ internal sealed partial class ConfigServerDiscoveryService { private static readonly AssemblyLoader AssemblyLoader = new(); private readonly IConfiguration _configuration; - private readonly ConfigServerClientOptions _options; + private readonly ILoggerFactory _loggerFactory; private readonly ILogger _logger; + private readonly LockPrimitive _initLock = new(); private ServiceProvider? _temporaryServiceProviderForDiscoveryClients; + private volatile ICollection? _discoveryClients; - internal ICollection DiscoveryClients { get; private set; } + internal ICollection? DiscoveryClients + { + get => _discoveryClients; + private set => _discoveryClients = value; + } - public ConfigServerDiscoveryService(IConfiguration configuration, ConfigServerClientOptions options, ILoggerFactory loggerFactory) + public ConfigServerDiscoveryService(IConfiguration configuration, ILoggerFactory loggerFactory) { ArgumentNullException.ThrowIfNull(configuration); - ArgumentNullException.ThrowIfNull(options); ArgumentNullException.ThrowIfNull(loggerFactory); _configuration = configuration; - _options = options; + _loggerFactory = loggerFactory; _logger = loggerFactory.CreateLogger(); - DiscoveryClients = SetupDiscoveryClients(loggerFactory); } - // Create discovery clients to be used (hopefully only) during startup - private IDiscoveryClient[] SetupDiscoveryClients(ILoggerFactory loggerFactory) + private void EnsureInitialized() + { + if (_discoveryClients == null) + { + lock (_initLock) + { + if (_discoveryClients == null) + { + SetupDiscoveryClients(); + } + } + } + } + + private void SetupDiscoveryClients() { var tempServices = new ServiceCollection(); - tempServices.AddSingleton(loggerFactory); + tempServices.AddSingleton(_loggerFactory); tempServices.AddSingleton(typeof(ILogger<>), typeof(Logger<>)); // force settings to make sure we don't register the app here @@ -70,7 +94,7 @@ private IDiscoveryClient[] SetupDiscoveryClients(ILoggerFactory loggerFactory) WireEurekaDiscoveryClient(tempServices); } - return GetDiscoveryClientsFromServiceCollection(tempServices); + _discoveryClients = GetDiscoveryClientsFromServiceCollection(tempServices); } [MethodImpl(MethodImplOptions.NoInlining)] @@ -105,23 +129,29 @@ private IDiscoveryClient[] GetDiscoveryClientsFromServiceCollection(ServiceColle return discoveryClients; } - internal async Task> GetConfigServerInstancesAsync(CancellationToken cancellationToken) + internal async Task> GetConfigServerInstancesAsync(ConfigServerClientOptions optionsSnapshot, CancellationToken cancellationToken) { + ArgumentNullException.ThrowIfNull(optionsSnapshot); + + EnsureInitialized(); + int attempts = 0; - int backOff = _options.Retry.InitialInterval; + int backOff = optionsSnapshot.Retry.InitialInterval; List instances = []; do { - LogLocatingConfigServer(_options.Discovery.ServiceId); + LogLocatingConfigServer(optionsSnapshot.Discovery.ServiceId); - if (_options.Discovery.ServiceId != null) + if (optionsSnapshot.Discovery.ServiceId != null) { - foreach (IDiscoveryClient discoveryClient in DiscoveryClients) + foreach (IDiscoveryClient discoveryClient in _discoveryClients ?? []) { try { - IList serviceInstances = await discoveryClient.GetInstancesAsync(_options.Discovery.ServiceId, cancellationToken); + IList serviceInstances = + await discoveryClient.GetInstancesAsync(optionsSnapshot.Discovery.ServiceId, cancellationToken); + instances.AddRange(serviceInstances); } catch (Exception exception) when (!exception.IsCancellation()) @@ -131,18 +161,18 @@ internal async Task> GetConfigServerInstancesAsync } } - if (!_options.Retry.Enabled || instances.Count > 0) + if (!optionsSnapshot.Retry.Enabled || instances.Count > 0) { break; } attempts++; - if (attempts <= _options.Retry.MaxAttempts) + if (attempts <= optionsSnapshot.Retry.MaxAttempts) { - Thread.CurrentThread.Join(backOff); - int nextBackOff = (int)(backOff * _options.Retry.Multiplier); - backOff = Math.Min(nextBackOff, _options.Retry.MaxInterval); + await Task.Delay(backOff, cancellationToken); + int nextBackOff = (int)(backOff * optionsSnapshot.Retry.Multiplier); + backOff = Math.Min(nextBackOff, optionsSnapshot.Retry.MaxInterval); } else { @@ -169,7 +199,7 @@ internal async Task ShutdownAsync(CancellationToken cancellationToken) { if (_temporaryServiceProviderForDiscoveryClients != null) { - foreach (IDiscoveryClient discoveryClient in DiscoveryClients) + foreach (IDiscoveryClient discoveryClient in _discoveryClients ?? []) { await discoveryClient.ShutdownAsync(cancellationToken); } diff --git a/src/Configuration/src/ConfigServer/ConfigServerHealthContributor.cs b/src/Configuration/src/ConfigServer/ConfigServerHealthContributor.cs index 34b6b2f0c2..5517cb3b11 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerHealthContributor.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerHealthContributor.cs @@ -46,12 +46,14 @@ public ConfigServerHealthContributor(IConfiguration configuration, TimeProvider return health; } - if (!IsEnabled()) + ConfigServerClientOptions optionsSnapshot = Provider.ClientOptions; + + if (!optionsSnapshot.Health.Enabled) { return null; } - IList? sources = await GetPropertySourcesAsync(Provider, cancellationToken); + IList? sources = await GetPropertySourcesAsync(Provider, optionsSnapshot, cancellationToken); if (sources == null || sources.Count == 0) { @@ -74,45 +76,46 @@ internal void UpdateHealth(HealthCheckResult health, IList sourc foreach (PropertySource source in sources) { - LogReturningPropertySource(source.Name); names.Add(source.Name); } + LogReturningPropertySources(string.Join(", ", names)); health.Details.Add("propertySources", names); } - internal async Task?> GetPropertySourcesAsync(ConfigServerConfigurationProvider provider, CancellationToken cancellationToken) + internal async Task?> GetPropertySourcesAsync(ConfigServerConfigurationProvider provider, ConfigServerClientOptions optionsSnapshot, + CancellationToken cancellationToken) { long currentTime = _timeProvider.GetUtcNow().ToUnixTimeMilliseconds(); - if (IsCacheStale(currentTime)) + if (IsCacheStale(currentTime, optionsSnapshot)) { LastAccess = currentTime; LogCacheStale(); - Cached = await provider.LoadInternalAsync(false, cancellationToken); + + try + { + Cached = await provider.LoadInternalAsync(optionsSnapshot, false, cancellationToken); + } + catch (ConfigServerException exception) + { + LogFetchFailed(exception); + Cached = null; + return null; + } } return Cached?.PropertySources; } - internal bool IsCacheStale(long accessTime) + internal bool IsCacheStale(long accessTime, ConfigServerClientOptions optionsSnapshot) { if (Cached == null) { return true; } - return accessTime - LastAccess >= GetTimeToLive(); - } - - internal bool IsEnabled() - { - return Provider is { ClientOptions.Health.Enabled: true }; - } - - internal long GetTimeToLive() - { - return Provider != null ? Provider.ClientOptions.Health.TimeToLive : long.MaxValue; + return accessTime - LastAccess >= optionsSnapshot.Health.TimeToLive; } [LoggerMessage(Level = LogLevel.Warning, Message = "No Config Server provider found, health check disabled.")] @@ -121,14 +124,17 @@ internal long GetTimeToLive() [LoggerMessage(Level = LogLevel.Debug, Message = "No Config Server provider found.")] private partial void LogNoProviderFound(); + [LoggerMessage(Level = LogLevel.Debug, Message = "Failed fetching remote configuration from server(s).")] + private partial void LogFetchFailed(Exception exception); + [LoggerMessage(Level = LogLevel.Debug, Message = "No property sources found.")] private partial void LogNoPropertySourcesFound(); [LoggerMessage(Level = LogLevel.Debug, Message = "Config Server health check returning UP.")] private partial void LogHealthCheckReturningUp(); - [LoggerMessage(Level = LogLevel.Debug, Message = "Returning property source {PropertySource}.")] - private partial void LogReturningPropertySource(string? propertySource); + [LoggerMessage(Level = LogLevel.Debug, Message = "Returning property sources: {PropertySources}.")] + private partial void LogReturningPropertySources(string propertySources); [LoggerMessage(Level = LogLevel.Debug, Message = "Cache stale, fetching config server health.")] private partial void LogCacheStale(); diff --git a/src/Configuration/src/ConfigServer/ConfigServerHostBuilderExtensions.cs b/src/Configuration/src/ConfigServer/ConfigServerHostBuilderExtensions.cs index bf3e0d0337..ab3dde3d9a 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerHostBuilderExtensions.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerHostBuilderExtensions.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -23,7 +24,7 @@ public static class ConfigServerHostBuilderExtensions /// public static IWebHostBuilder AddConfigServer(this IWebHostBuilder builder) { - return AddConfigServer(builder, NullLoggerFactory.Instance); + return AddConfigServer(builder, null, NullLoggerFactory.Instance); } /// @@ -39,12 +40,49 @@ public static IWebHostBuilder AddConfigServer(this IWebHostBuilder builder) /// The incoming so that additional calls can be chained. /// public static IWebHostBuilder AddConfigServer(this IWebHostBuilder builder, ILoggerFactory loggerFactory) + { + return AddConfigServer(builder, null, loggerFactory); + } + + /// + /// Adds Config Server and Cloud Foundry as application configuration sources. Adds Config Server health check contributor to the service container. + /// + /// + /// The to configure. + /// + /// + /// An optional delegate that further configures options from code, after settings from have been applied. + /// + /// + /// The incoming so that additional calls can be chained. + /// + public static IWebHostBuilder AddConfigServer(this IWebHostBuilder builder, Action? configure) + { + return AddConfigServer(builder, configure, NullLoggerFactory.Instance); + } + + /// + /// Adds Config Server and Cloud Foundry as application configuration sources. Adds Config Server health check contributor to the service container. + /// + /// + /// The to configure. + /// + /// + /// An optional delegate that further configures options from code, after settings from have been applied. + /// + /// + /// Used for internal logging. Pass to disable logging. + /// + /// + /// The incoming so that additional calls can be chained. + /// + public static IWebHostBuilder AddConfigServer(this IWebHostBuilder builder, Action? configure, ILoggerFactory loggerFactory) { ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(loggerFactory); HostBuilderWrapper wrapper = HostBuilderWrapper.Wrap(builder); - wrapper.AddConfigServer(loggerFactory); + wrapper.AddConfigServer(configure, loggerFactory); return builder; } @@ -60,7 +98,7 @@ public static IWebHostBuilder AddConfigServer(this IWebHostBuilder builder, ILog /// public static IHostBuilder AddConfigServer(this IHostBuilder builder) { - return AddConfigServer(builder, NullLoggerFactory.Instance); + return AddConfigServer(builder, null, NullLoggerFactory.Instance); } /// @@ -76,12 +114,49 @@ public static IHostBuilder AddConfigServer(this IHostBuilder builder) /// The incoming so that additional calls can be chained. /// public static IHostBuilder AddConfigServer(this IHostBuilder builder, ILoggerFactory loggerFactory) + { + return AddConfigServer(builder, null, loggerFactory); + } + + /// + /// Adds Config Server and Cloud Foundry as application configuration sources. Adds Config Server health check contributor to the service container. + /// + /// + /// The to configure. + /// + /// + /// An optional delegate that further configures options from code, after settings from have been applied. + /// + /// + /// The incoming so that additional calls can be chained. + /// + public static IHostBuilder AddConfigServer(this IHostBuilder builder, Action? configure) + { + return AddConfigServer(builder, configure, NullLoggerFactory.Instance); + } + + /// + /// Adds Config Server and Cloud Foundry as application configuration sources. Adds Config Server health check contributor to the service container. + /// + /// + /// The to configure. + /// + /// + /// An optional delegate that further configures options from code, after settings from have been applied. + /// + /// + /// Used for internal logging. Pass to disable logging. + /// + /// + /// The incoming so that additional calls can be chained. + /// + public static IHostBuilder AddConfigServer(this IHostBuilder builder, Action? configure, ILoggerFactory loggerFactory) { ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(loggerFactory); HostBuilderWrapper wrapper = HostBuilderWrapper.Wrap(builder); - wrapper.AddConfigServer(loggerFactory); + wrapper.AddConfigServer(configure, loggerFactory); return builder; } @@ -98,7 +173,7 @@ public static IHostBuilder AddConfigServer(this IHostBuilder builder, ILoggerFac /// public static IHostApplicationBuilder AddConfigServer(this IHostApplicationBuilder builder) { - return AddConfigServer(builder, NullLoggerFactory.Instance); + return AddConfigServer(builder, null, NullLoggerFactory.Instance); } /// @@ -115,12 +190,52 @@ public static IHostApplicationBuilder AddConfigServer(this IHostApplicationBuild /// The incoming so that additional calls can be chained. /// public static IHostApplicationBuilder AddConfigServer(this IHostApplicationBuilder builder, ILoggerFactory loggerFactory) + { + return AddConfigServer(builder, null, loggerFactory); + } + + /// + /// Adds Config Server and Cloud Foundry as application configuration sources. Also adds Config Server health check contributor and related services to + /// the service container. + /// + /// + /// The to configure. + /// + /// + /// An optional delegate that further configures options from code, after settings from have been applied. + /// + /// + /// The incoming so that additional calls can be chained. + /// + public static IHostApplicationBuilder AddConfigServer(this IHostApplicationBuilder builder, Action? configure) + { + return AddConfigServer(builder, configure, NullLoggerFactory.Instance); + } + + /// + /// Adds Config Server and Cloud Foundry as application configuration sources. Also adds Config Server health check contributor and related services to + /// the service container. + /// + /// + /// The to configure. + /// + /// + /// An optional delegate that further configures options from code, after settings from have been applied. + /// + /// + /// Used for internal logging. Pass to disable logging. + /// + /// + /// The incoming so that additional calls can be chained. + /// + public static IHostApplicationBuilder AddConfigServer(this IHostApplicationBuilder builder, Action? configure, + ILoggerFactory loggerFactory) { ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(loggerFactory); HostBuilderWrapper wrapper = HostBuilderWrapper.Wrap(builder); - wrapper.AddConfigServer(loggerFactory); + wrapper.AddConfigServer(configure, loggerFactory); return builder; } diff --git a/src/Configuration/src/ConfigServer/ConfigServerServiceCollectionExtensions.cs b/src/Configuration/src/ConfigServer/ConfigServerServiceCollectionExtensions.cs index 624aa54ee4..8c891be1cd 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerServiceCollectionExtensions.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerServiceCollectionExtensions.cs @@ -25,11 +25,40 @@ public static class ConfigServerServiceCollectionExtensions /// The incoming so that additional calls can be chained. /// public static IServiceCollection ConfigureConfigServerClientOptions(this IServiceCollection services) + { + return ConfigureConfigServerClientOptions(services, null); + } + + /// + /// Adds for use with the options pattern. + /// + /// + /// The to add services to. + /// + /// + /// An optional delegate that further configures options from code, after settings from have been applied. + /// + /// + /// The incoming so that additional calls can be chained. + /// + public static IServiceCollection ConfigureConfigServerClientOptions(this IServiceCollection services, Action? configure) { ArgumentNullException.ThrowIfNull(services); services.AddOptions(); - services.TryAddEnumerable(ServiceDescriptor.Singleton, ConfigureConfigServerClientOptions>()); + + services.AddSingleton(serviceProvider => + { + var configuration = serviceProvider.GetRequiredService(); + return new ConfigureConfigServerClientOptions(configuration, configure); + }); + + services.TryAddEnumerable( + ServiceDescriptor.Singleton, ConfigureConfigServerClientOptions>(serviceProvider => + serviceProvider.GetRequiredService())); + + services.TryAddEnumerable(ServiceDescriptor + .Singleton, ConfigurationChangeTokenSource>()); return services; } @@ -64,10 +93,28 @@ public static IServiceCollection AddConfigServerHealthContributor(this IServiceC /// The incoming so that additional calls can be chained. /// public static IServiceCollection AddConfigServerServices(this IServiceCollection services) + { + return AddConfigServerServices(services, null); + } + + /// + /// Configures , hosted service and health contributor, and ensures is + /// available. + /// + /// + /// The to add services to. + /// + /// + /// An optional delegate that further configures options from code, after settings from have been applied. + /// + /// + /// The incoming so that additional calls can be chained. + /// + public static IServiceCollection AddConfigServerServices(this IServiceCollection services, Action? configure) { ArgumentNullException.ThrowIfNull(services); - services.ConfigureConfigServerClientOptions(); + services.ConfigureConfigServerClientOptions(configure); services.TryAddSingleton(serviceProvider => (IConfigurationRoot)serviceProvider.GetRequiredService()); services.AddHostedService(); services.AddConfigServerHealthContributor(); diff --git a/src/Configuration/src/ConfigServer/ConfigurationSchema.json b/src/Configuration/src/ConfigServer/ConfigurationSchema.json index 8f9e677fc0..0bfa06a7a1 100644 --- a/src/Configuration/src/ConfigServer/ConfigurationSchema.json +++ b/src/Configuration/src/ConfigServer/ConfigurationSchema.json @@ -173,7 +173,7 @@ }, "TokenRenewRate": { "type": "integer", - "description": "Gets or sets the vault token renew rate (in milliseconds). Default value: 60_000 (1 minute)." + "description": "Gets or sets the Vault token renew rate (in milliseconds). Default value: 60_000 (1 minute)." }, "TokenTtl": { "type": "integer", diff --git a/src/Configuration/src/ConfigServer/ConfigureConfigServerClientOptions.cs b/src/Configuration/src/ConfigServer/ConfigureConfigServerClientOptions.cs index 4a7ae64ffe..d1855d5d15 100644 --- a/src/Configuration/src/ConfigServer/ConfigureConfigServerClientOptions.cs +++ b/src/Configuration/src/ConfigServer/ConfigureConfigServerClientOptions.cs @@ -5,23 +5,26 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; using Steeltoe.Common; +using Steeltoe.Common.Certificates; using Steeltoe.Configuration.CloudFoundry; namespace Steeltoe.Configuration.ConfigServer; internal sealed class ConfigureConfigServerClientOptions : IConfigureOptions { - private const string VcapServicesConfigServerCredentialsPrefix = "vcap:services:p-config-server:0:credentials"; - private const string VcapServicesConfigServer30CredentialsPrefix = "vcap:services:p.config-server:0:credentials"; + private const string VcapServicesConfigServerVersion2CredentialsPrefix = "vcap:services:p-config-server:0:credentials"; + private const string VcapServicesConfigServerVersion3CredentialsPrefix = "vcap:services:p.config-server:0:credentials"; private const string VcapServicesConfigServerCredentialsAltPrefix = "vcap:services:config-server:0:credentials"; private readonly IConfiguration _configuration; + private readonly Action? _configure; - public ConfigureConfigServerClientOptions(IConfiguration configuration) + public ConfigureConfigServerClientOptions(IConfiguration configuration, Action? configure) { ArgumentNullException.ThrowIfNull(configuration); _configuration = configuration; + _configure = configure; } public void Configure(ConfigServerClientOptions options) @@ -29,17 +32,21 @@ public void Configure(ConfigServerClientOptions options) ArgumentNullException.ThrowIfNull(options); _configuration.GetSection(ConfigServerClientOptions.ConfigurationPrefix).Bind(options); + _configure?.Invoke(options); + OverrideFromVcapServicesCredentials(options); + ConfigureClientCertificate(options); options.Name ??= GetApplicationName(); + options.Environment ??= "Production"; } private void OverrideFromVcapServicesCredentials(ConfigServerClientOptions options) { VcapServicesConfigServerCredentialsOptions credentialsOptions = new(); _configuration.GetSection(VcapServicesConfigServerCredentialsAltPrefix).Bind(credentialsOptions); - _configuration.GetSection(VcapServicesConfigServer30CredentialsPrefix).Bind(credentialsOptions); - _configuration.GetSection(VcapServicesConfigServerCredentialsPrefix).Bind(credentialsOptions); + _configuration.GetSection(VcapServicesConfigServerVersion2CredentialsPrefix).Bind(credentialsOptions); + _configuration.GetSection(VcapServicesConfigServerVersion3CredentialsPrefix).Bind(credentialsOptions); options.Uri = credentialsOptions.Uri ?? options.Uri; options.ClientId = credentialsOptions.ClientId ?? options.ClientId; @@ -47,6 +54,26 @@ private void OverrideFromVcapServicesCredentials(ConfigServerClientOptions optio options.AccessTokenUri = credentialsOptions.AccessTokenUri ?? options.AccessTokenUri; } + private void ConfigureClientCertificate(ConfigServerClientOptions options) + { + if (options.ClientCertificate.Certificate != null) + { + return; + } + + var certificateConfigurer = new ConfigureCertificateOptions(_configuration); + + var certificateOptions = new CertificateOptions(); + certificateConfigurer.Configure("ConfigServer", certificateOptions); + + if (certificateOptions.Certificate == null) + { + certificateConfigurer.Configure(certificateOptions); + } + + options.ClientCertificate = certificateOptions.Clone(); + } + private string? GetApplicationName() { var vcapOptions = new CloudFoundryApplicationOptions(); diff --git a/src/Configuration/src/ConfigServer/HostBuilderWrapperExtensions.cs b/src/Configuration/src/ConfigServer/HostBuilderWrapperExtensions.cs index 4709af0345..7c1c699ae2 100644 --- a/src/Configuration/src/ConfigServer/HostBuilderWrapperExtensions.cs +++ b/src/Configuration/src/ConfigServer/HostBuilderWrapperExtensions.cs @@ -10,36 +10,37 @@ namespace Steeltoe.Configuration.ConfigServer; internal static class HostBuilderWrapperExtensions { - public static HostBuilderWrapper AddConfigServer(this HostBuilderWrapper wrapper, ILoggerFactory loggerFactory) + public static HostBuilderWrapper AddConfigServer(this HostBuilderWrapper wrapper, Action? configure, + ILoggerFactory loggerFactory) { ArgumentNullException.ThrowIfNull(wrapper); ArgumentNullException.ThrowIfNull(loggerFactory); wrapper.ConfigureAppConfiguration((context, builder) => { - ConfigServerClientOptions options = CreateOptions(context.HostEnvironment); - builder.AddConfigServer(options, loggerFactory); + Action configureOptions = CreateOptionsConfigurer(configure, context.HostEnvironment); + builder.AddConfigServer(configureOptions, loggerFactory); }); - wrapper.ConfigureServices(services => services.AddConfigServerServices()); + wrapper.ConfigureServices((context, services) => + { + Action configureOptions = CreateOptionsConfigurer(configure, context.HostEnvironment); + services.AddConfigServerServices(configureOptions); + }); return wrapper; } - private static ConfigServerClientOptions CreateOptions(IHostEnvironment hostEnvironment) + private static Action CreateOptionsConfigurer(Action? configure, IHostEnvironment hostEnvironment) { - var options = new ConfigServerClientOptions(); - - if (!string.IsNullOrEmpty(hostEnvironment.EnvironmentName) && hostEnvironment.EnvironmentName != "Production") + return options => { - // Only take IHostEnvironment.EnvironmentName when it was explicitly set (it defaults to "Production"). - // In the default case, we want the various other ways of setting the environment name to kick in. - options.Environment = hostEnvironment.EnvironmentName; - } - - // Intentionally NOT taking hostEnvironment.ApplicationName here, because that would disable the various other ways of setting the application name. - // Ultimately, its value ends up in configuration key "applicationName", whose value is used if nothing else is configured. + configure?.Invoke(options); - return options; + if (!string.IsNullOrEmpty(hostEnvironment.EnvironmentName)) + { + options.Environment ??= hostEnvironment.EnvironmentName; + } + }; } } diff --git a/src/Configuration/src/ConfigServer/PublicAPI.Unshipped.txt b/src/Configuration/src/ConfigServer/PublicAPI.Unshipped.txt index 7dc5c58110..ee4a75b906 100644 --- a/src/Configuration/src/ConfigServer/PublicAPI.Unshipped.txt +++ b/src/Configuration/src/ConfigServer/PublicAPI.Unshipped.txt @@ -1 +1,12 @@ #nullable enable +static Steeltoe.Configuration.ConfigServer.ConfigServerConfigurationBuilderExtensions.AddConfigServer(this Microsoft.Extensions.Configuration.IConfigurationBuilder! builder, Steeltoe.Configuration.ConfigServer.ConfigServerClientOptions! options) -> Microsoft.Extensions.Configuration.IConfigurationBuilder! +static Steeltoe.Configuration.ConfigServer.ConfigServerConfigurationBuilderExtensions.AddConfigServer(this Microsoft.Extensions.Configuration.IConfigurationBuilder! builder, System.Action? configure) -> Microsoft.Extensions.Configuration.IConfigurationBuilder! +static Steeltoe.Configuration.ConfigServer.ConfigServerConfigurationBuilderExtensions.AddConfigServer(this Microsoft.Extensions.Configuration.IConfigurationBuilder! builder, System.Action? configure, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> Microsoft.Extensions.Configuration.IConfigurationBuilder! +static Steeltoe.Configuration.ConfigServer.ConfigServerHostBuilderExtensions.AddConfigServer(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! builder, System.Action? configure) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! +static Steeltoe.Configuration.ConfigServer.ConfigServerHostBuilderExtensions.AddConfigServer(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! builder, System.Action? configure, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! +static Steeltoe.Configuration.ConfigServer.ConfigServerHostBuilderExtensions.AddConfigServer(this Microsoft.Extensions.Hosting.IHostApplicationBuilder! builder, System.Action? configure) -> Microsoft.Extensions.Hosting.IHostApplicationBuilder! +static Steeltoe.Configuration.ConfigServer.ConfigServerHostBuilderExtensions.AddConfigServer(this Microsoft.Extensions.Hosting.IHostApplicationBuilder! builder, System.Action? configure, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> Microsoft.Extensions.Hosting.IHostApplicationBuilder! +static Steeltoe.Configuration.ConfigServer.ConfigServerHostBuilderExtensions.AddConfigServer(this Microsoft.Extensions.Hosting.IHostBuilder! builder, System.Action? configure) -> Microsoft.Extensions.Hosting.IHostBuilder! +static Steeltoe.Configuration.ConfigServer.ConfigServerHostBuilderExtensions.AddConfigServer(this Microsoft.Extensions.Hosting.IHostBuilder! builder, System.Action? configure, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> Microsoft.Extensions.Hosting.IHostBuilder! +static Steeltoe.Configuration.ConfigServer.ConfigServerServiceCollectionExtensions.AddConfigServerServices(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action? configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Steeltoe.Configuration.ConfigServer.ConfigServerServiceCollectionExtensions.ConfigureConfigServerClientOptions(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action? configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Configuration/test/ConfigServer.Discovery.Test/ConfigServerClientOptionsTest.cs b/src/Configuration/test/ConfigServer.Discovery.Test/ConfigServerClientOptionsTest.cs new file mode 100644 index 0000000000..5ca2eab3c0 --- /dev/null +++ b/src/Configuration/test/ConfigServer.Discovery.Test/ConfigServerClientOptionsTest.cs @@ -0,0 +1,430 @@ +// 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.Logging.Abstractions; +using Microsoft.Extensions.Options; +using RichardSzalay.MockHttp; +using Steeltoe.Common.TestResources; + +namespace Steeltoe.Configuration.ConfigServer.Discovery.Test; + +public sealed class ConfigServerClientOptionsTest +{ + [Fact] + public void Config_Server_URI_is_resolved_from_discovery_and_survives_changes_in_IConfiguration() + { + const string configServerResponseJson = """ + { + "name": "example-app-name", + "profiles": [ + "example-profile" + ], + "label": "example-label", + "version": "1", + "propertySources": [ + { + "name": "example-source", + "source": { + "example-server-key": "example-server-value" + } + } + ] + } + """; + + var fileProvider = new MemoryFileProvider(); + + fileProvider.IncludeAppSettingsJsonFile(""" + { + "spring": { + "cloud": { + "config": { + "discovery": { + "enabled": true + }, + "uri": "http://overridden-by-discovery", + "name": "example-app-name", + "env": "example-profile", + "timeout": 30000, + "label": "example-label" + } + } + }, + "discovery": { + "services": [ + { + "serviceId": "configserver", + "host": "discovered-server.com", + "port": 9999, + "isSecure": true, + "metadata": { + "user": "example-user", + "password": "example-password", + "configPath": "internal" + } + } + ] + }, + "eureka": { + "client": { + "enabled": false + } + }, + "consul": { + "discovery": { + "enabled": false + } + } + } + """); + + using var handler = new DelegateToMockHttpClientHandler(); + + handler.Mock.Expect(HttpMethod.Get, "https://discovered-server.com:9999/internal/example-app-name/example-profile/example-label") + .Respond("application/json", configServerResponseJson); + + Action configureOptions = options => options.ValidateCertificates = false; + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); + // ReSharper disable once AccessToDisposedClosure + configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), configureOptions, () => handler, NullLoggerFactory.Instance); + IConfigurationRoot configuration = configurationBuilder.Build(); + + handler.Mock.VerifyNoOutstandingExpectation(); + + ConfigServerConfigurationProvider provider = configuration.Providers.OfType().Single(); + + IServiceCollection services = new ServiceCollection(); + services.AddSingleton(configuration); + services.ConfigureConfigServerClientOptions(configureOptions); + + using ServiceProvider serviceProvider = services.BuildServiceProvider(true); + var optionsMonitor = serviceProvider.GetRequiredService>(); + + provider.ClientOptions.Uri.Should().Be("http://overridden-by-discovery"); + provider.ClientOptions.Username.Should().BeNull(); + provider.ClientOptions.Password.Should().BeNull(); + provider.ClientOptions.Name.Should().Be("example-app-name"); + provider.ClientOptions.Environment.Should().Be("example-profile"); + provider.ClientOptions.Timeout.Should().Be(30_000); + provider.ClientOptions.Label.Should().Be("example-label"); + provider.ClientOptions.ValidateCertificates.Should().BeFalse(); + + optionsMonitor.CurrentValue.Uri.Should().Be("https://discovered-server.com:9999/internal"); + optionsMonitor.CurrentValue.Username.Should().Be("example-user"); + optionsMonitor.CurrentValue.Password.Should().Be("example-password"); + optionsMonitor.CurrentValue.Name.Should().Be(provider.ClientOptions.Name); + optionsMonitor.CurrentValue.Environment.Should().Be(provider.ClientOptions.Environment); + optionsMonitor.CurrentValue.Timeout.Should().Be(provider.ClientOptions.Timeout); + optionsMonitor.CurrentValue.Label.Should().Be(provider.ClientOptions.Label); + optionsMonitor.CurrentValue.ValidateCertificates.Should().BeFalse(); + + configuration["example-server-key"].Should().Be("example-server-value"); + + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "spring": { + "cloud": { + "config": { + "discovery": { + "enabled": true + }, + "uri": "http://overridden-by-discovery", + "name": "alternate-name-1", + "env": "example-profile", + "timeout": 15000, + "label": "example-label" + } + } + }, + "discovery": { + "services": [ + { + "serviceId": "configserver", + "host": "ignored-other-discovered-server.com", + "port": 3333, + "isSecure": true, + "metadata": { + "user": "ignored-other-example-user", + "password": "ignored-other-example-password", + "configPath": "ignored-other-internal" + } + } + ] + }, + "eureka": { + "client": { + "enabled": false + } + }, + "consul": { + "discovery": { + "enabled": false + } + } + } + """); + + fileProvider.NotifyChanged(); + + provider.ClientOptions.Uri.Should().Be("http://overridden-by-discovery"); + provider.ClientOptions.Username.Should().BeNull(); + provider.ClientOptions.Password.Should().BeNull(); + provider.ClientOptions.Name.Should().Be("alternate-name-1"); + provider.ClientOptions.Environment.Should().Be("example-profile"); + provider.ClientOptions.Timeout.Should().Be(15_000); + provider.ClientOptions.Label.Should().Be("example-label"); + provider.ClientOptions.ValidateCertificates.Should().BeFalse(); + + // Discovery changes don't propagate until the provider reloads. + optionsMonitor.CurrentValue.Uri.Should().Be("https://discovered-server.com:9999/internal"); + optionsMonitor.CurrentValue.Username.Should().Be("example-user"); + optionsMonitor.CurrentValue.Password.Should().Be("example-password"); + optionsMonitor.CurrentValue.Name.Should().Be(provider.ClientOptions.Name); + optionsMonitor.CurrentValue.Environment.Should().Be(provider.ClientOptions.Environment); + optionsMonitor.CurrentValue.Timeout.Should().Be(provider.ClientOptions.Timeout); + optionsMonitor.CurrentValue.Label.Should().Be(provider.ClientOptions.Label); + optionsMonitor.CurrentValue.ValidateCertificates.Should().BeFalse(); + + configuration["example-server-key"].Should().Be("example-server-value"); + + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "spring": { + "cloud": { + "config": { + "discovery": { + "enabled": false + }, + "uri": "https://explicit-server:7777", + "name": "alternate-name-2", + "env": "example-profile", + "timeout": 10000 + } + } + } + } + """); + + fileProvider.NotifyChanged(); + + provider.ClientOptions.Uri.Should().Be("https://explicit-server:7777"); + provider.ClientOptions.Name.Should().Be("alternate-name-2"); + provider.ClientOptions.Environment.Should().Be("example-profile"); + provider.ClientOptions.Timeout.Should().Be(10_000); + provider.ClientOptions.Label.Should().BeNull(); + provider.ClientOptions.ValidateCertificates.Should().BeFalse(); + + // Discovery changes don't propagate until the provider reloads. + optionsMonitor.CurrentValue.Uri.Should().Be("https://discovered-server.com:9999/internal"); + optionsMonitor.CurrentValue.Name.Should().Be(provider.ClientOptions.Name); + optionsMonitor.CurrentValue.Environment.Should().Be(provider.ClientOptions.Environment); + optionsMonitor.CurrentValue.Timeout.Should().Be(provider.ClientOptions.Timeout); + optionsMonitor.CurrentValue.Label.Should().Be(provider.ClientOptions.Label); + optionsMonitor.CurrentValue.ValidateCertificates.Should().BeFalse(); + + configuration["example-server-key"].Should().Be("example-server-value"); + } + + [Fact] + public void Updates_discovered_Config_Server_URI_on_provider_reload() + { + const string configServerResponseJson = """ + { + "name": "example-app-name", + "profiles": [ + "example-profile" + ], + "label": "example-label", + "version": "1", + "propertySources": [ + { + "name": "example-source", + "source": { + "example-server-key": "example-server-value" + } + } + ] + } + """; + + var fileProvider = new MemoryFileProvider(); + + fileProvider.IncludeAppSettingsJsonFile(""" + { + "spring": { + "cloud": { + "config": { + "discovery": { + "enabled": true + }, + "uri": "http://overridden-by-discovery", + "name": "example-app-name", + "env": "example-profile", + "label": "example-label" + } + } + }, + "discovery": { + "services": [ + { + "serviceId": "configserver", + "host": "discovered-server.com", + "port": 9999, + "isSecure": true, + "metadata": { + "user": "example-user", + "password": "example-password", + "configPath": "internal" + } + } + ] + }, + "eureka": { + "client": { + "enabled": false + } + }, + "consul": { + "discovery": { + "enabled": false + } + } + } + """); + + using var handler = new DelegateToMockHttpClientHandler(); + + handler.Mock.Expect(HttpMethod.Get, "https://discovered-server.com:9999/internal/example-app-name/example-profile/example-label") + .Respond("application/json", configServerResponseJson); + + Action configureOptions = options => options.ValidateCertificates = false; + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); + // ReSharper disable once AccessToDisposedClosure + configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), configureOptions, () => handler, NullLoggerFactory.Instance); + IConfigurationRoot configuration = configurationBuilder.Build(); + + handler.Mock.VerifyNoOutstandingExpectation(); + handler.Mock.Clear(); + + ConfigServerConfigurationProvider provider = configuration.Providers.OfType().Single(); + + IServiceCollection services = new ServiceCollection(); + services.AddSingleton(configuration); + services.ConfigureConfigServerClientOptions(configureOptions); + + using ServiceProvider serviceProvider = services.BuildServiceProvider(true); + var optionsMonitor = serviceProvider.GetRequiredService>(); + + provider.ClientOptions.Uri.Should().Be("http://overridden-by-discovery"); + provider.ClientOptions.Username.Should().BeNull(); + provider.ClientOptions.Password.Should().BeNull(); + provider.ClientOptions.Name.Should().Be("example-app-name"); + provider.ClientOptions.Environment.Should().Be("example-profile"); + provider.ClientOptions.Label.Should().Be("example-label"); + provider.ClientOptions.ValidateCertificates.Should().BeFalse(); + + optionsMonitor.CurrentValue.Uri.Should().Be("https://discovered-server.com:9999/internal"); + optionsMonitor.CurrentValue.Username.Should().Be("example-user"); + optionsMonitor.CurrentValue.Password.Should().Be("example-password"); + optionsMonitor.CurrentValue.Name.Should().Be(provider.ClientOptions.Name); + optionsMonitor.CurrentValue.Environment.Should().Be(provider.ClientOptions.Environment); + optionsMonitor.CurrentValue.Label.Should().Be(provider.ClientOptions.Label); + optionsMonitor.CurrentValue.ValidateCertificates.Should().BeFalse(); + + configuration["example-server-key"].Should().Be("example-server-value"); + + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "spring": { + "cloud": { + "config": { + "discovery": { + "enabled": true + }, + "uri": "http://overridden-again-by-discovery", + "name": "alternate-name", + "env": "alternate-profile", + "label": "alternate-label" + } + } + }, + "discovery": { + "services": [ + { + "serviceId": "configserver", + "host": "alternate-discovered-server.com", + "port": 7777, + "isSecure": true, + "metadata": { + "user": "alternate-user", + "password": "alternate-password", + "configPath": "internal" + } + } + ] + }, + "eureka": { + "client": { + "enabled": false + } + }, + "consul": { + "discovery": { + "enabled": false + } + } + } + """); + + fileProvider.NotifyChanged(); + + provider.ClientOptions.Uri.Should().Be("http://overridden-again-by-discovery"); + provider.ClientOptions.Username.Should().BeNull(); + provider.ClientOptions.Password.Should().BeNull(); + provider.ClientOptions.Name.Should().Be("alternate-name"); + provider.ClientOptions.Environment.Should().Be("alternate-profile"); + provider.ClientOptions.Label.Should().Be("alternate-label"); + provider.ClientOptions.ValidateCertificates.Should().BeFalse(); + + optionsMonitor.CurrentValue.Uri.Should().Be("https://discovered-server.com:9999/internal"); + optionsMonitor.CurrentValue.Username.Should().Be("example-user"); + optionsMonitor.CurrentValue.Password.Should().Be("example-password"); + optionsMonitor.CurrentValue.Name.Should().Be(provider.ClientOptions.Name); + optionsMonitor.CurrentValue.Environment.Should().Be(provider.ClientOptions.Environment); + optionsMonitor.CurrentValue.Label.Should().Be(provider.ClientOptions.Label); + optionsMonitor.CurrentValue.ValidateCertificates.Should().BeFalse(); + + configuration["example-server-key"].Should().Be("example-server-value"); + + handler.Mock.Expect(HttpMethod.Get, "https://alternate-discovered-server.com:7777/internal/alternate-name/alternate-profile/alternate-label") + .Respond("application/json", configServerResponseJson); + + provider.Load(); + handler.Mock.VerifyNoOutstandingExpectation(); + + provider.ClientOptions.Uri.Should().Be("http://overridden-again-by-discovery"); + provider.ClientOptions.Username.Should().BeNull(); + provider.ClientOptions.Password.Should().BeNull(); + provider.ClientOptions.Name.Should().Be("alternate-name"); + provider.ClientOptions.Environment.Should().Be("alternate-profile"); + provider.ClientOptions.Label.Should().Be("alternate-label"); + provider.ClientOptions.ValidateCertificates.Should().BeFalse(); + + optionsMonitor.CurrentValue.Uri.Should().Be("https://alternate-discovered-server.com:7777/internal"); + optionsMonitor.CurrentValue.Username.Should().Be("alternate-user"); + optionsMonitor.CurrentValue.Password.Should().Be("alternate-password"); + optionsMonitor.CurrentValue.Name.Should().Be(provider.ClientOptions.Name); + optionsMonitor.CurrentValue.Environment.Should().Be(provider.ClientOptions.Environment); + optionsMonitor.CurrentValue.Label.Should().Be(provider.ClientOptions.Label); + optionsMonitor.CurrentValue.ValidateCertificates.Should().BeFalse(); + + configuration["example-server-key"].Should().Be("example-server-value"); + } +} diff --git a/src/Configuration/test/ConfigServer.Discovery.Test/ConfigServerDiscoveryServiceTest.cs b/src/Configuration/test/ConfigServer.Discovery.Test/ConfigServerDiscoveryServiceTest.cs index 73770ae0a3..2b193cad38 100644 --- a/src/Configuration/test/ConfigServer.Discovery.Test/ConfigServerDiscoveryServiceTest.cs +++ b/src/Configuration/test/ConfigServer.Discovery.Test/ConfigServerDiscoveryServiceTest.cs @@ -15,12 +15,12 @@ namespace Steeltoe.Configuration.ConfigServer.Discovery.Test; public sealed class ConfigServerDiscoveryServiceTest { [Fact] - public void ConfigServerDiscoveryService_FindsDiscoveryClients() + public async Task ConfigServerDiscoveryService_FindsDiscoveryClients() { IConfiguration configuration = new ConfigurationBuilder().Add(FastTestConfigurations.ConfigServer | FastTestConfigurations.Discovery).Build(); - var options = new ConfigServerClientOptions(); - var service = new ConfigServerDiscoveryService(configuration, options, NullLoggerFactory.Instance); + var service = new ConfigServerDiscoveryService(configuration, NullLoggerFactory.Instance); + await service.GetConfigServerInstancesAsync(new ConfigServerClientOptions(), TestContext.Current.CancellationToken); service.DiscoveryClients.Should().HaveCount(3); service.DiscoveryClients.OfType().Should().ContainSingle(); @@ -41,8 +41,8 @@ public async Task InvokeGetInstances_ReturnsExpected() IConfigurationRoot configurationRoot = builder.Build(); var options = new ConfigServerClientOptions(); - var service = new ConfigServerDiscoveryService(configurationRoot, options, NullLoggerFactory.Instance); - IEnumerable result = await service.GetConfigServerInstancesAsync(TestContext.Current.CancellationToken); + var service = new ConfigServerDiscoveryService(configurationRoot, NullLoggerFactory.Instance); + IEnumerable result = await service.GetConfigServerInstancesAsync(options, TestContext.Current.CancellationToken); result.Should().BeEmpty(); } @@ -66,8 +66,8 @@ public async Task InvokeGetInstances_RetryEnabled_ReturnsExpected() Timeout = 10 }; - var service = new ConfigServerDiscoveryService(configurationRoot, options, NullLoggerFactory.Instance); - IEnumerable result = await service.GetConfigServerInstancesAsync(TestContext.Current.CancellationToken); + var service = new ConfigServerDiscoveryService(configurationRoot, NullLoggerFactory.Instance); + IEnumerable result = await service.GetConfigServerInstancesAsync(options, TestContext.Current.CancellationToken); result.Should().BeEmpty(); } @@ -92,8 +92,8 @@ public async Task GetConfigServerInstances_ReturnsExpected() Timeout = 10 }; - var service = new ConfigServerDiscoveryService(configurationRoot, options, NullLoggerFactory.Instance); - IEnumerable result = await service.GetConfigServerInstancesAsync(TestContext.Current.CancellationToken); + var service = new ConfigServerDiscoveryService(configurationRoot, NullLoggerFactory.Instance); + IEnumerable result = await service.GetConfigServerInstancesAsync(options, TestContext.Current.CancellationToken); result.Should().BeEmpty(); } @@ -103,7 +103,7 @@ public async Task RuntimeReplacementsCanBeProvided() IConfigurationRoot configurationRoot = new ConfigurationBuilder().Add(FastTestConfigurations.ConfigServer | FastTestConfigurations.Discovery).Build(); var testDiscoveryClient = new TestDiscoveryClient(); - var service = new ConfigServerDiscoveryService(configurationRoot, new ConfigServerClientOptions(), NullLoggerFactory.Instance); + var service = new ConfigServerDiscoveryService(configurationRoot, NullLoggerFactory.Instance); await service.ProvideRuntimeReplacementsAsync([testDiscoveryClient], TestContext.Current.CancellationToken); diff --git a/src/Configuration/test/ConfigServer.Integration.Test/ConfigServerConfigurationExtensionsIntegrationTest.cs b/src/Configuration/test/ConfigServer.Integration.Test/ConfigServerConfigurationExtensionsIntegrationTest.cs index f06a592b64..f8457950fe 100644 --- a/src/Configuration/test/ConfigServer.Integration.Test/ConfigServerConfigurationExtensionsIntegrationTest.cs +++ b/src/Configuration/test/ConfigServer.Integration.Test/ConfigServerConfigurationExtensionsIntegrationTest.cs @@ -6,7 +6,6 @@ using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Configuration; using Steeltoe.Common.TestResources; -using Steeltoe.Common.TestResources.IO; namespace Steeltoe.Configuration.ConfigServer.Integration.Test; @@ -41,15 +40,11 @@ public void SpringCloudConfigServer_ReturnsExpectedDefaultData() } """; - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings); - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.SetBasePath(directory); - - configurationBuilder.AddJsonFile(fileName); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsJsonFile(appSettings); + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); configurationBuilder.AddConfigServer(); IConfigurationRoot root = configurationBuilder.Build(); @@ -85,21 +80,13 @@ public async Task SpringCloudConfigServer_ReturnsExpectedDefaultData_AsInjectedO } """; - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings); - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsJsonFile(appSettings); WebHostBuilder builder = TestWebHostBuilderFactory.Create(); builder.UseEnvironment("development"); builder.UseStartup(); - - builder.ConfigureAppConfiguration(configurationBuilder => - { - configurationBuilder.SetBasePath(directory); - configurationBuilder.AddJsonFile(fileName); - }); - + builder.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider)); builder.AddConfigServer(); using IWebHost host = builder.Build(); @@ -176,21 +163,13 @@ public async Task SpringCloudConfigServer_ConfiguredViaCloudfoundryEnv_ReturnsEx } """; - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings); - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsJsonFile(appSettings); WebHostBuilder builder = TestWebHostBuilderFactory.Create(); builder.UseEnvironment("development"); builder.UseStartup(); - - builder.ConfigureAppConfiguration(configurationBuilder => - { - configurationBuilder.SetBasePath(directory); - configurationBuilder.AddJsonFile(fileName); - }); - + builder.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider)); builder.AddConfigServer(); using IWebHost host = builder.Build(); @@ -231,15 +210,12 @@ public void SpringCloudConfigServer_DiscoveryFirst_ReturnsExpectedDefaultData() } """; - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings); - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsJsonFile(appSettings); var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.Add(FastTestConfigurations.Discovery); - configurationBuilder.SetBasePath(directory); - configurationBuilder.AddJsonFile(fileName); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); configurationBuilder.AddConfigServer(); IConfigurationRoot root = configurationBuilder.Build(); @@ -275,20 +251,12 @@ public async Task SpringCloudConfigServer_WithHealthEnabled_ReturnsHealth() } """; - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings); - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsJsonFile(appSettings); WebHostBuilder builder = TestWebHostBuilderFactory.Create(); builder.UseStartup(); - - builder.ConfigureAppConfiguration(configurationBuilder => - { - configurationBuilder.SetBasePath(directory); - configurationBuilder.AddJsonFile(fileName); - }); - + builder.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider)); builder.AddConfigServer(); using IWebHost host = builder.Build(); diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerClientOptionsTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerClientOptionsTest.cs index 828b6dd46f..2c648dc0b3 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerClientOptionsTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerClientOptionsTest.cs @@ -3,11 +3,14 @@ // See the LICENSE file in the project root for more information. using System.Reflection; +using System.Security.Cryptography.X509Certificates; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; +using RichardSzalay.MockHttp; using Steeltoe.Common.TestResources; -using Steeltoe.Common.TestResources.IO; +using Steeltoe.Configuration.Placeholder; namespace Steeltoe.Configuration.ConfigServer.Test; @@ -18,7 +21,7 @@ public void DefaultConstructor_InitializedWithDefaults() { var options = new ConfigServerClientOptions(); - TestHelper.VerifyDefaults(options, null); + TestHelper.VerifyDefaults(options, null, null); } [Fact] @@ -36,7 +39,7 @@ public async Task ConfigureConfigServerClientOptions_WithDefaults() var optionsMonitor = serviceProvider.GetRequiredService>(); string? expectedAppName = Assembly.GetEntryAssembly()!.GetName().Name; - TestHelper.VerifyDefaults(optionsMonitor.CurrentValue, expectedAppName); + TestHelper.VerifyDefaults(optionsMonitor.CurrentValue, expectedAppName, "Production"); } [Fact] @@ -69,17 +72,15 @@ public async Task ConfigureConfigServerClientOptions_WithValues() } """; - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings); - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsJsonFile(appSettings); + var builder = new ConfigurationBuilder(); - builder.SetBasePath(directory); - builder.AddJsonFile(fileName); + builder.AddInMemoryAppSettingsJsonFile(fileProvider); IConfiguration configuration = builder.Build(); services.AddSingleton(configuration); - services.ConfigureConfigServerClientOptions(); + services.ConfigureConfigServerClientOptions(options => options.Environment = "staging"); await using ServiceProvider serviceProvider = services.BuildServiceProvider(true); var service = serviceProvider.GetRequiredService>(); @@ -88,7 +89,7 @@ public async Task ConfigureConfigServerClientOptions_WithValues() options.Enabled.Should().BeTrue(); options.FailFast.Should().BeTrue(); options.Uri.Should().Be("http://localhost:8888"); - options.Environment.Should().Be("development"); + options.Environment.Should().Be("staging"); options.AccessTokenUri.Should().BeNull(); options.ClientId.Should().BeNull(); options.ClientSecret.Should().BeNull(); @@ -114,4 +115,339 @@ public async Task ConfigureConfigServerClientOptions_WithValues() options.Headers.Should().ContainKey("bar").WhoseValue.Should().Be("foo"); options.Headers.Should().ContainKey("foo").WhoseValue.Should().Be("bar"); } + + [Fact] + public void Clone_preserves_all_properties_and_produces_independent_nested_objects() + { + using var certificate = X509Certificate2.CreateFromPemFile("instance.crt", "instance.key"); + using var issuerCertificate = X509Certificate2.CreateFromPemFile("instance.crt", "instance.key"); + + var original = new ConfigServerClientOptions + { + ClientCertificate = + { + Certificate = certificate, + IssuerChain = + { + issuerCertificate + } + }, + Enabled = false, + FailFast = true, + Environment = "staging", + Label = "feature/x", + Name = "my-app", + Uri = "https://config.example.com:9999", + Username = "user", + Password = "pass", + Token = "vault-token-123", + Timeout = 42_000, + PollingInterval = TimeSpan.FromSeconds(15), + ValidateCertificates = false, + Retry = + { + Enabled = true, + InitialInterval = 500, + MaxInterval = 5000, + Multiplier = 2.0, + MaxAttempts = 10 + }, + Discovery = + { + Enabled = true, + ServiceId = "my-config-server" + }, + Health = + { + Enabled = false, + TimeToLive = 999 + }, + AccessTokenUri = "https://uaa.example.com/oauth/token", + ClientSecret = "secret", + ClientId = "client-id", + TokenTtl = 600_000, + TokenRenewRate = 120_000, + DisableTokenRenewal = true, + Headers = + { + ["X-Custom"] = "value" + } + }; + + ConfigServerClientOptions clone = original.Clone(); + + clone.ClientCertificate.Should().NotBeSameAs(original.ClientCertificate); + clone.ClientCertificate.Certificate.Should().BeSameAs(original.ClientCertificate.Certificate); + clone.ClientCertificate.IssuerChain.Should().NotBeSameAs(original.ClientCertificate.IssuerChain); + clone.ClientCertificate.IssuerChain.Should().ContainSingle().Which.Should().BeSameAs(issuerCertificate); + + original.ClientCertificate.IssuerChain.Clear(); + clone.ClientCertificate.IssuerChain.Should().ContainSingle(); + + clone.Enabled.Should().Be(original.Enabled); + clone.FailFast.Should().Be(original.FailFast); + clone.Environment.Should().Be(original.Environment); + clone.Label.Should().Be(original.Label); + clone.Name.Should().Be(original.Name); + clone.Uri.Should().Be(original.Uri); + clone.Username.Should().Be(original.Username); + clone.Password.Should().Be(original.Password); + clone.Token.Should().Be(original.Token); + clone.Timeout.Should().Be(original.Timeout); + clone.PollingInterval.Should().Be(original.PollingInterval); + clone.ValidateCertificates.Should().Be(original.ValidateCertificates); + + clone.Retry.Should().NotBeSameAs(original.Retry); + clone.Retry.Enabled.Should().Be(original.Retry.Enabled); + clone.Retry.InitialInterval.Should().Be(original.Retry.InitialInterval); + clone.Retry.MaxInterval.Should().Be(original.Retry.MaxInterval); + clone.Retry.Multiplier.Should().Be(original.Retry.Multiplier); + clone.Retry.MaxAttempts.Should().Be(original.Retry.MaxAttempts); + + clone.Discovery.Should().NotBeSameAs(original.Discovery); + clone.Discovery.Enabled.Should().Be(original.Discovery.Enabled); + clone.Discovery.ServiceId.Should().Be(original.Discovery.ServiceId); + + clone.Health.Should().NotBeSameAs(original.Health); + clone.Health.Enabled.Should().Be(original.Health.Enabled); + clone.Health.TimeToLive.Should().Be(original.Health.TimeToLive); + + clone.AccessTokenUri.Should().Be(original.AccessTokenUri); + clone.ClientSecret.Should().Be(original.ClientSecret); + clone.ClientId.Should().Be(original.ClientId); + clone.TokenTtl.Should().Be(original.TokenTtl); + clone.TokenRenewRate.Should().Be(original.TokenRenewRate); + clone.DisableTokenRenewal.Should().Be(original.DisableTokenRenewal); + + clone.Headers.Should().NotBeSameAs(original.Headers); + clone.Headers.Should().BeEquivalentTo(original.Headers); + } + + [Fact] + public void Certificate_configuration_survives_options_reload() + { + const string configServerResponseJson = """ + { + "name": "myName", + "profiles": [ + "Production" + ], + "label": "test-label", + "version": "test-version", + "propertySources": [] + } + """; + + var fileProvider = new MemoryFileProvider(); + + fileProvider.IncludeAppSettingsJsonFile(""" + { + "spring": { + "cloud": { + "config": { + "name": "myName", + "timeout": 30000 + } + } + }, + "Certificates": { + "ConfigServer": { + "CertificateFilePath": "instance.crt", + "PrivateKeyFilePath": "instance.key" + } + } + } + """); + + using var handler = new DelegateToMockHttpClientHandler(); + + handler.Mock.Expect(HttpMethod.Get, "http://localhost:8888/myName/Production").Respond("application/json", configServerResponseJson); + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); + // ReSharper disable once AccessToDisposedClosure + configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), null, () => handler, NullLoggerFactory.Instance); + IConfigurationRoot configuration = configurationBuilder.Build(); + + handler.Mock.VerifyNoOutstandingExpectation(); + + ConfigServerConfigurationProvider provider = configuration.Providers.OfType().Single(); + + IServiceCollection services = new ServiceCollection(); + services.AddSingleton(configuration); + services.ConfigureConfigServerClientOptions(); + + using ServiceProvider serviceProvider = services.BuildServiceProvider(true); + var optionsMonitor = serviceProvider.GetRequiredService>(); + + provider.ClientOptions.ClientCertificate.Certificate.Should().NotBeNull(); + optionsMonitor.CurrentValue.ClientCertificate.Certificate.Should().NotBeNull(); + + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "spring": { + "cloud": { + "config": { + "name": "myName", + "timeout": 15000 + } + } + }, + "Certificates": { + "ConfigServer": { + "CertificateFilePath": "instance.crt", + "PrivateKeyFilePath": "instance.key" + } + } + } + """); + + fileProvider.NotifyChanged(); + + provider.ClientOptions.Timeout.Should().Be(15_000); + provider.ClientOptions.ClientCertificate.Certificate.Should().NotBeNull(); + optionsMonitor.CurrentValue.Timeout.Should().Be(15_000); + optionsMonitor.CurrentValue.ClientCertificate.Certificate.Should().NotBeNull(); + } + + [Fact] + public void Changes_in_IConfiguration_update_provider_options_and_injected_options() + { + const string configServerResponseJson = """ + { + "name": "example-app-name", + "profiles": [ + "example-profile" + ], + "label": "example-label", + "version": "1", + "propertySources": [ + { + "name": "example-source", + "source": { + "example-server-key": "example-server-value" + } + } + ] + } + """; + + var fileProvider = new MemoryFileProvider(); + + fileProvider.IncludeAppSettingsJsonFile(""" + { + "custom": { + "profileName": "example-profile" + }, + "spring": { + "cloud": { + "config": { + "uri": "https://config.server.com:9999", + "name": "example-app-name", + "env": "${custom:profileName}", + "timeout": 30000 + } + } + } + } + """); + + using var handler = new DelegateToMockHttpClientHandler(); + + handler.Mock.Expect(HttpMethod.Get, "https://config.server.com:9999/example-app-name/example-profile/example-label") + .Respond("application/json", configServerResponseJson); + + var defaultOptions = new ConfigServerClientOptions + { + Name = "ignored-because-overridden-from-appsettings", + Label = "example-label" // used, but missing in IConfiguration and injected options + }; + + Action configureOptions = options => options.FailFast = true; + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); + configurationBuilder.AddPlaceholderResolver(); + // ReSharper disable once AccessToDisposedClosure + configurationBuilder.AddConfigServer(defaultOptions, configureOptions, () => handler, NullLoggerFactory.Instance); + IConfigurationRoot configuration = configurationBuilder.Build(); + + handler.Mock.VerifyNoOutstandingExpectation(); + handler.Mock.Clear(); + + ConfigServerConfigurationProvider provider = configuration.Providers.OfType().Single(); + + IServiceCollection services = new ServiceCollection(); + services.AddSingleton(configuration); + services.ConfigureConfigServerClientOptions(configureOptions); + + using ServiceProvider serviceProvider = services.BuildServiceProvider(true); + var optionsMonitor = serviceProvider.GetRequiredService>(); + + provider.ClientOptions.Uri.Should().Be("https://config.server.com:9999"); + provider.ClientOptions.Name.Should().Be("example-app-name"); + provider.ClientOptions.Environment.Should().Be("example-profile"); + provider.ClientOptions.Timeout.Should().Be(30_000); + provider.ClientOptions.Label.Should().Be("example-label"); + provider.ClientOptions.FailFast.Should().BeTrue(); + + optionsMonitor.CurrentValue.Uri.Should().Be(provider.ClientOptions.Uri); + optionsMonitor.CurrentValue.Name.Should().Be(provider.ClientOptions.Name); + optionsMonitor.CurrentValue.Environment.Should().Be(provider.ClientOptions.Environment); + optionsMonitor.CurrentValue.Timeout.Should().Be(provider.ClientOptions.Timeout); + optionsMonitor.CurrentValue.Label.Should().BeNull(); + optionsMonitor.CurrentValue.FailFast.Should().BeTrue(); + + configuration["example-server-key"].Should().Be("example-server-value"); + + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "custom": { + "profileName": "example-profile" + }, + "spring": { + "cloud": { + "config": { + "uri": "https://alternate-config.server.com:7777", + "name": "alternate-name", + "env": "${custom:profileName}", + "timeout": 15000, + "label": "alternate-label" + } + } + } + } + """); + + fileProvider.NotifyChanged(); + + AssertFinal(); + + handler.Mock.Expect(HttpMethod.Get, "https://alternate-config.server.com:7777/alternate-name/example-profile/alternate-label") + .Respond("application/json", configServerResponseJson); + + provider.Load(); + handler.Mock.VerifyNoOutstandingExpectation(); + + AssertFinal(); + + void AssertFinal() + { + provider.ClientOptions.Uri.Should().Be("https://alternate-config.server.com:7777"); + provider.ClientOptions.Name.Should().Be("alternate-name"); + provider.ClientOptions.Environment.Should().Be("example-profile"); + provider.ClientOptions.Timeout.Should().Be(15_000); + provider.ClientOptions.Label.Should().Be("alternate-label"); + provider.ClientOptions.FailFast.Should().BeTrue(); + + optionsMonitor.CurrentValue.Uri.Should().Be(provider.ClientOptions.Uri); + optionsMonitor.CurrentValue.Name.Should().Be(provider.ClientOptions.Name); + optionsMonitor.CurrentValue.Environment.Should().Be(provider.ClientOptions.Environment); + optionsMonitor.CurrentValue.Timeout.Should().Be(provider.ClientOptions.Timeout); + optionsMonitor.CurrentValue.Label.Should().Be(provider.ClientOptions.Label); + optionsMonitor.CurrentValue.FailFast.Should().BeTrue(); + + configuration["example-server-key"].Should().Be("example-server-value"); + } + } } diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsCoreTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsCoreTest.cs index b78ec586fa..f299b94b3a 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsCoreTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsCoreTest.cs @@ -5,7 +5,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Steeltoe.Common.TestResources; -using Steeltoe.Common.TestResources.IO; using Steeltoe.Configuration.Placeholder; namespace Steeltoe.Configuration.ConfigServer.Test; @@ -43,7 +42,8 @@ public void AddConfigServer_WithLoggerFactorySucceeds() IList logMessages = loggerProvider.GetAll(); - logMessages.Should().Contain("DBUG Steeltoe.Configuration.ConfigServer.ConfigServerConfigurationProvider: Fetching configuration from server(s)."); + logMessages.Should().Contain( + "DBUG Steeltoe.Configuration.ConfigServer.ConfigServerConfigurationProvider: Fetching remote configuration from server(s)."); } [Fact] @@ -81,14 +81,11 @@ public void AddConfigServer_JsonAppSettingsConfiguresClient() } """; - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings); - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsJsonFile(appSettings); var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.SetBasePath(directory); - configurationBuilder.AddJsonFile(fileName); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); configurationBuilder.AddConfigServer(); IConfigurationRoot configurationRoot = configurationBuilder.Build(); @@ -135,14 +132,11 @@ public void AddConfigServer_ValidateCertificates_DisablesCertValidation() } """; - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings); - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsJsonFile(appSettings); var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.SetBasePath(directory); - configurationBuilder.AddJsonFile(fileName); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); configurationBuilder.AddConfigServer(); IConfigurationRoot configurationRoot = configurationBuilder.Build(); @@ -169,14 +163,11 @@ public void AddConfigServer_Validate_Certificates_DisablesCertValidation() } """; - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings); - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsJsonFile(appSettings); var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.SetBasePath(directory); - configurationBuilder.AddJsonFile(fileName); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); configurationBuilder.AddConfigServer(); IConfigurationRoot configurationRoot = configurationBuilder.Build(); @@ -208,14 +199,11 @@ public void AddConfigServer_XmlAppSettingsConfiguresClient() """; - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile("appsettings.xml", appSettings); - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsXmlFile(appSettings); var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.SetBasePath(directory); - configurationBuilder.AddXmlFile(fileName); + configurationBuilder.AddInMemoryAppSettingsXmlFile(fileProvider); configurationBuilder.AddConfigServer(); IConfigurationRoot configurationRoot = configurationBuilder.Build(); @@ -250,14 +238,11 @@ public void AddConfigServer_IniAppSettingsConfiguresClient() password=myPassword """; - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile("appsettings.ini", appSettings); - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsIniFile(appSettings); var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.SetBasePath(directory); - configurationBuilder.AddIniFile(fileName); + configurationBuilder.AddInMemoryAppSettingsIniFile(fileProvider); configurationBuilder.AddConfigServer(); IConfigurationRoot configurationRoot = configurationBuilder.Build(); @@ -347,15 +332,11 @@ public void AddConfigServer_SubstitutesPlaceholders() } """; - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings); - - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsJsonFile(appSettings); var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.SetBasePath(directory); - configurationBuilder.AddJsonFile(fileName); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); configurationBuilder.AddPlaceholderResolver(); configurationBuilder.AddConfigServer(); IConfigurationRoot configurationRoot = configurationBuilder.Build(); @@ -442,21 +423,15 @@ public void AddConfigServer_WithCloudfoundryEnvironment_ConfiguresClientCorrectl } """; - using var sandbox = new Sandbox(); - string appSettingsPath = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings); - string appSettingsFileName = Path.GetFileName(appSettingsPath); - - string vcapAppPath = sandbox.CreateFile("vcapapp.json", vcapApplication); - string vcapAppFileName = Path.GetFileName(vcapAppPath); - - string vcapServicesPath = sandbox.CreateFile("vcapservices.json", vcapServices); - string vcapServicesFileName = Path.GetFileName(vcapServicesPath); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsJsonFile(appSettings); + fileProvider.IncludeFile("vcapapp.json", vcapApplication); + fileProvider.IncludeFile("vcapservices.json", vcapServices); var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.SetBasePath(sandbox.FullPath); - configurationBuilder.AddJsonFile(appSettingsFileName); - configurationBuilder.AddJsonFile(vcapAppFileName); - configurationBuilder.AddJsonFile(vcapServicesFileName); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); + configurationBuilder.AddInMemoryJsonFile(fileProvider, "vcapapp.json"); + configurationBuilder.AddInMemoryJsonFile(fileProvider, "vcapservices.json"); configurationBuilder.AddConfigServer(); IConfigurationRoot configurationRoot = configurationBuilder.Build(); @@ -550,21 +525,15 @@ public void AddConfigServer_WithCloudfoundryEnvironmentSCS3_ConfiguresClientCorr } """; - using var sandbox = new Sandbox(); - string appSettingsPath = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings); - string appSettingsFileName = Path.GetFileName(appSettingsPath); - - string vcapAppPath = sandbox.CreateFile("vcapapp.json", vcapApplication); - string vcapAppFileName = Path.GetFileName(vcapAppPath); - - string vcapServicesPath = sandbox.CreateFile("vcapservices.json", vcapServices); - string vcapServicesFileName = Path.GetFileName(vcapServicesPath); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsJsonFile(appSettings); + fileProvider.IncludeFile("vcapapp.json", vcapApplication); + fileProvider.IncludeFile("vcapservices.json", vcapServices); var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.SetBasePath(sandbox.FullPath); - configurationBuilder.AddJsonFile(appSettingsFileName); - configurationBuilder.AddJsonFile(vcapAppFileName); - configurationBuilder.AddJsonFile(vcapServicesFileName); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); + configurationBuilder.AddInMemoryJsonFile(fileProvider, "vcapapp.json"); + configurationBuilder.AddInMemoryJsonFile(fileProvider, "vcapservices.json"); configurationBuilder.AddConfigServer(); IConfigurationRoot configurationRoot = configurationBuilder.Build(); diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsTest.cs index 57ed14e112..cdfeb6bbb4 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsTest.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; using Steeltoe.Common.TestResources; using Steeltoe.Configuration.CloudFoundry; @@ -130,12 +129,11 @@ public void AddConfigServer_WithConfigServerCertificate_AddsConfigServerSourceWi var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.AddInMemoryCollection(appSettings); - configurationBuilder.AddConfigServer(options, NullLoggerFactory.Instance); - _ = configurationBuilder.Build(); + configurationBuilder.AddConfigServer(options); + IConfigurationRoot configurationRoot = configurationBuilder.Build(); - ConfigServerConfigurationSource? source = configurationBuilder.EnumerateSources().SingleOrDefault(); - source.Should().NotBeNull(); - source.DefaultOptions.ClientCertificate.Should().NotBeNull(); + ConfigServerConfigurationProvider provider = configurationRoot.EnumerateProviders().Single(); + provider.ClientOptions.ClientCertificate.Certificate.Should().NotBeNull(); } [Fact] @@ -154,12 +152,11 @@ public void AddConfigServer_WithGlobalCertificate_AddsConfigServerSourceWithCert var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.AddInMemoryCollection(appSettings); - configurationBuilder.AddConfigServer(options, NullLoggerFactory.Instance); - _ = configurationBuilder.Build(); + configurationBuilder.AddConfigServer(options); + IConfigurationRoot configurationRoot = configurationBuilder.Build(); - ConfigServerConfigurationSource? source = configurationBuilder.EnumerateSources().SingleOrDefault(); - source.Should().NotBeNull(); - source.DefaultOptions.ClientCertificate.Should().NotBeNull(); + ConfigServerConfigurationProvider provider = configurationRoot.EnumerateProviders().Single(); + provider.ClientOptions.ClientCertificate.Certificate.Should().NotBeNull(); } [Fact] @@ -172,21 +169,6 @@ public void AddConfigServer_AddsConfigServerSourceToList() source.Should().NotBeNull(); } - [Fact] - public void AddConfigServer_WithLoggerFactorySucceeds() - { - CapturingLoggerProvider loggerProvider = new(); - using var loggerFactory = new LoggerFactory([loggerProvider]); - - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddConfigServer(loggerFactory); - _ = configurationBuilder.Build(); - - IList logMessages = loggerProvider.GetAll(); - - logMessages.Should().Contain("DBUG Steeltoe.Configuration.ConfigServer.ConfigServerConfigurationProvider: Fetching configuration from server(s)."); - } - [Theory] [InlineData(VcapServicesV2)] [InlineData(VcapServicesV3)] @@ -217,37 +199,94 @@ public void AddConfigServer_VCAP_SERVICES_Override_Defaults(string vcapServices) provider.Should().BeOfType(); provider.ClientOptions.Uri.Should().NotBe("https://uri-from-settings"); provider.ClientOptions.Uri.Should().Be("https://uri-from-vcap-services"); + provider.ClientOptions.ClientId.Should().Be("some-client-id"); + provider.ClientOptions.ClientSecret.Should().Be("some-secret"); + provider.ClientOptions.AccessTokenUri.Should().Be("https://uaa-uri-from-vcap-services/oauth/token"); } [Fact] - public void AddConfigServer_PaysAttentionToSettings() + public void AddConfigServer_CallbackOverridesConfigurationAndDefaultOptions() { var options = new ConfigServerClientOptions { - Name = "testConfigName", - Label = "testConfigLabel", - Environment = "testEnv", - Username = "testUser", - Password = "testPassword", + Name = "nameInOptions", + Label = "labelInOptions", + Environment = "environmentInOptions", + Username = "usernameInOptions", + Password = "passwordInOptions", Timeout = 10, Retry = { - Enabled = false + InitialInterval = 5, + MaxInterval = 15, + MaxAttempts = 12 } }; + var fileProvider = new MemoryFileProvider(); + + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Spring": { + "Cloud": { + "Config": { + "Name": "nameInAppSettings", + "Label": "labelInAppSettings", + "Timeout": 50, + "Retry": { + "MaxInterval": 100, + "MaxAttempts": 9 + } + } + } + } + } + """); + + Action configureOptions = clientOptions => clientOptions.Retry.MaxAttempts = 2; + var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddConfigServer(options, NullLoggerFactory.Instance); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); + configurationBuilder.AddConfigServer(options, configureOptions, null, NullLoggerFactory.Instance); IConfigurationRoot configurationRoot = configurationBuilder.Build(); ConfigServerConfigurationProvider? provider = configurationRoot.EnumerateProviders().FirstOrDefault(); provider.Should().NotBeNull(); - provider.ClientOptions.Label.Should().Be("testConfigLabel"); - provider.ClientOptions.Name.Should().Be("testConfigName"); - provider.ClientOptions.Environment.Should().Be("testEnv"); - provider.ClientOptions.Username.Should().Be("testUser"); - provider.ClientOptions.Password.Should().Be("testPassword"); + provider.ClientOptions.Name.Should().Be("nameInAppSettings"); + provider.ClientOptions.Label.Should().Be("labelInAppSettings"); + provider.ClientOptions.Environment.Should().Be("environmentInOptions"); + provider.ClientOptions.Username.Should().Be("usernameInOptions"); + provider.ClientOptions.Password.Should().Be("passwordInOptions"); + provider.ClientOptions.Timeout.Should().Be(50); + provider.ClientOptions.Retry.InitialInterval.Should().Be(5); + provider.ClientOptions.Retry.MaxInterval.Should().Be(100); + provider.ClientOptions.Retry.MaxAttempts.Should().Be(2); + + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Spring": { + "Cloud": { + "Config": { + "Name": "alternateNameInAppSettings", + "Username": "alternateUsernameInAppSettings" + } + } + } + } + """); + + fileProvider.NotifyChanged(); + + provider.ClientOptions.Name.Should().Be("alternateNameInAppSettings"); + provider.ClientOptions.Label.Should().Be("labelInOptions"); + provider.ClientOptions.Environment.Should().Be("environmentInOptions"); + provider.ClientOptions.Username.Should().Be("alternateUsernameInAppSettings"); + provider.ClientOptions.Password.Should().Be("passwordInOptions"); + provider.ClientOptions.Timeout.Should().Be(10); + provider.ClientOptions.Retry.InitialInterval.Should().Be(5); + provider.ClientOptions.Retry.MaxInterval.Should().Be(15); + provider.ClientOptions.Retry.MaxAttempts.Should().Be(2); } [Fact] diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs index e5f9f669ac..257fdb3821 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs @@ -2,13 +2,18 @@ // 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.Net; +using System.Net.Sockets; +using System.Reflection; +using System.Text; using FluentAssertions.Extensions; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; +using RichardSzalay.MockHttp; using Steeltoe.Common.TestResources; +// ReSharper disable AccessToDisposedClosure + namespace Steeltoe.Configuration.ConfigServer.Test; public sealed partial class ConfigServerConfigurationProviderTest @@ -22,11 +27,12 @@ public async Task RemoteLoadAsync_HostTimesOut() }; var httpClientHandler = new SlowHttpClientHandler(1.Seconds(), new HttpResponseMessage()); - using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => httpClientHandler, NullLoggerFactory.Instance); + provider.Load(); + List requestUris = [new("http://localhost:9999/app/profile")]; - // ReSharper disable once AccessToDisposedClosure - Func action = async () => await provider.RemoteLoadAsync(requestUris, null, TestContext.Current.CancellationToken); + Func action = async () => await provider.RemoteLoadAsync(provider.ClientOptions, requestUris, null, TestContext.Current.CancellationToken); (await action.Should().ThrowExactlyAsync()).WithInnerExceptionExactly(); } @@ -34,59 +40,42 @@ public async Task RemoteLoadAsync_HostTimesOut() [Fact] public async Task RemoteLoadAsync_ConfigServerReturnsGreaterThanEqualBadRequest() { - using var startup = new TestConfigServerStartup(); - startup.ReturnStatus = [500]; + ConfigServerClientOptions options = GetCommonOptions(); - await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build(); - startup.Configure(app); - await app.StartAsync(TestContext.Current.CancellationToken); + using var handler = new DelegateToMockHttpClientHandler(); + handler.Mock.Expect(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}").Respond(HttpStatusCode.InternalServerError); - using TestServer server = app.GetTestServer(); - server.BaseAddress = new Uri("http://localhost:8888"); + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); - ConfigServerClientOptions options = GetCommonOptions(); - using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance); - - // ReSharper disable once AccessToDisposedClosure - Func action = async () => await provider.RemoteLoadAsync(options.GetUris(), null, TestContext.Current.CancellationToken); + Func action = async () => await provider.RemoteLoadAsync(provider.ClientOptions, options.GetUris(), null, TestContext.Current.CancellationToken); await action.Should().ThrowExactlyAsync(); - startup.LastRequest.Should().NotBeNull(); - startup.LastRequest.Path.Value.Should().Be($"/{options.Name}/{options.Environment}"); + handler.Mock.VerifyNoOutstandingExpectation(); } [Fact] public async Task RemoteLoadAsync_ConfigServerReturnsLessThanBadRequest() { - using var startup = new TestConfigServerStartup(); - startup.ReturnStatus = [204]; - - await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build(); - startup.Configure(app); - await app.StartAsync(TestContext.Current.CancellationToken); + ConfigServerClientOptions options = GetCommonOptions(); - using TestServer server = app.GetTestServer(); - server.BaseAddress = new Uri("http://localhost:8888"); + using var handler = new DelegateToMockHttpClientHandler(); + handler.Mock.Expect(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}").Respond(HttpStatusCode.NoContent); - ConfigServerClientOptions options = GetCommonOptions(); - using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); - ConfigEnvironment? result = await provider.RemoteLoadAsync(options.GetUris(), null, TestContext.Current.CancellationToken); + ConfigEnvironment? result = await provider.RemoteLoadAsync(provider.ClientOptions, options.GetUris(), null, TestContext.Current.CancellationToken); - startup.LastRequest.Should().NotBeNull(); - startup.LastRequest.Path.Value.Should().Be($"/{options.Name}/{options.Environment}"); + handler.Mock.VerifyNoOutstandingExpectation(); result.Should().BeNull(); } [Fact] - public async Task Create_WithPollingTimer() + public async Task Create_WithConfigurationReloadTimer() { await TestFailureTracer.CaptureAsync(async tracer => { - const string environment = """ + const string responseJson = """ { "name": "test-name", "profiles": [ @@ -98,18 +87,6 @@ await TestFailureTracer.CaptureAsync(async tracer => } """; - using var startup = new TestConfigServerStartup(); - startup.Response = environment; - startup.ReturnStatus = [.. Enumerable.Repeat(200, 100)]; - startup.Label = "test-label"; - - await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build(); - startup.Configure(app); - await app.StartAsync(TestContext.Current.CancellationToken); - - using TestServer server = app.GetTestServer(); - server.BaseAddress = new Uri("http://localhost:8888"); - var options = new ConfigServerClientOptions { Name = "myName", @@ -117,28 +94,45 @@ await TestFailureTracer.CaptureAsync(async tracer => Label = "label,test-label" }; - using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, tracer.LoggerFactory); + using var handler = new DelegateToMockHttpClientHandler(); + handler.Mock.When(HttpMethod.Get, "http://localhost:8888/myName/Production/label").Respond(HttpStatusCode.NotFound); + + using var firstRequestCountdownEvent = new CountdownEvent(1); + + MockedRequest testLabelRequest = handler.Mock.When(HttpMethod.Get, "http://localhost:8888/myName/Production/test-label").Respond(_ => + { + if (!firstRequestCountdownEvent.IsSet) + { + firstRequestCountdownEvent.Signal(); + } + + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(responseJson, Encoding.UTF8, "application/json") + }; + }); + + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, tracer.LoggerFactory); + provider.Load(); - bool firstRequestCompleted = startup.WaitForFirstRequest(2.Seconds()); + bool firstRequestCompleted = firstRequestCountdownEvent.Wait(2.Seconds(), TestContext.Current.CancellationToken); firstRequestCompleted.Should().BeTrue(); - startup.RequestCount.Should().BeGreaterThanOrEqualTo(1); - startup.LastRequest.Should().NotBeNull(); + handler.Mock.GetMatchCount(testLabelRequest).Should().BeGreaterThanOrEqualTo(1); await Task.Delay(2.Seconds(), TestContext.Current.CancellationToken); - startup.RequestCount.Should().BeGreaterThanOrEqualTo(2); + handler.Mock.GetMatchCount(testLabelRequest).Should().BeGreaterThanOrEqualTo(2); provider.GetReloadToken().HasChanged.Should().BeFalse(); }); } [Fact] - public async Task Create_FailFastEnabledAndExceptionThrownDuringPolling_DoesNotCrash() + public async Task Create_FailFastEnabledAndExceptionThrownDuringPolledConfigurationReload_DoesNotCrash() { await TestFailureTracer.CaptureAsync(async tracer => { - const string environment = """ + const string responseJson = """ { "name": "test-name", "profiles": [ @@ -150,20 +144,6 @@ await TestFailureTracer.CaptureAsync(async tracer => } """; - using var startup = new TestConfigServerStartup(); - startup.Response = environment; - - // Initial requests succeed, but later requests return 400 status code so that an exception is thrown during polling - startup.ReturnStatus = [.. Enumerable.Repeat(200, 2).Concat(Enumerable.Repeat(400, 100))]; - startup.Label = "test-label"; - - await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build(); - startup.Configure(app); - await app.StartAsync(TestContext.Current.CancellationToken); - - using TestServer server = app.GetTestServer(); - server.BaseAddress = new Uri("http://localhost:8888"); - var options = new ConfigServerClientOptions { Name = "myName", @@ -172,28 +152,139 @@ await TestFailureTracer.CaptureAsync(async tracer => Label = "test-label" }; - using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, tracer.LoggerFactory); + using var handler = new DelegateToMockHttpClientHandler(); + using var firstRequestCountdownEvent = new CountdownEvent(1); + int requestCount = 0; + + MockedRequest testLabelRequest = handler.Mock.When(HttpMethod.Get, "http://localhost:8888/myName/Production/test-label").Respond(_ => + { + int currentCount = Interlocked.Increment(ref requestCount); + + if (!firstRequestCountdownEvent.IsSet) + { + firstRequestCountdownEvent.Signal(); + } + + if (currentCount <= 2) + { + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(responseJson, Encoding.UTF8, "application/json") + }; + } - bool firstRequestCompleted = startup.WaitForFirstRequest(2.Seconds()); + return new HttpResponseMessage(HttpStatusCode.BadRequest); + }); + + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, tracer.LoggerFactory); + provider.Load(); + + bool firstRequestCompleted = firstRequestCountdownEvent.Wait(2.Seconds(), TestContext.Current.CancellationToken); firstRequestCompleted.Should().BeTrue(); - startup.RequestCount.Should().BeGreaterThanOrEqualTo(1); - startup.LastRequest.Should().NotBeNull(); + handler.Mock.GetMatchCount(testLabelRequest).Should().BeGreaterThanOrEqualTo(1); await Task.Delay(2.Seconds(), TestContext.Current.CancellationToken); - startup.RequestCount.Should().BeGreaterThanOrEqualTo(2); + handler.Mock.GetMatchCount(testLabelRequest).Should().BeGreaterThanOrEqualTo(2); provider.GetReloadToken().HasChanged.Should().BeFalse(); }); } [Fact] - public async Task Create_WithNonZeroPollingIntervalAndClientDisabled_PollingDisabled() + public async Task Create_WithNonZeroPollingIntervalAndClientDisabled_PollingConfigurationReloadDisabled() { - const string environment = """ + var options = new ConfigServerClientOptions + { + Name = "myName", + Enabled = false, + PollingInterval = 300.Milliseconds(), + Label = "label,test-label" + }; + + using var handler = new DelegateToMockHttpClientHandler(); + MockedRequest request = handler.Mock.When(HttpMethod.Get, "http://localhost:8888/myName/Production/label").Respond(HttpStatusCode.OK); + + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); + provider.Load(); + + await Task.Delay(2.Seconds(), TestContext.Current.CancellationToken); + handler.Mock.GetMatchCount(request).Should().Be(0); + } + + [Theory] + [InlineData(false, "00:00:01")] + [InlineData(true, "00:00:00")] + public void OnSettingsChanged_stops_reload_timer_when_polling_no_longer_enabled(bool enabled, string pollingInterval) + { + const string responseJson = """ { - "name": "test-name", + "name": "myName", + "profiles": [ + "Production" + ], + "label": "test-label", + "version": "test-version", + "propertySources": [] + } + """; + + var fileProvider = new MemoryFileProvider(); + + fileProvider.IncludeAppSettingsJsonFile(""" + { + "spring": { + "cloud": { + "config": { + "name": "myName", + "enabled": true, + "pollingInterval": "00:00:01" + } + } + } + } + """); + + using var handler = new DelegateToMockHttpClientHandler(); + handler.Mock.When(HttpMethod.Get, "http://localhost:8888/myName/Production").Respond("application/json", responseJson); + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); + configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), null, () => handler, NullLoggerFactory.Instance); + IConfigurationRoot configuration = configurationBuilder.Build(); + + ConfigServerConfigurationProvider provider = configuration.Providers.OfType().Single(); + + FieldInfo reloadTimerField = + typeof(ConfigServerConfigurationProvider).GetField("_configurationReloadTimer", BindingFlags.NonPublic | BindingFlags.Instance)!; + + reloadTimerField.GetValue(provider).Should().NotBeNull(); + + fileProvider.ReplaceAppSettingsJsonFile($$""" + { + "spring": { + "cloud": { + "config": { + "name": "myName", + "enabled": {{(enabled ? "true" : "false")}}, + "pollingInterval": "{{pollingInterval}}" + } + } + } + } + """); + + fileProvider.NotifyChanged(); + + reloadTimerField.GetValue(provider).Should().BeNull(); + } + + [Fact] + public void OnSettingsChanged_reschedules_reload_timer_when_polling_interval_changes() + { + const string responseJson = """ + { + "name": "myName", "profiles": [ "Production" ], @@ -203,37 +294,164 @@ public async Task Create_WithNonZeroPollingIntervalAndClientDisabled_PollingDisa } """; - using var startup = new TestConfigServerStartup(); - startup.Response = environment; - startup.ReturnStatus = [.. Enumerable.Repeat(200, 100)]; - startup.Label = "test-label"; + var fileProvider = new MemoryFileProvider(); + + fileProvider.IncludeAppSettingsJsonFile(""" + { + "spring": { + "cloud": { + "config": { + "name": "myName", + "enabled": true, + "pollingInterval": "00:00:05" + } + } + } + } + """); + + using var handler = new DelegateToMockHttpClientHandler(); + handler.Mock.When(HttpMethod.Get, "http://localhost:8888/myName/Production").Respond("application/json", responseJson); + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); + configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), null, () => handler, NullLoggerFactory.Instance); + IConfigurationRoot configuration = configurationBuilder.Build(); + + ConfigServerConfigurationProvider provider = configuration.Providers.OfType().Single(); + + FieldInfo reloadTimerField = + typeof(ConfigServerConfigurationProvider).GetField("_configurationReloadTimer", BindingFlags.NonPublic | BindingFlags.Instance)!; + + reloadTimerField.GetValue(provider).Should().NotBeNull(); + + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "spring": { + "cloud": { + "config": { + "name": "myName", + "enabled": true, + "pollingInterval": "00:00:10" + } + } + } + } + """); - await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build(); - startup.Configure(app); - await app.StartAsync(TestContext.Current.CancellationToken); + fileProvider.NotifyChanged(); - using TestServer server = app.GetTestServer(); - server.BaseAddress = new Uri("http://localhost:8888"); + reloadTimerField.GetValue(provider).Should().NotBeNull("timer should be rescheduled, not stopped"); + } + + [Fact] + public void OnSettingsChanged_stops_vault_renew_timer_when_renewal_becomes_disabled() + { + const string responseJson = """ + { + "name": "myName", + "profiles": [ + "Production" + ], + "label": "test-label", + "version": "test-version", + "propertySources": [] + } + """; + + var fileProvider = new MemoryFileProvider(); + + fileProvider.IncludeAppSettingsJsonFile(""" + { + "spring": { + "cloud": { + "config": { + "name": "myName", + "token": "MyVaultToken" + } + } + } + } + """); + + using var handler = new DelegateToMockHttpClientHandler(); + handler.Mock.When(HttpMethod.Get, "http://localhost:8888/myName/Production").Respond("application/json", responseJson); + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); + configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), null, () => handler, NullLoggerFactory.Instance); + IConfigurationRoot configuration = configurationBuilder.Build(); + + ConfigServerConfigurationProvider provider = configuration.Providers.OfType().Single(); + FieldInfo vaultTimerField = typeof(ConfigServerConfigurationProvider).GetField("_vaultRenewTimer", BindingFlags.NonPublic | BindingFlags.Instance)!; + + vaultTimerField.GetValue(provider).Should().NotBeNull(); + + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "spring": { + "cloud": { + "config": { + "name": "myName", + "token": "MyVaultToken", + "disableTokenRenewal": true + } + } + } + } + """); + + fileProvider.NotifyChanged(); + + vaultTimerField.GetValue(provider).Should().BeNull(); + } + + [Fact] + public void Load_MultipleConfigServers_SocketError_FallsBackToNextServer() + { + const string responseJson = """ + { + "name": "test-name", + "profiles": [ + "Production" + ], + "label": "test-label", + "version": "test-version", + "propertySources": [ + { + "name": "source", + "source": { + "key1": "value1" + } + } + ] + } + """; + + using var handler = new DelegateToMockHttpClientHandler(); + + handler.Mock.When(HttpMethod.Get, "http://server1:8888/myName/Production") + .Throw(new HttpRequestException("Connection refused", new SocketException((int)SocketError.ConnectionRefused))); + + handler.Mock.When(HttpMethod.Get, "http://server2:8888/myName/Production").Respond("application/json", responseJson); var options = new ConfigServerClientOptions { Name = "myName", - Enabled = false, - PollingInterval = 300.Milliseconds(), - Label = "label,test-label" + Uri = "http://server1:8888, http://server2:8888" }; - using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - - using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); + provider.Load(); - startup.WaitForFirstRequest(2.Seconds()).Should().BeFalse(); + provider.TryGet("key1", out string? value).Should().BeTrue(); + value.Should().Be("value1"); } [Fact] - public async Task DoLoad_MultipleLabels_ChecksAllLabels() + public void Load_IdenticalData_DoesNotTriggerReload() { - const string environment = """ + const string responseJson = """ { "name": "test-name", "profiles": [ @@ -245,49 +463,77 @@ public async Task DoLoad_MultipleLabels_ChecksAllLabels() { "name": "source", "source": { - "key1": "value1", - "key2": 10 + "key1": "value1" } } ] } """; - using var startup = new TestConfigServerStartup(); - startup.Response = environment; + using var handler = new DelegateToMockHttpClientHandler(); + handler.Mock.When(HttpMethod.Get, "http://localhost:8888/myName/Production").Respond("application/json", responseJson); + + var options = new ConfigServerClientOptions + { + Name = "myName" + }; - startup.ReturnStatus = - [ - 404, - 200 - ]; + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); + provider.Load(); - startup.Label = "test-label"; + provider.TryGet("key1", out string? value).Should().BeTrue(); + value.Should().Be("value1"); - await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build(); - startup.Configure(app); - await app.StartAsync(TestContext.Current.CancellationToken); + bool reloadFired = false; + provider.GetReloadToken().RegisterChangeCallback(_ => reloadFired = true, null); - using TestServer server = app.GetTestServer(); - server.BaseAddress = new Uri("http://localhost:8888"); + provider.Load(); + + reloadFired.Should().BeFalse("identical data should not trigger OnReload"); + provider.TryGet("key1", out value).Should().BeTrue(); + value.Should().Be("value1"); + } + + [Fact] + public void Load_MultipleLabels_ChecksAllLabels() + { + const string responseJson = """ + { + "name": "test-name", + "profiles": [ + "Production" + ], + "label": "test-label", + "version": "test-version", + "propertySources": [ + { + "name": "source", + "source": { + "key1": "value1", + "key2": 10 + } + } + ] + } + """; ConfigServerClientOptions options = GetCommonOptions(); options.Label = "label,test-label"; - using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance); + using var handler = new DelegateToMockHttpClientHandler(); + handler.Mock.Expect(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}/label").Respond(HttpStatusCode.NotFound); + handler.Mock.Expect(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}/test-label").Respond("application/json", responseJson); - await provider.DoLoadAsync(true, TestContext.Current.CancellationToken); + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); + provider.Load(); - startup.LastRequest.Should().NotBeNull(); - startup.RequestCount.Should().Be(2); - startup.LastRequest.Path.Value.Should().Be($"/{options.Name}/{options.Environment}/test-label"); + handler.Mock.VerifyNoOutstandingExpectation(); } [Fact] public async Task RemoteLoadAsync_ConfigServerReturnsGood() { - const string environment = """ + const string responseJson = """ { "name": "test-name", "profiles": [ @@ -307,24 +553,16 @@ public async Task RemoteLoadAsync_ConfigServerReturnsGood() } """; - using var startup = new TestConfigServerStartup(); - startup.Response = environment; - - await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build(); - startup.Configure(app); - await app.StartAsync(TestContext.Current.CancellationToken); + ConfigServerClientOptions options = GetCommonOptions(); - using TestServer server = app.GetTestServer(); - server.BaseAddress = new Uri("http://localhost:8888"); + using var handler = new DelegateToMockHttpClientHandler(); + handler.Mock.Expect(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}").Respond("application/json", responseJson); - ConfigServerClientOptions options = GetCommonOptions(); - using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); - ConfigEnvironment? env = await provider.RemoteLoadAsync(options.GetUris(), null, TestContext.Current.CancellationToken); + ConfigEnvironment? env = await provider.RemoteLoadAsync(provider.ClientOptions, options.GetUris(), null, TestContext.Current.CancellationToken); - startup.LastRequest.Should().NotBeNull(); - startup.LastRequest.Path.Value.Should().Be($"/{options.Name}/{options.Environment}"); + handler.Mock.VerifyNoOutstandingExpectation(); env.Should().NotBeNull(); env.Name.Should().Be("test-name"); @@ -342,245 +580,156 @@ public async Task RemoteLoadAsync_ConfigServerReturnsGood() [Fact] public async Task Load_MultipleConfigServers_ReturnsGreaterThanEqualBadRequest_StopsChecking() { - using var startup = new TestConfigServerStartup(); - - startup.ReturnStatus = - [ - 500, - 200 - ]; - - await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build(); - startup.Configure(app); - await app.StartAsync(TestContext.Current.CancellationToken); - - using TestServer server = app.GetTestServer(); - server.BaseAddress = new Uri("http://localhost:8888"); - ConfigServerClientOptions options = GetCommonOptions(); options.Uri = "http://localhost:8888, http://localhost:8888"; - using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance); - await provider.LoadInternalAsync(true, TestContext.Current.CancellationToken); + using var handler = new DelegateToMockHttpClientHandler(); + + MockedRequest request = handler.Mock.When(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}") + .Respond(HttpStatusCode.InternalServerError); - startup.LastRequest.Should().NotBeNull(); - startup.LastRequest.Path.Value.Should().Be($"/{options.Name}/{options.Environment}"); - startup.RequestCount.Should().Be(1); + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); + provider.Load(); + + handler.Mock.GetMatchCount(request).Should().Be(1); await Task.Delay(2.Seconds(), TestContext.Current.CancellationToken); - startup.RequestCount.Should().Be(1); + handler.Mock.GetMatchCount(request).Should().Be(1); } [Fact] public async Task Load_MultipleConfigServers_ReturnsNotFoundStatus_DoesNotContinueChecking() { - using var startup = new TestConfigServerStartup(); - - startup.ReturnStatus = - [ - 404, - 200 - ]; - - await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build(); - startup.Configure(app); - await app.StartAsync(TestContext.Current.CancellationToken); - - using TestServer server = app.GetTestServer(); - server.BaseAddress = new Uri("http://localhost:8888"); - ConfigServerClientOptions options = GetCommonOptions(); options.Uri = "http://localhost:8888, http://localhost:8888"; - using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance); + using var handler = new DelegateToMockHttpClientHandler(); - await provider.LoadInternalAsync(true, TestContext.Current.CancellationToken); + MockedRequest request = handler.Mock.When(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}") + .Respond(HttpStatusCode.NotFound); - startup.LastRequest.Should().NotBeNull(); - startup.LastRequest.Path.Value.Should().Be($"/{options.Name}/{options.Environment}"); - startup.RequestCount.Should().Be(1); + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); + provider.Load(); + + handler.Mock.GetMatchCount(request).Should().Be(1); await Task.Delay(2.Seconds(), TestContext.Current.CancellationToken); - startup.RequestCount.Should().Be(1); + handler.Mock.GetMatchCount(request).Should().Be(1); } [Fact] - public async Task Load_ConfigServerReturnsNotFoundStatus() + public void Load_ConfigServerReturnsNotFoundStatus() { - using var startup = new TestConfigServerStartup(); - startup.ReturnStatus = [404]; - - await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build(); - startup.Configure(app); - await app.StartAsync(TestContext.Current.CancellationToken); - - using TestServer server = app.GetTestServer(); - server.BaseAddress = new Uri("http://localhost:8888"); - ConfigServerClientOptions options = GetCommonOptions(); - using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance); - await provider.LoadInternalAsync(true, TestContext.Current.CancellationToken); + using var handler = new DelegateToMockHttpClientHandler(); + handler.Mock.Expect(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}").Respond(HttpStatusCode.NotFound); + + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); + provider.Load(); - startup.LastRequest.Should().NotBeNull(); - startup.LastRequest.Path.Value.Should().Be($"/{options.Name}/{options.Environment}"); - provider.Properties.Should().HaveCount(27); + handler.Mock.VerifyNoOutstandingExpectation(); + provider.InnerData.Should().BeEmpty(); } [Fact] - public async Task Load_ConfigServerReturnsNotFoundStatus_FailFastEnabled() + public void Load_ConfigServerReturnsNotFoundStatus_FailFastEnabled() { - using var startup = new TestConfigServerStartup(); - startup.ReturnStatus = [404]; - - await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build(); - startup.Configure(app); - await app.StartAsync(TestContext.Current.CancellationToken); - - using TestServer server = app.GetTestServer(); - server.BaseAddress = new Uri("http://localhost:8888"); - ConfigServerClientOptions options = GetCommonOptions(); options.FailFast = true; - using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance); + using var handler = new DelegateToMockHttpClientHandler(); + handler.Mock.Expect(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}").Respond(HttpStatusCode.NotFound); - // ReSharper disable once AccessToDisposedClosure - Func action = async () => await provider.LoadInternalAsync(true, TestContext.Current.CancellationToken); + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); - await action.Should().ThrowExactlyAsync(); + Action action = provider.Load; + + action.Should().ThrowExactly(); + handler.Mock.VerifyNoOutstandingExpectation(); } [Fact] public async Task Load_MultipleConfigServers_ReturnsNotFoundStatus__DoesNotContinueChecking_FailFastEnabled() { - using var startup = new TestConfigServerStartup(); - - await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build(); - startup.Configure(app); - await app.StartAsync(TestContext.Current.CancellationToken); - - using TestServer server = app.GetTestServer(); - server.BaseAddress = new Uri("http://localhost:8888"); - ConfigServerClientOptions options = GetCommonOptions(); options.FailFast = true; options.Uri = "http://localhost:8888,http://localhost:8888"; - using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance); + using var handler = new DelegateToMockHttpClientHandler(); - startup.Reset(); + MockedRequest request = handler.Mock.When(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}") + .Respond(HttpStatusCode.NotFound); - startup.ReturnStatus = - [ - 404, - 200 - ]; + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); - // ReSharper disable once AccessToDisposedClosure - Func action = async () => await provider.LoadInternalAsync(true, TestContext.Current.CancellationToken); + Action action = provider.Load; - await action.Should().ThrowExactlyAsync(); - startup.RequestCount.Should().Be(1); + action.Should().ThrowExactly(); + handler.Mock.GetMatchCount(request).Should().Be(1); await Task.Delay(2.Seconds(), TestContext.Current.CancellationToken); - startup.RequestCount.Should().Be(1); + handler.Mock.GetMatchCount(request).Should().Be(1); } [Fact] - public async Task Load_UriInvalid_FailFastEnabled() + public void Load_UriInvalid_FailFastEnabled() { - using var startup = new TestConfigServerStartup(); - startup.ReturnStatus = [500]; - - await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build(); - startup.Configure(app); - await app.StartAsync(TestContext.Current.CancellationToken); - - using TestServer server = app.GetTestServer(); - server.BaseAddress = new Uri("http://localhost:8888"); - ConfigServerClientOptions options = GetCommonOptions(); options.Uri = "http://username:p@ssword@localhost:8888"; options.FailFast = true; - using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance); + using var handler = new DelegateToMockHttpClientHandler(); + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); - // ReSharper disable once AccessToDisposedClosure - Func action = async () => await provider.LoadInternalAsync(true, TestContext.Current.CancellationToken); + Action action = provider.Load; - await action.Should().ThrowExactlyAsync().WithMessage("One or more Config Server URIs in configuration are invalid."); + action.Should().ThrowExactly().WithMessage("One or more Config Server URIs in configuration are invalid."); + handler.Mock.VerifyNoOutstandingExpectation(); } [Fact] - public async Task Load_ConfigServerReturnsBadStatus_FailFastEnabled() + public void Load_ConfigServerReturnsBadStatus_FailFastEnabled() { - using var startup = new TestConfigServerStartup(); - startup.ReturnStatus = [500]; - - await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build(); - startup.Configure(app); - await app.StartAsync(TestContext.Current.CancellationToken); - - using TestServer server = app.GetTestServer(); - server.BaseAddress = new Uri("http://localhost:8888"); - ConfigServerClientOptions options = GetCommonOptions(); options.FailFast = true; - using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance); + using var handler = new DelegateToMockHttpClientHandler(); + handler.Mock.Expect(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}").Respond(HttpStatusCode.InternalServerError); + + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); - // ReSharper disable once AccessToDisposedClosure - Func action = async () => await provider.LoadInternalAsync(true, TestContext.Current.CancellationToken); + Action action = provider.Load; - await action.Should().ThrowExactlyAsync(); + action.Should().ThrowExactly(); + handler.Mock.VerifyNoOutstandingExpectation(); } [Fact] public async Task Load_MultipleConfigServers_ReturnsBadStatus_StopsChecking_FailFastEnabled() { - using var startup = new TestConfigServerStartup(); - - startup.ReturnStatus = - [ - 500, - 500, - 500 - ]; - - await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build(); - startup.Configure(app); - await app.StartAsync(TestContext.Current.CancellationToken); - - using TestServer server = app.GetTestServer(); - server.BaseAddress = new Uri("http://localhost:8888"); - ConfigServerClientOptions options = GetCommonOptions(); options.FailFast = true; options.Uri = "http://localhost:8888, http://localhost:8888, http://localhost:8888"; - using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance); + using var handler = new DelegateToMockHttpClientHandler(); - // ReSharper disable once AccessToDisposedClosure - Func action = async () => await provider.LoadInternalAsync(true, TestContext.Current.CancellationToken); + MockedRequest request = handler.Mock.When(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}") + .Respond(HttpStatusCode.InternalServerError); - await action.Should().ThrowExactlyAsync(); - startup.RequestCount.Should().Be(1); + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); + + Action action = provider.Load; + + action.Should().ThrowExactly(); + handler.Mock.GetMatchCount(request).Should().Be(1); await Task.Delay(2.Seconds(), TestContext.Current.CancellationToken); - startup.RequestCount.Should().Be(1); + handler.Mock.GetMatchCount(request).Should().Be(1); } [Fact] @@ -588,16 +737,6 @@ public async Task Load_ConfigServerReturnsBadStatus_FailFastEnabled_RetryEnabled { await TestFailureTracer.CaptureAsync(async tracer => { - using var startup = new TestConfigServerStartup(); - startup.ReturnStatus = [.. Enumerable.Repeat(500, 100)]; - - await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build(); - startup.Configure(app); - await app.StartAsync(TestContext.Current.CancellationToken); - - using TestServer server = app.GetTestServer(); - server.BaseAddress = new Uri("http://localhost:8888"); - var options = new ConfigServerClientOptions { Name = "myName", @@ -610,24 +749,25 @@ await TestFailureTracer.CaptureAsync(async tracer => Timeout = 1000 }; - using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, tracer.LoggerFactory); + using var handler = new DelegateToMockHttpClientHandler(); + MockedRequest request = handler.Mock.When(HttpMethod.Get, "http://localhost:8888/myName/Production").Respond(HttpStatusCode.InternalServerError); + + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, tracer.LoggerFactory); - // ReSharper disable once AccessToDisposedClosure - Func action = async () => await provider.LoadInternalAsync(true, TestContext.Current.CancellationToken); + Action action = () => provider.Load(); - await action.Should().ThrowExactlyAsync(); + action.Should().ThrowExactly(); await Task.Delay(2.Seconds(), TestContext.Current.CancellationToken); - startup.RequestCount.Should().BeGreaterThan(3); + handler.Mock.GetMatchCount(request).Should().BeGreaterThan(3); }); } [Fact] - public async Task Load_ChangesDataDictionary() + public void Load_ChangesDataDictionary() { - const string environment = """ + const string responseJson = """ { "name": "test-name", "profiles": [ @@ -635,6 +775,7 @@ public async Task Load_ChangesDataDictionary() ], "label": "test-label", "version": "test-version", + "state": "test-state", "propertySources": [ { "name": "source", @@ -647,35 +788,30 @@ public async Task Load_ChangesDataDictionary() } """; - using var startup = new TestConfigServerStartup(); - startup.Response = environment; - - await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build(); - startup.Configure(app); - await app.StartAsync(TestContext.Current.CancellationToken); - - using TestServer server = app.GetTestServer(); - server.BaseAddress = new Uri("http://localhost:8888"); - ConfigServerClientOptions options = GetCommonOptions(); - using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance); - await provider.LoadInternalAsync(true, TestContext.Current.CancellationToken); + using var handler = new DelegateToMockHttpClientHandler(); + handler.Mock.Expect(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}").Respond("application/json", responseJson); - startup.LastRequest.Should().NotBeNull(); - startup.LastRequest.Path.Value.Should().Be($"/{options.Name}/{options.Environment}"); + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); + provider.Load(); + handler.Mock.VerifyNoOutstandingExpectation(); provider.TryGet("key1", out string? value).Should().BeTrue(); value.Should().Be("value1"); provider.TryGet("key2", out value).Should().BeTrue(); value.Should().Be("10"); + + provider.TryGet("spring:cloud:config:client:version", out value).Should().BeTrue(); + value.Should().Be("test-version"); + provider.TryGet("spring:cloud:config:client:state", out value).Should().BeTrue(); + value.Should().Be("test-state"); } [Fact] - public async Task ReLoad_DataDictionary_With_New_Configurations() + public void ReLoad_DataDictionary_With_New_Configurations() { - const string environment = """ + const string responseJson = """ { "name": "test-name", "profiles": [ @@ -697,23 +833,15 @@ public async Task ReLoad_DataDictionary_With_New_Configurations() } """; - using var startup = new TestConfigServerStartup(); - startup.Response = environment; - - await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build(); - startup.Configure(app); - await app.StartAsync(TestContext.Current.CancellationToken); - - using TestServer server = app.GetTestServer(); - server.BaseAddress = new Uri("http://localhost:8888"); - ConfigServerClientOptions options = GetCommonOptions(); - using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance); + using var handler = new DelegateToMockHttpClientHandler(); + handler.Mock.Expect(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}").Respond("application/json", responseJson); + + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); provider.Load(); + handler.Mock.VerifyNoOutstandingExpectation(); - startup.LastRequest.Should().NotBeNull(); provider.TryGet("featureToggles:ShowModule:0", out string? value).Should().BeTrue(); value.Should().Be("FT1"); provider.TryGet("featureToggles:ShowModule:1", out value).Should().BeTrue(); @@ -723,28 +851,30 @@ public async Task ReLoad_DataDictionary_With_New_Configurations() provider.TryGet("enableSettings", out value).Should().BeTrue(); value.Should().Be("true"); - startup.Reset(); + handler.Mock.Clear(); - startup.Response = """ - { - "name": "test-name", - "profiles": [ - "Production" - ], - "label": "test-label", - "version": "test-version", - "propertySources": [ + const string newResponseJson = """ { - "name": "source", - "source": { - "featureToggles.ShowModule[0]": "none" - } + "name": "test-name", + "profiles": [ + "Production" + ], + "label": "test-label", + "version": "test-version", + "propertySources": [ + { + "name": "source", + "source": { + "featureToggles.ShowModule[0]": "none" + } + } + ] } - ] - } - """; + """; + handler.Mock.Expect(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}").Respond("application/json", newResponseJson); provider.Load(); + handler.Mock.VerifyNoOutstandingExpectation(); provider.TryGet("featureToggles:ShowModule:0", out value).Should().BeTrue(); value.Should().Be("none"); @@ -754,35 +884,29 @@ public async Task ReLoad_DataDictionary_With_New_Configurations() } [Fact] - public void AddConfigServerClientSettings_ChangesDataDictionary() + public void DataDictionary_DoesNotContainRedundantClientSettings() { var options = new ConfigServerClientOptions { Enabled = false, FailFast = true, Environment = "environment", - Label = "label", + Label = "main", Name = "name", Uri = "https://foo.bar/", - Username = "username", - Password = "password", - Token = "vaultToken", + Username = "user", + Password = "pass", + Token = "vault-token", Timeout = 75_000, - PollingInterval = 35.5.Seconds(), + PollingInterval = TimeSpan.FromSeconds(30), ValidateCertificates = false, - AccessTokenUri = "https://token.server.com/", - ClientSecret = "client_secret", - ClientId = "client_id", - TokenTtl = 2, - TokenRenewRate = 1, - DisableTokenRenewal = true, Retry = { Enabled = true, - InitialInterval = 8, - MaxInterval = 16, - Multiplier = 1.1, - MaxAttempts = 7 + InitialInterval = 500, + MaxInterval = 5000, + Multiplier = 2.0, + MaxAttempts = 10 }, Discovery = { @@ -792,59 +916,63 @@ public void AddConfigServerClientSettings_ChangesDataDictionary() Health = { Enabled = false, - TimeToLive = 9 + TimeToLive = 999 }, + AccessTokenUri = "https://uaa.example.com/oauth/token", + ClientSecret = "secret", + ClientId = "client-id", + TokenTtl = 600_000, + TokenRenewRate = 120_000, + DisableTokenRenewal = true, Headers = { - ["headerName1"] = "headerValue1", - ["headerName2"] = "headerValue2" + ["X-Custom"] = "value" } }; - using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); - provider.AddConfigServerClientOptions(); - - AssertDataValue("spring:cloud:config:enabled", "False"); - AssertDataValue("spring:cloud:config:failFast", "True"); - AssertDataValue("spring:cloud:config:env", "environment"); - AssertDataValue("spring:cloud:config:label", "label"); - AssertDataValue("spring:cloud:config:name", "name"); - AssertDataValue("spring:cloud:config:uri", "https://foo.bar/"); - AssertDataValue("spring:cloud:config:username", "username"); - AssertDataValue("spring:cloud:config:password", "password"); - AssertDataValue("spring:cloud:config:token", "vaultToken"); - AssertDataValue("spring:cloud:config:timeout", "75000"); - AssertDataValue("spring:cloud:config:pollingInterval", "00:00:35.5000000"); - AssertDataValue("spring:cloud:config:validateCertificates", "False"); - AssertDataValue("spring:cloud:config:accessTokenUri", "https://token.server.com/"); - AssertDataValue("spring:cloud:config:clientSecret", "client_secret"); - AssertDataValue("spring:cloud:config:clientId", "client_id"); - AssertDataValue("spring:cloud:config:tokenTtl", "2"); - AssertDataValue("spring:cloud:config:tokenRenewRate", "1"); - AssertDataValue("spring:cloud:config:disableTokenRenewal", "True"); - AssertDataValue("spring:cloud:config:retry:enabled", "True"); - AssertDataValue("spring:cloud:config:retry:initialInterval", "8"); - AssertDataValue("spring:cloud:config:retry:maxInterval", "16"); - AssertDataValue("spring:cloud:config:retry:multiplier", "1.1"); - AssertDataValue("spring:cloud:config:retry:maxAttempts", "7"); - AssertDataValue("spring:cloud:config:discovery:enabled", "True"); - AssertDataValue("spring:cloud:config:discovery:serviceId", "my-config-server"); - AssertDataValue("spring:cloud:config:health:enabled", "False"); - AssertDataValue("spring:cloud:config:health:timeToLive", "9"); - AssertDataValue("spring:cloud:config:headers:headerName1", "headerValue1"); - AssertDataValue("spring:cloud:config:headers:headerName2", "headerValue2"); - - void AssertDataValue(string key, string expected) - { - provider.TryGet(key, out string? value).Should().BeTrue(); - value.Should().Be(expected); - } + using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); + provider.Load(); + + provider.TryGet("spring:cloud:config:enabled", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:failFast", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:env", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:label", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:name", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:uri", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:username", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:password", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:token", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:timeout", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:pollingInterval", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:validateCertificates", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:validate_Certificates", out _).Should().BeFalse(); + + provider.TryGet("spring:cloud:config:retry:enabled", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:retry:initialInterval", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:retry:maxInterval", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:retry:multiplier", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:retry:maxAttempts", out _).Should().BeFalse(); + + provider.TryGet("spring:cloud:config:discovery:enabled", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:discovery:serviceId", out _).Should().BeFalse(); + + provider.TryGet("spring:cloud:config:health:enabled", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:health:timeToLive", out _).Should().BeFalse(); + + provider.TryGet("spring:cloud:config:accessTokenUri", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:clientSecret", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:clientId", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:tokenTtl", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:tokenRenewRate", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:disableTokenRenewal", out _).Should().BeFalse(); + + provider.TryGet("spring:cloud:config:headers:X-Custom", out _).Should().BeFalse(); } [Fact] - public async Task Reload_And_Bind_Without_Throwing_Exception() + public void Reload_And_Bind_Without_Throwing_Exception() { - const string environment = """ + const string responseJson = """ { "name": "test-name", "profiles": [ @@ -864,30 +992,22 @@ public async Task Reload_And_Bind_Without_Throwing_Exception() } """; - using var startup = new TestConfigServerStartup(); - startup.Response = environment; - - await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build(); - startup.Configure(app); - await app.StartAsync(TestContext.Current.CancellationToken); - ConfigServerClientOptions clientOptions = GetCommonOptions(); - using TestServer server = app.GetTestServer(); - server.BaseAddress = new Uri(clientOptions.Uri!); - using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(clientOptions, null, httpClientHandler, NullLoggerFactory.Instance); + using var handler = new DelegateToMockHttpClientHandler(); + handler.Mock.When(HttpMethod.Get, $"http://localhost:8888/{clientOptions.Name}/{clientOptions.Environment}").Respond("application/json", responseJson); var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.Add(new TestConfigServerConfigurationSource(provider)); + configurationBuilder.AddConfigServer(clientOptions, null, () => handler, NullLoggerFactory.Instance); IConfigurationRoot configurationRoot = configurationBuilder.Build(); + using ConfigServerConfigurationProvider provider = configurationRoot.EnumerateProviders().Single(); + TestOptions? testOptions = null; using var tokenSource = new CancellationTokenSource(250.Milliseconds()); _ = Task.Run(() => { - // ReSharper disable once AccessToDisposedClosure while (!tokenSource.IsCancellationRequested) { configurationRoot.Reload(); @@ -908,7 +1028,8 @@ private static ConfigServerClientOptions GetCommonOptions() { return new ConfigServerClientOptions { - Name = "myName" + Name = "myName", + Environment = "Staging" }; } diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Settings.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Settings.cs index 8380debc6b..1cd6f664f6 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Settings.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Settings.cs @@ -15,22 +15,23 @@ public sealed partial class ConfigServerConfigurationProviderTest public void DefaultConstructor_InitializedWithDefaultSettings() { var options = new ConfigServerClientOptions(); - using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); + provider.Load(); string? expectedAppName = Assembly.GetEntryAssembly()!.GetName().Name; - TestHelper.VerifyDefaults(provider.ClientOptions, expectedAppName); + TestHelper.VerifyDefaults(provider.ClientOptions, expectedAppName, "Production"); } [Fact] public void SourceConstructor_WithDefaults_InitializesWithDefaultSettings() { - IConfiguration configuration = new ConfigurationBuilder().Build(); var options = new ConfigServerClientOptions(); - var source = new ConfigServerConfigurationSource(options, configuration, NullLoggerFactory.Instance); + var source = new ConfigServerConfigurationSource(options, [], null, null, null, NullLoggerFactory.Instance); using var provider = new ConfigServerConfigurationProvider(source, NullLoggerFactory.Instance); + provider.Load(); string? expectedAppName = Assembly.GetEntryAssembly()!.GetName().Name; - TestHelper.VerifyDefaults(provider.ClientOptions, expectedAppName); + TestHelper.VerifyDefaults(provider.ClientOptions, expectedAppName, "Production"); } [Fact] @@ -41,12 +42,13 @@ public void SourceConstructor_WithTimeoutConfigured_InitializesHttpClientWithCon ["spring:cloud:config:timeout"] = "30000" }; - IConfiguration configuration = new ConfigurationBuilder().AddInMemoryCollection(appSettings).Build(); + var builder = new ConfigurationBuilder(); + builder.AddInMemoryCollection(appSettings); + builder.AddConfigServer(); + IConfigurationRoot configuration = builder.Build(); - var options = new ConfigServerClientOptions(); - var source = new ConfigServerConfigurationSource(options, configuration, NullLoggerFactory.Instance); - using var provider = new ConfigServerConfigurationProvider(source, NullLoggerFactory.Instance); - using HttpClient httpClient = provider.CreateHttpClient(options); + ConfigServerConfigurationProvider provider = configuration.EnumerateProviders().Single(); + using HttpClient httpClient = provider.CreateHttpClient(provider.ClientOptions); httpClient.Should().NotBeNull(); httpClient.Timeout.Should().Be(30.Seconds()); @@ -61,10 +63,11 @@ public void GetConfigServerUri_NoBaseUri_Throws() Environment = "Production" }; - using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); - // ReSharper disable once AccessToDisposedClosure - Action action = () => provider.BuildConfigServerUri(null!, null); + // ReSharper disable AccessToDisposedClosure + Action action = () => provider.BuildConfigServerUri(provider.ClientOptions, null!, null); + // ReSharper restore AccessToDisposedClosure action.Should().ThrowExactly(); } @@ -78,8 +81,10 @@ public void GetConfigServerUri_NoLabel() Environment = "Production" }; - using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); - string uri = provider.BuildConfigServerUri(new Uri(options.Uri!), null).ToString(); + using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); + provider.Load(); + + string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri!), null).ToString(); uri.Should().Be($"{options.Uri}/{options.Name}/{options.Environment}"); } @@ -94,8 +99,10 @@ public void GetConfigServerUri_WithLabel() Label = "myLabel" }; - using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); - string uri = provider.BuildConfigServerUri(new Uri(options.Uri!), options.Label).ToString(); + using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); + provider.Load(); + + string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri!), options.Label).ToString(); uri.Should().Be($"{options.Uri}/{options.Name}/{options.Environment}/{options.Label}"); } @@ -110,8 +117,10 @@ public void GetConfigServerUri_WithLabelContainingSlash() Label = "myLabel/version" }; - using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); - string uri = provider.BuildConfigServerUri(new Uri(options.Uri!), options.Label).ToString(); + using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); + provider.Load(); + + string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri!), options.Label).ToString(); uri.Should().Be($"{options.Uri}/{options.Name}/{options.Environment}/myLabel(_)version"); } @@ -126,8 +135,10 @@ public void GetConfigServerUri_WithExtraPathInfo() Environment = "Production" }; - using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); - string uri = provider.BuildConfigServerUri(new Uri(options.Uri), null).ToString(); + using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); + provider.Load(); + + string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri), null).ToString(); uri.Should().Be($"http://localhost:9999/myPath/path/{options.Name}/{options.Environment}"); } @@ -142,8 +153,10 @@ public void GetConfigServerUri_WithExtraPathInfo_NoEndingSlash() Environment = "Production" }; - using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); - string uri = provider.BuildConfigServerUri(new Uri(options.Uri), null).ToString(); + using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); + provider.Load(); + + string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri), null).ToString(); uri.Should().Be($"http://localhost:9999/myPath/path/{options.Name}/{options.Environment}"); } @@ -158,8 +171,10 @@ public void GetConfigServerUri_NoEndingSlash() Environment = "Production" }; - using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); - string uri = provider.BuildConfigServerUri(new Uri(options.Uri), null).ToString(); + using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); + provider.Load(); + + string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri), null).ToString(); uri.Should().Be($"http://localhost:9999/{options.Name}/{options.Environment}"); } @@ -174,8 +189,10 @@ public void GetConfigServerUri_WithEndingSlash() Environment = "Production" }; - using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); - string uri = provider.BuildConfigServerUri(new Uri(options.Uri), null).ToString(); + using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); + provider.Load(); + + string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri), null).ToString(); uri.Should().Be($"http://localhost:9999/{options.Name}/{options.Environment}"); } @@ -190,8 +207,10 @@ public void GetConfigServerUri_MultipleEnvironments_EncodesComma() Label = "demo" }; - using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); - string uri = provider.BuildConfigServerUri(new Uri(options.Uri!), options.Label).ToString(); + using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); + provider.Load(); + + string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri!), options.Label).ToString(); uri.Should().Be("http://localhost:8888/myName/one%2Ctwo/demo"); } @@ -208,8 +227,10 @@ public void GetConfigServerUri_EncodesSpecialCharacters() Password = "#&:$<>'/so,\"me" }; - using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); - string uri = provider.BuildConfigServerUri(new Uri(options.Uri!), options.Label).ToString(); + using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); + provider.Load(); + + string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri!), options.Label).ToString(); uri.Should().Be( "http://a%22%3A%3F%27*%2Cb%2F%5C%26:%23%26%3A%24%3C%3E%27%2Fso%2C%22me@localhost:8888/my%24<>%3A%2C\"%27Name/%40%2F%26%3F%3A\"%27/^%26%24%40%23*<>%2B"); diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.cs index 1f13886305..5cccbdb791 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.cs @@ -65,9 +65,10 @@ public async Task Deserialize_GoodJson() public void GetLabels_Null() { var options = new ConfigServerClientOptions(); - using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); + provider.Load(); - string[] result = provider.GetLabels(); + string[] result = provider.GetLabels(provider.ClientOptions); result.Should().ContainSingle().Which.Should().BeEmpty(); } @@ -79,9 +80,10 @@ public void GetLabels_Empty() Label = string.Empty }; - using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); + provider.Load(); - string[] result = provider.GetLabels(); + string[] result = provider.GetLabels(provider.ClientOptions); result.Should().ContainSingle().Which.Should().BeEmpty(); } @@ -93,9 +95,10 @@ public void GetLabels_SingleString() Label = "foobar" }; - using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); + provider.Load(); - string[] result = provider.GetLabels(); + string[] result = provider.GetLabels(provider.ClientOptions); result.Should().ContainSingle().Which.Should().Be("foobar"); } @@ -107,9 +110,10 @@ public void GetLabels_MultiString() Label = "1,2,3," }; - using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); + provider.Load(); - string[] result = provider.GetLabels(); + string[] result = provider.GetLabels(provider.ClientOptions); result.Should().HaveCount(3); result.Should().HaveElementAt(0, "1"); result.Should().HaveElementAt(1, "2"); @@ -124,9 +128,10 @@ public void GetLabels_MultiStringHoles() Label = "1,,2,3," }; - using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); + provider.Load(); - string[] result = provider.GetLabels(); + string[] result = provider.GetLabels(provider.ClientOptions); result.Should().HaveCount(3); result.Should().HaveElementAt(0, "1"); result.Should().HaveElementAt(1, "2"); @@ -143,10 +148,11 @@ public async Task GetRequestMessage_AddsBasicAuthIfUserNameAndPasswordInURL() Environment = "development" }; - using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); + provider.Load(); - Uri requestUri = provider.BuildConfigServerUri(new Uri(options.Uri), null); - HttpRequestMessage request = await provider.GetRequestMessageAsync(requestUri, TestContext.Current.CancellationToken); + Uri requestUri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri), null); + HttpRequestMessage request = await provider.GetRequestMessageAsync(provider.ClientOptions, requestUri, TestContext.Current.CancellationToken); request.Method.Should().Be(HttpMethod.Get); request.RequestUri.Should().Be(requestUri); @@ -167,10 +173,11 @@ public async Task GetRequestMessage_AddsBasicAuthIfUserNameAndPasswordInSettings Password = "password" }; - using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); + provider.Load(); - Uri requestUri = provider.BuildConfigServerUri(new Uri(options.Uri), null); - HttpRequestMessage request = await provider.GetRequestMessageAsync(requestUri, TestContext.Current.CancellationToken); + Uri requestUri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri), null); + HttpRequestMessage request = await provider.GetRequestMessageAsync(provider.ClientOptions, requestUri, TestContext.Current.CancellationToken); request.Method.Should().Be(HttpMethod.Get); request.RequestUri.Should().Be(requestUri); @@ -191,10 +198,11 @@ public async Task GetRequestMessage_BasicAuthInSettingsOverridesUserNameAndPassw Password = "password" }; - using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); + provider.Load(); - Uri requestUri = provider.BuildConfigServerUri(new Uri(options.Uri), null); - HttpRequestMessage request = await provider.GetRequestMessageAsync(requestUri, TestContext.Current.CancellationToken); + Uri requestUri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri), null); + HttpRequestMessage request = await provider.GetRequestMessageAsync(provider.ClientOptions, requestUri, TestContext.Current.CancellationToken); request.Method.Should().Be(HttpMethod.Get); request.RequestUri.Should().Be(requestUri); @@ -213,10 +221,11 @@ public async Task GetRequestMessage_AddsVaultToken_IfNeeded() Token = "MyVaultToken" }; - using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); + provider.Load(); - Uri requestUri = provider.BuildConfigServerUri(new Uri(options.Uri!), null); - HttpRequestMessage request = await provider.GetRequestMessageAsync(requestUri, TestContext.Current.CancellationToken); + Uri requestUri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri!), null); + HttpRequestMessage request = await provider.GetRequestMessageAsync(provider.ClientOptions, requestUri, TestContext.Current.CancellationToken); request.Method.Should().Be(HttpMethod.Get); request.RequestUri.Should().Be(requestUri); @@ -225,6 +234,41 @@ public async Task GetRequestMessage_AddsVaultToken_IfNeeded() headerValues.Should().Contain("MyVaultToken"); } + [Fact] + public async Task GetRequestMessage_AddsBearerToken_WhenAccessTokenUriIsSet() + { + var options = new ConfigServerClientOptions + { + Uri = "http://localhost:8888/", + Name = "foo", + Environment = "development", + AccessTokenUri = "https://auth.server.com", + ClientId = "test-client-id", + ClientSecret = "test-client-secret" + }; + + using var handler = new DelegateToMockHttpClientHandler(); + + handler.Mock.Expect(HttpMethod.Post, "https://auth.server.com/").WithHeaders("Authorization", "Basic dGVzdC1jbGllbnQtaWQ6dGVzdC1jbGllbnQtc2VjcmV0") + .WithFormData("grant_type=client_credentials").Respond("application/json", """ + { + "access_token": "my-bearer-token" + } + """); + + // ReSharper disable once AccessToDisposedClosure + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); + + Uri requestUri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri), null); + HttpRequestMessage request = await provider.GetRequestMessageAsync(provider.ClientOptions, requestUri, TestContext.Current.CancellationToken); + + handler.Mock.VerifyNoOutstandingExpectation(); + + request.Headers.Authorization.Should().NotBeNull(); + request.Headers.Authorization.Scheme.Should().Be("Bearer"); + request.Headers.Authorization.Parameter.Should().Be("my-bearer-token"); + } + [Fact] public async Task RefreshVaultToken_Succeeds() { @@ -240,8 +284,11 @@ public async Task RefreshVaultToken_Succeeds() handler.Mock.Expect(HttpMethod.Post, "http://localhost:8888/vault/v1/auth/token/renew-self").WithHeaders("X-Vault-Token", "MyVaultToken") .WithContent("{\"increment\":300}").Respond(HttpStatusCode.NoContent); - using var provider = new ConfigServerConfigurationProvider(options, null, handler, NullLoggerFactory.Instance); - await provider.RefreshVaultTokenAsync(TestContext.Current.CancellationToken); + // ReSharper disable once AccessToDisposedClosure + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); + provider.Load(); + + await provider.RefreshVaultTokenAsync(provider.ClientOptions, TestContext.Current.CancellationToken); handler.Mock.VerifyNoOutstandingExpectation(); } @@ -267,8 +314,10 @@ public async Task RefreshVaultToken_With_AccessTokenUri_Succeeds() handler.Mock.Expect(HttpMethod.Post, "http://localhost:8888/vault/v1/auth/token/renew-self").WithHeaders("X-Vault-Token", "MyVaultToken") .WithHeaders("Authorization", "Bearer secret").WithContent("{\"increment\":300}").Respond(HttpStatusCode.NoContent); - using var provider = new ConfigServerConfigurationProvider(options, null, handler, NullLoggerFactory.Instance); - await provider.RefreshVaultTokenAsync(TestContext.Current.CancellationToken); + // ReSharper disable once AccessToDisposedClosure + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); + + await provider.RefreshVaultTokenAsync(provider.ClientOptions, TestContext.Current.CancellationToken); handler.Mock.VerifyNoOutstandingExpectation(); } @@ -287,8 +336,10 @@ public void GetHttpClient_AddsHeaders_IfConfigured() } }; - using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); - using HttpClient httpClient = provider.CreateHttpClient(options); + using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); + provider.Load(); + + using HttpClient httpClient = provider.CreateHttpClient(provider.ClientOptions); httpClient.Should().NotBeNull(); httpClient.DefaultRequestHeaders.GetValues("foo").SingleOrDefault().Should().Be("bar"); @@ -308,42 +359,42 @@ public void IsDiscoveryFirstEnabled_ReturnsExpected() } }; - using (var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance)) + using (var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance)) { - provider.IsDiscoveryFirstEnabled().Should().BeTrue(); + provider.Load(); + provider.ClientOptions.Discovery.Enabled.Should().BeTrue(); } - var values = new Dictionary + var appSettings = new Dictionary { ["spring:cloud:config:discovery:enabled"] = "True" }; - IConfiguration configuration = new ConfigurationBuilder().AddInMemoryCollection(values).Build(); - options = new ConfigServerClientOptions { Name = "foo", Environment = "development" }; - var source = new ConfigServerConfigurationSource(options, configuration, NullLoggerFactory.Instance); + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(appSettings); + configurationBuilder.AddConfigServer(options); + IConfigurationRoot configuration = configurationBuilder.Build(); - using (var provider = new ConfigServerConfigurationProvider(source, NullLoggerFactory.Instance)) + using (ConfigServerConfigurationProvider provider = configuration.EnumerateProviders().Single()) { - provider.IsDiscoveryFirstEnabled().Should().BeTrue(); + provider.ClientOptions.Discovery.Enabled.Should().BeTrue(); } } [Fact] public void UpdateSettingsFromDiscovery_UpdatesSettingsCorrectly() { - var values = new Dictionary + var appSettings = new Dictionary { ["spring:cloud:config:discovery:enabled"] = "True" }; - IConfiguration configuration = new ConfigurationBuilder().AddInMemoryCollection(values).Build(); - var options = new ConfigServerClientOptions { Uri = "http://localhost:8888/", @@ -351,13 +402,19 @@ public void UpdateSettingsFromDiscovery_UpdatesSettingsCorrectly() Environment = "development" }; - var source = new ConfigServerConfigurationSource(options, configuration, NullLoggerFactory.Instance); - using var provider = new ConfigServerConfigurationProvider(source, NullLoggerFactory.Instance); + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(appSettings); + configurationBuilder.AddConfigServer(options); + IConfigurationRoot configuration = configurationBuilder.Build(); - provider.UpdateSettingsFromDiscovery(new List(), options); - options.Username.Should().BeNull(); - options.Password.Should().BeNull(); - options.Uri.Should().Be("http://localhost:8888/"); + using ConfigServerConfigurationProvider provider = configuration.EnumerateProviders().Single(); + + ConfigServerClientOptions optionsSnapshot = provider.ClientOptions; + provider.SetLastDiscoveryLookupResult(new List()); + provider.ApplyLastDiscoveryLookupResultToClientOptions(optionsSnapshot); + optionsSnapshot.Username.Should().BeNull(); + optionsSnapshot.Password.Should().BeNull(); + optionsSnapshot.Uri.Should().Be("http://localhost:8888/"); var metadata1 = new Dictionary { @@ -377,24 +434,24 @@ public void UpdateSettingsFromDiscovery_UpdatesSettingsCorrectly() new TestServiceInstance("s", "i2", new Uri("https://foo.bar.baz:9999/"), metadata2) ]; - provider.UpdateSettingsFromDiscovery(instances, options); - options.Username.Should().Be("secondUser"); - options.Password.Should().Be("secondPassword"); - options.Uri.Should().Be("https://foo.bar:8888/,https://foo.bar.baz:9999/configPath"); + optionsSnapshot = provider.ClientOptions; + provider.SetLastDiscoveryLookupResult(instances); + provider.ApplyLastDiscoveryLookupResultToClientOptions(optionsSnapshot); + optionsSnapshot.Username.Should().Be("secondUser"); + optionsSnapshot.Password.Should().Be("secondPassword"); + optionsSnapshot.Uri.Should().Be("https://foo.bar:8888/,https://foo.bar.baz:9999/configPath"); } [Fact] - public async Task DiscoverServerInstances_FailsFast() + public void DiscoverServerInstances_FailsFast() { - var values = new Dictionary + var appSettings = new Dictionary { ["spring:cloud:config:discovery:enabled"] = "True", ["spring:cloud:config:failFast"] = "True", ["eureka:client:eurekaServer:retryCount"] = "0" }; - IConfiguration configuration = new ConfigurationBuilder().AddInMemoryCollection(values).Build(); - var options = new ConfigServerClientOptions { Name = "foo", @@ -402,30 +459,17 @@ public async Task DiscoverServerInstances_FailsFast() Timeout = 10 }; - var source = new ConfigServerConfigurationSource(options, configuration, NullLoggerFactory.Instance); - using var provider = new ConfigServerConfigurationProvider(source, NullLoggerFactory.Instance); + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(appSettings); + configurationBuilder.AddConfigServer(options); - // ReSharper disable once AccessToDisposedClosure - Func action = async () => await provider.LoadInternalAsync(true, TestContext.Current.CancellationToken); + Action action = () => _ = configurationBuilder.Build(); - await action.Should().ThrowExactlyAsync().WithMessage("Could not locate Config Server via discovery*"); + action.Should().ThrowExactly().WithMessage("Could not locate Config Server via discovery*"); } private static string GetEncodedUserPassword(string user, string password) { return Convert.ToBase64String(Encoding.ASCII.GetBytes($"{user}:{password}")); } - - private sealed class TestServiceInstance(string serviceId, string instanceId, Uri uri, IReadOnlyDictionary metadata) : IServiceInstance - { - public string ServiceId { get; } = serviceId; - public string InstanceId { get; } = instanceId; - public string Host { get; } = uri.Host; - public int Port { get; } = uri.Port; - public bool IsSecure { get; } = uri.Scheme == Uri.UriSchemeHttps; - public Uri Uri { get; } = uri; - public Uri? NonSecureUri => IsSecure ? null : Uri; - public Uri? SecureUri => IsSecure ? Uri : null; - public IReadOnlyDictionary Metadata { get; } = metadata; - } } diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationSourceTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationSourceTest.cs index 1be77f49a0..a66b415c5c 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationSourceTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationSourceTest.cs @@ -21,7 +21,7 @@ public void Constructors_InitializesProperties() var source = new ConfigServerConfigurationSource(options, sources, new Dictionary { ["foo"] = "bar" - }, NullLoggerFactory.Instance); + }, null, null, NullLoggerFactory.Instance); source.DefaultOptions.Should().Be(options); source.Configuration.Should().BeNull(); @@ -29,14 +29,6 @@ public void Constructors_InitializesProperties() source.Sources.Should().ContainSingle().Which.Should().Be(memSource); source.Properties.Should().ContainSingle(); source.Properties.Should().ContainKey("foo").WhoseValue.Should().Be("bar"); - - IConfigurationRoot configurationRoot = new ConfigurationBuilder().AddInMemoryCollection().Build(); - source = new ConfigServerConfigurationSource(options, configurationRoot, NullLoggerFactory.Instance); - source.DefaultOptions.Should().Be(options); - - ConfigurationRoot? root = source.Configuration.Should().BeOfType().Subject; - - root.Should().BeSameAs(configurationRoot); } [Fact] @@ -46,7 +38,7 @@ public void Build_ReturnsProvider() var memSource = new MemoryConfigurationSource(); List sources = [memSource]; - var source = new ConfigServerConfigurationSource(options, sources, null, NullLoggerFactory.Instance); + var source = new ConfigServerConfigurationSource(options, sources, null, null, null, NullLoggerFactory.Instance); IConfigurationProvider provider = source.Build(new ConfigurationBuilder()); provider.Should().BeOfType(); diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerDiscoveryServiceTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerDiscoveryServiceTest.cs index 55d238660e..647f657180 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerDiscoveryServiceTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerDiscoveryServiceTest.cs @@ -11,12 +11,12 @@ namespace Steeltoe.Configuration.ConfigServer.Test; public sealed class ConfigServerDiscoveryServiceTest { [Fact] - public void ConfigServerDiscoveryService_FindsNoDiscoveryClients() + public async Task ConfigServerDiscoveryService_FindsNoDiscoveryClients() { IConfiguration configuration = new ConfigurationBuilder().Add(FastTestConfigurations.ConfigServer).Build(); - var options = new ConfigServerClientOptions(); - var service = new ConfigServerDiscoveryService(configuration, options, NullLoggerFactory.Instance); + var service = new ConfigServerDiscoveryService(configuration, NullLoggerFactory.Instance); + await service.GetConfigServerInstancesAsync(new ConfigServerClientOptions(), TestContext.Current.CancellationToken); service.DiscoveryClients.Should().BeEmpty(); } diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerHealthContributorTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerHealthContributorTest.cs index f0a408d663..8d6f046141 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerHealthContributorTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerHealthContributorTest.cs @@ -27,7 +27,6 @@ public void Constructor_FindsConfigServerProviderInsidePlaceholderProvider() builder.AddInMemoryCollection(values); builder.AddConfigServer(); builder.AddPlaceholderResolver(); - IConfigurationRoot configurationRoot = builder.Build(); var contributor = new ConfigServerHealthContributor(configurationRoot, TimeProvider.System, NullLogger.Instance); @@ -48,57 +47,12 @@ public void FindProvider_FindsProvider() var builder = new ConfigurationBuilder(); builder.AddInMemoryCollection(values); builder.AddConfigServer(); - IConfigurationRoot configurationRoot = builder.Build(); var contributor = new ConfigServerHealthContributor(configurationRoot, TimeProvider.System, NullLogger.Instance); contributor.Provider.Should().NotBeNull(); } - [Fact] - public void GetTimeToLive_ReturnsExpected() - { - var values = new Dictionary(TestSettingsFactory.Get(FastTestConfigurations.ConfigServer)) - { - ["spring:cloud:config:uri"] = "http://localhost:8888/", - ["spring:cloud:config:name"] = "myName", - ["spring:cloud:config:label"] = "myLabel", - ["spring:cloud:config:health:timeToLive"] = "100000", - ["spring:cloud:config:timeout"] = "10" - }; - - var builder = new ConfigurationBuilder(); - builder.AddInMemoryCollection(values); - builder.AddConfigServer(); - - IConfigurationRoot configurationRoot = builder.Build(); - - var contributor = new ConfigServerHealthContributor(configurationRoot, TimeProvider.System, NullLogger.Instance); - contributor.GetTimeToLive().Should().Be(100_000); - } - - [Fact] - public void IsEnabled_ReturnsExpected() - { - var values = new Dictionary(TestSettingsFactory.Get(FastTestConfigurations.ConfigServer)) - { - ["spring:cloud:config:uri"] = "http://localhost:8888/", - ["spring:cloud:config:name"] = "myName", - ["spring:cloud:config:label"] = "myLabel", - ["spring:cloud:config:health:enabled"] = "true", - ["spring:cloud:config:timeout"] = "10" - }; - - var builder = new ConfigurationBuilder(); - builder.AddInMemoryCollection(values); - builder.AddConfigServer(); - - IConfigurationRoot configurationRoot = builder.Build(); - - var contributor = new ConfigServerHealthContributor(configurationRoot, TimeProvider.System, NullLogger.Instance); - contributor.IsEnabled().Should().BeTrue(); - } - [Fact] public void IsCacheStale_ReturnsExpected() { @@ -115,15 +69,16 @@ public void IsCacheStale_ReturnsExpected() var builder = new ConfigurationBuilder(); builder.AddInMemoryCollection(values); builder.AddConfigServer(); - IConfigurationRoot configurationRoot = builder.Build(); var contributor = new ConfigServerHealthContributor(configurationRoot, TimeProvider.System, NullLogger.Instance); - contributor.IsCacheStale(0).Should().BeTrue(); // No cache established yet + ConfigServerClientOptions optionsSnapshot = contributor.Provider!.ClientOptions; + + contributor.IsCacheStale(0, optionsSnapshot).Should().BeTrue(); // No cache established yet contributor.Cached = new ConfigEnvironment(); contributor.LastAccess = 9; - contributor.IsCacheStale(10).Should().BeTrue(); - contributor.IsCacheStale(8).Should().BeFalse(); + contributor.IsCacheStale(10, optionsSnapshot).Should().BeTrue(); + contributor.IsCacheStale(8, optionsSnapshot).Should().BeFalse(); } [Fact] @@ -143,7 +98,6 @@ public async Task GetPropertySources_ReturnsExpected() var builder = new ConfigurationBuilder(); builder.AddInMemoryCollection(values); builder.AddConfigServer(); - IConfigurationRoot configurationRoot = builder.Build(); var contributor = new ConfigServerHealthContributor(configurationRoot, TimeProvider.System, NullLogger.Instance) @@ -152,7 +106,9 @@ public async Task GetPropertySources_ReturnsExpected() }; long lastAccess = contributor.LastAccess = DateTimeOffset.Now.ToUnixTimeMilliseconds() - 100; - IList? sources = await contributor.GetPropertySourcesAsync(contributor.Provider!, TestContext.Current.CancellationToken); + + IList? sources = + await contributor.GetPropertySourcesAsync(contributor.Provider!, contributor.Provider!.ClientOptions, TestContext.Current.CancellationToken); contributor.LastAccess.Should().NotBe(lastAccess); sources.Should().BeNull(); diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerHostBuilderExtensionsTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerHostBuilderExtensionsTest.cs index 776dcc8631..ad06440291 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerHostBuilderExtensionsTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerHostBuilderExtensionsTest.cs @@ -102,8 +102,8 @@ public void AddConfigServer_HostBuilder_DisposesTimer() provider = configurationRoot.EnumerateProviders().Single(); } - FieldInfo refreshTimerField = provider.GetType().GetField("_refreshTimer", BindingFlags.NonPublic | BindingFlags.Instance)!; - refreshTimerField.GetValue(provider).Should().BeNull(); + FieldInfo reloadTimerField = provider.GetType().GetField("_configurationReloadTimer", BindingFlags.NonPublic | BindingFlags.Instance)!; + reloadTimerField.GetValue(provider).Should().BeNull(); } [Fact] @@ -114,20 +114,20 @@ public void AddConfigServer_WebHostBuilder_DisposesTimer() ["spring:cloud:config:pollingInterval"] = 1.Seconds().ToString() }; - WebHostBuilder builder = TestWebHostBuilderFactory.Create(); - builder.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.AddInMemoryCollection(appSettings)); - builder.AddConfigServer(); + WebHostBuilder hostBuilder = TestWebHostBuilderFactory.Create(); + hostBuilder.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.AddInMemoryCollection(appSettings)); + hostBuilder.AddConfigServer(); ConfigServerConfigurationProvider provider; - using (IWebHost webHost = builder.Build()) + using (IWebHost webHost = hostBuilder.Build()) { var configurationRoot = (IConfigurationRoot)webHost.Services.GetRequiredService(); provider = configurationRoot.EnumerateProviders().Single(); } - FieldInfo refreshTimerField = provider.GetType().GetField("_refreshTimer", BindingFlags.NonPublic | BindingFlags.Instance)!; - refreshTimerField.GetValue(provider).Should().BeNull(); + FieldInfo reloadTimerField = provider.GetType().GetField("_configurationReloadTimer", BindingFlags.NonPublic | BindingFlags.Instance)!; + reloadTimerField.GetValue(provider).Should().BeNull(); } [Fact] @@ -150,8 +150,8 @@ public async Task AddConfigServer_WebApplicationBuilder_DisposesTimer() _ = host.Services.GetRequiredService(); } - FieldInfo refreshTimerField = provider.GetType().GetField("_refreshTimer", BindingFlags.NonPublic | BindingFlags.Instance)!; - refreshTimerField.GetValue(provider).Should().BeNull(); + FieldInfo reloadTimerField = provider.GetType().GetField("_configurationReloadTimer", BindingFlags.NonPublic | BindingFlags.Instance)!; + reloadTimerField.GetValue(provider).Should().BeNull(); } [Fact] @@ -174,8 +174,8 @@ public void AddConfigServer_HostApplicationBuilder_DisposesTimer() _ = host.Services.GetRequiredService(); } - FieldInfo refreshTimerField = provider.GetType().GetField("_refreshTimer", BindingFlags.NonPublic | BindingFlags.Instance)!; - refreshTimerField.GetValue(provider).Should().BeNull(); + FieldInfo reloadTimerField = provider.GetType().GetField("_configurationReloadTimer", BindingFlags.NonPublic | BindingFlags.Instance)!; + reloadTimerField.GetValue(provider).Should().BeNull(); } [Fact] diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerHostedServiceTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerHostedServiceTest.cs index ac140148b3..11de4911de 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerHostedServiceTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerHostedServiceTest.cs @@ -18,7 +18,9 @@ public async Task ServiceConstructsAndOperatesWithConfigurationRoot() var provider = new ConfigServerConfigurationProvider(new ConfigServerClientOptions { Enabled = false - }, null, null, NullLoggerFactory.Instance); + }, null, null, null, NullLoggerFactory.Instance); + + provider.Load(); var configurationRoot = new ConfigurationRoot([provider]); var service = new ConfigServerHostedService(configurationRoot, []); diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerServiceCollectionExtensionsTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerServiceCollectionExtensionsTest.cs index 05fe12d3a4..9bf40b414a 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerServiceCollectionExtensionsTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerServiceCollectionExtensionsTest.cs @@ -2,10 +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. -using System.Reflection; using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; using Steeltoe.Configuration.Encryption; using Steeltoe.Configuration.Placeholder; @@ -13,24 +10,6 @@ namespace Steeltoe.Configuration.ConfigServer.Test; public sealed class ConfigServerServiceCollectionExtensionsTest { - [Fact] - public async Task ConfigureConfigServerClientOptions_ConfiguresConfigServerClientOptions_WithDefaults() - { - var builder = new ConfigurationBuilder(); - builder.AddConfigServer(); - IConfiguration configuration = builder.Build(); - - var services = new ServiceCollection(); - services.AddSingleton(configuration); - services.ConfigureConfigServerClientOptions(); - - await using ServiceProvider serviceProvider = services.BuildServiceProvider(true); - var optionsMonitor = serviceProvider.GetRequiredService>(); - - string? expectedAppName = Assembly.GetEntryAssembly()!.GetName().Name; - TestHelper.VerifyDefaults(optionsMonitor.CurrentValue, expectedAppName); - } - [Fact] public void DoesNotAddConfigServerMultipleTimes() { diff --git a/src/Configuration/test/ConfigServer.Test/ForwardingHttpClientHandler.cs b/src/Configuration/test/ConfigServer.Test/ForwardingHttpClientHandler.cs deleted file mode 100644 index 27100c24a7..0000000000 --- a/src/Configuration/test/ConfigServer.Test/ForwardingHttpClientHandler.cs +++ /dev/null @@ -1,22 +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. - -namespace Steeltoe.Configuration.ConfigServer.Test; - -internal sealed class ForwardingHttpClientHandler : HttpClientHandler -{ - private readonly HttpMessageInvoker _invoker; - - public ForwardingHttpClientHandler(HttpMessageHandler innerHandler) - { - ArgumentNullException.ThrowIfNull(innerHandler); - - _invoker = new HttpMessageInvoker(innerHandler, false); - } - - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - return _invoker.SendAsync(request, cancellationToken); - } -} diff --git a/src/Configuration/test/ConfigServer.Test/TestConfigServerConfigurationSource.cs b/src/Configuration/test/ConfigServer.Test/TestConfigServerConfigurationSource.cs deleted file mode 100644 index 23c99470bf..0000000000 --- a/src/Configuration/test/ConfigServer.Test/TestConfigServerConfigurationSource.cs +++ /dev/null @@ -1,17 +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.ConfigServer.Test; - -internal sealed class TestConfigServerConfigurationSource(IConfigurationProvider provider) : IConfigurationSource -{ - private readonly IConfigurationProvider _provider = provider; - - public IConfigurationProvider Build(IConfigurationBuilder builder) - { - return _provider; - } -} diff --git a/src/Configuration/test/ConfigServer.Test/TestConfigServerStartup.cs b/src/Configuration/test/ConfigServer.Test/TestConfigServerStartup.cs deleted file mode 100644 index e3947d75d1..0000000000 --- a/src/Configuration/test/ConfigServer.Test/TestConfigServerStartup.cs +++ /dev/null @@ -1,129 +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.Http; - -namespace Steeltoe.Configuration.ConfigServer.Test; - -internal sealed class TestConfigServerStartup : IDisposable -{ - private volatile CountdownEvent _firstRequestCountdownEvent = new(1); - private volatile string? _response; - private volatile int[] _returnStatus = [200]; - private volatile HttpRequestInfo? _lastRequest; - private volatile int _requestCount; - private volatile string _label = string.Empty; - private volatile string _appName = string.Empty; - private volatile string _env = string.Empty; - - public string? Response - { - get => _response; - set => _response = value; - } - - public int[] ReturnStatus - { - get => _returnStatus; - set => _returnStatus = value; - } - - public HttpRequestInfo? LastRequest - { - get => _lastRequest; - set => _lastRequest = value; - } - - public int RequestCount - { - get => _requestCount; - set => _requestCount = value; - } - - public string Label - { - get => _label; - set => _label = value; - } - - public string AppName - { - get => _appName; - set => _appName = value; - } - - public string Env - { - get => _env; - set => _env = value; - } - - public void Reset() - { - Response = null; - ReturnStatus = [200]; - LastRequest = null; - RequestCount = 0; - Label = AppName = Env = string.Empty; - - _firstRequestCountdownEvent.Dispose(); - _firstRequestCountdownEvent = new CountdownEvent(1); - } - - public void Configure(IApplicationBuilder app) - { - app.Run(async context => - { - LastRequest = await HttpRequestInfo.CopyFromAsync(context); - context.Response.StatusCode = GetStatusCode(context.Request.Path); - RequestCount++; - - if (context.Response.StatusCode == 200) - { - context.Response.Headers.Append("content-type", "application/json"); - - if (Response != null) - { - await context.Response.WriteAsync(Response, context.RequestAborted); - } - } - - if (RequestCount == 1) - { - _firstRequestCountdownEvent.Signal(); - } - }); - } - - private int GetStatusCode(string path) - { - if (!string.IsNullOrEmpty(Label) && !path.Contains(Label, StringComparison.Ordinal)) - { - return 404; - } - - if (!string.IsNullOrEmpty(Env) && !path.Contains(Env, StringComparison.Ordinal)) - { - return 404; - } - - if (!string.IsNullOrEmpty(AppName) && !path.Contains(AppName, StringComparison.Ordinal)) - { - return 404; - } - - return ReturnStatus[RequestCount]; - } - - public bool WaitForFirstRequest(TimeSpan timeout) - { - return _firstRequestCountdownEvent.Wait(timeout, TestContext.Current.CancellationToken); - } - - public void Dispose() - { - _firstRequestCountdownEvent.Dispose(); - } -} diff --git a/src/Configuration/test/ConfigServer.Test/TestHelper.cs b/src/Configuration/test/ConfigServer.Test/TestHelper.cs index 57db7c8e6f..6fd0ff93ac 100644 --- a/src/Configuration/test/ConfigServer.Test/TestHelper.cs +++ b/src/Configuration/test/ConfigServer.Test/TestHelper.cs @@ -6,12 +6,12 @@ namespace Steeltoe.Configuration.ConfigServer.Test; internal static class TestHelper { - public static void VerifyDefaults(ConfigServerClientOptions options, string? expectedAppName) + public static void VerifyDefaults(ConfigServerClientOptions options, string? expectedAppName, string? expectedEnvironment) { options.Enabled.Should().BeTrue(); options.FailFast.Should().BeFalse(); options.Uri.Should().Be("http://localhost:8888"); - options.Environment.Should().Be("Production"); + options.Environment.Should().Be(expectedEnvironment); options.AccessTokenUri.Should().BeNull(); options.ClientId.Should().BeNull(); options.ClientSecret.Should().BeNull(); diff --git a/src/Configuration/test/ConfigServer.Test/TestServiceInstance.cs b/src/Configuration/test/ConfigServer.Test/TestServiceInstance.cs new file mode 100644 index 0000000000..91167ccba5 --- /dev/null +++ b/src/Configuration/test/ConfigServer.Test/TestServiceInstance.cs @@ -0,0 +1,20 @@ +// 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.Common.Discovery; + +namespace Steeltoe.Configuration.ConfigServer.Test; + +internal sealed class TestServiceInstance(string serviceId, string instanceId, Uri uri, IReadOnlyDictionary metadata) : IServiceInstance +{ + public string ServiceId { get; } = serviceId; + public string InstanceId { get; } = instanceId; + public string Host { get; } = uri.Host; + public int Port { get; } = uri.Port; + public bool IsSecure { get; } = uri.Scheme == Uri.UriSchemeHttps; + public Uri Uri { get; } = uri; + public Uri? NonSecureUri => IsSecure ? null : Uri; + public Uri? SecureUri => IsSecure ? Uri : null; + public IReadOnlyDictionary Metadata { get; } = metadata; +} diff --git a/src/Configuration/test/Placeholder.Test/PlaceholderConfigurationTest.cs b/src/Configuration/test/Placeholder.Test/PlaceholderConfigurationTest.cs index 7869c46492..f9f74b68a2 100644 --- a/src/Configuration/test/Placeholder.Test/PlaceholderConfigurationTest.cs +++ b/src/Configuration/test/Placeholder.Test/PlaceholderConfigurationTest.cs @@ -336,16 +336,16 @@ public void Reloads_options_on_change(int placeholderCount) { MemoryFileProvider fileProvider = new(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "TestRoot": { - "Value": "valueA" - } - } - """); + fileProvider.IncludeAppSettingsJsonFile(""" + { + "TestRoot": { + "Value": "valueA" + } + } + """); var builder = new ConfigurationBuilder(); - builder.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.AddInMemoryAppSettingsJsonFile(fileProvider); #pragma warning disable SA1312 // Variable names should begin with lower-case letter foreach (int _ in Enumerable.Repeat(0, placeholderCount)) @@ -375,13 +375,13 @@ public void Reloads_options_on_change(int placeholderCount) _ = optionsMonitor.CurrentValue; configurer.ConfigureCount.Should().Be(1); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "TestRoot": { - "Value": "valueB" - } - } - """); + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "TestRoot": { + "Value": "valueB" + } + } + """); fileProvider.NotifyChanged(); diff --git a/src/Configuration/test/Placeholder.Test/PlaceholderWebApplicationTest.cs b/src/Configuration/test/Placeholder.Test/PlaceholderWebApplicationTest.cs index 544688e850..3dcb81fb43 100644 --- a/src/Configuration/test/Placeholder.Test/PlaceholderWebApplicationTest.cs +++ b/src/Configuration/test/Placeholder.Test/PlaceholderWebApplicationTest.cs @@ -2,14 +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. -using FluentAssertions.Extensions; 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; namespace Steeltoe.Configuration.Placeholder.Test; @@ -26,16 +24,15 @@ public PlaceholderWebApplicationTest(ITestOutputHelper testOutputHelper) [Fact] public async Task Reloads_options_on_change() { - const string appSettings = """ + var fileProvider = new MemoryFileProvider(); + + fileProvider.IncludeAppSettingsJsonFile(""" { "TestRoot": { "AppName": "AppOne" } } - """; - - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings); + """); var memorySettings = new Dictionary { @@ -44,9 +41,8 @@ public async Task Reloads_options_on_change() WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Services.AddSingleton(_loggerFactory); - builder.Configuration.SetBasePath(sandbox.FullPath); builder.Configuration.AddInMemoryCollection(memorySettings); - builder.Configuration.AddJsonFile(MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Configuration.AddPlaceholderResolver(_loggerFactory); builder.Services.Configure(builder.Configuration.GetSection("TestRoot")); builder.Services.AddSingleton, ConfigureTestOptions>(); @@ -55,27 +51,27 @@ public async Task Reloads_options_on_change() var optionsMonitor = app.Services.GetRequiredService>(); optionsMonitor.CurrentValue.Value.Should().Be("AppOne"); - await File.WriteAllTextAsync(path, """ - { - "TestRoot": { - "AppName": "AppTwo" - } - } - """, TestContext.Current.CancellationToken); + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "TestRoot": { + "AppName": "AppTwo" + } + } + """); - await Task.Delay(2.Seconds(), TestContext.Current.CancellationToken); + fileProvider.NotifyChanged(); optionsMonitor.CurrentValue.Value.Should().Be("AppTwo"); - await File.WriteAllTextAsync(path, """ - { - "TestRoot": { - "AppName": "AppThree" - } - } - """, TestContext.Current.CancellationToken); + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "TestRoot": { + "AppName": "AppThree" + } + } + """); - await Task.Delay(2.Seconds(), TestContext.Current.CancellationToken); + fileProvider.NotifyChanged(); optionsMonitor.CurrentValue.Value.Should().Be("AppThree"); } @@ -121,11 +117,9 @@ public void Can_rebuild_sources() [Fact] public async Task Can_substitute_across_multiple_sources() { - const string appSettingsJsonFileName = "appsettings.json"; - const string appSettingsXmlFileName = "appsettings.xml"; - const string appSettingsIniFileName = "appsettings.ini"; + var fileProvider = new MemoryFileProvider(); - const string appSettingsJsonContent = """ + fileProvider.IncludeAppSettingsJsonFile(""" { "JsonTestRoot": { "JsonSubLevel": { @@ -134,9 +128,9 @@ public async Task Can_substitute_across_multiple_sources() } } } - """; + """); - const string appSettingsXmlContent = """ + fileProvider.IncludeAppSettingsXmlFile(""" @@ -145,13 +139,13 @@ public async Task Can_substitute_across_multiple_sources() - """; + """); - const string appSettingsIniContent = """ + fileProvider.IncludeAppSettingsIniFile(""" [IniTestRoot:IniSubLevel] IniKey=IniValue CmdSource=IniTo${CmdTestRoot:CmdSubLevel:CmdKey} - """; + """); string[] appSettingsCommandLine = [ @@ -159,16 +153,10 @@ public async Task Can_substitute_across_multiple_sources() "--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.AddInMemoryAppSettingsJsonFile(fileProvider); + builder.Configuration.AddInMemoryAppSettingsXmlFile(fileProvider); + builder.Configuration.AddInMemoryAppSettingsIniFile(fileProvider); builder.Configuration.AddCommandLine(appSettingsCommandLine); builder.Configuration.AddPlaceholderResolver(_loggerFactory); diff --git a/src/Connectors/test/Connectors.Test/ConfigurationChangeDetectionTest.cs b/src/Connectors/test/Connectors.Test/ConfigurationChangeDetectionTest.cs index 9d9aca9d3e..c16d6df3bb 100644 --- a/src/Connectors/test/Connectors.Test/ConfigurationChangeDetectionTest.cs +++ b/src/Connectors/test/Connectors.Test/ConfigurationChangeDetectionTest.cs @@ -4,7 +4,6 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Npgsql; @@ -35,9 +34,9 @@ public async Task Applies_local_configuration_changes_using_WebApplicationBuilde """; var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, fileContents); + fileProvider.IncludeAppSettingsJsonFile(fileContents); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.AddPostgreSql(configureOptions => configureOptions.DetectConfigurationChanges = true, null); await using WebApplication app = builder.Build(); @@ -63,7 +62,7 @@ public async Task Applies_local_configuration_changes_using_WebApplicationBuilde } """; - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, fileContents); + fileProvider.ReplaceAppSettingsJsonFile(fileContents); fileProvider.NotifyChanged(); connectionString = connectorFactory.Get("examplePostgreSqlService").Options.ConnectionString; @@ -83,7 +82,7 @@ public async Task Applies_local_configuration_changes_using_WebApplicationBuilde } """; - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, fileContents); + fileProvider.ReplaceAppSettingsJsonFile(fileContents); fileProvider.NotifyChanged(); connectionString = connectorFactory.Get("examplePostgreSqlService").Options.ConnectionString; @@ -110,11 +109,11 @@ public void Applies_local_configuration_changes_using_WebHostBuilder() """; var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, fileContents); + fileProvider.IncludeAppSettingsJsonFile(fileContents); builder.ConfigureAppConfiguration(configurationBuilder => { - configurationBuilder.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); configurationBuilder.ConfigurePostgreSql(options => options.DetectConfigurationChanges = true); }); @@ -142,7 +141,7 @@ public void Applies_local_configuration_changes_using_WebHostBuilder() } """; - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, fileContents); + fileProvider.ReplaceAppSettingsJsonFile(fileContents); fileProvider.NotifyChanged(); connectionString = connectorFactory.Get("examplePostgreSqlService").Options.ConnectionString; @@ -162,7 +161,7 @@ public void Applies_local_configuration_changes_using_WebHostBuilder() } """; - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, fileContents); + fileProvider.ReplaceAppSettingsJsonFile(fileContents); fileProvider.NotifyChanged(); connectionString = connectorFactory.Get("examplePostgreSqlService").Options.ConnectionString; @@ -189,11 +188,11 @@ public void Applies_local_configuration_changes_using_HostBuilder() """; var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, fileContents); + fileProvider.IncludeAppSettingsJsonFile(fileContents); builder.ConfigureAppConfiguration(configurationBuilder => { - configurationBuilder.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); configurationBuilder.ConfigurePostgreSql(options => options.DetectConfigurationChanges = true); }); @@ -221,7 +220,7 @@ public void Applies_local_configuration_changes_using_HostBuilder() } """; - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, fileContents); + fileProvider.ReplaceAppSettingsJsonFile(fileContents); fileProvider.NotifyChanged(); connectionString = connectorFactory.Get("examplePostgreSqlService").Options.ConnectionString; @@ -241,7 +240,7 @@ public void Applies_local_configuration_changes_using_HostBuilder() } """; - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, fileContents); + fileProvider.ReplaceAppSettingsJsonFile(fileContents); fileProvider.NotifyChanged(); connectionString = connectorFactory.Get("examplePostgreSqlService").Options.ConnectionString; diff --git a/src/Discovery/src/Consul/Registry/ConsulServiceRegistrar.cs b/src/Discovery/src/Consul/Registry/ConsulServiceRegistrar.cs index ef0d9d392e..17b8c93391 100644 --- a/src/Discovery/src/Consul/Registry/ConsulServiceRegistrar.cs +++ b/src/Discovery/src/Consul/Registry/ConsulServiceRegistrar.cs @@ -136,7 +136,7 @@ private async Task DoWithRetryAsync(Func retryable, Con if (attempts < options.MaxAttempts) { LogStartingRetry(exception, attempts); - Thread.CurrentThread.Join(backOff); + await Task.Delay(backOff, cancellationToken); int nextBackOff = (int)(backOff * options.Multiplier); backOff = Math.Min(nextBackOff, options.MaxInterval); } diff --git a/src/Discovery/src/Eureka/EurekaApplicationInfoManager.cs b/src/Discovery/src/Eureka/EurekaApplicationInfoManager.cs index 3bf7b05b3c..dfb1702476 100644 --- a/src/Discovery/src/Eureka/EurekaApplicationInfoManager.cs +++ b/src/Discovery/src/Eureka/EurekaApplicationInfoManager.cs @@ -31,7 +31,7 @@ public sealed partial class EurekaApplicationInfoManager : IDisposable // Readers must never be blocked, as it may delay the periodic heartbeat. // Updates from user code must be synchronized with configuration changes. // After update, the readonly snapshot is replaced. Volatile prevents reading stale data. - // Once metadata has been set from user code, it overrules what's in configuration. + // Once metadata has been set from user code, it overrides what's in configuration. private volatile InstanceInfo _instance; private IReadOnlyDictionary? _explicitMetadata; @@ -125,7 +125,7 @@ private void InnerUpdateInstance(EurekaInstanceOptions newInstanceOptions, bool newInstance = previousInstance; } - // Status in configuration is the initial startup status. New or previous instance status always overrules it. + // Status in configuration is the initial startup status. New or previous instance status always overrides it. newInstance.ReplaceStatus(newStatus ?? previousInstance.Status); if (newOverriddenStatus != null) diff --git a/src/Discovery/test/Configuration.Test/ConfigurationDiscoveryClientTest.cs b/src/Discovery/test/Configuration.Test/ConfigurationDiscoveryClientTest.cs index 90f12d9797..dcbb000c47 100644 --- a/src/Discovery/test/Configuration.Test/ConfigurationDiscoveryClientTest.cs +++ b/src/Discovery/test/Configuration.Test/ConfigurationDiscoveryClientTest.cs @@ -7,7 +7,6 @@ using Microsoft.Extensions.Hosting; using Steeltoe.Common.Discovery; using Steeltoe.Common.TestResources; -using Steeltoe.Common.TestResources.IO; namespace Steeltoe.Discovery.Configuration.Test; @@ -119,13 +118,11 @@ public async Task AddConfigurationDiscoveryClient_AddsClientWithOptions() } """; - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings); - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsJsonFile(appSettings); + var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.SetBasePath(directory); - configurationBuilder.AddJsonFile(fileName); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); IConfiguration configuration = configurationBuilder.Build(); var services = new ServiceCollection(); diff --git a/src/Discovery/test/Eureka.Test/EurekaClientOptionsTest.cs b/src/Discovery/test/Eureka.Test/EurekaClientOptionsTest.cs index 8194b53020..dabd6d4039 100644 --- a/src/Discovery/test/Eureka.Test/EurekaClientOptionsTest.cs +++ b/src/Discovery/test/Eureka.Test/EurekaClientOptionsTest.cs @@ -6,7 +6,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Steeltoe.Common.TestResources; -using Steeltoe.Common.TestResources.IO; using Steeltoe.Discovery.Eureka.Configuration; namespace Steeltoe.Discovery.Eureka.Test; @@ -98,14 +97,11 @@ public void Constructor_ConfiguresEurekaDiscovery_Correctly() } """; - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings); - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.SetBasePath(directory); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsJsonFile(appSettings); - configurationBuilder.AddJsonFile(fileName); + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); IConfiguration configuration = configurationBuilder.Build(); IConfigurationSection clientSection = configuration.GetSection(EurekaClientOptions.ConfigurationPrefix); diff --git a/src/Discovery/test/Eureka.Test/EurekaInstanceOptionsTest.cs b/src/Discovery/test/Eureka.Test/EurekaInstanceOptionsTest.cs index 180fe7fbf9..445262fbb4 100644 --- a/src/Discovery/test/Eureka.Test/EurekaInstanceOptionsTest.cs +++ b/src/Discovery/test/Eureka.Test/EurekaInstanceOptionsTest.cs @@ -4,7 +4,6 @@ using Microsoft.Extensions.Configuration; using Steeltoe.Common.TestResources; -using Steeltoe.Common.TestResources.IO; using Steeltoe.Discovery.Eureka.AppInfo; using Steeltoe.Discovery.Eureka.Configuration; @@ -104,14 +103,11 @@ public void Constructor_ConfiguresEurekaDiscovery_Correctly() } """; - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings); - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.SetBasePath(directory); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsJsonFile(appSettings); - configurationBuilder.AddJsonFile(fileName); + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); IConfiguration configuration = configurationBuilder.Build(); IConfigurationSection instanceSection = configuration.GetSection(EurekaInstanceOptions.ConfigurationPrefix); diff --git a/src/Discovery/test/HttpClients.Test/RegisterMultipleDiscoveryClientsTest.cs b/src/Discovery/test/HttpClients.Test/RegisterMultipleDiscoveryClientsTest.cs index 07ef73750c..a4a3a846f1 100644 --- a/src/Discovery/test/HttpClients.Test/RegisterMultipleDiscoveryClientsTest.cs +++ b/src/Discovery/test/HttpClients.Test/RegisterMultipleDiscoveryClientsTest.cs @@ -15,7 +15,6 @@ using Steeltoe.Common.HealthChecks; using Steeltoe.Common.Http.HttpClientPooling; using Steeltoe.Common.TestResources; -using Steeltoe.Common.TestResources.IO; using Steeltoe.Configuration.CloudFoundry; using Steeltoe.Configuration.CloudFoundry.ServiceBindings; using Steeltoe.Configuration.CloudFoundry.ServiceBindings.PostProcessors; @@ -57,14 +56,11 @@ public async Task WithEurekaConfiguration_AddsDiscoveryClient() } """; - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings); - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsJsonFile(appSettings); var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.SetBasePath(directory); - configurationBuilder.AddJsonFile(fileName); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); IConfiguration configuration = configurationBuilder.Build(); IServiceCollection services = new ServiceCollection(); @@ -656,14 +652,11 @@ public async Task WithConsulConfiguration_AddsDiscoveryClient() } """; - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings); - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsJsonFile(appSettings); var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.SetBasePath(directory); - configurationBuilder.AddJsonFile(fileName); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); IConfiguration configuration = configurationBuilder.Build(); var services = new ServiceCollection(); @@ -829,10 +822,12 @@ public async Task WithAppConfiguration_AddsAndWorks() } """; - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsJsonFile(appSettings); - IConfiguration configuration = new ConfigurationBuilder().SetBasePath(Path.GetDirectoryName(path)!).AddJsonFile(Path.GetFileName(path)).Build(); + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); + IConfiguration configuration = configurationBuilder.Build(); IServiceCollection services = new ServiceCollection(); services.AddOptions(); diff --git a/src/Logging/test/DynamicConsole.Test/DynamicConsoleLoggerProviderTest.cs b/src/Logging/test/DynamicConsole.Test/DynamicConsoleLoggerProviderTest.cs index 3e85796499..dce86c3f4e 100644 --- a/src/Logging/test/DynamicConsole.Test/DynamicConsoleLoggerProviderTest.cs +++ b/src/Logging/test/DynamicConsole.Test/DynamicConsoleLoggerProviderTest.cs @@ -400,19 +400,17 @@ public async Task AppliesChangedConfiguration() { MemoryFileProvider fileProvider = new(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Logging": { - "LogLevel": { - "A": "Warning" + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Logging": { + "LogLevel": { + "A": "Warning" + } + } } - } - } - """); - - using IDynamicLoggerProvider provider = CreateLoggerProvider(configurationBuilder => - configurationBuilder.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true)); + """); + using IDynamicLoggerProvider provider = CreateLoggerProvider(configurationBuilder => configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider)); DynamicLoggingTestContext testContext = new(provider, _consoleOutput); await testContext.Parent.AssertMinLevelAsync(LogLevel.Warning); @@ -426,16 +424,16 @@ public async Task AppliesChangedConfiguration() await testContext.Self.AssertMinLevelAsync(LogLevel.Error, LogLevel.Warning); await testContext.Child.AssertMinLevelAsync(LogLevel.Error); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Logging": { - "LogLevel": { - "A": "Trace", - "A.B.C": "Debug" + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Logging": { + "LogLevel": { + "A": "Trace", + "A.B.C": "Debug" + } + } } - } - } - """); + """); fileProvider.NotifyChanged(); testContext.Refresh(); diff --git a/src/Logging/test/DynamicSerilog.Test/DynamicSerilogLoggerProviderTest.cs b/src/Logging/test/DynamicSerilog.Test/DynamicSerilogLoggerProviderTest.cs index 682a90fdc4..41545a488f 100644 --- a/src/Logging/test/DynamicSerilog.Test/DynamicSerilogLoggerProviderTest.cs +++ b/src/Logging/test/DynamicSerilog.Test/DynamicSerilogLoggerProviderTest.cs @@ -198,24 +198,22 @@ public void AppliesChangedConfiguration() { MemoryFileProvider fileProvider = new(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Serilog": { - "MinimumLevel": { - "Override": { - "A": "Warning" + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Serilog": { + "MinimumLevel": { + "Override": { + "A": "Warning" + } + }, + "WriteTo": { + "Name": "Console" + } } - }, - "WriteTo": { - "Name": "Console" } - } - } - """); - - using IDynamicLoggerProvider provider = CreateLoggerProvider(configurationBuilder => - configurationBuilder.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true)); + """); + using IDynamicLoggerProvider provider = CreateLoggerProvider(configurationBuilder => configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider)); DynamicLoggingTestContext testContext = new(provider, _consoleOutput); testContext.Parent.AssertMinLevel(LogLevel.Warning); @@ -229,19 +227,19 @@ public void AppliesChangedConfiguration() testContext.Self.AssertMinLevel(LogLevel.Error, LogLevel.Warning); testContext.Child.AssertMinLevel(LogLevel.Error); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Serilog": { - "MinimumLevel": { - "Override": { - "A": "Verbose", - "A.B.C": "Debug" + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Serilog": { + "MinimumLevel": { + "Override": { + "A": "Verbose", + "A.B.C": "Debug" + } + }, + "WriteTo": "Console" } - }, - "WriteTo": "Console" - } - } - """); + } + """); fileProvider.NotifyChanged(); testContext.Refresh(); diff --git a/src/Management/src/Endpoint/Configuration/ManagementOptions.cs b/src/Management/src/Endpoint/Configuration/ManagementOptions.cs index cb9bcc5f86..0314b64669 100644 --- a/src/Management/src/Endpoint/Configuration/ManagementOptions.cs +++ b/src/Management/src/Endpoint/Configuration/ManagementOptions.cs @@ -55,7 +55,7 @@ public sealed class ManagementOptions public bool SslEnabled { get; set; } /// - /// Gets or sets a value indicating whether the HTTP response status code is based on the health status. This setting can be overruled by sending an + /// Gets or sets a value indicating whether the HTTP response status code is based on the health status. This setting can be overridden by sending an /// X-Use-Status-Code-From-Response HTTP header. Default value: true. /// public bool UseStatusCodeFromResponse { get; set; } = true; diff --git a/src/Management/src/Endpoint/ConfigurationSchema.json b/src/Management/src/Endpoint/ConfigurationSchema.json index 883f3049cd..6d5e8eacf8 100644 --- a/src/Management/src/Endpoint/ConfigurationSchema.json +++ b/src/Management/src/Endpoint/ConfigurationSchema.json @@ -810,7 +810,7 @@ }, "UseStatusCodeFromResponse": { "type": "boolean", - "description": "Gets or sets a value indicating whether the HTTP response status code is based on the health status. This setting can be overruled by sending an X-Use-Status-Code-From-Response HTTP header. Default value: true." + "description": "Gets or sets a value indicating whether the HTTP response status code is based on the health status. This setting can be overridden by sending an X-Use-Status-Code-From-Response HTTP header. Default value: true." }, "Web": { "type": "object", diff --git a/src/Management/test/Endpoint.Test/Actuators/CloudFoundry/CloudFoundryActuatorTest.cs b/src/Management/test/Endpoint.Test/Actuators/CloudFoundry/CloudFoundryActuatorTest.cs index 14f42128c6..6a4e565aea 100644 --- a/src/Management/test/Endpoint.Test/Actuators/CloudFoundry/CloudFoundryActuatorTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/CloudFoundry/CloudFoundryActuatorTest.cs @@ -522,21 +522,21 @@ public async Task Can_change_configuration_at_runtime() var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "Info": { - "Enabled": false + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "Info": { + "Enabled": false + } + } } } - } - } - """); + """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddCloudFoundry(); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddCloudFoundryActuator(); builder.Services.AddInfoActuator(); builder.Services.AddHealthActuator(); @@ -568,17 +568,17 @@ public async Task Can_change_configuration_at_runtime() } """); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "Health": { - "Enabled": false + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "Health": { + "Enabled": false + } + } } } - } - } - """); + """); fileProvider.NotifyChanged(); diff --git a/src/Management/test/Endpoint.Test/Actuators/Environment/EnvironmentActuatorTest.cs b/src/Management/test/Endpoint.Test/Actuators/Environment/EnvironmentActuatorTest.cs index 13fe635f8b..897202ec36 100644 --- a/src/Management/test/Endpoint.Test/Actuators/Environment/EnvironmentActuatorTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/Environment/EnvironmentActuatorTest.cs @@ -323,34 +323,34 @@ public async Task Can_change_configuration_at_runtime() { var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "Actuator": { - "Exposure": { - "Include": [ - "env" - ] + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "Actuator": { + "Exposure": { + "Include": [ + "env" + ] + } + }, + "Env": { + "KeysToSanitize": [ + "Password" + ] + } } }, - "Env": { - "KeysToSanitize": [ - "Password" - ] + "TestSettings": { + "Password": "secret-password", + "AccessToken": "secret-token" } } - }, - "TestSettings": { - "Password": "secret-password", - "AccessToken": "secret-token" - } - } - """); + """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.Sources.Clear(); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddEnvironmentActuator(); await using WebApplication host = builder.Build(); @@ -390,30 +390,30 @@ public async Task Can_change_configuration_at_runtime() } """); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "Actuator": { - "Exposure": { - "Include": [ - "env" - ] + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "Actuator": { + "Exposure": { + "Include": [ + "env" + ] + } + }, + "Env": { + "KeysToSanitize": [ + "AccessToken" + ] + } } }, - "Env": { - "KeysToSanitize": [ - "AccessToken" - ] + "TestSettings": { + "Password": "secret-password", + "AccessToken": "secret-token" } } - }, - "TestSettings": { - "Password": "secret-password", - "AccessToken": "secret-token" - } - } - """); + """); fileProvider.NotifyChanged(); diff --git a/src/Management/test/Endpoint.Test/Actuators/Health/HealthActuatorTest.cs b/src/Management/test/Endpoint.Test/Actuators/Health/HealthActuatorTest.cs index 2310840bea..4a5e408466 100644 --- a/src/Management/test/Endpoint.Test/Actuators/Health/HealthActuatorTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/Health/HealthActuatorTest.cs @@ -578,26 +578,26 @@ public async Task Can_change_configuration_at_runtime() { var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "Health": { - "Groups": { - "ping-group": { - "include": "ping", - "ShowComponents": "Always" + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "Health": { + "Groups": { + "ping-group": { + "include": "ping", + "ShowComponents": "Always" + } + } } } } } - } - } - """); + """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddInMemoryCollection(AppSettings); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddHealthActuator(); WebApplication host = builder.Build(); @@ -622,21 +622,21 @@ public async Task Can_change_configuration_at_runtime() } """); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "Health": { - "Groups": { - "ping-group": { - "include": "ping" + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "Health": { + "Groups": { + "ping-group": { + "include": "ping" + } + } } } } } - } - } - """); + """); fileProvider.NotifyChanged(); diff --git a/src/Management/test/Endpoint.Test/Actuators/HttpExchanges/HttpExchangesActuatorTest.cs b/src/Management/test/Endpoint.Test/Actuators/HttpExchanges/HttpExchangesActuatorTest.cs index 25f49eb277..476a670815 100644 --- a/src/Management/test/Endpoint.Test/Actuators/HttpExchanges/HttpExchangesActuatorTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/HttpExchanges/HttpExchangesActuatorTest.cs @@ -422,17 +422,17 @@ public async Task Can_change_configuration_at_runtime() { var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "HttpExchanges": { - "IncludeQueryString": false + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "HttpExchanges": { + "IncludeQueryString": false + } + } } } - } - } - """); + """); DateTime currentTime = 19.September(2024); @@ -446,7 +446,7 @@ public async Task Can_change_configuration_at_runtime() WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddInMemoryCollection(AppSettings); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddSingleton(new FakeHttpExchangeRecorder(httpExchanges)); builder.Services.AddHttpExchangesActuator(); await using WebApplication host = builder.Build(); @@ -487,17 +487,17 @@ public async Task Can_change_configuration_at_runtime() } """); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "HttpExchanges": { - "Reverse": false + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "HttpExchanges": { + "Reverse": false + } + } } } - } - } - """); + """); fileProvider.NotifyChanged(); diff --git a/src/Management/test/Endpoint.Test/Actuators/Hypermedia/HypermediaActuatorTest.cs b/src/Management/test/Endpoint.Test/Actuators/Hypermedia/HypermediaActuatorTest.cs index abba10aa7b..7325d9b4b1 100644 --- a/src/Management/test/Endpoint.Test/Actuators/Hypermedia/HypermediaActuatorTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/Hypermedia/HypermediaActuatorTest.cs @@ -452,20 +452,20 @@ public async Task Can_change_configuration_at_runtime() { var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "Info": { - "Enabled": false + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "Info": { + "Enabled": false + } + } } } - } - } - """); + """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddHypermediaActuator(); builder.Services.AddInfoActuator(); builder.Services.AddHealthActuator(); @@ -496,17 +496,17 @@ public async Task Can_change_configuration_at_runtime() } """); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "Health": { - "Enabled": false + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "Health": { + "Enabled": false + } + } } } - } - } - """); + """); fileProvider.NotifyChanged(); diff --git a/src/Management/test/Endpoint.Test/Actuators/Loggers/LoggersActuatorSerilogTest.cs b/src/Management/test/Endpoint.Test/Actuators/Loggers/LoggersActuatorSerilogTest.cs index 70e96e837a..d7d800c214 100644 --- a/src/Management/test/Endpoint.Test/Actuators/Loggers/LoggersActuatorSerilogTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/Loggers/LoggersActuatorSerilogTest.cs @@ -272,23 +272,23 @@ public async Task Can_change_serilog_configuration_at_runtime() { var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Serilog": { - "MinimumLevel": { - "Default": "Error", - "Override": { - "Fake": "Warning", - "Fake.Category.AtDebugLevel": "Debug" + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Serilog": { + "MinimumLevel": { + "Default": "Error", + "Override": { + "Fake": "Warning", + "Fake.Category.AtDebugLevel": "Debug" + } + } } } - } - } - """); + """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddInMemoryCollection(AppSettings); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddSingleton(); builder.Logging.AddDynamicSerilog(); builder.Services.AddLoggersActuator(); @@ -342,19 +342,19 @@ public async Task Can_change_serilog_configuration_at_runtime() } """); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Serilog": { - "MinimumLevel": { - "Default": "Information", - "Override": { - "Fake.Some": "Error", - "Fake.Category": "Warning" + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Serilog": { + "MinimumLevel": { + "Default": "Information", + "Override": { + "Fake.Some": "Error", + "Fake.Category": "Warning" + } + } } } - } - } - """); + """); fileProvider.NotifyChanged(); diff --git a/src/Management/test/Endpoint.Test/Actuators/Loggers/LoggersActuatorTest.cs b/src/Management/test/Endpoint.Test/Actuators/Loggers/LoggersActuatorTest.cs index bde4c60857..0e28b20870 100644 --- a/src/Management/test/Endpoint.Test/Actuators/Loggers/LoggersActuatorTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/Loggers/LoggersActuatorTest.cs @@ -588,21 +588,21 @@ public async Task Can_change_configuration_at_runtime() { var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Logging": { - "LogLevel": { - "Default": "Error", - "Fake": "Warning", - "Fake.Category.AtDebugLevel": "Debug" + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Logging": { + "LogLevel": { + "Default": "Error", + "Fake": "Warning", + "Fake.Category.AtDebugLevel": "Debug" + } + } } - } - } - """); + """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddInMemoryCollection(AppSettings); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); EnsureLoggingConfigurationIsBound(builder.Logging, builder.Configuration); builder.Services.AddSingleton(); builder.Services.AddLoggersActuator(); @@ -656,17 +656,17 @@ public async Task Can_change_configuration_at_runtime() } """); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Logging": { - "LogLevel": { - "Default": "Information", - "Fake.Some": "Error", - "Fake.Category": "Warning" + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Logging": { + "LogLevel": { + "Default": "Information", + "Fake.Some": "Error", + "Fake.Category": "Warning" + } + } } - } - } - """); + """); fileProvider.NotifyChanged(); diff --git a/src/Management/test/Endpoint.Test/Actuators/Refresh/HttpVerbInConventionalRoutingTest.cs b/src/Management/test/Endpoint.Test/Actuators/Refresh/HttpVerbInConventionalRoutingTest.cs index 902db72709..bb571357e7 100644 --- a/src/Management/test/Endpoint.Test/Actuators/Refresh/HttpVerbInConventionalRoutingTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/Refresh/HttpVerbInConventionalRoutingTest.cs @@ -186,22 +186,22 @@ public async Task Can_change_allowed_verbs_at_runtime() { MemoryFileProvider fileProvider = new(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "Actuator": { - "Exposure": { - "Include": ["refresh"] + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "Actuator": { + "Exposure": { + "Include": ["refresh"] + } + } } } } - } - } - """); + """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddControllersWithViews(options => options.EnableEndpointRouting = false); builder.Services.AddRefreshActuator(); @@ -218,22 +218,22 @@ public async Task Can_change_allowed_verbs_at_runtime() HttpResponseMessage postResponse = await httpClient.PostAsync(requestUri, null, TestContext.Current.CancellationToken); postResponse.StatusCode.Should().Be(HttpStatusCode.OK); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "Actuator": { - "Exposure": { - "Include": ["refresh"] + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "Actuator": { + "Exposure": { + "Include": ["refresh"] + } + }, + "Refresh": { + "AllowedVerbs": ["GET"] + } } - }, - "Refresh": { - "AllowedVerbs": ["GET"] } } - } - } - """); + """); fileProvider.NotifyChanged(); diff --git a/src/Management/test/Endpoint.Test/Actuators/Refresh/HttpVerbInEndpointRoutingTest.cs b/src/Management/test/Endpoint.Test/Actuators/Refresh/HttpVerbInEndpointRoutingTest.cs index 3687723972..80610b9771 100644 --- a/src/Management/test/Endpoint.Test/Actuators/Refresh/HttpVerbInEndpointRoutingTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/Refresh/HttpVerbInEndpointRoutingTest.cs @@ -186,22 +186,22 @@ public async Task Can_change_allowed_verbs_at_runtime() { MemoryFileProvider fileProvider = new(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "Actuator": { - "Exposure": { - "Include": ["refresh"] + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "Actuator": { + "Exposure": { + "Include": ["refresh"] + } + } } } } - } - } - """); + """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddControllersWithViews(); builder.Services.AddRefreshActuator(); @@ -218,22 +218,22 @@ public async Task Can_change_allowed_verbs_at_runtime() HttpResponseMessage postResponse = await httpClient.PostAsync(requestUri, null, TestContext.Current.CancellationToken); postResponse.StatusCode.Should().Be(HttpStatusCode.OK); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "Actuator": { - "Exposure": { - "Include": ["refresh"] + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "Actuator": { + "Exposure": { + "Include": ["refresh"] + } + }, + "Refresh": { + "AllowedVerbs": ["GET"] + } } - }, - "Refresh": { - "AllowedVerbs": ["GET"] } } - } - } - """); + """); fileProvider.NotifyChanged(); diff --git a/src/Management/test/Endpoint.Test/Actuators/Refresh/RefreshActuatorTest.cs b/src/Management/test/Endpoint.Test/Actuators/Refresh/RefreshActuatorTest.cs index 0faf8a75af..7ede0f15bd 100644 --- a/src/Management/test/Endpoint.Test/Actuators/Refresh/RefreshActuatorTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/Refresh/RefreshActuatorTest.cs @@ -200,21 +200,21 @@ public async Task Can_change_configuration_at_runtime() { var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "Refresh": { - "ReturnConfiguration": false + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "Refresh": { + "ReturnConfiguration": false + } + } } } - } - } - """); + """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddInMemoryCollection(AppSettings); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddRefreshActuator(); await using WebApplication host = builder.Build(); @@ -229,10 +229,10 @@ public async Task Can_change_configuration_at_runtime() responseBody1.Should().BeJson("[]"); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - } - """); + fileProvider.ReplaceAppSettingsJsonFile(""" + { + } + """); fileProvider.NotifyChanged(); diff --git a/src/Management/test/Endpoint.Test/Actuators/RouteMappings/RouteMappingsActuatorTest.cs b/src/Management/test/Endpoint.Test/Actuators/RouteMappings/RouteMappingsActuatorTest.cs index 3262df4951..6c63c31926 100644 --- a/src/Management/test/Endpoint.Test/Actuators/RouteMappings/RouteMappingsActuatorTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/RouteMappings/RouteMappingsActuatorTest.cs @@ -373,21 +373,21 @@ public async Task Can_change_configuration_at_runtime() { var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "Mappings": { - "IncludeActuators": false + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "Mappings": { + "IncludeActuators": false + } + } } } - } - } - """); + """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddInMemoryCollection(AppSettings); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddAllActuators(); await using WebApplication host = builder.Build(); @@ -400,10 +400,10 @@ public async Task Can_change_configuration_at_runtime() responseNode1["contexts"]!["application"]!["mappings"]!["dispatcherServlets"]!["dispatcherServlet"].Should().BeOfType().Subject.Should() .BeEmpty(); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - } - """); + fileProvider.ReplaceAppSettingsJsonFile(""" + { + } + """); fileProvider.NotifyChanged(); diff --git a/src/Management/test/Endpoint.Test/EndpointOptionsTest.cs b/src/Management/test/Endpoint.Test/EndpointOptionsTest.cs index 5610cf05e4..f5d9858575 100644 --- a/src/Management/test/Endpoint.Test/EndpointOptionsTest.cs +++ b/src/Management/test/Endpoint.Test/EndpointOptionsTest.cs @@ -86,14 +86,14 @@ public async Task CanTurnOffEndpointAtRuntimeFromExposureConfiguration() { MemoryFileProvider fileProvider = new(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management:Endpoints:Actuator:Exposure:Include:0": "env" - } - """); + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Management:Endpoints:Actuator:Exposure:Include:0": "env" + } + """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddAllActuators(); await using WebApplication app = builder.Build(); @@ -103,12 +103,12 @@ public async Task CanTurnOffEndpointAtRuntimeFromExposureConfiguration() HttpResponseMessage response1 = await httpClient.GetAsync(new Uri("/actuator/env", UriKind.Relative), TestContext.Current.CancellationToken); response1.StatusCode.Should().Be(HttpStatusCode.OK); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management:Endpoints:Actuator:Exposure:Include:0": "env", - "Management:Endpoints:Actuator:Exposure:Exclude:0": "*" - } - """); + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Management:Endpoints:Actuator:Exposure:Include:0": "env", + "Management:Endpoints:Actuator:Exposure:Exclude:0": "*" + } + """); fileProvider.NotifyChanged(); @@ -121,15 +121,15 @@ public async Task CanTurnOnEndpointAtRuntimeFromExposureConfiguration() { MemoryFileProvider fileProvider = new(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management:Endpoints:Actuator:Exposure:Include:0": "env", - "Management:Endpoints:Actuator:Exposure:Exclude:0": "*" - } - """); + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Management:Endpoints:Actuator:Exposure:Include:0": "env", + "Management:Endpoints:Actuator:Exposure:Exclude:0": "*" + } + """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddAllActuators(); await using WebApplication app = builder.Build(); @@ -139,11 +139,11 @@ public async Task CanTurnOnEndpointAtRuntimeFromExposureConfiguration() HttpResponseMessage response1 = await httpClient.GetAsync(new Uri("/actuator/env", UriKind.Relative), TestContext.Current.CancellationToken); response1.StatusCode.Should().Be(HttpStatusCode.NotFound); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management:Endpoints:Actuator:Exposure:Include:0": "env" - } - """); + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Management:Endpoints:Actuator:Exposure:Include:0": "env" + } + """); fileProvider.NotifyChanged(); @@ -156,15 +156,15 @@ public async Task CanTurnOffEndpointAtRuntimeFromEndpointConfiguration() { MemoryFileProvider fileProvider = new(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management:Endpoints:Actuator:Exposure:Include:0": "env", - "Management:Endpoints:Env:Enabled": "true" - } - """); + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Management:Endpoints:Actuator:Exposure:Include:0": "env", + "Management:Endpoints:Env:Enabled": "true" + } + """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddAllActuators(); await using WebApplication app = builder.Build(); @@ -174,12 +174,12 @@ public async Task CanTurnOffEndpointAtRuntimeFromEndpointConfiguration() HttpResponseMessage response1 = await httpClient.GetAsync(new Uri("/actuator/env", UriKind.Relative), TestContext.Current.CancellationToken); response1.StatusCode.Should().Be(HttpStatusCode.OK); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management:Endpoints:Actuator:Exposure:Include:0": "env", - "Management:Endpoints:Env:Enabled": "false" - } - """); + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Management:Endpoints:Actuator:Exposure:Include:0": "env", + "Management:Endpoints:Env:Enabled": "false" + } + """); fileProvider.NotifyChanged(); @@ -192,15 +192,15 @@ public async Task CanTurnOnEndpointAtRuntimeFromEndpointConfiguration() { MemoryFileProvider fileProvider = new(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management:Endpoints:Actuator:Exposure:Include:0": "env", - "Management:Endpoints:Env:Enabled": "false" - } - """); + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Management:Endpoints:Actuator:Exposure:Include:0": "env", + "Management:Endpoints:Env:Enabled": "false" + } + """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddAllActuators(); await using WebApplication app = builder.Build(); @@ -210,12 +210,12 @@ public async Task CanTurnOnEndpointAtRuntimeFromEndpointConfiguration() HttpResponseMessage response1 = await httpClient.GetAsync(new Uri("/actuator/env", UriKind.Relative), TestContext.Current.CancellationToken); response1.StatusCode.Should().Be(HttpStatusCode.NotFound); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management:Endpoints:Actuator:Exposure:Include:0": "env", - "Management:Endpoints:Env:Enabled": "true" - } - """); + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Management:Endpoints:Actuator:Exposure:Include:0": "env", + "Management:Endpoints:Env:Enabled": "true" + } + """); fileProvider.NotifyChanged(); diff --git a/src/Management/test/Endpoint.Test/SpringBootAdminClient/HostBuilderTest.cs b/src/Management/test/Endpoint.Test/SpringBootAdminClient/HostBuilderTest.cs index 7687a578c4..81dfc18dee 100644 --- a/src/Management/test/Endpoint.Test/SpringBootAdminClient/HostBuilderTest.cs +++ b/src/Management/test/Endpoint.Test/SpringBootAdminClient/HostBuilderTest.cs @@ -154,7 +154,7 @@ public async Task PeriodicRefreshCanBeTurnedOnAfterStart() { var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, $$""" + fileProvider.IncludeAppSettingsJsonFile($$""" { "Spring": { "Boot": { @@ -171,7 +171,7 @@ public async Task PeriodicRefreshCanBeTurnedOnAfterStart() """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.CreateDefault(false); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddSingleton(); builder.Services.AddSpringBootAdminClient(); @@ -186,7 +186,7 @@ public async Task PeriodicRefreshCanBeTurnedOnAfterStart() handler.Mock.GetMatchCount(registerMock).Should().Be(1); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, $$""" + fileProvider.ReplaceAppSettingsJsonFile($$""" { "Spring": { "Boot": { @@ -213,7 +213,7 @@ public async Task PeriodicRefreshCanBeTurnedOffAfterStart() { var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, $$""" + fileProvider.IncludeAppSettingsJsonFile($$""" { "Spring": { "Boot": { @@ -230,7 +230,7 @@ public async Task PeriodicRefreshCanBeTurnedOffAfterStart() """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.CreateDefault(false); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddSingleton(); builder.Services.AddSpringBootAdminClient(); @@ -246,7 +246,7 @@ public async Task PeriodicRefreshCanBeTurnedOffAfterStart() handler.Mock.GetMatchCount(registerMock).Should().BeGreaterThan(1); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, $$""" + fileProvider.ReplaceAppSettingsJsonFile($$""" { "Spring": { "Boot": { @@ -370,7 +370,7 @@ public async Task UnregistersFromPreviousServerOnConfigurationChange() { var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, $$""" + fileProvider.IncludeAppSettingsJsonFile($$""" { "Spring": { "Boot": { @@ -387,7 +387,7 @@ public async Task UnregistersFromPreviousServerOnConfigurationChange() """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.CreateDefault(false); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddSingleton(); builder.Services.AddSpringBootAdminClient(); @@ -405,7 +405,7 @@ public async Task UnregistersFromPreviousServerOnConfigurationChange() handler.Mock.GetMatchCount(registerMock1).Should().BeGreaterThan(1); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, $$""" + fileProvider.ReplaceAppSettingsJsonFile($$""" { "Spring": { "Boot": { @@ -434,7 +434,7 @@ public async Task UnregistersFromPreviousServerOnShutdownAfterConfigurationBecam { var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, $$""" + fileProvider.IncludeAppSettingsJsonFile($$""" { "Spring": { "Boot": { @@ -451,7 +451,7 @@ public async Task UnregistersFromPreviousServerOnShutdownAfterConfigurationBecam """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.CreateDefault(false); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddSingleton(); builder.Services.AddSpringBootAdminClient(); @@ -470,7 +470,7 @@ public async Task UnregistersFromPreviousServerOnShutdownAfterConfigurationBecam handler.Mock.GetMatchCount(registerMock1).Should().BeGreaterThan(1); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, $$""" + fileProvider.ReplaceAppSettingsJsonFile($$""" { "Spring": { "Boot": { diff --git a/src/Management/test/RazorPagesTestWebApp/Program.cs b/src/Management/test/RazorPagesTestWebApp/Program.cs index 508e5edcfa..b4295ca480 100644 --- a/src/Management/test/RazorPagesTestWebApp/Program.cs +++ b/src/Management/test/RazorPagesTestWebApp/Program.cs @@ -12,7 +12,7 @@ { // This project intentionally does NOT include appsettings*.json files, because they get copied to test projects // that reference this project, and that affects test outcomes. For example, setting the minimum log level - // to Trace on WebApplicationBuilder wouldn't work, because these files overrule log levels. + // to Trace on WebApplicationBuilder wouldn't work, because these files override log levels. ["DetailedErrors"] = builder.Environment.IsDevelopment() ? "true" : "false", ["Logging:LogLevel:Default"] = "Information",