diff --git a/source/Calamari.Tests/Fixtures/FindPackage/FindAndRegisterPackageFixture.cs b/source/Calamari.Tests/Fixtures/FindPackage/FindAndRegisterPackageFixture.cs new file mode 100644 index 0000000000..25d93eff8b --- /dev/null +++ b/source/Calamari.Tests/Fixtures/FindPackage/FindAndRegisterPackageFixture.cs @@ -0,0 +1,450 @@ +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; + +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 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] + 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); + + 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) + { + return Invoke(Calamari() + .Action("find-and-register-package") + .Argument("packageId", id) + .Argument("packageVersion", version) + .Argument("taskId", "ServerTasks-12345") + .Argument("packageVersionFormat", versionFormat) + .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() + { + 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(); + + // 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)); + } + } + + [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(); + result.AssertErrorOutput("No task ID was specified. Please pass --taskId YourTaskId"); + } + } + + [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(); + } + } + } + + [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); + AssertJournalContainsEntryFor(mavenPackageId, VersionFactory.CreateMavenVersion(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"); + } + + 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 new file mode 100644 index 0000000000..de23f7193f --- /dev/null +++ b/source/Calamari.Tests/Fixtures/PackageDownload/DownloadAndRegisterPackageFixture.cs @@ -0,0 +1,724 @@ +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; +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"); + + const string PublicFeedUri = "https://f.feedz.io/octopus-deploy/integration-tests/nuget/index.json"; + const string NuGetFeedUri = "https://www.nuget.org/api/v2/"; + + 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() { 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" }; + + 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"), + PackageId = "com.google.guava:guava" + }; + + static string AuthFeedUri; + static string AuthFeedUsername; + static string AuthFeedPassword; + + static readonly CancellationTokenSource CancellationTokenSource = new(); + 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() + { + if (Directory.Exists(TentacleHome)) + Directory.Delete(TentacleHome, true); + + Directory.CreateDirectory(TentacleHome); + Directory.SetCurrentDirectory(TentacleHome); + Environment.SetEnvironmentVariable("TentacleHome", TentacleHome); + } + + [TearDown] + public void TearDown() + { + // Change to a safe directory before deleting TentacleHome + Directory.SetCurrentDirectory(Path.GetTempPath()); + + if (Directory.Exists(TentacleHome)) + Directory.Delete(TentacleHome, true); + Environment.SetEnvironmentVariable("TentacleHome", null); + } + + [Test] + public void ShouldDownloadAndRegisterPackage() + { + var result = DownloadAndRegisterPackage( + FeedzPackage.PackageId, + FeedzPackage.Version.ToString(), + FeedzPackage.Id, + PublicFeedUri); + + result.AssertSuccess(); + 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); + AssertJournalContainsEntryFor(FeedzPackage.PackageId, FeedzPackage.Version); + } + + [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 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); + AssertJournalContainsEntryFor(MavenPublicFeed.PackageId, MavenPublicFeed.Version); + } + + [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) + ".*")); + + // 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] + public void ShouldUsePackageFromCacheAndStillRegister() + { + // First download + var firstResult = DownloadAndRegisterPackage( + FeedzPackage.PackageId, + FeedzPackage.Version.ToString(), + FeedzPackage.Id, + PublicFeedUri); + + 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( + 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); + + // 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] + 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(); + result.AssertErrorOutput("No task ID was specified. Please pass --taskId YourTaskId"); + } + + [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); + AssertJournalContainsEntryFor(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); + AssertJournalContainsEntryFor(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); + AssertJournalContainsEntryFor(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); + AssertJournalContainsEntryFor(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); + AssertJournalContainsEntryFor(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); + AssertJournalContainsEntryFor(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); + AssertJournalContainsEntryFor(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); + AssertJournalContainsEntryFor(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); + AssertJournalContainsEntryFor(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); + AssertJournalContainsEntryFor(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); + AssertJournalContainsEntryFor(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"); + } + + [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, + 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 + ".*")); + } + + 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())); + } + + 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..63f95f9b8a --- /dev/null +++ b/source/Calamari/Commands/DownloadAndRegisterPackageCommand.cs @@ -0,0 +1,72 @@ +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 PackageDownloadAndRegisterOptions options; + + 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 PackageDownloadAndRegisterOptions(); + PackageDownloadAndRegisterOptions.ConfigureOptions(Options, options); + } + + public override int Execute(string[] commandLineArguments) + { + Options.Parse(commandLineArguments); + + try + { + var pkg = downloadService.DownloadPackageForRegistration(options); + var version = VersionFactory.TryCreateVersion(options.PackageVersion, options.VersionFormat); + RegisterPackageUse(pkg, version); + return 0; + } + 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); + } + } + + void RegisterPackageUse(PackagePhysicalFileMetadata pkg, IVersion version) + { + if (string.IsNullOrEmpty(pkg.FullFilePath)) + return; + + var package = new PackageIdentity( + new PackageId(pkg.PackageId), + version, + new PackagePath(pkg.FullFilePath)); + var size = (ulong)pkg.Size; + journal.RegisterPackageUse(package, new ServerTaskId(options.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..77fab5216d --- /dev/null +++ b/source/Calamari/Commands/FindAndRegisterPackageCommand.cs @@ -0,0 +1,75 @@ +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 PackageFindRegistrationOptions options; + + 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 PackageFindRegistrationOptions(); + + PackageFindRegistrationOptions.ConfigureOptions(Options, options); + } + + public override int Execute(string[] commandLineArguments) + { + Options.Parse(commandLineArguments); + + try + { + var package = findService.FindPackageForRegistration(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) + { + if (string.IsNullOrEmpty(pkg.FullFilePath)) + return; + + var package = new PackageIdentity( + new PackageId(pkg.PackageId), + version, + new PackagePath(pkg.FullFilePath)); + var size = fileSystem.GetFileSize(package.Path.Value); + journal.RegisterPackageUse(package, new ServerTaskId(options.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/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/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..98e6796eaa --- /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 : 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 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); + 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..7de2a41bf1 --- /dev/null +++ b/source/Calamari/Commands/Support/PackageDownloadService.cs @@ -0,0 +1,151 @@ +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 +{ + 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 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) + { + var validatedPackageDownloadOptions = ValidateCommonOptions(options); + return DownloadPackageUsingStrategy(options,validatedPackageDownloadOptions); + } + + PackagePhysicalFileMetadata DownloadPackageUsingStrategy(IPackageDownloadOptions options, ValidatedPackageDownloadOptions validatedPackageDownloadOptions) + { + var packageDownloaderStrategy = new PackageDownloaderStrategy( + log, + scriptEngine, + fileSystem, + commandLineRunner, + variables); + + var pkg = packageDownloaderStrategy.DownloadPackage( + 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, 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) + { + log.SetOutputVariableButDoNotAddToVariables("StagedPackage.Hash", pkg.Hash); + log.SetOutputVariableButDoNotAddToVariables("StagedPackage.Size", + pkg.Size.ToString(CultureInfo.InvariantCulture)); + log.SetOutputVariableButDoNotAddToVariables("StagedPackage.FullPathOnRemoteMachine", + 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 new file mode 100644 index 0000000000..fb250c3969 --- /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 : 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 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); + 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/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 new file mode 100644 index 0000000000..4359d5bfdc --- /dev/null +++ b/source/Calamari/Commands/Support/PackageFindService.cs @@ -0,0 +1,123 @@ +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 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."); + + 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)); + } + } +}