|
4 | 4 | #pragma warning disable ASPIREEXTENSION001 |
5 | 5 | #pragma warning disable ASPIREPIPELINES001 |
6 | 6 | #pragma warning disable ASPIREPUBLISHERS001 |
| 7 | +#pragma warning disable ASPIREDOCKERFILEBUILDER001 |
7 | 8 | using System.Diagnostics; |
8 | 9 | using System.Diagnostics.CodeAnalysis; |
9 | 10 | using Aspire.Hosting.ApplicationModel; |
| 11 | +using Aspire.Hosting.ApplicationModel.Docker; |
10 | 12 | using Aspire.Hosting.Dashboard; |
11 | 13 | using Aspire.Hosting.Dcp; |
12 | 14 | using Aspire.Hosting.Dcp.Model; |
13 | | -using Aspire.Hosting.Dcp.Process; |
14 | 15 | using Aspire.Hosting.Pipelines; |
15 | 16 | using Aspire.Hosting.Publishing; |
16 | 17 | using Aspire.Hosting.Utils; |
@@ -1096,162 +1097,134 @@ private static PipelineStep CreateProjectBuildImageStep(string stepName, IResour |
1096 | 1097 | Name = stepName, |
1097 | 1098 | Action = async ctx => |
1098 | 1099 | { |
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) |
1101 | 1107 | { |
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; |
1103 | 1117 | } |
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 |
1107 | 1120 | await containerImageBuilder.BuildImageAsync( |
1108 | 1121 | resource, |
1109 | 1122 | new ContainerBuildOptions |
1110 | 1123 | { |
1111 | 1124 | TargetPlatform = ContainerTargetPlatform.LinuxAmd64 |
1112 | 1125 | }, |
1113 | 1126 | 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)) |
1184 | 1130 | { |
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}"); |
1188 | 1133 | } |
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 |
1190 | 1141 | { |
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!) |
1207 | 1155 | { |
1208 | | - if (!string.IsNullOrWhiteSpace(output)) |
| 1156 | + var source = containerFileDestination.Source; |
| 1157 | + |
| 1158 | + if (!source.TryGetContainerImageName(out var sourceImageName)) |
1209 | 1159 | { |
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); |
1211 | 1176 | } |
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; |
1229 | 1177 | } |
1230 | | - |
1231 | | - // The last non-empty line should contain the PublishDir value |
1232 | | - var publishDir = outputLines.LastOrDefault(); |
1233 | 1178 |
|
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 |
1235 | 1186 | { |
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); |
1238 | 1209 | } |
1239 | | - |
1240 | | - // Make it an absolute path if it's relative |
1241 | | - if (!Path.IsPathRooted(publishDir)) |
| 1210 | + finally |
1242 | 1211 | { |
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 | + } |
1245 | 1225 | } |
| 1226 | + }, |
| 1227 | + Tags = [WellKnownPipelineTags.BuildCompute] |
| 1228 | + }; |
1246 | 1229 |
|
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 | | - } |
1257 | 1230 | } |
0 commit comments