-
Notifications
You must be signed in to change notification settings - Fork 737
[Feature] Add package source mapping to package download
#6885
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
4bd63e6
9f2deed
fa0cfb6
a0a2be5
b713d79
253adf9
192c72e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,8 @@ | ||
| // Copyright (c) .NET Foundation. All rights reserved. | ||
| // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
|
||
| #nullable enable | ||
|
|
||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Globalization; | ||
|
|
@@ -48,28 +50,45 @@ public static async Task<int> RunAsync(PackageDownloadArgs args, CancellationTok | |
|
|
||
| public static async Task<int> RunAsync(PackageDownloadArgs args, ILoggerWithColor logger, IReadOnlyList<PackageSource> packageSources, ISettings settings, CancellationToken token) | ||
| { | ||
| // Check for insecure sources | ||
| if (DetectAndReportInsecureSources(args.AllowInsecureConnections, packageSources, logger)) | ||
| var packageSourceMapping = PackageSourceMapping.GetPackageSourceMapping(settings); | ||
| var hasSourcesArg = args.Sources != null && args.Sources.Count > 0; | ||
Nigusu-Allehu marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| var mappingDisabled = (packageSourceMapping != null && !packageSourceMapping.IsEnabled) || packageSourceMapping == null; | ||
| if ((mappingDisabled || hasSourcesArg) && DetectAndReportInsecureSources(args.AllowInsecureConnections, packageSources, logger)) | ||
Nigusu-Allehu marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| { | ||
| return ExitCodeError; | ||
| } | ||
|
|
||
| string outputDirectory = args.OutputDirectory ?? Directory.GetCurrentDirectory(); | ||
| var cache = new SourceCacheContext(); | ||
| IReadOnlyList<SourceRepository> sourceRepositories = GetSourceRepositories(packageSources); | ||
|
|
||
| IReadOnlyDictionary<string, SourceRepository> sourceRepositoriesMap = GetSourceRepositories(packageSources); | ||
|
|
||
| bool downloadedAllSuccessfully = true; | ||
|
|
||
| foreach (var package in args.Packages) | ||
| foreach (var package in args.Packages ?? []) | ||
| { | ||
| logger.LogMinimal(string.Format( | ||
| CultureInfo.CurrentCulture, | ||
| Strings.PackageDownloadCommand_Starting, | ||
| package.Id, | ||
| string.IsNullOrEmpty(package.NuGetVersion?.ToNormalizedString()) ? Strings.PackageDownloadCommand_LatestVersion : package.NuGetVersion.ToNormalizedString())); | ||
|
|
||
| // Resolve which repositories to use for this package | ||
| if (!TryGetRepositoriesForPackage( | ||
| package.Id, | ||
| args, | ||
| packageSources, | ||
| packageSourceMapping, | ||
| sourceRepositoriesMap, | ||
| logger, | ||
| out List<SourceRepository> sourceRepositories)) | ||
| { | ||
| return ExitCodeError; | ||
| } | ||
|
|
||
| try | ||
| { | ||
| (NuGetVersion version, SourceRepository downloadRepository) = | ||
| (NuGetVersion? version, SourceRepository? downloadRepository) = | ||
| await ResolvePackageDownloadVersion( | ||
| package, | ||
| sourceRepositories, | ||
|
|
@@ -88,7 +107,7 @@ await ResolvePackageDownloadVersion( | |
| bool success = await DownloadPackageAsync( | ||
| package.Id, | ||
| version, | ||
| downloadRepository, | ||
| downloadRepository!, | ||
| cache, | ||
| settings, | ||
| outputDirectory, | ||
|
|
@@ -127,16 +146,16 @@ await ResolvePackageDownloadVersion( | |
| return downloadedAllSuccessfully ? ExitCodeSuccess : ExitCodeError; | ||
| } | ||
|
|
||
| internal static async Task<(NuGetVersion, SourceRepository)> ResolvePackageDownloadVersion( | ||
| internal static async Task<(NuGetVersion?, SourceRepository?)> ResolvePackageDownloadVersion( | ||
| PackageWithNuGetVersion packageWithNuGetVersion, | ||
| IEnumerable<SourceRepository> sourceRepositories, | ||
| SourceCacheContext cache, | ||
| ILoggerWithColor logger, | ||
| bool includePrerelease, | ||
| CancellationToken token) | ||
| { | ||
| NuGetVersion versionToDownload = null; | ||
| SourceRepository downloadSourceRepository = null; | ||
| NuGetVersion? versionToDownload = null; | ||
| SourceRepository? downloadSourceRepository = null; | ||
| bool versionSpecified = packageWithNuGetVersion.NuGetVersion != null; | ||
|
|
||
| foreach (var repo in sourceRepositories) | ||
|
|
@@ -188,6 +207,67 @@ await ResolvePackageDownloadVersion( | |
| return (versionToDownload, downloadSourceRepository); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Builds the set of SourceRepository objects to use for a given package, | ||
| /// applying package source mapping (when --source is not provided) and | ||
| /// validating HTTP usage only on the *effective* sources. | ||
| /// </summary> | ||
| private static bool TryGetRepositoriesForPackage( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm finding this method really hard to read/maintain. There's too many nested control statements and three different code paths for a return statement. One suggestion would be to factor the foreach statement out into its own helper function. Ideally, there should be a single return statement. |
||
| string packageId, | ||
| PackageDownloadArgs args, | ||
| IReadOnlyList<PackageSource> allPackageSources, | ||
| PackageSourceMapping? packageSourceMapping, | ||
| IReadOnlyDictionary<string, SourceRepository> sourceRepositoriesMap, | ||
| ILoggerWithColor logger, | ||
| out List<SourceRepository> repositories) | ||
| { | ||
| List<PackageSource> effectiveSources; | ||
|
|
||
| var sourceExplicitlyProvided = args.Sources?.Count > 0; | ||
| if (sourceExplicitlyProvided || (packageSourceMapping != null && !packageSourceMapping.IsEnabled)) | ||
| { | ||
| // --source given OR mapping disabled: use all provided sources as-is | ||
| effectiveSources = [.. allPackageSources]; | ||
| } | ||
Nigusu-Allehu marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| else | ||
| { | ||
| // Mapping enabled, no --source: try mapped names first | ||
| var mappedNames = packageSourceMapping == null ? [] : packageSourceMapping.GetConfiguredPackageSources(packageId); | ||
|
|
||
| // Build effective sources in the same order as mappedNames | ||
| var mapped = mappedNames | ||
| .Select(n => allPackageSources.FirstOrDefault(ps => | ||
Nigusu-Allehu marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| string.Equals(ps.Name, n, StringComparison.OrdinalIgnoreCase))) | ||
| .ToList(); | ||
|
|
||
| // Only validate insecure sources when mapping produced something | ||
| if (mapped.Count > 0) | ||
| { | ||
| if (DetectAndReportInsecureSources(args.AllowInsecureConnections, mapped!, logger)) | ||
| { | ||
| repositories = []; | ||
| return false; | ||
| } | ||
|
|
||
| effectiveSources = mapped!; | ||
| } | ||
| else | ||
| { | ||
| // No mapping for this package: fall back to all sources | ||
| effectiveSources = [.. allPackageSources]; | ||
| } | ||
| } | ||
|
|
||
| // Convert effective sources to repositories | ||
| repositories = new List<SourceRepository>(effectiveSources.Count); | ||
| foreach (var src in effectiveSources) | ||
| { | ||
| repositories.Add(sourceRepositoriesMap[src.Name]); | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| private static async Task<bool> DownloadPackageAsync( | ||
| string id, | ||
| NuGetVersion version, | ||
|
|
@@ -239,7 +319,7 @@ private static async Task<bool> DownloadPackageAsync( | |
| return success; | ||
| } | ||
|
|
||
| private static IReadOnlyList<PackageSource> GetPackageSources(IList<string> sources, IPackageSourceProvider sourceProvider) | ||
| private static IReadOnlyList<PackageSource> GetPackageSources(IList<string>? sources, IPackageSourceProvider sourceProvider) | ||
| { | ||
| IEnumerable<PackageSource> configuredSources = sourceProvider.LoadPackageSources() | ||
| .Where(s => s.IsEnabled); | ||
|
|
@@ -271,13 +351,13 @@ private static bool DetectAndReportInsecureSources( | |
| return false; | ||
| } | ||
|
|
||
| private static IReadOnlyList<SourceRepository> GetSourceRepositories(IReadOnlyList<PackageSource> packageSources) | ||
| private static IReadOnlyDictionary<string, SourceRepository> GetSourceRepositories(IReadOnlyList<PackageSource> packageSources) | ||
| { | ||
| IEnumerable<Lazy<INuGetResourceProvider>> providers = Repository.Provider.GetCoreV3(); | ||
| List<SourceRepository> sourceRepositories = []; | ||
| Dictionary<string, SourceRepository> sourceRepositories = []; | ||
Nigusu-Allehu marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| foreach (var source in packageSources) | ||
| { | ||
| sourceRepositories.Add(Repository.CreateSource(providers, source, FeedType.Undefined)); | ||
| sourceRepositories[source.Name] = Repository.CreateSource(providers, source, FeedType.Undefined); | ||
| } | ||
|
|
||
| return sourceRepositories; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ | |
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
| using FluentAssertions; | ||
| using Test.Utility; | ||
| using Moq; | ||
| using NuGet.CommandLine.XPlat; | ||
| using NuGet.CommandLine.XPlat.Commands.Package; | ||
|
|
@@ -426,4 +427,178 @@ [new PackageSource(context.PackageSource)], | |
| File.Exists(Path.Combine(context.WorkingDirectory, $"{id.ToLowerInvariant()}.{v}.nupkg")) | ||
| .Should().BeFalse("Package does not exist in sources"); | ||
| } | ||
|
|
||
| public static IEnumerable<object[]> Cases() | ||
| { | ||
| // Parameters: | ||
| // A-packages, B-packages, sourceMappings, sourcesArgs, downloadId, downloadVersion, | ||
| // allowInsecureConnections, expectSuccess, expectedInstalled | ||
|
|
||
| // --source specified, mapping ignored, package only in A -> success | ||
| yield return new object[] | ||
| { | ||
| new List<(string,string)> { ("Contoso.Lib", "1.0.0") }, // A | ||
| new List<(string,string)>(), // B | ||
| new List<(string,string)> { ("B", "Contoso.*") }, // mapping ignored | ||
| new List<string> { "A" }, // --source A | ||
| "Contoso.Lib", "1.0.0", // downloadId, downloadVersion | ||
| true, // allow insecure | ||
| true, // expect success | ||
| ("Contoso.Lib", "1.0.0") // expectedInstalled | ||
| }; | ||
|
|
||
| // no --source, mapping -> B, package only in B -> success | ||
| yield return new object[] | ||
| { | ||
| new List<(string,string)>(), // A | ||
| new List<(string,string)> { ("Contoso.Mapped", "2.0.0") }, // B | ||
| new List<(string,string)> { ("B", "Contoso.*") }, // mapping -> B | ||
| null, // no --source | ||
| "Contoso.Mapped", "2.0.0", // downloadId, downloadVersion | ||
| true, // allow insecure | ||
| true, // expect success | ||
| ("Contoso.Mapped", "2.0.0") // expectedInstalled | ||
| }; | ||
|
|
||
| // no --source, mapping -> A, package only in B -> fail | ||
| yield return new object[] | ||
| { | ||
| new List<(string,string)>(), // A | ||
| new List<(string,string)> { ("Contoso.Mapped", "2.0.0") }, | ||
| new List<(string,string)> { ("A", "Contoso.*") }, // mapped to A | ||
| null, | ||
| "Contoso.Mapped", "2.0.0", | ||
| true, | ||
| false, | ||
| null! | ||
| }; | ||
|
|
||
| // --source specified, no source mapping with an insecure source | ||
| yield return new object[] | ||
| { | ||
| new List<(string,string)> { ("Contoso.Lib", "1.0.0") }, // A | ||
| new List<(string,string)>(), | ||
| new List<(string,string)> { ("A", "Contoso.*") }, | ||
| new List<string> { "A" }, // --source | ||
| "Contoso.Lib", "1.0.0", | ||
| false, // allow insecure connections false / not set to true | ||
| false, | ||
| null! | ||
| }; | ||
|
|
||
| // no --source, mapping -> B, allow insecure not enabled -> fail | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We are missing a test with no --source, mapping -> A,B, B has latest -> retrieves the latest package from B. |
||
| yield return new object[] | ||
| { | ||
| new List<(string,string)>(), // A | ||
| new List<(string,string)> { ("Contoso.Mapped", "1.0.0") }, | ||
| new List<(string,string)> { ("B", "Contoso.*") }, | ||
| null, | ||
| "Contoso.Mapped", "1.0.0", | ||
| false, // allow insecure connections false / not set to true | ||
| false, | ||
| null! | ||
| }; | ||
| } | ||
|
|
||
| [Theory] | ||
| [MemberData(nameof(Cases))] | ||
| public async Task RunAsync_WithSourceMapping_ListDriven_UsingCleanSetup( | ||
| IReadOnlyList<(string id, string version)> sourceAPackages, | ||
| IReadOnlyList<(string id, string version)> sourceBPackages, | ||
| IReadOnlyList<(string source, string pattern)> sourceMappings, | ||
| IReadOnlyList<string> sourcesArgs, | ||
| string downloadId, | ||
| string downloadVersion, | ||
| bool allowInsecureConnections, | ||
| bool expectSuccess, | ||
| (string id, string version)? expectedInstalled) | ||
| { | ||
| // Arrange | ||
| using var context = new SimpleTestPathContext(); | ||
| string srcADirectory = Path.Combine(context.PackageSource, "SourceA"); | ||
| string srcBDirectory = Path.Combine(context.PackageSource, "SourceB"); | ||
|
|
||
| using var serverA = new FileSystemBackedV3MockServer(srcADirectory); | ||
| using var serverB = new FileSystemBackedV3MockServer(srcBDirectory); | ||
|
|
||
| foreach (var (id, ver) in sourceAPackages) | ||
| { | ||
| await SimpleTestPackageUtility.CreateFullPackageAsync(srcADirectory, id, ver); | ||
| } | ||
|
|
||
| foreach (var (id, ver) in sourceBPackages) | ||
| { | ||
| await SimpleTestPackageUtility.CreateFullPackageAsync(srcBDirectory, id, ver); | ||
| } | ||
|
|
||
| serverA.Start(); | ||
| serverB.Start(); | ||
|
|
||
| // sources | ||
| context.Settings.AddSource("A", serverA.ServiceIndexUri); | ||
| context.Settings.AddSource("B", serverB.ServiceIndexUri); | ||
|
|
||
| // mapping | ||
| foreach (var (src, pattern) in sourceMappings) | ||
| { | ||
| context.Settings.AddPackageSourceMapping(src, pattern); | ||
| } | ||
|
|
||
| var settings = Settings.LoadSettingsGivenConfigPaths([context.Settings.ConfigPath]); | ||
|
|
||
| var packageSources = new List<PackageSource> | ||
| { | ||
| new(serverA.ServiceIndexUri, "A"), | ||
| new(serverB.ServiceIndexUri, "B") | ||
| }; | ||
|
|
||
| // args | ||
| var args = new PackageDownloadArgs | ||
| { | ||
| Packages = | ||
| [ | ||
| new PackageWithNuGetVersion | ||
| { | ||
| Id = downloadId, | ||
| NuGetVersion = downloadVersion is null ? null : NuGetVersion.Parse(downloadVersion) | ||
| } | ||
| ], | ||
| OutputDirectory = context.WorkingDirectory, | ||
| AllowInsecureConnections = allowInsecureConnections, | ||
| Sources = sourcesArgs == null ? [] : sourcesArgs.ToList() | ||
| }; | ||
|
|
||
| string capturedLogs = string.Empty; | ||
| var logger = new Mock<ILoggerWithColor>(MockBehavior.Loose); | ||
| logger | ||
| .Setup(l => l.LogError(It.IsAny<string>())) | ||
| .Callback<string>(msg => capturedLogs += msg + Environment.NewLine); | ||
|
|
||
| // Act | ||
| var exit = await PackageDownloadRunner.RunAsync( | ||
| args, | ||
| logger.Object, | ||
| packageSources, | ||
| settings, | ||
| CancellationToken.None); | ||
|
|
||
| serverA.Stop(); | ||
| serverB.Stop(); | ||
|
|
||
| // Assert | ||
| if (expectSuccess) | ||
| { | ||
| exit.Should().Be(PackageDownloadRunner.ExitCodeSuccess, because: capturedLogs); | ||
| expectedInstalled.Should().NotBeNull(); | ||
|
|
||
| var (expId, expVer) = expectedInstalled!.Value; | ||
| var installDir = Path.Combine(context.WorkingDirectory, expId.ToLowerInvariant(), expVer); | ||
| Directory.Exists(installDir).Should().BeTrue(); | ||
| File.Exists(Path.Combine(installDir, $"{expId.ToLowerInvariant()}.{expVer}.nupkg")).Should().BeTrue(); | ||
| } | ||
| else | ||
| { | ||
| exit.Should().Be(PackageDownloadRunner.ExitCodeError); | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.