diff --git a/src/Aspire.Hosting.Docker/Aspire.Hosting.Docker.csproj b/src/Aspire.Hosting.Docker/Aspire.Hosting.Docker.csproj index c0db0e8d78a..80489508689 100644 --- a/src/Aspire.Hosting.Docker/Aspire.Hosting.Docker.csproj +++ b/src/Aspire.Hosting.Docker/Aspire.Hosting.Docker.csproj @@ -11,6 +11,9 @@ + + + diff --git a/src/Aspire.Hosting.Docker/DockerComposeEnvironmentResource.cs b/src/Aspire.Hosting.Docker/DockerComposeEnvironmentResource.cs index 9f92d22ecec..1c26d2f9e0a 100644 --- a/src/Aspire.Hosting.Docker/DockerComposeEnvironmentResource.cs +++ b/src/Aspire.Hosting.Docker/DockerComposeEnvironmentResource.cs @@ -5,11 +5,11 @@ #pragma warning disable ASPIREPIPELINES003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Dcp.Process; using Aspire.Hosting.Docker.Resources; using Aspire.Hosting.Pipelines; using Aspire.Hosting.Publishing; using Aspire.Hosting.Utils; -using Microsoft.Extensions.DependencyInjection; namespace Aspire.Hosting.Docker; @@ -31,11 +31,6 @@ public class DockerComposeEnvironmentResource : Resource, IComputeEnvironmentRes /// public string? DefaultNetworkName { get; set; } - /// - /// Determines whether to build container images for the resources in this environment. - /// - public bool BuildContainerImages { get; set; } = true; - /// /// Determines whether to include an Aspire dashboard for telemetry visualization in this environment. /// @@ -58,15 +53,103 @@ public class DockerComposeEnvironmentResource : Resource, IComputeEnvironmentRes /// The name of the Docker Compose environment. public DockerComposeEnvironmentResource(string name) : base(name) { - Annotations.Add(new PipelineStepAnnotation(context => + Annotations.Add(new PipelineStepAnnotation(async (factoryContext) => { - var step = new PipelineStep + var model = factoryContext.PipelineContext.Model; + var steps = new List(); + + var publishStep = new PipelineStep { Name = $"publish-{Name}", Action = ctx => PublishAsync(ctx) }; - step.RequiredBy(WellKnownPipelineSteps.Publish); - return step; + publishStep.RequiredBy(WellKnownPipelineSteps.Publish); + steps.Add(publishStep); + + var dockerComposeUpStep = new PipelineStep + { + Name = $"docker-compose-up-{Name}", + Action = ctx => DockerComposeUpAsync(ctx), + Tags = ["docker-compose-up"], + DependsOnSteps = [$"publish-{Name}"] + }; + dockerComposeUpStep.RequiredBy(WellKnownPipelineSteps.Deploy); + steps.Add(dockerComposeUpStep); + + var dockerComposeDownStep = new PipelineStep + { + Name = $"docker-compose-down-{Name}", + Action = ctx => DockerComposeDownAsync(ctx), + Tags = ["docker-compose-down"] + }; + steps.Add(dockerComposeDownStep); + + // Expand deployment target steps for all compute resources + foreach (var computeResource in model.GetComputeResources()) + { + var deploymentTarget = computeResource.GetDeploymentTargetAnnotation(this)?.DeploymentTarget; + + if (deploymentTarget != null && deploymentTarget.TryGetAnnotationsOfType(out var annotations)) + { + // Resolve the deployment target's PipelineStepAnnotation and expand its steps + // We do this because the deployment target is not in the model + foreach (var annotation in annotations) + { + var childFactoryContext = new PipelineStepFactoryContext + { + PipelineContext = factoryContext.PipelineContext, + Resource = deploymentTarget + }; + + var deploymentTargetSteps = await annotation.CreateStepsAsync(childFactoryContext).ConfigureAwait(false); + + foreach (var step in deploymentTargetSteps) + { + // Ensure the step is associated with the deployment target resource + step.Resource ??= deploymentTarget; + } + + steps.AddRange(deploymentTargetSteps); + } + } + } + + return steps; + })); + + // Add pipeline configuration annotation to wire up dependencies + // This is where we wire up the build steps created by the resources + Annotations.Add(new PipelineConfigurationAnnotation(context => + { + // Wire up build step dependencies + // Build steps are created by ProjectResource and ContainerResource + foreach (var computeResource in context.Model.GetComputeResources()) + { + var deploymentTarget = computeResource.GetDeploymentTargetAnnotation(this)?.DeploymentTarget; + + if (deploymentTarget is null) + { + continue; + } + + // Execute the PipelineConfigurationAnnotation callbacks on the deployment target + if (deploymentTarget.TryGetAnnotationsOfType(out var annotations)) + { + foreach (var annotation in annotations) + { + annotation.Callback(context); + } + } + } + + // This ensures that resources that have to be built before deployments are handled + foreach (var computeResource in context.Model.GetBuildResources()) + { + context.GetSteps(computeResource, WellKnownPipelineTags.BuildCompute) + .RequiredBy(WellKnownPipelineSteps.Deploy) + .RequiredBy($"docker-compose-up-{Name}") + .DependsOn(WellKnownPipelineSteps.DeployPrereq); + } })); } @@ -87,11 +170,9 @@ ReferenceExpression IComputeEnvironmentResource.GetHostAddressExpression(Endpoin private Task PublishAsync(PipelineStepContext context) { var outputPath = PublishingContextUtils.GetEnvironmentOutputPath(context, this); - var imageBuilder = context.Services.GetRequiredService(); var dockerComposePublishingContext = new DockerComposePublishingContext( context.ExecutionContext, - imageBuilder, outputPath, context.Logger, context.ReportingStep, @@ -100,6 +181,104 @@ private Task PublishAsync(PipelineStepContext context) return dockerComposePublishingContext.WriteModelAsync(context.Model, this); } + private async Task DockerComposeUpAsync(PipelineStepContext context) + { + var outputPath = PublishingContextUtils.GetEnvironmentOutputPath(context, this); + var dockerComposeFilePath = Path.Combine(outputPath, "docker-compose.yaml"); + + if (!File.Exists(dockerComposeFilePath)) + { + throw new InvalidOperationException($"Docker Compose file not found at {dockerComposeFilePath}"); + } + + var deployTask = await context.ReportingStep.CreateTaskAsync($"Running docker compose up for **{Name}**", context.CancellationToken).ConfigureAwait(false); + await using (deployTask.ConfigureAwait(false)) + { + try + { + var spec = new ProcessSpec("docker") + { + Arguments = $"compose -f \"{dockerComposeFilePath}\" up -d --remove-orphans", + WorkingDirectory = outputPath, + ThrowOnNonZeroReturnCode = false, + InheritEnv = true + }; + + var (pendingProcessResult, processDisposable) = ProcessUtil.Run(spec); + + await using (processDisposable) + { + var processResult = await pendingProcessResult + .WaitAsync(context.CancellationToken) + .ConfigureAwait(false); + + if (processResult.ExitCode != 0) + { + await deployTask.FailAsync($"docker compose up failed with exit code {processResult.ExitCode}", cancellationToken: context.CancellationToken).ConfigureAwait(false); + } + else + { + await deployTask.CompleteAsync($"Docker Compose deployment complete for **{Name}**", CompletionState.Completed, context.CancellationToken).ConfigureAwait(false); + } + } + } + catch (Exception ex) + { + await deployTask.CompleteAsync($"Docker Compose deployment failed: {ex.Message}", CompletionState.CompletedWithError, context.CancellationToken).ConfigureAwait(false); + throw; + } + } + } + + private async Task DockerComposeDownAsync(PipelineStepContext context) + { + var outputPath = PublishingContextUtils.GetEnvironmentOutputPath(context, this); + var dockerComposeFilePath = Path.Combine(outputPath, "docker-compose.yaml"); + + if (!File.Exists(dockerComposeFilePath)) + { + throw new InvalidOperationException($"Docker Compose file not found at {dockerComposeFilePath}"); + } + + var deployTask = await context.ReportingStep.CreateTaskAsync($"Running docker compose down for **{Name}**", context.CancellationToken).ConfigureAwait(false); + await using (deployTask.ConfigureAwait(false)) + { + try + { + var spec = new ProcessSpec("docker") + { + Arguments = $"compose -f \"{dockerComposeFilePath}\" down", + WorkingDirectory = outputPath, + ThrowOnNonZeroReturnCode = false, + InheritEnv = true + }; + + var (pendingProcessResult, processDisposable) = ProcessUtil.Run(spec); + + await using (processDisposable) + { + var processResult = await pendingProcessResult + .WaitAsync(context.CancellationToken) + .ConfigureAwait(false); + + if (processResult.ExitCode != 0) + { + await deployTask.FailAsync($"docker compose down failed with exit code {processResult.ExitCode}", cancellationToken: context.CancellationToken).ConfigureAwait(false); + } + else + { + await deployTask.CompleteAsync($"Docker Compose shutdown complete for **{Name}**", CompletionState.Completed, context.CancellationToken).ConfigureAwait(false); + } + } + } + catch (Exception ex) + { + await deployTask.CompleteAsync($"Docker Compose shutdown failed: {ex.Message}", CompletionState.CompletedWithError, context.CancellationToken).ConfigureAwait(false); + throw; + } + } + } + internal string AddEnvironmentVariable(string name, string? description = null, string? defaultValue = null, object? source = null) { CapturedEnvironmentVariables[name] = (description, defaultValue, source); diff --git a/src/Aspire.Hosting.Docker/DockerComposePublishingContext.cs b/src/Aspire.Hosting.Docker/DockerComposePublishingContext.cs index 0a3f629f27b..9b231d1ec4e 100644 --- a/src/Aspire.Hosting.Docker/DockerComposePublishingContext.cs +++ b/src/Aspire.Hosting.Docker/DockerComposePublishingContext.cs @@ -10,7 +10,6 @@ using Aspire.Hosting.Docker.Resources.ComposeNodes; using Aspire.Hosting.Docker.Resources.ServiceNodes; using Aspire.Hosting.Pipelines; -using Aspire.Hosting.Publishing; using Microsoft.Extensions.Logging; namespace Aspire.Hosting.Docker; @@ -25,7 +24,6 @@ namespace Aspire.Hosting.Docker; /// internal sealed class DockerComposePublishingContext( DistributedApplicationExecutionContext executionContext, - IResourceContainerImageBuilder imageBuilder, string outputPath, ILogger logger, IReportingStep reportingStep, @@ -36,7 +34,6 @@ internal sealed class DockerComposePublishingContext( UnixFileMode.GroupRead | UnixFileMode.GroupWrite | UnixFileMode.OtherRead | UnixFileMode.OtherWrite; - public readonly IResourceContainerImageBuilder ImageBuilder = imageBuilder; public readonly string OutputPath = outputPath ?? throw new InvalidOperationException("OutputPath is required for Docker Compose publishing."); internal async Task WriteModelAsync(DistributedApplicationModel model, DockerComposeEnvironmentResource environment) @@ -78,17 +75,10 @@ private async Task WriteDockerComposeOutputAsync(DistributedApplicationModel mod ? [r, .. model.Resources] : model.Resources; - var containerImagesToBuild = new List(); - foreach (var resource in resources) { if (resource.GetDeploymentTargetAnnotation(environment)?.DeploymentTarget is DockerComposeServiceResource serviceResource) { - if (environment.BuildContainerImages) - { - containerImagesToBuild.Add(serviceResource.TargetResource); - } - // Materialize Dockerfile factories for resources with DockerfileBuildAnnotation if (serviceResource.TargetResource.TryGetLastAnnotation(out var dockerfileBuildAnnotation) && dockerfileBuildAnnotation.DockerfileFactory is not null) @@ -143,12 +133,6 @@ private async Task WriteDockerComposeOutputAsync(DistributedApplicationModel mod } } - // Build container images for the services that require it - if (containerImagesToBuild.Count > 0) - { - await ImageBuilder.BuildImagesAsync(containerImagesToBuild, options: null, cancellationToken).ConfigureAwait(false); - } - var writeTask = await reportingStep.CreateTaskAsync( "Writing the Docker Compose file to the output path.", cancellationToken: cancellationToken).ConfigureAwait(false); @@ -175,10 +159,10 @@ private async Task WriteDockerComposeOutputAsync(DistributedApplicationModel mod var (key, (description, defaultValue, source)) = entry; var onlyIfMissing = true; - // If the source is a parameter and there's no explicit default value, - // resolve the parameter's default value asynchronously - if (defaultValue is null && source is ParameterResource parameter && !parameter.Secret && parameter.Default is not null) + // Handle parameter resources by resolving their actual values + if (source is ParameterResource parameter) { + // For non-secret parameters, get the actual parameter value defaultValue = await parameter.GetValueAsync(cancellationToken).ConfigureAwait(false); } diff --git a/src/Aspire.Hosting.Docker/DockerComposeServiceResource.cs b/src/Aspire.Hosting.Docker/DockerComposeServiceResource.cs index 952f4d85b0b..44abb3b72ae 100644 --- a/src/Aspire.Hosting.Docker/DockerComposeServiceResource.cs +++ b/src/Aspire.Hosting.Docker/DockerComposeServiceResource.cs @@ -81,6 +81,14 @@ internal Service BuildComposeService() SetContainerImage(containerImageName, composeService); } + // Disable the pull policy if the target resource requires an image build + // since we'll want to use local images built during the pipeline instead + // of pulling images from a registry. + if (TargetResource.RequiresImageBuild()) + { + composeService.PullPolicy = "never"; + } + SetContainerName(composeService); SetEntryPoint(composeService); AddEnvironmentVariablesAndCommandLineArgs(composeService); diff --git a/src/Aspire.Hosting.Docker/EnvFile.cs b/src/Aspire.Hosting.Docker/EnvFile.cs index a95d668bf8e..d84df9f8b6f 100644 --- a/src/Aspire.Hosting.Docker/EnvFile.cs +++ b/src/Aspire.Hosting.Docker/EnvFile.cs @@ -35,10 +35,36 @@ public static EnvFile Load(string path) public void Add(string key, string? value, string? comment, bool onlyIfMissing = true) { - if (onlyIfMissing && _keys.Contains(key)) + if (_keys.Contains(key)) { - return; + if (onlyIfMissing) + { + return; + } + + // If the key already exists and we want to update it (onlyIfMissing = false), + // we need to find and replace the existing entry + // Find the existing key-value line and replace it + for (var i = 0; i < _lines.Count; i++) + { + var line = _lines[i].TrimStart(); + if (!line.StartsWith('#') && line.Contains('=')) + { + var eqIndex = line.IndexOf('='); + if (eqIndex > 0) + { + var existingKey = line[..eqIndex].Trim(); + if (existingKey == key) + { + _lines[i] = value is not null ? $"{key}={value}" : $"{key}="; + return; + } + } + } + } } + + // Add new entry if (!string.IsNullOrWhiteSpace(comment)) { _lines.Add($"# {comment}"); diff --git a/src/Aspire.Hosting.Docker/Resources/ComposeNodes/Service.cs b/src/Aspire.Hosting.Docker/Resources/ComposeNodes/Service.cs index 7dc59b758c8..1fb08081938 100644 --- a/src/Aspire.Hosting.Docker/Resources/ComposeNodes/Service.cs +++ b/src/Aspire.Hosting.Docker/Resources/ComposeNodes/Service.cs @@ -34,6 +34,16 @@ public sealed class Service : NamedComposeMember [YamlMember(Alias = "image")] public string? Image { get; set; } + /// + /// Specifies the pull policy for the Docker image. + /// + /// + /// The pull policy determines when to pull the image from the registry. + /// It can be set to values like "always", "if-not-present", or "never". + /// + [YamlMember(Alias = "pull_policy")] + public string? PullPolicy { get; set; } + /// /// Specifies the name of the container to be used. /// This property maps to the "container_name" field in a Docker Compose file. diff --git a/src/Aspire.Hosting/ApplicationModel/ProjectResource.cs b/src/Aspire.Hosting/ApplicationModel/ProjectResource.cs index 17cdd191d32..2dbb892f995 100644 --- a/src/Aspire.Hosting/ApplicationModel/ProjectResource.cs +++ b/src/Aspire.Hosting/ApplicationModel/ProjectResource.cs @@ -95,7 +95,7 @@ await containerImageBuilder.BuildImageAsync( this, new ContainerBuildOptions { - TargetPlatform = ContainerTargetPlatform.LinuxAmd64 + TargetPlatform = ContainerTargetPlatform.AllLinux }, ctx.CancellationToken).ConfigureAwait(false); diff --git a/src/Aspire.Hosting/CompatibilitySuppressions.xml b/src/Aspire.Hosting/CompatibilitySuppressions.xml index 61c71755871..7b55bd08e21 100644 --- a/src/Aspire.Hosting/CompatibilitySuppressions.xml +++ b/src/Aspire.Hosting/CompatibilitySuppressions.xml @@ -64,6 +64,13 @@ lib/net8.0/Aspire.Hosting.dll true + + CP0002 + F:Aspire.Hosting.Publishing.ContainerTargetPlatform.Linux386 + lib/net8.0/Aspire.Hosting.dll + lib/net8.0/Aspire.Hosting.dll + true + CP0002 M:Aspire.Hosting.ApplicationModel.AllocatedEndpoint.#ctor(Aspire.Hosting.ApplicationModel.EndpointAnnotation,System.String,System.Int32,Aspire.Hosting.ApplicationModel.EndpointBindingMode,System.String,System.String) @@ -197,4 +204,39 @@ lib/net8.0/Aspire.Hosting.dll true + + CP0011 + F:Aspire.Hosting.Publishing.ContainerTargetPlatform.LinuxAmd64 + lib/net8.0/Aspire.Hosting.dll + lib/net8.0/Aspire.Hosting.dll + true + + + CP0011 + F:Aspire.Hosting.Publishing.ContainerTargetPlatform.LinuxArm + lib/net8.0/Aspire.Hosting.dll + lib/net8.0/Aspire.Hosting.dll + true + + + CP0011 + F:Aspire.Hosting.Publishing.ContainerTargetPlatform.LinuxArm64 + lib/net8.0/Aspire.Hosting.dll + lib/net8.0/Aspire.Hosting.dll + true + + + CP0011 + F:Aspire.Hosting.Publishing.ContainerTargetPlatform.WindowsAmd64 + lib/net8.0/Aspire.Hosting.dll + lib/net8.0/Aspire.Hosting.dll + true + + + CP0011 + F:Aspire.Hosting.Publishing.ContainerTargetPlatform.WindowsArm64 + lib/net8.0/Aspire.Hosting.dll + lib/net8.0/Aspire.Hosting.dll + true + \ No newline at end of file diff --git a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs index 1b77f8bde13..02ff40248dc 100644 --- a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs @@ -46,7 +46,7 @@ await containerImageBuilder.BuildImageAsync( builder.Resource, new ContainerBuildOptions { - TargetPlatform = ContainerTargetPlatform.LinuxAmd64 + TargetPlatform = ContainerTargetPlatform.AllLinux }, ctx.CancellationToken).ConfigureAwait(false); }, diff --git a/src/Aspire.Hosting/Publishing/ResourceContainerImageBuilder.cs b/src/Aspire.Hosting/Publishing/ResourceContainerImageBuilder.cs index adf56cc6f5f..7f7729288a8 100644 --- a/src/Aspire.Hosting/Publishing/ResourceContainerImageBuilder.cs +++ b/src/Aspire.Hosting/Publishing/ResourceContainerImageBuilder.cs @@ -33,37 +33,38 @@ public enum ContainerImageFormat /// Specifies the target platform for container images. /// [Experimental("ASPIREPIPELINES003", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] +[Flags] public enum ContainerTargetPlatform { /// /// Linux AMD64 (linux/amd64). /// - LinuxAmd64, + LinuxAmd64 = 1, /// /// Linux ARM64 (linux/arm64). /// - LinuxArm64, + LinuxArm64 = 2, /// /// Linux ARM (linux/arm). /// - LinuxArm, + LinuxArm = 4, /// - /// Linux 386 (linux/386). + /// Windows AMD64 (windows/amd64). /// - Linux386, + WindowsAmd64 = 16, /// - /// Windows AMD64 (windows/amd64). + /// Windows ARM64 (windows/arm64). /// - WindowsAmd64, + WindowsArm64 = 32, /// - /// Windows ARM64 (windows/arm64). + /// All Linux platforms (AMD64, ARM64, ARM, 386). /// - WindowsArm64 + AllLinux = LinuxAmd64 | LinuxArm64, } /// @@ -266,7 +267,7 @@ private async Task ExecuteDotnetPublishAsync(IResource resource, Container if (options.TargetPlatform is not null) { - arguments += $" /p:ContainerRuntimeIdentifier=\"{options.TargetPlatform.Value.ToMSBuildRuntimeIdentifierString()}\""; + arguments += $" /p:RuntimeIdentifiers=\"{options.TargetPlatform.Value.ToMSBuildRuntimeIdentifierString()}\""; } } @@ -424,32 +425,77 @@ internal static class ContainerTargetPlatformExtensions /// /// Converts the target platform to the format used by container runtimes (Docker/Podman). /// - /// The target platform. - /// The platform string in the format used by container runtimes. - public static string ToRuntimePlatformString(this ContainerTargetPlatform platform) => platform switch + /// The target platform(s). + /// The platform string(s) in the format used by container runtimes. + public static string ToRuntimePlatformString(this ContainerTargetPlatform platform) { - ContainerTargetPlatform.LinuxAmd64 => "linux/amd64", - ContainerTargetPlatform.LinuxArm64 => "linux/arm64", - ContainerTargetPlatform.LinuxArm => "linux/arm", - ContainerTargetPlatform.Linux386 => "linux/386", - ContainerTargetPlatform.WindowsAmd64 => "windows/amd64", - ContainerTargetPlatform.WindowsArm64 => "windows/arm64", - _ => throw new ArgumentOutOfRangeException(nameof(platform), platform, "Unknown container target platform") - }; + var platformStrings = new List(); + + if ((platform & ContainerTargetPlatform.LinuxAmd64) != 0) + { + platformStrings.Add("linux/amd64"); + } + if ((platform & ContainerTargetPlatform.LinuxArm64) != 0) + { + platformStrings.Add("linux/arm64"); + } + if ((platform & ContainerTargetPlatform.LinuxArm) != 0) + { + platformStrings.Add("linux/arm"); + } + if ((platform & ContainerTargetPlatform.WindowsAmd64) != 0) + { + platformStrings.Add("windows/amd64"); + } + if ((platform & ContainerTargetPlatform.WindowsArm64) != 0) + { + platformStrings.Add("windows/arm64"); + } + + if (platformStrings.Count == 0) + { + throw new ArgumentException("At least one platform must be specified.", nameof(platform)); + } + + return string.Join(",", platformStrings); + } /// /// Converts the target platform to the format used by MSBuild ContainerRuntimeIdentifier. + /// MSBuild supports multiple runtime identifiers separated by semicolons. /// - /// The target platform. - /// The platform string in the format used by MSBuild. - public static string ToMSBuildRuntimeIdentifierString(this ContainerTargetPlatform platform) => platform switch + /// The target platform(s). + /// The platform string(s) in the format used by MSBuild, separated by semicolons if multiple. + public static string ToMSBuildRuntimeIdentifierString(this ContainerTargetPlatform platform) { - ContainerTargetPlatform.LinuxAmd64 => "linux-x64", - ContainerTargetPlatform.LinuxArm64 => "linux-arm64", - ContainerTargetPlatform.LinuxArm => "linux-arm", - ContainerTargetPlatform.Linux386 => "linux-x86", - ContainerTargetPlatform.WindowsAmd64 => "win-x64", - ContainerTargetPlatform.WindowsArm64 => "win-arm64", - _ => throw new ArgumentOutOfRangeException(nameof(platform), platform, "Unknown container target platform") - }; + var rids = new List(); + + if ((platform & ContainerTargetPlatform.LinuxAmd64) != 0) + { + rids.Add("linux-x64"); + } + if ((platform & ContainerTargetPlatform.LinuxArm64) != 0) + { + rids.Add("linux-arm64"); + } + if ((platform & ContainerTargetPlatform.LinuxArm) != 0) + { + rids.Add("linux-arm"); + } + if ((platform & ContainerTargetPlatform.WindowsAmd64) != 0) + { + rids.Add("win-x64"); + } + if ((platform & ContainerTargetPlatform.WindowsArm64) != 0) + { + rids.Add("win-arm64"); + } + + if (rids.Count == 0) + { + throw new ArgumentException("At least one platform must be specified.", nameof(platform)); + } + + return string.Join(";", rids); + } } diff --git a/tests/Aspire.Hosting.Docker.Tests/DockerComposePublisherTests.cs b/tests/Aspire.Hosting.Docker.Tests/DockerComposePublisherTests.cs index 507fdcb3b4d..270e2ce4f40 100644 --- a/tests/Aspire.Hosting.Docker.Tests/DockerComposePublisherTests.cs +++ b/tests/Aspire.Hosting.Docker.Tests/DockerComposePublisherTests.cs @@ -25,8 +25,8 @@ public async Task PublishAsync_GeneratesValidDockerComposeFile() builder.AddDockerComposeEnvironment("docker-compose"); - var param0 = builder.AddParameter("param0"); - var param1 = builder.AddParameter("param1", secret: true); + var param0 = builder.AddParameter("param0", "default0"); + var param1 = builder.AddParameter("param1", "defaultSecret", secret: true); var param2 = builder.AddParameter("param2", "default", publishValueAsDefault: true); var cs = builder.AddConnectionString("cs", ReferenceExpression.Create($"Url={param0}, Secret={param1}")); @@ -161,19 +161,16 @@ public async Task DockerComposeCorrectlyEmitsPortMappings() await Verify(File.ReadAllText(composePath), "yaml"); } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void DockerComposeHandleImageBuilding(bool shouldBuildImages) + [Fact] + public void DockerComposePublishShouldNotHandleImageBuilding() { using var tempDir = new TempDirectory(); - using var builder = TestDistributedApplicationBuilder.Create(["--operation", "publish", "--publisher", "default", "--output-path", tempDir.Path]) + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, step: "publish-docker-compose", outputPath: tempDir.Path) .WithTestAndResourceLogging(outputHelper); builder.Services.AddSingleton(); - builder.AddDockerComposeEnvironment("docker-compose") - .WithProperties(e => e.BuildContainerImages = shouldBuildImages); + builder.AddDockerComposeEnvironment("docker-compose"); builder.Services.AddSingleton(); @@ -192,7 +189,7 @@ public void DockerComposeHandleImageBuilding(bool shouldBuildImages) var composePath = Path.Combine(tempDir.Path, "docker-compose.yaml"); Assert.True(File.Exists(composePath)); - Assert.Equal(shouldBuildImages, mockImageBuilder.BuildImageCalled); + Assert.False(mockImageBuilder.BuildImageCalled); } [Fact] @@ -260,7 +257,7 @@ void PublishApp() builder.Services.AddSingleton(); builder.AddDockerComposeEnvironment("docker-compose"); - var param = builder.AddParameter("param1"); + var param = builder.AddParameter("param1", "default"); builder.AddContainer("app", "busybox").WithEnvironment("param1", param); var app = builder.Build(); app.Run(); @@ -292,7 +289,7 @@ void PublishApp(params string[] paramNames) builder.AddDockerComposeEnvironment("docker-compose"); - var parmeters = paramNames.Select(name => builder.AddParameter(name).Resource).ToArray(); + var parmeters = paramNames.Select(name => builder.AddParameter(name, $"{name}-value").Resource).ToArray(); builder.AddContainer("app", "busybox") .WithEnvironment(context => @@ -478,7 +475,7 @@ public async Task PublishAsync_WithDockerfileFactory_WritesDockerfileToOutputFol var dockerfilePath = Path.Combine(tempDir.Path, "testcontainer.Dockerfile"); Assert.True(File.Exists(dockerfilePath), $"Dockerfile should exist at {dockerfilePath}"); var actualContent = await File.ReadAllTextAsync(dockerfilePath); - + await Verify(actualContent); } diff --git a/tests/Aspire.Hosting.Docker.Tests/Snapshots/DockerComposePublisherTests.DockerComposeAppendsNewKeysToEnvFileOnPublish#00.verified.env b/tests/Aspire.Hosting.Docker.Tests/Snapshots/DockerComposePublisherTests.DockerComposeAppendsNewKeysToEnvFileOnPublish#00.verified.env index c9a8d7c3a31..7fc8051e167 100644 --- a/tests/Aspire.Hosting.Docker.Tests/Snapshots/DockerComposePublisherTests.DockerComposeAppendsNewKeysToEnvFileOnPublish#00.verified.env +++ b/tests/Aspire.Hosting.Docker.Tests/Snapshots/DockerComposePublisherTests.DockerComposeAppendsNewKeysToEnvFileOnPublish#00.verified.env @@ -1,3 +1,3 @@ # Parameter param1 -PARAM1=changed +PARAM1=changedparam1-value diff --git a/tests/Aspire.Hosting.Docker.Tests/Snapshots/DockerComposePublisherTests.DockerComposeAppendsNewKeysToEnvFileOnPublish#01.verified.env b/tests/Aspire.Hosting.Docker.Tests/Snapshots/DockerComposePublisherTests.DockerComposeAppendsNewKeysToEnvFileOnPublish#01.verified.env index c2fbff884af..9ded86c337a 100644 --- a/tests/Aspire.Hosting.Docker.Tests/Snapshots/DockerComposePublisherTests.DockerComposeAppendsNewKeysToEnvFileOnPublish#01.verified.env +++ b/tests/Aspire.Hosting.Docker.Tests/Snapshots/DockerComposePublisherTests.DockerComposeAppendsNewKeysToEnvFileOnPublish#01.verified.env @@ -1,6 +1,6 @@ # Parameter param1 -PARAM1=changed +PARAM1=changedparam1-value # Parameter param2 -PARAM2= +PARAM2=param2-value diff --git a/tests/Aspire.Hosting.Docker.Tests/Snapshots/DockerComposePublisherTests.DockerComposeDoesNotOverwriteEnvFileOnPublish#00.verified.env b/tests/Aspire.Hosting.Docker.Tests/Snapshots/DockerComposePublisherTests.DockerComposeDoesNotOverwriteEnvFileOnPublish#00.verified.env index c9a8d7c3a31..c89334be92c 100644 --- a/tests/Aspire.Hosting.Docker.Tests/Snapshots/DockerComposePublisherTests.DockerComposeDoesNotOverwriteEnvFileOnPublish#00.verified.env +++ b/tests/Aspire.Hosting.Docker.Tests/Snapshots/DockerComposePublisherTests.DockerComposeDoesNotOverwriteEnvFileOnPublish#00.verified.env @@ -1,3 +1,3 @@ # Parameter param1 -PARAM1=changed +PARAM1=changeddefault diff --git a/tests/Aspire.Hosting.Docker.Tests/Snapshots/DockerComposePublisherTests.DockerComposeDoesNotOverwriteEnvFileOnPublish#01.verified.env b/tests/Aspire.Hosting.Docker.Tests/Snapshots/DockerComposePublisherTests.DockerComposeDoesNotOverwriteEnvFileOnPublish#01.verified.env index c9a8d7c3a31..c89334be92c 100644 --- a/tests/Aspire.Hosting.Docker.Tests/Snapshots/DockerComposePublisherTests.DockerComposeDoesNotOverwriteEnvFileOnPublish#01.verified.env +++ b/tests/Aspire.Hosting.Docker.Tests/Snapshots/DockerComposePublisherTests.DockerComposeDoesNotOverwriteEnvFileOnPublish#01.verified.env @@ -1,3 +1,3 @@ # Parameter param1 -PARAM1=changed +PARAM1=changeddefault diff --git a/tests/Aspire.Hosting.Docker.Tests/Snapshots/DockerComposePublisherTests.DockerComposeMapsPortsProperly.verified.txt b/tests/Aspire.Hosting.Docker.Tests/Snapshots/DockerComposePublisherTests.DockerComposeMapsPortsProperly.verified.txt index 1f58ee0bd60..ddc34a2cc0d 100644 --- a/tests/Aspire.Hosting.Docker.Tests/Snapshots/DockerComposePublisherTests.DockerComposeMapsPortsProperly.verified.txt +++ b/tests/Aspire.Hosting.Docker.Tests/Snapshots/DockerComposePublisherTests.DockerComposeMapsPortsProperly.verified.txt @@ -1,6 +1,7 @@ services: service: image: "${SERVICE_IMAGE}" + pull_policy: "never" environment: PORT: "8000" expose: diff --git a/tests/Aspire.Hosting.Docker.Tests/Snapshots/DockerComposePublisherTests.DockerComposeWithProjectResources.verified.yaml b/tests/Aspire.Hosting.Docker.Tests/Snapshots/DockerComposePublisherTests.DockerComposeWithProjectResources.verified.yaml index 50e77caf0e0..bbe76c84913 100644 --- a/tests/Aspire.Hosting.Docker.Tests/Snapshots/DockerComposePublisherTests.DockerComposeWithProjectResources.verified.yaml +++ b/tests/Aspire.Hosting.Docker.Tests/Snapshots/DockerComposePublisherTests.DockerComposeWithProjectResources.verified.yaml @@ -9,6 +9,7 @@ restart: "always" project1: image: "${PROJECT1_IMAGE}" + pull_policy: "never" environment: OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES: "true" OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES: "true" diff --git a/tests/Aspire.Hosting.Docker.Tests/Snapshots/DockerComposePublisherTests.PublishAsync_GeneratesValidDockerComposeFile.verified.env b/tests/Aspire.Hosting.Docker.Tests/Snapshots/DockerComposePublisherTests.PublishAsync_GeneratesValidDockerComposeFile.verified.env index 4fc5900e199..dee0e08d7b8 100644 --- a/tests/Aspire.Hosting.Docker.Tests/Snapshots/DockerComposePublisherTests.PublishAsync_GeneratesValidDockerComposeFile.verified.env +++ b/tests/Aspire.Hosting.Docker.Tests/Snapshots/DockerComposePublisherTests.PublishAsync_GeneratesValidDockerComposeFile.verified.env @@ -1,8 +1,8 @@ # Parameter param0 -PARAM0= +PARAM0=default0 # Parameter param1 -PARAM1= +PARAM1=defaultSecret # Parameter param2 PARAM2=default diff --git a/tests/Aspire.Hosting.Docker.Tests/Snapshots/DockerComposePublisherTests.PublishAsync_GeneratesValidDockerComposeFile.verified.yaml b/tests/Aspire.Hosting.Docker.Tests/Snapshots/DockerComposePublisherTests.PublishAsync_GeneratesValidDockerComposeFile.verified.yaml index 12cb38a4de2..ec5e8fbed7d 100644 --- a/tests/Aspire.Hosting.Docker.Tests/Snapshots/DockerComposePublisherTests.PublishAsync_GeneratesValidDockerComposeFile.verified.yaml +++ b/tests/Aspire.Hosting.Docker.Tests/Snapshots/DockerComposePublisherTests.PublishAsync_GeneratesValidDockerComposeFile.verified.yaml @@ -64,6 +64,7 @@ - "aspire" project1: image: "${PROJECT1_IMAGE}" + pull_policy: "never" environment: OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES: "true" OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES: "true"