This is an upgrader that will query an anonymous endpoint to get which version
of VFS for Git it should download from a NuGet feed.
This commit is contained in:
Jameson Miller 2019-04-14 17:23:42 -04:00
Родитель 2fb1163575
Коммит b725639b6d
9 изменённых файлов: 513 добавлений и 18 удалений

Просмотреть файл

@ -44,6 +44,7 @@ namespace GVFS.Common
public const string UpgradeRing = "upgrade.ring";
public const string UpgradeFeedPackageName = "upgrade.feedpackagename";
public const string UpgradeFeedUrl = "upgrade.feedurl";
public const string OrgInfoServerUrl = "upgrade.orgInfoServerUrl";
}
public static class GitStatusCache

Просмотреть файл

@ -25,6 +25,7 @@ namespace GVFS.Common
public GVFSPlatformConstants Constants { get; }
public UnderConstructionFlags UnderConstruction { get; }
public abstract string Name { get; }
public static void Register(GVFSPlatform platform)
{

Просмотреть файл

@ -1,6 +1,7 @@
using GVFS.Common.FileSystem;
using GVFS.Common.Git;
using GVFS.Common.Tracing;
using NuGet.Packaging.Core;
using NuGet.Protocol.Core.Types;
using System;
using System.Collections.Generic;
@ -14,13 +15,14 @@ namespace GVFS.Common.NuGetUpgrade
{
public class NuGetUpgrader : ProductUpgrader
{
protected readonly NuGetUpgraderConfig nuGetUpgraderConfig;
protected Version highestVersionAvailable;
private const string ContentDirectoryName = "content";
private const string InstallManifestFileName = "install-manifest.json";
private const string ExtractedInstallerDirectoryName = "InstallerTemp";
private readonly NuGetUpgraderConfig nuGetUpgraderConfig;
private InstallManifest installManifest;
private IPackageSearchMetadata highestVersionAvailable;
private NuGetFeed nuGetFeed;
private ICredentialStore credentialStore;
private bool isNuGetFeedInitialized;
@ -216,33 +218,33 @@ namespace GVFS.Common.NuGetUpgrade
IList<IPackageSearchMetadata> queryResults = this.QueryFeed(firstAttempt: true);
// Find the package with the highest version
IPackageSearchMetadata highestVersionAvailable = null;
IPackageSearchMetadata newestPackage = null;
foreach (IPackageSearchMetadata result in queryResults)
{
if (highestVersionAvailable == null || result.Identity.Version > highestVersionAvailable.Identity.Version)
if (newestPackage == null || result.Identity.Version > newestPackage.Identity.Version)
{
highestVersionAvailable = result;
newestPackage = result;
}
}
if (highestVersionAvailable != null &&
highestVersionAvailable.Identity.Version.Version > this.installedVersion)
if (newestPackage != null &&
newestPackage.Identity.Version.Version > this.installedVersion)
{
this.highestVersionAvailable = highestVersionAvailable;
this.highestVersionAvailable = newestPackage.Identity.Version.Version;
}
newVersion = this.highestVersionAvailable?.Identity?.Version?.Version;
newVersion = this.highestVersionAvailable;
if (newVersion != null)
{
this.tracer.RelatedInfo($"{nameof(this.TryQueryNewestVersion)} - new version available: installedVersion: {this.installedVersion}, highestVersionAvailable: {newVersion}");
message = $"New version {highestVersionAvailable.Identity.Version} is available.";
message = $"New version {newestPackage.Identity.Version} is available.";
return true;
}
else if (highestVersionAvailable != null)
else if (newestPackage != null)
{
this.tracer.RelatedInfo($"{nameof(this.TryQueryNewestVersion)} - up-to-date");
message = $"highest version available is {highestVersionAvailable.Identity.Version}, you are up-to-date";
message = $"highest version available is {newestPackage.Identity.Version}, you are up-to-date";
return true;
}
else
@ -277,6 +279,11 @@ namespace GVFS.Common.NuGetUpgrade
return false;
}
if (!this.EnsureNuGetFeedInitialized(out errorMessage))
{
return false;
}
if (!this.TryCreateAndConfigureDownloadDirectory(this.tracer, out errorMessage))
{
this.tracer.RelatedError($"{nameof(NuGetUpgrader)}.{nameof(this.TryCreateAndConfigureDownloadDirectory)} failed. {errorMessage}");
@ -287,7 +294,15 @@ namespace GVFS.Common.NuGetUpgrade
{
try
{
this.DownloadedPackagePath = this.nuGetFeed.DownloadPackageAsync(this.highestVersionAvailable.Identity).GetAwaiter().GetResult();
PackageIdentity packageId = this.GetPackageForVersion(this.highestVersionAvailable);
if (packageId == null)
{
errorMessage = $"The specified version {this.highestVersionAvailable} was not found in the NuGet feed. Please check with your administrator to make sure the feed is set up correctly.";
return false;
}
this.DownloadedPackagePath = this.nuGetFeed.DownloadPackageAsync(packageId).GetAwaiter().GetResult();
}
catch (Exception ex)
{
@ -463,6 +478,23 @@ namespace GVFS.Common.NuGetUpgrade
return "{" + tokenString + "}";
}
private PackageIdentity GetPackageForVersion(Version version)
{
IList<IPackageSearchMetadata> queryResults = this.QueryFeed(firstAttempt: true);
IPackageSearchMetadata packageForVersion = null;
foreach (IPackageSearchMetadata result in queryResults)
{
if (result.Identity.Version.Version == version)
{
packageForVersion = result;
break;
}
}
return packageForVersion?.Identity;
}
private bool TryGetPersonalAccessToken(string credentialUrl, ITracer tracer, out string token, out string error)
{
error = null;
@ -619,8 +651,8 @@ namespace GVFS.Common.NuGetUpgrade
public class NuGetUpgraderConfig
{
private readonly ITracer tracer;
private readonly LocalGVFSConfig localConfig;
protected readonly ITracer tracer;
protected readonly LocalGVFSConfig localConfig;
public NuGetUpgraderConfig(ITracer tracer, LocalGVFSConfig localGVFSConfig)
{
@ -648,7 +680,7 @@ namespace GVFS.Common.NuGetUpgrade
/// NuGetUpgrader is considered ready if all required
/// config settings are present.
/// </summary>
public bool IsReady(out string error)
public virtual bool IsReady(out string error)
{
if (string.IsNullOrEmpty(this.FeedUrl) ||
string.IsNullOrEmpty(this.PackageFeedName))
@ -667,7 +699,7 @@ namespace GVFS.Common.NuGetUpgrade
/// <summary>
/// Check if the NuGetUpgrader is configured.
/// </summary>
public bool IsConfigured(out string error)
public virtual bool IsConfigured(out string error)
{
if (string.IsNullOrEmpty(this.FeedUrl) &&
string.IsNullOrEmpty(this.PackageFeedName))
@ -686,7 +718,7 @@ namespace GVFS.Common.NuGetUpgrade
/// <summary>
/// Try to load the config for a NuGet upgrader. Returns false if there was an error reading the config.
/// </summary>
public bool TryLoad(out string error)
public virtual bool TryLoad(out string error)
{
string configValue;
if (!this.localConfig.TryGetConfig(GVFSConstants.LocalGVFSConfig.UpgradeFeedUrl, out configValue, out error))

Просмотреть файл

@ -0,0 +1,262 @@
using GVFS.Common.FileSystem;
using GVFS.Common.Git;
using GVFS.Common.Tracing;
using System;
using System.Net.Http;
using System.Runtime.Serialization;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace GVFS.Common.NuGetUpgrade
{
public class OrgNuGetUpgrader : NuGetUpgrader
{
private HttpClient httpClient;
private string platform;
public OrgNuGetUpgrader(
string currentVersion,
ITracer tracer,
PhysicalFileSystem fileSystem,
HttpClient httpClient,
bool dryRun,
bool noVerify,
OrgNuGetUpgraderConfig config,
string downloadFolder,
string platform,
ICredentialStore credentialStore)
: base(
currentVersion,
tracer,
fileSystem,
dryRun,
noVerify,
config,
downloadFolder,
credentialStore)
{
this.httpClient = httpClient;
this.platform = platform;
}
public OrgNuGetUpgrader(
string currentVersion,
ITracer tracer,
PhysicalFileSystem fileSystem,
HttpClient httpClient,
bool dryRun,
bool noVerify,
OrgNuGetUpgraderConfig config,
string platform,
NuGetFeed nuGetFeed,
ICredentialStore credentialStore)
: base(
currentVersion,
tracer,
dryRun,
noVerify,
fileSystem,
config,
nuGetFeed,
credentialStore)
{
this.httpClient = httpClient;
this.platform = platform;
}
public override bool SupportsAnonymousVersionQuery { get => true; }
private OrgNuGetUpgraderConfig Config { get => this.nuGetUpgraderConfig as OrgNuGetUpgraderConfig; }
private string OrgInfoServerUrl { get => this.Config.OrgInfoServer; }
private string Ring { get => this.Config.UpgradeRing; }
public static bool TryCreate(
ITracer tracer,
PhysicalFileSystem fileSystem,
LocalGVFSConfig gvfsConfig,
HttpClient httpClient,
ICredentialStore credentialStore,
bool dryRun,
bool noVerify,
out OrgNuGetUpgrader upgrader,
out string error)
{
OrgNuGetUpgraderConfig upgraderConfig = new OrgNuGetUpgraderConfig(tracer, gvfsConfig);
upgrader = null;
if (!upgraderConfig.TryLoad(out error))
{
upgrader = null;
return false;
}
if (!upgraderConfig.IsConfigured(out error))
{
return false;
}
if (!upgraderConfig.IsReady(out error))
{
return false;
}
string platform = GVFSPlatform.Instance.Name;
upgrader = new OrgNuGetUpgrader(
ProcessHelper.GetCurrentProcessVersion(),
tracer,
fileSystem,
httpClient,
dryRun,
noVerify,
upgraderConfig,
ProductUpgraderInfo.GetAssetDownloadsPath(),
platform,
credentialStore);
return true;
}
public override bool TryQueryNewestVersion(out Version newVersion, out string message)
{
newVersion = null;
if (!TryParseOrgFromNuGetFeedUrl(this.Config.FeedUrl, out string orgName))
{
message = "OrgNuGetUpgrader is not able to parse org name from NuGet Package Feed URL";
return false;
}
OrgInfoApiClient infoServer = new OrgInfoApiClient(this.httpClient, this.OrgInfoServerUrl);
try
{
this.highestVersionAvailable = infoServer.QueryNewestVersion(orgName, this.platform, this.Ring);
}
catch (Exception exception) when (exception is HttpRequestException ||
exception is TaskCanceledException)
{
// GetStringAsync can also throw a TaskCanceledException to indicate a timeout
// https://github.com/dotnet/corefx/issues/20296
message = string.Format("Network error: could not connect to server ({0}). {1}", this.OrgInfoServerUrl, exception.Message);
this.TraceException(exception, nameof(this.TryQueryNewestVersion), "Error connecting to server.");
return false;
}
catch (SerializationException exception)
{
message = string.Format("Parse error: could not parse response from server({0}). {1}", this.OrgInfoServerUrl, exception.Message);
this.TraceException(exception, nameof(this.TryQueryNewestVersion), "Error parsing response from server.");
return false;
}
catch (Exception exception) when (exception is ArgumentException ||
exception is FormatException ||
exception is OverflowException)
{
message = string.Format("Unexpected response from server: could nor parse version({0}). {1}", this.OrgInfoServerUrl, exception.Message);
this.TraceException(exception, nameof(this.TryQueryNewestVersion), "Error parsing response from server.");
return false;
}
if (this.highestVersionAvailable != null &&
this.highestVersionAvailable > this.installedVersion)
{
newVersion = this.highestVersionAvailable;
}
if (newVersion != null)
{
this.tracer.RelatedInfo($"{nameof(this.TryQueryNewestVersion)} - new version available: installedVersion: {this.installedVersion}, highestVersionAvailable: {newVersion}");
message = $"New version {newVersion} is available.";
return true;
}
else if (this.highestVersionAvailable != null)
{
this.tracer.RelatedInfo($"{nameof(this.TryQueryNewestVersion)} - up-to-date");
message = $"Highest version available is {this.highestVersionAvailable}, you are up-to-date";
return true;
}
else
{
this.tracer.RelatedInfo($"{nameof(this.TryQueryNewestVersion)} - no versions available from feed.");
message = "No versions available via endpoint.";
return true;
}
}
private static bool TryParseOrgFromNuGetFeedUrl(string packageFeedUrl, out string orgName)
{
// We expect a URL of the form https://pkgs.dev.azure.com/{org}
// and want to convert it to a URL of the form https://{org}.visualstudio.com
Regex packageUrlRegex = new Regex(
@"^https://pkgs.dev.azure.com/(?<org>.+?)/",
RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
Match urlMatch = packageUrlRegex.Match(packageFeedUrl);
if (!urlMatch.Success)
{
orgName = null;
return false;
}
orgName = urlMatch.Groups["org"].Value;
return true;
}
public class OrgNuGetUpgraderConfig : NuGetUpgraderConfig
{
public OrgNuGetUpgraderConfig(ITracer tracer, LocalGVFSConfig localGVFSConfig)
: base(tracer, localGVFSConfig)
{
}
public string OrgInfoServer { get; set; }
public string UpgradeRing { get; set; }
public override bool TryLoad(out string error)
{
if (!base.TryLoad(out error))
{
return false;
}
if (!this.localConfig.TryGetConfig(GVFSConstants.LocalGVFSConfig.OrgInfoServerUrl, out string orgInfoServerUrl, out error))
{
this.tracer.RelatedError(error);
return false;
}
this.OrgInfoServer = orgInfoServerUrl;
if (!this.localConfig.TryGetConfig(GVFSConstants.LocalGVFSConfig.UpgradeRing, out string upgradeRing, out error))
{
this.tracer.RelatedError(error);
return false;
}
this.UpgradeRing = upgradeRing;
return true;
}
public override bool IsReady(out string error)
{
if (!base.IsReady(out error) ||
string.IsNullOrEmpty(this.UpgradeRing) ||
string.IsNullOrEmpty(this.OrgInfoServer))
{
error = string.Join(
Environment.NewLine,
"One or more required settings for OrgNuGetUpgrader are missing.",
"Use `gvfs config [{GVFSConstants.LocalGVFSConfig.UpgradeFeedUrl} | {GVFSConstants.LocalGVFSConfig.UpgradeFeedPackageName} | {GVFSConstants.LocalGVFSConfig.UpgradeRing} | {GVFSConstants.LocalGVFSConfig.OrgInfoServerUrl}] <value>` to set the config.");
return false;
}
return true;
}
}
}
}

Просмотреть файл

@ -43,6 +43,11 @@ namespace GVFS.Common
string responseString = this.client.GetStringAsync(this.ConstructRequest(this.VersionUrl, queryParams)).GetAwaiter().GetResult();
VersionResponse versionResponse = VersionResponse.FromJsonString(responseString);
if (string.IsNullOrEmpty(versionResponse.Version))
{
return null;
}
return new Version(versionResponse.Version);
}

Просмотреть файл

@ -13,6 +13,7 @@ namespace GVFS.Platform.Mac
public override IDiskLayoutUpgradeData DiskLayoutUpgrade { get; } = new MacDiskLayoutUpgradeData();
public override IKernelDriver KernelDriver { get; } = new ProjFSKext();
public override string Name { get => "macOS"; }
public override string GetOSVersionInformation()
{

Просмотреть файл

@ -37,6 +37,7 @@ namespace GVFS.Platform.Windows
public override IGitInstallation GitInstallation { get; } = new WindowsGitInstallation();
public override IDiskLayoutUpgradeData DiskLayoutUpgrade { get; } = new WindowsDiskLayoutUpgradeData();
public override IPlatformFileSystem FileSystem { get; } = new WindowsFileSystem();
public override string Name { get => "Windows"; }
public static string GetStringFromRegistry(string key, string valueName)
{

Просмотреть файл

@ -0,0 +1,191 @@
using GVFS.Common;
using GVFS.Common.Git;
using GVFS.Common.NuGetUpgrade;
using GVFS.Common.Tracing;
using GVFS.Tests.Should;
using GVFS.UnitTests.Category;
using GVFS.UnitTests.Mock.Common;
using GVFS.UnitTests.Mock.FileSystem;
using Moq;
using Moq.Protected;
using NuGet.Packaging.Core;
using NuGet.Protocol.Core.Types;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace GVFS.UnitTests.Common.NuGetUpgrade
{
[TestFixture]
public class OrgNuGetUpgraderTests
{
private const string CurrentVersion = "1.5.1185.0";
private const string NewerVersion = "1.6.1185.0";
private const string DefaultUpgradeFeedPackageName = "package";
private const string DefaultUpgradeFeedUrl = "https://pkgs.dev.azure.com/contoso/";
private const string DefaultOrgInfoServerUrl = "https://www.contoso.com";
private const string DefaultRing = "slow";
private OrgNuGetUpgrader upgrader;
private MockTracer tracer;
private OrgNuGetUpgrader.OrgNuGetUpgraderConfig upgraderConfig;
private Mock<NuGetFeed> mockNuGetFeed;
private MockFileSystem mockFileSystem;
private Mock<ICredentialStore> mockCredentialManager;
private Mock<HttpMessageHandler> httpMessageHandlerMock;
private string downloadDirectoryPath = Path.Combine(
$"mock:{Path.DirectorySeparatorChar}",
ProductUpgraderInfo.UpgradeDirectoryName,
ProductUpgraderInfo.DownloadDirectory);
private interface IHttpMessageHandlerProtectedMembers
{
Task<HttpResponseMessage> SendAsync(HttpRequestMessage message, CancellationToken token);
}
public static IEnumerable<Exception> NetworkFailureCases()
{
yield return new HttpRequestException("Response status code does not indicate success: 401: (Unauthorized)");
yield return new TaskCanceledException("Task canceled");
}
[SetUp]
public void SetUp()
{
MockLocalGVFSConfig mockGvfsConfig = new MockLocalGVFSConfigBuilder(
DefaultRing,
DefaultUpgradeFeedUrl,
DefaultUpgradeFeedPackageName,
DefaultOrgInfoServerUrl)
.WithUpgradeRing()
.WithUpgradeFeedPackageName()
.WithUpgradeFeedUrl()
.WithOrgInfoServerUrl()
.Build();
this.upgraderConfig = new OrgNuGetUpgrader.OrgNuGetUpgraderConfig(this.tracer, mockGvfsConfig);
this.upgraderConfig.TryLoad(out _);
this.tracer = new MockTracer();
this.mockNuGetFeed = new Mock<NuGetFeed>(
DefaultUpgradeFeedUrl,
DefaultUpgradeFeedPackageName,
this.downloadDirectoryPath,
null,
this.tracer);
this.mockFileSystem = new MockFileSystem(
new MockDirectory(
Path.GetDirectoryName(this.downloadDirectoryPath),
new[] { new MockDirectory(this.downloadDirectoryPath, null, null) },
null));
this.mockCredentialManager = new Mock<ICredentialStore>();
string credentialManagerString = "value";
string emptyString = string.Empty;
this.mockCredentialManager.Setup(foo => foo.TryGetCredential(It.IsAny<ITracer>(), It.IsAny<string>(), out credentialManagerString, out credentialManagerString, out credentialManagerString)).Returns(true);
this.httpMessageHandlerMock = new Mock<HttpMessageHandler>();
this.httpMessageHandlerMock.Protected().As<IHttpMessageHandlerProtectedMembers>()
.Setup(m => m.SendAsync(It.IsAny<HttpRequestMessage>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new HttpResponseMessage()
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(this.ConstructResponseContent(NewerVersion))
});
HttpClient httpClient = new HttpClient(this.httpMessageHandlerMock.Object);
this.upgrader = new OrgNuGetUpgrader(
CurrentVersion,
this.tracer,
this.mockFileSystem,
httpClient,
false,
false,
this.upgraderConfig,
"windows",
this.mockNuGetFeed.Object,
this.mockCredentialManager.Object);
}
[TestCase]
public void SupportsAnonymousQuery()
{
this.upgrader.SupportsAnonymousVersionQuery.ShouldBeTrue();
}
[TestCase]
public void TryQueryNewestVersion()
{
Version newVersion;
string message;
bool success = this.upgrader.TryQueryNewestVersion(out newVersion, out message);
success.ShouldBeTrue();
newVersion.ShouldNotBeNull();
newVersion.ShouldEqual<Version>(new Version(NewerVersion));
message.ShouldNotBeNull();
message.ShouldEqual($"New version {OrgNuGetUpgraderTests.NewerVersion} is available.");
}
[TestCaseSource("NetworkFailureCases")]
public void HandlesNetworkErrors(Exception ex)
{
Version newVersion;
string message;
this.httpMessageHandlerMock.Protected().As<IHttpMessageHandlerProtectedMembers>()
.Setup(m => m.SendAsync(It.IsAny<HttpRequestMessage>(), It.IsAny<CancellationToken>()))
.ThrowsAsync(ex);
bool success = this.upgrader.TryQueryNewestVersion(out newVersion, out message);
success.ShouldBeFalse();
newVersion.ShouldBeNull();
message.ShouldNotBeNull();
message.ShouldContain("Network error");
}
[TestCase]
public void HandlesEmptyVersion()
{
Version newVersion;
string message;
this.httpMessageHandlerMock.Protected().As<IHttpMessageHandlerProtectedMembers>()
.Setup(m => m.SendAsync(It.IsAny<HttpRequestMessage>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new HttpResponseMessage()
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(this.ConstructResponseContent(string.Empty))
});
bool success = this.upgrader.TryQueryNewestVersion(out newVersion, out message);
success.ShouldBeTrue();
newVersion.ShouldBeNull();
message.ShouldNotBeNull();
message.ShouldContain("No versions available");
}
private string ConstructResponseContent(string version)
{
return $"{{\"version\" : \"{version}\"}} ";
}
}
}

Просмотреть файл

@ -26,6 +26,7 @@ namespace GVFS.UnitTests.Mock.Common
public override IDiskLayoutUpgradeData DiskLayoutUpgrade => throw new NotSupportedException();
public override IPlatformFileSystem FileSystem { get; } = new MockPlatformFileSystem();
public override string Name { get => "Mock"; }
public HashSet<int> ActiveProcesses { get; } = new HashSet<int>();