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 58% rename from src/Aspire.Hosting/ApplicationModel/Docker/DockerfileStageExtensions.cs rename to src/Aspire.Hosting/ApplicationModel/Docker/ContainerFilesExtensions.cs index 598f9bab422..e8e1d9266ee 100644 --- a/src/Aspire.Hosting/ApplicationModel/Docker/DockerfileStageExtensions.cs +++ b/src/Aspire.Hosting/ApplicationModel/Docker/ContainerFilesExtensions.cs @@ -7,10 +7,47 @@ 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. + /// + /// 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 . /// @@ -28,7 +65,7 @@ public static class DockerfileStageExtensions /// 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 @@ -53,12 +90,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 +107,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 +116,8 @@ public static DockerfileStage AddContainerFiles(this DockerfileStage stage, IRes } return stage; } + + private static string GetSourceImageArgName(IResource source) => $"{source.Name.ToUpperInvariant()}_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..c78cc15674d --- /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..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,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..0000645836a --- /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..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,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..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,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..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,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