Skip to content

Commit

Permalink
Give the users the ability to have control over ConfigurationClient i…
Browse files Browse the repository at this point in the history
…nstance(s) used by the provider (#598)

* Introduced a new `AzureAppConfigurationClientFactory` class to handle the creation of `ConfigurationClient` instances

* remove clients dictionary since we will not have hits and clients are already stored in ConfigurationClientManager

* revert

* add license + remove unused usings

* ran dotnet format

* add capability of fallback to different stores

* add explicit type

* address comments

* remove scheme validation

---------

Co-authored-by: Sami Sadfa <[email protected]>
  • Loading branch information
samsadsam and SamSadfa authored Oct 25, 2024
1 parent 0afb56a commit 2ea9632
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 79 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
using Azure.Core;
using Azure.Data.AppConfiguration;
using Microsoft.Extensions.Azure;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Microsoft.Extensions.Configuration.AzureAppConfiguration
{
internal class AzureAppConfigurationClientFactory : IAzureClientFactory<ConfigurationClient>
{
private readonly ConfigurationClientOptions _clientOptions;

private readonly TokenCredential _credential;
private readonly IEnumerable<string> _connectionStrings;

public AzureAppConfigurationClientFactory(
IEnumerable<string> connectionStrings,
ConfigurationClientOptions clientOptions)
{
if (connectionStrings == null || !connectionStrings.Any())
{
throw new ArgumentNullException(nameof(connectionStrings));
}

_connectionStrings = connectionStrings;

_clientOptions = clientOptions ?? throw new ArgumentNullException(nameof(clientOptions));
}

public AzureAppConfigurationClientFactory(
TokenCredential credential,
ConfigurationClientOptions clientOptions)
{
_credential = credential ?? throw new ArgumentNullException(nameof(credential));
_clientOptions = clientOptions ?? throw new ArgumentNullException(nameof(clientOptions));
}

public ConfigurationClient CreateClient(string endpoint)
{
if (string.IsNullOrEmpty(endpoint))
{
throw new ArgumentNullException(nameof(endpoint));
}

if (!Uri.TryCreate(endpoint, UriKind.Absolute, out Uri uriResult))
{
throw new ArgumentException("Invalid host URI.");
}

if (_credential != null)
{
return new ConfigurationClient(uriResult, _credential, _clientOptions);
}

string connectionString = _connectionStrings.FirstOrDefault(cs => ConnectionStringUtils.Parse(cs, ConnectionStringUtils.EndpointSection) == endpoint);

//
// falback to the first connection string
if (connectionString == null)
{
string id = ConnectionStringUtils.Parse(_connectionStrings.First(), ConnectionStringUtils.IdSection);
string secret = ConnectionStringUtils.Parse(_connectionStrings.First(), ConnectionStringUtils.SecretSection);

connectionString = ConnectionStringUtils.Build(uriResult, id, secret);
}

return new ConfigurationClient(connectionString, _clientOptions);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
//
using Azure.Core;
using Azure.Data.AppConfiguration;
using Microsoft.Extensions.Azure;
using Microsoft.Extensions.Configuration.AzureAppConfiguration.AzureKeyVault;
using Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions;
using Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement;
Expand Down Expand Up @@ -131,6 +132,11 @@ internal IEnumerable<IKeyValueAdapter> Adapters
/// </summary>
internal StartupOptions Startup { get; set; } = new StartupOptions();

/// <summary>
/// Client factory that is responsible for creating instances of ConfigurationClient.
/// </summary>
internal IAzureClientFactory<ConfigurationClient> ClientFactory { get; private set; }

/// <summary>
/// Initializes a new instance of the <see cref="AzureAppConfigurationOptions"/> class.
/// </summary>
Expand All @@ -144,6 +150,17 @@ public AzureAppConfigurationOptions()
};
}

/// <summary>
/// Sets the client factory used to create ConfigurationClient instances.
/// </summary>
/// <param name="factory">The client factory.</param>
/// <returns>The current <see cref="AzureAppConfigurationOptions"/> instance.</returns>
public AzureAppConfigurationOptions SetClientFactory(IAzureClientFactory<ConfigurationClient> factory)
{
ClientFactory = factory ?? throw new ArgumentNullException(nameof(factory));
return this;
}

/// <summary>
/// Specify what key-values to include in the configuration provider.
/// <see cref="Select"/> can be called multiple times to include multiple sets of key-values.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
using Azure.Data.AppConfiguration;
using Microsoft.Extensions.Azure;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Microsoft.Extensions.Configuration.AzureAppConfiguration
{
Expand Down Expand Up @@ -29,35 +33,33 @@ public IConfigurationProvider Build(IConfigurationBuilder builder)
try
{
AzureAppConfigurationOptions options = _optionsProvider();
IConfigurationClientManager clientManager;

if (options.ClientManager != null)
{
clientManager = options.ClientManager;
return new AzureAppConfigurationProvider(options.ClientManager, options, _optional);
}
else if (options.ConnectionStrings != null)

IEnumerable<Uri> endpoints;
IAzureClientFactory<ConfigurationClient> clientFactory = options.ClientFactory;

if (options.ConnectionStrings != null)
{
clientManager = new ConfigurationClientManager(
options.ConnectionStrings,
options.ClientOptions,
options.ReplicaDiscoveryEnabled,
options.LoadBalancingEnabled);
endpoints = options.ConnectionStrings.Select(cs => new Uri(ConnectionStringUtils.Parse(cs, ConnectionStringUtils.EndpointSection)));

clientFactory ??= new AzureAppConfigurationClientFactory(options.ConnectionStrings, options.ClientOptions);
}
else if (options.Endpoints != null && options.Credential != null)
{
clientManager = new ConfigurationClientManager(
options.Endpoints,
options.Credential,
options.ClientOptions,
options.ReplicaDiscoveryEnabled,
options.LoadBalancingEnabled);
endpoints = options.Endpoints;

clientFactory ??= new AzureAppConfigurationClientFactory(options.Credential, options.ClientOptions);
}
else
{
throw new ArgumentException($"Please call {nameof(AzureAppConfigurationOptions)}.{nameof(AzureAppConfigurationOptions.Connect)} to specify how to connect to Azure App Configuration.");
}

provider = new AzureAppConfigurationProvider(clientManager, options, _optional);
provider = new AzureAppConfigurationProvider(new ConfigurationClientManager(clientFactory, endpoints, options.ReplicaDiscoveryEnabled, options.LoadBalancingEnabled), options, _optional);
}
catch (InvalidOperationException ex) // InvalidOperationException is thrown when any problems are found while configuring AzureAppConfigurationOptions or when SDK fails to create a configurationClient.
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
// Licensed under the MIT license.
//

using Azure.Core;
using Azure.Data.AppConfiguration;
using DnsClient;
using DnsClient.Protocol;
using Microsoft.Extensions.Azure;
using Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions;
using System;
using System.Collections.Generic;
Expand All @@ -26,12 +26,11 @@ namespace Microsoft.Extensions.Configuration.AzureAppConfiguration
/// </remarks>
internal class ConfigurationClientManager : IConfigurationClientManager, IDisposable
{
private readonly IAzureClientFactory<ConfigurationClient> _clientFactory;
private readonly IList<ConfigurationClientWrapper> _clients;

private readonly Uri _endpoint;
private readonly string _secret;
private readonly string _id;
private readonly TokenCredential _credential;
private readonly ConfigurationClientOptions _clientOptions;

private readonly bool _replicaDiscoveryEnabled;
private readonly SrvLookupClient _srvLookupClient;
private readonly string _validDomain;
Expand All @@ -52,61 +51,20 @@ internal class ConfigurationClientManager : IConfigurationClientManager, IDispos
internal int RefreshClientsCalled { get; set; } = 0;

public ConfigurationClientManager(
IEnumerable<string> connectionStrings,
ConfigurationClientOptions clientOptions,
bool replicaDiscoveryEnabled,
bool loadBalancingEnabled)
{
if (connectionStrings == null || !connectionStrings.Any())
{
throw new ArgumentNullException(nameof(connectionStrings));
}

string connectionString = connectionStrings.First();
_endpoint = new Uri(ConnectionStringUtils.Parse(connectionString, ConnectionStringUtils.EndpointSection));
_secret = ConnectionStringUtils.Parse(connectionString, ConnectionStringUtils.SecretSection);
_id = ConnectionStringUtils.Parse(connectionString, ConnectionStringUtils.IdSection);
_clientOptions = clientOptions;
_replicaDiscoveryEnabled = replicaDiscoveryEnabled;

// If load balancing is enabled, shuffle the passed in connection strings to randomize the endpoint used on startup
if (loadBalancingEnabled)
{
connectionStrings = connectionStrings.ToList().Shuffle();
}

_validDomain = GetValidDomain(_endpoint);
_srvLookupClient = new SrvLookupClient();

_clients = connectionStrings
.Select(cs =>
{
var endpoint = new Uri(ConnectionStringUtils.Parse(cs, ConnectionStringUtils.EndpointSection));
return new ConfigurationClientWrapper(endpoint, new ConfigurationClient(cs, _clientOptions));
})
.ToList();
}

public ConfigurationClientManager(
IAzureClientFactory<ConfigurationClient> clientFactory,
IEnumerable<Uri> endpoints,
TokenCredential credential,
ConfigurationClientOptions clientOptions,
bool replicaDiscoveryEnabled,
bool loadBalancingEnabled)
{
_clientFactory = clientFactory ?? throw new ArgumentNullException(nameof(clientFactory));

if (endpoints == null || !endpoints.Any())
{
throw new ArgumentNullException(nameof(endpoints));
}

if (credential == null)
{
throw new ArgumentNullException(nameof(credential));
}

_endpoint = endpoints.First();
_credential = credential;
_clientOptions = clientOptions;

_replicaDiscoveryEnabled = replicaDiscoveryEnabled;

// If load balancing is enabled, shuffle the passed in endpoints to randomize the endpoint used on startup
Expand All @@ -119,7 +77,7 @@ public ConfigurationClientManager(
_srvLookupClient = new SrvLookupClient();

_clients = endpoints
.Select(endpoint => new ConfigurationClientWrapper(endpoint, new ConfigurationClient(endpoint, _credential, _clientOptions)))
.Select(endpoint => new ConfigurationClientWrapper(endpoint, clientFactory.CreateClient(endpoint.AbsoluteUri)))
.ToList();
}

Expand Down Expand Up @@ -289,9 +247,7 @@ private async Task RefreshFallbackClients(CancellationToken cancellationToken)
{
var targetEndpoint = new Uri($"https://{host}");

var configClient = _credential == null
? new ConfigurationClient(ConnectionStringUtils.Build(targetEndpoint, _id, _secret), _clientOptions)
: new ConfigurationClient(targetEndpoint, _credential, _clientOptions);
ConfigurationClient configClient = _clientFactory.CreateClient(targetEndpoint.AbsoluteUri);

newDynamicClients.Add(new ConfigurationClientWrapper(targetEndpoint, configClient));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<PackageReference Include="Azure.Messaging.EventGrid" Version="4.7.0" />
<PackageReference Include="Azure.Security.KeyVault.Secrets" Version="4.6.0" />
<PackageReference Include="DnsClient" Version="1.7.0" />
<PackageReference Include="Microsoft.Extensions.Azure" Version="1.7.6" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
Expand Down
19 changes: 9 additions & 10 deletions tests/Tests.AzureAppConfiguration/FailoverTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -268,10 +268,11 @@ public void FailOverTests_AutoFailover()
[Fact]
public void FailOverTests_ValidateEndpoints()
{
var clientFactory = new AzureAppConfigurationClientFactory(new DefaultAzureCredential(), new ConfigurationClientOptions());

var configClientManager = new ConfigurationClientManager(
clientFactory,
new[] { new Uri("https://foobar.azconfig.io") },
new DefaultAzureCredential(),
new ConfigurationClientOptions(),
true,
false);

Expand All @@ -285,9 +286,8 @@ public void FailOverTests_ValidateEndpoints()
Assert.False(configClientManager.IsValidEndpoint("azure.azconfig.bad.io"));

var configClientManager2 = new ConfigurationClientManager(
clientFactory,
new[] { new Uri("https://foobar.appconfig.azure.com") },
new DefaultAzureCredential(),
new ConfigurationClientOptions(),
true,
false);

Expand All @@ -301,19 +301,17 @@ public void FailOverTests_ValidateEndpoints()
Assert.False(configClientManager2.IsValidEndpoint("azure.appconfigbad.azure.com"));

var configClientManager3 = new ConfigurationClientManager(
clientFactory,
new[] { new Uri("https://foobar.azconfig-test.io") },
new DefaultAzureCredential(),
new ConfigurationClientOptions(),
true,
false);

Assert.False(configClientManager3.IsValidEndpoint("azure.azconfig-test.io"));
Assert.False(configClientManager3.IsValidEndpoint("azure.azconfig.io"));

var configClientManager4 = new ConfigurationClientManager(
clientFactory,
new[] { new Uri("https://foobar.z1.appconfig-test.azure.com") },
new DefaultAzureCredential(),
new ConfigurationClientOptions(),
true,
false);

Expand All @@ -325,10 +323,11 @@ public void FailOverTests_ValidateEndpoints()
[Fact]
public void FailOverTests_GetNoDynamicClient()
{
var clientFactory = new AzureAppConfigurationClientFactory(new DefaultAzureCredential(), new ConfigurationClientOptions());

var configClientManager = new ConfigurationClientManager(
clientFactory,
new[] { new Uri("https://azure.azconfig.io") },
new DefaultAzureCredential(),
new ConfigurationClientOptions(),
true,
false);

Expand Down

0 comments on commit 2ea9632

Please sign in to comment.