Add ability to use AppConfig snapshots. (#249)

This commit is contained in:
Steve Molloy 2024-10-11 11:14:56 -07:00 коммит произвёл GitHub
Родитель 58e788a30b
Коммит aca6c53bab
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
3 изменённых файлов: 278 добавлений и 133 удалений

Просмотреть файл

@ -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.
}