diff --git a/src/Aspire.Hosting.Docker/DockerComposeEnvironmentContext.cs b/src/Aspire.Hosting.Docker/DockerComposeEnvironmentContext.cs index 53aa0379b9c..a90fb60739d 100644 --- a/src/Aspire.Hosting.Docker/DockerComposeEnvironmentContext.cs +++ b/src/Aspire.Hosting.Docker/DockerComposeEnvironmentContext.cs @@ -141,9 +141,4 @@ private async Task ProcessArgumentsAsync(DockerComposeServiceResource serviceRes } } } - - public void AddEnv(string name, string description, string? defaultValue = null) - { - environment.CapturedEnvironmentVariables[name] = (description, defaultValue); - } } \ No newline at end of file diff --git a/src/Aspire.Hosting.Docker/DockerComposeEnvironmentResource.cs b/src/Aspire.Hosting.Docker/DockerComposeEnvironmentResource.cs index 1770145ec5c..f4cd8f70caf 100644 --- a/src/Aspire.Hosting.Docker/DockerComposeEnvironmentResource.cs +++ b/src/Aspire.Hosting.Docker/DockerComposeEnvironmentResource.cs @@ -39,7 +39,7 @@ public class DockerComposeEnvironmentResource : Resource, IComputeEnvironmentRes /// Gets the collection of environment variables captured from the Docker Compose environment. /// These will be populated into a top-level .env file adjacent to the Docker Compose file. /// - internal Dictionary CapturedEnvironmentVariables { get; } = []; + internal Dictionary CapturedEnvironmentVariables { get; } = []; /// The name of the Docker Compose environment. public DockerComposeEnvironmentResource(string name) : base(name) @@ -62,4 +62,11 @@ private Task PublishAsync(PublishingContext context) return dockerComposePublishingContext.WriteModelAsync(context.Model, this); } + + internal string AddEnvironmentVariable(string name, string? description = null, string? defaultValue = null, object? source = null) + { + CapturedEnvironmentVariables[name] = (description, defaultValue, source); + + return $"${{{name}}}"; + } } diff --git a/src/Aspire.Hosting.Docker/DockerComposePublishingContext.cs b/src/Aspire.Hosting.Docker/DockerComposePublishingContext.cs index 330dc2fe8cb..1846d8a6e11 100644 --- a/src/Aspire.Hosting.Docker/DockerComposePublishingContext.cs +++ b/src/Aspire.Hosting.Docker/DockerComposePublishingContext.cs @@ -116,7 +116,7 @@ private async Task WriteDockerComposeOutputAsync(DistributedApplicationModel mod foreach (var entry in environment.CapturedEnvironmentVariables ?? []) { - var (key, (description, defaultValue)) = entry; + var (key, (description, defaultValue, _)) = entry; envFile.AddIfMissing(key, defaultValue, description); } diff --git a/src/Aspire.Hosting.Docker/DockerComposeServiceExtensions.cs b/src/Aspire.Hosting.Docker/DockerComposeServiceExtensions.cs index 0fbe0055a42..93a8643e78e 100644 --- a/src/Aspire.Hosting.Docker/DockerComposeServiceExtensions.cs +++ b/src/Aspire.Hosting.Docker/DockerComposeServiceExtensions.cs @@ -48,4 +48,58 @@ public static IResourceBuilder PublishAsDockerComposeService(this IResourc return builder; } + + /// + /// Creates a placeholder for an environment variable in the Docker Compose file. + /// + /// The manifest expression provider. + /// The Docker Compose service resource to associate the environment variable with. + /// A string representing the environment variable placeholder in Docker Compose syntax (e.g., ${ENV_VAR}). + public static string AsEnvironmentPlaceholder(this IManifestExpressionProvider manifestExpressionProvider, DockerComposeServiceResource dockerComposeService) + { + var env = manifestExpressionProvider.ValueExpression.Replace("{", "") + .Replace("}", "") + .Replace(".", "_") + .Replace("-", "_") + .ToUpperInvariant(); + + return dockerComposeService.Parent.AddEnvironmentVariable( + env, + source: manifestExpressionProvider + ); + } + + /// + /// Creates a Docker Compose environment variable placeholder for the specified . + /// + /// The resource builder for the parameter resource. + /// The Docker Compose service resource to associate the environment variable with. + /// A string representing the environment variable placeholder in Docker Compose syntax (e.g., ${ENV_VAR}). + public static string AsEnvironmentPlaceholder(this IResourceBuilder builder, DockerComposeServiceResource dockerComposeService) + { + return builder.Resource.AsEnvironmentPlaceholder(dockerComposeService); + } + + /// + /// Creates a Docker Compose environment variable placeholder for this . + /// + /// The parameter resource for which to create the environment variable placeholder. + /// The Docker Compose service resource to associate the environment variable with. + /// A string representing the environment variable placeholder in Docker Compose syntax (e.g., ${ENV_VAR}). + public static string AsEnvironmentPlaceholder(this ParameterResource parameter, DockerComposeServiceResource dockerComposeService) + { + // Placeholder for resolving the actual parameter value + // https://docs.docker.com/compose/how-tos/environment-variables/variable-interpolation/#interpolation-syntax + + var env = parameter.Name.ToUpperInvariant().Replace("-", "_"); + + // Treat secrets as environment variable placeholders as for now + // this doesn't handle generation of parameter values with defaults + return dockerComposeService.Parent.AddEnvironmentVariable( + env, + description: $"Parameter {parameter.Name}", + defaultValue: parameter.Secret || parameter.Default is null ? null : parameter.Value, + source: parameter + ); + } } diff --git a/src/Aspire.Hosting.Docker/DockerComposeServiceResource.cs b/src/Aspire.Hosting.Docker/DockerComposeServiceResource.cs index 0aa6de9e122..095b5cfe4b4 100644 --- a/src/Aspire.Hosting.Docker/DockerComposeServiceResource.cs +++ b/src/Aspire.Hosting.Docker/DockerComposeServiceResource.cs @@ -98,9 +98,12 @@ private bool TryGetContainerImageName(IResource resourceInstance, out string? co { var imageEnvName = $"{resourceInstance.Name.ToUpperInvariant().Replace("-", "_")}_IMAGE"; - composeEnvironmentResource.CapturedEnvironmentVariables.Add(imageEnvName, ($"Container image name for {resourceInstance.Name}", $"{resourceInstance.Name}:latest")); - - containerImageName = $"${{{imageEnvName}}}"; + containerImageName = composeEnvironmentResource.AddEnvironmentVariable( + imageEnvName, + description: $"Container image name for {resourceInstance.Name}", + defaultValue: $"{resourceInstance.Name}:latest", + source: new ContainerImageReference(resourceInstance) + ); return true; } diff --git a/src/Aspire.Hosting.Docker/DockerComposeServiceResourceExtensions.cs b/src/Aspire.Hosting.Docker/DockerComposeServiceResourceExtensions.cs index d34c0aa32af..94fe6cde8ae 100644 --- a/src/Aspire.Hosting.Docker/DockerComposeServiceResourceExtensions.cs +++ b/src/Aspire.Hosting.Docker/DockerComposeServiceResourceExtensions.cs @@ -32,7 +32,7 @@ internal static async Task ProcessValueAsync(this DockerComposeServiceRe if (value is ParameterResource param) { - return AllocateParameter(param, context); + return param.AsEnvironmentPlaceholder(resource); } if (value is ConnectionStringReference cs) @@ -82,7 +82,7 @@ internal static async Task ProcessValueAsync(this DockerComposeServiceRe // If we don't know how to process the value, we just return it as an external reference if (value is IManifestExpressionProvider r) { - return ResolveUnknownValue(r, resource); + return r.AsEnvironmentPlaceholder(resource); } return value; // todo: we need to never get here really... @@ -107,42 +107,4 @@ string GetHostValue(string? prefix = null, string? suffix = null) return $"{prefix}{mapping.Host}{suffix}"; } } - - private static string ResolveParameterValue(ParameterResource parameter, DockerComposeEnvironmentContext context) - { - // Placeholder for resolving the actual parameter value - // https://docs.docker.com/compose/how-tos/environment-variables/variable-interpolation/#interpolation-syntax - - // Treat secrets as environment variable placeholders as for now - // this doesn't handle generation of parameter values with defaults - var env = parameter.Name.ToUpperInvariant().Replace("-", "_"); - - context.AddEnv(env, $"Parameter {parameter.Name}", - parameter.Secret || parameter.Default is null ? null : parameter.Value); - - return $"${{{env}}}"; - } - - private static string AllocateParameter(ParameterResource parameter, DockerComposeEnvironmentContext context) - { - return ResolveParameterValue(parameter, context); - } - - private static string ResolveUnknownValue(IManifestExpressionProvider parameter, DockerComposeServiceResource serviceResource) - { - // Placeholder for resolving the actual parameter value - // https://docs.docker.com/compose/how-tos/environment-variables/variable-interpolation/#interpolation-syntax - - // Treat secrets as environment variable placeholders as for now - // this doesn't handle generation of parameter values with defaults - var env = parameter.ValueExpression.Replace("{", "") - .Replace("}", "") - .Replace(".", "_") - .Replace("-", "_") - .ToUpperInvariant(); - - serviceResource.EnvironmentVariables.Add(env, $"Unknown reference {parameter.ValueExpression}"); - - return $"${{{env}}}"; - } } diff --git a/tests/Aspire.Hosting.Docker.Tests/DockerComposePublisherTests.cs b/tests/Aspire.Hosting.Docker.Tests/DockerComposePublisherTests.cs index 9b567708c98..13686ff37ee 100644 --- a/tests/Aspire.Hosting.Docker.Tests/DockerComposePublisherTests.cs +++ b/tests/Aspire.Hosting.Docker.Tests/DockerComposePublisherTests.cs @@ -137,6 +137,8 @@ public async Task DockerComposeAppliesServiceCustomizations() builder.Services.AddSingleton(); + var containerNameParam = builder.AddParameter("param-1", "default-name", publishValueAsDefault: true); + builder.AddDockerComposeEnvironment("docker-compose") .WithProperties(e => e.DefaultNetworkName = "default-network") .ConfigureComposeFile(file => @@ -160,6 +162,8 @@ public async Task DockerComposeAppliesServiceCustomizations() // Set a restart policy composeService.Restart = "always"; + composeService.ContainerName = containerNameParam.AsEnvironmentPlaceholder(serviceResource); + // Add a custom network composeService.Networks.Add("custom-network"); }); @@ -167,11 +171,15 @@ public async Task DockerComposeAppliesServiceCustomizations() var app = builder.Build(); app.Run(); + // Assert var composePath = Path.Combine(tempDir.Path, "docker-compose.yaml"); Assert.True(File.Exists(composePath)); + var envPath = Path.Combine(tempDir.Path, ".env"); + Assert.True(File.Exists(envPath)); await Verify(File.ReadAllText(composePath), "yaml") + .AppendContentAsFile(File.ReadAllText(envPath), "env") .UseHelixAwareDirectory(); } diff --git a/tests/Aspire.Hosting.Docker.Tests/Snapshots/DockerComposePublisherTests.DockerComposeAppliesServiceCustomizations.verified.env b/tests/Aspire.Hosting.Docker.Tests/Snapshots/DockerComposePublisherTests.DockerComposeAppliesServiceCustomizations.verified.env new file mode 100644 index 00000000000..63d7826c174 --- /dev/null +++ b/tests/Aspire.Hosting.Docker.Tests/Snapshots/DockerComposePublisherTests.DockerComposeAppliesServiceCustomizations.verified.env @@ -0,0 +1,3 @@ +# Parameter param-1 +PARAM_1=default-name + diff --git a/tests/Aspire.Hosting.Docker.Tests/Snapshots/DockerComposePublisherTests.DockerComposeAppliesServiceCustomizations.verified.yaml b/tests/Aspire.Hosting.Docker.Tests/Snapshots/DockerComposePublisherTests.DockerComposeAppliesServiceCustomizations.verified.yaml index 7f24b6dddc7..4402c591051 100644 --- a/tests/Aspire.Hosting.Docker.Tests/Snapshots/DockerComposePublisherTests.DockerComposeAppliesServiceCustomizations.verified.yaml +++ b/tests/Aspire.Hosting.Docker.Tests/Snapshots/DockerComposePublisherTests.DockerComposeAppliesServiceCustomizations.verified.yaml @@ -2,6 +2,7 @@ services: service: image: "nginx:latest" + container_name: "${PARAM_1}" environment: ORIGINAL_ENV: "value" CUSTOM_ENV: "custom-value"