Add ability to use AppConfig snapshots. (#249)
This commit is contained in:
Родитель
58e788a30b
Коммит
aca6c53bab
|
@ -53,7 +53,7 @@
|
|||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Data.AppConfiguration">
|
||||
<Version>1.1.0</Version>
|
||||
<Version>1.3.0</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Azure.Identity">
|
||||
<Version>1.11.4</Version>
|
||||
|
|
|
@ -6,6 +6,8 @@ using System.Collections.Concurrent;
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
|
@ -25,6 +27,7 @@ namespace Microsoft.Configuration.ConfigurationBuilders
|
|||
#pragma warning disable CS1591 // No xml comments for tag literals.
|
||||
public const string endpointTag = "endpoint";
|
||||
public const string connectionStringTag = "connectionString";
|
||||
public const string snapshotTag = "snapshot";
|
||||
public const string keyFilterTag = "keyFilter";
|
||||
public const string labelFilterTag = "labelFilter";
|
||||
public const string dateTimeFilterTag = "acceptDateTime";
|
||||
|
@ -41,6 +44,12 @@ namespace Microsoft.Configuration.ConfigurationBuilders
|
|||
/// </summary>
|
||||
public string ConnectionString { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of a Snapshot to retrieve config values from.
|
||||
/// If provided, this setting supercedes the Label, Key, and DateTime filters.
|
||||
/// </summary>
|
||||
public string Snapshot { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a 'Key Filter' to use when searching for config values.
|
||||
/// </summary>
|
||||
|
@ -63,6 +72,7 @@ namespace Microsoft.Configuration.ConfigurationBuilders
|
|||
|
||||
private ConcurrentDictionary<Uri, SecretClient> _kvClientCache;
|
||||
private ConfigurationClient _client;
|
||||
private FieldInfo _cachedValuesField;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the configuration builder lazily.
|
||||
|
@ -76,30 +86,45 @@ namespace Microsoft.Configuration.ConfigurationBuilders
|
|||
|
||||
base.LazyInitialize(name, config);
|
||||
|
||||
// TODO: This is super hacky. In a major-version update, this should be revamped in cooperation with the KVCB base class.
|
||||
// When we cache our values, we drew them from a source where case matters. Case should still matter in our cache.
|
||||
// Replace the built-in case-insensitive cache with a case-sensitive one.
|
||||
_cachedValuesField = typeof(KeyValueConfigBuilder).GetField("_cachedValues", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
if (_cachedValuesField != null)
|
||||
_cachedValuesField.SetValue(this, new Dictionary<string, string>());
|
||||
|
||||
// At this point, we have our 'Enabled' choice. If we are disabled, we can stop right here.
|
||||
if (Enabled == KeyValueEnabled.Disabled) return;
|
||||
|
||||
// keyFilter
|
||||
KeyFilter = UpdateConfigSettingWithAppSettings(keyFilterTag);
|
||||
if (String.IsNullOrWhiteSpace(KeyFilter))
|
||||
KeyFilter = null;
|
||||
// snapshot
|
||||
Snapshot = UpdateConfigSettingWithAppSettings(snapshotTag);
|
||||
if (String.IsNullOrWhiteSpace(Snapshot))
|
||||
{
|
||||
// Only configure other filters if 'snapshot' is not provided.
|
||||
Snapshot = null;
|
||||
|
||||
// labelFilter
|
||||
// Place some restrictions on label filter, similar to the .net core provider.
|
||||
// The idea is to restrict queries to one label, and one label only. Even if that
|
||||
// one label is the "empty" label. Doing so will remove the decision making process
|
||||
// from this builders hands about which key/value/label tuple to choose when there
|
||||
// are multiple.
|
||||
LabelFilter = UpdateConfigSettingWithAppSettings(labelFilterTag);
|
||||
if (String.IsNullOrWhiteSpace(LabelFilter)) {
|
||||
LabelFilter = null;
|
||||
}
|
||||
else if (LabelFilter.Contains('*') || LabelFilter.Contains(',')) {
|
||||
throw new ArgumentException("The characters '*' and ',' are not supported in label filters.", labelFilterTag);
|
||||
}
|
||||
// keyFilter
|
||||
KeyFilter = UpdateConfigSettingWithAppSettings(keyFilterTag);
|
||||
if (String.IsNullOrWhiteSpace(KeyFilter))
|
||||
KeyFilter = null;
|
||||
|
||||
// acceptDateTime
|
||||
AcceptDateTime = (UpdateConfigSettingWithAppSettings(dateTimeFilterTag) != null) ? DateTimeOffset.Parse(config[dateTimeFilterTag]) : AcceptDateTime;
|
||||
// labelFilter
|
||||
// Place some restrictions on label filter, similar to the .net core provider.
|
||||
// The idea is to restrict queries to one label, and one label only. Even if that
|
||||
// one label is the "empty" label. Doing so will remove the decision making process
|
||||
// from this builders hands about which key/value/label tuple to choose when there
|
||||
// are multiple.
|
||||
LabelFilter = UpdateConfigSettingWithAppSettings(labelFilterTag);
|
||||
if (String.IsNullOrWhiteSpace(LabelFilter)) {
|
||||
LabelFilter = null;
|
||||
}
|
||||
else if (LabelFilter.Contains('*') || LabelFilter.Contains(',')) {
|
||||
throw new ArgumentException("The characters '*' and ',' are not supported in label filters.", labelFilterTag);
|
||||
}
|
||||
|
||||
// acceptDateTime
|
||||
AcceptDateTime = (UpdateConfigSettingWithAppSettings(dateTimeFilterTag) != null) ? DateTimeOffset.Parse(config[dateTimeFilterTag]) : AcceptDateTime;
|
||||
}
|
||||
|
||||
// Azure Key Vault Integration
|
||||
UseAzureKeyVault = (UpdateConfigSettingWithAppSettings(useKeyVaultTag) != null) ? Boolean.Parse(config[useKeyVaultTag]) : UseAzureKeyVault;
|
||||
|
@ -148,14 +173,16 @@ namespace Microsoft.Configuration.ConfigurationBuilders
|
|||
// At this point we've got all our ducks in a row and are ready to go. And we know that
|
||||
// we will be used, because this is the 'lazy' initializer. But let's handle one oddball case
|
||||
// before we go.
|
||||
// If we have a keyFilter set, then we will always query a set of values instead of a single
|
||||
// value, regardless of whether we are in strict/token/greedy mode. But if we're not in
|
||||
// greedy mode, then the base KeyValueConfigBuilder will still request each key/value it is
|
||||
// interested in one at a time, and only cache that one result. So we will end up querying the
|
||||
// same set of values from the AppConfig service for every value. Let's only do this once and
|
||||
// cache the entire set to make those calls to GetValueInternal read from the cache instead of
|
||||
// hitting the service every time.
|
||||
if (KeyFilter != null && Mode != KeyValueMode.Greedy)
|
||||
// In non-Greedy modes, after this point, all values are fetched - and cached - one at a time.
|
||||
// But there are cases where the AppConfig SDK requires us to fetch _all_ matching values at
|
||||
// once instead of one-at-a-time. Might as well cache those once instead of fetching them
|
||||
// all every time we need to read the next value.
|
||||
// TODO: It would be better to do this in 'GetValue()', but there is not a good way to get a value
|
||||
// from the cache after populating it in that method - and changing the API between this class
|
||||
// and the base class to make that easier would require a major version update. The API already
|
||||
// anticipates this usage scenario for 'GetAllValues()' though, so we only need to do this
|
||||
// in non-Greedy modes.
|
||||
if ((Snapshot != null || KeyFilter != null) && Mode != KeyValueMode.Greedy)
|
||||
EnsureGreedyInitialized();
|
||||
}
|
||||
|
||||
|
@ -191,13 +218,11 @@ namespace Microsoft.Configuration.ConfigurationBuilders
|
|||
/// <returns>The value corresponding to the given 'key' or null if no value is found.</returns>
|
||||
public override string GetValue(string key)
|
||||
{
|
||||
// Quick shortcut. If we have a keyFilter set, then we've already populated the cache with
|
||||
// all possible values for this builder. If we get here, that means the key was not found in
|
||||
// the cache. Going further will query with just the key name, and no keyFilter applied. This
|
||||
// could result in finding a value... but we shouldn't, because the requested key does not
|
||||
// match the keyFilter - otherwise it would already be in the cache. Avoid the trouble and
|
||||
// shortcut return nothing in this case.
|
||||
if (KeyFilter != null)
|
||||
// Quick shortcut. If we have snapshot or keyFilter set, then we've already populated the cache
|
||||
// with all possible values for this builder. If we get here, that means the key was not found in
|
||||
// the cache. Going further will query again for a key that we probably won't find. Avoid the trouble
|
||||
// and shortcut return nothing in this case.
|
||||
if (KeyFilter != null || Snapshot != null)
|
||||
return null;
|
||||
|
||||
// Azure Key Vault keys are case-insensitive, so this should be fine.
|
||||
|
@ -282,44 +307,19 @@ namespace Microsoft.Configuration.ConfigurationBuilders
|
|||
if (_client == null)
|
||||
return null;
|
||||
|
||||
SettingSelector selector = new SettingSelector { KeyFilter = key };
|
||||
|
||||
if (LabelFilter != null)
|
||||
{
|
||||
selector.LabelFilter = LabelFilter;
|
||||
}
|
||||
if (AcceptDateTime > DateTimeOffset.MinValue)
|
||||
{
|
||||
selector.AcceptDateTime = AcceptDateTime;
|
||||
}
|
||||
// TODO: Reduce bandwidth by limiting the fields we retrieve.
|
||||
// Currently, content type doesn't get delivered, even if we add it to the selection. This prevents KeyVault recognition.
|
||||
//selector.Fields = SettingFields.Key | SettingFields.Value | SettingFields.ContentType;
|
||||
|
||||
try
|
||||
{
|
||||
AsyncPageable<ConfigurationSetting> settings = _client.GetConfigurationSettingsAsync(selector);
|
||||
IAsyncEnumerator<ConfigurationSetting> enumerator = settings.GetAsyncEnumerator();
|
||||
ConfigurationSetting setting = await GetConfigSettingAsync(key);
|
||||
|
||||
try
|
||||
if (setting == null)
|
||||
return null;
|
||||
|
||||
if (UseAzureKeyVault && setting is SecretReferenceConfigurationSetting secretReference)
|
||||
{
|
||||
// There should only be one result. If there's more, we're only returning the fisrt.
|
||||
await enumerator.MoveNextAsync();
|
||||
ConfigurationSetting current = enumerator.Current;
|
||||
if (current == null)
|
||||
return null;
|
||||
|
||||
if (UseAzureKeyVault && current is SecretReferenceConfigurationSetting secretReference)
|
||||
{
|
||||
return await GetKeyVaultValue(secretReference);
|
||||
}
|
||||
|
||||
return current.Value;
|
||||
}
|
||||
finally
|
||||
{
|
||||
await enumerator.DisposeAsync();
|
||||
return await GetKeyVaultValue(secretReference);
|
||||
}
|
||||
|
||||
return setting.Value;
|
||||
}
|
||||
catch (AggregateException ae)
|
||||
{
|
||||
|
@ -337,29 +337,12 @@ namespace Microsoft.Configuration.ConfigurationBuilders
|
|||
if (_client == null)
|
||||
return data;
|
||||
|
||||
SettingSelector selector = new SettingSelector();
|
||||
if (KeyFilter != null)
|
||||
{
|
||||
selector.KeyFilter = KeyFilter;
|
||||
}
|
||||
if (LabelFilter != null)
|
||||
{
|
||||
selector.LabelFilter = LabelFilter;
|
||||
}
|
||||
if (AcceptDateTime > DateTimeOffset.MinValue)
|
||||
{
|
||||
selector.AcceptDateTime = AcceptDateTime;
|
||||
}
|
||||
// TODO: Reduce bandwidth by limiting the fields we retrieve.
|
||||
// Currently, content type doesn't get delivered, even if we add it to the selection. This prevents KeyVault recognition.
|
||||
//selector.Fields = SettingFields.Key | SettingFields.Value | SettingFields.ContentType;
|
||||
|
||||
// We don't make any guarantees about which kv get precendence when there are multiple of the same key...
|
||||
// But the config service does seem to return kvs in a preferred order - no label first, then alphabetical by label.
|
||||
// Prefer the first kv we encounter from the config service.
|
||||
try
|
||||
{
|
||||
AsyncPageable<ConfigurationSetting> settings = _client.GetConfigurationSettingsAsync(selector);
|
||||
AsyncPageable<ConfigurationSetting> settings = GetConfigSettings();
|
||||
IAsyncEnumerator<ConfigurationSetting> enumerator = settings.GetAsyncEnumerator();
|
||||
try
|
||||
{
|
||||
|
@ -405,6 +388,61 @@ namespace Microsoft.Configuration.ConfigurationBuilders
|
|||
return data;
|
||||
}
|
||||
|
||||
private async Task<ConfigurationSetting> GetConfigSettingAsync(string name)
|
||||
{
|
||||
AsyncPageable<ConfigurationSetting> settings = GetConfigSettings(name);
|
||||
|
||||
// Alas, 'await using' isn't available in C# 7.3, which is technically the last supported C# for .NET Framework.
|
||||
IAsyncEnumerator<ConfigurationSetting> enumerator = settings.GetAsyncEnumerator();
|
||||
try
|
||||
{
|
||||
// TODO smolloy - In Snapshot mode, there is no way to select just this key.
|
||||
// There should only be one result. If there's more, we're only returning the fisrt.
|
||||
await enumerator.MoveNextAsync();
|
||||
return enumerator.Current;
|
||||
}
|
||||
finally
|
||||
{
|
||||
await enumerator.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private AsyncPageable<ConfigurationSetting> GetConfigSettings(string name = null)
|
||||
{
|
||||
// TODO: Reduce bandwidth by limiting the fields we retrieve.
|
||||
// Currently, content type doesn't get delivered, even if we add it to the selection. This prevents KeyVault recognition.
|
||||
//selector.Fields = SettingFields.Key | SettingFields.Value | SettingFields.ContentType;
|
||||
|
||||
// Use a snapshot if it was provided
|
||||
if (Snapshot != null)
|
||||
{
|
||||
return _client.GetConfigurationSettingsForSnapshotAsync(Snapshot);
|
||||
}
|
||||
else
|
||||
{
|
||||
SettingSelector selector = new SettingSelector();
|
||||
if (name != null)
|
||||
{
|
||||
selector.KeyFilter = name;
|
||||
}
|
||||
else if (KeyFilter != null)
|
||||
{
|
||||
selector.KeyFilter = KeyFilter;
|
||||
}
|
||||
if (LabelFilter != null)
|
||||
{
|
||||
selector.LabelFilter = LabelFilter;
|
||||
}
|
||||
if (AcceptDateTime > DateTimeOffset.MinValue)
|
||||
{
|
||||
selector.AcceptDateTime = AcceptDateTime;
|
||||
}
|
||||
|
||||
// We use 'GetSetting_s_' here because the singular version doesn't support multiple filters. :/
|
||||
return _client.GetConfigurationSettingsAsync(selector);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> GetKeyVaultValue(SecretReferenceConfigurationSetting secretReference)
|
||||
{
|
||||
KeyVaultSecretIdentifier secretIdentifier = new KeyVaultSecretIdentifier(secretReference.SecretId);
|
||||
|
|
|
@ -2,8 +2,10 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Configuration;
|
||||
using System.Reflection;
|
||||
using System.Runtime;
|
||||
using System.Web;
|
||||
using Azure;
|
||||
using Azure.Core;
|
||||
using Azure.Data.AppConfiguration;
|
||||
using Azure.Identity;
|
||||
|
@ -33,6 +35,14 @@ namespace Test
|
|||
}
|
||||
}
|
||||
|
||||
enum ConfigAge
|
||||
{
|
||||
IsAncient,
|
||||
IsOld,
|
||||
IsNew,
|
||||
IsSnapshot,
|
||||
};
|
||||
|
||||
public class AzureAppConfigTests
|
||||
{
|
||||
private static readonly string kva_value_old = "versionedValue-Older";
|
||||
|
@ -43,6 +53,7 @@ namespace Test
|
|||
private static readonly string commonEndPoint;
|
||||
private static readonly string customEndPoint;
|
||||
private static readonly string keyVaultName;
|
||||
private static readonly string testSnapName;
|
||||
private static readonly DateTimeOffset customEpochPlaceholder = DateTimeOffset.Parse("December 31, 1999 11:59pm");
|
||||
private static readonly DateTimeOffset oldTimeFilter = DateTimeOffset.Parse("April 15, 2002 9:00am");
|
||||
private static readonly DateTimeOffset customEpoch;
|
||||
|
@ -82,10 +93,11 @@ namespace Test
|
|||
// For efficiency when debugging, we might not want to "clear out" and restage this on every run
|
||||
bool recreateTestData = true;
|
||||
|
||||
// The Common config store gets filled out, but the store itself is assumed to already exist.
|
||||
ConfigurationClient cfgClient = new ConfigurationClient(new Uri(commonEndPoint), new DefaultAzureCredential());
|
||||
|
||||
if (recreateTestData)
|
||||
{
|
||||
// The Common config store gets filled out, but the store itself is assumed to already exist.
|
||||
ConfigurationClient cfgClient = new ConfigurationClient(new Uri(commonEndPoint), new DefaultAzureCredential());
|
||||
foreach (string key in CommonBuilderTests.CommonKeyValuePairs)
|
||||
{
|
||||
UpdateConfigSetting(cfgClient, key, CommonBuilderTests.CommonKeyValuePairs[key]);
|
||||
|
@ -107,7 +119,15 @@ namespace Test
|
|||
// keyVaultSetting kva_value_old kva_value_new kva_value_old kvb_value
|
||||
// superKeyVaultSetting kvb_value kva_value_old kva_value_new
|
||||
//
|
||||
// curious about onlyNewLabA andNewLabB
|
||||
// Snapshots - defined by 1-3 filters. Order determines precedence, as any key can only have one value in a
|
||||
// snapshot - meaning if filters with labelA and labelB are defined, the value for 'superTestSetting'
|
||||
// will ultimately be determined by which filter is primary.
|
||||
//
|
||||
// Define a snapshot with:
|
||||
// filter 1: superKeyVaultSetting + labelB
|
||||
// filter 2: testSetting
|
||||
// filter 3: labelA
|
||||
//
|
||||
|
||||
// First, ensure the KeyVault values are populated in Key Vault
|
||||
SecretClient kvClient = new SecretClient(new Uri($"https://{keyVaultName}.vault.azure.net"), new DefaultAzureCredential());
|
||||
|
@ -155,8 +175,21 @@ namespace Test
|
|||
}
|
||||
|
||||
// We always need to know the epoch time, so we can compare against it.
|
||||
var epochClient = new ConfigurationClient(new Uri(customEndPoint), new DefaultAzureCredential());
|
||||
customEpoch = ((DateTimeOffset)epochClient.GetConfigurationSetting("epochDTO").Value.LastModified).AddSeconds(1);
|
||||
customEpoch = ((DateTimeOffset)cfgClient.GetConfigurationSetting("epochDTO").Value.LastModified).AddSeconds(1);
|
||||
|
||||
// And setup our test snapshot
|
||||
var filters = new List<ConfigurationSettingsFilter> {
|
||||
// Priority order is reverse of this list for some reason. :/
|
||||
new ConfigurationSettingsFilter("") { Label = "labelA" },
|
||||
new ConfigurationSettingsFilter("testSetting"),
|
||||
new ConfigurationSettingsFilter("superKeyVaultSetting") { Label = "labelB" },
|
||||
};
|
||||
var testSnapshot = new ConfigurationSnapshot(filters);
|
||||
testSnapName = "testSnapshot_" + DateTime.Now.Ticks;
|
||||
var operation = cfgClient.CreateSnapshot(WaitUntil.Completed, testSnapName, testSnapshot);
|
||||
if (!operation.HasValue)
|
||||
throw new Exception("Creation of test snapshot for AppConfig failed.");
|
||||
cfgClient.ArchiveSnapshot(testSnapName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -242,6 +275,9 @@ namespace Test
|
|||
Assert.Equal(placeholderEndPoint, builder.Endpoint);
|
||||
Assert.Null(builder.ConnectionString);
|
||||
|
||||
// Snapshot
|
||||
Assert.Null(builder.Snapshot);
|
||||
|
||||
// KeyFilter
|
||||
Assert.Null(builder.KeyFilter);
|
||||
|
||||
|
@ -288,11 +324,41 @@ namespace Test
|
|||
Assert.Equal(placeholderEndPoint, builder.Endpoint);
|
||||
Assert.Null(builder.ConnectionString);
|
||||
|
||||
// KeyFilter, LabelFilter, and DateTimeFilter - Use 'Greedy' since KeyFilter+NotGreedy == preload all values on init
|
||||
var dts = DateTimeOffset.Now.ToString("O"); // This will get ToString() and back, losing some tick-granularity. So lets make the string our source of truth.
|
||||
builder = TestHelper.CreateBuilder<AzureAppConfigurationBuilder>(() => new AzureAppConfigurationBuilder(), "AzureAppConfigSettings4",
|
||||
new NameValueCollection() { { "endpoint", placeholderEndPoint }, { "mode", KeyValueMode.Greedy.ToString() }, { "keyFilter", "fOo" },
|
||||
{ "labelFilter", "baR" }, { "acceptDateTime", dts } });
|
||||
Assert.Equal(placeholderEndPoint, builder.Endpoint);
|
||||
Assert.Null(builder.ConnectionString);
|
||||
Assert.Equal("fOo", builder.KeyFilter);
|
||||
Assert.Equal("baR", builder.LabelFilter);
|
||||
Assert.Equal(DateTimeOffset.Parse(dts), builder.AcceptDateTime);
|
||||
|
||||
// Snapshot - Use 'Greedy' since Snapshot+NotGreedy == preload all values on init
|
||||
builder = TestHelper.CreateBuilder<AzureAppConfigurationBuilder>(() => new AzureAppConfigurationBuilder(), "AzureAppConfigSettings5",
|
||||
new NameValueCollection() { { "endpoint", placeholderEndPoint }, { "mode", KeyValueMode.Greedy.ToString() }, { "snapshot", "name_of_snapshot" }, });
|
||||
Assert.Equal(placeholderEndPoint, builder.Endpoint);
|
||||
Assert.Null(builder.ConnectionString);
|
||||
Assert.Equal("name_of_snapshot", builder.Snapshot);
|
||||
|
||||
// Snapshot takes precedence over other filters
|
||||
builder = TestHelper.CreateBuilder<AzureAppConfigurationBuilder>(() => new AzureAppConfigurationBuilder(), "AzureAppConfigSettings6",
|
||||
new NameValueCollection() { { "endpoint", placeholderEndPoint }, { "mode", KeyValueMode.Greedy.ToString() }, { "snapshot", "snapname" },
|
||||
{ "keyFilter", "FooBar" }, { "labelFilter", "Baz" }, { "acceptDateTime", dts }, { "useAzureKeyVault", "true" } });
|
||||
Assert.Equal(placeholderEndPoint, builder.Endpoint);
|
||||
Assert.Null(builder.ConnectionString);
|
||||
Assert.Equal("snapname", builder.Snapshot);
|
||||
Assert.Null(builder.KeyFilter);
|
||||
Assert.Null(builder.LabelFilter);
|
||||
Assert.Equal(DateTimeOffset.MinValue, builder.AcceptDateTime);
|
||||
Assert.True(builder.UseAzureKeyVault);
|
||||
|
||||
// These tests require executing the builder, which needs a valid endpoint.
|
||||
if (AppConfigTestsEnabled)
|
||||
{
|
||||
// UseKeyVault is case insensitive, allows reading KeyVault values
|
||||
builder = TestHelper.CreateBuilder<AzureAppConfigurationBuilder>(() => new AzureAppConfigurationBuilder(), "AzureAppConfigSettings4",
|
||||
builder = TestHelper.CreateBuilder<AzureAppConfigurationBuilder>(() => new AzureAppConfigurationBuilder(), "AzureAppConfigSettings7",
|
||||
new NameValueCollection() { { "endpoint", customEndPoint }, { "UsEaZuReKeYvAuLt", "tRUe" } });
|
||||
Assert.Equal(customEndPoint, builder.Endpoint);
|
||||
Assert.True(builder.UseAzureKeyVault);
|
||||
|
@ -301,7 +367,7 @@ namespace Test
|
|||
Assert.Equal(kva_value_old, TestHelper.GetValueFromCollection(allValues, "superKeyVaultSetting"));
|
||||
|
||||
// UseKeyVault is case insensitive, does not allow reading KeyVault values
|
||||
builder = TestHelper.CreateBuilder<AzureAppConfigurationBuilder>(() => new AzureAppConfigurationBuilder(), "AzureAppConfigSettings5",
|
||||
builder = TestHelper.CreateBuilder<AzureAppConfigurationBuilder>(() => new AzureAppConfigurationBuilder(), "AzureAppConfigSettings8",
|
||||
new NameValueCollection() { { "endpoint", customEndPoint }, { "useAzureKeyVault", "false" } });
|
||||
Assert.Equal(customEndPoint, builder.Endpoint);
|
||||
Assert.False(builder.UseAzureKeyVault);
|
||||
|
@ -339,17 +405,29 @@ namespace Test
|
|||
dtFilter = customEpoch;
|
||||
|
||||
// MinValue is interpretted as "no filter" by Azure AppConfig. So only our epoch time counts as "old."
|
||||
bool? isOld = null;
|
||||
ConfigAge age = ConfigAge.IsNew;
|
||||
if (dtFilter == customEpoch)
|
||||
isOld = true;
|
||||
else if (dtFilter != oldTimeFilter)
|
||||
isOld = false;
|
||||
age = ConfigAge.IsOld;
|
||||
else if (dtFilter < customEpoch && dtFilter != DateTimeOffset.MinValue)
|
||||
age = ConfigAge.IsAncient;
|
||||
|
||||
// Trying all sorts of combinations with just one test and lots of theory data
|
||||
var builder = TestHelper.CreateBuilder<AzureAppConfigurationBuilder>(() => new AzureAppConfigurationBuilder(), "AzureAppConfigFilters",
|
||||
new NameValueCollection() { { "endpoint", customEndPoint }, { "mode", mode.ToString() }, { "keyFilter", keyFilter }, { "labelFilter", labelFilter },
|
||||
{ "acceptDateTime", dtFilter.ToString() }, { "useAzureKeyVault", useAzure.ToString() }});
|
||||
ValidateExpectedConfig(builder, isOld);
|
||||
ValidateExpectedConfig(builder, age);
|
||||
}
|
||||
|
||||
[AppConfigTheory]
|
||||
[InlineData(KeyValueMode.Strict)]
|
||||
[InlineData(KeyValueMode.Greedy)]
|
||||
public void AzureAppConfig_Snapshot(KeyValueMode mode)
|
||||
{
|
||||
// Trying all sorts of combinations with just one test and lots of theory data
|
||||
var builder = TestHelper.CreateBuilder<AzureAppConfigurationBuilder>(() => new AzureAppConfigurationBuilder(), "AzureAppConfigFilters",
|
||||
new NameValueCollection() { { "endpoint", customEndPoint }, { "mode", mode.ToString() }, { "snapshot", testSnapName }, { "useAzureKeyVault", "true" } });
|
||||
|
||||
ValidateExpectedConfig(builder, ConfigAge.IsSnapshot);
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
|
@ -484,13 +562,55 @@ namespace Test
|
|||
// ======================================================================
|
||||
// Helpers
|
||||
// ======================================================================
|
||||
|
||||
private AppSettingsSection GetCustomAppSettings(bool addAllSettings)
|
||||
{
|
||||
var cfg = TestHelper.LoadMultiLevelConfig("empty.config", "customAppSettings.config");
|
||||
var appSettings = cfg.AppSettings;
|
||||
|
||||
// Prime the appSettings section as expected for this test
|
||||
appSettings.Settings.Add("casetestsetting", "untouched"); // Yes, the case is wrong. That's the point.
|
||||
if (addAllSettings)
|
||||
{
|
||||
appSettings.Settings.Add("testSetting", "untouched");
|
||||
appSettings.Settings.Add("newTestSetting", "untouched");
|
||||
appSettings.Settings.Add("superTestSetting", "untouched");
|
||||
appSettings.Settings.Add("keyVaultSetting", "untouched");
|
||||
appSettings.Settings.Add("superKeyVaultSetting", "untouched");
|
||||
}
|
||||
|
||||
return appSettings;
|
||||
}
|
||||
|
||||
// TODO: Mock ConfigurationClient. Much work, and we'd need to inject it into the builder.
|
||||
private string GetExpectedConfigValue(AzureAppConfigurationBuilder builder, string key, bool? beforeEpoch)
|
||||
private string GetExpectedConfigValue(AzureAppConfigurationBuilder builder, string key, ConfigAge age)
|
||||
{
|
||||
// Before everything, there should be nothing
|
||||
if (beforeEpoch == null)
|
||||
if (age == ConfigAge.IsAncient)
|
||||
return null;
|
||||
|
||||
// Snapshots are straight forward and don't bring in other filters
|
||||
if (age == ConfigAge.IsSnapshot)
|
||||
{
|
||||
switch (key)
|
||||
{
|
||||
case "caseTestSetting":
|
||||
return "altCaseTestValue";
|
||||
case "testSetting":
|
||||
return "newTestValue";
|
||||
case "newTestSetting":
|
||||
return "ntValueA";
|
||||
case "superTestSetting":
|
||||
return "newSuperAlpha";
|
||||
case "keyVaultSetting":
|
||||
return (builder.UseAzureKeyVault) ? kvb_value : kvUriRegex;
|
||||
case "superKeyVaultSetting":
|
||||
return (builder.UseAzureKeyVault) ? kva_value_new : kvUriRegex;
|
||||
}
|
||||
|
||||
return null; // Includes epochDTO
|
||||
}
|
||||
|
||||
// Key filter can be figured out before the switch to keep things simple
|
||||
if (!String.IsNullOrWhiteSpace(builder.KeyFilter))
|
||||
{
|
||||
|
@ -512,29 +632,29 @@ namespace Test
|
|||
// We don't validate the value. Just don't return null if it isn't filtered out by labels.
|
||||
return (noLabel) ? customEpochPlaceholder.ToString() : null;
|
||||
case "caseTestSetting":
|
||||
if (beforeEpoch.Value)
|
||||
if (age == ConfigAge.IsOld)
|
||||
return (noLabel || labelA) ? "altCaseTestValue" : null;
|
||||
return (labelA) ? "altCaseTestValue" : (noLabel) ? "newCaseTestValue" : null;
|
||||
case "testSetting":
|
||||
if (beforeEpoch.Value)
|
||||
if (age == ConfigAge.IsOld)
|
||||
return (labelA) ? "altTestValue" : (noLabel) ? "oldTestValue" : null;
|
||||
return (labelA) ? "newAltValue" : (noLabel) ? "newTestValue" : null;
|
||||
case "newTestSetting":
|
||||
if (beforeEpoch.Value)
|
||||
if (age == ConfigAge.IsOld)
|
||||
return null;
|
||||
return (labelA) ? "ntValueA" : (noLabel) ? "ntOGValue" : null;
|
||||
case "superTestSetting":
|
||||
if (beforeEpoch.Value)
|
||||
if (age == ConfigAge.IsOld)
|
||||
return (noLabel) ? "oldSuperValue" : null;
|
||||
return (labelA) ? "newSuperAlpha" : (noLabel) ? "oldSuperValue" : null; // Probably null - unless label was 'labelB' which we are using in tests yet
|
||||
case "keyVaultSetting":
|
||||
if (beforeEpoch.Value)
|
||||
if (age == ConfigAge.IsOld)
|
||||
kvreturn = (labelA) ? kva_value_new : (noLabel) ? kva_value_old : null;
|
||||
else
|
||||
kvreturn = (labelA) ? kvb_value : (noLabel) ? kva_value_old : null;
|
||||
break;
|
||||
case "superKeyVaultSetting":
|
||||
kvreturn = (!noLabel) ? null : (beforeEpoch.Value) ? kvb_value : kva_value_old;
|
||||
kvreturn = (!noLabel) ? null : (age == ConfigAge.IsOld) ? kvb_value : kva_value_old;
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -544,23 +664,10 @@ namespace Test
|
|||
return kvreturn;
|
||||
}
|
||||
|
||||
private void ValidateExpectedConfig(AzureAppConfigurationBuilder builder, bool? beforeEpoch)
|
||||
private void ValidateExpectedConfig(AzureAppConfigurationBuilder builder, ConfigAge age)
|
||||
{
|
||||
var cfg = TestHelper.LoadMultiLevelConfig("empty.config", "customAppSettings.config");
|
||||
var appSettings = cfg.AppSettings;
|
||||
|
||||
// Prime the appSettings section as expected for this test
|
||||
appSettings.Settings.Add("casetestsetting", "untouched"); // Yes, the case is wrong. That's the point.
|
||||
if (builder.Mode != KeyValueMode.Greedy)
|
||||
{
|
||||
appSettings.Settings.Add("testSetting", "untouched");
|
||||
appSettings.Settings.Add("newTestSetting", "untouched");
|
||||
appSettings.Settings.Add("superTestSetting", "untouched");
|
||||
appSettings.Settings.Add("keyVaultSetting", "untouched");
|
||||
appSettings.Settings.Add("superKeyVaultSetting", "untouched");
|
||||
}
|
||||
|
||||
// Run the settings through the builder
|
||||
var appSettings = GetCustomAppSettings(builder.Mode != KeyValueMode.Greedy);
|
||||
appSettings = (AppSettingsSection)builder.ProcessConfigurationSection(appSettings);
|
||||
|
||||
// Validation of values kind of assumes this is true. Make sure of it.
|
||||
|
@ -579,30 +686,30 @@ namespace Test
|
|||
int expectedCount = 1;
|
||||
|
||||
// We don't verify 'epochDTO', but it might get sucked in in greedy mode.
|
||||
if (GetExpectedConfigValue(builder, "epochDTO", beforeEpoch) != null)
|
||||
if (GetExpectedConfigValue(builder, "epochDTO", age) != null)
|
||||
expectedCount++;
|
||||
|
||||
// In Greedy mode, we'll get a value for 'caseTestSetting' and then back in .Net config world, we
|
||||
// will put that in place of the already existing 'casetestsetting' value.
|
||||
var expectedValue = GetExpectedConfigValue(builder, "caseTestSetting", beforeEpoch) ?? "untouched";
|
||||
var expectedValue = GetExpectedConfigValue(builder, "caseTestSetting", age) ?? "untouched";
|
||||
Assert.Equal(expectedValue, appSettings.Settings["caseTestSetting"]?.Value);
|
||||
|
||||
expectedValue = GetExpectedConfigValue(builder, "testSetting", beforeEpoch);
|
||||
expectedValue = GetExpectedConfigValue(builder, "testSetting", age);
|
||||
if (expectedValue != null)
|
||||
expectedCount++;
|
||||
Assert.Equal(expectedValue, appSettings.Settings["testSetting"]?.Value);
|
||||
|
||||
expectedValue = GetExpectedConfigValue(builder, "newTestSetting", beforeEpoch);
|
||||
expectedValue = GetExpectedConfigValue(builder, "newTestSetting", age);
|
||||
if (expectedValue != null)
|
||||
expectedCount++;
|
||||
Assert.Equal(expectedValue, appSettings.Settings["newTestSetting"]?.Value);
|
||||
|
||||
expectedValue = GetExpectedConfigValue(builder, "superTestSetting", beforeEpoch);
|
||||
expectedValue = GetExpectedConfigValue(builder, "superTestSetting", age);
|
||||
if (expectedValue != null)
|
||||
expectedCount++;
|
||||
Assert.Equal(expectedValue, appSettings.Settings["superTestSetting"]?.Value);
|
||||
|
||||
expectedValue = GetExpectedConfigValue(builder, "keyVaultSetting", beforeEpoch);
|
||||
expectedValue = GetExpectedConfigValue(builder, "keyVaultSetting", age);
|
||||
if (expectedValue != null)
|
||||
{
|
||||
expectedCount++;
|
||||
|
@ -611,7 +718,7 @@ namespace Test
|
|||
else
|
||||
Assert.Null(appSettings.Settings["keyVaultSetting"]?.Value);
|
||||
|
||||
expectedValue = GetExpectedConfigValue(builder, "superKeyVaultSetting", beforeEpoch);
|
||||
expectedValue = GetExpectedConfigValue(builder, "superKeyVaultSetting", age);
|
||||
if (expectedValue != null)
|
||||
{
|
||||
expectedCount++;
|
||||
|
@ -626,11 +733,11 @@ namespace Test
|
|||
{
|
||||
// In strict mode, we ask Azure AppConfig directly for 'casetestsetting' and get nothing since the case doesn't match. So it stays as 'untouched'.
|
||||
Assert.Equal("untouched", appSettings.Settings["caseTestSetting"]?.Value);
|
||||
Assert.Equal(GetExpectedConfigValue(builder, "testSetting", beforeEpoch) ?? "untouched", appSettings.Settings["testSetting"]?.Value);
|
||||
Assert.Equal(GetExpectedConfigValue(builder, "newTestSetting", beforeEpoch) ?? "untouched", appSettings.Settings["newTestSetting"]?.Value);
|
||||
Assert.Equal(GetExpectedConfigValue(builder, "superTestSetting", beforeEpoch) ?? "untouched", appSettings.Settings["superTestSetting"]?.Value);
|
||||
Assert.Matches(GetExpectedConfigValue(builder, "keyVaultSetting", beforeEpoch) ?? "untouched", appSettings.Settings["keyVaultSetting"]?.Value);
|
||||
Assert.Matches(GetExpectedConfigValue(builder, "superKeyVaultSetting", beforeEpoch) ?? "untouched", appSettings.Settings["superKeyVaultSetting"]?.Value);
|
||||
Assert.Equal(GetExpectedConfigValue(builder, "testSetting", age) ?? "untouched", appSettings.Settings["testSetting"]?.Value);
|
||||
Assert.Equal(GetExpectedConfigValue(builder, "newTestSetting", age) ?? "untouched", appSettings.Settings["newTestSetting"]?.Value);
|
||||
Assert.Equal(GetExpectedConfigValue(builder, "superTestSetting", age) ?? "untouched", appSettings.Settings["superTestSetting"]?.Value);
|
||||
Assert.Matches(GetExpectedConfigValue(builder, "keyVaultSetting", age) ?? "untouched", appSettings.Settings["keyVaultSetting"]?.Value);
|
||||
Assert.Matches(GetExpectedConfigValue(builder, "superKeyVaultSetting", age) ?? "untouched", appSettings.Settings["superKeyVaultSetting"]?.Value);
|
||||
|
||||
Assert.Equal(6, appSettings.Settings.Count); // No 'epochDTO' in our staged config.
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче