From 8208c44f769eb7ed4ca010878cd634ac61e882b8 Mon Sep 17 00:00:00 2001 From: Sean McDonald Date: Sat, 2 May 2026 10:16:49 -0700 Subject: [PATCH 1/2] Pass target platform to DCP for Dockerfile builds The publishing path (DockerContainerRuntime/PodmanContainerRuntime) reads TargetPlatform from ContainerBuildOptionsCallbackAnnotation and passes --platform to the runtime, but the local dev DCP path (ContainerCreator.ApplyBuildArgumentsAsync) ignored the annotation. As a result, docker build defaulted to the host architecture, so on hosts whose architecture differed from the resource's target platform (e.g. arm64 hosts with a linux/amd64 target), Dockerfile builds could fail or produce images for the wrong platform. Add a Platform property to the DCP BuildContext model and populate it from the resolved ContainerBuildOptionsCallbackContext when applying build arguments. The DCP runtime already supports a json:"platform" field on the build spec. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/Aspire.Hosting/Dcp/ContainerCreator.cs | 13 +++++++++++ src/Aspire.Hosting/Dcp/Model/Container.cs | 4 ++++ .../Dcp/DcpExecutorTests.cs | 22 +++++++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/src/Aspire.Hosting/Dcp/ContainerCreator.cs b/src/Aspire.Hosting/Dcp/ContainerCreator.cs index fe377d8bef6..4fcd9ca20c0 100644 --- a/src/Aspire.Hosting/Dcp/ContainerCreator.cs +++ b/src/Aspire.Hosting/Dcp/ContainerCreator.cs @@ -4,16 +4,19 @@ #pragma warning disable ASPIREEXTENSION001 #pragma warning disable ASPIRECERTIFICATES001 #pragma warning disable ASPIRECONTAINERSHELLEXECUTION001 +#pragma warning disable ASPIREPIPELINES003 using System.Collections.Immutable; using System.Diagnostics; using System.Net.Sockets; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Dcp.Model; +using Aspire.Hosting.Publishing; using Aspire.Hosting.Utils; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; namespace Aspire.Hosting.Dcp; @@ -783,6 +786,16 @@ private static async Task ApplyBuildArgumentsAsync(Container dcpContainerResourc Args = dcpBuildArgs, Secrets = dcpBuildSecrets }; + + var buildOptionsContext = await modelContainerResource.ProcessContainerBuildOptionsCallbackAsync( + serviceProvider, + NullLogger.Instance, + cancellationToken: cancellationToken).ConfigureAwait(false); + + if (buildOptionsContext.TargetPlatform is { } targetPlatform) + { + dcpContainerResource.Spec.Build.Platform = targetPlatform.ToRuntimePlatformString(); + } } } diff --git a/src/Aspire.Hosting/Dcp/Model/Container.cs b/src/Aspire.Hosting/Dcp/Model/Container.cs index 7a9279632f7..80be74cb549 100644 --- a/src/Aspire.Hosting/Dcp/Model/Container.cs +++ b/src/Aspire.Hosting/Dcp/Model/Container.cs @@ -126,6 +126,10 @@ internal sealed class BuildContext // Optional labels to apply to the built image [JsonPropertyName("labels")] public List? Labels { get; set; } + + // Optional target platform for the build (e.g. "linux/amd64") + [JsonPropertyName("platform")] + public string? Platform { get; set; } } internal sealed class BuildContextSecret diff --git a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs index 0dee41d64fe..c43a07e52fe 100644 --- a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs +++ b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs @@ -51,6 +51,28 @@ public async Task ContainersArePassedOtelServiceName() Assert.Equal("CustomName", container.Metadata.Annotations["otel-service-name"]); } + [Fact] + public async Task DockerfileContainerBuildSpecIncludesPlatform() + { + using var tempDockerfileContext = await DockerfileUtils.CreateTemporaryDockerfileAsync(); + + var builder = DistributedApplication.CreateBuilder(); + builder.AddDockerfile("mycontainer", tempDockerfileContext.ContextPath, tempDockerfileContext.DockerfilePath); + + var kubernetesService = new TestKubernetesService(); + + using var app = builder.Build(); + var distributedAppModel = app.Services.GetRequiredService(); + + var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService); + + await appExecutor.RunApplicationAsync(); + + var container = Assert.Single(kubernetesService.CreatedResources.OfType()); + Assert.NotNull(container.Spec.Build); + Assert.Equal("linux/amd64", container.Spec.Build!.Platform); + } + [Fact] public async Task ResourceStarted_ProjectHasReplicas_EventRaisedOnce() { From b61de7ef9cafb349614bc15ed2724d26aac3d8d7 Mon Sep 17 00:00:00 2001 From: Sean McDonald Date: Sat, 2 May 2026 11:50:19 -0700 Subject: [PATCH 2/2] Address review feedback - Plumb the per-resource ILogger through ApplyBuildArgumentsAsync instead of NullLogger so warnings/errors from build-options callbacks surface in local dev. Matches the other ProcessContainerBuildOptionsCallbackAsync call sites. - Narrow the ASPIREPIPELINES003 suppression to the specific call/usage rather than disabling it file-wide. - Make DockerfileContainerBuildSpecIncludesPlatform explicitly set TargetPlatform via WithContainerBuildOptions (using LinuxArm64) so the test validates propagation rather than relying on AddDockerfile's default of LinuxAmd64. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/Aspire.Hosting/Dcp/ContainerCreator.cs | 10 +++++----- tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs | 8 ++++++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Aspire.Hosting/Dcp/ContainerCreator.cs b/src/Aspire.Hosting/Dcp/ContainerCreator.cs index 4fcd9ca20c0..2219babcb54 100644 --- a/src/Aspire.Hosting/Dcp/ContainerCreator.cs +++ b/src/Aspire.Hosting/Dcp/ContainerCreator.cs @@ -4,7 +4,6 @@ #pragma warning disable ASPIREEXTENSION001 #pragma warning disable ASPIRECERTIFICATES001 #pragma warning disable ASPIRECONTAINERSHELLEXECUTION001 -#pragma warning disable ASPIREPIPELINES003 using System.Collections.Immutable; using System.Diagnostics; @@ -16,7 +15,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; namespace Aspire.Hosting.Dcp; @@ -263,7 +261,7 @@ private async Task BuildAndCreateContainerAsync(RenderedModelResource var dcpContainer = cr.DcpResource; var modelContainer = cr.ModelResource; - await ApplyBuildArgumentsAsync(dcpContainer, cr.ModelResource, _executionContext.ServiceProvider, cToken).ConfigureAwait(false); + await ApplyBuildArgumentsAsync(dcpContainer, cr.ModelResource, _executionContext.ServiceProvider, logger, cToken).ConfigureAwait(false); var spec = dcpContainer.Spec; @@ -735,7 +733,7 @@ await modelResource.ProcessContainerRuntimeArgValues( return (runArgs, failedToApplyArgs); } - private static async Task ApplyBuildArgumentsAsync(Container dcpContainerResource, IResource modelContainerResource, IServiceProvider serviceProvider, CancellationToken cancellationToken) + private static async Task ApplyBuildArgumentsAsync(Container dcpContainerResource, IResource modelContainerResource, IServiceProvider serviceProvider, ILogger logger, CancellationToken cancellationToken) { if (modelContainerResource.Annotations.OfType().SingleOrDefault() is { } dockerfileBuildAnnotation) { @@ -787,15 +785,17 @@ private static async Task ApplyBuildArgumentsAsync(Container dcpContainerResourc Secrets = dcpBuildSecrets }; +#pragma warning disable ASPIREPIPELINES003 // ContainerBuildOptions APIs are experimental. var buildOptionsContext = await modelContainerResource.ProcessContainerBuildOptionsCallbackAsync( serviceProvider, - NullLogger.Instance, + logger, cancellationToken: cancellationToken).ConfigureAwait(false); if (buildOptionsContext.TargetPlatform is { } targetPlatform) { dcpContainerResource.Spec.Build.Platform = targetPlatform.ToRuntimePlatformString(); } +#pragma warning restore ASPIREPIPELINES003 } } diff --git a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs index c43a07e52fe..230ecc826e7 100644 --- a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs +++ b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs @@ -12,6 +12,7 @@ using Aspire.Dashboard.Model; using Aspire.Hosting.Dcp; using Aspire.Hosting.Dcp.Model; +using Aspire.Hosting.Publishing; using Aspire.Hosting.Tests.Utils; using k8s.Models; using Microsoft.AspNetCore.InternalTesting; @@ -57,7 +58,10 @@ public async Task DockerfileContainerBuildSpecIncludesPlatform() using var tempDockerfileContext = await DockerfileUtils.CreateTemporaryDockerfileAsync(); var builder = DistributedApplication.CreateBuilder(); - builder.AddDockerfile("mycontainer", tempDockerfileContext.ContextPath, tempDockerfileContext.DockerfilePath); +#pragma warning disable ASPIREPIPELINES003 // ContainerBuildOptions APIs are experimental. + builder.AddDockerfile("mycontainer", tempDockerfileContext.ContextPath, tempDockerfileContext.DockerfilePath) + .WithContainerBuildOptions(ctx => ctx.TargetPlatform = ContainerTargetPlatform.LinuxArm64); +#pragma warning restore ASPIREPIPELINES003 var kubernetesService = new TestKubernetesService(); @@ -70,7 +74,7 @@ public async Task DockerfileContainerBuildSpecIncludesPlatform() var container = Assert.Single(kubernetesService.CreatedResources.OfType()); Assert.NotNull(container.Spec.Build); - Assert.Equal("linux/amd64", container.Spec.Build!.Platform); + Assert.Equal("linux/arm64", container.Spec.Build!.Platform); } [Fact]