From f644625269079bface3421cb857a85f0089d247c Mon Sep 17 00:00:00 2001 From: Justin Yoo Date: Sat, 17 Aug 2024 01:26:52 +0900 Subject: [PATCH 1/5] Add NuGet packages to ApiApp --- src/AzureOpenAIProxy.ApiApp/AzureOpenAIProxy.ApiApp.csproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/AzureOpenAIProxy.ApiApp/AzureOpenAIProxy.ApiApp.csproj b/src/AzureOpenAIProxy.ApiApp/AzureOpenAIProxy.ApiApp.csproj index 4657650c..7e65d79c 100644 --- a/src/AzureOpenAIProxy.ApiApp/AzureOpenAIProxy.ApiApp.csproj +++ b/src/AzureOpenAIProxy.ApiApp/AzureOpenAIProxy.ApiApp.csproj @@ -11,6 +11,8 @@ + + From aeb1f340c7e3d05b4d25ac8bb5e9ebc167e3d6cf Mon Sep 17 00:00:00 2001 From: Justin Yoo Date: Sat, 17 Aug 2024 02:36:52 +0900 Subject: [PATCH 2/5] Add KeyVault client --- .../Configurations/AzureSettings.cs | 7 +++++- .../Configurations/KeyVaultSettings.cs | 22 +++++++++++++++++++ src/AzureOpenAIProxy.ApiApp/Program.cs | 18 +++++++++++++++ .../appsettings.Development.sample.json | 4 ++++ src/AzureOpenAIProxy.ApiApp/appsettings.json | 4 ++++ 5 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 src/AzureOpenAIProxy.ApiApp/Configurations/KeyVaultSettings.cs diff --git a/src/AzureOpenAIProxy.ApiApp/Configurations/AzureSettings.cs b/src/AzureOpenAIProxy.ApiApp/Configurations/AzureSettings.cs index 4fff9294..59cb53cd 100644 --- a/src/AzureOpenAIProxy.ApiApp/Configurations/AzureSettings.cs +++ b/src/AzureOpenAIProxy.ApiApp/Configurations/AzureSettings.cs @@ -14,4 +14,9 @@ public class AzureSettings /// Gets or sets the instance. /// public OpenAISettings OpenAI { get; set; } = new(); -} \ No newline at end of file + + /// + /// Gets or sets the instance. + /// + public KeyVaultSettings KeyVault { get; set; } = new(); +} diff --git a/src/AzureOpenAIProxy.ApiApp/Configurations/KeyVaultSettings.cs b/src/AzureOpenAIProxy.ApiApp/Configurations/KeyVaultSettings.cs new file mode 100644 index 00000000..80c3a51b --- /dev/null +++ b/src/AzureOpenAIProxy.ApiApp/Configurations/KeyVaultSettings.cs @@ -0,0 +1,22 @@ +namespace AzureOpenAIProxy.ApiApp.Configurations; + +/// +/// This represents the settings entity for Key Vault. +/// +public class KeyVaultSettings +{ + /// + /// Gets the name of the configuration settings. + /// + public const string Name = "KeyVault"; + + /// + /// Gets or sets the Key Vault URI. + /// + public string? VaultUri { get; set; } + + /// + /// Gets or sets the secret name. + /// + public string? SecretName { get; set; } +} \ No newline at end of file diff --git a/src/AzureOpenAIProxy.ApiApp/Program.cs b/src/AzureOpenAIProxy.ApiApp/Program.cs index 5a68a0be..17ef45e7 100644 --- a/src/AzureOpenAIProxy.ApiApp/Program.cs +++ b/src/AzureOpenAIProxy.ApiApp/Program.cs @@ -1,3 +1,7 @@ +using Azure.Identity; +using Azure.Security.KeyVault.Secrets; + +using AzureOpenAIProxy.ApiApp.Configurations; using AzureOpenAIProxy.ApiApp.Endpoints; using AzureOpenAIProxy.ApiApp.Extensions; @@ -11,6 +15,20 @@ // Add OpenAPI service builder.Services.AddOpenApiService(); +// Add KeyVault service +builder.Services.AddScoped(sp => +{ + var configuration = sp.GetService() + ?? throw new InvalidOperationException($"{nameof(IConfiguration)} service is not registered."); + + var settings = configuration.GetSection(AzureSettings.Name).GetSection(KeyVaultSettings.Name).Get() + ?? throw new InvalidOperationException($"{nameof(KeyVaultSettings)} could not be retrieved from the configuration."); + + var client = new SecretClient(new Uri(settings.VaultUri!), new DefaultAzureCredential()); + + return client; +}); + var app = builder.Build(); app.MapDefaultEndpoints(); diff --git a/src/AzureOpenAIProxy.ApiApp/appsettings.Development.sample.json b/src/AzureOpenAIProxy.ApiApp/appsettings.Development.sample.json index ca17328b..534af650 100644 --- a/src/AzureOpenAIProxy.ApiApp/appsettings.Development.sample.json +++ b/src/AzureOpenAIProxy.ApiApp/appsettings.Development.sample.json @@ -18,6 +18,10 @@ ] } ] + }, + "KeyVault": { + "VaultUri": "https://{{key-vault-name}}.vault.azure.net/", + "SecretName": "azure-openai-instances" } } } diff --git a/src/AzureOpenAIProxy.ApiApp/appsettings.json b/src/AzureOpenAIProxy.ApiApp/appsettings.json index a153d8b8..ef2a97e6 100644 --- a/src/AzureOpenAIProxy.ApiApp/appsettings.json +++ b/src/AzureOpenAIProxy.ApiApp/appsettings.json @@ -18,6 +18,10 @@ ] } ] + }, + "KeyVault": { + "VaultUri": "https://{{key-vault-name}}.vault.azure.net/", + "SecretName": "azure-openai-instances" } }, From 9aee89010bad3b3cfa28f9c1fe0037825ab8e6b0 Mon Sep 17 00:00:00 2001 From: Justin Yoo Date: Sat, 17 Aug 2024 13:54:19 +0900 Subject: [PATCH 3/5] Refactor DI for Key Vault service --- .../Extensions/ServiceCollectionExtensions.cs | 28 ++++++++++++++++++- src/AzureOpenAIProxy.ApiApp/Program.cs | 21 ++------------ 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/src/AzureOpenAIProxy.ApiApp/Extensions/ServiceCollectionExtensions.cs b/src/AzureOpenAIProxy.ApiApp/Extensions/ServiceCollectionExtensions.cs index d39a1712..9ded0ab8 100644 --- a/src/AzureOpenAIProxy.ApiApp/Extensions/ServiceCollectionExtensions.cs +++ b/src/AzureOpenAIProxy.ApiApp/Extensions/ServiceCollectionExtensions.cs @@ -1,4 +1,7 @@ -using AzureOpenAIProxy.ApiApp.Builders; +using Azure.Identity; +using Azure.Security.KeyVault.Secrets; + +using AzureOpenAIProxy.ApiApp.Builders; using AzureOpenAIProxy.ApiApp.Configurations; using AzureOpenAIProxy.ApiApp.Filters; using AzureOpenAIProxy.ApiApp.Services; @@ -12,6 +15,29 @@ namespace AzureOpenAIProxy.ApiApp.Extensions; /// public static class ServiceCollectionExtensions { + /// + /// Adds the KeyVault service to the service collection. + /// + /// instance. + /// Returns instance. + public static IServiceCollection AddKeyVaultService(this IServiceCollection services) + { + services.AddScoped(sp => + { + var configuration = sp.GetService() + ?? throw new InvalidOperationException($"{nameof(IConfiguration)} service is not registered."); + + var settings = configuration.GetSection(AzureSettings.Name).GetSection(KeyVaultSettings.Name).Get() + ?? throw new InvalidOperationException($"{nameof(KeyVaultSettings)} could not be retrieved from the configuration."); + + var client = new SecretClient(new Uri(settings.VaultUri!), new DefaultAzureCredential()); + + return client; + }); + + return services; + } + /// /// Adds the OpenAI configuration settings to the service collection by reading appsettings.json. /// diff --git a/src/AzureOpenAIProxy.ApiApp/Program.cs b/src/AzureOpenAIProxy.ApiApp/Program.cs index 17ef45e7..39464608 100644 --- a/src/AzureOpenAIProxy.ApiApp/Program.cs +++ b/src/AzureOpenAIProxy.ApiApp/Program.cs @@ -1,7 +1,3 @@ -using Azure.Identity; -using Azure.Security.KeyVault.Secrets; - -using AzureOpenAIProxy.ApiApp.Configurations; using AzureOpenAIProxy.ApiApp.Endpoints; using AzureOpenAIProxy.ApiApp.Extensions; @@ -9,26 +5,15 @@ builder.AddServiceDefaults(); +// Add KeyVault service +builder.Services.AddKeyVaultService(); + // Add Azure OpenAI service. builder.Services.AddOpenAIService(); // Add OpenAPI service builder.Services.AddOpenApiService(); -// Add KeyVault service -builder.Services.AddScoped(sp => -{ - var configuration = sp.GetService() - ?? throw new InvalidOperationException($"{nameof(IConfiguration)} service is not registered."); - - var settings = configuration.GetSection(AzureSettings.Name).GetSection(KeyVaultSettings.Name).Get() - ?? throw new InvalidOperationException($"{nameof(KeyVaultSettings)} could not be retrieved from the configuration."); - - var client = new SecretClient(new Uri(settings.VaultUri!), new DefaultAzureCredential()); - - return client; -}); - var app = builder.Build(); app.MapDefaultEndpoints(); From fdfad8fe7abc17da39411c55defe57f735e13a4b Mon Sep 17 00:00:00 2001 From: Justin Yoo Date: Sat, 17 Aug 2024 13:54:26 +0900 Subject: [PATCH 4/5] Add tests --- .../ServiceCollectionExtensionsTests.cs | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 test/AzureOpenAIProxy.ApiApp.Tests/Extensions/ServiceCollectionExtensionsTests.cs diff --git a/test/AzureOpenAIProxy.ApiApp.Tests/Extensions/ServiceCollectionExtensionsTests.cs b/test/AzureOpenAIProxy.ApiApp.Tests/Extensions/ServiceCollectionExtensionsTests.cs new file mode 100644 index 00000000..2a84aff7 --- /dev/null +++ b/test/AzureOpenAIProxy.ApiApp.Tests/Extensions/ServiceCollectionExtensionsTests.cs @@ -0,0 +1,133 @@ +using Azure.Security.KeyVault.Secrets; + +using AzureOpenAIProxy.ApiApp.Extensions; + +using FluentAssertions; + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace AzureOpenAIProxy.ApiApp.Tests.Extensions; + +public class ServiceCollectionExtensionsTests +{ + [Fact] + public void Given_ServiceCollection_When_Invoked_AddKeyVaultService_Then_It_Should_Contain_SecretClient() + { + // Arrange + var services = new ServiceCollection(); + + // Act + services.AddKeyVaultService(); + + // Assert + services.SingleOrDefault(p => p.ServiceType == typeof(SecretClient)).Should().NotBeNull(); + } + + [Fact] + public void Given_ServiceCollection_When_Invoked_AddKeyVaultService_Then_It_Should_Throw_Exception() + { + // Arrange + var services = new ServiceCollection(); + services.AddKeyVaultService(); + + // Act + Action action = () => services.BuildServiceProvider().GetService(); + + // Assert + action.Should().Throw(); + } + + [Fact] + public void Given_Empty_AzureSettings_When_Invoked_AddKeyVaultService_Then_It_Should_Throw_Exception() + { + // Arrange + var services = new ServiceCollection(); + var dict = new Dictionary() + { + { "Azure", string.Empty }, + }; +#pragma warning disable CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types. + var config = new ConfigurationBuilder().AddInMemoryCollection(dict).Build(); +#pragma warning restore CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types. + services.AddSingleton(config); + services.AddKeyVaultService(); + + // Act + Action action = () => services.BuildServiceProvider().GetService(); + + // Assert + action.Should().Throw(); + } + + [Fact] + public void Given_Empty_KeyVaultSettings_When_Invoked_AddKeyVaultService_Then_It_Should_Throw_Exception() + { + // Arrange + var services = new ServiceCollection(); + var dict = new Dictionary() + { + { "Azure:KeyVault", string.Empty }, + }; +#pragma warning disable CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types. + var config = new ConfigurationBuilder().AddInMemoryCollection(dict).Build(); +#pragma warning restore CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types. + services.AddSingleton(config); + services.AddKeyVaultService(); + + // Act + Action action = () => services.BuildServiceProvider().GetService(); + + // Assert + action.Should().Throw(); + } + + [Theory] + [InlineData("http://localhost")] + public void Given_AppSettings_When_Invoked_AddKeyVaultService_Then_It_Should_Return_SecretClient(string vaultUri) + { + // Arrange + var services = new ServiceCollection(); + var dict = new Dictionary() + { + { "Azure:KeyVault:VaultUri", vaultUri }, + }; +#pragma warning disable CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types. + var config = new ConfigurationBuilder().AddInMemoryCollection(dict).Build(); +#pragma warning restore CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types. + services.AddSingleton(config); + services.AddKeyVaultService(); + + // Act + var result = services.BuildServiceProvider().GetService(); + + // Assert + result.Should().NotBeNull() + .And.BeOfType(); + } + + [Theory] + [InlineData("http://localhost")] + public void Given_AppSettings_When_Invoked_AddKeyVaultService_Then_It_Should_Return_VaultUri(string vaultUri) + { + // Arrange + var services = new ServiceCollection(); + var dict = new Dictionary() + { + { "Azure:KeyVault:VaultUri", vaultUri }, + }; +#pragma warning disable CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types. + var config = new ConfigurationBuilder().AddInMemoryCollection(dict).Build(); +#pragma warning restore CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types. + services.AddSingleton(config); + services.AddKeyVaultService(); + + var expected = new Uri(vaultUri); + + // Act + var result = services.BuildServiceProvider().GetService(); + + // Assert + result?.VaultUri.Should().BeEquivalentTo(expected); + } +} From 13ae265a1f56c368ef352f8c85b7230e4e125a83 Mon Sep 17 00:00:00 2001 From: Justin Yoo Date: Sat, 17 Aug 2024 17:13:46 +0900 Subject: [PATCH 5/5] Update GHA workflow --- .github/workflows/azure-dev.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/azure-dev.yml b/.github/workflows/azure-dev.yml index 05834032..45dc663d 100644 --- a/.github/workflows/azure-dev.yml +++ b/.github/workflows/azure-dev.yml @@ -11,8 +11,6 @@ on: permissions: id-token: write contents: read - # issues: write - # pull-requests: write jobs: build-test-deploy: