diff --git a/src/ImageBuilder.Tests/AnnotateEolDigestsCommandTests.cs b/src/ImageBuilder.Tests/AnnotateEolDigestsCommandTests.cs index 39701a638..add6963a1 100644 --- a/src/ImageBuilder.Tests/AnnotateEolDigestsCommandTests.cs +++ b/src/ImageBuilder.Tests/AnnotateEolDigestsCommandTests.cs @@ -145,11 +145,12 @@ private AnnotateEolDigestsCommand InitializeCommand( AnnotateEolDigestsCommand command = new( loggerServiceMock.Object, lifecycleMetadataServiceMock.Object, - Mock.Of()); + Mock.Of(), + TestHelper.CreateArtifactService(tempFolderContext.Path)); command.Options.RepoPrefix = RepoPrefix; command.Options.AcrName = AcrName; command.Options.EolDigestsListPath = eolDigestsListPath; - command.Options.AnnotationDigestsOutputPath = Path.Combine(tempFolderContext.Path, AnnotationsOutputPath); + command.Options.AnnotationDigestsOutputPath = AnnotationsOutputPath; return command; } diff --git a/src/ImageBuilder.Tests/ArtifactServiceTests.cs b/src/ImageBuilder.Tests/ArtifactServiceTests.cs new file mode 100644 index 000000000..632a61ae1 --- /dev/null +++ b/src/ImageBuilder.Tests/ArtifactServiceTests.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using Microsoft.DotNet.ImageBuilder.Configuration; +using Microsoft.DotNet.ImageBuilder.Tests.Helpers; +using Microsoft.Extensions.Options; +using Shouldly; + +namespace Microsoft.DotNet.ImageBuilder.Tests; + +[TestClass] +public class ArtifactServiceTests +{ + private static readonly string s_outputRoot = Path.Combine(Path.GetTempPath(), "artifacts"); + + [TestMethod] + public void ResolvePath_RelativePath_ResolvesUnderArtifactStagingDirectory() + { + var service = CreateService(new InMemoryFileSystem()); + + string path = service.ResolvePath(Path.Combine("image-info", "input.json")); + + path.ShouldBe(Path.Combine(s_outputRoot, "image-info", "input.json")); + } + + [TestMethod] + public void WriteAllText_WritesUnderArtifactStagingDirectory() + { + var fileSystem = new InMemoryFileSystem(); + var service = CreateService(fileSystem); + string expectedPath = Path.Combine(s_outputRoot, "image-info", "output.json"); + + service.WriteAllText(Path.Combine("image-info", "output.json"), "contents"); + + fileSystem.DirectoriesCreated.ShouldContain(Path.GetDirectoryName(expectedPath)); + fileSystem.GetFileText(expectedPath).ShouldBe("contents"); + } + + [TestMethod] + public void WriteAllText_MissingArtifactStagingDirectory_Throws() + { + var service = new ArtifactService( + new InMemoryFileSystem(), + Options.Create(new BuildConfiguration())); + + InvalidOperationException exception = Should.Throw( + () => service.WriteAllText("output.json", "contents")); + + exception.Message.ShouldContain(nameof(BuildConfiguration.ArtifactStagingDirectory)); + } + + [TestMethod] + public void WriteAllText_PathOutsideArtifactStagingDirectory_Throws() + { + var service = CreateService(new InMemoryFileSystem()); + + Should.Throw( + () => service.WriteAllText(Path.Combine("..", "output.json"), "contents")); + } + + private static ArtifactService CreateService(IFileSystem fileSystem) => + new( + fileSystem, + Options.Create( + new BuildConfiguration + { + ArtifactStagingDirectory = s_outputRoot + })); +} diff --git a/src/ImageBuilder.Tests/CopyAcrImagesCommandTests.cs b/src/ImageBuilder.Tests/CopyAcrImagesCommandTests.cs index 529c5c195..a6eb250cb 100644 --- a/src/ImageBuilder.Tests/CopyAcrImagesCommandTests.cs +++ b/src/ImageBuilder.Tests/CopyAcrImagesCommandTests.cs @@ -37,11 +37,12 @@ public async Task CopyAcrImagesCommand_CustomDockerfileName() CopyAcrImagesCommand command = new( TestHelper.CreateManifestJsonService(), copyImageServiceMock.Object, - Mock.Of>()); + Mock.Of>(), + TestHelper.CreateArtifactService(tempFolderContext.Path)); command.Options.Manifest = Path.Combine(tempFolderContext.Path, "manifest.json"); command.Options.SourceRepoPrefix = command.Options.RepoPrefix = "test/"; command.Options.SourceRegistry = SourceRegistry; - command.Options.ImageInfoPath = "image-info.json"; + command.Options.ImageInfoPath = Path.Combine(tempFolderContext.Path, "image-info.json"); const string runtimeRelativeDir = "1.0/runtime/os"; Directory.CreateDirectory(Path.Combine(tempFolderContext.Path, runtimeRelativeDir)); @@ -127,11 +128,12 @@ public async Task CopyAcrImagesCommand_SharedDockerfile() var command = new CopyAcrImagesCommand( TestHelper.CreateManifestJsonService(), copyImageServiceMock.Object, - Mock.Of>()); + Mock.Of>(), + TestHelper.CreateArtifactService(tempFolderContext.Path)); command.Options.Manifest = Path.Combine(tempFolderContext.Path, "manifest.json"); command.Options.SourceRepoPrefix = command.Options.RepoPrefix = "test/"; command.Options.SourceRegistry = SourceRegistry; - command.Options.ImageInfoPath = "image-info.json"; + command.Options.ImageInfoPath = Path.Combine(tempFolderContext.Path, "image-info.json"); const string runtimeRelativeDir = "1.0/runtime/os"; Directory.CreateDirectory(Path.Combine(tempFolderContext.Path, runtimeRelativeDir)); @@ -229,11 +231,12 @@ public async Task CopyAcrImagesCommand_RuntimeDepsSharing() var command = new CopyAcrImagesCommand( TestHelper.CreateManifestJsonService(), copyImageServiceMock.Object, - Mock.Of>()); + Mock.Of>(), + TestHelper.CreateArtifactService(tempFolderContext.Path)); command.Options.Manifest = Path.Combine(tempFolderContext.Path, "manifest.json"); command.Options.SourceRepoPrefix = command.Options.RepoPrefix = "test/"; command.Options.SourceRegistry = SourceRegistry; - command.Options.ImageInfoPath = "image-info.json"; + command.Options.ImageInfoPath = Path.Combine(tempFolderContext.Path, "image-info.json"); string dockerfileRelativePath = DockerfileHelper.CreateDockerfile("3.1/runtime-deps/os", tempFolderContext); @@ -342,11 +345,12 @@ public async Task SyndicatedTags() var command = new CopyAcrImagesCommand( TestHelper.CreateManifestJsonService(), copyImageServiceMock.Object, - Mock.Of>()); + Mock.Of>(), + TestHelper.CreateArtifactService(tempFolderContext.Path)); command.Options.Manifest = Path.Combine(tempFolderContext.Path, "manifest.json"); command.Options.SourceRepoPrefix = command.Options.RepoPrefix = "test/"; command.Options.SourceRegistry = SourceRegistry; - command.Options.ImageInfoPath = "image-info.json"; + command.Options.ImageInfoPath = Path.Combine(tempFolderContext.Path, "image-info.json"); const string runtimeRelativeDir = "1.0/runtime/os"; Directory.CreateDirectory(Path.Combine(tempFolderContext.Path, runtimeRelativeDir)); @@ -456,11 +460,12 @@ public async Task CopyAcrImagesCommand_CopiesManifestListTags() CopyAcrImagesCommand command = new( TestHelper.CreateManifestJsonService(), copyImageServiceMock.Object, - Mock.Of>()); + Mock.Of>(), + TestHelper.CreateArtifactService(tempFolderContext.Path)); command.Options.Manifest = Path.Combine(tempFolderContext.Path, "manifest.json"); command.Options.SourceRepoPrefix = command.Options.RepoPrefix = "test/"; command.Options.SourceRegistry = SourceRegistry; - command.Options.ImageInfoPath = "image-info.json"; + command.Options.ImageInfoPath = Path.Combine(tempFolderContext.Path, "image-info.json"); string dockerfileRelativePath = DockerfileHelper.CreateDockerfile("1.0/runtime/os", tempFolderContext); @@ -562,11 +567,12 @@ public async Task CopyAcrImagesCommand_CopiesSyndicatedManifestListTags() CopyAcrImagesCommand command = new( TestHelper.CreateManifestJsonService(), copyImageServiceMock.Object, - Mock.Of>()); + Mock.Of>(), + TestHelper.CreateArtifactService(tempFolderContext.Path)); command.Options.Manifest = Path.Combine(tempFolderContext.Path, "manifest.json"); command.Options.SourceRepoPrefix = command.Options.RepoPrefix = "test/"; command.Options.SourceRegistry = SourceRegistry; - command.Options.ImageInfoPath = "image-info.json"; + command.Options.ImageInfoPath = Path.Combine(tempFolderContext.Path, "image-info.json"); string dockerfileRelativePath = DockerfileHelper.CreateDockerfile("1.0/runtime/os", tempFolderContext); @@ -678,11 +684,12 @@ public async Task CopyAcrImagesCommand_SkipsManifestListsWithNoManifestData() CopyAcrImagesCommand command = new( TestHelper.CreateManifestJsonService(), copyImageServiceMock.Object, - Mock.Of>()); + Mock.Of>(), + TestHelper.CreateArtifactService(tempFolderContext.Path)); command.Options.Manifest = Path.Combine(tempFolderContext.Path, "manifest.json"); command.Options.SourceRepoPrefix = command.Options.RepoPrefix = "test/"; command.Options.SourceRegistry = SourceRegistry; - command.Options.ImageInfoPath = "image-info.json"; + command.Options.ImageInfoPath = Path.Combine(tempFolderContext.Path, "image-info.json"); string dockerfileRelativePath = DockerfileHelper.CreateDockerfile("1.0/runtime/os", tempFolderContext); diff --git a/src/ImageBuilder.Tests/CreateManifestListCommandTests.cs b/src/ImageBuilder.Tests/CreateManifestListCommandTests.cs index 918525e4c..1d46cc39d 100644 --- a/src/ImageBuilder.Tests/CreateManifestListCommandTests.cs +++ b/src/ImageBuilder.Tests/CreateManifestListCommandTests.cs @@ -705,7 +705,8 @@ private static CreateManifestListCommand CreateCommand( copyImageServiceMock.Object, Mock.Of>(), dateTimeService, - Mock.Of()); + Mock.Of(), + TestHelper.CreateArtifactService(Path.GetTempPath())); private static void SetupCommand( CreateManifestListCommand command, diff --git a/src/ImageBuilder.Tests/GenerateEolAnnotationDataForPublishCommandTests.cs b/src/ImageBuilder.Tests/GenerateEolAnnotationDataForPublishCommandTests.cs index b59776879..d8c162f13 100644 --- a/src/ImageBuilder.Tests/GenerateEolAnnotationDataForPublishCommandTests.cs +++ b/src/ImageBuilder.Tests/GenerateEolAnnotationDataForPublishCommandTests.cs @@ -1148,10 +1148,11 @@ private static GenerateEolAnnotationDataForPublishCommand InitializeCommand( acrClientFactory: registryClientFactory, acrContentClientFactory: registryContentClientFactory, lifecycleMetadataService: lifecycleMetadataService, - registryCredentialsProvider: Mock.Of()); + registryCredentialsProvider: Mock.Of(), + artifactService: TestHelper.CreateArtifactService(Path.GetDirectoryName(newEolDigestsListPath))); command.Options.OldImageInfoPath = oldImageInfoPath; command.Options.NewImageInfoPath = newImageInfoPath; - command.Options.EolDigestsListPath = newEolDigestsListPath; + command.Options.EolDigestsListPath = Path.GetFileName(newEolDigestsListPath); command.Options.RegistryOptions = new() { RepoPrefix = repoPrefix, Registry = AcrName }; return command; } diff --git a/src/ImageBuilder.Tests/Helpers/TestHelper.cs b/src/ImageBuilder.Tests/Helpers/TestHelper.cs index 7489caddd..15f64f687 100644 --- a/src/ImageBuilder.Tests/Helpers/TestHelper.cs +++ b/src/ImageBuilder.Tests/Helpers/TestHelper.cs @@ -6,6 +6,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using Microsoft.DotNet.ImageBuilder.Configuration; +using Microsoft.Extensions.Options; namespace Microsoft.DotNet.ImageBuilder.Tests.Helpers { @@ -20,6 +22,16 @@ public static IManifestJsonService CreateManifestJsonService() => fileSystem: new FileSystem(), logger: new LoggerFactory().CreateLogger()); + public static IArtifactService CreateArtifactService(string artifactStagingDirectory) + { + BuildConfiguration buildConfiguration = new() + { + ArtifactStagingDirectory = artifactStagingDirectory + }; + + return new ArtifactService(new FileSystem(), Options.Create(buildConfiguration)); + } + public static TempFolderContext UseTempFolder() { return new TempFolderContext(); diff --git a/src/ImageBuilder.Tests/IngestKustoImageInfoCommandTests.cs b/src/ImageBuilder.Tests/IngestKustoImageInfoCommandTests.cs index e90a3d06c..70087c008 100644 --- a/src/ImageBuilder.Tests/IngestKustoImageInfoCommandTests.cs +++ b/src/ImageBuilder.Tests/IngestKustoImageInfoCommandTests.cs @@ -277,7 +277,11 @@ private async Task ValidateExecuteAsync( .Callback( (csv, _, _, table, _) => ingestedData.Add(table, csv)); - IngestKustoImageInfoCommand command = new(TestHelper.CreateManifestJsonService(), Mock.Of>(), kustoClientMock.Object); + IngestKustoImageInfoCommand command = new( + TestHelper.CreateManifestJsonService(), + Mock.Of>(), + kustoClientMock.Object, + TestHelper.CreateArtifactService(tempFolderContext.Path)); command.Options.ImageInfoPath = imageInfoPath; command.Options.Manifest = manifestPath; command.Options.ImageTable = "ImageInfo"; diff --git a/src/ImageBuilder.Tests/MergeImageInfoFilesCommandTests.cs b/src/ImageBuilder.Tests/MergeImageInfoFilesCommandTests.cs index 733247b4f..cd20822f6 100644 --- a/src/ImageBuilder.Tests/MergeImageInfoFilesCommandTests.cs +++ b/src/ImageBuilder.Tests/MergeImageInfoFilesCommandTests.cs @@ -181,9 +181,9 @@ public async Task MergeImageInfoFilesCommand_HappyPath() } }; - MergeImageInfoCommand command = new MergeImageInfoCommand(TestHelper.CreateManifestJsonService()); + MergeImageInfoCommand command = CreateCommand(context.Path); command.Options.SourceImageInfoFolderPath = Path.Combine(context.Path, "image-infos"); - command.Options.DestinationImageInfoPath = Path.Combine(context.Path, "output.json"); + command.Options.DestinationImageInfoPath = "output.json"; command.Options.Manifest = Path.Combine(context.Path, "manifest.json"); Directory.CreateDirectory(command.Options.SourceImageInfoFolderPath); @@ -228,7 +228,7 @@ public async Task MergeImageInfoFilesCommand_HappyPath() command.LoadManifest(); await command.ExecuteAsync(); - string resultsContent = File.ReadAllText(command.Options.DestinationImageInfoPath); + string resultsContent = File.ReadAllText(Path.Combine(context.Path, command.Options.DestinationImageInfoPath)); ImageArtifactDetails actual = JsonConvert.DeserializeObject(resultsContent); PlatformData expectedPlatform = CreatePlatform( @@ -485,9 +485,9 @@ public async Task MergeImageInfoFilesCommand_DuplicateDockerfilePaths() } }; - MergeImageInfoCommand command = new MergeImageInfoCommand(TestHelper.CreateManifestJsonService()); + MergeImageInfoCommand command = CreateCommand(context.Path); command.Options.SourceImageInfoFolderPath = Path.Combine(context.Path, "image-infos"); - command.Options.DestinationImageInfoPath = Path.Combine(context.Path, "output.json"); + command.Options.DestinationImageInfoPath = "output.json"; command.Options.Manifest = Path.Combine(context.Path, "manifest.json"); Directory.CreateDirectory(command.Options.SourceImageInfoFolderPath); @@ -524,7 +524,7 @@ public async Task MergeImageInfoFilesCommand_DuplicateDockerfilePaths() command.LoadManifest(); await command.ExecuteAsync(); - string resultsContent = File.ReadAllText(command.Options.DestinationImageInfoPath); + string resultsContent = File.ReadAllText(Path.Combine(context.Path, command.Options.DestinationImageInfoPath)); ImageArtifactDetails actual = JsonConvert.DeserializeObject(resultsContent); ImageArtifactDetails expected = new ImageArtifactDetails @@ -620,7 +620,7 @@ public async Task MergeImageInfoFilesCommand_DuplicateDockerfilePaths() [TestMethod] public async Task MergeImageInfoFilesCommand_SourceFolderPathNotFound() { - MergeImageInfoCommand command = new MergeImageInfoCommand(TestHelper.CreateManifestJsonService()); + MergeImageInfoCommand command = CreateCommand(Path.GetTempPath()); command.Options.SourceImageInfoFolderPath = "foo"; command.Options.DestinationImageInfoPath = "output.json"; @@ -643,7 +643,7 @@ public async Task MergeImageInfoFilesCommand_SourceFolderEmpty() // Store the content in a .txt file which the command should NOT be looking for. File.WriteAllText("image-info.txt", JsonHelper.SerializeObject(imageArtifactDetails)); - MergeImageInfoCommand command = new MergeImageInfoCommand(TestHelper.CreateManifestJsonService()); + MergeImageInfoCommand command = CreateCommand(context.Path); command.Options.SourceImageInfoFolderPath = context.Path; command.Options.DestinationImageInfoPath = "output.json"; @@ -772,9 +772,9 @@ public async Task MergeImageInfoFilesCommand_Publish_ReplaceContent() } }; - MergeImageInfoCommand command = new(TestHelper.CreateManifestJsonService()); + MergeImageInfoCommand command = CreateCommand(tempFolderContext.Path); command.Options.SourceImageInfoFolderPath = Path.Combine(tempFolderContext.Path, "image-infos"); - command.Options.DestinationImageInfoPath = Path.Combine(tempFolderContext.Path, "output.json"); + command.Options.DestinationImageInfoPath = "output.json"; command.Options.Manifest = Path.Combine(tempFolderContext.Path, "manifest.json"); command.Options.IsPublishScenario = true; @@ -828,7 +828,8 @@ public async Task MergeImageInfoFilesCommand_Publish_ReplaceContent() } }; - string resultsContent = File.ReadAllText(command.Options.DestinationImageInfoPath); + string resultsContent = File.ReadAllText( + Path.Combine(tempFolderContext.Path, command.Options.DestinationImageInfoPath)); ImageArtifactDetails actual = JsonConvert.DeserializeObject(resultsContent); CompareImageArtifactDetails(expectedImageArtifactDetails, actual); @@ -939,9 +940,9 @@ public async Task MergeImageInfoFilesCommand_Publish_RemoveOutOfDateContent() } }; - MergeImageInfoCommand command = new(TestHelper.CreateManifestJsonService()); + MergeImageInfoCommand command = CreateCommand(tempFolderContext.Path); command.Options.SourceImageInfoFolderPath = Path.Combine(tempFolderContext.Path, "image-infos"); - command.Options.DestinationImageInfoPath = Path.Combine(tempFolderContext.Path, "output.json"); + command.Options.DestinationImageInfoPath = "output.json"; command.Options.Manifest = Path.Combine(tempFolderContext.Path, "manifest.json"); command.Options.IsPublishScenario = true; @@ -983,7 +984,8 @@ public async Task MergeImageInfoFilesCommand_Publish_RemoveOutOfDateContent() } }; - string resultsContent = File.ReadAllText(command.Options.DestinationImageInfoPath); + string resultsContent = File.ReadAllText( + Path.Combine(tempFolderContext.Path, command.Options.DestinationImageInfoPath)); ImageArtifactDetails actual = JsonConvert.DeserializeObject(resultsContent); CompareImageArtifactDetails(expectedImageArtifactDetails, actual); @@ -1116,9 +1118,9 @@ public async Task MergeImageInfoFilesCommand_CommitUrlOverride() File.WriteAllText(updatedImageInfoFile, JsonHelper.SerializeObject(updatedImageInfo)); File.WriteAllText(manifestFile, JsonConvert.SerializeObject(manifest)); - var outputImageInfoFile = Path.Combine(context.Path, "merged-image-info.json"); + const string outputImageInfoFile = "merged-image-info.json"; - MergeImageInfoCommand command = new MergeImageInfoCommand(TestHelper.CreateManifestJsonService()); + MergeImageInfoCommand command = CreateCommand(context.Path); command.Options.SourceImageInfoFolderPath = sourceImageInfoDir; command.Options.DestinationImageInfoPath = outputImageInfoFile; command.Options.InitialImageInfoPath = initialImageInfoFile; @@ -1128,7 +1130,7 @@ public async Task MergeImageInfoFilesCommand_CommitUrlOverride() await command.ExecuteAsync(); // Verify the merged result - string resultContent = File.ReadAllText(outputImageInfoFile); + string resultContent = File.ReadAllText(Path.Combine(context.Path, outputImageInfoFile)); ImageArtifactDetails mergedImageInfo = ImageArtifactDetails.FromJson(resultContent); mergedImageInfo.Repos.ShouldHaveSingleItem(); @@ -1154,5 +1156,10 @@ public async Task MergeImageInfoFilesCommand_CommitUrlOverride() var initialCommitResult = initialShaMatches[0].Value; initialCommitResult.ShouldBe(CommitOverride); } + + private static MergeImageInfoCommand CreateCommand(string artifactStagingDirectory) => + new( + TestHelper.CreateManifestJsonService(), + TestHelper.CreateArtifactService(artifactStagingDirectory)); } } diff --git a/src/ImageBuilder.Tests/PublishImageInfoCommandTests.cs b/src/ImageBuilder.Tests/PublishImageInfoCommandTests.cs index 70fe61520..cb60800a2 100644 --- a/src/ImageBuilder.Tests/PublishImageInfoCommandTests.cs +++ b/src/ImageBuilder.Tests/PublishImageInfoCommandTests.cs @@ -152,7 +152,12 @@ public async Task PublishImageInfoCommand_HappyPath() actualImageArtifactDetailsContents = File.ReadAllText(path); }); - PublishImageInfoCommand command = new(TestHelper.CreateManifestJsonService(), gitServiceMock.Object, Mock.Of(), Mock.Of>()); + PublishImageInfoCommand command = new( + TestHelper.CreateManifestJsonService(), + gitServiceMock.Object, + Mock.Of(), + Mock.Of>(), + TestHelper.CreateArtifactService(tempFolderContext.Path)); command.Options.ImageInfoPath = file; command.Options.GitOptions = gitOptions; command.Options.Manifest = Path.Combine(tempFolderContext.Path, "manifest.json"); diff --git a/src/ImageBuilder.Tests/TrimUnchangedPlatformsCommandTests.cs b/src/ImageBuilder.Tests/TrimUnchangedPlatformsCommandTests.cs index 6fdd10cd1..0eea90fd1 100644 --- a/src/ImageBuilder.Tests/TrimUnchangedPlatformsCommandTests.cs +++ b/src/ImageBuilder.Tests/TrimUnchangedPlatformsCommandTests.cs @@ -240,7 +240,9 @@ private async Task RunTestAsync(ImageArtifactDetails input, ImageArtifactDetails { using TempFolderContext tempFolderContext = new TempFolderContext(); - TrimUnchangedPlatformsCommand command = new TrimUnchangedPlatformsCommand(Mock.Of>()); + TrimUnchangedPlatformsCommand command = new( + Mock.Of>(), + TestHelper.CreateArtifactService(tempFolderContext.Path)); command.Options.ImageInfoPath = Path.Combine(tempFolderContext.Path, "imageinfo.json"); File.WriteAllText(command.Options.ImageInfoPath, JsonHelper.SerializeObject(input)); diff --git a/src/ImageBuilder.Tests/WaitForMarAnnotationIngestionCommandTests.cs b/src/ImageBuilder.Tests/WaitForMarAnnotationIngestionCommandTests.cs index 2f317a0f7..2a07757ee 100644 --- a/src/ImageBuilder.Tests/WaitForMarAnnotationIngestionCommandTests.cs +++ b/src/ImageBuilder.Tests/WaitForMarAnnotationIngestionCommandTests.cs @@ -31,7 +31,8 @@ public async Task WaitForMarAnnotationIngestionCommand() Mock ingestionReporter = new(); WaitForMarAnnotationIngestionCommand cmd = new( Mock.Of>(), - ingestionReporter.Object); + ingestionReporter.Object, + TestHelper.CreateArtifactService(tempFolderContext.Path)); cmd.Options.AnnotationDigestsPath = annotationsDigestsPath; await cmd.ExecuteAsync(); diff --git a/src/ImageBuilder.Tests/WaitForMcrImageIngestionCommandTests.cs b/src/ImageBuilder.Tests/WaitForMcrImageIngestionCommandTests.cs index 3d1133a7f..906f6afce 100644 --- a/src/ImageBuilder.Tests/WaitForMcrImageIngestionCommandTests.cs +++ b/src/ImageBuilder.Tests/WaitForMcrImageIngestionCommandTests.cs @@ -47,7 +47,8 @@ public async Task SuccessfulPublish(string repoPrefix) WaitForMcrImageIngestionCommand command = new( TestHelper.CreateManifestJsonService(), Mock.Of>(), - imageIngestionReporterMock.Object); + imageIngestionReporterMock.Object, + TestHelper.CreateArtifactService(Path.GetTempPath())); using TempFolderContext tempFolderContext = TestHelper.UseTempFolder(); @@ -181,7 +182,8 @@ public async Task SyndicatedTags(string syndicatedManifestDigest) WaitForMcrImageIngestionCommand command = new( TestHelper.CreateManifestJsonService(), Mock.Of>(), - imageIngestionReporterMock.Object); + imageIngestionReporterMock.Object, + TestHelper.CreateArtifactService(Path.GetTempPath())); using TempFolderContext tempFolderContext = TestHelper.UseTempFolder(); diff --git a/src/ImageBuilder/ArtifactService.cs b/src/ImageBuilder/ArtifactService.cs new file mode 100644 index 000000000..62802493f --- /dev/null +++ b/src/ImageBuilder/ArtifactService.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using Microsoft.DotNet.ImageBuilder.Configuration; +using Microsoft.Extensions.Options; + +namespace Microsoft.DotNet.ImageBuilder; + +/// +public sealed class ArtifactService(IFileSystem fileSystem, IOptions buildConfigOptions) + : IArtifactService +{ + private readonly IFileSystem _fileSystem = fileSystem; + private readonly BuildConfiguration _buildConfig = buildConfigOptions.Value; + + /// + public void WriteAllText(string artifactPath, string contents) + { + string outputPath = ResolvePath(artifactPath); + string outputDirectory = Path.GetDirectoryName(outputPath) + ?? throw new InvalidOperationException($"Output path '{outputPath}' has no directory."); + _fileSystem.CreateDirectory(outputDirectory); + _fileSystem.WriteAllText(outputPath, contents); + } + + /// + public string ResolvePath(string artifactPath) + { + if (string.IsNullOrWhiteSpace(_buildConfig.ArtifactStagingDirectory)) + { + throw new InvalidOperationException( + $"{nameof(BuildConfiguration.ArtifactStagingDirectory)} is not set. " + + "Configure it in appsettings.json or via environment variables."); + } + + // Canonicalize both paths so traversal segments can be checked reliably. + string artifactRoot = Path.GetFullPath(_buildConfig.ArtifactStagingDirectory); + string resolvedArtifactPath = Path.GetFullPath(artifactPath, artifactRoot); + string relativeArtifactPath = Path.GetRelativePath(artifactRoot, resolvedArtifactPath); + + // Reject relative paths that escape the configured artifact staging directory. + if (relativeArtifactPath == ".." + || relativeArtifactPath.StartsWith($"..{Path.DirectorySeparatorChar}", StringComparison.Ordinal)) + { + throw new ArgumentException( + "Artifact paths must remain within the artifact staging directory.", + nameof(artifactPath)); + } + + return resolvedArtifactPath; + } +} diff --git a/src/ImageBuilder/Commands/AnnotateEolDigestsCommand.cs b/src/ImageBuilder/Commands/AnnotateEolDigestsCommand.cs index 876fc4931..12d6d39bf 100644 --- a/src/ImageBuilder/Commands/AnnotateEolDigestsCommand.cs +++ b/src/ImageBuilder/Commands/AnnotateEolDigestsCommand.cs @@ -21,6 +21,7 @@ public class AnnotateEolDigestsCommand : Command private readonly ILogger _logger; private readonly ILifecycleMetadataService _lifecycleMetadataService; private readonly IRegistryCredentialsProvider _registryCredentialsProvider; + private readonly IArtifactService _artifactService; private readonly ConcurrentBag _failedAnnotationImageDigests = []; private readonly ConcurrentBag _skippedAnnotationImageDigests = []; private readonly ConcurrentBag _existingAnnotationImageDigests = []; @@ -36,18 +37,21 @@ public class AnnotateEolDigestsCommand : Command public AnnotateEolDigestsCommand( ILogger logger, ILifecycleMetadataService lifecycleMetadataService, - IRegistryCredentialsProvider registryCredentialsProvider) + IRegistryCredentialsProvider registryCredentialsProvider, + IArtifactService artifactService) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _lifecycleMetadataService = lifecycleMetadataService ?? throw new ArgumentNullException(nameof(lifecycleMetadataService)); _registryCredentialsProvider = registryCredentialsProvider ?? throw new ArgumentNullException(nameof(registryCredentialsProvider)); + _artifactService = artifactService ?? throw new ArgumentNullException(nameof(artifactService)); } protected override string Description => "Annotates EOL digests in Docker Registry"; public override async Task ExecuteAsync() { - EolAnnotationsData eolAnnotations = LoadEolAnnotationsData(Options.EolDigestsListPath); + string eolDigestsListPath = _artifactService.ResolvePath(Options.EolDigestsListPath); + EolAnnotationsData eolAnnotations = LoadEolAnnotationsData(eolDigestsListPath); DateOnly? globalEolDate = eolAnnotations.EolDate; await _registryCredentialsProvider.ExecuteWithCredentialsAsync( @@ -78,7 +82,13 @@ await Parallel.ForEachAsync(eolAnnotations.EolDigests, CancellationToken.None, $"Some digest annotations failed or were skipped due to existing non-matching EOL date annotations (failed: {_failedAnnotationImageDigests.Count}, skipped: {_existingAnnotationImageDigests.Count})."); } - File.WriteAllLines(Options.AnnotationDigestsOutputPath, _createdAnnotationDigests.Order()); + string annotationDigests = string.Join(Environment.NewLine, _createdAnnotationDigests.Order()); + if (annotationDigests.Length > 0) + { + annotationDigests += Environment.NewLine; + } + + _artifactService.WriteAllText(Options.AnnotationDigestsOutputPath, annotationDigests); } private void WriteNonEmptySummaryForAnnotationDigests(IEnumerable annotationDigests, string message) diff --git a/src/ImageBuilder/Commands/AnnotateEolDigestsOptions.cs b/src/ImageBuilder/Commands/AnnotateEolDigestsOptions.cs index 4c92d1064..6275026b8 100644 --- a/src/ImageBuilder/Commands/AnnotateEolDigestsOptions.cs +++ b/src/ImageBuilder/Commands/AnnotateEolDigestsOptions.cs @@ -34,7 +34,7 @@ public class AnnotateEolDigestsOptions : Options private static readonly Argument AnnotationDigestsOutputPathArgument = new(nameof(AnnotationDigestsOutputPath)) { - Description = "Output path of file containing the list of annotation digests that were created" + Description = "Artifact-relative output path for the list of annotation digests that were created" }; public override IEnumerable