Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Give the users the ability to have control over ConfigurationClient instance(s) used by the provider #598

Merged
merged 9 commits into from
Oct 25, 2024
Merged
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;
zhiyuanliang-ms marked this conversation as resolved.
Show resolved Hide resolved
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)
samsadsam marked this conversation as resolved.
Show resolved Hide resolved
{
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
Loading