From c24b775b47934f411372f1fbde99f3298dbc15cb Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Wed, 5 Nov 2025 16:24:39 -0600 Subject: [PATCH 1/2] Support a docker build arg to specify the source image in PublishWithContainerFiles This allows an external system to provide the image name for the source image to copy the files from instead of always hard coding the image tag inline. --- .../JavaScriptHostingExtensions.cs | 5 +- .../PythonAppResourceBuilderExtensions.cs | 10 +++- .../YarpResourceExtensions.cs | 6 +- ...ensions.cs => ContainerFilesExtensions.cs} | 56 +++++++++++++++++-- .../Docker/DockerfileBuilder.cs | 53 +++++++++++++++++- .../ApplicationModel/ProjectResource.cs | 2 + .../AddNodeAppTests.cs | 4 +- ...lesGeneratesCorrectDockerfile.verified.txt | 24 ++++++++ ...eratesDockerfileInPublishMode.verified.txt | 8 ++- .../ProjectResourceTests.cs | 18 +++++- .../Publishing/FakeContainerRuntime.cs | 9 ++- ...lesDestinationAnnotationWorks.verified.txt | 7 +++ ...lesGeneratesCorrectDockerfile.verified.txt | 8 ++- ...ileWithMultipleContainerFiles.verified.txt | 10 +++- ...hMultipleSourceContainerFiles.verified.txt | 17 ++++-- 15 files changed, 209 insertions(+), 28 deletions(-) rename src/Aspire.Hosting/ApplicationModel/Docker/{DockerfileStageExtensions.cs => ContainerFilesExtensions.cs} (56%) create mode 100644 tests/Aspire.Hosting.JavaScript.Tests/Snapshots/AddNodeAppTests.VerifyNodeAppWithContainerFilesGeneratesCorrectDockerfile.verified.txt create mode 100644 tests/Aspire.Hosting.Tests/Snapshots/ProjectResourceTests.ProjectResourceWithContainerFilesDestinationAnnotationWorks.verified.txt diff --git a/src/Aspire.Hosting.JavaScript/JavaScriptHostingExtensions.cs b/src/Aspire.Hosting.JavaScript/JavaScriptHostingExtensions.cs index 7635fcbb4ea..b7aebbb0cf1 100644 --- a/src/Aspire.Hosting.JavaScript/JavaScriptHostingExtensions.cs +++ b/src/Aspire.Hosting.JavaScript/JavaScriptHostingExtensions.cs @@ -130,13 +130,16 @@ public static IResourceBuilder AddNodeApp(this IDistributedAppl } } + var logger = dockerfileContext.Services.GetService>(); + dockerfileContext.Builder.AddContainerFilesStages(dockerfileContext.Resource, logger); + var baseRuntimeImage = baseImageAnnotation?.RuntimeImage ?? defaultBaseImage.Value; var runtimeBuilder = dockerfileContext.Builder .From(baseRuntimeImage, "runtime") .EmptyLine() .WorkDir("/app") .CopyFrom("build", "/app", "/app") - .AddContainerFiles(dockerfileContext.Resource, "/app", dockerfileContext.Services.GetService>()) + .AddContainerFiles(dockerfileContext.Resource, "/app", logger) .EmptyLine() .Env("NODE_ENV", "production") .Expose(3000) diff --git a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs index 8035f541006..355dc1b26d5 100644 --- a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs @@ -550,10 +550,13 @@ private static void GenerateUvDockerfile(DockerfileBuilderCallbackContext contex "type=cache,target=/root/.cache/uv"); } + var logger = context.Services.GetService>(); + context.Builder.AddContainerFilesStages(context.Resource, logger); + var runtimeBuilder = context.Builder .From(runtimeImage, "app") .EmptyLine() - .AddContainerFiles(context.Resource, "/app", context.Services.GetService>()) + .AddContainerFiles(context.Resource, "/app", logger) .Comment("------------------------------") .Comment("🚀 Runtime stage") .Comment("------------------------------") @@ -603,10 +606,13 @@ private static void GenerateFallbackDockerfile(DockerfileBuilderCallbackContext var requirementsTxtPath = Path.Combine(resource.WorkingDirectory, "requirements.txt"); var hasRequirementsTxt = File.Exists(requirementsTxtPath); + var logger = context.Services.GetService>(); + context.Builder.AddContainerFilesStages(context.Resource, logger); + var stage = context.Builder .From(runtimeImage) .EmptyLine() - .AddContainerFiles(context.Resource, "/app", context.Services.GetService>()) + .AddContainerFiles(context.Resource, "/app", logger) .Comment("------------------------------") .Comment("🚀 Python Application") .Comment("------------------------------") diff --git a/src/Aspire.Hosting.Yarp/YarpResourceExtensions.cs b/src/Aspire.Hosting.Yarp/YarpResourceExtensions.cs index 269fb6dbc3b..74c2c79cb5f 100644 --- a/src/Aspire.Hosting.Yarp/YarpResourceExtensions.cs +++ b/src/Aspire.Hosting.Yarp/YarpResourceExtensions.cs @@ -172,10 +172,14 @@ private static IResourceBuilder EnsurePublishWithStaticFilesDocker return builder.WithDockerfileBuilder(".", ctx => { + var logger = ctx.Services.GetService>(); var imageName = GetYarpImageName(ctx.Resource); + + ctx.Builder.AddContainerFilesStages(ctx.Resource, logger); + ctx.Builder.From(imageName) .WorkDir("/app") - .AddContainerFiles(ctx.Resource, "/app/wwwroot", ctx.Services.GetService>()); + .AddContainerFiles(ctx.Resource, "/app/wwwroot", logger); }); } diff --git a/src/Aspire.Hosting/ApplicationModel/Docker/DockerfileStageExtensions.cs b/src/Aspire.Hosting/ApplicationModel/Docker/ContainerFilesExtensions.cs similarity index 56% rename from src/Aspire.Hosting/ApplicationModel/Docker/DockerfileStageExtensions.cs rename to src/Aspire.Hosting/ApplicationModel/Docker/ContainerFilesExtensions.cs index 598f9bab422..60fe13e485c 100644 --- a/src/Aspire.Hosting/ApplicationModel/Docker/DockerfileStageExtensions.cs +++ b/src/Aspire.Hosting/ApplicationModel/Docker/ContainerFilesExtensions.cs @@ -7,10 +7,50 @@ namespace Aspire.Hosting.ApplicationModel.Docker; /// -/// Provides extension methods for . +/// Provides Dockerfile builder extension methods for supporting . /// -public static class DockerfileStageExtensions +public static class ContainerFilesExtensions { + /// + /// Adds Dockerfile instructions to include container files from the specified resource into the Dockerfile build + /// process. + /// + /// If the resource does not provide valid container image names for its sources, those sources + /// are skipped and a warning is logged if a logger is provided. This method is experimental and its behavior may + /// change in future releases. + /// The Dockerfile builder to which container file instructions will be added. Cannot be null. + /// The resource containing container files to be added to the Dockerfile. Cannot be null. + /// An optional logger used to record warnings if container image names cannot be determined for source resources. + /// The same DockerfileBuilder instance with additional instructions for container files, enabling method chaining. + [Experimental("ASPIREDOCKERFILEBUILDER001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] + public static DockerfileBuilder AddContainerFilesStages(this DockerfileBuilder builder, IResource resource, ILogger? logger) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(resource); + + if (resource.TryGetAnnotationsOfType(out var containerFilesDestinationAnnotations)) + { + foreach (var containerFileDestination in containerFilesDestinationAnnotations) + { + var source = containerFileDestination.Source; + + // get image name - skip this source if it doesn't have an image name + if (!source.TryGetContainerImageName(out var sourceImageName)) + { + logger?.LogWarning("Cannot get container image name for source resource {SourceName}, skipping", source.Name); + continue; + } + + var sourceImageArgName = GetSourceImageArgName(source); + builder.Arg(sourceImageArgName, sourceImageName); + + var sourceImageStageName = GetSourceStageName(source); + builder.From("${" + sourceImageArgName + "}", sourceImageStageName); + } + } + return builder; + } + /// /// Adds COPY --from statements to the Dockerfile stage for container files from resources referenced by . /// @@ -53,12 +93,14 @@ public static DockerfileStage AddContainerFiles(this DockerfileStage stage, IRes var source = containerFileDestination.Source; // get image name - skip this source if it doesn't have an image name - if (!source.TryGetContainerImageName(out var sourceImageName)) + if (!source.TryGetContainerImageName(out var _)) { logger?.LogWarning("Cannot get container image name for source resource {SourceName}, skipping", source.Name); continue; } + var sourceImageStageName = GetSourceStageName(source); + var destinationPath = containerFileDestination.DestinationPath; if (!destinationPath.StartsWith('/')) { @@ -68,8 +110,8 @@ public static DockerfileStage AddContainerFiles(this DockerfileStage stage, IRes foreach (var containerFilesSource in source.Annotations.OfType()) { logger?.LogDebug("Adding COPY --from={SourceImage} {SourcePath} {DestinationPath}", - sourceImageName, containerFilesSource.SourcePath, destinationPath); - stage.CopyFrom(sourceImageName, containerFilesSource.SourcePath, destinationPath); + sourceImageStageName, containerFilesSource.SourcePath, destinationPath); + stage.CopyFrom(sourceImageStageName, containerFilesSource.SourcePath, destinationPath); } } @@ -77,4 +119,8 @@ public static DockerfileStage AddContainerFiles(this DockerfileStage stage, IRes } return stage; } + + private static string GetSourceImageArgName(IResource source) => $"{source.Name}_IMAGENAME"; + + private static string GetSourceStageName(IResource source) => $"{source.Name}_stage"; } diff --git a/src/Aspire.Hosting/ApplicationModel/Docker/DockerfileBuilder.cs b/src/Aspire.Hosting/ApplicationModel/Docker/DockerfileBuilder.cs index e5be106c9b5..89f2dd0c672 100644 --- a/src/Aspire.Hosting/ApplicationModel/Docker/DockerfileBuilder.cs +++ b/src/Aspire.Hosting/ApplicationModel/Docker/DockerfileBuilder.cs @@ -12,6 +12,7 @@ namespace Aspire.Hosting.ApplicationModel.Docker; public class DockerfileBuilder { private readonly List _stages = []; + private readonly List _globalArgs = []; /// /// Initializes a new instance of the class. @@ -25,6 +26,44 @@ public DockerfileBuilder() /// public IReadOnlyList Stages => _stages.AsReadOnly(); + /// + /// Adds a global ARG statement to define a build-time variable before any stages. + /// + /// The name of the build argument. + /// The current DockerfileBuilder instance for method chaining. + /// + /// Global ARG statements appear before the first FROM statement and can be used + /// to parameterize the base image selection. + /// + [Experimental("ASPIREDOCKERFILEBUILDER001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] + public DockerfileBuilder Arg(string name) + { + ArgumentException.ThrowIfNullOrEmpty(name); + + _globalArgs.Add(new DockerfileArgStatement(name)); + return this; + } + + /// + /// Adds a global ARG statement to define a build-time variable with a default value before any stages. + /// + /// The name of the build argument. + /// The default value for the build argument. + /// The current DockerfileBuilder instance for method chaining. + /// + /// Global ARG statements appear before the first FROM statement and can be used + /// to parameterize the base image selection. + /// + [Experimental("ASPIREDOCKERFILEBUILDER001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] + public DockerfileBuilder Arg(string name, string defaultValue) + { + ArgumentException.ThrowIfNullOrEmpty(name); + ArgumentException.ThrowIfNullOrEmpty(defaultValue); + + _globalArgs.Add(new DockerfileArgStatement(name, defaultValue)); + return this; + } + /// /// Adds a FROM statement to start a new named stage. /// @@ -66,6 +105,18 @@ public DockerfileStage From(string image) public async Task WriteAsync(StreamWriter writer, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(writer); + + // Write global ARG statements first + if (_globalArgs.Count > 0) + { + foreach (var arg in _globalArgs) + { + await arg.WriteStatementAsync(writer, cancellationToken).ConfigureAwait(false); + } + + // Add a blank line after global args + await writer.WriteLineAsync().ConfigureAwait(false); + } foreach (var stage in _stages) { @@ -80,4 +131,4 @@ public async Task WriteAsync(StreamWriter writer, CancellationToken cancellation await writer.FlushAsync(cancellationToken).ConfigureAwait(false); } -} \ No newline at end of file +} diff --git a/src/Aspire.Hosting/ApplicationModel/ProjectResource.cs b/src/Aspire.Hosting/ApplicationModel/ProjectResource.cs index 7db967b9bd7..eb089c61016 100644 --- a/src/Aspire.Hosting/ApplicationModel/ProjectResource.cs +++ b/src/Aspire.Hosting/ApplicationModel/ProjectResource.cs @@ -121,6 +121,8 @@ await containerImageBuilder.BuildImageAsync( // Generate a Dockerfile that layers the container files on top var dockerfileBuilder = new DockerfileBuilder(); + dockerfileBuilder.AddContainerFilesStages(this, logger); + var stage = dockerfileBuilder.From(tempImageName); var projectMetadata = this.GetProjectMetadata(); diff --git a/tests/Aspire.Hosting.JavaScript.Tests/AddNodeAppTests.cs b/tests/Aspire.Hosting.JavaScript.Tests/AddNodeAppTests.cs index 3d7e341205c..dd1a889b740 100644 --- a/tests/Aspire.Hosting.JavaScript.Tests/AddNodeAppTests.cs +++ b/tests/Aspire.Hosting.JavaScript.Tests/AddNodeAppTests.cs @@ -335,9 +335,7 @@ public async Task VerifyNodeAppWithContainerFilesGeneratesCorrectDockerfile() var dockerfileContent = File.ReadAllText(nodeDockerfilePath); - // Verify that the Dockerfile includes the COPY --from statement for container files - // Note: The image name is prefixed with the resource name, so it's "source:source-tag" - Assert.Contains("COPY --from=source:source-tag /app/dist /app/./static", dockerfileContent); + await Verify(dockerfileContent); } private sealed class MyFilesContainer(string name, string command, string workingDirectory) diff --git a/tests/Aspire.Hosting.JavaScript.Tests/Snapshots/AddNodeAppTests.VerifyNodeAppWithContainerFilesGeneratesCorrectDockerfile.verified.txt b/tests/Aspire.Hosting.JavaScript.Tests/Snapshots/AddNodeAppTests.VerifyNodeAppWithContainerFilesGeneratesCorrectDockerfile.verified.txt new file mode 100644 index 00000000000..c9fca74f3c9 --- /dev/null +++ b/tests/Aspire.Hosting.JavaScript.Tests/Snapshots/AddNodeAppTests.VerifyNodeAppWithContainerFilesGeneratesCorrectDockerfile.verified.txt @@ -0,0 +1,24 @@ +ARG source_IMAGENAME=source:source-tag + +FROM node:22-alpine AS build + +WORKDIR /app +COPY . . + +RUN npm install + +FROM ${source_IMAGENAME} AS source_stage + +FROM node:22-alpine AS runtime + +WORKDIR /app +COPY --from=build /app /app +COPY --from=source_stage /app/dist /app/./static + + +ENV NODE_ENV=production +EXPOSE 3000 + +USER node + +ENTRYPOINT ["node","app.js"] diff --git a/tests/Aspire.Hosting.Python.Tests/Snapshots/AddUvicornAppTests.WithUv_GeneratesDockerfileInPublishMode.verified.txt b/tests/Aspire.Hosting.Python.Tests/Snapshots/AddUvicornAppTests.WithUv_GeneratesDockerfileInPublishMode.verified.txt index 46f91fa3f97..7465cb440de 100644 --- a/tests/Aspire.Hosting.Python.Tests/Snapshots/AddUvicornAppTests.WithUv_GeneratesDockerfileInPublishMode.verified.txt +++ b/tests/Aspire.Hosting.Python.Tests/Snapshots/AddUvicornAppTests.WithUv_GeneratesDockerfileInPublishMode.verified.txt @@ -1,4 +1,6 @@ -FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS builder +ARG exe_IMAGENAME=exe:deterministc-tag + +FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS builder # Enable bytecode compilation and copy mode for the virtual environment ENV UV_COMPILE_BYTECODE=1 @@ -16,9 +18,11 @@ COPY . /app RUN --mount=type=cache,target=/root/.cache/uv \ uv sync --locked --no-dev +FROM ${exe_IMAGENAME} AS exe_stage + FROM python:3.12-slim-bookworm AS app -COPY --from=exe:deterministc-tag /app/dist /app/./static +COPY --from=exe_stage /app/dist /app/./static # ------------------------------ # 🚀 Runtime stage diff --git a/tests/Aspire.Hosting.Tests/ProjectResourceTests.cs b/tests/Aspire.Hosting.Tests/ProjectResourceTests.cs index 30bc57df18b..ebce20f65ae 100644 --- a/tests/Aspire.Hosting.Tests/ProjectResourceTests.cs +++ b/tests/Aspire.Hosting.Tests/ProjectResourceTests.cs @@ -7,6 +7,7 @@ #pragma warning disable ASPIRECONTAINERRUNTIME001 using System.Text; +using System.Text.RegularExpressions; using Aspire.Hosting.Pipelines; using Aspire.Hosting.Publishing; using Aspire.Hosting.Testing; @@ -819,7 +820,21 @@ public async Task ProjectResourceWithContainerFilesDestinationAnnotationWorks() builder.AddProject("projectName", launchProfileName: null) .PublishWithContainerFiles(sourceContainer, "./wwwroot"); + var dockerfileVerified = false; + using var app = builder.Build(); + var fakeContainerRuntime = (FakeContainerRuntime)app.Services.GetRequiredService(); + + fakeContainerRuntime.BuildImageAsyncCallback = async (contextPath, dockerfilePath, imageName, options, buildArgs, buildSecrets, stage, cancellationToken) => + { + // Verify that the Dockerfile contains the expected COPY command + var dockerFileContent = File.ReadAllText(dockerfilePath); + await Verify(dockerFileContent) + .ScrubLinesWithReplace(s => Regex.Replace(s, "FROM projectname:temp-.*", "FROM projectname:temp-")); + + dockerfileVerified = true; + }; + await app.StartAsync(); await app.WaitForShutdownAsync(); @@ -829,7 +844,6 @@ public async Task ProjectResourceWithContainerFilesDestinationAnnotationWorks() Assert.Equal("projectName", builtImage.Name); Assert.False(mockImageBuilder.PushImageCalled); - var fakeContainerRuntime = (FakeContainerRuntime)app.Services.GetRequiredService(); Assert.True(fakeContainerRuntime.WasTagImageCalled); var tagCall = Assert.Single(fakeContainerRuntime.TagImageCalls); Assert.Equal("projectname", tagCall.localImageName); @@ -841,6 +855,8 @@ public async Task ProjectResourceWithContainerFilesDestinationAnnotationWorks() Assert.Empty(buildCall.contextPath); Assert.NotEmpty(buildCall.dockerfilePath); + Assert.True(dockerfileVerified); + Assert.True(fakeContainerRuntime.WasRemoveImageCalled); var removeCall = Assert.Single(fakeContainerRuntime.RemoveImageCalls); Assert.StartsWith("projectname:temp-", removeCall); diff --git a/tests/Aspire.Hosting.Tests/Publishing/FakeContainerRuntime.cs b/tests/Aspire.Hosting.Tests/Publishing/FakeContainerRuntime.cs index 59036886156..8f5878772be 100644 --- a/tests/Aspire.Hosting.Tests/Publishing/FakeContainerRuntime.cs +++ b/tests/Aspire.Hosting.Tests/Publishing/FakeContainerRuntime.cs @@ -25,6 +25,7 @@ public sealed class FakeContainerRuntime(bool shouldFail = false) : IContainerRu public Dictionary? CapturedBuildArguments { get; private set; } public Dictionary? CapturedBuildSecrets { get; private set; } public string? CapturedStage { get; private set; } + public Func, Dictionary, string?, CancellationToken, Task>? BuildImageAsyncCallback { get; set; } public Task CheckIfRunningAsync(CancellationToken cancellationToken) { @@ -65,7 +66,7 @@ public Task PushImageAsync(string imageName, CancellationToken cancellationToken return Task.CompletedTask; } - public Task BuildImageAsync(string contextPath, string dockerfilePath, string imageName, ContainerBuildOptions? options, Dictionary buildArguments, Dictionary buildSecrets, string? stage, CancellationToken cancellationToken) + public async Task BuildImageAsync(string contextPath, string dockerfilePath, string imageName, ContainerBuildOptions? options, Dictionary buildArguments, Dictionary buildSecrets, string? stage, CancellationToken cancellationToken) { // Capture the arguments for verification in tests CapturedBuildArguments = buildArguments; @@ -79,8 +80,12 @@ public Task BuildImageAsync(string contextPath, string dockerfilePath, string im throw new InvalidOperationException("Fake container runtime is configured to fail"); } + if (BuildImageAsyncCallback is not null) + { + await BuildImageAsyncCallback(contextPath, dockerfilePath, imageName, options, buildArguments, buildSecrets, stage, cancellationToken); + } + // For testing, we don't need to actually build anything - return Task.CompletedTask; } public Task LoginToRegistryAsync(string registryServer, string username, string password, CancellationToken cancellationToken) diff --git a/tests/Aspire.Hosting.Tests/Snapshots/ProjectResourceTests.ProjectResourceWithContainerFilesDestinationAnnotationWorks.verified.txt b/tests/Aspire.Hosting.Tests/Snapshots/ProjectResourceTests.ProjectResourceWithContainerFilesDestinationAnnotationWorks.verified.txt new file mode 100644 index 00000000000..95d28cc7fa1 --- /dev/null +++ b/tests/Aspire.Hosting.Tests/Snapshots/ProjectResourceTests.ProjectResourceWithContainerFilesDestinationAnnotationWorks.verified.txt @@ -0,0 +1,7 @@ +ARG source_IMAGENAME=myimage:latest + +FROM ${source_IMAGENAME} AS source_stage + +FROM projectname:temp- +COPY --from=source_stage /app/dist /app/./wwwroot + diff --git a/tests/Aspire.Hosting.Yarp.Tests/Snapshots/AddYarpTests.VerifyPublishWithStaticFilesGeneratesCorrectDockerfile.verified.txt b/tests/Aspire.Hosting.Yarp.Tests/Snapshots/AddYarpTests.VerifyPublishWithStaticFilesGeneratesCorrectDockerfile.verified.txt index 4203ad83578..ea8a2e38a42 100644 --- a/tests/Aspire.Hosting.Yarp.Tests/Snapshots/AddYarpTests.VerifyPublishWithStaticFilesGeneratesCorrectDockerfile.verified.txt +++ b/tests/Aspire.Hosting.Yarp.Tests/Snapshots/AddYarpTests.VerifyPublishWithStaticFilesGeneratesCorrectDockerfile.verified.txt @@ -1,4 +1,8 @@ -FROM mcr.microsoft.com/dotnet/nightly/yarp:2.3.0-preview.4 +ARG source_IMAGENAME=sourceimage:latest + +FROM ${source_IMAGENAME} AS source_stage + +FROM mcr.microsoft.com/dotnet/nightly/yarp:2.3.0-preview.4 WORKDIR /app -COPY --from=sourceimage:latest /app/dist /app/wwwroot +COPY --from=source_stage /app/dist /app/wwwroot diff --git a/tests/Aspire.Hosting.Yarp.Tests/Snapshots/AddYarpTests.VerifyPublishWithStaticFilesGeneratesCorrectDockerfileWithMultipleContainerFiles.verified.txt b/tests/Aspire.Hosting.Yarp.Tests/Snapshots/AddYarpTests.VerifyPublishWithStaticFilesGeneratesCorrectDockerfileWithMultipleContainerFiles.verified.txt index 0cd85cacd3c..ce2bc3b11d3 100644 --- a/tests/Aspire.Hosting.Yarp.Tests/Snapshots/AddYarpTests.VerifyPublishWithStaticFilesGeneratesCorrectDockerfileWithMultipleContainerFiles.verified.txt +++ b/tests/Aspire.Hosting.Yarp.Tests/Snapshots/AddYarpTests.VerifyPublishWithStaticFilesGeneratesCorrectDockerfileWithMultipleContainerFiles.verified.txt @@ -1,5 +1,9 @@ -FROM mcr.microsoft.com/dotnet/nightly/yarp:2.3.0-preview.4 +ARG source_IMAGENAME=sourceimage:latest + +FROM ${source_IMAGENAME} AS source_stage + +FROM mcr.microsoft.com/dotnet/nightly/yarp:2.3.0-preview.4 WORKDIR /app -COPY --from=sourceimage:latest /app/dist /app/wwwroot -COPY --from=sourceimage:latest /app/assets /app/wwwroot +COPY --from=source_stage /app/dist /app/wwwroot +COPY --from=source_stage /app/assets /app/wwwroot diff --git a/tests/Aspire.Hosting.Yarp.Tests/Snapshots/AddYarpTests.VerifyPublishWithStaticFilesGeneratesCorrectDockerfileWithMultipleSourceContainerFiles.verified.txt b/tests/Aspire.Hosting.Yarp.Tests/Snapshots/AddYarpTests.VerifyPublishWithStaticFilesGeneratesCorrectDockerfileWithMultipleSourceContainerFiles.verified.txt index 3a77a7f4d6b..f78bb9e95bc 100644 --- a/tests/Aspire.Hosting.Yarp.Tests/Snapshots/AddYarpTests.VerifyPublishWithStaticFilesGeneratesCorrectDockerfileWithMultipleSourceContainerFiles.verified.txt +++ b/tests/Aspire.Hosting.Yarp.Tests/Snapshots/AddYarpTests.VerifyPublishWithStaticFilesGeneratesCorrectDockerfileWithMultipleSourceContainerFiles.verified.txt @@ -1,7 +1,14 @@ -FROM mcr.microsoft.com/dotnet/nightly/yarp:2.3.0-preview.4 +ARG source1_IMAGENAME=sourceimage:latest +ARG source2_IMAGENAME=sourceimage2:latest + +FROM ${source1_IMAGENAME} AS source1_stage + +FROM ${source2_IMAGENAME} AS source2_stage + +FROM mcr.microsoft.com/dotnet/nightly/yarp:2.3.0-preview.4 WORKDIR /app -COPY --from=sourceimage:latest /app/dist /app/wwwroot -COPY --from=sourceimage:latest /app/assets /app/wwwroot -COPY --from=sourceimage2:latest /app/dist2 /app/wwwroot -COPY --from=sourceimage2:latest /app/assets2 /app/wwwroot +COPY --from=source1_stage /app/dist /app/wwwroot +COPY --from=source1_stage /app/assets /app/wwwroot +COPY --from=source2_stage /app/dist2 /app/wwwroot +COPY --from=source2_stage /app/assets2 /app/wwwroot From c4ca22795cad13c2a3cc4737f82c8f961cecaaef Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Wed, 5 Nov 2025 17:58:47 -0600 Subject: [PATCH 2/2] Address PR feedback --- .../ApplicationModel/Docker/ContainerFilesExtensions.cs | 7 ++----- ...hContainerFilesGeneratesCorrectDockerfile.verified.txt | 4 ++-- ...s.WithUv_GeneratesDockerfileInPublishMode.verified.txt | 4 ++-- ...hContainerFilesDestinationAnnotationWorks.verified.txt | 4 ++-- ...WithStaticFilesGeneratesCorrectDockerfile.verified.txt | 4 ++-- ...rrectDockerfileWithMultipleContainerFiles.verified.txt | 4 ++-- ...ockerfileWithMultipleSourceContainerFiles.verified.txt | 8 ++++---- 7 files changed, 16 insertions(+), 19 deletions(-) diff --git a/src/Aspire.Hosting/ApplicationModel/Docker/ContainerFilesExtensions.cs b/src/Aspire.Hosting/ApplicationModel/Docker/ContainerFilesExtensions.cs index 60fe13e485c..e8e1d9266ee 100644 --- a/src/Aspire.Hosting/ApplicationModel/Docker/ContainerFilesExtensions.cs +++ b/src/Aspire.Hosting/ApplicationModel/Docker/ContainerFilesExtensions.cs @@ -15,9 +15,6 @@ public static class ContainerFilesExtensions /// Adds Dockerfile instructions to include container files from the specified resource into the Dockerfile build /// process. /// - /// If the resource does not provide valid container image names for its sources, those sources - /// are skipped and a warning is logged if a logger is provided. This method is experimental and its behavior may - /// change in future releases. /// The Dockerfile builder to which container file instructions will be added. Cannot be null. /// The resource containing container files to be added to the Dockerfile. Cannot be null. /// An optional logger used to record warnings if container image names cannot be determined for source resources. @@ -68,7 +65,7 @@ public static DockerfileBuilder AddContainerFilesStages(this DockerfileBuilder b /// For each annotation: /// /// If the source resource has a container image name (via TryGetContainerImageName), COPY statements are generated - /// If the source resource does not have a container image name, it is silently skipped + /// If the source resource does not have a container image name, it is skipped /// Relative destination paths are combined with /// Absolute destination paths are used as-is /// Each on the source resource generates a COPY statement @@ -120,7 +117,7 @@ public static DockerfileStage AddContainerFiles(this DockerfileStage stage, IRes return stage; } - private static string GetSourceImageArgName(IResource source) => $"{source.Name}_IMAGENAME"; + private static string GetSourceImageArgName(IResource source) => $"{source.Name.ToUpperInvariant()}_IMAGENAME"; private static string GetSourceStageName(IResource source) => $"{source.Name}_stage"; } diff --git a/tests/Aspire.Hosting.JavaScript.Tests/Snapshots/AddNodeAppTests.VerifyNodeAppWithContainerFilesGeneratesCorrectDockerfile.verified.txt b/tests/Aspire.Hosting.JavaScript.Tests/Snapshots/AddNodeAppTests.VerifyNodeAppWithContainerFilesGeneratesCorrectDockerfile.verified.txt index c9fca74f3c9..c78cc15674d 100644 --- a/tests/Aspire.Hosting.JavaScript.Tests/Snapshots/AddNodeAppTests.VerifyNodeAppWithContainerFilesGeneratesCorrectDockerfile.verified.txt +++ b/tests/Aspire.Hosting.JavaScript.Tests/Snapshots/AddNodeAppTests.VerifyNodeAppWithContainerFilesGeneratesCorrectDockerfile.verified.txt @@ -1,4 +1,4 @@ -ARG source_IMAGENAME=source:source-tag +ARG SOURCE_IMAGENAME=source:source-tag FROM node:22-alpine AS build @@ -7,7 +7,7 @@ COPY . . RUN npm install -FROM ${source_IMAGENAME} AS source_stage +FROM ${SOURCE_IMAGENAME} AS source_stage FROM node:22-alpine AS runtime diff --git a/tests/Aspire.Hosting.Python.Tests/Snapshots/AddUvicornAppTests.WithUv_GeneratesDockerfileInPublishMode.verified.txt b/tests/Aspire.Hosting.Python.Tests/Snapshots/AddUvicornAppTests.WithUv_GeneratesDockerfileInPublishMode.verified.txt index 7465cb440de..2f08cbbf4d3 100644 --- a/tests/Aspire.Hosting.Python.Tests/Snapshots/AddUvicornAppTests.WithUv_GeneratesDockerfileInPublishMode.verified.txt +++ b/tests/Aspire.Hosting.Python.Tests/Snapshots/AddUvicornAppTests.WithUv_GeneratesDockerfileInPublishMode.verified.txt @@ -1,4 +1,4 @@ -ARG exe_IMAGENAME=exe:deterministc-tag +ARG EXE_IMAGENAME=exe:deterministc-tag FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS builder @@ -18,7 +18,7 @@ COPY . /app RUN --mount=type=cache,target=/root/.cache/uv \ uv sync --locked --no-dev -FROM ${exe_IMAGENAME} AS exe_stage +FROM ${EXE_IMAGENAME} AS exe_stage FROM python:3.12-slim-bookworm AS app diff --git a/tests/Aspire.Hosting.Tests/Snapshots/ProjectResourceTests.ProjectResourceWithContainerFilesDestinationAnnotationWorks.verified.txt b/tests/Aspire.Hosting.Tests/Snapshots/ProjectResourceTests.ProjectResourceWithContainerFilesDestinationAnnotationWorks.verified.txt index 95d28cc7fa1..0000645836a 100644 --- a/tests/Aspire.Hosting.Tests/Snapshots/ProjectResourceTests.ProjectResourceWithContainerFilesDestinationAnnotationWorks.verified.txt +++ b/tests/Aspire.Hosting.Tests/Snapshots/ProjectResourceTests.ProjectResourceWithContainerFilesDestinationAnnotationWorks.verified.txt @@ -1,6 +1,6 @@ -ARG source_IMAGENAME=myimage:latest +ARG SOURCE_IMAGENAME=myimage:latest -FROM ${source_IMAGENAME} AS source_stage +FROM ${SOURCE_IMAGENAME} AS source_stage FROM projectname:temp- COPY --from=source_stage /app/dist /app/./wwwroot diff --git a/tests/Aspire.Hosting.Yarp.Tests/Snapshots/AddYarpTests.VerifyPublishWithStaticFilesGeneratesCorrectDockerfile.verified.txt b/tests/Aspire.Hosting.Yarp.Tests/Snapshots/AddYarpTests.VerifyPublishWithStaticFilesGeneratesCorrectDockerfile.verified.txt index ea8a2e38a42..4c65c9d1dbb 100644 --- a/tests/Aspire.Hosting.Yarp.Tests/Snapshots/AddYarpTests.VerifyPublishWithStaticFilesGeneratesCorrectDockerfile.verified.txt +++ b/tests/Aspire.Hosting.Yarp.Tests/Snapshots/AddYarpTests.VerifyPublishWithStaticFilesGeneratesCorrectDockerfile.verified.txt @@ -1,6 +1,6 @@ -ARG source_IMAGENAME=sourceimage:latest +ARG SOURCE_IMAGENAME=sourceimage:latest -FROM ${source_IMAGENAME} AS source_stage +FROM ${SOURCE_IMAGENAME} AS source_stage FROM mcr.microsoft.com/dotnet/nightly/yarp:2.3.0-preview.4 WORKDIR /app diff --git a/tests/Aspire.Hosting.Yarp.Tests/Snapshots/AddYarpTests.VerifyPublishWithStaticFilesGeneratesCorrectDockerfileWithMultipleContainerFiles.verified.txt b/tests/Aspire.Hosting.Yarp.Tests/Snapshots/AddYarpTests.VerifyPublishWithStaticFilesGeneratesCorrectDockerfileWithMultipleContainerFiles.verified.txt index ce2bc3b11d3..92a37e3e372 100644 --- a/tests/Aspire.Hosting.Yarp.Tests/Snapshots/AddYarpTests.VerifyPublishWithStaticFilesGeneratesCorrectDockerfileWithMultipleContainerFiles.verified.txt +++ b/tests/Aspire.Hosting.Yarp.Tests/Snapshots/AddYarpTests.VerifyPublishWithStaticFilesGeneratesCorrectDockerfileWithMultipleContainerFiles.verified.txt @@ -1,6 +1,6 @@ -ARG source_IMAGENAME=sourceimage:latest +ARG SOURCE_IMAGENAME=sourceimage:latest -FROM ${source_IMAGENAME} AS source_stage +FROM ${SOURCE_IMAGENAME} AS source_stage FROM mcr.microsoft.com/dotnet/nightly/yarp:2.3.0-preview.4 WORKDIR /app diff --git a/tests/Aspire.Hosting.Yarp.Tests/Snapshots/AddYarpTests.VerifyPublishWithStaticFilesGeneratesCorrectDockerfileWithMultipleSourceContainerFiles.verified.txt b/tests/Aspire.Hosting.Yarp.Tests/Snapshots/AddYarpTests.VerifyPublishWithStaticFilesGeneratesCorrectDockerfileWithMultipleSourceContainerFiles.verified.txt index f78bb9e95bc..cf7caff77be 100644 --- a/tests/Aspire.Hosting.Yarp.Tests/Snapshots/AddYarpTests.VerifyPublishWithStaticFilesGeneratesCorrectDockerfileWithMultipleSourceContainerFiles.verified.txt +++ b/tests/Aspire.Hosting.Yarp.Tests/Snapshots/AddYarpTests.VerifyPublishWithStaticFilesGeneratesCorrectDockerfileWithMultipleSourceContainerFiles.verified.txt @@ -1,9 +1,9 @@ -ARG source1_IMAGENAME=sourceimage:latest -ARG source2_IMAGENAME=sourceimage2:latest +ARG SOURCE1_IMAGENAME=sourceimage:latest +ARG SOURCE2_IMAGENAME=sourceimage2:latest -FROM ${source1_IMAGENAME} AS source1_stage +FROM ${SOURCE1_IMAGENAME} AS source1_stage -FROM ${source2_IMAGENAME} AS source2_stage +FROM ${SOURCE2_IMAGENAME} AS source2_stage FROM mcr.microsoft.com/dotnet/nightly/yarp:2.3.0-preview.4 WORKDIR /app