From 9561cbf7bf3178afd0a329a20b64a475f49a1132 Mon Sep 17 00:00:00 2001 From: David Staniec Date: Fri, 8 May 2026 12:07:51 +0800 Subject: [PATCH 1/5] Spike test to run find / register and download / register as single commands --- .../DownloadAndRegisterPackageCommandTest.cs | 150 +++++++++++ .../FindAndRegisterPackageCommandTest.cs | 129 +++++++++ .../FindAndRegisterPackageFixture.cs | 131 +++++++++ .../DownloadAndRegisterPackageFixture.cs | 253 ++++++++++++++++++ .../DownloadAndRegisterPackageCommand.cs | 79 ++++++ .../Commands/DownloadPackageCommand.cs | 155 +---------- .../Commands/FindAndRegisterPackageCommand.cs | 78 ++++++ .../Calamari/Commands/FindPackageCommand.cs | 110 +------- .../PackageDownloadArgumentValidator.cs | 71 +++++ .../Support/PackageDownloadOptions.cs | 55 ++++ .../Support/PackageDownloadService.cs | 92 +++++++ .../Commands/Support/PackageFindOptions.cs | 34 +++ .../Commands/Support/PackageFindService.cs | 107 ++++++++ 13 files changed, 1194 insertions(+), 250 deletions(-) create mode 100644 source/Calamari.Tests/Fixtures/Commands/DownloadAndRegisterPackageCommandTest.cs create mode 100644 source/Calamari.Tests/Fixtures/Commands/FindAndRegisterPackageCommandTest.cs create mode 100644 source/Calamari.Tests/Fixtures/FindPackage/FindAndRegisterPackageFixture.cs create mode 100644 source/Calamari.Tests/Fixtures/PackageDownload/DownloadAndRegisterPackageFixture.cs create mode 100644 source/Calamari/Commands/DownloadAndRegisterPackageCommand.cs create mode 100644 source/Calamari/Commands/FindAndRegisterPackageCommand.cs create mode 100644 source/Calamari/Commands/Support/PackageDownloadArgumentValidator.cs create mode 100644 source/Calamari/Commands/Support/PackageDownloadOptions.cs create mode 100644 source/Calamari/Commands/Support/PackageDownloadService.cs create mode 100644 source/Calamari/Commands/Support/PackageFindOptions.cs create mode 100644 source/Calamari/Commands/Support/PackageFindService.cs diff --git a/source/Calamari.Tests/Fixtures/Commands/DownloadAndRegisterPackageCommandTest.cs b/source/Calamari.Tests/Fixtures/Commands/DownloadAndRegisterPackageCommandTest.cs new file mode 100644 index 0000000000..1e4feb4ff1 --- /dev/null +++ b/source/Calamari.Tests/Fixtures/Commands/DownloadAndRegisterPackageCommandTest.cs @@ -0,0 +1,150 @@ +using System; +using Calamari.Commands; +using Calamari.Common.Commands; +using Calamari.Common.Features.Processes; +using Calamari.Common.Features.Scripting; +using Calamari.Common.Plumbing.Deployment.PackageRetention; +using Calamari.Common.Plumbing.FileSystem; +using Calamari.Common.Plumbing.Logging; +using Calamari.Common.Plumbing.Variables; +using FluentAssertions; +using NSubstitute; +using NUnit.Framework; + +namespace Calamari.Tests.Fixtures.Commands +{ + [TestFixture] + public class DownloadAndRegisterPackageCommandTest + { + [Test] + public void SupportsSemverVersionFormats() + { + var command = new DownloadAndRegisterPackageCommand( + Substitute.For(), + new CalamariVariables(), + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For()); + + // This should parse without throwing - we expect it to fail on download since we're using mocks, + // but version parsing should succeed + var result = command.Execute(new[] + { + "--packageId=TestPackage", + "--packageVersion=1.0.0", + "--taskId=ServerTasks-12345", + "--packageVersionFormat=Semver", + "--feedId=test-feed", + "--feedUri=https://test.feed.com", + "--feedType=NuGet" + }); + + // Command will fail at download stage with mocks, but version format was valid + result.Should().NotBe(0); // Fails at download, not at version parsing + } + + [Test] + public void SupportsMavenVersionFormats() + { + var command = new DownloadAndRegisterPackageCommand( + Substitute.For(), + new CalamariVariables(), + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For()); + + // Test Maven version format parsing + var result = command.Execute(new[] + { + "--packageId=com.example:test", + "--packageVersion=3.7.4.20220919T144341Z", + "--taskId=ServerTasks-12345", + "--packageVersionFormat=Maven", + "--feedId=test-feed", + "--feedUri=https://maven.test.com", + "--feedType=Maven" + }); + + // Command will fail at download stage with mocks, but version format was valid + result.Should().NotBe(0); // Fails at download, not at version parsing + } + + [Test] + public void RequiresTaskIdArgument() + { + // Set TentacleHome so validation doesn't fail early + var tentacleHome = System.IO.Path.GetTempPath(); + System.Environment.SetEnvironmentVariable("TentacleHome", tentacleHome); + + try + { + var log = Substitute.For(); + var command = new DownloadAndRegisterPackageCommand( + Substitute.For(), + new CalamariVariables(), + Substitute.For(), + Substitute.For(), + log, + Substitute.For()); + + var result = command.Execute(new[] + { + "--packageId=TestPackage", + "--packageVersion=1.0.0", + "--feedId=test-feed", + "--feedUri=https://test.feed.com" + // Deliberately NOT providing --taskId + }); + + // Should fail because taskId is not provided + result.Should().NotBe(0); + + // Should log error about missing taskId (not about network or download failure) + log.Received().Error(Arg.Is(s => s.Equals("No task ID was specified. Please pass --taskId YourTaskId"))); + } + finally + { + Environment.SetEnvironmentVariable("TentacleHome", null); + } + } + + [Test] + public void FailsIfRegistrationFails() + { + var variables = new CalamariVariables(); + variables.Set("Octopus.Task.Id", "ServerTasks-12345"); + + var fileSystem = Substitute.For(); + fileSystem.GetFileSize(Arg.Any()).Returns(1000); + + var journal = Substitute.For(); + // Make RegisterPackageUse throw an exception + journal.When(x => x.RegisterPackageUse( + Arg.Any(), + Arg.Any(), + Arg.Any())) + .Do(x => { throw new Exception("Journal write failed"); }); + + var command = new DownloadAndRegisterPackageCommand( + Substitute.For(), + variables, + fileSystem, + Substitute.For(), + Substitute.For(), + journal); + + var result = command.Execute(new[] + { + "--packageId=TestPackage", + "--packageVersion=1.0.0", + "--feedId=test-feed", + "--feedUri=https://test.feed.com" + }); + + // Should fail because registration failed + result.Should().NotBe(0); + } + } +} diff --git a/source/Calamari.Tests/Fixtures/Commands/FindAndRegisterPackageCommandTest.cs b/source/Calamari.Tests/Fixtures/Commands/FindAndRegisterPackageCommandTest.cs new file mode 100644 index 0000000000..ad646b3770 --- /dev/null +++ b/source/Calamari.Tests/Fixtures/Commands/FindAndRegisterPackageCommandTest.cs @@ -0,0 +1,129 @@ +using Calamari.Commands; +using Calamari.Common.Commands; +using Calamari.Common.Features.Packages; +using Calamari.Common.Plumbing.Deployment.PackageRetention; +using Calamari.Common.Plumbing.FileSystem; +using Calamari.Common.Plumbing.Logging; +using Calamari.Common.Plumbing.Variables; +using Calamari.Integration.FileSystem; +using FluentAssertions; +using NSubstitute; +using NUnit.Framework; + +namespace Calamari.Tests.Fixtures.Commands +{ + [TestFixture] + public class FindAndRegisterPackageCommandTest + { + [Test] + public void SupportsSemverVersionFormats() + { + var command = new FindAndRegisterPackageCommand( + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For()); + + // Version parsing should succeed (command will fail at find stage with mocks) + var result = command.Execute(new[] + { + "--packageId=TestPackage", + "--packageVersion=1.0.0", + "--taskId=ServerTasks-12345", + "--packageVersionFormat=Semver", + "--packageHash=abc123" + }); + + // Command will fail at find stage with mocks, but version format was valid + result.Should().Be(0); // Returns 0 even when package not found (matches FindPackageCommand behavior) + } + + [Test] + public void SupportsMavenVersionFormats() + { + var command = new FindAndRegisterPackageCommand( + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For()); + + // Test Maven version format parsing + var result = command.Execute(new[] + { + "--packageId=com.example:test", + "--packageVersion=3.7.4.20220919T144341Z", + "--taskId=ServerTasks-12345", + "--packageVersionFormat=Maven", + "--packageHash=abc123" + }); + + // Command will fail at find stage with mocks, but version format was valid + result.Should().Be(0); // Returns 0 even when package not found (matches FindPackageCommand behavior) + } + + [Test] + [Timeout(5000)] // Fail fast if validation is missing + public void RequiresTaskIdArgument() + { + var log = Substitute.For(); + var command = new FindAndRegisterPackageCommand( + log, + Substitute.For(), + Substitute.For(), + Substitute.For()); + + var result = command.Execute(new[] + { + "--packageId=TestPackage", + "--packageVersion=1.0.0", + "--packageHash=abc123" + // Deliberately NOT providing --taskId + }); + + // Should fail because taskId is not provided + result.Should().NotBe(0); + + // Should fail because taskId is not provided + log.Received().Error(Arg.Is(s => s.Equals("No task ID was specified. Please pass --taskId YourTaskId"))); + } + + [Test] + public void FailsIfRegistrationFails() + { + var packageStore = Substitute.For(); + // Return a mock package so we reach the registration step + var version = Octopus.Versioning.VersionFactory.CreateSemanticVersion("1.0.0"); + var metadata = new PackageFileNameMetadata("TestPackage", version, version, ".nupkg"); + packageStore.GetPackage(Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(new PackagePhysicalFileMetadata(metadata, "/test/path.nupkg", "abc123", 1000)); + + var fileSystem = Substitute.For(); + fileSystem.GetFileSize(Arg.Any()).Returns(1000); + + var journal = Substitute.For(); + // Make RegisterPackageUse throw an exception + journal.When(x => x.RegisterPackageUse( + Arg.Any(), + Arg.Any(), + Arg.Any())) + .Do(x => { throw new System.Exception("Journal write failed"); }); + + var command = new FindAndRegisterPackageCommand( + Substitute.For(), + packageStore, + journal, + fileSystem); + + var result = command.Execute(new[] + { + "--packageId=TestPackage", + "--packageVersion=1.0.0", + "--taskId=ServerTasks-12345", + "--packageHash=abc123" + }); + + // Should fail because registration failed + result.Should().NotBe(0); + } + } +} diff --git a/source/Calamari.Tests/Fixtures/FindPackage/FindAndRegisterPackageFixture.cs b/source/Calamari.Tests/Fixtures/FindPackage/FindAndRegisterPackageFixture.cs new file mode 100644 index 0000000000..0f74dd97ba --- /dev/null +++ b/source/Calamari.Tests/Fixtures/FindPackage/FindAndRegisterPackageFixture.cs @@ -0,0 +1,131 @@ +using System; +using System.IO; +using Calamari.Common.Features.Packages; +using Calamari.Common.Plumbing.FileSystem; +using Calamari.Testing.Helpers; +using Calamari.Tests.Fixtures.Deployment.Packages; +using Calamari.Tests.Helpers; +using NUnit.Framework; +using Octopus.Versioning; + +namespace Calamari.Tests.Fixtures.FindPackage +{ + [TestFixture] + public class FindAndRegisterPackageFixture : CalamariFixture + { + readonly static string tentacleHome = TestEnvironment.GetTestPath("temp", "FindAndRegisterPackage"); + readonly static string downloadPath = Path.Combine(tentacleHome, "Files"); + readonly string packageId = "Acme.Web"; + readonly string packageVersion = "1.0.0"; + + [OneTimeSetUp] + public void TestFixtureSetUp() + { + Environment.SetEnvironmentVariable("TentacleHome", tentacleHome); + } + + [OneTimeTearDown] + public void TestFixtureTearDown() + { + Environment.SetEnvironmentVariable("TentacleHome", null); + } + + [SetUp] + public void SetUp() + { + if (!Directory.Exists(downloadPath)) + Directory.CreateDirectory(downloadPath); + } + + [TearDown] + public void TearDown() + { + if (Directory.Exists(downloadPath)) + Directory.Delete(downloadPath, true); + } + + CalamariResult FindAndRegisterPackage(string id, string version, string hash, VersionFormat versionFormat = VersionFormat.Semver) + { + return Invoke(Calamari() + .Action("find-and-register-package") + .Argument("packageId", id) + .Argument("packageVersion", version) + .Argument("taskId", "ServerTasks-12345") + .Argument("packageVersionFormat", versionFormat) + .Argument("packageHash", hash)); + } + + [Test] + public void ShouldFindAndRegisterPackageAlreadyUploaded() + { + using (var acmeWeb = new TemporaryFile(PackageBuilder.BuildSamplePackage(packageId, packageVersion))) + { + var destinationFilePath = Path.Combine(downloadPath, PackageName.ToCachedFileName(packageId, VersionFactory.CreateSemanticVersion(packageVersion), ".nupkg")); + File.Copy(acmeWeb.FilePath, destinationFilePath); + + var result = FindAndRegisterPackage(packageId, packageVersion, acmeWeb.Hash); + + result.AssertSuccess(); + result.AssertOutput("Package {0} {1} hash {2} has already been uploaded", packageId, packageVersion, acmeWeb.Hash); + + result.AssertCalamariFoundPackageServiceMessage(Is.True); + result.AssertFoundPackageServiceMessage(); + } + } + + [Test] + public void ShouldNotRegisterWhenPackageNotFound() + { + using (var acmeWeb = new TemporaryFile(PackageBuilder.BuildSamplePackage(packageId, packageVersion))) + { + // Don't copy the package to downloadPath, so it won't be found + var result = FindAndRegisterPackage(packageId, packageVersion, acmeWeb.Hash); + + result.AssertSuccess(); + result.AssertOutput("Package {0} version {1} hash {2} has not been uploaded.", packageId, packageVersion, acmeWeb.Hash); + } + } + + [Test] + public void ShouldFailWhenTaskIdNotProvided() + { + using (var acmeWeb = new TemporaryFile(PackageBuilder.BuildSamplePackage(packageId, packageVersion))) + { + var destinationFilePath = Path.Combine(downloadPath, PackageName.ToCachedFileName(packageId, VersionFactory.CreateSemanticVersion(packageVersion), ".nupkg")); + File.Copy(acmeWeb.FilePath, destinationFilePath); + + // Don't provide taskId argument + var result = Invoke(Calamari() + .Action("find-and-register-package") + .Argument("packageId", packageId) + .Argument("packageVersion", packageVersion) + .Argument("packageHash", acmeWeb.Hash)); + + result.AssertFailure(); + } + } + + [Test] + public void ShouldNotRegisterEarlierPackageVersions() + { + using (var acmeWeb = new TemporaryFile(PackageBuilder.BuildSamplePackage(packageId, packageVersion))) + { + var destinationFilePath = Path.Combine(downloadPath, PackageName.ToCachedFileName(packageId, VersionFactory.CreateSemanticVersion(packageVersion), ".nupkg")); + File.Copy(acmeWeb.FilePath, destinationFilePath); + + // Request a different version (1.0.1) which doesn't exist + using (var newAcmeWeb = new TemporaryFile(PackageBuilder.BuildSamplePackage(packageId, "1.0.1"))) + { + var result = FindAndRegisterPackage(packageId, "1.0.1", newAcmeWeb.Hash); + + result.AssertSuccess(); + result.AssertOutput("Package {0} version {1} hash {2} has not been uploaded.", packageId, "1.0.1", newAcmeWeb.Hash); + result.AssertOutput("Found 1 earlier version of {0} on this Tentacle", packageId); + + // Should still log the found package service message for the earlier version + result.AssertFoundPackageServiceMessage(); + } + } + } + } +} diff --git a/source/Calamari.Tests/Fixtures/PackageDownload/DownloadAndRegisterPackageFixture.cs b/source/Calamari.Tests/Fixtures/PackageDownload/DownloadAndRegisterPackageFixture.cs new file mode 100644 index 0000000000..a82658692f --- /dev/null +++ b/source/Calamari.Tests/Fixtures/PackageDownload/DownloadAndRegisterPackageFixture.cs @@ -0,0 +1,253 @@ +using System; +using System.Globalization; +using System.IO; +using Calamari.Common.Features.Packages; +using Calamari.Testing.Helpers; +using Calamari.Testing.Requirements; +using Calamari.Tests.Helpers; +using NUnit.Framework; +using Octopus.Versioning; +using Octopus.Versioning.Semver; + +namespace Calamari.Tests.Fixtures.PackageDownload +{ + [TestFixture] + public class DownloadAndRegisterPackageFixture : CalamariFixture + { + static readonly string TentacleHome = TestEnvironment.GetTestPath("Fixtures", "DownloadAndRegisterPackage"); + static readonly string DownloadPath = TestEnvironment.GetTestPath(TentacleHome, "Files"); + + static readonly string PublicFeedUri = "https://f.feedz.io/octopus-deploy/integration-tests/nuget/index.json"; + static readonly string ExpectedPackageHash = "1e0856338eb5ada3b30903b980cef9892ebf7201"; + static readonly long ExpectedPackageSize = 3749; + static readonly SampleFeedPackage FeedzPackage = new SampleFeedPackage() + { + Id = "feeds-feedz", + Version = new SemanticVersion("1.0.0"), + PackageId = "OctoConsole" + }; + + static readonly string MavenPublicFeedUri = "https://repo.maven.apache.org/maven2/"; + static readonly string ExpectedMavenPackageHash = "3564ef3803de51fb0530a8377ec6100b33b0d073"; + static readonly long ExpectedMavenPackageSize = 2575022; + static readonly SampleFeedPackage MavenPublicFeed = new SampleFeedPackage("#") + { + Id = "feeds-maven", + Version = VersionFactory.CreateMavenVersion("22.0"), + PackageId = "com.google.guava:guava" + }; + + [SetUp] + public void SetUp() + { + if (!Directory.Exists(TentacleHome)) + Directory.CreateDirectory(TentacleHome); + + Directory.SetCurrentDirectory(TentacleHome); + + Environment.SetEnvironmentVariable("TentacleHome", TentacleHome); + Console.WriteLine("TentacleHome is set to: " + TentacleHome); + } + + [TearDown] + public void TearDown() + { + if (Directory.Exists(DownloadPath)) + Directory.Delete(DownloadPath, true); + Environment.SetEnvironmentVariable("TentacleHome", null); + } + + [Test] + public void ShouldDownloadAndRegisterPackage() + { + var result = DownloadAndRegisterPackage( + FeedzPackage.PackageId, + FeedzPackage.Version.ToString(), + FeedzPackage.Id, + PublicFeedUri); + + result.AssertSuccess(); + + result.AssertOutput("Downloading package {0} v{1}...", FeedzPackage.PackageId, FeedzPackage.Version); + result.AssertOutput("Downloading NuGet package {0} v{1} from feed: '{2}'", + FeedzPackage.PackageId, FeedzPackage.Version, PublicFeedUri); + result.AssertOutput("Package {0} v{1} successfully downloaded from feed: '{2}'", + FeedzPackage.PackageId, FeedzPackage.Version, PublicFeedUri); + + AssertPackageHashMatchesExpected(result, ExpectedPackageHash); + AssertPackageSizeMatchesExpected(result, ExpectedPackageSize); + AssertStagePackageOutputVariableSet(result, FeedzPackage); + } + + [Test] + [RequiresNonFreeBSDPlatform] + public void ShouldDownloadAndRegisterMavenPackage() + { + var result = DownloadAndRegisterPackage( + MavenPublicFeed.PackageId, + MavenPublicFeed.Version.ToString(), + MavenPublicFeed.Id, + MavenPublicFeedUri, + feedType: FeedType.Maven, + versionFormat: VersionFormat.Maven); + + result.AssertSuccess(); + + result.AssertOutput("Downloading package {0} v{1}...", MavenPublicFeed.PackageId, MavenPublicFeed.Version); + result.AssertOutput("Downloading Maven package {0} v{1} from feed: '{2}'", + MavenPublicFeed.PackageId, MavenPublicFeed.Version, MavenPublicFeedUri); + result.AssertOutput("Package {0} v{1} successfully downloaded from feed: '{2}'", + MavenPublicFeed.PackageId, MavenPublicFeed.Version, MavenPublicFeedUri); + + AssertPackageHashMatchesExpected(result, ExpectedMavenPackageHash); + AssertPackageSizeMatchesExpected(result, ExpectedMavenPackageSize); + AssertStagePackageOutputVariableSet(result, MavenPublicFeed); + } + + [Test] + public void ShouldSetOutputVariables() + { + var result = DownloadAndRegisterPackage( + FeedzPackage.PackageId, + FeedzPackage.Version.ToString(), + FeedzPackage.Id, + PublicFeedUri); + + result.AssertSuccess(); + + // Verify all expected output variables are set + result.AssertOutputVariable("StagedPackage.Hash", Is.EqualTo(ExpectedPackageHash)); + result.AssertOutputVariable("StagedPackage.Size", + Is.EqualTo(ExpectedPackageSize.ToString(CultureInfo.InvariantCulture))); + result.AssertOutputVariable("StagedPackage.FullPathOnRemoteMachine", + Does.Match(PackageName.ToRegexPattern(FeedzPackage.PackageId, FeedzPackage.Version, FeedzPackage.DownloadFolder) + ".*")); + } + + [Test] + public void ShouldUsePackageFromCacheAndStillRegister() + { + // First download + var firstResult = DownloadAndRegisterPackage( + FeedzPackage.PackageId, + FeedzPackage.Version.ToString(), + FeedzPackage.Id, + PublicFeedUri); + + firstResult.AssertSuccess(); + + // Second download should use cache but still register + var secondResult = DownloadAndRegisterPackage( + FeedzPackage.PackageId, + FeedzPackage.Version.ToString(), + FeedzPackage.Id, + PublicFeedUri); + + secondResult.AssertSuccess(); + + secondResult.AssertOutput("Checking package cache for package {0} v{1}", + FeedzPackage.PackageId, FeedzPackage.Version); + secondResult.AssertOutputMatches(string.Format( + "Package was found in cache\\. No need to download. Using file: '{0}'", + PackageName.ToRegexPattern(FeedzPackage.PackageId, FeedzPackage.Version, FeedzPackage.DownloadFolder))); + + AssertPackageHashMatchesExpected(secondResult, ExpectedPackageHash); + AssertPackageSizeMatchesExpected(secondResult, ExpectedPackageSize); + AssertStagePackageOutputVariableSet(secondResult, FeedzPackage); + } + + [Test] + public void ShouldFailWhenTaskIdNotProvided() + { + var calamari = Calamari() + .Action("download-and-register-package") + .Argument("packageId", FeedzPackage.PackageId) + .Argument("packageVersion", FeedzPackage.Version.ToString()) + .Argument("packageVersionFormat", VersionFormat.Semver) + .Argument("feedId", FeedzPackage.Id) + .Argument("feedUri", PublicFeedUri) + .Argument("feedType", FeedType.NuGet); + // Don't provide taskId argument + + var result = Invoke(calamari); + + result.AssertFailure(); + } + + CalamariResult DownloadAndRegisterPackage( + string packageId, + string packageVersion, + string feedId, + string feedUri, + string feedUsername = "", + string feedPassword = "", + FeedType feedType = FeedType.NuGet, + VersionFormat versionFormat = VersionFormat.Semver, + bool forcePackageDownload = false, + int attempts = 5, + int attemptBackoffSeconds = 0) + { + var calamari = Calamari() + .Action("download-and-register-package") + .Argument("packageId", packageId) + .Argument("packageVersion", packageVersion) + .Argument("taskId", "ServerTasks-12345") + .Argument("packageVersionFormat", versionFormat) + .Argument("feedId", feedId) + .Argument("feedUri", feedUri) + .Argument("feedType", feedType) + .Argument("attempts", attempts.ToString()) + .Argument("attemptBackoffSeconds", attemptBackoffSeconds.ToString()); + + if (!String.IsNullOrWhiteSpace(feedUsername)) + calamari.Argument("feedUsername", feedUsername); + + if (!String.IsNullOrWhiteSpace(feedPassword)) + calamari.Argument("feedPassword", feedPassword); + + if (forcePackageDownload) + calamari.Flag("forcePackageDownload"); + + return Invoke(calamari); + } + + static void AssertPackageHashMatchesExpected(CalamariResult result, string expectedHash) + { + result.AssertOutputVariable("StagedPackage.Hash", Is.EqualTo(expectedHash)); + } + + static void AssertPackageSizeMatchesExpected(CalamariResult result, long expectedSize) + { + result.AssertOutputVariable("StagedPackage.Size", + Is.EqualTo(expectedSize.ToString(CultureInfo.InvariantCulture))); + } + + static void AssertStagePackageOutputVariableSet(CalamariResult result, SampleFeedPackage testFeed) + { + var newPackageRegex = PackageName.ToRegexPattern(testFeed.PackageId, testFeed.Version, testFeed.DownloadFolder); + result.AssertOutputVariable("StagedPackage.FullPathOnRemoteMachine", Does.Match(newPackageRegex + ".*")); + } + + class SampleFeedPackage + { + public SampleFeedPackage() + { + Delimiter = "."; + } + + public SampleFeedPackage(string delimiter) + { + Delimiter = delimiter; + } + + private string Delimiter { get; set; } + + public string Id { get; set; } + + public string PackageId { get; set; } + + public IVersion Version { get; set; } + + public string DownloadFolder => Path.Combine(DownloadPath, Id); + } + } +} diff --git a/source/Calamari/Commands/DownloadAndRegisterPackageCommand.cs b/source/Calamari/Commands/DownloadAndRegisterPackageCommand.cs new file mode 100644 index 0000000000..0f5883a12c --- /dev/null +++ b/source/Calamari/Commands/DownloadAndRegisterPackageCommand.cs @@ -0,0 +1,79 @@ +using System; +using Calamari.Commands.Support; +using Calamari.Common.Commands; +using Calamari.Common.Features.Packages; +using Calamari.Common.Features.Processes; +using Calamari.Common.Features.Scripting; +using Calamari.Common.Plumbing.Deployment.PackageRetention; +using Calamari.Common.Plumbing.FileSystem; +using Calamari.Common.Plumbing.Logging; +using Calamari.Common.Plumbing.Variables; +using Octopus.Versioning; + +namespace Calamari.Commands +{ + [Command("download-and-register-package", + Description = "Downloads a package and atomically registers its use in the package journal")] + public class DownloadAndRegisterPackageCommand : Command + { + readonly ILog log; + readonly IManagePackageCache journal; + readonly PackageDownloadService downloadService; + readonly PackageDownloadOptions options; + + ServerTaskId taskId; + + public DownloadAndRegisterPackageCommand( + IScriptEngine scriptEngine, + IVariables variables, + ICalamariFileSystem fileSystem, + ICommandLineRunner commandLineRunner, + ILog log, + IManagePackageCache journal) + { + this.log = log; + this.journal = journal; + this.downloadService = new PackageDownloadService(scriptEngine, variables, fileSystem, commandLineRunner, log); + this.options = new PackageDownloadOptions(); + + PackageDownloadOptions.ConfigureOptions(Options, options); + Options.Add("taskId=", "Id of the task that is using the package", v => taskId = new ServerTaskId(v)); + } + + public override int Execute(string[] commandLineArguments) + { + Options.Parse(commandLineArguments); + if (taskId == null) + throw new CommandException("No task ID was specified. Please pass --taskId YourTaskId"); + + try + { + + var pkg = downloadService.DownloadPackage(options); + var version = VersionFactory.TryCreateVersion(options.PackageVersion, options.VersionFormat); + RegisterPackageUse(pkg, version); + + } + catch (Exception ex) + { + log.ErrorFormat("Failed to download and register package {0} v{1} from feed: '{2}'", + options.PackageId, options.PackageVersion, options.FeedUri); + return ConsoleFormatter.PrintError(log, ex); + } + + return 0; + + } + + void RegisterPackageUse(PackagePhysicalFileMetadata pkg, IVersion version) + { + var package = new PackageIdentity( + new PackageId(pkg.PackageId), + version, + new PackagePath(pkg.FullFilePath)); + var size = (ulong)pkg.Size; + journal.RegisterPackageUse(package, taskId, size); + } + + } +} diff --git a/source/Calamari/Commands/DownloadPackageCommand.cs b/source/Calamari/Commands/DownloadPackageCommand.cs index 342551af62..b7488288fa 100644 --- a/source/Calamari/Commands/DownloadPackageCommand.cs +++ b/source/Calamari/Commands/DownloadPackageCommand.cs @@ -1,17 +1,12 @@ using System; -using System.Diagnostics; -using System.Globalization; -using System.Threading; using Calamari.Commands.Support; using Calamari.Common.Commands; using Calamari.Common.Features.Packages; using Calamari.Common.Features.Processes; using Calamari.Common.Features.Scripting; -using Calamari.Common.Plumbing; using Calamari.Common.Plumbing.FileSystem; using Calamari.Common.Plumbing.Logging; using Calamari.Common.Plumbing.Variables; -using Calamari.Integration.Packages.Download; using Octopus.Versioning; namespace Calamari.Commands @@ -19,23 +14,9 @@ namespace Calamari.Commands [Command("download-package", Description = "Downloads a NuGet package from a NuGet feed")] public class DownloadPackageCommand : Command { - private readonly IScriptEngine scriptEngine; - readonly IVariables variables; - readonly ICalamariFileSystem fileSystem; readonly ILog log; - readonly ICommandLineRunner commandLineRunner; - - string packageId; - string packageVersion; - bool forcePackageDownload; - string feedId; - string feedUri; - string feedUsername; - string feedPassword; - string maxDownloadAttempts = "5"; - string attemptBackoffSeconds = "10"; - private FeedType feedType = FeedType.NuGet; - private VersionFormat versionFormat = VersionFormat.Semver; + readonly PackageDownloadService downloadService; + readonly PackageDownloadOptions options; public DownloadPackageCommand( IScriptEngine scriptEngine, @@ -44,151 +25,31 @@ public DownloadPackageCommand( ICommandLineRunner commandLineRunner, ILog log) { - this.scriptEngine = scriptEngine; - this.variables = variables; - this.fileSystem = fileSystem; this.log = log; - this.commandLineRunner = commandLineRunner; - Options.Add("packageId=", "Package ID to download", v => packageId = v); - Options.Add("packageVersion=", "Package version to download", v => packageVersion = v); - Options.Add("packageVersionFormat=", $"[Optional] Format of version. Options {string.Join(", ", Enum.GetNames(typeof(VersionFormat)))}. Defaults to `{VersionFormat.Semver}`.", - v => - { - if (!Enum.TryParse(v, out VersionFormat format)) - { - throw new CommandException($"The provided version format `{format}` is not recognised."); - } - versionFormat = format; - }); - Options.Add("feedId=", "Id of the feed", v => feedId = v); - Options.Add("feedUri=", "URL to feed", v => feedUri = v); - Options.Add("feedUsername=", "[Optional] Username to use for an authenticated feed", v => feedUsername = v); - Options.Add("feedPassword=", "[Optional] Password to use for an authenticated feed", v => feedPassword = v); - Options.Add("feedType=", $"[Optional] Type of feed. Options {string.Join(", ", Enum.GetNames(typeof(FeedType)))}. Defaults to `{FeedType.NuGet}`.", - v => - { - if (!Enum.TryParse(v, out FeedType type)) - { - throw new CommandException($"The provided feed type `{type}` is not recognised."); - } + this.downloadService = new PackageDownloadService(scriptEngine, variables, fileSystem, commandLineRunner, log); + this.options = new PackageDownloadOptions(); - feedType = type; - }); - Options.Add("attempts=", $"[Optional] The number of times to attempt downloading the package. Default: {maxDownloadAttempts}", v => maxDownloadAttempts = v); - Options.Add("attemptBackoffSeconds=", $"[Optional] The number of seconds to apply as a linear backoff between each download attempt. Default: {attemptBackoffSeconds}", v => attemptBackoffSeconds = v); - Options.Add("forcePackageDownload", "[Optional, Flag] if specified, the package will be downloaded even if it is already in the package cache", v => forcePackageDownload = true); + PackageDownloadOptions.ConfigureOptions(Options, options); } public override int Execute(string[] commandLineArguments) { Options.Parse(commandLineArguments); - - // Add Feed Type so we can tell which auth to use when downloading - variables.Set(AuthenticationVariables.FeedType, feedType.ToString()); try { - CheckArguments( - packageId, - packageVersion, - feedId, - feedUri, - feedUsername, - feedPassword, - maxDownloadAttempts, - attemptBackoffSeconds, - out var version, - out var uri, - out var parsedMaxDownloadAttempts, - out var parsedAttemptBackoff); - - var packageDownloaderStrategy = new PackageDownloaderStrategy(log, - scriptEngine, - fileSystem, - commandLineRunner, - variables); - - var pkg = packageDownloaderStrategy.DownloadPackage( - packageId, - version, - feedId, - uri, - feedType, - feedUsername, - feedPassword, - forcePackageDownload, - parsedMaxDownloadAttempts, - parsedAttemptBackoff); - - log.VerboseFormat("Package {0} v{1} successfully downloaded from feed: '{2}'", packageId, version, feedUri); - log.SetOutputVariableButDoNotAddToVariables("StagedPackage.Hash", pkg.Hash); - log.SetOutputVariableButDoNotAddToVariables("StagedPackage.Size", pkg.Size.ToString(CultureInfo.InvariantCulture)); - log.SetOutputVariableButDoNotAddToVariables("StagedPackage.FullPathOnRemoteMachine", pkg.FullFilePath); + downloadService.DownloadPackage(options); } catch (Exception ex) { - log.ErrorFormat("Failed to download package {0} v{1} from feed: '{2}'", packageId, packageVersion, feedUri); + log.ErrorFormat("Failed to download package {0} v{1} from feed: '{2}'", + options.PackageId, options.PackageVersion, options.FeedUri); return ConsoleFormatter.PrintError(log, ex); } return 0; } - // ReSharper disable UnusedParameter.Local - void CheckArguments( - string packageId, - string packageVersion, - string feedId, - string feedUri, - string feedUsername, - string feedPassword, - string maxDownloadAttempts, - string attemptBackoffSeconds, - out IVersion version, - out Uri uri, - out int parsedMaxDownloadAttempts, - out TimeSpan parsedAttemptBackoff) - { - Guard.NotNullOrWhiteSpace(packageId, "No package ID was specified. Please pass --packageId YourPackage"); - Guard.NotNullOrWhiteSpace(packageVersion, "No package version was specified. Please pass --packageVersion 1.0.0.0"); - Guard.NotNullOrWhiteSpace(feedId, "No feed ID was specified. Please pass --feedId feed-id"); - - var usingOidc = !string.IsNullOrWhiteSpace(variables.Get("Jwt")); - if (feedType != FeedType.S3 && feedType != FeedType.AwsElasticContainerRegistry) - { - Guard.NotNullOrWhiteSpace(feedUri, "No feed URI was specified. Please pass --feedUri https://url/to/nuget/feed"); - } - - version = VersionFactory.TryCreateVersion(packageVersion, versionFormat); - if (version == null) - { - throw new CommandException($"Package version '{packageVersion}' specified is not a valid {versionFormat.ToString()} version string"); - } - - if (feedType == FeedType.S3 || feedType == FeedType.AwsElasticContainerRegistry) - { - uri = null; - } - else if (!Uri.TryCreate(feedUri, UriKind.Absolute, out uri)) - throw new CommandException($"URI specified '{feedUri}' is not a valid URI"); - - if (!String.IsNullOrWhiteSpace(feedUsername) && String.IsNullOrWhiteSpace(feedPassword) && !usingOidc) - throw new CommandException("A username was specified but no password was provided. Please pass --feedPassword \"FeedPassword\""); - - if (!int.TryParse(maxDownloadAttempts, out parsedMaxDownloadAttempts)) - throw new CommandException($"The requested number of download attempts '{maxDownloadAttempts}' is not a valid integer number"); - - if (parsedMaxDownloadAttempts <= 0) - throw new CommandException("The requested number of download attempts should be more than zero"); - - if (!int.TryParse(attemptBackoffSeconds, out var parsedAttemptBackoffSeconds)) - throw new CommandException($"Retry requested download attempt retry backoff '{attemptBackoffSeconds}' is not a valid integer number of seconds"); - - if (parsedAttemptBackoffSeconds < 0) - throw new CommandException("The requested download attempt retry backoff should be a positive integer number of seconds"); - - parsedAttemptBackoff = TimeSpan.FromSeconds(parsedAttemptBackoffSeconds); - } } } \ No newline at end of file diff --git a/source/Calamari/Commands/FindAndRegisterPackageCommand.cs b/source/Calamari/Commands/FindAndRegisterPackageCommand.cs new file mode 100644 index 0000000000..619656743c --- /dev/null +++ b/source/Calamari/Commands/FindAndRegisterPackageCommand.cs @@ -0,0 +1,78 @@ +using System; +using Calamari.Commands.Support; +using Calamari.Common.Commands; +using Calamari.Common.Features.Packages; +using Calamari.Common.Plumbing.Deployment.PackageRetention; +using Calamari.Common.Plumbing.FileSystem; +using Calamari.Common.Plumbing.Logging; +using Calamari.Integration.FileSystem; +using Octopus.Versioning; + +namespace Calamari.Commands +{ + [Command("find-and-register-package", + Description = "Finds a package and atomically registers its use in the package journal")] + public class FindAndRegisterPackageCommand : Command + { + readonly ILog log; + readonly IManagePackageCache journal; + readonly ICalamariFileSystem fileSystem; + readonly PackageFindService findService; + readonly PackageFindOptions options; + + ServerTaskId taskId; + + public FindAndRegisterPackageCommand( + ILog log, + IPackageStore packageStore, + IManagePackageCache journal, + ICalamariFileSystem fileSystem) + { + this.log = log; + this.journal = journal; + this.fileSystem = fileSystem; + this.findService = new PackageFindService(log, packageStore); + this.options = new PackageFindOptions(); + + PackageFindOptions.ConfigureOptions(Options, options); + Options.Add("taskId=", "Id of the task that is using the package", v => taskId = new ServerTaskId(v)); + } + + public override int Execute(string[] commandLineArguments) + { + Options.Parse(commandLineArguments); + + try + { + if (taskId == null) + throw new CommandException("No task ID was specified. Please pass --taskId YourTaskId"); + + var package = findService.FindPackage(options); + + if (package != null) + { + var version = VersionFactory.TryCreateVersion(options.PackageVersion, options.VersionFormat); + RegisterPackageUse(package, version); + } + + return 0; + } + catch (Exception ex) + { + log.ErrorFormat("Failed to find and register package {0} v{1} hash {2}", + options.PackageId, options.PackageVersion, options.PackageHash); + return ConsoleFormatter.PrintError(log, ex); + } + } + + void RegisterPackageUse(PackagePhysicalFileMetadata pkg, IVersion version) + { + var package = new PackageIdentity( + new PackageId(pkg.PackageId), + version, + new PackagePath(pkg.FullFilePath)); + var size = fileSystem.GetFileSize(package.Path.Value); + journal.RegisterPackageUse(package, taskId, (ulong)size); + } + } +} diff --git a/source/Calamari/Commands/FindPackageCommand.cs b/source/Calamari/Commands/FindPackageCommand.cs index 5b20060b35..d93314d221 100644 --- a/source/Calamari/Commands/FindPackageCommand.cs +++ b/source/Calamari/Commands/FindPackageCommand.cs @@ -1,128 +1,32 @@ using System; -using System.Linq; using Calamari.Commands.Support; using Calamari.Common.Commands; -using Calamari.Common.Plumbing; using Calamari.Common.Plumbing.Logging; using Calamari.Integration.FileSystem; -using Octopus.Versioning; namespace Calamari.Commands { [Command("find-package", Description = "Finds the package that matches the specified ID and version. If no exact match is found, it returns a list of the nearest packages that matches the ID")] public class FindPackageCommand : Command { - readonly ILog log; - readonly IPackageStore packageStore; - string packageId; - string rawPackageVersion; - string packageHash; - bool exactMatchOnly; - VersionFormat versionFormat = VersionFormat.Semver; - + readonly PackageFindService findService; + readonly PackageFindOptions options; + public FindPackageCommand(ILog log, IPackageStore packageStore) { - this.log = log; - this.packageStore = packageStore; - - Options.Add("packageId=", "Package ID to find", v => packageId = v); - Options.Add("packageVersion=", "Package version to find", v => rawPackageVersion = v); - Options.Add("packageHash=", "Package hash to compare against", v => packageHash = v); - Options.Add("packageVersionFormat=", $"[Optional] Format of version. Options {string.Join(", ", Enum.GetNames(typeof(VersionFormat)))}. Defaults to `{VersionFormat.Semver}`.", - v => - { - if (!Enum.TryParse(v, out VersionFormat format)) - { - throw new CommandException($"The provided version format `{format}` is not recognised."); - } + this.findService = new PackageFindService(log, packageStore); + this.options = new PackageFindOptions(); - versionFormat = format; - }); - Options.Add("exactMatch=", "Only return exact matches", v => exactMatchOnly = bool.Parse(v)); + PackageFindOptions.ConfigureOptions(Options, options); } public override int Execute(string[] commandLineArguments) { Options.Parse(commandLineArguments); - Guard.NotNullOrWhiteSpace(packageId, "No package ID was specified. Please pass --packageId YourPackage"); - Guard.NotNullOrWhiteSpace(rawPackageVersion, "No package version was specified. Please pass --packageVersion 1.0.0.0"); - Guard.NotNullOrWhiteSpace(packageHash, "No package hash was specified. Please pass --packageHash YourPackageHash"); - - var version = VersionFactory.TryCreateVersion(rawPackageVersion, versionFormat); - if (version == null) - throw new CommandException($"Package version '{rawPackageVersion}' is not a valid {versionFormat} version string. Please pass --packageVersionFormat with a different version type."); - - var package = packageStore.GetPackage(packageId, version, packageHash); - if (package == null) - { - log.Verbose($"Package {packageId} version {version} hash {packageHash} has not been uploaded."); + findService.FindPackage(options); - if (exactMatchOnly) - return 0; - - FindEarlierPackages(version); - - return 0; - } - - log.VerboseFormat("Package {0} {1} hash {2} has already been uploaded", package.PackageId, package.Version, package.Hash); - LogPackageFound( - package.PackageId, - package.FileVersion, - package.Hash, - package.Extension, - package.FullFilePath, - true - ); return 0; } - - void FindEarlierPackages(IVersion version) - { - log.VerboseFormat("Finding earlier packages that have been uploaded to this Tentacle."); - var nearestPackages = packageStore.GetNearestPackages(packageId, version).ToList(); - if (!nearestPackages.Any()) - { - log.VerboseFormat("No earlier packages for {0} has been uploaded", packageId); - } - - log.VerboseFormat("Found {0} earlier {1} of {2} on this Tentacle", - nearestPackages.Count, nearestPackages.Count == 1 ? "version" : "versions", packageId); - foreach (var nearestPackage in nearestPackages) - { - log.VerboseFormat(" - {0}: {1}", nearestPackage.Version, nearestPackage.FullFilePath); - LogPackageFound( - nearestPackage.PackageId, - nearestPackage.FileVersion, - nearestPackage.Hash, - nearestPackage.Extension, - nearestPackage.FullFilePath, - false - ); - } - } - - - public void LogPackageFound( - string packageId, - IVersion packageVersion, - string packageHash, - string packageFileExtension, - string packageFullPath, - bool exactMatchExists - ) - { - if (exactMatchExists) - log.Verbose("##octopus[calamari-found-package]"); - - log.VerboseFormat("##octopus[foundPackage id=\"{0}\" version=\"{1}\" versionFormat=\"{2}\" hash=\"{3}\" remotePath=\"{4}\" fileExtension=\"{5}\"]", - AbstractLog.ConvertServiceMessageValue(packageId), - AbstractLog.ConvertServiceMessageValue(packageVersion.ToString()), - AbstractLog.ConvertServiceMessageValue(packageVersion.Format.ToString()), - AbstractLog.ConvertServiceMessageValue(packageHash), - AbstractLog.ConvertServiceMessageValue(packageFullPath), - AbstractLog.ConvertServiceMessageValue(packageFileExtension)); - } } } \ No newline at end of file diff --git a/source/Calamari/Commands/Support/PackageDownloadArgumentValidator.cs b/source/Calamari/Commands/Support/PackageDownloadArgumentValidator.cs new file mode 100644 index 0000000000..0396505a32 --- /dev/null +++ b/source/Calamari/Commands/Support/PackageDownloadArgumentValidator.cs @@ -0,0 +1,71 @@ +using System; +using Calamari.Common.Commands; +using Calamari.Common.Features.Packages; +using Calamari.Common.Plumbing; +using Calamari.Common.Plumbing.Variables; +using Octopus.Versioning; + +namespace Calamari.Commands.Support +{ + public static class PackageDownloadArgumentValidator + { + // ReSharper disable UnusedParameter.Local + public static void CheckArguments( + string packageId, + string packageVersion, + string feedId, + string feedUri, + string feedUsername, + string feedPassword, + string maxDownloadAttempts, + string attemptBackoffSeconds, + FeedType feedType, + VersionFormat versionFormat, + IVariables variables, + out IVersion version, + out Uri uri, + out int parsedMaxDownloadAttempts, + out TimeSpan parsedAttemptBackoff) + { + Guard.NotNullOrWhiteSpace(packageId, "No package ID was specified. Please pass --packageId YourPackage"); + Guard.NotNullOrWhiteSpace(packageVersion, "No package version was specified. Please pass --packageVersion 1.0.0.0"); + Guard.NotNullOrWhiteSpace(feedId, "No feed ID was specified. Please pass --feedId feed-id"); + + var usingOidc = !string.IsNullOrWhiteSpace(variables.Get("Jwt")); + if (feedType != FeedType.S3 && feedType != FeedType.AwsElasticContainerRegistry) + { + Guard.NotNullOrWhiteSpace(feedUri, "No feed URI was specified. Please pass --feedUri https://url/to/nuget/feed"); + } + + version = VersionFactory.TryCreateVersion(packageVersion, versionFormat); + if (version == null) + { + throw new CommandException($"Package version '{packageVersion}' specified is not a valid {versionFormat.ToString()} version string"); + } + + if (feedType == FeedType.S3 || feedType == FeedType.AwsElasticContainerRegistry) + { + uri = null; + } + else if (!Uri.TryCreate(feedUri, UriKind.Absolute, out uri)) + throw new CommandException($"URI specified '{feedUri}' is not a valid URI"); + + if (!String.IsNullOrWhiteSpace(feedUsername) && String.IsNullOrWhiteSpace(feedPassword) && !usingOidc) + throw new CommandException("A username was specified but no password was provided. Please pass --feedPassword \"FeedPassword\""); + + if (!int.TryParse(maxDownloadAttempts, out parsedMaxDownloadAttempts)) + throw new CommandException($"The requested number of download attempts '{maxDownloadAttempts}' is not a valid integer number"); + + if (parsedMaxDownloadAttempts <= 0) + throw new CommandException("The requested number of download attempts should be more than zero"); + + if (!int.TryParse(attemptBackoffSeconds, out var parsedAttemptBackoffSeconds)) + throw new CommandException($"Retry requested download attempt retry backoff '{attemptBackoffSeconds}' is not a valid integer number of seconds"); + + if (parsedAttemptBackoffSeconds < 0) + throw new CommandException("The requested download attempt retry backoff should be a positive integer number of seconds"); + + parsedAttemptBackoff = TimeSpan.FromSeconds(parsedAttemptBackoffSeconds); + } + } +} diff --git a/source/Calamari/Commands/Support/PackageDownloadOptions.cs b/source/Calamari/Commands/Support/PackageDownloadOptions.cs new file mode 100644 index 0000000000..d2a5afcd73 --- /dev/null +++ b/source/Calamari/Commands/Support/PackageDownloadOptions.cs @@ -0,0 +1,55 @@ +using System; +using Calamari.Common.Commands; +using Calamari.Common.Features.Packages; +using Calamari.Common.Plumbing.Commands.Options; +using Octopus.Versioning; + +namespace Calamari.Commands.Support +{ + public class PackageDownloadOptions + { + public string PackageId { get; set; } + public string PackageVersion { get; set; } + public bool ForcePackageDownload { get; set; } + public string FeedId { get; set; } + public string FeedUri { get; set; } + public string FeedUsername { get; set; } + public string FeedPassword { get; set; } + public string MaxDownloadAttempts { get; set; } = "5"; + public string AttemptBackoffSeconds { get; set; } = "10"; + public FeedType FeedType { get; set; } = FeedType.NuGet; + public VersionFormat VersionFormat { get; set; } = VersionFormat.Semver; + + public static void ConfigureOptions(OptionSet options, PackageDownloadOptions downloadOptions) + { + options.Add("packageId=", "Package ID to download", v => downloadOptions.PackageId = v); + options.Add("packageVersion=", "Package version to download", v => downloadOptions.PackageVersion = v); + options.Add("packageVersionFormat=", $"[Optional] Format of version. Options {string.Join(", ", Enum.GetNames(typeof(VersionFormat)))}. Defaults to `{VersionFormat.Semver}`.", + v => + { + if (!Enum.TryParse(v, out VersionFormat format)) + { + throw new CommandException($"The provided version format `{format}` is not recognised."); + } + downloadOptions.VersionFormat = format; + }); + options.Add("feedId=", "Id of the feed", v => downloadOptions.FeedId = v); + options.Add("feedUri=", "URL to feed", v => downloadOptions.FeedUri = v); + options.Add("feedUsername=", "[Optional] Username to use for an authenticated feed", v => downloadOptions.FeedUsername = v); + options.Add("feedPassword=", "[Optional] Password to use for an authenticated feed", v => downloadOptions.FeedPassword = v); + options.Add("feedType=", $"[Optional] Type of feed. Options {string.Join(", ", Enum.GetNames(typeof(FeedType)))}. Defaults to `{FeedType.NuGet}`.", + v => + { + if (!Enum.TryParse(v, out FeedType type)) + { + throw new CommandException($"The provided feed type `{type}` is not recognised."); + } + + downloadOptions.FeedType = type; + }); + options.Add("attempts=", $"[Optional] The number of times to attempt downloading the package. Default: {downloadOptions.MaxDownloadAttempts}", v => downloadOptions.MaxDownloadAttempts = v); + options.Add("attemptBackoffSeconds=", $"[Optional] The number of seconds to apply as a linear backoff between each download attempt. Default: {downloadOptions.AttemptBackoffSeconds}", v => downloadOptions.AttemptBackoffSeconds = v); + options.Add("forcePackageDownload", "[Optional, Flag] if specified, the package will be downloaded even if it is already in the package cache", v => downloadOptions.ForcePackageDownload = true); + } + } +} diff --git a/source/Calamari/Commands/Support/PackageDownloadService.cs b/source/Calamari/Commands/Support/PackageDownloadService.cs new file mode 100644 index 0000000000..05885af065 --- /dev/null +++ b/source/Calamari/Commands/Support/PackageDownloadService.cs @@ -0,0 +1,92 @@ +using System; +using System.Globalization; +using Calamari.Common.Features.Packages; +using Calamari.Common.Features.Processes; +using Calamari.Common.Features.Scripting; +using Calamari.Common.Plumbing.FileSystem; +using Calamari.Common.Plumbing.Logging; +using Calamari.Common.Plumbing.Variables; +using Calamari.Integration.Packages.Download; + +namespace Calamari.Commands.Support +{ + public class PackageDownloadService + { + readonly IScriptEngine scriptEngine; + readonly IVariables variables; + readonly ICalamariFileSystem fileSystem; + readonly ICommandLineRunner commandLineRunner; + readonly ILog log; + + public PackageDownloadService( + IScriptEngine scriptEngine, + IVariables variables, + ICalamariFileSystem fileSystem, + ICommandLineRunner commandLineRunner, + ILog log) + { + this.scriptEngine = scriptEngine; + this.variables = variables; + this.fileSystem = fileSystem; + this.commandLineRunner = commandLineRunner; + this.log = log; + } + + public PackagePhysicalFileMetadata DownloadPackage(PackageDownloadOptions options) + { + variables.Set(AuthenticationVariables.FeedType, options.FeedType.ToString()); + + PackageDownloadArgumentValidator.CheckArguments( + options.PackageId, + options.PackageVersion, + options.FeedId, + options.FeedUri, + options.FeedUsername, + options.FeedPassword, + options.MaxDownloadAttempts, + options.AttemptBackoffSeconds, + options.FeedType, + options.VersionFormat, + variables, + out var version, + out var uri, + out var parsedMaxDownloadAttempts, + out var parsedAttemptBackoff); + + var packageDownloaderStrategy = new PackageDownloaderStrategy( + log, + scriptEngine, + fileSystem, + commandLineRunner, + variables); + + var pkg = packageDownloaderStrategy.DownloadPackage( + options.PackageId, + version, + options.FeedId, + uri, + options.FeedType, + options.FeedUsername, + options.FeedPassword, + options.ForcePackageDownload, + parsedMaxDownloadAttempts, + parsedAttemptBackoff); + + log.VerboseFormat("Package {0} v{1} successfully downloaded from feed: '{2}'", + options.PackageId, version, options.FeedUri); + + SetOutputVariables(pkg); + + return pkg; + } + + void SetOutputVariables(PackagePhysicalFileMetadata pkg) + { + log.SetOutputVariableButDoNotAddToVariables("StagedPackage.Hash", pkg.Hash); + log.SetOutputVariableButDoNotAddToVariables("StagedPackage.Size", + pkg.Size.ToString(CultureInfo.InvariantCulture)); + log.SetOutputVariableButDoNotAddToVariables("StagedPackage.FullPathOnRemoteMachine", + pkg.FullFilePath); + } + } +} diff --git a/source/Calamari/Commands/Support/PackageFindOptions.cs b/source/Calamari/Commands/Support/PackageFindOptions.cs new file mode 100644 index 0000000000..0180793cf5 --- /dev/null +++ b/source/Calamari/Commands/Support/PackageFindOptions.cs @@ -0,0 +1,34 @@ +using System; +using Calamari.Common.Commands; +using Calamari.Common.Plumbing.Commands.Options; +using Octopus.Versioning; + +namespace Calamari.Commands.Support +{ + public class PackageFindOptions + { + public string PackageId { get; set; } + public string PackageVersion { get; set; } + public string PackageHash { get; set; } + public bool ExactMatchOnly { get; set; } + public VersionFormat VersionFormat { get; set; } = VersionFormat.Semver; + + public static void ConfigureOptions(OptionSet options, PackageFindOptions findOptions) + { + options.Add("packageId=", "Package ID to find", v => findOptions.PackageId = v); + options.Add("packageVersion=", "Package version to find", v => findOptions.PackageVersion = v); + options.Add("packageHash=", "Package hash to compare against", v => findOptions.PackageHash = v); + options.Add("packageVersionFormat=", $"[Optional] Format of version. Options {string.Join(", ", Enum.GetNames(typeof(VersionFormat)))}. Defaults to `{VersionFormat.Semver}`.", + v => + { + if (!Enum.TryParse(v, out VersionFormat format)) + { + throw new CommandException($"The provided version format `{format}` is not recognised."); + } + + findOptions.VersionFormat = format; + }); + options.Add("exactMatch=", "Only return exact matches", v => findOptions.ExactMatchOnly = bool.Parse(v)); + } + } +} diff --git a/source/Calamari/Commands/Support/PackageFindService.cs b/source/Calamari/Commands/Support/PackageFindService.cs new file mode 100644 index 0000000000..7b695cb229 --- /dev/null +++ b/source/Calamari/Commands/Support/PackageFindService.cs @@ -0,0 +1,107 @@ +using System; +using System.Linq; +using Calamari.Common.Commands; +using Calamari.Common.Features.Packages; +using Calamari.Common.Plumbing; +using Calamari.Common.Plumbing.Logging; +using Calamari.Integration.FileSystem; +using Octopus.Versioning; + +namespace Calamari.Commands.Support +{ + public class PackageFindService + { + readonly ILog log; + readonly IPackageStore packageStore; + + public PackageFindService(ILog log, IPackageStore packageStore) + { + this.log = log; + this.packageStore = packageStore; + } + + public PackagePhysicalFileMetadata FindPackage(PackageFindOptions options) + { + Guard.NotNullOrWhiteSpace(options.PackageId, "No package ID was specified. Please pass --packageId YourPackage"); + Guard.NotNullOrWhiteSpace(options.PackageVersion, "No package version was specified. Please pass --packageVersion 1.0.0.0"); + Guard.NotNullOrWhiteSpace(options.PackageHash, "No package hash was specified. Please pass --packageHash YourPackageHash"); + + var version = VersionFactory.TryCreateVersion(options.PackageVersion, options.VersionFormat); + if (version == null) + throw new CommandException($"Package version '{options.PackageVersion}' is not a valid {options.VersionFormat} version string. Please pass --packageVersionFormat with a different version type."); + + var package = packageStore.GetPackage(options.PackageId, version, options.PackageHash); + if (package == null) + { + log.Verbose($"Package {options.PackageId} version {version} hash {options.PackageHash} has not been uploaded."); + + if (!options.ExactMatchOnly) + { + FindEarlierPackages(options.PackageId, version, options.VersionFormat); + } + + return null; + } + + log.VerboseFormat("Package {0} {1} hash {2} has already been uploaded", package.PackageId, package.Version, package.Hash); + LogPackageFound( + package.PackageId, + package.FileVersion, + package.Hash, + package.Extension, + package.FullFilePath, + true, + options.VersionFormat + ); + return package; + } + + void FindEarlierPackages(string packageId, IVersion version, VersionFormat versionFormat) + { + log.VerboseFormat("Finding earlier packages that have been uploaded to this Tentacle."); + var nearestPackages = packageStore.GetNearestPackages(packageId, version).ToList(); + if (!nearestPackages.Any()) + { + log.VerboseFormat("No earlier packages for {0} has been uploaded", packageId); + } + + log.VerboseFormat("Found {0} earlier {1} of {2} on this Tentacle", + nearestPackages.Count, nearestPackages.Count == 1 ? "version" : "versions", packageId); + foreach (var nearestPackage in nearestPackages) + { + log.VerboseFormat(" - {0}: {1}", nearestPackage.Version, nearestPackage.FullFilePath); + LogPackageFound( + nearestPackage.PackageId, + nearestPackage.FileVersion, + nearestPackage.Hash, + nearestPackage.Extension, + nearestPackage.FullFilePath, + false, + versionFormat + ); + } + } + + void LogPackageFound( + string packageId, + IVersion packageVersion, + string packageHash, + string packageFileExtension, + string packageFullPath, + bool exactMatchExists, + VersionFormat versionFormat + ) + { + if (exactMatchExists) + log.Verbose("##octopus[calamari-found-package]"); + + log.VerboseFormat("##octopus[foundPackage id=\"{0}\" version=\"{1}\" versionFormat=\"{2}\" hash=\"{3}\" remotePath=\"{4}\" fileExtension=\"{5}\"]", + AbstractLog.ConvertServiceMessageValue(packageId), + AbstractLog.ConvertServiceMessageValue(packageVersion.ToString()), + AbstractLog.ConvertServiceMessageValue(packageVersion.Format.ToString()), + AbstractLog.ConvertServiceMessageValue(packageHash), + AbstractLog.ConvertServiceMessageValue(packageFullPath), + AbstractLog.ConvertServiceMessageValue(packageFileExtension)); + } + } +} From 343007bb3bf0d01444bdcc4f11a9fe7d66a8d1d8 Mon Sep 17 00:00:00 2001 From: David Staniec Date: Thu, 14 May 2026 15:26:04 +0800 Subject: [PATCH 2/5] Test changes / fixes --- .../DownloadAndRegisterPackageCommandTest.cs | 150 ------ .../FindAndRegisterPackageCommandTest.cs | 129 ------ .../FindAndRegisterPackageFixture.cs | 259 +++++++++++ .../DownloadAndRegisterPackageFixture.cs | 431 +++++++++++++++++- .../DownloadAndRegisterPackageCommand.cs | 11 +- .../Commands/FindAndRegisterPackageCommand.cs | 9 +- 6 files changed, 694 insertions(+), 295 deletions(-) delete mode 100644 source/Calamari.Tests/Fixtures/Commands/DownloadAndRegisterPackageCommandTest.cs delete mode 100644 source/Calamari.Tests/Fixtures/Commands/FindAndRegisterPackageCommandTest.cs diff --git a/source/Calamari.Tests/Fixtures/Commands/DownloadAndRegisterPackageCommandTest.cs b/source/Calamari.Tests/Fixtures/Commands/DownloadAndRegisterPackageCommandTest.cs deleted file mode 100644 index 1e4feb4ff1..0000000000 --- a/source/Calamari.Tests/Fixtures/Commands/DownloadAndRegisterPackageCommandTest.cs +++ /dev/null @@ -1,150 +0,0 @@ -using System; -using Calamari.Commands; -using Calamari.Common.Commands; -using Calamari.Common.Features.Processes; -using Calamari.Common.Features.Scripting; -using Calamari.Common.Plumbing.Deployment.PackageRetention; -using Calamari.Common.Plumbing.FileSystem; -using Calamari.Common.Plumbing.Logging; -using Calamari.Common.Plumbing.Variables; -using FluentAssertions; -using NSubstitute; -using NUnit.Framework; - -namespace Calamari.Tests.Fixtures.Commands -{ - [TestFixture] - public class DownloadAndRegisterPackageCommandTest - { - [Test] - public void SupportsSemverVersionFormats() - { - var command = new DownloadAndRegisterPackageCommand( - Substitute.For(), - new CalamariVariables(), - Substitute.For(), - Substitute.For(), - Substitute.For(), - Substitute.For()); - - // This should parse without throwing - we expect it to fail on download since we're using mocks, - // but version parsing should succeed - var result = command.Execute(new[] - { - "--packageId=TestPackage", - "--packageVersion=1.0.0", - "--taskId=ServerTasks-12345", - "--packageVersionFormat=Semver", - "--feedId=test-feed", - "--feedUri=https://test.feed.com", - "--feedType=NuGet" - }); - - // Command will fail at download stage with mocks, but version format was valid - result.Should().NotBe(0); // Fails at download, not at version parsing - } - - [Test] - public void SupportsMavenVersionFormats() - { - var command = new DownloadAndRegisterPackageCommand( - Substitute.For(), - new CalamariVariables(), - Substitute.For(), - Substitute.For(), - Substitute.For(), - Substitute.For()); - - // Test Maven version format parsing - var result = command.Execute(new[] - { - "--packageId=com.example:test", - "--packageVersion=3.7.4.20220919T144341Z", - "--taskId=ServerTasks-12345", - "--packageVersionFormat=Maven", - "--feedId=test-feed", - "--feedUri=https://maven.test.com", - "--feedType=Maven" - }); - - // Command will fail at download stage with mocks, but version format was valid - result.Should().NotBe(0); // Fails at download, not at version parsing - } - - [Test] - public void RequiresTaskIdArgument() - { - // Set TentacleHome so validation doesn't fail early - var tentacleHome = System.IO.Path.GetTempPath(); - System.Environment.SetEnvironmentVariable("TentacleHome", tentacleHome); - - try - { - var log = Substitute.For(); - var command = new DownloadAndRegisterPackageCommand( - Substitute.For(), - new CalamariVariables(), - Substitute.For(), - Substitute.For(), - log, - Substitute.For()); - - var result = command.Execute(new[] - { - "--packageId=TestPackage", - "--packageVersion=1.0.0", - "--feedId=test-feed", - "--feedUri=https://test.feed.com" - // Deliberately NOT providing --taskId - }); - - // Should fail because taskId is not provided - result.Should().NotBe(0); - - // Should log error about missing taskId (not about network or download failure) - log.Received().Error(Arg.Is(s => s.Equals("No task ID was specified. Please pass --taskId YourTaskId"))); - } - finally - { - Environment.SetEnvironmentVariable("TentacleHome", null); - } - } - - [Test] - public void FailsIfRegistrationFails() - { - var variables = new CalamariVariables(); - variables.Set("Octopus.Task.Id", "ServerTasks-12345"); - - var fileSystem = Substitute.For(); - fileSystem.GetFileSize(Arg.Any()).Returns(1000); - - var journal = Substitute.For(); - // Make RegisterPackageUse throw an exception - journal.When(x => x.RegisterPackageUse( - Arg.Any(), - Arg.Any(), - Arg.Any())) - .Do(x => { throw new Exception("Journal write failed"); }); - - var command = new DownloadAndRegisterPackageCommand( - Substitute.For(), - variables, - fileSystem, - Substitute.For(), - Substitute.For(), - journal); - - var result = command.Execute(new[] - { - "--packageId=TestPackage", - "--packageVersion=1.0.0", - "--feedId=test-feed", - "--feedUri=https://test.feed.com" - }); - - // Should fail because registration failed - result.Should().NotBe(0); - } - } -} diff --git a/source/Calamari.Tests/Fixtures/Commands/FindAndRegisterPackageCommandTest.cs b/source/Calamari.Tests/Fixtures/Commands/FindAndRegisterPackageCommandTest.cs deleted file mode 100644 index ad646b3770..0000000000 --- a/source/Calamari.Tests/Fixtures/Commands/FindAndRegisterPackageCommandTest.cs +++ /dev/null @@ -1,129 +0,0 @@ -using Calamari.Commands; -using Calamari.Common.Commands; -using Calamari.Common.Features.Packages; -using Calamari.Common.Plumbing.Deployment.PackageRetention; -using Calamari.Common.Plumbing.FileSystem; -using Calamari.Common.Plumbing.Logging; -using Calamari.Common.Plumbing.Variables; -using Calamari.Integration.FileSystem; -using FluentAssertions; -using NSubstitute; -using NUnit.Framework; - -namespace Calamari.Tests.Fixtures.Commands -{ - [TestFixture] - public class FindAndRegisterPackageCommandTest - { - [Test] - public void SupportsSemverVersionFormats() - { - var command = new FindAndRegisterPackageCommand( - Substitute.For(), - Substitute.For(), - Substitute.For(), - Substitute.For()); - - // Version parsing should succeed (command will fail at find stage with mocks) - var result = command.Execute(new[] - { - "--packageId=TestPackage", - "--packageVersion=1.0.0", - "--taskId=ServerTasks-12345", - "--packageVersionFormat=Semver", - "--packageHash=abc123" - }); - - // Command will fail at find stage with mocks, but version format was valid - result.Should().Be(0); // Returns 0 even when package not found (matches FindPackageCommand behavior) - } - - [Test] - public void SupportsMavenVersionFormats() - { - var command = new FindAndRegisterPackageCommand( - Substitute.For(), - Substitute.For(), - Substitute.For(), - Substitute.For()); - - // Test Maven version format parsing - var result = command.Execute(new[] - { - "--packageId=com.example:test", - "--packageVersion=3.7.4.20220919T144341Z", - "--taskId=ServerTasks-12345", - "--packageVersionFormat=Maven", - "--packageHash=abc123" - }); - - // Command will fail at find stage with mocks, but version format was valid - result.Should().Be(0); // Returns 0 even when package not found (matches FindPackageCommand behavior) - } - - [Test] - [Timeout(5000)] // Fail fast if validation is missing - public void RequiresTaskIdArgument() - { - var log = Substitute.For(); - var command = new FindAndRegisterPackageCommand( - log, - Substitute.For(), - Substitute.For(), - Substitute.For()); - - var result = command.Execute(new[] - { - "--packageId=TestPackage", - "--packageVersion=1.0.0", - "--packageHash=abc123" - // Deliberately NOT providing --taskId - }); - - // Should fail because taskId is not provided - result.Should().NotBe(0); - - // Should fail because taskId is not provided - log.Received().Error(Arg.Is(s => s.Equals("No task ID was specified. Please pass --taskId YourTaskId"))); - } - - [Test] - public void FailsIfRegistrationFails() - { - var packageStore = Substitute.For(); - // Return a mock package so we reach the registration step - var version = Octopus.Versioning.VersionFactory.CreateSemanticVersion("1.0.0"); - var metadata = new PackageFileNameMetadata("TestPackage", version, version, ".nupkg"); - packageStore.GetPackage(Arg.Any(), Arg.Any(), Arg.Any()) - .Returns(new PackagePhysicalFileMetadata(metadata, "/test/path.nupkg", "abc123", 1000)); - - var fileSystem = Substitute.For(); - fileSystem.GetFileSize(Arg.Any()).Returns(1000); - - var journal = Substitute.For(); - // Make RegisterPackageUse throw an exception - journal.When(x => x.RegisterPackageUse( - Arg.Any(), - Arg.Any(), - Arg.Any())) - .Do(x => { throw new System.Exception("Journal write failed"); }); - - var command = new FindAndRegisterPackageCommand( - Substitute.For(), - packageStore, - journal, - fileSystem); - - var result = command.Execute(new[] - { - "--packageId=TestPackage", - "--packageVersion=1.0.0", - "--taskId=ServerTasks-12345", - "--packageHash=abc123" - }); - - // Should fail because registration failed - result.Should().NotBe(0); - } - } -} diff --git a/source/Calamari.Tests/Fixtures/FindPackage/FindAndRegisterPackageFixture.cs b/source/Calamari.Tests/Fixtures/FindPackage/FindAndRegisterPackageFixture.cs index 0f74dd97ba..a6f2dc4337 100644 --- a/source/Calamari.Tests/Fixtures/FindPackage/FindAndRegisterPackageFixture.cs +++ b/source/Calamari.Tests/Fixtures/FindPackage/FindAndRegisterPackageFixture.cs @@ -1,7 +1,9 @@ using System; using System.IO; +using System.Security.Cryptography; using Calamari.Common.Features.Packages; using Calamari.Common.Plumbing.FileSystem; +using Calamari.Common.Plumbing.ServiceMessages; using Calamari.Testing.Helpers; using Calamari.Tests.Fixtures.Deployment.Packages; using Calamari.Tests.Helpers; @@ -16,12 +18,22 @@ public class FindAndRegisterPackageFixture : CalamariFixture readonly static string tentacleHome = TestEnvironment.GetTestPath("temp", "FindAndRegisterPackage"); readonly static string downloadPath = Path.Combine(tentacleHome, "Files"); readonly string packageId = "Acme.Web"; + readonly string mavenPackageId = "com.acme:web"; readonly string packageVersion = "1.0.0"; + readonly string newpackageVersion = "1.0.1"; + + private readonly string mavenPackage = TestEnvironment.GetTestPath("Java", "Fixtures", "Deployment", "Packages", "HelloWorld.0.0.1.jar"); + private string mavenPackageHash; [OneTimeSetUp] public void TestFixtureSetUp() { Environment.SetEnvironmentVariable("TentacleHome", tentacleHome); + + using (var file = File.OpenRead(mavenPackage)) + { + mavenPackageHash = BitConverter.ToString(SHA1.Create().ComputeHash(file)).Replace("-", "").ToLowerInvariant(); + } } [OneTimeTearDown] @@ -55,6 +67,17 @@ CalamariResult FindAndRegisterPackage(string id, string version, string hash, Ve .Argument("packageHash", hash)); } + CalamariResult FindAndRegisterPackageExact(string id, string version, string hash, bool exactMatch) + { + return Invoke(Calamari() + .Action("find-and-register-package") + .Argument("packageId", id) + .Argument("packageVersion", version) + .Argument("taskId", "ServerTasks-12345") + .Argument("packageHash", hash) + .Argument("exactMatch", exactMatch)); + } + [Test] public void ShouldFindAndRegisterPackageAlreadyUploaded() { @@ -70,6 +93,9 @@ public void ShouldFindAndRegisterPackageAlreadyUploaded() result.AssertCalamariFoundPackageServiceMessage(Is.True); result.AssertFoundPackageServiceMessage(); + + // Verify package was registered with the journal + result.AssertOutput("Registered package use/lock for {0} v{1} and task ServerTasks-12345", packageId, packageVersion); } } @@ -127,5 +153,238 @@ public void ShouldNotRegisterEarlierPackageVersions() } } } + + [Test] + public void ShouldFindNoEarlierPackageVersions() + { + using (var acmeWeb = new TemporaryFile(PackageBuilder.BuildSamplePackage(packageId, packageVersion))) + { + var result = FindAndRegisterPackage(packageId, packageVersion, acmeWeb.Hash); + + result.AssertSuccess(); + result.AssertOutput("Package {0} version {1} hash {2} has not been uploaded.", packageId, packageVersion, acmeWeb.Hash); + result.AssertOutput("Finding earlier packages that have been uploaded to this Tentacle"); + result.AssertOutput("No earlier packages for {0} has been uploaded", packageId); + } + } + + [Test] + public void ShouldFindNoEarlierMavenPackageVersions() + { + var result = FindAndRegisterPackage(mavenPackageId, packageVersion, mavenPackageHash); + + result.AssertSuccess(); + result.AssertOutput("Package {0} version {1} hash {2} has not been uploaded.", + mavenPackageId, + packageVersion, mavenPackageHash); + result.AssertOutput("Finding earlier packages that have been uploaded to this Tentacle"); + result.AssertOutput("No earlier packages for {0} has been uploaded", mavenPackageId); + } + + [Test] + public void ShouldFindOneEarlierPackageVersion() + { + using (var acmeWeb = new TemporaryFile(PackageBuilder.BuildSamplePackage(packageId, packageVersion))) + { + var destinationFilePath = Path.Combine(downloadPath, PackageName.ToCachedFileName(packageId, VersionFactory.CreateSemanticVersion(packageVersion), ".nupkg")); + File.Copy(acmeWeb.FilePath, destinationFilePath); + + using (var newAcmeWeb = + new TemporaryFile(PackageBuilder.BuildSamplePackage(packageId, newpackageVersion))) + { + var result = FindAndRegisterPackage(packageId, newpackageVersion, newAcmeWeb.Hash); + + result.AssertSuccess(); + result.AssertOutput("Package {0} version {1} hash {2} has not been uploaded.", packageId, newpackageVersion, newAcmeWeb.Hash); + result.AssertOutput("Finding earlier packages that have been uploaded to this Tentacle"); + result.AssertOutput("Found 1 earlier version of {0} on this Tentacle", packageId); + result.AssertOutput(" - {0}: {1}", packageVersion, destinationFilePath); + + result.AssertFoundPackageServiceMessage(); + var foundPackage = result.CapturedOutput.FoundPackage; + Assert.AreEqual(VersionFactory.CreateSemanticVersion(packageVersion), foundPackage.Version); + Assert.AreEqual(acmeWeb.Hash, foundPackage.Hash); + Assert.AreEqual(destinationFilePath, foundPackage.RemotePath); + Assert.AreEqual(".nupkg", foundPackage.FileExtension); + Assert.AreEqual(packageId, foundPackage.PackageId); + } + } + } + + [Test] + public void ShouldNotFindEarlierPackageVersionWhenExactMatchRequested() + { + using (var acmeWeb = new TemporaryFile(PackageBuilder.BuildSamplePackage(packageId, packageVersion))) + { + var destinationFilePath = Path.Combine(downloadPath, PackageName.ToCachedFileName(packageId, VersionFactory.CreateSemanticVersion(packageVersion), ".nupkg")); + File.Copy(acmeWeb.FilePath, destinationFilePath); + + using (var newAcmeWeb = new TemporaryFile(PackageBuilder.BuildSamplePackage(packageId, newpackageVersion))) + { + var result = FindAndRegisterPackageExact(packageId, newpackageVersion, newAcmeWeb.Hash, true); + + result.AssertSuccess(); + result.AssertOutput("Package {0} version {1} hash {2} has not been uploaded.", packageId, newpackageVersion, + newAcmeWeb.Hash); + result.AssertNoOutput("Finding earlier packages that have been uploaded to this Tentacle"); + } + } + } + + [Test] + public void ShouldFindOneEarlierMavenPackageVersion() + { + var destinationFilePath = Path.Combine(downloadPath, PackageName.ToCachedFileName(mavenPackageId, VersionFactory.CreateMavenVersion(packageVersion), ".jar")); + File.Copy(mavenPackage, destinationFilePath); + + var result = FindAndRegisterPackage(mavenPackageId, newpackageVersion, mavenPackageHash, VersionFormat.Maven); + + result.AssertSuccess(); + result.AssertOutput("Package {0} version {1} hash {2} has not been uploaded.", + mavenPackageId, + newpackageVersion, + mavenPackageHash); + result.AssertOutput("Finding earlier packages that have been uploaded to this Tentacle"); + result.AssertOutput("Found 1 earlier version of {0} on this Tentacle", mavenPackageId); + result.AssertOutput(" - {0}: {1}", packageVersion, destinationFilePath); + + var foundPackage = result.CapturedOutput.FoundPackage; + Assert.AreEqual(VersionFactory.CreateMavenVersion(packageVersion), foundPackage.Version); + Assert.AreEqual(mavenPackageHash, foundPackage.Hash); + Assert.AreEqual(destinationFilePath, foundPackage.RemotePath); + Assert.AreEqual(".jar", foundPackage.FileExtension); + Assert.AreEqual(mavenPackageId, foundPackage.PackageId); + } + + [Test] + public void ShouldFindTheCorrectPackageWhenSimilarPackageExist() + { + using (var acmeWeb = new TemporaryFile(PackageBuilder.BuildSamplePackage(packageId, packageVersion))) + using (var acmeWebTest = new TemporaryFile(PackageBuilder.BuildSamplePackage(packageId + ".Tests", packageVersion))) + { + var destinationFilePath = Path.Combine(downloadPath, PackageName.ToCachedFileName(packageId, VersionFactory.CreateVersion(packageVersion, VersionFormat.Semver), ".nupkg")); + File.Copy(acmeWeb.FilePath, destinationFilePath); + + var destinationFilePathTest = Path.Combine(downloadPath, PackageName.ToCachedFileName(packageId + ".Tests", VersionFactory.CreateVersion(packageVersion, VersionFormat.Semver), ".nupkg")); + File.Copy(acmeWebTest.FilePath, destinationFilePathTest); + + using (var newAcmeWeb = + new TemporaryFile(PackageBuilder.BuildSamplePackage(packageId, newpackageVersion))) + { + var result = FindAndRegisterPackage(packageId, newpackageVersion, newAcmeWeb.Hash); + + result.AssertSuccess(); + result.AssertOutput("Package {0} version {1} hash {2} has not been uploaded.", packageId, + newpackageVersion, + newAcmeWeb.Hash); + result.AssertOutput("Finding earlier packages that have been uploaded to this Tentacle"); + result.AssertOutput("Found 1 earlier version of {0} on this Tentacle", packageId); + result.AssertOutput(" - {0}: {1}", packageVersion, destinationFilePath); + + result.AssertFoundPackageServiceMessage(); + var foundPackage = result.CapturedOutput.FoundPackage; + Assert.AreEqual(VersionFactory.CreateSemanticVersion(packageVersion), foundPackage.Version); + Assert.AreEqual(acmeWeb.Hash, foundPackage.Hash); + Assert.AreEqual(destinationFilePath, foundPackage.RemotePath); + Assert.AreEqual(".nupkg", foundPackage.FileExtension); + Assert.AreEqual(packageId, foundPackage.PackageId); + } + } + } + + [Test] + public void ShouldFindTheCorrectMavenPackageWhenSimilarPackageExist() + { + var destinationFilePath = Path.Combine(downloadPath, PackageName.ToCachedFileName(mavenPackageId, VersionFactory.CreateMavenVersion(packageVersion), ".jar")); + File.Copy(mavenPackage, destinationFilePath); + + var destination2FilePath = Path.Combine(downloadPath, PackageName.ToCachedFileName(mavenPackageId + ".Test", VersionFactory.CreateMavenVersion(packageVersion), ".jar")); + File.Copy(mavenPackage, destination2FilePath); + + var result = FindAndRegisterPackage(mavenPackageId, newpackageVersion, mavenPackageHash, VersionFormat.Maven); + + result.AssertSuccess(); + result.AssertOutput("Package {0} version {1} hash {2} has not been uploaded.", + mavenPackageId, + newpackageVersion, + mavenPackageHash); + result.AssertOutput("Finding earlier packages that have been uploaded to this Tentacle"); + result.AssertOutput("Found 1 earlier version of {0} on this Tentacle", mavenPackageId); + result.AssertOutput(" - {0}: {1}", packageVersion, destinationFilePath); + + result.AssertFoundPackageServiceMessage(); + var foundPackage = result.CapturedOutput.FoundPackage; + Assert.AreEqual(VersionFactory.CreateMavenVersion(packageVersion), foundPackage.Version); + Assert.AreEqual(mavenPackageHash, foundPackage.Hash); + Assert.AreEqual(destinationFilePath, foundPackage.RemotePath); + Assert.AreEqual(".jar", foundPackage.FileExtension); + Assert.AreEqual(mavenPackageId, foundPackage.PackageId); + } + + [Test] + public void ShouldFindMavenPackageAlreadyUploaded() + { + var destinationFilePath = Path.Combine(downloadPath, PackageName.ToCachedFileName(mavenPackageId, VersionFactory.CreateMavenVersion(packageVersion), ".jar")); + File.Copy(mavenPackage, destinationFilePath); + + var result = FindAndRegisterPackage(mavenPackageId, packageVersion, mavenPackageHash, VersionFormat.Maven); + + result.AssertSuccess(); + result.AssertCalamariFoundPackageServiceMessage(Is.True, + message: "Expected service message '{0}' to be True", + args: ServiceMessageNames.CalamariFoundPackage.Name); + + result.AssertOutput( + "Package {0} {1} hash {2} has already been uploaded", + mavenPackageId, + packageVersion, + mavenPackageHash); + + var foundPackage = result.CapturedOutput.FoundPackage; + Assert.AreEqual(VersionFactory.CreateMavenVersion(packageVersion), foundPackage.Version); + Assert.AreEqual(mavenPackageHash, foundPackage.Hash); + Assert.AreEqual(destinationFilePath, foundPackage.RemotePath); + Assert.AreEqual(".jar", foundPackage.FileExtension); + Assert.AreEqual(mavenPackageId, foundPackage.PackageId); + + // Verify package was registered with the journal + result.AssertOutput("Registered package use/lock for {0} v{1} and task ServerTasks-12345", mavenPackageId, packageVersion); + } + + [Test] + public void ShouldFailWhenNoPackageIdIsSpecified() + { + var result = FindAndRegisterPackage("", "1.0.0", "Hash"); + + result.AssertFailure(); + result.AssertErrorOutput("No package ID was specified. Please pass --packageId YourPackage"); + } + + [Test] + public void ShouldFailWhenNoPackageVersionIsSpecified() + { + var result = FindAndRegisterPackage("Calamari", "", "Hash"); + + result.AssertFailure(); + result.AssertErrorOutput("No package version was specified. Please pass --packageVersion 1.0.0.0"); + } + + [Test] + public void ShouldFailWhenInvalidPackageVersionIsSpecified() + { + var result = FindAndRegisterPackage("Calamari", "1.0.*", "Hash"); + + result.AssertFailure(); + result.AssertErrorOutput("Package version '1.0.*' is not a valid Semver version string. Please pass --packageVersionFormat with a different version type."); + } + + [Test] + public void ShouldFailWhenNoPackageHashIsSpecified() + { + var result = FindAndRegisterPackage("Calamari", "1.0.0", ""); + + result.AssertFailure(); + result.AssertErrorOutput("No package hash was specified. Please pass --packageHash YourPackageHash"); + } } } diff --git a/source/Calamari.Tests/Fixtures/PackageDownload/DownloadAndRegisterPackageFixture.cs b/source/Calamari.Tests/Fixtures/PackageDownload/DownloadAndRegisterPackageFixture.cs index a82658692f..cc109ee711 100644 --- a/source/Calamari.Tests/Fixtures/PackageDownload/DownloadAndRegisterPackageFixture.cs +++ b/source/Calamari.Tests/Fixtures/PackageDownload/DownloadAndRegisterPackageFixture.cs @@ -1,9 +1,14 @@ using System; using System.Globalization; using System.IO; +using System.Threading; +using System.Threading.Tasks; using Calamari.Common.Features.Packages; +using Calamari.Common.Plumbing.FileSystem; +using Calamari.Testing; using Calamari.Testing.Helpers; using Calamari.Testing.Requirements; +using Calamari.Tests.Fixtures.Deployment.Packages; using Calamari.Tests.Helpers; using NUnit.Framework; using Octopus.Versioning; @@ -18,6 +23,8 @@ public class DownloadAndRegisterPackageFixture : CalamariFixture static readonly string DownloadPath = TestEnvironment.GetTestPath(TentacleHome, "Files"); static readonly string PublicFeedUri = "https://f.feedz.io/octopus-deploy/integration-tests/nuget/index.json"; + static readonly string NuGetFeedUri = "https://www.nuget.org/api/v2/"; + static readonly string ExpectedPackageHash = "1e0856338eb5ada3b30903b980cef9892ebf7201"; static readonly long ExpectedPackageSize = 3749; static readonly SampleFeedPackage FeedzPackage = new SampleFeedPackage() @@ -26,6 +33,8 @@ public class DownloadAndRegisterPackageFixture : CalamariFixture Version = new SemanticVersion("1.0.0"), PackageId = "OctoConsole" }; + static readonly SampleFeedPackage FileShare = new SampleFeedPackage() { Id = "feeds-local", Version = new SemanticVersion(1, 0, 0), PackageId = "Acme.Web" }; + static readonly SampleFeedPackage NuGetFeed = new SampleFeedPackage() { Id = "feeds-nuget", Version = new SemanticVersion(2, 1, 0), PackageId = "Abp.Castle.Log4Net" }; static readonly string MavenPublicFeedUri = "https://repo.maven.apache.org/maven2/"; static readonly string ExpectedMavenPackageHash = "3564ef3803de51fb0530a8377ec6100b33b0d073"; @@ -37,6 +46,21 @@ public class DownloadAndRegisterPackageFixture : CalamariFixture PackageId = "com.google.guava:guava" }; + static string AuthFeedUri; + static string AuthFeedUsername; + static string AuthFeedPassword; + + static readonly CancellationTokenSource CancellationTokenSource = new CancellationTokenSource(); + readonly CancellationToken cancellationToken = CancellationTokenSource.Token; + + [OneTimeSetUp] + public async Task OneTimeSetup() + { + AuthFeedUri = await ExternalVariables.Get(ExternalVariable.MyGetFeedUrl, cancellationToken); + AuthFeedUsername = await ExternalVariables.Get(ExternalVariable.MyGetFeedUsername, cancellationToken); + AuthFeedPassword = await ExternalVariables.Get(ExternalVariable.MyGetFeedPassword, cancellationToken); + } + [SetUp] public void SetUp() { @@ -52,8 +76,11 @@ public void SetUp() [TearDown] public void TearDown() { - if (Directory.Exists(DownloadPath)) - Directory.Delete(DownloadPath, true); + // Change to a safe directory before deleting TentacleHome + Directory.SetCurrentDirectory(Path.GetTempPath()); + + if (Directory.Exists(TentacleHome)) + Directory.Delete(TentacleHome, true); Environment.SetEnvironmentVariable("TentacleHome", null); } @@ -67,16 +94,15 @@ public void ShouldDownloadAndRegisterPackage() PublicFeedUri); result.AssertSuccess(); - - result.AssertOutput("Downloading package {0} v{1}...", FeedzPackage.PackageId, FeedzPackage.Version); - result.AssertOutput("Downloading NuGet package {0} v{1} from feed: '{2}'", - FeedzPackage.PackageId, FeedzPackage.Version, PublicFeedUri); result.AssertOutput("Package {0} v{1} successfully downloaded from feed: '{2}'", FeedzPackage.PackageId, FeedzPackage.Version, PublicFeedUri); AssertPackageHashMatchesExpected(result, ExpectedPackageHash); AssertPackageSizeMatchesExpected(result, ExpectedPackageSize); AssertStagePackageOutputVariableSet(result, FeedzPackage); + + // Verify package was registered with the journal + result.AssertOutput("Registered package use/lock for {0} v{1} and task ServerTasks-12345", FeedzPackage.PackageId, FeedzPackage.Version); } [Test] @@ -93,15 +119,18 @@ public void ShouldDownloadAndRegisterMavenPackage() result.AssertSuccess(); - result.AssertOutput("Downloading package {0} v{1}...", MavenPublicFeed.PackageId, MavenPublicFeed.Version); result.AssertOutput("Downloading Maven package {0} v{1} from feed: '{2}'", MavenPublicFeed.PackageId, MavenPublicFeed.Version, MavenPublicFeedUri); result.AssertOutput("Package {0} v{1} successfully downloaded from feed: '{2}'", MavenPublicFeed.PackageId, MavenPublicFeed.Version, MavenPublicFeedUri); + result.AssertOutput("Found package {0} v{1}", MavenPublicFeed.PackageId, MavenPublicFeed.Version); AssertPackageHashMatchesExpected(result, ExpectedMavenPackageHash); AssertPackageSizeMatchesExpected(result, ExpectedMavenPackageSize); AssertStagePackageOutputVariableSet(result, MavenPublicFeed); + + // Verify package was registered with the journal + result.AssertOutput("Registered package use/lock for {0} v{1} and task ServerTasks-12345", MavenPublicFeed.PackageId, MavenPublicFeed.Version); } [Test] @@ -121,6 +150,9 @@ public void ShouldSetOutputVariables() Is.EqualTo(ExpectedPackageSize.ToString(CultureInfo.InvariantCulture))); result.AssertOutputVariable("StagedPackage.FullPathOnRemoteMachine", Does.Match(PackageName.ToRegexPattern(FeedzPackage.PackageId, FeedzPackage.Version, FeedzPackage.DownloadFolder) + ".*")); + + // Verify package was registered with the journal + result.AssertOutput("Registered package use/lock for {0} v{1} and task ServerTasks-12345", FeedzPackage.PackageId, FeedzPackage.Version); } [Test] @@ -134,6 +166,7 @@ public void ShouldUsePackageFromCacheAndStillRegister() PublicFeedUri); firstResult.AssertSuccess(); + firstResult.AssertOutput("Registered package use/lock for {0} v{1} and task ServerTasks-12345", FeedzPackage.PackageId, FeedzPackage.Version); // Second download should use cache but still register var secondResult = DownloadAndRegisterPackage( @@ -153,6 +186,9 @@ public void ShouldUsePackageFromCacheAndStillRegister() AssertPackageHashMatchesExpected(secondResult, ExpectedPackageHash); AssertPackageSizeMatchesExpected(secondResult, ExpectedPackageSize); AssertStagePackageOutputVariableSet(secondResult, FeedzPackage); + + // Verify package was registered with the journal (even when using cache) + secondResult.AssertOutput("Registered package use/lock for {0} v{1} and task ServerTasks-12345", FeedzPackage.PackageId, FeedzPackage.Version); } [Test] @@ -173,6 +209,382 @@ public void ShouldFailWhenTaskIdNotProvided() result.AssertFailure(); } + [Test] + public void ShouldDownloadAndRegisterPackageWithRepositoryMetadata() + { + var result = DownloadAndRegisterPackage(NuGetFeed.PackageId, NuGetFeed.Version.ToString(), NuGetFeed.Id, NuGetFeedUri); + + result.AssertSuccess(); + + result.AssertOutput( + $"Downloading NuGet package {NuGetFeed.PackageId} v{NuGetFeed.Version} from feed: '{NuGetFeedUri}'"); + result.AssertOutput($"Downloaded package will be stored in: '{NuGetFeed.DownloadFolder}"); + + AssertStagePackageOutputVariableSet(result, NuGetFeed); + result.AssertOutput($"Package {NuGetFeed.PackageId} v{NuGetFeed.Version} successfully downloaded from feed: '{NuGetFeedUri}'"); + + // Verify package was registered with the journal + result.AssertOutput("Registered package use/lock for {0} v{1} and task ServerTasks-12345", NuGetFeed.PackageId, NuGetFeed.Version); + } + + [Test] + [RequiresNonFreeBSDPlatform] + public void ShouldUseMavenPackageFromCacheAndStillRegister() + { + DownloadAndRegisterPackage(MavenPublicFeed.PackageId, + MavenPublicFeed.Version.ToString(), + MavenPublicFeed.Id, + MavenPublicFeedUri, + feedType: FeedType.Maven, + versionFormat: VersionFormat.Maven) + .AssertSuccess(); + + var result = DownloadAndRegisterPackage(MavenPublicFeed.PackageId, + MavenPublicFeed.Version.ToString(), + MavenPublicFeed.Id, + MavenPublicFeedUri, + feedType: FeedType.Maven, + versionFormat: VersionFormat.Maven); + + result.AssertSuccess(); + + result.AssertOutput("Checking package cache for package {0} v{1}", MavenPublicFeed.PackageId, MavenPublicFeed.Version); + result.AssertOutputMatches(string.Format("Package was found in cache\\. No need to download. Using file: '{0}'", PackageName.ToRegexPattern(MavenPublicFeed.PackageId, MavenPublicFeed.Version, MavenPublicFeed.DownloadFolder))); + AssertPackageHashMatchesExpected(result, ExpectedMavenPackageHash); + AssertPackageSizeMatchesExpected(result, ExpectedMavenPackageSize); + AssertStagePackageOutputVariableSet(result, MavenPublicFeed); + + // Verify package was registered with the journal (even when using cache) + result.AssertOutput("Registered package use/lock for {0} v{1} and task ServerTasks-12345", MavenPublicFeed.PackageId, MavenPublicFeed.Version); + } + + [Test] + [RequiresNonFreeBSDPlatform] + public void ShouldUseMavenSnapshotPackageFromCacheAndStillRegister() + { + DownloadAndRegisterPackage(MavenPublicFeed.PackageId, + MavenPublicFeed.Version.ToString(), + MavenPublicFeed.Id, + MavenPublicFeedUri, + feedType: FeedType.Maven, + versionFormat: VersionFormat.Maven) + .AssertSuccess(); + + var result = DownloadAndRegisterPackage(MavenPublicFeed.PackageId, + MavenPublicFeed.Version.ToString(), + MavenPublicFeed.Id, MavenPublicFeedUri, + feedType: FeedType.Maven, + versionFormat: VersionFormat.Maven); + + result.AssertSuccess(); + + result.AssertOutput("Checking package cache for package {0} v{1}", MavenPublicFeed.PackageId, MavenPublicFeed.Version); + result.AssertOutputMatches($"Package was found in cache\\. No need to download. Using file: '{PackageName.ToRegexPattern(MavenPublicFeed.PackageId, MavenPublicFeed.Version, MavenPublicFeed.DownloadFolder)}'"); + AssertPackageHashMatchesExpected(result, ExpectedMavenPackageHash); + AssertPackageSizeMatchesExpected(result, ExpectedMavenPackageSize); + AssertStagePackageOutputVariableSet(result, MavenPublicFeed); + + // Verify package was registered with the journal (even when using cache) + result.AssertOutput("Registered package use/lock for {0} v{1} and task ServerTasks-12345", MavenPublicFeed.PackageId, MavenPublicFeed.Version); + } + + [Test] + public void ShouldByPassCacheAndDownloadAndRegisterPackage() + { + DownloadAndRegisterPackage(FeedzPackage.PackageId, + FeedzPackage.Version.ToString(), + FeedzPackage.Id, PublicFeedUri).AssertSuccess(); + + var result = DownloadAndRegisterPackage(FeedzPackage.PackageId, + FeedzPackage.Version.ToString(), + FeedzPackage.Id, PublicFeedUri, + forcePackageDownload: true); + + result.AssertSuccess(); + + result.AssertOutput("Downloading NuGet package {0} v{1} from feed: '{2}'", FeedzPackage.PackageId, FeedzPackage.Version, PublicFeedUri); + result.AssertOutput("Downloaded package will be stored in: '{0}'", FeedzPackage.DownloadFolder); + AssertPackageHashMatchesExpected(result, ExpectedPackageHash); + AssertPackageSizeMatchesExpected(result, ExpectedPackageSize); + AssertStagePackageOutputVariableSet(result, FeedzPackage); + result.AssertOutput("Package {0} v{1} successfully downloaded from feed: '{2}'", FeedzPackage.PackageId, FeedzPackage.Version, PublicFeedUri); + + // Verify package was registered with the journal + result.AssertOutput("Registered package use/lock for {0} v{1} and task ServerTasks-12345", FeedzPackage.PackageId, FeedzPackage.Version); + } + + [Test] + [RequiresNonFreeBSDPlatform] + public void ShouldByPassCacheAndDownloadAndRegisterMavenPackage() + { + var firstDownload = DownloadAndRegisterPackage( + MavenPublicFeed.PackageId, + MavenPublicFeed.Version.ToString(), + MavenPublicFeed.Id, + MavenPublicFeedUri, + versionFormat: VersionFormat.Maven, + feedType: FeedType.Maven); + + firstDownload.AssertSuccess(); + + var secondDownload = DownloadAndRegisterPackage( + MavenPublicFeed.PackageId, + MavenPublicFeed.Version.ToString(), + MavenPublicFeed.Id, + MavenPublicFeedUri, + feedType: FeedType.Maven, + versionFormat: VersionFormat.Maven, + forcePackageDownload: true); + + secondDownload.AssertSuccess(); + + secondDownload.AssertOutput("Downloading Maven package {0} v{1} from feed: '{2}'", MavenPublicFeed.PackageId, MavenPublicFeed.Version, MavenPublicFeedUri); + secondDownload.AssertOutput("Downloaded package will be stored in: '{0}'", MavenPublicFeed.DownloadFolder); + secondDownload.AssertOutput("Found package {0} v{1}", MavenPublicFeed.PackageId, MavenPublicFeed.Version); + AssertPackageHashMatchesExpected(secondDownload, ExpectedMavenPackageHash); + AssertPackageSizeMatchesExpected(secondDownload, ExpectedMavenPackageSize); + AssertStagePackageOutputVariableSet(secondDownload, MavenPublicFeed); + secondDownload.AssertOutput("Package {0} v{1} successfully downloaded from feed: '{2}'", MavenPublicFeed.PackageId, MavenPublicFeed.Version, MavenPublicFeedUri); + + // Verify package was registered with the journal + secondDownload.AssertOutput("Registered package use/lock for {0} v{1} and task ServerTasks-12345", MavenPublicFeed.PackageId, MavenPublicFeed.Version); + } + + [Test] + [Ignore("Auth Feed Failing On Mono")] + public void PrivateNuGetFeedShouldDownloadAndRegisterPackage() + { + var result = DownloadAndRegisterPackage(FeedzPackage.PackageId, FeedzPackage.Version.ToString(), FeedzPackage.Id, AuthFeedUri, AuthFeedUsername, AuthFeedPassword); + + result.AssertSuccess(); + result.AssertOutput("Downloading NuGet package {0} v{1} from feed: '{2}'", FeedzPackage.PackageId, FeedzPackage.Version, AuthFeedUri); + result.AssertOutput("Downloaded package will be stored in: '{0}'", FeedzPackage.DownloadFolder); + + AssertPackageHashMatchesExpected(result, ExpectedPackageHash); + AssertPackageSizeMatchesExpected(result, ExpectedPackageSize); + AssertStagePackageOutputVariableSet(result, FeedzPackage); + result.AssertOutput("Package {0} v{1} successfully downloaded from feed: '{2}'", FeedzPackage.PackageId, FeedzPackage.Version, AuthFeedUri); + + // Verify package was registered with the journal + result.AssertOutput("Registered package use/lock for {0} v{1} and task ServerTasks-12345", FeedzPackage.PackageId, FeedzPackage.Version); + } + + [Test] + [Ignore("Auth Feed Failing On Mono")] + public void PrivateNuGetFeedShouldUsePackageFromCacheAndStillRegister() + { + DownloadAndRegisterPackage(FeedzPackage.PackageId, FeedzPackage.Version.ToString(), FeedzPackage.Id, AuthFeedUri, AuthFeedUsername, AuthFeedPassword) + .AssertSuccess(); + + var result = DownloadAndRegisterPackage(FeedzPackage.PackageId, FeedzPackage.Version.ToString(), FeedzPackage.Id, AuthFeedUri, AuthFeedUsername, AuthFeedPassword); + + result.AssertSuccess(); + + result.AssertOutput("Checking package cache for package {0} v{1}", FeedzPackage.PackageId, FeedzPackage.Version); + result.AssertOutputMatches($"Package was found in cache\\. No need to download. Using file: '{PackageName.ToRegexPattern(FeedzPackage.PackageId, FeedzPackage.Version, FeedzPackage.DownloadFolder)}'"); + AssertPackageHashMatchesExpected(result, ExpectedPackageHash); + AssertPackageSizeMatchesExpected(result, ExpectedPackageSize); + AssertStagePackageOutputVariableSet(result, FeedzPackage); + + // Verify package was registered with the journal (even when using cache) + result.AssertOutput("Registered package use/lock for {0} v{1} and task ServerTasks-12345", FeedzPackage.PackageId, FeedzPackage.Version); + } + + [Test] + [Ignore("Auth Feed Failing On Mono")] + public void PrivateNuGetFeedShouldByPassCacheAndDownloadAndRegisterPackage() + { + DownloadAndRegisterPackage(FeedzPackage.PackageId, FeedzPackage.Version.ToString(), FeedzPackage.Id, AuthFeedUri, AuthFeedUsername, AuthFeedPassword).AssertSuccess(); + + var result = DownloadAndRegisterPackage(FeedzPackage.PackageId, FeedzPackage.Version.ToString(), FeedzPackage.Id, AuthFeedUri, AuthFeedUsername, AuthFeedPassword, forcePackageDownload: true); + + result.AssertSuccess(); + + result.AssertOutput("Downloading NuGet package {0} v{1} from feed: '{2}'", FeedzPackage.PackageId, FeedzPackage.Version, AuthFeedUri); + result.AssertOutput("Downloaded package will be stored in: '{0}'", FeedzPackage.DownloadFolder); + + AssertPackageHashMatchesExpected(result, ExpectedPackageHash); + AssertPackageSizeMatchesExpected(result, ExpectedPackageSize); + AssertStagePackageOutputVariableSet(result, FeedzPackage); + result.AssertOutput("Package {0} v{1} successfully downloaded from feed: '{2}'", FeedzPackage.PackageId, FeedzPackage.Version, AuthFeedUri); + + // Verify package was registered with the journal + result.AssertOutput("Registered package use/lock for {0} v{1} and task ServerTasks-12345", FeedzPackage.PackageId, FeedzPackage.Version); + } + + [Test] + [Ignore("Auth Feed Failing On Mono")] + public void PrivateNuGetFeedShouldFailDownloadAndRegisterPackageWhenInvalidCredentials() + { + var result = DownloadAndRegisterPackage(FeedzPackage.PackageId, FeedzPackage.Version.ToString(), FeedzPackage.Id, AuthFeedUri, "fake-feed-username", "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"); + + result.AssertFailure(); + + result.AssertOutput("Downloading NuGet package {0} v{1} from feed: '{2}'", FeedzPackage.PackageId, FeedzPackage.Version, AuthFeedUri); + result.AssertOutput("Downloaded package will be stored in: '{0}'", FeedzPackage.DownloadFolder); + result.AssertErrorOutput("Unable to download package: The remote server returned an error: (401) Unauthorized."); + } + + [Test] + public void FileShareFeedShouldDownloadAndRegisterPackage() + { + using (var acmeWeb = CreateSamplePackage()) + { + var result = DownloadAndRegisterPackage(FileShare.PackageId, FileShare.Version.ToString(), FileShare.Id, acmeWeb.DirectoryPath); + + result.AssertSuccess(); + + result.AssertOutput("Downloading NuGet package {0} v{1} from feed: '{2}'", FileShare.PackageId, FileShare.Version, new Uri(acmeWeb.DirectoryPath)); + result.AssertOutput("Downloaded package will be stored in: '{0}'", FileShare.DownloadFolder); + result.AssertOutput("Found package {0} v{1}", FileShare.PackageId, FileShare.Version); + AssertPackageHashMatchesExpected(result, acmeWeb.Hash); + AssertPackageSizeMatchesExpected(result, acmeWeb.Size); + AssertStagePackageOutputVariableSet(result, FileShare); + result.AssertOutput("Package {0} v{1} successfully downloaded from feed: '{2}'", FileShare.PackageId, FileShare.Version, acmeWeb.DirectoryPath); + + // Verify package was registered with the journal + result.AssertOutput("Registered package use/lock for {0} v{1} and task ServerTasks-12345", FileShare.PackageId, FileShare.Version); + } + } + + [Test] + public void FileShareFeedShouldUsePackageFromCacheAndStillRegister() + { + using (var acmeWeb = CreateSamplePackage()) + { + DownloadAndRegisterPackage(FileShare.PackageId, FileShare.Version.ToString(), FileShare.Id, acmeWeb.DirectoryPath).AssertSuccess(); + + var result = DownloadAndRegisterPackage(FileShare.PackageId, FileShare.Version.ToString(), FileShare.Id, acmeWeb.DirectoryPath); + result.AssertSuccess(); + + result.AssertOutput("Checking package cache for package {0} v{1}", FileShare.PackageId, FileShare.Version); + result.AssertOutputMatches($"Package was found in cache\\. No need to download. Using file: '{PackageName.ToRegexPattern(FileShare.PackageId, FileShare.Version, FileShare.DownloadFolder)}'"); + AssertPackageHashMatchesExpected(result, acmeWeb.Hash); + AssertPackageSizeMatchesExpected(result, acmeWeb.Size); + AssertStagePackageOutputVariableSet(result, FileShare); + + // Verify package was registered with the journal (even when using cache) + result.AssertOutput("Registered package use/lock for {0} v{1} and task ServerTasks-12345", FileShare.PackageId, FileShare.Version); + } + } + + [Test] + public void FileShareFeedShouldByPassCacheAndDownloadAndRegisterPackage() + { + using (var acmeWeb = CreateSamplePackage()) + { + DownloadAndRegisterPackage(FileShare.PackageId, FileShare.Version.ToString(), FileShare.Id, acmeWeb.DirectoryPath) + .AssertSuccess(); + + var result = DownloadAndRegisterPackage(FileShare.PackageId, FileShare.Version.ToString(), FileShare.Id, acmeWeb.DirectoryPath, + forcePackageDownload: true); + + result.AssertSuccess(); + + result.AssertOutput("Downloading NuGet package {0} v{1} from feed: '{2}'", FileShare.PackageId, FileShare.Version, new Uri(acmeWeb.DirectoryPath)); + result.AssertOutput("Downloaded package will be stored in: '{0}'", FileShare.DownloadFolder); + result.AssertOutput("Found package {0} v{1}", FileShare.PackageId, FileShare.Version); + AssertPackageHashMatchesExpected(result, acmeWeb.Hash); + AssertPackageSizeMatchesExpected(result, acmeWeb.Size); + AssertStagePackageOutputVariableSet(result, FileShare); + result.AssertOutput("Package {0} v{1} successfully downloaded from feed: '{2}'", FileShare.PackageId, FileShare.Version, acmeWeb.DirectoryPath); + + // Verify package was registered with the journal + result.AssertOutput("Registered package use/lock for {0} v{1} and task ServerTasks-12345", FileShare.PackageId, FileShare.Version); + } + } + + [Test] + public void FileShareFeedShouldFailDownloadAndRegisterPackageWhenInvalidFileShare() + { + using (var acmeWeb = CreateSamplePackage()) + { + var invalidFileShareUri = new Uri(Path.Combine(acmeWeb.DirectoryPath, "InvalidPath")); + + var result = DownloadAndRegisterPackage(FileShare.PackageId, FileShare.Version.ToString(), FileShare.Id, invalidFileShareUri.ToString()); + result.AssertFailure(); + + result.AssertOutput("Downloading NuGet package {0} v{1} from feed: '{2}'", FileShare.PackageId, FileShare.Version, invalidFileShareUri); + result.AssertErrorOutput("Failed to download and register package Acme.Web v1.0.0 from feed: '{0}'", invalidFileShareUri); + result.AssertErrorOutput("Failed to download and register package {0} v{1} from feed: '{2}'", FileShare.PackageId, FileShare.Version, invalidFileShareUri); + } + } + + [Test] + public void ShouldFailWhenNoPackageId() + { + var result = DownloadAndRegisterPackage("", FeedzPackage.Version.ToString(), FeedzPackage.Id, PublicFeedUri); + result.AssertFailure(); + + result.AssertErrorOutput("No package ID was specified"); + } + + [Test] + public void ShouldFailWhenInvalidPackageId() + { + var invalidPackageId = string.Format("X{0}X", FeedzPackage.PackageId); + var result = DownloadAndRegisterPackage(invalidPackageId, FeedzPackage.Version.ToString(), FeedzPackage.Id, PublicFeedUri); + result.AssertFailure(); + + result.AssertErrorOutput("Failed to download and register package {0} v{1} from feed: '{2}'", invalidPackageId, FeedzPackage.Version, PublicFeedUri); + } + + [Test] + public void ShouldFailWhenNoFeedVersion() + { + var result = DownloadAndRegisterPackage(FeedzPackage.PackageId, "", FeedzPackage.Id, PublicFeedUri); + result.AssertFailure(); + + result.AssertErrorOutput("No package version was specified"); + } + + [Test] + public void ShouldFailWhenInvalidFeedVersion() + { + const string invalidFeedVersion = "1.0.x"; + var result = DownloadAndRegisterPackage(FeedzPackage.PackageId, invalidFeedVersion, FeedzPackage.Id, PublicFeedUri); + result.AssertFailure(); + + result.AssertErrorOutput("Package version '{0}' specified is not a valid Semver version string", invalidFeedVersion); + } + + [Test] + public void ShouldFailWhenNoFeedId() + { + var result = DownloadAndRegisterPackage(FeedzPackage.PackageId, FeedzPackage.Version.ToString(), "", PublicFeedUri); + result.AssertFailure(); + + result.AssertErrorOutput("No feed ID was specified"); + } + + [Test] + public void ShouldFailWhenNoFeedUri() + { + var result = DownloadAndRegisterPackage(FeedzPackage.PackageId, FeedzPackage.Version.ToString(), FeedzPackage.Id, ""); + result.AssertFailure(); + + result.AssertErrorOutput("No feed URI was specified"); + } + + [Test] + public void ShouldFailWhenInvalidFeedUri() + { + var result = DownloadAndRegisterPackage(FeedzPackage.PackageId, FeedzPackage.Version.ToString(), FeedzPackage.Id, "www.myget.org/F/octopusdeploy-tests"); + result.AssertFailure(); + + result.AssertErrorOutput("URI specified 'www.myget.org/F/octopusdeploy-tests' is not a valid URI"); + } + + [Test] + [Ignore("for now, runs fine locally...not sure why it's not failing in TC, will investigate")] + public void ShouldFailWhenUsernameIsSpecifiedButNoPassword() + { + var result = DownloadAndRegisterPackage(FeedzPackage.PackageId, FeedzPackage.Version.ToString(), FeedzPackage.Id, PublicFeedUri, AuthFeedUsername); + result.AssertFailure(); + + result.AssertErrorOutput("A username was specified but no password was provided"); + } + CalamariResult DownloadAndRegisterPackage( string packageId, string packageVersion, @@ -227,6 +639,11 @@ static void AssertStagePackageOutputVariableSet(CalamariResult result, SampleFee result.AssertOutputVariable("StagedPackage.FullPathOnRemoteMachine", Does.Match(newPackageRegex + ".*")); } + private TemporaryFile CreateSamplePackage() + { + return new TemporaryFile(PackageBuilder.BuildSamplePackage(FileShare.PackageId, FileShare.Version.ToString())); + } + class SampleFeedPackage { public SampleFeedPackage() diff --git a/source/Calamari/Commands/DownloadAndRegisterPackageCommand.cs b/source/Calamari/Commands/DownloadAndRegisterPackageCommand.cs index 0f5883a12c..eac623549e 100644 --- a/source/Calamari/Commands/DownloadAndRegisterPackageCommand.cs +++ b/source/Calamari/Commands/DownloadAndRegisterPackageCommand.cs @@ -43,16 +43,18 @@ public DownloadAndRegisterPackageCommand( public override int Execute(string[] commandLineArguments) { Options.Parse(commandLineArguments); - if (taskId == null) - throw new CommandException("No task ID was specified. Please pass --taskId YourTaskId"); try { - var pkg = downloadService.DownloadPackage(options); var version = VersionFactory.TryCreateVersion(options.PackageVersion, options.VersionFormat); + + if (taskId == null) + throw new CommandException("No task ID was specified. Please pass --taskId YourTaskId"); + RegisterPackageUse(pkg, version); + return 0; } catch (Exception ex) { @@ -60,9 +62,6 @@ public override int Execute(string[] commandLineArguments) options.PackageId, options.PackageVersion, options.FeedUri); return ConsoleFormatter.PrintError(log, ex); } - - return 0; - } void RegisterPackageUse(PackagePhysicalFileMetadata pkg, IVersion version) diff --git a/source/Calamari/Commands/FindAndRegisterPackageCommand.cs b/source/Calamari/Commands/FindAndRegisterPackageCommand.cs index 619656743c..f492cdf322 100644 --- a/source/Calamari/Commands/FindAndRegisterPackageCommand.cs +++ b/source/Calamari/Commands/FindAndRegisterPackageCommand.cs @@ -44,14 +44,17 @@ public override int Execute(string[] commandLineArguments) try { - if (taskId == null) - throw new CommandException("No task ID was specified. Please pass --taskId YourTaskId"); - + // Find and validate package options first (packageId, packageVersion, packageHash) var package = findService.FindPackage(options); if (package != null) { var version = VersionFactory.TryCreateVersion(options.PackageVersion, options.VersionFormat); + + // Then validate taskId before registration + if (taskId == null) + throw new CommandException("No task ID was specified. Please pass --taskId YourTaskId"); + RegisterPackageUse(package, version); } From 3939f52a538af9e85e4ac496b60d167ba7bc084e Mon Sep 17 00:00:00 2001 From: David Staniec Date: Fri, 15 May 2026 12:27:48 +0800 Subject: [PATCH 3/5] Making more of the code common for download / find registration and non-registration variants --- .../FindAndRegisterPackageFixture.cs | 1 + .../DownloadAndRegisterPackageFixture.cs | 1 + .../DownloadAndRegisterPackageCommand.cs | 19 +-- .../Commands/FindAndRegisterPackageCommand.cs | 19 +-- .../Support/IPackageDownloadOptions.cs | 20 +++ .../Commands/Support/IPackageFindOptions.cs | 12 ++ .../PackageDownloadAndRegisterOptions.cs | 29 ++++ .../Support/PackageDownloadOptions.cs | 4 +- .../Support/PackageDownloadService.cs | 129 +++++++++++++----- .../Commands/Support/PackageFindOptions.cs | 4 +- .../Support/PackageFindRegistrationOptions.cs | 23 ++++ .../Commands/Support/PackageFindService.cs | 34 +++-- 12 files changed, 219 insertions(+), 76 deletions(-) create mode 100644 source/Calamari/Commands/Support/IPackageDownloadOptions.cs create mode 100644 source/Calamari/Commands/Support/IPackageFindOptions.cs create mode 100644 source/Calamari/Commands/Support/PackageDownloadAndRegisterOptions.cs create mode 100644 source/Calamari/Commands/Support/PackageFindRegistrationOptions.cs diff --git a/source/Calamari.Tests/Fixtures/FindPackage/FindAndRegisterPackageFixture.cs b/source/Calamari.Tests/Fixtures/FindPackage/FindAndRegisterPackageFixture.cs index a6f2dc4337..a2de05f8bf 100644 --- a/source/Calamari.Tests/Fixtures/FindPackage/FindAndRegisterPackageFixture.cs +++ b/source/Calamari.Tests/Fixtures/FindPackage/FindAndRegisterPackageFixture.cs @@ -128,6 +128,7 @@ public void ShouldFailWhenTaskIdNotProvided() .Argument("packageHash", acmeWeb.Hash)); result.AssertFailure(); + result.AssertErrorOutput("No task ID was specified. Please pass --taskId YourTaskId"); } } diff --git a/source/Calamari.Tests/Fixtures/PackageDownload/DownloadAndRegisterPackageFixture.cs b/source/Calamari.Tests/Fixtures/PackageDownload/DownloadAndRegisterPackageFixture.cs index cc109ee711..5d9e00a628 100644 --- a/source/Calamari.Tests/Fixtures/PackageDownload/DownloadAndRegisterPackageFixture.cs +++ b/source/Calamari.Tests/Fixtures/PackageDownload/DownloadAndRegisterPackageFixture.cs @@ -207,6 +207,7 @@ public void ShouldFailWhenTaskIdNotProvided() var result = Invoke(calamari); result.AssertFailure(); + result.AssertErrorOutput("No task ID was specified. Please pass --taskId YourTaskId"); } [Test] diff --git a/source/Calamari/Commands/DownloadAndRegisterPackageCommand.cs b/source/Calamari/Commands/DownloadAndRegisterPackageCommand.cs index eac623549e..6ebe932204 100644 --- a/source/Calamari/Commands/DownloadAndRegisterPackageCommand.cs +++ b/source/Calamari/Commands/DownloadAndRegisterPackageCommand.cs @@ -19,9 +19,7 @@ public class DownloadAndRegisterPackageCommand : Command readonly ILog log; readonly IManagePackageCache journal; readonly PackageDownloadService downloadService; - readonly PackageDownloadOptions options; - - ServerTaskId taskId; + readonly PackageDownloadAndRegisterOptions options; public DownloadAndRegisterPackageCommand( IScriptEngine scriptEngine, @@ -34,10 +32,8 @@ public DownloadAndRegisterPackageCommand( this.log = log; this.journal = journal; this.downloadService = new PackageDownloadService(scriptEngine, variables, fileSystem, commandLineRunner, log); - this.options = new PackageDownloadOptions(); - - PackageDownloadOptions.ConfigureOptions(Options, options); - Options.Add("taskId=", "Id of the task that is using the package", v => taskId = new ServerTaskId(v)); + this.options = new PackageDownloadAndRegisterOptions(); + PackageDownloadAndRegisterOptions.ConfigureOptions(Options, options); } public override int Execute(string[] commandLineArguments) @@ -46,14 +42,9 @@ public override int Execute(string[] commandLineArguments) try { - var pkg = downloadService.DownloadPackage(options); + var pkg = downloadService.DownloadPackageForRegistration(options); var version = VersionFactory.TryCreateVersion(options.PackageVersion, options.VersionFormat); - - if (taskId == null) - throw new CommandException("No task ID was specified. Please pass --taskId YourTaskId"); - RegisterPackageUse(pkg, version); - return 0; } catch (Exception ex) @@ -71,7 +62,7 @@ void RegisterPackageUse(PackagePhysicalFileMetadata pkg, IVersion version) version, new PackagePath(pkg.FullFilePath)); var size = (ulong)pkg.Size; - journal.RegisterPackageUse(package, taskId, size); + journal.RegisterPackageUse(package, new ServerTaskId(options.TaskId), size); } } diff --git a/source/Calamari/Commands/FindAndRegisterPackageCommand.cs b/source/Calamari/Commands/FindAndRegisterPackageCommand.cs index f492cdf322..9ae23e052d 100644 --- a/source/Calamari/Commands/FindAndRegisterPackageCommand.cs +++ b/source/Calamari/Commands/FindAndRegisterPackageCommand.cs @@ -18,9 +18,7 @@ public class FindAndRegisterPackageCommand : Command readonly IManagePackageCache journal; readonly ICalamariFileSystem fileSystem; readonly PackageFindService findService; - readonly PackageFindOptions options; - - ServerTaskId taskId; + readonly PackageFindRegistrationOptions options; public FindAndRegisterPackageCommand( ILog log, @@ -32,10 +30,9 @@ public FindAndRegisterPackageCommand( this.journal = journal; this.fileSystem = fileSystem; this.findService = new PackageFindService(log, packageStore); - this.options = new PackageFindOptions(); + this.options = new PackageFindRegistrationOptions(); - PackageFindOptions.ConfigureOptions(Options, options); - Options.Add("taskId=", "Id of the task that is using the package", v => taskId = new ServerTaskId(v)); + PackageFindRegistrationOptions.ConfigureOptions(Options, options); } public override int Execute(string[] commandLineArguments) @@ -44,17 +41,11 @@ public override int Execute(string[] commandLineArguments) try { - // Find and validate package options first (packageId, packageVersion, packageHash) - var package = findService.FindPackage(options); + var package = findService.FindPackageForRegistration(options); if (package != null) { var version = VersionFactory.TryCreateVersion(options.PackageVersion, options.VersionFormat); - - // Then validate taskId before registration - if (taskId == null) - throw new CommandException("No task ID was specified. Please pass --taskId YourTaskId"); - RegisterPackageUse(package, version); } @@ -75,7 +66,7 @@ void RegisterPackageUse(PackagePhysicalFileMetadata pkg, IVersion version) version, new PackagePath(pkg.FullFilePath)); var size = fileSystem.GetFileSize(package.Path.Value); - journal.RegisterPackageUse(package, taskId, (ulong)size); + journal.RegisterPackageUse(package, new ServerTaskId(options.TaskId), (ulong)size); } } } diff --git a/source/Calamari/Commands/Support/IPackageDownloadOptions.cs b/source/Calamari/Commands/Support/IPackageDownloadOptions.cs new file mode 100644 index 0000000000..cb8af0d681 --- /dev/null +++ b/source/Calamari/Commands/Support/IPackageDownloadOptions.cs @@ -0,0 +1,20 @@ +using System; +using Calamari.Common.Features.Packages; +using Octopus.Versioning; + +namespace Calamari.Commands.Support; + +public interface IPackageDownloadOptions +{ + string PackageId { get; set; } + string PackageVersion { get; set; } + bool ForcePackageDownload { get; set; } + string FeedId { get; set; } + string FeedUri { get; set; } + string FeedUsername { get; set; } + string FeedPassword { get; set; } + string MaxDownloadAttempts { get; set; } + string AttemptBackoffSeconds { get; set; } + FeedType FeedType { get; set; } + VersionFormat VersionFormat { get; set; } +} \ No newline at end of file diff --git a/source/Calamari/Commands/Support/IPackageFindOptions.cs b/source/Calamari/Commands/Support/IPackageFindOptions.cs new file mode 100644 index 0000000000..56a82f4157 --- /dev/null +++ b/source/Calamari/Commands/Support/IPackageFindOptions.cs @@ -0,0 +1,12 @@ +using Octopus.Versioning; + +namespace Calamari.Commands.Support; + +public interface IPackageFindOptions +{ + public string PackageId { get; set; } + public string PackageVersion { get; set; } + public string PackageHash { get; set; } + public bool ExactMatchOnly { get; set; } + public VersionFormat VersionFormat { get; set; } +} \ No newline at end of file diff --git a/source/Calamari/Commands/Support/PackageDownloadAndRegisterOptions.cs b/source/Calamari/Commands/Support/PackageDownloadAndRegisterOptions.cs new file mode 100644 index 0000000000..26fbfa1003 --- /dev/null +++ b/source/Calamari/Commands/Support/PackageDownloadAndRegisterOptions.cs @@ -0,0 +1,29 @@ +using System; +using Calamari.Common.Features.Packages; +using Calamari.Common.Plumbing.Commands.Options; +using Octopus.Versioning; + +namespace Calamari.Commands.Support +{ + public class PackageDownloadAndRegisterOptions : IPackageDownloadOptions + { + public string PackageId { get; set; } + public string PackageVersion { get; set; } + public bool ForcePackageDownload { get; set; } + public string FeedId { get; set; } + public string FeedUri { get; set; } + public string FeedUsername { get; set; } + public string FeedPassword { get; set; } + public string MaxDownloadAttempts { get; set; } = "5"; + public string AttemptBackoffSeconds { get; set; } = "10"; + public FeedType FeedType { get; set; } = FeedType.NuGet; + public VersionFormat VersionFormat { get; set; } = VersionFormat.Semver; + public string TaskId { get; set; } + + public static void ConfigureOptions(OptionSet options, PackageDownloadAndRegisterOptions downloadOptions) + { + PackageDownloadOptions.ConfigureOptions(options, downloadOptions); + options.Add("taskId=", "No task ID was specified.", v => downloadOptions.TaskId = v); + } + } +} diff --git a/source/Calamari/Commands/Support/PackageDownloadOptions.cs b/source/Calamari/Commands/Support/PackageDownloadOptions.cs index d2a5afcd73..98e6796eaa 100644 --- a/source/Calamari/Commands/Support/PackageDownloadOptions.cs +++ b/source/Calamari/Commands/Support/PackageDownloadOptions.cs @@ -6,7 +6,7 @@ namespace Calamari.Commands.Support { - public class PackageDownloadOptions + public class PackageDownloadOptions : IPackageDownloadOptions { public string PackageId { get; set; } public string PackageVersion { get; set; } @@ -20,7 +20,7 @@ public class PackageDownloadOptions public FeedType FeedType { get; set; } = FeedType.NuGet; public VersionFormat VersionFormat { get; set; } = VersionFormat.Semver; - public static void ConfigureOptions(OptionSet options, PackageDownloadOptions downloadOptions) + public static void ConfigureOptions(OptionSet options, IPackageDownloadOptions downloadOptions) { options.Add("packageId=", "Package ID to download", v => downloadOptions.PackageId = v); options.Add("packageVersion=", "Package version to download", v => downloadOptions.PackageVersion = v); diff --git a/source/Calamari/Commands/Support/PackageDownloadService.cs b/source/Calamari/Commands/Support/PackageDownloadService.cs index 05885af065..7de2a41bf1 100644 --- a/source/Calamari/Commands/Support/PackageDownloadService.cs +++ b/source/Calamari/Commands/Support/PackageDownloadService.cs @@ -1,12 +1,15 @@ using System; using System.Globalization; +using Calamari.Common.Commands; using Calamari.Common.Features.Packages; using Calamari.Common.Features.Processes; using Calamari.Common.Features.Scripting; +using Calamari.Common.Plumbing; using Calamari.Common.Plumbing.FileSystem; using Calamari.Common.Plumbing.Logging; using Calamari.Common.Plumbing.Variables; using Calamari.Integration.Packages.Download; +using Octopus.Versioning; namespace Calamari.Commands.Support { @@ -31,54 +34,94 @@ public PackageDownloadService( this.commandLineRunner = commandLineRunner; this.log = log; } + + public PackagePhysicalFileMetadata DownloadPackageForRegistration(PackageDownloadAndRegisterOptions options) + { + var validatedPackageDownloadOptions = ValidateCommonOptions(options); + Guard.NotNullOrWhiteSpace(options.TaskId, "No task ID was specified. Please pass --taskId YourTaskId"); + return DownloadPackageUsingStrategy(options, validatedPackageDownloadOptions); + } public PackagePhysicalFileMetadata DownloadPackage(PackageDownloadOptions options) { - variables.Set(AuthenticationVariables.FeedType, options.FeedType.ToString()); - - PackageDownloadArgumentValidator.CheckArguments( - options.PackageId, - options.PackageVersion, - options.FeedId, - options.FeedUri, - options.FeedUsername, - options.FeedPassword, - options.MaxDownloadAttempts, - options.AttemptBackoffSeconds, - options.FeedType, - options.VersionFormat, - variables, - out var version, - out var uri, - out var parsedMaxDownloadAttempts, - out var parsedAttemptBackoff); + var validatedPackageDownloadOptions = ValidateCommonOptions(options); + return DownloadPackageUsingStrategy(options,validatedPackageDownloadOptions); + } + PackagePhysicalFileMetadata DownloadPackageUsingStrategy(IPackageDownloadOptions options, ValidatedPackageDownloadOptions validatedPackageDownloadOptions) + { var packageDownloaderStrategy = new PackageDownloaderStrategy( - log, - scriptEngine, - fileSystem, - commandLineRunner, - variables); + log, + scriptEngine, + fileSystem, + commandLineRunner, + variables); var pkg = packageDownloaderStrategy.DownloadPackage( - options.PackageId, - version, - options.FeedId, - uri, - options.FeedType, - options.FeedUsername, - options.FeedPassword, - options.ForcePackageDownload, - parsedMaxDownloadAttempts, - parsedAttemptBackoff); + options.PackageId, + validatedPackageDownloadOptions.Version, + options.FeedId, + validatedPackageDownloadOptions.Uri, + options.FeedType, + options.FeedUsername, + options.FeedPassword, + options.ForcePackageDownload, + validatedPackageDownloadOptions.ParsedMaxDownloadAttempts, + validatedPackageDownloadOptions.ParsedAttemptBackoff); log.VerboseFormat("Package {0} v{1} successfully downloaded from feed: '{2}'", - options.PackageId, version, options.FeedUri); + options.PackageId, validatedPackageDownloadOptions.Version, options.FeedUri); SetOutputVariables(pkg); - + return pkg; } + + public ValidatedPackageDownloadOptions ValidateCommonOptions(IPackageDownloadOptions options) + { + variables.Set(AuthenticationVariables.FeedType, options.FeedType.ToString()); + + Guard.NotNullOrWhiteSpace(options.PackageId, "No package ID was specified. Please pass --packageId YourPackage"); + Guard.NotNullOrWhiteSpace(options.PackageVersion, "No package version was specified. Please pass --packageVersion 1.0.0.0"); + Guard.NotNullOrWhiteSpace(options.FeedId, "No feed ID was specified. Please pass --feedId feed-id"); + + var usingOidc = !string.IsNullOrWhiteSpace(variables.Get("Jwt")); + if (options.FeedType != FeedType.S3 && options.FeedType != FeedType.AwsElasticContainerRegistry) + { + Guard.NotNullOrWhiteSpace(options.FeedUri, "No feed URI was specified. Please pass --feedUri https://url/to/nuget/feed"); + } + + var version = VersionFactory.TryCreateVersion(options.PackageVersion, options.VersionFormat); + if (version == null) + { + throw new CommandException($"Package version '{options.PackageVersion}' specified is not a valid {options.VersionFormat.ToString()} version string"); + } + + Uri? uri; + if (options.FeedType == FeedType.S3 || options.FeedType == FeedType.AwsElasticContainerRegistry) + { + uri = null; + } + else if (!Uri.TryCreate(options.FeedUri, UriKind.Absolute, out uri)) + throw new CommandException($"URI specified '{options.FeedUri}' is not a valid URI"); + + if (!String.IsNullOrWhiteSpace(options.FeedUsername) && String.IsNullOrWhiteSpace(options.FeedPassword) && !usingOidc) + throw new CommandException("A username was specified but no password was provided. Please pass --feedPassword \"FeedPassword\""); + + if (!int.TryParse(options.MaxDownloadAttempts, out var parsedMaxDownloadAttempts)) + throw new CommandException($"The requested number of download attempts '{options.MaxDownloadAttempts}' is not a valid integer number"); + + if (parsedMaxDownloadAttempts <= 0) + throw new CommandException("The requested number of download attempts should be more than zero"); + + if (!int.TryParse(options.AttemptBackoffSeconds, out var parsedAttemptBackoffSeconds)) + throw new CommandException($"Retry requested download attempt retry backoff '{options.AttemptBackoffSeconds}' is not a valid integer number of seconds"); + + if (parsedAttemptBackoffSeconds < 0) + throw new CommandException("The requested download attempt retry backoff should be a positive integer number of seconds"); + + return new ValidatedPackageDownloadOptions(version, uri, parsedMaxDownloadAttempts, TimeSpan.FromSeconds(parsedAttemptBackoffSeconds)); + } void SetOutputVariables(PackagePhysicalFileMetadata pkg) { @@ -89,4 +132,20 @@ void SetOutputVariables(PackagePhysicalFileMetadata pkg) pkg.FullFilePath); } } + + public class ValidatedPackageDownloadOptions + { + public ValidatedPackageDownloadOptions(IVersion version, Uri uri, int parsedMaxDownloadAttempts, TimeSpan parsedAttemptBackoff) + { + Version = version; + Uri = uri; + ParsedMaxDownloadAttempts = parsedMaxDownloadAttempts; + ParsedAttemptBackoff = parsedAttemptBackoff; + } + + public IVersion Version { get; } + public Uri Uri { get; } + public int ParsedMaxDownloadAttempts { get; } + public TimeSpan ParsedAttemptBackoff { get; } + } } diff --git a/source/Calamari/Commands/Support/PackageFindOptions.cs b/source/Calamari/Commands/Support/PackageFindOptions.cs index 0180793cf5..fb250c3969 100644 --- a/source/Calamari/Commands/Support/PackageFindOptions.cs +++ b/source/Calamari/Commands/Support/PackageFindOptions.cs @@ -5,7 +5,7 @@ namespace Calamari.Commands.Support { - public class PackageFindOptions + public class PackageFindOptions : IPackageFindOptions { public string PackageId { get; set; } public string PackageVersion { get; set; } @@ -13,7 +13,7 @@ public class PackageFindOptions public bool ExactMatchOnly { get; set; } public VersionFormat VersionFormat { get; set; } = VersionFormat.Semver; - public static void ConfigureOptions(OptionSet options, PackageFindOptions findOptions) + public static void ConfigureOptions(OptionSet options, IPackageFindOptions findOptions) { options.Add("packageId=", "Package ID to find", v => findOptions.PackageId = v); options.Add("packageVersion=", "Package version to find", v => findOptions.PackageVersion = v); diff --git a/source/Calamari/Commands/Support/PackageFindRegistrationOptions.cs b/source/Calamari/Commands/Support/PackageFindRegistrationOptions.cs new file mode 100644 index 0000000000..ac5cb8d549 --- /dev/null +++ b/source/Calamari/Commands/Support/PackageFindRegistrationOptions.cs @@ -0,0 +1,23 @@ +using System; +using Calamari.Common.Commands; +using Calamari.Common.Plumbing.Commands.Options; +using Octopus.Versioning; + +namespace Calamari.Commands.Support +{ + public class PackageFindRegistrationOptions : IPackageFindOptions + { + public string PackageId { get; set; } + public string PackageVersion { get; set; } + public string PackageHash { get; set; } + public bool ExactMatchOnly { get; set; } + public VersionFormat VersionFormat { get; set; } = VersionFormat.Semver; + public string TaskId { get; private set; } + + public static void ConfigureOptions(OptionSet options, PackageFindRegistrationOptions findOptions) + { + PackageFindOptions.ConfigureOptions(options, findOptions); + options.Add("taskId=", "No task ID was specified.", v => findOptions.TaskId = v); + } + } +} diff --git a/source/Calamari/Commands/Support/PackageFindService.cs b/source/Calamari/Commands/Support/PackageFindService.cs index 7b695cb229..4359d5bfdc 100644 --- a/source/Calamari/Commands/Support/PackageFindService.cs +++ b/source/Calamari/Commands/Support/PackageFindService.cs @@ -19,13 +19,29 @@ public PackageFindService(ILog log, IPackageStore packageStore) this.log = log; this.packageStore = packageStore; } + + public PackagePhysicalFileMetadata FindPackageForRegistration(PackageFindRegistrationOptions options) + { + ValidateCommonOptions(options); + Guard.NotNullOrWhiteSpace(options.TaskId, "No task ID was specified. Please pass --taskId YourTaskId"); + return GetPackage(options); + } public PackagePhysicalFileMetadata FindPackage(PackageFindOptions options) + { + ValidateCommonOptions(options); + return GetPackage(options); + } + + void ValidateCommonOptions(IPackageFindOptions options) { Guard.NotNullOrWhiteSpace(options.PackageId, "No package ID was specified. Please pass --packageId YourPackage"); Guard.NotNullOrWhiteSpace(options.PackageVersion, "No package version was specified. Please pass --packageVersion 1.0.0.0"); Guard.NotNullOrWhiteSpace(options.PackageHash, "No package hash was specified. Please pass --packageHash YourPackageHash"); + } + PackagePhysicalFileMetadata GetPackage(IPackageFindOptions options) + { var version = VersionFactory.TryCreateVersion(options.PackageVersion, options.VersionFormat); if (version == null) throw new CommandException($"Package version '{options.PackageVersion}' is not a valid {options.VersionFormat} version string. Please pass --packageVersionFormat with a different version type."); @@ -45,17 +61,17 @@ public PackagePhysicalFileMetadata FindPackage(PackageFindOptions options) log.VerboseFormat("Package {0} {1} hash {2} has already been uploaded", package.PackageId, package.Version, package.Hash); LogPackageFound( - package.PackageId, - package.FileVersion, - package.Hash, - package.Extension, - package.FullFilePath, - true, - options.VersionFormat - ); + package.PackageId, + package.FileVersion, + package.Hash, + package.Extension, + package.FullFilePath, + true, + options.VersionFormat + ); return package; } - + void FindEarlierPackages(string packageId, IVersion version, VersionFormat versionFormat) { log.VerboseFormat("Finding earlier packages that have been uploaded to this Tentacle."); From 3ed961fc62fb95b1931268689f4485a4ebbc0a25 Mon Sep 17 00:00:00 2001 From: David Staniec Date: Thu, 21 May 2026 11:26:41 +0800 Subject: [PATCH 4/5] PR feedback --- .../DownloadAndRegisterPackageFixture.cs | 70 ++++++++++++++----- 1 file changed, 53 insertions(+), 17 deletions(-) diff --git a/source/Calamari.Tests/Fixtures/PackageDownload/DownloadAndRegisterPackageFixture.cs b/source/Calamari.Tests/Fixtures/PackageDownload/DownloadAndRegisterPackageFixture.cs index 5d9e00a628..3de1cca3f0 100644 --- a/source/Calamari.Tests/Fixtures/PackageDownload/DownloadAndRegisterPackageFixture.cs +++ b/source/Calamari.Tests/Fixtures/PackageDownload/DownloadAndRegisterPackageFixture.cs @@ -1,11 +1,13 @@ using System; using System.Globalization; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Calamari.Common.Features.Packages; using Calamari.Common.Plumbing.FileSystem; using Calamari.Testing; +using Newtonsoft.Json.Linq; using Calamari.Testing.Helpers; using Calamari.Testing.Requirements; using Calamari.Tests.Fixtures.Deployment.Packages; @@ -22,24 +24,24 @@ public class DownloadAndRegisterPackageFixture : CalamariFixture static readonly string TentacleHome = TestEnvironment.GetTestPath("Fixtures", "DownloadAndRegisterPackage"); static readonly string DownloadPath = TestEnvironment.GetTestPath(TentacleHome, "Files"); - static readonly string PublicFeedUri = "https://f.feedz.io/octopus-deploy/integration-tests/nuget/index.json"; - static readonly string NuGetFeedUri = "https://www.nuget.org/api/v2/"; + const string PublicFeedUri = "https://f.feedz.io/octopus-deploy/integration-tests/nuget/index.json"; + const string NuGetFeedUri = "https://www.nuget.org/api/v2/"; - static readonly string ExpectedPackageHash = "1e0856338eb5ada3b30903b980cef9892ebf7201"; - static readonly long ExpectedPackageSize = 3749; - static readonly SampleFeedPackage FeedzPackage = new SampleFeedPackage() + const string ExpectedPackageHash = "1e0856338eb5ada3b30903b980cef9892ebf7201"; + const long ExpectedPackageSize = 3749; + static readonly SampleFeedPackage FeedzPackage = new() { Id = "feeds-feedz", Version = new SemanticVersion("1.0.0"), PackageId = "OctoConsole" }; - static readonly SampleFeedPackage FileShare = new SampleFeedPackage() { Id = "feeds-local", Version = new SemanticVersion(1, 0, 0), PackageId = "Acme.Web" }; - static readonly SampleFeedPackage NuGetFeed = new SampleFeedPackage() { Id = "feeds-nuget", Version = new SemanticVersion(2, 1, 0), PackageId = "Abp.Castle.Log4Net" }; + static readonly SampleFeedPackage FileShare = new() { Id = "feeds-local", Version = new SemanticVersion(1, 0, 0), PackageId = "Acme.Web" }; + static readonly SampleFeedPackage NuGetFeed = new() { Id = "feeds-nuget", Version = new SemanticVersion(2, 1, 0), PackageId = "Abp.Castle.Log4Net" }; - static readonly string MavenPublicFeedUri = "https://repo.maven.apache.org/maven2/"; - static readonly string ExpectedMavenPackageHash = "3564ef3803de51fb0530a8377ec6100b33b0d073"; - static readonly long ExpectedMavenPackageSize = 2575022; - static readonly SampleFeedPackage MavenPublicFeed = new SampleFeedPackage("#") + const string MavenPublicFeedUri = "https://repo.maven.apache.org/maven2/"; + const string ExpectedMavenPackageHash = "3564ef3803de51fb0530a8377ec6100b33b0d073"; + const long ExpectedMavenPackageSize = 2575022; + static readonly SampleFeedPackage MavenPublicFeed = new("#") { Id = "feeds-maven", Version = VersionFactory.CreateMavenVersion("22.0"), @@ -50,7 +52,7 @@ public class DownloadAndRegisterPackageFixture : CalamariFixture static string AuthFeedUsername; static string AuthFeedPassword; - static readonly CancellationTokenSource CancellationTokenSource = new CancellationTokenSource(); + static readonly CancellationTokenSource CancellationTokenSource = new(); readonly CancellationToken cancellationToken = CancellationTokenSource.Token; [OneTimeSetUp] @@ -64,13 +66,12 @@ public async Task OneTimeSetup() [SetUp] public void SetUp() { - if (!Directory.Exists(TentacleHome)) - Directory.CreateDirectory(TentacleHome); + if (Directory.Exists(TentacleHome)) + Directory.Delete(TentacleHome, true); + Directory.CreateDirectory(TentacleHome); Directory.SetCurrentDirectory(TentacleHome); - Environment.SetEnvironmentVariable("TentacleHome", TentacleHome); - Console.WriteLine("TentacleHome is set to: " + TentacleHome); } [TearDown] @@ -103,6 +104,7 @@ public void ShouldDownloadAndRegisterPackage() // Verify package was registered with the journal result.AssertOutput("Registered package use/lock for {0} v{1} and task ServerTasks-12345", FeedzPackage.PackageId, FeedzPackage.Version); + AssertJournalContainsEntryFor(FeedzPackage.PackageId, FeedzPackage.Version); } [Test] @@ -131,6 +133,7 @@ public void ShouldDownloadAndRegisterMavenPackage() // Verify package was registered with the journal result.AssertOutput("Registered package use/lock for {0} v{1} and task ServerTasks-12345", MavenPublicFeed.PackageId, MavenPublicFeed.Version); + AssertJournalContainsEntryFor(MavenPublicFeed.PackageId, MavenPublicFeed.Version); } [Test] @@ -153,6 +156,7 @@ public void ShouldSetOutputVariables() // Verify package was registered with the journal result.AssertOutput("Registered package use/lock for {0} v{1} and task ServerTasks-12345", FeedzPackage.PackageId, FeedzPackage.Version); + AssertJournalContainsEntryFor(FeedzPackage.PackageId, FeedzPackage.Version); } [Test] @@ -167,6 +171,7 @@ public void ShouldUsePackageFromCacheAndStillRegister() firstResult.AssertSuccess(); firstResult.AssertOutput("Registered package use/lock for {0} v{1} and task ServerTasks-12345", FeedzPackage.PackageId, FeedzPackage.Version); + AssertJournalContainsEntryFor(FeedzPackage.PackageId, FeedzPackage.Version); // Second download should use cache but still register var secondResult = DownloadAndRegisterPackage( @@ -189,6 +194,7 @@ public void ShouldUsePackageFromCacheAndStillRegister() // Verify package was registered with the journal (even when using cache) secondResult.AssertOutput("Registered package use/lock for {0} v{1} and task ServerTasks-12345", FeedzPackage.PackageId, FeedzPackage.Version); + AssertJournalContainsEntryFor(FeedzPackage.PackageId, FeedzPackage.Version); } [Test] @@ -226,6 +232,7 @@ public void ShouldDownloadAndRegisterPackageWithRepositoryMetadata() // Verify package was registered with the journal result.AssertOutput("Registered package use/lock for {0} v{1} and task ServerTasks-12345", NuGetFeed.PackageId, NuGetFeed.Version); + AssertJournalContainsEntryFor(NuGetFeed.PackageId, NuGetFeed.Version); } [Test] @@ -257,6 +264,7 @@ public void ShouldUseMavenPackageFromCacheAndStillRegister() // Verify package was registered with the journal (even when using cache) result.AssertOutput("Registered package use/lock for {0} v{1} and task ServerTasks-12345", MavenPublicFeed.PackageId, MavenPublicFeed.Version); + AssertJournalContainsEntryFor(MavenPublicFeed.PackageId, MavenPublicFeed.Version); } [Test] @@ -287,6 +295,7 @@ public void ShouldUseMavenSnapshotPackageFromCacheAndStillRegister() // Verify package was registered with the journal (even when using cache) result.AssertOutput("Registered package use/lock for {0} v{1} and task ServerTasks-12345", MavenPublicFeed.PackageId, MavenPublicFeed.Version); + AssertJournalContainsEntryFor(MavenPublicFeed.PackageId, MavenPublicFeed.Version); } [Test] @@ -312,6 +321,7 @@ public void ShouldByPassCacheAndDownloadAndRegisterPackage() // Verify package was registered with the journal result.AssertOutput("Registered package use/lock for {0} v{1} and task ServerTasks-12345", FeedzPackage.PackageId, FeedzPackage.Version); + AssertJournalContainsEntryFor(FeedzPackage.PackageId, FeedzPackage.Version); } [Test] @@ -349,6 +359,7 @@ public void ShouldByPassCacheAndDownloadAndRegisterMavenPackage() // Verify package was registered with the journal secondDownload.AssertOutput("Registered package use/lock for {0} v{1} and task ServerTasks-12345", MavenPublicFeed.PackageId, MavenPublicFeed.Version); + AssertJournalContainsEntryFor(MavenPublicFeed.PackageId, MavenPublicFeed.Version); } [Test] @@ -368,6 +379,7 @@ public void PrivateNuGetFeedShouldDownloadAndRegisterPackage() // Verify package was registered with the journal result.AssertOutput("Registered package use/lock for {0} v{1} and task ServerTasks-12345", FeedzPackage.PackageId, FeedzPackage.Version); + AssertJournalContainsEntryFor(FeedzPackage.PackageId, FeedzPackage.Version); } [Test] @@ -389,6 +401,7 @@ public void PrivateNuGetFeedShouldUsePackageFromCacheAndStillRegister() // Verify package was registered with the journal (even when using cache) result.AssertOutput("Registered package use/lock for {0} v{1} and task ServerTasks-12345", FeedzPackage.PackageId, FeedzPackage.Version); + AssertJournalContainsEntryFor(FeedzPackage.PackageId, FeedzPackage.Version); } [Test] @@ -411,6 +424,7 @@ public void PrivateNuGetFeedShouldByPassCacheAndDownloadAndRegisterPackage() // Verify package was registered with the journal result.AssertOutput("Registered package use/lock for {0} v{1} and task ServerTasks-12345", FeedzPackage.PackageId, FeedzPackage.Version); + AssertJournalContainsEntryFor(FeedzPackage.PackageId, FeedzPackage.Version); } [Test] @@ -445,6 +459,7 @@ public void FileShareFeedShouldDownloadAndRegisterPackage() // Verify package was registered with the journal result.AssertOutput("Registered package use/lock for {0} v{1} and task ServerTasks-12345", FileShare.PackageId, FileShare.Version); + AssertJournalContainsEntryFor(FileShare.PackageId, FileShare.Version); } } @@ -466,6 +481,7 @@ public void FileShareFeedShouldUsePackageFromCacheAndStillRegister() // Verify package was registered with the journal (even when using cache) result.AssertOutput("Registered package use/lock for {0} v{1} and task ServerTasks-12345", FileShare.PackageId, FileShare.Version); + AssertJournalContainsEntryFor(FileShare.PackageId, FileShare.Version); } } @@ -492,6 +508,7 @@ public void FileShareFeedShouldByPassCacheAndDownloadAndRegisterPackage() // Verify package was registered with the journal result.AssertOutput("Registered package use/lock for {0} v{1} and task ServerTasks-12345", FileShare.PackageId, FileShare.Version); + AssertJournalContainsEntryFor(FileShare.PackageId, FileShare.Version); } } @@ -640,7 +657,26 @@ static void AssertStagePackageOutputVariableSet(CalamariResult result, SampleFee result.AssertOutputVariable("StagedPackage.FullPathOnRemoteMachine", Does.Match(newPackageRegex + ".*")); } - private TemporaryFile CreateSamplePackage() + static void AssertJournalContainsEntryFor(string packageId, IVersion version) + { + var journalPath = Path.Combine(TentacleHome, "PackageRetentionJournal.json"); + + Assert.That(File.Exists(journalPath), Is.True, + $"Journal file not found at: {journalPath}"); + + var json = File.ReadAllText(journalPath); + var root = JObject.Parse(json); + var entries = root["JournalEntries"] as JArray ?? new JArray(); + + var match = entries.FirstOrDefault(e => + string.Equals((string)e["Package"]?["PackageId"]?["Value"], packageId, StringComparison.OrdinalIgnoreCase) && + string.Equals((string)e["Package"]?["Version"]?["Version"], version.ToString(), StringComparison.OrdinalIgnoreCase)); + + Assert.That(match, Is.Not.Null, + $"Expected a journal entry for {packageId} v{version} but the journal contains: {json}"); + } + + TemporaryFile CreateSamplePackage() { return new TemporaryFile(PackageBuilder.BuildSamplePackage(FileShare.PackageId, FileShare.Version.ToString())); } From 1085c2997d40680fcfb1af328aeac0831f61d3e6 Mon Sep 17 00:00:00 2001 From: David Staniec Date: Mon, 25 May 2026 11:46:06 +0800 Subject: [PATCH 5/5] Handling case where package file path is empty --- .../FindAndRegisterPackageFixture.cs | 59 +++++++++++++++++++ .../DownloadAndRegisterPackageFixture.cs | 17 ++++++ .../DownloadAndRegisterPackageCommand.cs | 3 + .../Commands/FindAndRegisterPackageCommand.cs | 3 + 4 files changed, 82 insertions(+) diff --git a/source/Calamari.Tests/Fixtures/FindPackage/FindAndRegisterPackageFixture.cs b/source/Calamari.Tests/Fixtures/FindPackage/FindAndRegisterPackageFixture.cs index a2de05f8bf..25d93eff8b 100644 --- a/source/Calamari.Tests/Fixtures/FindPackage/FindAndRegisterPackageFixture.cs +++ b/source/Calamari.Tests/Fixtures/FindPackage/FindAndRegisterPackageFixture.cs @@ -1,12 +1,19 @@ using System; using System.IO; +using System.Linq; using System.Security.Cryptography; +using Calamari.Commands; using Calamari.Common.Features.Packages; +using Calamari.Common.Plumbing.Deployment.PackageRetention; using Calamari.Common.Plumbing.FileSystem; +using Calamari.Common.Plumbing.Logging; using Calamari.Common.Plumbing.ServiceMessages; +using Calamari.Integration.FileSystem; using Calamari.Testing.Helpers; using Calamari.Tests.Fixtures.Deployment.Packages; using Calamari.Tests.Helpers; +using Newtonsoft.Json.Linq; +using NSubstitute; using NUnit.Framework; using Octopus.Versioning; @@ -54,6 +61,10 @@ public void TearDown() { if (Directory.Exists(downloadPath)) Directory.Delete(downloadPath, true); + + var journalPath = Path.Combine(tentacleHome, "PackageRetentionJournal.json"); + if (File.Exists(journalPath)) + File.Delete(journalPath); } CalamariResult FindAndRegisterPackage(string id, string version, string hash, VersionFormat versionFormat = VersionFormat.Semver) @@ -96,6 +107,7 @@ public void ShouldFindAndRegisterPackageAlreadyUploaded() // Verify package was registered with the journal result.AssertOutput("Registered package use/lock for {0} v{1} and task ServerTasks-12345", packageId, packageVersion); + AssertJournalContainsEntryFor(packageId, VersionFactory.CreateSemanticVersion(packageVersion)); } } @@ -350,6 +362,7 @@ public void ShouldFindMavenPackageAlreadyUploaded() // Verify package was registered with the journal result.AssertOutput("Registered package use/lock for {0} v{1} and task ServerTasks-12345", mavenPackageId, packageVersion); + AssertJournalContainsEntryFor(mavenPackageId, VersionFactory.CreateMavenVersion(packageVersion)); } [Test] @@ -387,5 +400,51 @@ public void ShouldFailWhenNoPackageHashIsSpecified() result.AssertFailure(); result.AssertErrorOutput("No package hash was specified. Please pass --packageHash YourPackageHash"); } + + static void AssertJournalContainsEntryFor(string packageId, IVersion version) + { + var journalPath = Path.Combine(tentacleHome, "PackageRetentionJournal.json"); + + Assert.That(File.Exists(journalPath), Is.True, + $"Journal file not found at: {journalPath}"); + + var json = File.ReadAllText(journalPath); + var root = JObject.Parse(json); + var entries = root["JournalEntries"] as JArray ?? new JArray(); + + var match = entries.FirstOrDefault(e => + string.Equals((string)e["Package"]?["PackageId"]?["Value"], packageId, StringComparison.OrdinalIgnoreCase) && + string.Equals((string)e["Package"]?["Version"]?["Version"], version.ToString(), StringComparison.OrdinalIgnoreCase)); + + Assert.That(match, Is.Not.Null, + $"Expected a journal entry for {packageId} v{version} but the journal contains: {json}"); + } + + [Test] + public void ShouldNotRegisterWhenFullFilePathIsEmpty() + { + var log = Substitute.For(); + var journal = Substitute.For(); + var fileSystem = Substitute.For(); + var packageStore = Substitute.For(); + + var identity = new PackageFileNameMetadata(packageId, VersionFactory.CreateSemanticVersion(packageVersion), VersionFactory.CreateSemanticVersion(packageVersion), ".nupkg"); + var emptyPathMetadata = new PackagePhysicalFileMetadata(identity, string.Empty, "abc123", 100); + + packageStore.GetPackage(Arg.Any(), Arg.Any(), Arg.Any()).Returns(emptyPathMetadata); + fileSystem.GetFileSize(Arg.Any()).Returns(0L); + + var command = new FindAndRegisterPackageCommand(log, packageStore, journal, fileSystem); + var exitCode = command.Execute(new[] + { + "--packageId", packageId, + "--packageVersion", packageVersion, + "--taskId", "ServerTasks-12345", + "--packageHash", "abc123" + }); + + Assert.AreEqual(0, exitCode); + journal.DidNotReceive().RegisterPackageUse(Arg.Any(), Arg.Any(), Arg.Any()); + } } } diff --git a/source/Calamari.Tests/Fixtures/PackageDownload/DownloadAndRegisterPackageFixture.cs b/source/Calamari.Tests/Fixtures/PackageDownload/DownloadAndRegisterPackageFixture.cs index 3de1cca3f0..de23f7193f 100644 --- a/source/Calamari.Tests/Fixtures/PackageDownload/DownloadAndRegisterPackageFixture.cs +++ b/source/Calamari.Tests/Fixtures/PackageDownload/DownloadAndRegisterPackageFixture.cs @@ -603,6 +603,23 @@ public void ShouldFailWhenUsernameIsSpecifiedButNoPassword() result.AssertErrorOutput("A username was specified but no password was provided"); } + [Test] + [RequiresDockerInstalled] + public void ShouldNotRegisterDockerImageBecauseFullFilePathIsEmpty() + { + // DockerImagePackageDownloader produces PackagePhysicalFileMetadata with string.Empty + // as FullFilePath. The registration guard should skip journal.RegisterPackageUse in this case. + var result = DownloadAndRegisterPackage( + "alpine", + "3.6.5", + "feeds-docker-hub", + "https://index.docker.io", + feedType: FeedType.Docker); + + result.AssertSuccess(); + result.AssertNoOutput("Registered package use/lock for alpine"); + } + CalamariResult DownloadAndRegisterPackage( string packageId, string packageVersion, diff --git a/source/Calamari/Commands/DownloadAndRegisterPackageCommand.cs b/source/Calamari/Commands/DownloadAndRegisterPackageCommand.cs index 6ebe932204..63f95f9b8a 100644 --- a/source/Calamari/Commands/DownloadAndRegisterPackageCommand.cs +++ b/source/Calamari/Commands/DownloadAndRegisterPackageCommand.cs @@ -57,6 +57,9 @@ public override int Execute(string[] commandLineArguments) void RegisterPackageUse(PackagePhysicalFileMetadata pkg, IVersion version) { + if (string.IsNullOrEmpty(pkg.FullFilePath)) + return; + var package = new PackageIdentity( new PackageId(pkg.PackageId), version, diff --git a/source/Calamari/Commands/FindAndRegisterPackageCommand.cs b/source/Calamari/Commands/FindAndRegisterPackageCommand.cs index 9ae23e052d..77fab5216d 100644 --- a/source/Calamari/Commands/FindAndRegisterPackageCommand.cs +++ b/source/Calamari/Commands/FindAndRegisterPackageCommand.cs @@ -61,6 +61,9 @@ public override int Execute(string[] commandLineArguments) void RegisterPackageUse(PackagePhysicalFileMetadata pkg, IVersion version) { + if (string.IsNullOrEmpty(pkg.FullFilePath)) + return; + var package = new PackageIdentity( new PackageId(pkg.PackageId), version,