| | | 1 | | namespace Envilder.Infrastructure; |
| | | 2 | | |
| | | 3 | | using Amazon; |
| | | 4 | | using Amazon.Runtime.CredentialManagement; |
| | | 5 | | using Amazon.SimpleSystemsManagement; |
| | | 6 | | using Envilder.Domain; |
| | | 7 | | using Envilder.Domain.Ports; |
| | | 8 | | using Envilder.Infrastructure.Aws; |
| | | 9 | | using Envilder.Infrastructure.Azure; |
| | | 10 | | using global::Azure.Identity; |
| | | 11 | | using global::Azure.Security.KeyVault.Secrets; |
| | | 12 | | using System; |
| | | 13 | | |
| | | 14 | | /// <summary> |
| | | 15 | | /// Creates the appropriate <see cref="ISecretProvider"/> implementation |
| | | 16 | | /// based on the map file configuration and optional runtime overrides. |
| | | 17 | | /// </summary> |
| | | 18 | | public static class SecretProviderFactory |
| | | 19 | | { |
| | 1 | 20 | | private static readonly RegionEndpoint FallbackRegion = RegionEndpoint.USEast1; |
| | | 21 | | |
| | | 22 | | /// <summary> |
| | | 23 | | /// Creates an <see cref="ISecretProvider"/> for the provider specified in |
| | | 24 | | /// <paramref name="config"/>. When <paramref name="options"/> is provided, |
| | | 25 | | /// its values take precedence over <paramref name="config"/>. |
| | | 26 | | /// </summary> |
| | | 27 | | /// <param name="config">Configuration from the <c>$config</c> section of a map file.</param> |
| | | 28 | | /// <param name="options">Optional runtime overrides (e.g. CLI flags).</param> |
| | | 29 | | /// <returns>A ready-to-use secret provider.</returns> |
| | | 30 | | /// <exception cref="InvalidOperationException"> |
| | | 31 | | /// Thrown when Azure is selected but no Vault URL is provided. |
| | | 32 | | /// </exception> |
| | | 33 | | public static ISecretProvider Create(MapFileConfig config, EnvilderOptions? options = null) |
| | | 34 | | { |
| | 1 | 35 | | if (config is null) |
| | | 36 | | { |
| | 1 | 37 | | throw new ArgumentNullException(nameof(config)); |
| | | 38 | | } |
| | | 39 | | |
| | 1 | 40 | | var provider = options?.Provider ?? config.Provider; |
| | | 41 | | |
| | 1 | 42 | | return provider switch |
| | 1 | 43 | | { |
| | 1 | 44 | | SecretProviderType.Azure => CreateAzureSecretProvider(config, options), |
| | 1 | 45 | | _ => CreateAwsSecretProvider(config, options), |
| | 1 | 46 | | }; |
| | | 47 | | } |
| | | 48 | | |
| | | 49 | | private static AzureKeyVaultSecretProvider CreateAzureSecretProvider(MapFileConfig config, EnvilderOptions? options) |
| | | 50 | | { |
| | 1 | 51 | | var vaultUrl = options?.VaultUrl ?? config.VaultUrl; |
| | | 52 | | |
| | 1 | 53 | | if (string.IsNullOrWhiteSpace(vaultUrl)) |
| | | 54 | | { |
| | 1 | 55 | | throw new InvalidOperationException("Vault URL must be provided for Azure Key Vault provider."); |
| | | 56 | | } |
| | | 57 | | |
| | 1 | 58 | | var secretClient = new SecretClient(new Uri(vaultUrl), new DefaultAzureCredential()); |
| | 1 | 59 | | return new(secretClient); |
| | | 60 | | } |
| | | 61 | | |
| | | 62 | | private static AwsSsmSecretProvider CreateAwsSecretProvider(MapFileConfig config, EnvilderOptions? options) |
| | | 63 | | { |
| | 1 | 64 | | var profile = options?.Profile ?? config.Profile; |
| | | 65 | | |
| | 1 | 66 | | if (!string.IsNullOrWhiteSpace(profile)) |
| | | 67 | | { |
| | 1 | 68 | | var chain = new CredentialProfileStoreChain(); |
| | 1 | 69 | | if (chain.TryGetAWSCredentials(profile, out var credentials)) |
| | | 70 | | { |
| | 0 | 71 | | var region = ResolveProfileRegion(chain, profile!); |
| | 0 | 72 | | return new(new AmazonSimpleSystemsManagementClient(credentials, region)); |
| | | 73 | | } |
| | | 74 | | |
| | 1 | 75 | | throw new InvalidOperationException( |
| | 1 | 76 | | $"AWS profile '{profile}' was not found in the credential store."); |
| | | 77 | | } |
| | | 78 | | |
| | 1 | 79 | | return new(new AmazonSimpleSystemsManagementClient(new AmazonSimpleSystemsManagementConfig |
| | 1 | 80 | | { |
| | 1 | 81 | | RegionEndpoint = ResolveRegion(), |
| | 1 | 82 | | })); |
| | | 83 | | } |
| | | 84 | | |
| | | 85 | | private static RegionEndpoint ResolveProfileRegion(CredentialProfileStoreChain chain, string profile) |
| | | 86 | | { |
| | 0 | 87 | | if (chain.TryGetProfile(profile, out var credentialProfile) && credentialProfile.Region != null) |
| | | 88 | | { |
| | 0 | 89 | | return credentialProfile.Region; |
| | | 90 | | } |
| | | 91 | | |
| | 0 | 92 | | return ResolveRegion(); |
| | | 93 | | } |
| | | 94 | | |
| | | 95 | | /// <summary> |
| | | 96 | | /// Resolves the AWS region from environment variables (<c>AWS_REGION</c> or |
| | | 97 | | /// <c>AWS_DEFAULT_REGION</c>), falling back to <c>us-east-1</c> when neither is set. |
| | | 98 | | /// </summary> |
| | | 99 | | private static RegionEndpoint ResolveRegion() |
| | | 100 | | { |
| | 1 | 101 | | var regionName = Environment.GetEnvironmentVariable("AWS_REGION") |
| | 1 | 102 | | ?? Environment.GetEnvironmentVariable("AWS_DEFAULT_REGION"); |
| | | 103 | | |
| | 1 | 104 | | return string.IsNullOrWhiteSpace(regionName) |
| | 1 | 105 | | ? FallbackRegion |
| | 1 | 106 | | : RegionEndpoint.GetBySystemName(regionName); |
| | | 107 | | } |
| | | 108 | | } |