Skip to content

Commit eaf7c93

Browse files
authored
Add WithKeyVault method (#228)
1 parent 9cdde7c commit eaf7c93

File tree

5 files changed

+182
-10
lines changed

5 files changed

+182
-10
lines changed

.github/workflows/azure-dev-build-only.yml

+1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ jobs:
5858
dotnet test --logger "trx" --collect:"XPlat Code Coverage"
5959
6060
- name: Publish test results
61+
if: ${{ !cancelled() }}
6162
id: test-report
6263
uses: bibipkins/dotnet-test-reporter@main
6364
with:

src/AzureOpenAIProxy.ApiApp/Extensions/OpenAISettingsBuilderExtensions.cs

+31-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
using AzureOpenAIProxy.ApiApp.Builders;
1+
using System.Text.Json;
2+
3+
using Azure.Security.KeyVault.Secrets;
4+
5+
using AzureOpenAIProxy.ApiApp.Builders;
26
using AzureOpenAIProxy.ApiApp.Configurations;
37

48
namespace AzureOpenAIProxy.ApiApp.Extensions;
@@ -26,4 +30,30 @@ public static IOpenAISettingsBuilder WithAppSettings(this IOpenAISettingsBuilder
2630

2731
return builder;
2832
}
33+
34+
/// <summary>
35+
/// Sets the list of <see cref="OpenAIInstanceSettings"/> instances from Key Vault.
36+
/// </summary>
37+
/// <param name="builder"><see cref="IOpenAISettingsBuilder"/> instance.</param>
38+
/// <param name="sp"><see cref="IServiceProvider"/> instance.</param>
39+
/// <returns>Returns <see cref="IOpenAISettingsBuilder"/> instance.</returns>
40+
public static IOpenAISettingsBuilder WithKeyVault(this IOpenAISettingsBuilder builder, IServiceProvider sp)
41+
{
42+
var configuration = sp.GetService<IConfiguration>()
43+
?? throw new InvalidOperationException($"{nameof(IConfiguration)} service is not registered.");
44+
45+
var settings = configuration.GetSection(AzureSettings.Name).GetSection(KeyVaultSettings.Name).Get<KeyVaultSettings>()
46+
?? throw new InvalidOperationException($"{nameof(KeyVaultSettings)} could not be retrieved from the configuration.");
47+
48+
var client = sp.GetService<SecretClient>()
49+
?? throw new InvalidOperationException($"{nameof(SecretClient)} service is not registered.");
50+
51+
var value = client.GetSecret(settings.SecretName!).Value.Value;
52+
53+
var instances = JsonSerializer.Deserialize<List<OpenAIInstanceSettings>>(value);
54+
55+
builder.SetOpenAIInstances(instances);
56+
57+
return builder;
58+
}
2959
}

src/AzureOpenAIProxy.ApiApp/Extensions/ServiceCollectionExtensions.cs

