diff --git a/playground/TestShop/AppHost/ParameterExtensions.cs b/playground/TestShop/AppHost/ParameterExtensions.cs new file mode 100644 index 0000000000..8be0b6778e --- /dev/null +++ b/playground/TestShop/AppHost/ParameterExtensions.cs @@ -0,0 +1,117 @@ +using System.Diagnostics; +using System.Reflection; +using Aspire.Hosting.Publishing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.UserSecrets; +using Microsoft.Extensions.Hosting; + +namespace Aspire.Hosting; + +internal static class ParameterExtensions +{ + /// + /// Creates a password parameter that in the development environment is generated once and stored in the user secrets store. + /// + /// + /// The password is only stable when in the development environment and the application is running. In all other cases, the password is generated each time. + /// + public static IResourceBuilder CreateStablePassword(this IDistributedApplicationBuilder builder, string name, + bool lower = true, bool upper = true, bool numeric = true, bool special = true, + int minLower = 0, int minUpper = 0, int minNumeric = 0, int minSpecial = 0) + { + ParameterDefault generatedPassword = new GenerateParameterDefault + { + MinLength = 22, // enough to give 128 bits of entropy when using the default 67 possible characters. See remarks in PasswordGenerator.Generate + Lower = lower, + Upper = upper, + Numeric = numeric, + Special = special, + MinLower = minLower, + MinUpper = minUpper, + MinNumeric = minNumeric, + MinSpecial = minSpecial + }; + + if (builder.Environment.IsDevelopment() && builder.ExecutionContext.IsRunMode) + { + // In development mode, generate a new password each time the application starts + generatedPassword = new UserSecretsParameterDefault(builder.Environment.ApplicationName, name, generatedPassword); + } + + var parameterResource = new ParameterResource(name, parameterDefault => GetParameterValue(builder.Configuration, name, parameterDefault), true) + { + Default = generatedPassword + }; + + return ResourceBuilder.Create(parameterResource, builder); + } + + private static string GetParameterValue(IConfiguration configuration, string name, ParameterDefault? parameterDefault) + { + var configurationKey = $"Parameters:{name}"; + return configuration[configurationKey] + ?? parameterDefault?.GetDefaultValue() + ?? throw new DistributedApplicationException($"Parameter resource could not be used because configuration key '{configurationKey}' is missing and the Parameter has no default value."); ; + } + + sealed class UserSecretsParameterDefault(string applicationName, string parameterName, ParameterDefault parameterDefault) : ParameterDefault + { + public override string GetDefaultValue() + { + var value = parameterDefault.GetDefaultValue(); + var configurationKey = $"Parameters:{parameterName}"; + TrySetUserSecret(applicationName, configurationKey, value); + return value; + } + + public override void WriteToManifest(ManifestPublishingContext context) => parameterDefault.WriteToManifest(context); + + private static bool TrySetUserSecret(string applicationName, string name, string value) + { + if (!string.IsNullOrEmpty(applicationName)) + { + var appAssembly = Assembly.Load(new AssemblyName(applicationName)); + if (appAssembly is not null && appAssembly.GetCustomAttribute()?.UserSecretsId is { } userSecretsId) + { + // Save the value to the secret store + try + { + var startInfo = new ProcessStartInfo + { + FileName = "dotnet", + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden + }; + new List(["user-secrets", "set", name, value, "--id", userSecretsId]).ForEach(startInfo.ArgumentList.Add); + var setUserSecrets = Process.Start(startInfo); + setUserSecrets?.WaitForExit(TimeSpan.FromSeconds(10)); + return setUserSecrets?.ExitCode == 0; + } + catch (Exception) { } + } + } + + return false; + } + } + + sealed class ResourceBuilder + { + public static IResourceBuilder Create(T resource, IDistributedApplicationBuilder distributedApplicationBuilder) where T : IResource + { + return new ResourceBuilder(resource, distributedApplicationBuilder); + } + } + + sealed class ResourceBuilder(T resource, IDistributedApplicationBuilder distributedApplicationBuilder) : IResourceBuilder where T : IResource + { + public IDistributedApplicationBuilder ApplicationBuilder { get; } = distributedApplicationBuilder; + + public T Resource { get; } = resource; + + public IResourceBuilder WithAnnotation(TAnnotation annotation, ResourceAnnotationMutationBehavior behavior = ResourceAnnotationMutationBehavior.Append) where TAnnotation : IResourceAnnotation + { + throw new NotImplementedException(); + } + } +} diff --git a/playground/TestShop/AppHost/Program.cs b/playground/TestShop/AppHost/Program.cs index 586c121b6c..4cda81ba0b 100644 --- a/playground/TestShop/AppHost/Program.cs +++ b/playground/TestShop/AppHost/Program.cs @@ -12,8 +12,7 @@ .WithReference(catalogDb) .WithReplicas(2); -var rabbitMqPassword = builder.AddParameter("rabbitmq-password", secret: true); -var messaging = builder.AddRabbitMQ("messaging", password: rabbitMqPassword) +var messaging = builder.AddRabbitMQ("messaging", password: builder.CreateStablePassword("rabbitmq-password", special: false)) .WithDataVolume() .WithManagementPlugin() .PublishAsContainer();