Skip to content

Commit cd9a23f

Browse files
Copiloteerhardt
andcommitted
Refactor to build project image first, then layer container files via Dockerfile
- Build ProjectResource image with normal flow first - Tag built image with temporary GUID tag - Generate Dockerfile that FROMs temp image and COPY --from source containers - Build final image from generated Dockerfile with real tag - Removed CopyContainerFilesAsync from IContainerRuntime (no longer needed) - Removed GetPublishDirectoryAsync helper (no longer needed) Co-authored-by: eerhardt <8291187+eerhardt@users.noreply.github.com>
1 parent 6719d44 commit cd9a23f

File tree

1 file changed

+110
-137
lines changed

1 file changed

+110
-137
lines changed

src/Aspire.Hosting/ProjectResourceBuilderExtensions.cs

Lines changed: 110 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@
44
#pragma warning disable ASPIREEXTENSION001
55
#pragma warning disable ASPIREPIPELINES001
66
#pragma warning disable ASPIREPUBLISHERS001
7+
#pragma warning disable ASPIREDOCKERFILEBUILDER001
78
using System.Diagnostics;
89
using System.Diagnostics.CodeAnalysis;
910
using Aspire.Hosting.ApplicationModel;
11+
using Aspire.Hosting.ApplicationModel.Docker;
1012
using Aspire.Hosting.Dashboard;
1113
using Aspire.Hosting.Dcp;
1214
using Aspire.Hosting.Dcp.Model;
13-
using Aspire.Hosting.Dcp.Process;
1415
using Aspire.Hosting.Pipelines;
1516
using Aspire.Hosting.Publishing;
1617
using Aspire.Hosting.Utils;
@@ -1096,162 +1097,134 @@ private static PipelineStep CreateProjectBuildImageStep(string stepName, IResour
10961097
Name = stepName,
10971098
Action = async ctx =>
10981099
{
1099-
// Copy files from source containers if ContainerFilesDestinationAnnotation is present
1100-
if (resource.TryGetAnnotationsOfType<ContainerFilesDestinationAnnotation>(out var containerFilesAnnotations))
1100+
var containerImageBuilder = ctx.Services.GetRequiredService<IResourceContainerImageBuilder>();
1101+
var logger = ctx.Services.GetRequiredService<ILoggerFactory>().CreateLogger(typeof(ProjectResourceBuilderExtensions));
1102+
1103+
// Check if we need to copy container files
1104+
var hasContainerFiles = resource.TryGetAnnotationsOfType<ContainerFilesDestinationAnnotation>(out var containerFilesAnnotations);
1105+
1106+
if (!hasContainerFiles)
11011107
{
1102-
await CopyContainerFilesToProjectAsync(resource, containerFilesAnnotations, ctx.Services, ctx.CancellationToken).ConfigureAwait(false);
1108+
// No container files to copy, just build the image normally
1109+
await containerImageBuilder.BuildImageAsync(
1110+
resource,
1111+
new ContainerBuildOptions
1112+
{
1113+
TargetPlatform = ContainerTargetPlatform.LinuxAmd64
1114+
},
1115+
ctx.CancellationToken).ConfigureAwait(false);
1116+
return;
11031117
}
1104-
1105-
// Build the container image for the project
1106-
var containerImageBuilder = ctx.Services.GetRequiredService<IResourceContainerImageBuilder>();
1118+
1119+
// Build the container image for the project first
11071120
await containerImageBuilder.BuildImageAsync(
11081121
resource,
11091122
new ContainerBuildOptions
11101123
{
11111124
TargetPlatform = ContainerTargetPlatform.LinuxAmd64
11121125
},
11131126
ctx.CancellationToken).ConfigureAwait(false);
1114-
},
1115-
Tags = [WellKnownPipelineTags.BuildCompute]
1116-
};
1117-
1118-
private static async Task CopyContainerFilesToProjectAsync(
1119-
IResource resource,
1120-
IEnumerable<ContainerFilesDestinationAnnotation> containerFilesAnnotations,
1121-
IServiceProvider services,
1122-
CancellationToken cancellationToken)
1123-
{
1124-
var logger = services.GetRequiredService<ILoggerFactory>().CreateLogger(typeof(ProjectResourceBuilderExtensions));
1125-
var projectMetadata = resource.TryGetLastAnnotation<IProjectMetadata>(out var metadata) ? metadata : null;
1126-
1127-
if (projectMetadata == null)
1128-
{
1129-
logger.LogWarning("Project metadata not found for resource {ResourceName}. Cannot copy container files.", resource.Name);
1130-
return;
1131-
}
1132-
1133-
var projectPath = projectMetadata.ProjectPath;
1134-
1135-
// Get the PublishDir using dotnet msbuild
1136-
var publishDir = await GetPublishDirectoryAsync(projectPath, logger, cancellationToken).ConfigureAwait(false);
1137-
1138-
if (publishDir == null)
1139-
{
1140-
logger.LogWarning("Could not determine publish directory for project {ProjectPath}. Cannot copy container files.", projectPath);
1141-
return;
1142-
}
1143-
1144-
// Ensure the publish directory exists
1145-
Directory.CreateDirectory(publishDir);
1146-
1147-
// Get the container runtime
1148-
var dcpOptions = services.GetRequiredService<IOptions<DcpOptions>>();
1149-
var containerRuntime = dcpOptions.Value.ContainerRuntime switch
1150-
{
1151-
string rt => services.GetRequiredKeyedService<IContainerRuntime>(rt),
1152-
null => services.GetRequiredKeyedService<IContainerRuntime>("docker")
1153-
};
1154-
1155-
foreach (var containerFileDestination in containerFilesAnnotations)
1156-
{
1157-
var source = containerFileDestination.Source;
1158-
1159-
// Get the image name from the source resource
1160-
if (!source.TryGetContainerImageName(out var imageName))
1161-
{
1162-
logger.LogWarning("Cannot copy container files from {SourceName}: Source resource does not have a container image name.", source.Name);
1163-
continue;
1164-
}
1165-
1166-
logger.LogInformation("Copying container files from {ImageName} to {PublishDir}", imageName, publishDir);
1167-
1168-
// For each ContainerFilesSourceAnnotation on the source resource, copy the files
1169-
foreach (var containerFilesSource in source.Annotations.OfType<ContainerFilesSourceAnnotation>())
1170-
{
1171-
var sourcePath = containerFilesSource.SourcePath;
1172-
var destinationPath = containerFileDestination.DestinationPath;
1173-
1174-
// If destination path is relative, make it relative to the publish directory
1175-
if (!Path.IsPathRooted(destinationPath))
1176-
{
1177-
destinationPath = Path.Combine(publishDir, destinationPath);
1178-
}
1179-
1180-
// Ensure the destination directory exists
1181-
Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!);
1182-
1183-
try
1127+
1128+
// Get the built image name
1129+
if (!resource.TryGetContainerImageName(out var originalImageName))
11841130
{
1185-
await containerRuntime.CopyContainerFilesAsync(imageName, sourcePath, destinationPath, cancellationToken).ConfigureAwait(false);
1186-
logger.LogInformation("Successfully copied files from {ImageName}:{SourcePath} to {DestinationPath}",
1187-
imageName, sourcePath, destinationPath);
1131+
logger.LogError("Cannot get container image name for resource {ResourceName}", resource.Name);
1132+
throw new InvalidOperationException($"Cannot get container image name for resource {resource.Name}");
11881133
}
1189-
catch (Exception ex)
1134+
1135+
// Tag the built image with a temporary tag
1136+
var tempTag = $"temp-{Guid.NewGuid():N}";
1137+
var tempImageName = $"{originalImageName.Split(':')[0]}:{tempTag}";
1138+
1139+
var dcpOptions = ctx.Services.GetRequiredService<IOptions<DcpOptions>>();
1140+
var containerRuntime = dcpOptions.Value.ContainerRuntime switch
11901141
{
1191-
logger.LogError(ex, "Error copying files from {ImageName}:{SourcePath} to {DestinationPath}",
1192-
imageName, sourcePath, destinationPath);
1193-
}
1194-
}
1195-
}
1196-
}
1197-
1198-
private static async Task<string?> GetPublishDirectoryAsync(string projectPath, ILogger logger, CancellationToken cancellationToken)
1199-
{
1200-
try
1201-
{
1202-
var outputLines = new List<string>();
1203-
var spec = new ProcessSpec("dotnet")
1204-
{
1205-
Arguments = $"msbuild -p:Configuration=Release -getProperty:PublishDir \"{projectPath}\"",
1206-
OnOutputData = output =>
1142+
string rt => ctx.Services.GetRequiredKeyedService<IContainerRuntime>(rt),
1143+
null => ctx.Services.GetRequiredKeyedService<IContainerRuntime>("docker")
1144+
};
1145+
1146+
logger.LogInformation("Tagging image {OriginalImageName} as {TempImageName}", originalImageName, tempImageName);
1147+
await containerRuntime.TagImageAsync(originalImageName, tempImageName, ctx.CancellationToken).ConfigureAwait(false);
1148+
1149+
// Generate a Dockerfile that layers the container files on top
1150+
var dockerfileBuilder = new DockerfileBuilder();
1151+
var stage = dockerfileBuilder.From(tempImageName);
1152+
1153+
// Add COPY --from: statements for each source
1154+
foreach (var containerFileDestination in containerFilesAnnotations!)
12071155
{
1208-
if (!string.IsNullOrWhiteSpace(output))
1156+
var source = containerFileDestination.Source;
1157+
1158+
if (!source.TryGetContainerImageName(out var sourceImageName))
12091159
{
1210-
outputLines.Add(output.Trim());
1160+
logger.LogWarning("Cannot get container image name for source resource {SourceName}, skipping", source.Name);
1161+
continue;
1162+
}
1163+
1164+
var destinationPath = containerFileDestination.DestinationPath;
1165+
if (!destinationPath.StartsWith('/'))
1166+
{
1167+
// Make it an absolute path relative to /app (typical .NET container working directory)
1168+
destinationPath = $"/app/{destinationPath}";
1169+
}
1170+
1171+
foreach (var containerFilesSource in source.Annotations.OfType<ContainerFilesSourceAnnotation>())
1172+
{
1173+
logger.LogInformation("Adding COPY --from={SourceImage} {SourcePath} {DestinationPath}",
1174+
sourceImageName, containerFilesSource.SourcePath, destinationPath);
1175+
stage.CopyFrom(sourceImageName, containerFilesSource.SourcePath, destinationPath);
12111176
}
1212-
},
1213-
OnErrorData = error => logger.LogDebug("dotnet msbuild (stderr): {Error}", error),
1214-
ThrowOnNonZeroReturnCode = false
1215-
};
1216-
1217-
logger.LogDebug("Running dotnet msbuild to get PublishDir for project {ProjectPath}", projectPath);
1218-
var (pendingResult, processDisposable) = ProcessUtil.Run(spec);
1219-
1220-
await using (processDisposable.ConfigureAwait(false))
1221-
{
1222-
var result = await pendingResult.WaitAsync(cancellationToken).ConfigureAwait(false);
1223-
1224-
if (result.ExitCode != 0)
1225-
{
1226-
logger.LogWarning("Failed to get PublishDir from dotnet msbuild for project {ProjectPath}. Exit code: {ExitCode}",
1227-
projectPath, result.ExitCode);
1228-
return null;
12291177
}
1230-
1231-
// The last non-empty line should contain the PublishDir value
1232-
var publishDir = outputLines.LastOrDefault();
12331178

1234-
if (string.IsNullOrWhiteSpace(publishDir))
1179+
// Write the Dockerfile to a temporary location
1180+
var projectResource = (ProjectResource)resource;
1181+
var projectMetadata = projectResource.GetProjectMetadata();
1182+
var projectDir = Path.GetDirectoryName(projectMetadata.ProjectPath)!;
1183+
var tempDockerfilePath = Path.Combine(projectDir, $"Dockerfile.{Guid.NewGuid():N}");
1184+
1185+
try
12351186
{
1236-
logger.LogWarning("dotnet msbuild returned empty PublishDir for project {ProjectPath}", projectPath);
1237-
return null;
1187+
using (var writer = new StreamWriter(tempDockerfilePath))
1188+
{
1189+
await dockerfileBuilder.WriteAsync(writer, ctx.CancellationToken).ConfigureAwait(false);
1190+
}
1191+
1192+
logger.LogDebug("Generated temporary Dockerfile at {DockerfilePath}", tempDockerfilePath);
1193+
1194+
// Build the final image from the generated Dockerfile
1195+
await containerRuntime.BuildImageAsync(
1196+
projectDir,
1197+
tempDockerfilePath,
1198+
originalImageName,
1199+
new ContainerBuildOptions
1200+
{
1201+
TargetPlatform = ContainerTargetPlatform.LinuxAmd64
1202+
},
1203+
new Dictionary<string, string?>(),
1204+
new Dictionary<string, string?>(),
1205+
null,
1206+
ctx.CancellationToken).ConfigureAwait(false);
1207+
1208+
logger.LogInformation("Successfully built final image {ImageName} with container files", originalImageName);
12381209
}
1239-
1240-
// Make it an absolute path if it's relative
1241-
if (!Path.IsPathRooted(publishDir))
1210+
finally
12421211
{
1243-
var projectDir = Path.GetDirectoryName(projectPath);
1244-
publishDir = Path.GetFullPath(Path.Combine(projectDir!, publishDir));
1212+
// Clean up the temporary Dockerfile
1213+
if (File.Exists(tempDockerfilePath))
1214+
{
1215+
try
1216+
{
1217+
File.Delete(tempDockerfilePath);
1218+
logger.LogDebug("Deleted temporary Dockerfile {DockerfilePath}", tempDockerfilePath);
1219+
}
1220+
catch (Exception ex)
1221+
{
1222+
logger.LogWarning(ex, "Failed to delete temporary Dockerfile {DockerfilePath}", tempDockerfilePath);
1223+
}
1224+
}
12451225
}
1226+
},
1227+
Tags = [WellKnownPipelineTags.BuildCompute]
1228+
};
12461229

1247-
logger.LogDebug("Resolved PublishDir for project {ProjectPath}: {PublishDir}", projectPath, publishDir);
1248-
return publishDir;
1249-
}
1250-
}
1251-
catch (Exception ex)
1252-
{
1253-
logger.LogError(ex, "Error getting PublishDir for project {ProjectPath}", projectPath);
1254-
return null;
1255-
}
1256-
}
12571230
}

0 commit comments

Comments
 (0)