+14-4
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,25 @@ public static class ServiceCollectionExtensions
2222
/// <returns>Returns <see cref="IServiceCollection"/> instance.</returns>
2323
public static IServiceCollection AddKeyVaultService(this IServiceCollection services)
2424
{
25-
services.AddScoped<SecretClient>(sp =>
25+
services.AddSingleton<SecretClient>(sp =>
2626
{
2727
var configuration = sp.GetService<IConfiguration>()
2828
?? throw new InvalidOperationException($"{nameof(IConfiguration)} service is not registered.");
2929

3030
var settings = configuration.GetSection(AzureSettings.Name).GetSection(KeyVaultSettings.Name).Get<KeyVaultSettings>()
3131
?? throw new InvalidOperationException($"{nameof(KeyVaultSettings)} could not be retrieved from the configuration.");
3232

33-
var client = new SecretClient(new Uri(settings.VaultUri!), new DefaultAzureCredential());
33+
if (string.IsNullOrWhiteSpace(settings.VaultUri) == true)
34+
{
35+
throw new InvalidOperationException($"{nameof(KeyVaultSettings.VaultUri)} is not defined.");
36+
}
37+
38+
if (string.IsNullOrWhiteSpace(settings.SecretName) == true)
39+
{
40+
throw new InvalidOperationException($"{nameof(KeyVaultSettings.SecretName)} is not defined.");
41+
}
42+
43+
var client = new SecretClient(new Uri(settings.VaultUri), new DefaultAzureCredential());
3444

3545
return client;
3646
});
@@ -48,8 +58,8 @@ public static IServiceCollection AddOpenAISettings(this IServiceCollection servi
4858
services.AddSingleton<OpenAISettings>(sp =>
4959
{
5060
var settings = new OpenAISettingsBuilder()
51-
.WithAppSettings(sp)
52-
//.WithKeyVault(sp)
61+
//.WithAppSettings(sp)
62+
.WithKeyVault(sp)
5363
.Build();
5464

5565
return settings;

test/AzureOpenAIProxy.ApiApp.Tests/Extensions/OpenAISettingsBuilderExtensionsTests.cs

+56-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
using AzureOpenAIProxy.ApiApp.Builders;
1+
using Azure.Identity;
2+
using Azure.Security.KeyVault.Secrets;
3+
4+
using AzureOpenAIProxy.ApiApp.Builders;
25
using AzureOpenAIProxy.ApiApp.Extensions;
36

47
using FluentAssertions;
@@ -217,4 +220,56 @@ public void Given_AppSettings_When_Invoked_WithAppSettings_Then_It_Should_Return
217220
result.Instances.First().DeploymentNames.Should().Contain(dn);
218221
}
219222
}
223+
224+
[Fact]
225+
public void Given_Empty_KeyVaultSettings_When_Invoked_WithKeyVault_Then_It_Should_Throw_Exception()
226+
{
227+
// Arrange
228+
var dict = new Dictionary<string, string>()
229+
{
230+
{ "Azure:KeyVault", string.Empty }
231+
};
232+
#pragma warning disable CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
233+
var config = new ConfigurationBuilder().AddInMemoryCollection(dict).Build();
234+
#pragma warning restore CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
235+
236+
var sp = Substitute.For<IServiceProvider>();
237+
ServiceProviderServiceExtensions.GetService<IConfiguration>(sp).Returns(config);
238+
239+
var builder = new OpenAISettingsBuilder();
240+
241+
// Act
242+
Action action = () => builder.WithKeyVault(sp)
243+
.Build();
244+
245+
// Assert
246+
action.Should().Throw<InvalidOperationException>();
247+
}
248+
249+
[Theory]
250+
[InlineData("http://localhost")]
251+
public void Given_Null_SecretClient_When_Invoked_WithKeyVault_Then_It_Should_Throw_Exception(string vaultUri)
252+
{
253+
// Arrange
254+
var dict = new Dictionary<string, string>()
255+
{
256+
{ "Azure:KeyVault:VaultUri", vaultUri }
257+
};
258+
#pragma warning disable CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
259+
var config = new ConfigurationBuilder().AddInMemoryCollection(dict).Build();
260+
#pragma warning restore CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
261+
262+
var client = new SecretClient(new Uri(vaultUri), new DefaultAzureCredential());
263+
264+
var sp = Substitute.For<IServiceProvider>();
265+
ServiceProviderServiceExtensions.GetService<IConfiguration>(sp).Returns(config);
266+
267+
var builder = new OpenAISettingsBuilder();
268+
269+
// Act
270+
Action action = () => builder.WithKeyVault(sp).Build();
271+
272+
// Assert
273+
action.Should().Throw<InvalidOperationException>();
274+
}
220275
}

test/AzureOpenAIProxy.ApiApp.Tests/Extensions/ServiceCollectionExtensionsTests.cs

+80-4
Original file line numberDiff line numberDiff line change
@@ -83,14 +83,89 @@ public void Given_Empty_KeyVaultSettings_When_Invoked_AddKeyVaultService_Then_It
8383
}
8484

8585
[Theory]
86-
[InlineData("http://localhost")]
87-
public void Given_AppSettings_When_Invoked_AddKeyVaultService_Then_It_Should_Return_SecretClient(string vaultUri)
86+
[InlineData(default(string), "secret-name")]
87+
[InlineData("", "secret-name")]
88+
public void Given_NullOrEmpty_VaultUri_When_Invoked_AddKeyVaultService_Then_It_Should_Throw_Exception(string? vaultUri, string secretName)
89+
{
90+
// Arrange
91+
var services = new ServiceCollection();
92+
var dict = new Dictionary<string, string>()
93+
{
94+
{ "Azure:KeyVault:VaultUri", vaultUri! },
95+
{ "Azure:KeyVault:SecretName", secretName },
96+
};
97+
#pragma warning disable CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
98+
var config = new ConfigurationBuilder().AddInMemoryCollection(dict).Build();
99+
#pragma warning restore CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
100+
services.AddSingleton<IConfiguration>(config);
101+
services.AddKeyVaultService();
102+
103+
// Act
104+
Action action = () => services.BuildServiceProvider().GetService<SecretClient>();
105+
106+
// Assert
107+
action.Should().Throw<InvalidOperationException>();
108+
}
109+
110+
[Theory]
111+
[InlineData("http://localhost", default(string))]
112+
[InlineData("http://localhost", "")]
113+
public void Given_NullOrEmpty_SecretName_When_Invoked_AddKeyVaultService_Then_It_Should_Throw_Exception(string vaultUri, string? secretName)
114+
{
115+
// Arrange
116+
var services = new ServiceCollection();
117+
var dict = new Dictionary<string, string>()
118+
{
119+
{ "Azure:KeyVault:VaultUri", vaultUri },
120+
{ "Azure:KeyVault:SecretName", secretName! },
121+
};
122+
#pragma warning disable CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
123+
var config = new ConfigurationBuilder().AddInMemoryCollection(dict).Build();
124+
#pragma warning restore CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
125+
services.AddSingleton<IConfiguration>(config);
126+
services.AddKeyVaultService();
127+
128+
// Act
129+
Action action = () => services.BuildServiceProvider().GetService<SecretClient>();
130+
131+
// Assert
132+
action.Should().Throw<InvalidOperationException>();
133+
}
134+
135+
[Theory]
136+
[InlineData("abcde", "secret-name")]
137+
public void Given_Invalid_VaultUri_When_Invoked_AddKeyVaultService_Then_It_Should_Throw_Exception(string vaultUri, string secretName)
138+
{
139+
// Arrange
140+
var services = new ServiceCollection();
141+
var dict = new Dictionary<string, string>()
142+
{
143+
{ "Azure:KeyVault:VaultUri", vaultUri },
144+
{ "Azure:KeyVault:SecretName", secretName },
145+
};
146+
#pragma warning disable CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
147+
var config = new ConfigurationBuilder().AddInMemoryCollection(dict).Build();
148+
#pragma warning restore CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
149+
services.AddSingleton<IConfiguration>(config);
150+
services.AddKeyVaultService();
151+
152+
// Act
153+
Action action = () => services.BuildServiceProvider().GetService<SecretClient>();
154+
155+
// Assert
156+
action.Should().Throw<UriFormatException>();
157+
}
158+
159+
[Theory]
160+
[InlineData("http://localhost", "secret-name")]
161+
public void Given_AppSettings_When_Invoked_AddKeyVaultService_Then_It_Should_Return_SecretClient(string vaultUri, string secretName)
88162
{
89163
// Arrange
90164
var services = new ServiceCollection();
91165
var dict = new Dictionary<string, string>()
92166
{
93167
{ "Azure:KeyVault:VaultUri", vaultUri },
168+
{ "Azure:KeyVault:SecretName", secretName },
94169
};
95170
#pragma warning disable CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
96171
var config = new ConfigurationBuilder().AddInMemoryCollection(dict).Build();
@@ -107,14 +182,15 @@ public void Given_AppSettings_When_Invoked_AddKeyVaultService_Then_It_Should_Ret
107182
}
108183

109184
[Theory]
110-
[InlineData("http://localhost")]
111-
public void Given_AppSettings_When_Invoked_AddKeyVaultService_Then_It_Should_Return_VaultUri(string vaultUri)
185+
[InlineData("http://localhost", "secret-name")]
186+
public void Given_AppSettings_When_Invoked_AddKeyVaultService_Then_It_Should_Return_VaultUri(string vaultUri, string secretName)
112187
{
113188
// Arrange
114189
var services = new ServiceCollection();
115190
var dict = new Dictionary<string, string>()
116191
{
117192
{ "Azure:KeyVault:VaultUri", vaultUri },
193+
{ "Azure:KeyVault:SecretName", secretName },
118194
};
119195
#pragma warning disable CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
120196
var config = new ConfigurationBuilder().AddInMemoryCollection(dict).Build();

0 commit comments

Comments
 (0)