From 0f1c34b39bf732cd8c3c4722f92fc7eb3250569a Mon Sep 17 00:00:00 2001 From: Scott Bommarito Date: Wed, 28 Nov 2018 13:32:03 -0800 Subject: [PATCH 1/7] CollectAzureCdnLogs should properly handle files ending in .tmp (#673) --- .../Ftp/FtpRawLogClient.cs | 8 +++----- src/Stats.CollectAzureCdnLogs/IRawLogClient.cs | 2 +- src/Stats.CollectAzureCdnLogs/Job.cs | 6 ++++-- .../RawLogFileInfo.cs | 18 ++++++------------ .../RawLogFileInfoTests.cs | 1 + 5 files changed, 15 insertions(+), 20 deletions(-) diff --git a/src/Stats.CollectAzureCdnLogs/Ftp/FtpRawLogClient.cs b/src/Stats.CollectAzureCdnLogs/Ftp/FtpRawLogClient.cs index f934766c9..87cb6a42a 100644 --- a/src/Stats.CollectAzureCdnLogs/Ftp/FtpRawLogClient.cs +++ b/src/Stats.CollectAzureCdnLogs/Ftp/FtpRawLogClient.cs @@ -97,7 +97,7 @@ public async Task DeleteAsync(Uri uri) } } - public async Task> GetRawLogFiles(Uri uri) + public async Task> GetRawLogFileUris(Uri uri) { if (uri == null) { @@ -124,15 +124,13 @@ public async Task> GetRawLogFiles(Uri uri) var fileNames = directoryList.Split(Environment.NewLine.ToCharArray(), StringSplitOptions.RemoveEmptyEntries); - var rawLogFiles = fileNames.Select(fn => new RawLogFileInfo(new Uri(uri.EnsureTrailingSlash(), fn))); - - return rawLogFiles; + return fileNames.Select(fn => new Uri(uri.EnsureTrailingSlash(), fn)); } catch (Exception e) { Logger.LogError(LogEvents.FailedBlobListing, e, "Failed to get raw log files."); - return Enumerable.Empty(); + return Enumerable.Empty(); } } } diff --git a/src/Stats.CollectAzureCdnLogs/IRawLogClient.cs b/src/Stats.CollectAzureCdnLogs/IRawLogClient.cs index 8dcf34c96..0386aef22 100644 --- a/src/Stats.CollectAzureCdnLogs/IRawLogClient.cs +++ b/src/Stats.CollectAzureCdnLogs/IRawLogClient.cs @@ -10,7 +10,7 @@ namespace Stats.CollectAzureCdnLogs { internal interface IRawLogClient { - Task> GetRawLogFiles(Uri uri); + Task> GetRawLogFileUris(Uri uri); Task OpenReadAsync(Uri uri); Task RenameAsync(Uri uri, string newFileName); Task DeleteAsync(Uri uri); diff --git a/src/Stats.CollectAzureCdnLogs/Job.cs b/src/Stats.CollectAzureCdnLogs/Job.cs index e6c997ee0..9b8351b2a 100644 --- a/src/Stats.CollectAzureCdnLogs/Job.cs +++ b/src/Stats.CollectAzureCdnLogs/Job.cs @@ -114,15 +114,17 @@ public override async Task Run() var azureClient = new CloudBlobRawLogClient(LoggerFactory, _cloudStorageAccount); // Collect directory listing. - var rawLogFiles = await ftpClient.GetRawLogFiles(_ftpServerUri); + var rawLogFileUris = await ftpClient.GetRawLogFileUris(_ftpServerUri); // Prepare cloud storage blob container. var cloudBlobContainer = await azureClient.CreateContainerIfNotExistsAsync(_cloudStorageContainerName); - foreach (var rawLogFile in rawLogFiles) + foreach (var rawLogFileUri in rawLogFileUris) { try { + var rawLogFile = new RawLogFileInfo(rawLogFileUri); + if (_azureCdnPlatform != rawLogFile.AzureCdnPlatform || !_azureCdnAccountNumber.Equals(rawLogFile.AzureCdnAccountNumber, StringComparison.InvariantCultureIgnoreCase)) { diff --git a/src/Stats.CollectAzureCdnLogs/RawLogFileInfo.cs b/src/Stats.CollectAzureCdnLogs/RawLogFileInfo.cs index 2d781397c..a90b0eb87 100644 --- a/src/Stats.CollectAzureCdnLogs/RawLogFileInfo.cs +++ b/src/Stats.CollectAzureCdnLogs/RawLogFileInfo.cs @@ -70,10 +70,8 @@ private void TryParseFileName() if (Extension.EndsWith(FileExtensions.Gzip, StringComparison.InvariantCultureIgnoreCase)) { ContentType = _contentTypeGzip; - } - else - { - throw new InvalidRawLogFileNameException(FileName); + + return; } } else if (lastPart.Count() == 4) @@ -85,17 +83,13 @@ private void TryParseFileName() { IsPendingDownload = true; ContentType = _contentTypeGzip; + + return; } } - else - { - throw new InvalidRawLogFileNameException(FileName); - } - } - else - { - throw new InvalidRawLogFileNameException(FileName); } + + throw new InvalidRawLogFileNameException(FileName); } private int TryParseRollingFileNumber(string rollingFileNumberString) diff --git a/tests/Tests.Stats.CollectAzureCdnLogs/RawLogFileInfoTests.cs b/tests/Tests.Stats.CollectAzureCdnLogs/RawLogFileInfoTests.cs index cfbcf7a87..e412f5cee 100644 --- a/tests/Tests.Stats.CollectAzureCdnLogs/RawLogFileInfoTests.cs +++ b/tests/Tests.Stats.CollectAzureCdnLogs/RawLogFileInfoTests.cs @@ -60,6 +60,7 @@ public void ThrowsWhenInvalidUri() [InlineData("ftp://someserver/logs/wpc_A000_20150603_0058.log")] [InlineData("ftp://someserver/logs/wpc_A000_20151342_0058.log.gz")] [InlineData("ftp://someserver/logs/wpc_A000_20150603_0058.log.download")] + [InlineData("ftp://someserver/logs/wpc_A000_20150603_0058.log.gz.tmp")] public void ThrowsWhenInvalidRawLogFileName(string uriString) { Assert.Throws(() => new RawLogFileInfo(new Uri(uriString))); From 6d9b8e464598183bfdcd0f6ee6f509c332551035 Mon Sep 17 00:00:00 2001 From: Xavier Decoster Date: Thu, 29 Nov 2018 12:04:44 +0100 Subject: [PATCH 2/7] Recognize "NuGet VS VSIX" as valid NuGet client (#681) --- .../Programmability/Functions/dbo.IsNuGetClient.sql | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Stats.Warehouse/Programmability/Functions/dbo.IsNuGetClient.sql b/src/Stats.Warehouse/Programmability/Functions/dbo.IsNuGetClient.sql index f732b2caf..8e85f5f19 100644 --- a/src/Stats.Warehouse/Programmability/Functions/dbo.IsNuGetClient.sql +++ b/src/Stats.Warehouse/Programmability/Functions/dbo.IsNuGetClient.sql @@ -14,6 +14,9 @@ BEGIN CHARINDEX('NuGet Cross-Platform Command Line', @ClientName) > 0 OR CHARINDEX('NuGet Client V3', @ClientName) > 0 + -- VS NuGet 4.6+ + OR CHARINDEX('NuGet VS VSIX', @ClientName) > 0 + -- VS NuGet 2.8+ OR CHARINDEX('NuGet VS PowerShell Console', @ClientName) > 0 OR CHARINDEX('NuGet VS Packages Dialog - Solution', @ClientName) > 0 From 0076494b2fbac68288fc4a1580a00c6a0a696121 Mon Sep 17 00:00:00 2001 From: Christy Henriksson Date: Tue, 27 Nov 2018 16:47:03 -0800 Subject: [PATCH 3/7] Convert CollectCdnLogs jobs to JsonConfig --- .../Collect/AzureStatsLogDestination.cs | 4 +- .../Collect/AzureStatsLogSource.cs | 4 +- .../CollectAzureCdnLogsConfiguration.cs | 22 +++ src/Stats.CollectAzureCdnLogs/Job.cs | 72 +++++--- .../Scripts/Stats.CollectAzureCdnLogs.cmd | 14 +- .../Settings/dev.json | 18 ++ .../Settings/int.json | 18 ++ .../Settings/prod.json | 18 ++ .../Stats.CollectAzureCdnLogs.csproj | 5 + .../Stats.CollectAzureCdnLogs.nuspec | 2 + .../ArgumentNames.cs | 16 -- .../CollectAzureChinaCdnLogsConfiguration.cs | 20 +++ src/Stats.CollectAzureChinaCDNLogs/Job.cs | 55 +++--- .../Stats.CollectAzureChinaCDNLogs.cmd | 25 +-- .../Settings/dev.json | 17 ++ .../Settings/int.json | 17 ++ .../Settings/prod.json | 17 ++ .../Stats.CollectAzureChinaCDNLogs.csproj | 6 +- .../Stats.CollectAzureChinaCDNLogs.nuspec | 2 + .../JobTests.cs | 160 ++++++++---------- .../Tests.Stats.CollectAzureCdnLogs.csproj | 3 + .../JobTests.cs | 93 +++++++--- ...ests.Stats.CollectAzureChinaCDNLogs.csproj | 3 + 23 files changed, 416 insertions(+), 195 deletions(-) create mode 100644 src/Stats.CollectAzureCdnLogs/Configuration/CollectAzureCdnLogsConfiguration.cs create mode 100644 src/Stats.CollectAzureCdnLogs/Settings/dev.json create mode 100644 src/Stats.CollectAzureCdnLogs/Settings/int.json create mode 100644 src/Stats.CollectAzureCdnLogs/Settings/prod.json delete mode 100644 src/Stats.CollectAzureChinaCDNLogs/ArgumentNames.cs create mode 100644 src/Stats.CollectAzureChinaCDNLogs/Configuration/CollectAzureChinaCdnLogsConfiguration.cs create mode 100644 src/Stats.CollectAzureChinaCDNLogs/Settings/dev.json create mode 100644 src/Stats.CollectAzureChinaCDNLogs/Settings/int.json create mode 100644 src/Stats.CollectAzureChinaCDNLogs/Settings/prod.json diff --git a/src/Stats.AzureCdnLogs.Common/Collect/AzureStatsLogDestination.cs b/src/Stats.AzureCdnLogs.Common/Collect/AzureStatsLogDestination.cs index c394d3be6..0837857ad 100644 --- a/src/Stats.AzureCdnLogs.Common/Collect/AzureStatsLogDestination.cs +++ b/src/Stats.AzureCdnLogs.Common/Collect/AzureStatsLogDestination.cs @@ -24,9 +24,9 @@ public class AzureStatsLogDestination : ILogDestination private CloudBlobClient _cloudBlobClient; private CloudBlobContainer _cloudBlobContainer; - public AzureStatsLogDestination(string connectionString, string containerName) + public AzureStatsLogDestination(CloudStorageAccount storageAccount, string containerName) { - _azureAccount = CloudStorageAccount.Parse(connectionString); + _azureAccount = storageAccount; _cloudBlobClient = _azureAccount.CreateCloudBlobClient(); _cloudBlobContainer = _cloudBlobClient.GetContainerReference(containerName); _cloudBlobContainer.CreateIfNotExists(); diff --git a/src/Stats.AzureCdnLogs.Common/Collect/AzureStatsLogSource.cs b/src/Stats.AzureCdnLogs.Common/Collect/AzureStatsLogSource.cs index 51d47a0b4..6265a0082 100644 --- a/src/Stats.AzureCdnLogs.Common/Collect/AzureStatsLogSource.cs +++ b/src/Stats.AzureCdnLogs.Common/Collect/AzureStatsLogSource.cs @@ -33,9 +33,9 @@ public class AzureStatsLogSource : ILogSource /// /// The connection string for the Azure account. /// The container name. - public AzureStatsLogSource(string connectionString, string containerName, int azureServerTimeoutInSeconds) + public AzureStatsLogSource(CloudStorageAccount storageAccount, string containerName, int azureServerTimeoutInSeconds) { - _azureAccount = CloudStorageAccount.Parse(connectionString); + _azureAccount = storageAccount; _blobClient = _azureAccount.CreateCloudBlobClient(); _container = _blobClient.GetContainerReference(containerName); _blobRequestOptions = new BlobRequestOptions(); diff --git a/src/Stats.CollectAzureCdnLogs/Configuration/CollectAzureCdnLogsConfiguration.cs b/src/Stats.CollectAzureCdnLogs/Configuration/CollectAzureCdnLogsConfiguration.cs new file mode 100644 index 000000000..53507bc69 --- /dev/null +++ b/src/Stats.CollectAzureCdnLogs/Configuration/CollectAzureCdnLogsConfiguration.cs @@ -0,0 +1,22 @@ +// 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. + +namespace Stats.CollectAzureCdnLogs +{ + public class CollectAzureCdnLogsConfiguration + { + public string AzureCdnAccountNumber { get; set; } + + public string AzureCdnCloudStorageAccount { get; set; } + + public string AzureCdnCloudStorageContainerName { get; set; } + + public string AzureCdnPlatform { get; set; } + + public string FtpSourceUri { get; set; } + + public string FtpSourceUsername { get; set; } + + public string FtpSourcePassword { get; set; } + } +} diff --git a/src/Stats.CollectAzureCdnLogs/Job.cs b/src/Stats.CollectAzureCdnLogs/Job.cs index 9b8351b2a..14d69ee93 100644 --- a/src/Stats.CollectAzureCdnLogs/Job.cs +++ b/src/Stats.CollectAzureCdnLogs/Job.cs @@ -9,9 +9,13 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; +using Autofac; using ICSharpCode.SharpZipLib; using ICSharpCode.SharpZipLib.GZip; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.WindowsAzure.Storage; using NuGet.Jobs; using Stats.AzureCdnLogs.Common; @@ -20,31 +24,50 @@ namespace Stats.CollectAzureCdnLogs { - public class Job - : JobBase + public class Job : JsonConfigurationJob { private static readonly DateTime _unixTimestamp = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); private Uri _ftpServerUri; - private string _ftpUsername; - private string _ftpPassword; - private string _azureCdnAccountNumber; private AzureCdnPlatform _azureCdnPlatform; private CloudStorageAccount _cloudStorageAccount; - private string _cloudStorageContainerName; + + private CollectAzureCdnLogsConfiguration _configuration; public override void Init(IServiceContainer serviceContainer, IDictionary jobArgsDictionary) { - var ftpLogFolder = JobConfigurationManager.GetArgument(jobArgsDictionary, JobArgumentNames.FtpSourceUri); - var azureCdnPlatform = JobConfigurationManager.GetArgument(jobArgsDictionary, JobArgumentNames.AzureCdnPlatform); - var cloudStorageAccount = JobConfigurationManager.GetArgument(jobArgsDictionary, JobArgumentNames.AzureCdnCloudStorageAccount); - _cloudStorageContainerName = JobConfigurationManager.GetArgument(jobArgsDictionary, JobArgumentNames.AzureCdnCloudStorageContainerName); - _azureCdnAccountNumber = JobConfigurationManager.GetArgument(jobArgsDictionary, JobArgumentNames.AzureCdnAccountNumber); - _ftpUsername = JobConfigurationManager.GetArgument(jobArgsDictionary, JobArgumentNames.FtpSourceUsername); - _ftpPassword = JobConfigurationManager.GetArgument(jobArgsDictionary, JobArgumentNames.FtpSourcePassword); - - _ftpServerUri = ValidateFtpUri(ftpLogFolder); - _azureCdnPlatform = ValidateAzureCdnPlatform(azureCdnPlatform); - _cloudStorageAccount = ValidateAzureCloudStorageAccount(cloudStorageAccount); + base.Init(serviceContainer, jobArgsDictionary); + + InitializeJobConfiguration(_serviceProvider); + } + + public void InitializeJobConfiguration(IServiceProvider serviceProvider) + { + _configuration = serviceProvider.GetRequiredService>().Value; + + if (string.IsNullOrEmpty(_configuration.AzureCdnAccountNumber)) + { + throw new ArgumentException("Configuration 'AzureCdnAccountNumber' is required", nameof(_configuration)); + } + + if (string.IsNullOrEmpty(_configuration.AzureCdnCloudStorageContainerName)) + { + throw new ArgumentException("Configuration 'AzureCdnCloudStorageContainerName' is required", nameof(_configuration)); + } + + if (string.IsNullOrEmpty(_configuration.FtpSourceUsername)) { + throw new ArgumentException("Configuration 'FtpSourceUsername' is required", nameof(_configuration)); + } + + if (string.IsNullOrEmpty(_configuration.FtpSourcePassword)) + { + throw new ArgumentException("Configuration 'FtpSourcePassword' is required", nameof(_configuration)); + } + + _cloudStorageAccount = ValidateAzureCloudStorageAccount(_configuration.AzureCdnCloudStorageAccount); + + _azureCdnPlatform = ValidateAzureCdnPlatform(_configuration.AzureCdnPlatform); + + _ftpServerUri = ValidateFtpUri(_configuration.FtpSourceUri); } private static CloudStorageAccount ValidateAzureCloudStorageAccount(string cloudStorageAccount) @@ -110,14 +133,14 @@ private static Uri ValidateFtpUri(string serverUrl) public override async Task Run() { - var ftpClient = new FtpRawLogClient(LoggerFactory, _ftpUsername, _ftpPassword); + var ftpClient = new FtpRawLogClient(LoggerFactory, _configuration.FtpSourceUsername, _configuration.FtpSourcePassword); var azureClient = new CloudBlobRawLogClient(LoggerFactory, _cloudStorageAccount); // Collect directory listing. var rawLogFileUris = await ftpClient.GetRawLogFileUris(_ftpServerUri); // Prepare cloud storage blob container. - var cloudBlobContainer = await azureClient.CreateContainerIfNotExistsAsync(_cloudStorageContainerName); + var cloudBlobContainer = await azureClient.CreateContainerIfNotExistsAsync(_configuration.AzureCdnCloudStorageContainerName); foreach (var rawLogFileUri in rawLogFileUris) { @@ -126,7 +149,7 @@ public override async Task Run() var rawLogFile = new RawLogFileInfo(rawLogFileUri); if (_azureCdnPlatform != rawLogFile.AzureCdnPlatform - || !_azureCdnAccountNumber.Equals(rawLogFile.AzureCdnAccountNumber, StringComparison.InvariantCultureIgnoreCase)) + || !_configuration.AzureCdnAccountNumber.Equals(rawLogFile.AzureCdnAccountNumber, StringComparison.InvariantCultureIgnoreCase)) { // Only process the raw log files matching the target CDN platform and account number. continue; @@ -367,5 +390,14 @@ private static string ToUnixTimeStamp(DateTime dateTime) var secondsPastEpoch = (dateTime - _unixTimestamp).TotalSeconds; return secondsPastEpoch.ToString(CultureInfo.InvariantCulture); } + + protected override void ConfigureAutofacServices(ContainerBuilder containerBuilder) + { + } + + protected override void ConfigureJobServices(IServiceCollection services, IConfigurationRoot configurationRoot) + { + ConfigureInitializationSection(services, configurationRoot); + } } } diff --git a/src/Stats.CollectAzureCdnLogs/Scripts/Stats.CollectAzureCdnLogs.cmd b/src/Stats.CollectAzureCdnLogs/Scripts/Stats.CollectAzureCdnLogs.cmd index 39943a693..bda6afb0a 100644 --- a/src/Stats.CollectAzureCdnLogs/Scripts/Stats.CollectAzureCdnLogs.cmd +++ b/src/Stats.CollectAzureCdnLogs/Scripts/Stats.CollectAzureCdnLogs.cmd @@ -3,12 +3,16 @@ cd bin :Top - echo "Starting job - #{Jobs.stats.collectazurecdnlogs.Title}" +echo "Starting job - #{Jobs.stats.collectazurecdnlogs.Title}" - title #{Jobs.stats.collectazurecdnlogs.Title} +title #{Jobs.stats.collectazurecdnlogs.Title} - start /w stats.collectazurecdnlogs.exe -VaultName "#{Deployment.Azure.KeyVault.VaultName}" -ClientId "#{Deployment.Azure.KeyVault.ClientId}" -CertificateThumbprint "#{Deployment.Azure.KeyVault.CertificateThumbprint}" -FtpSourceUri "#{Jobs.stats.collectazurecdnlogs.FtpSource.Uri}" -FtpSourceUsername "#{Jobs.stats.collectazurecdnlogs.FtpSource.Username}" -FtpSourcePassword "#{Jobs.stats.collectazurecdnlogs.FtpSource.Password}" -AzureCdnAccountNumber "#{Jobs.stats.collectazurecdnlogs.AzureCdn.AccountNumber}" -AzureCdnPlatform "#{Jobs.stats.collectazurecdnlogs.AzureCdn.Platform}" -AzureCdnCloudStorageAccount "#{Jobs.stats.collectazurecdnlogs.AzureCdn.CloudStorageAccount}" -AzureCdnCloudStorageContainerName "#{Jobs.stats.collectazurecdnlogs.AzureCdn.CloudStorageContainerName}" -InstrumentationKey "#{Jobs.stats.collectazurecdnlogs.InstrumentationKey}" -verbose true -Interval #{Jobs.stats.collectazurecdnlogs.Interval} +start /w stats.collectazurecdnlogs.exe ^ +-Configuration "#{Jobs.stats.collectazurecdnlogs.Configuration}" ^ +-InstrumentationKey "#{Jobs.stats.collectazurecdnlogs.InstrumentationKey}" ^ +-verbose true ^ +-Interval #{Jobs.stats.collectazurecdnlogs.Interval} - echo "Finished #{Jobs.stats.collectazurecdnlogs.Title}" +echo "Finished #{Jobs.stats.collectazurecdnlogs.Title}" - goto Top \ No newline at end of file +goto Top \ No newline at end of file diff --git a/src/Stats.CollectAzureCdnLogs/Settings/dev.json b/src/Stats.CollectAzureCdnLogs/Settings/dev.json new file mode 100644 index 000000000..dba31e592 --- /dev/null +++ b/src/Stats.CollectAzureCdnLogs/Settings/dev.json @@ -0,0 +1,18 @@ +{ + "Initialization": { + "FtpSourceUri": "#{Jobs.stats.collectazurecdnlogs.FtpSource.Uri}", + "FtpSourceUsername": "#{Jobs.stats.collectazurecdnlogs.FtpSource.Username}", + "FtpSourcePassword": "#{Jobs.stats.collectazurecdnlogs.FtpSource.Password}", + "AzureCdnAccountNumber": "#{Jobs.stats.collectazurecdnlogs.AzureCdn.AccountNumber}", + "AzureCdnPlatform": "#{Jobs.stats.collectazurecdnlogs.AzureCdn.Platform}", + "AzureCdnCloudStorageAccount": "#{Jobs.stats.collectazurecdnlogs.AzureCdn.CloudStorageAccount}", + "AzureCdnCloudStorageContainerName": "#{Jobs.stats.collectazurecdnlogs.AzureCdn.CloudStorageContainerName}" + }, + + "KeyVault_VaultName": "#{Deployment.Azure.KeyVault.VaultName}", + "KeyVault_ClientId": "#{Deployment.Azure.KeyVault.ClientId}", + "KeyVault_CertificateThumbprint": "#{Deployment.Azure.KeyVault.CertificateThumbprint}", + "KeyVault_ValidateCertificate": true, + "KeyVault_StoreName": "My", + "KeyVault_StoreLocation": "LocalMachine" +} \ No newline at end of file diff --git a/src/Stats.CollectAzureCdnLogs/Settings/int.json b/src/Stats.CollectAzureCdnLogs/Settings/int.json new file mode 100644 index 000000000..dba31e592 --- /dev/null +++ b/src/Stats.CollectAzureCdnLogs/Settings/int.json @@ -0,0 +1,18 @@ +{ + "Initialization": { + "FtpSourceUri": "#{Jobs.stats.collectazurecdnlogs.FtpSource.Uri}", + "FtpSourceUsername": "#{Jobs.stats.collectazurecdnlogs.FtpSource.Username}", + "FtpSourcePassword": "#{Jobs.stats.collectazurecdnlogs.FtpSource.Password}", + "AzureCdnAccountNumber": "#{Jobs.stats.collectazurecdnlogs.AzureCdn.AccountNumber}", + "AzureCdnPlatform": "#{Jobs.stats.collectazurecdnlogs.AzureCdn.Platform}", + "AzureCdnCloudStorageAccount": "#{Jobs.stats.collectazurecdnlogs.AzureCdn.CloudStorageAccount}", + "AzureCdnCloudStorageContainerName": "#{Jobs.stats.collectazurecdnlogs.AzureCdn.CloudStorageContainerName}" + }, + + "KeyVault_VaultName": "#{Deployment.Azure.KeyVault.VaultName}", + "KeyVault_ClientId": "#{Deployment.Azure.KeyVault.ClientId}", + "KeyVault_CertificateThumbprint": "#{Deployment.Azure.KeyVault.CertificateThumbprint}", + "KeyVault_ValidateCertificate": true, + "KeyVault_StoreName": "My", + "KeyVault_StoreLocation": "LocalMachine" +} \ No newline at end of file diff --git a/src/Stats.CollectAzureCdnLogs/Settings/prod.json b/src/Stats.CollectAzureCdnLogs/Settings/prod.json new file mode 100644 index 000000000..dba31e592 --- /dev/null +++ b/src/Stats.CollectAzureCdnLogs/Settings/prod.json @@ -0,0 +1,18 @@ +{ + "Initialization": { + "FtpSourceUri": "#{Jobs.stats.collectazurecdnlogs.FtpSource.Uri}", + "FtpSourceUsername": "#{Jobs.stats.collectazurecdnlogs.FtpSource.Username}", + "FtpSourcePassword": "#{Jobs.stats.collectazurecdnlogs.FtpSource.Password}", + "AzureCdnAccountNumber": "#{Jobs.stats.collectazurecdnlogs.AzureCdn.AccountNumber}", + "AzureCdnPlatform": "#{Jobs.stats.collectazurecdnlogs.AzureCdn.Platform}", + "AzureCdnCloudStorageAccount": "#{Jobs.stats.collectazurecdnlogs.AzureCdn.CloudStorageAccount}", + "AzureCdnCloudStorageContainerName": "#{Jobs.stats.collectazurecdnlogs.AzureCdn.CloudStorageContainerName}" + }, + + "KeyVault_VaultName": "#{Deployment.Azure.KeyVault.VaultName}", + "KeyVault_ClientId": "#{Deployment.Azure.KeyVault.ClientId}", + "KeyVault_CertificateThumbprint": "#{Deployment.Azure.KeyVault.CertificateThumbprint}", + "KeyVault_ValidateCertificate": true, + "KeyVault_StoreName": "My", + "KeyVault_StoreLocation": "LocalMachine" +} \ No newline at end of file diff --git a/src/Stats.CollectAzureCdnLogs/Stats.CollectAzureCdnLogs.csproj b/src/Stats.CollectAzureCdnLogs/Stats.CollectAzureCdnLogs.csproj index 9af044a00..c96e6974b 100644 --- a/src/Stats.CollectAzureCdnLogs/Stats.CollectAzureCdnLogs.csproj +++ b/src/Stats.CollectAzureCdnLogs/Stats.CollectAzureCdnLogs.csproj @@ -49,6 +49,7 @@ + @@ -70,6 +71,9 @@ Designer + + + @@ -110,6 +114,7 @@ 7.1.2 + ..\..\build diff --git a/src/Stats.CollectAzureCdnLogs/Stats.CollectAzureCdnLogs.nuspec b/src/Stats.CollectAzureCdnLogs/Stats.CollectAzureCdnLogs.nuspec index 412a098df..6b97144bf 100644 --- a/src/Stats.CollectAzureCdnLogs/Stats.CollectAzureCdnLogs.nuspec +++ b/src/Stats.CollectAzureCdnLogs/Stats.CollectAzureCdnLogs.nuspec @@ -18,5 +18,7 @@ + + \ No newline at end of file diff --git a/src/Stats.CollectAzureChinaCDNLogs/ArgumentNames.cs b/src/Stats.CollectAzureChinaCDNLogs/ArgumentNames.cs deleted file mode 100644 index 1384c7a1d..000000000 --- a/src/Stats.CollectAzureChinaCDNLogs/ArgumentNames.cs +++ /dev/null @@ -1,16 +0,0 @@ -// 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. - -namespace Stats.CollectAzureChinaCDNLogs -{ - public class ArgumentNames - { - internal const string AzureAccountConnectionStringSource = "AzureAccountConnectionStringSource"; - internal const string AzureAccountConnectionStringDestination = "AzureAccountConnectionStringDestination"; - internal const string AzureContainerNameDestination = "AzureContainerNameDestination"; - internal const string AzureContainerNameSource = "AzureContainerNameSource"; - internal const string DestinationFilePrefix = "DestinationFilePrefix"; - //a timeout in seconds for an execution loop - internal const string ExecutionTimeoutInSeconds = "ExecutionTimeoutInSeconds"; - } -} diff --git a/src/Stats.CollectAzureChinaCDNLogs/Configuration/CollectAzureChinaCdnLogsConfiguration.cs b/src/Stats.CollectAzureChinaCDNLogs/Configuration/CollectAzureChinaCdnLogsConfiguration.cs new file mode 100644 index 000000000..267464209 --- /dev/null +++ b/src/Stats.CollectAzureChinaCDNLogs/Configuration/CollectAzureChinaCdnLogsConfiguration.cs @@ -0,0 +1,20 @@ +// 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. + +namespace Stats.CollectAzureChinaCDNLogs +{ + public class CollectAzureChinaCdnLogsConfiguration + { + public string AzureAccountConnectionStringSource { get; set; } + + public string AzureAccountConnectionStringDestination { get; set; } + + public string AzureContainerNameSource { get; set; } + + public string AzureContainerNameDestination { get; set; } + + public string DestinationFilePrefix { get; set; } + + public int? ExecutionTimeoutInSeconds { get; set; } + } +} diff --git a/src/Stats.CollectAzureChinaCDNLogs/Job.cs b/src/Stats.CollectAzureChinaCDNLogs/Job.cs index dc91f8acc..aafda542f 100644 --- a/src/Stats.CollectAzureChinaCDNLogs/Job.cs +++ b/src/Stats.CollectAzureChinaCDNLogs/Job.cs @@ -6,39 +6,47 @@ using System.ComponentModel.Design; using System.Threading; using System.Threading.Tasks; +using Autofac; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.WindowsAzure.Storage; using NuGet.Jobs; using Stats.AzureCdnLogs.Common.Collect; namespace Stats.CollectAzureChinaCDNLogs { - public class Job : JobBase + public class Job : JsonConfigurationJob { private const int DefaultExecutionTimeoutInSeconds = 14400; // 4 hours private const int MaxFilesToProcess = 4; - - private CloudStorageAccount _cloudStorageAccountSource; - private CloudStorageAccount _cloudStorageAccountDestination; - private string _cloudStorageContainerNameDestination; - private string _cloudStorageContainerNameSource; - private Collector _chinaCollector; + + private CollectAzureChinaCdnLogsConfiguration _configuration; private int _executionTimeoutInSeconds; - private string _destinationFilePrefix; + private Collector _chinaCollector; public override void Init(IServiceContainer serviceContainer, IDictionary jobArgsDictionary) { - var cloudStorageAccountConnStringSource = JobConfigurationManager.GetArgument(jobArgsDictionary, ArgumentNames.AzureAccountConnectionStringSource); - var cloudStorageAccountConnStringDest = JobConfigurationManager.GetArgument(jobArgsDictionary, ArgumentNames.AzureAccountConnectionStringDestination); - _cloudStorageAccountSource = ValidateAzureCloudStorageAccount(cloudStorageAccountConnStringSource); - _cloudStorageAccountDestination = ValidateAzureCloudStorageAccount(cloudStorageAccountConnStringDest); - _cloudStorageContainerNameDestination = JobConfigurationManager.GetArgument(jobArgsDictionary, ArgumentNames.AzureContainerNameDestination); - _cloudStorageContainerNameSource = JobConfigurationManager.GetArgument(jobArgsDictionary, ArgumentNames.AzureContainerNameSource); - _destinationFilePrefix = JobConfigurationManager.GetArgument(jobArgsDictionary, ArgumentNames.DestinationFilePrefix); - _executionTimeoutInSeconds = JobConfigurationManager.TryGetIntArgument(jobArgsDictionary, ArgumentNames.ExecutionTimeoutInSeconds) ?? DefaultExecutionTimeoutInSeconds; + base.Init(serviceContainer, jobArgsDictionary); + + InitializeJobConfiguration(_serviceProvider); + } + + public void InitializeJobConfiguration(IServiceProvider serviceProvider) + { + _configuration = serviceProvider.GetRequiredService>().Value; + _executionTimeoutInSeconds = _configuration.ExecutionTimeoutInSeconds ?? DefaultExecutionTimeoutInSeconds; + + var source = new AzureStatsLogSource( + ValidateAzureCloudStorageAccount(_configuration.AzureAccountConnectionStringSource), + _configuration.AzureContainerNameSource, + _executionTimeoutInSeconds / MaxFilesToProcess); + + var dest = new AzureStatsLogDestination( + ValidateAzureCloudStorageAccount(_configuration.AzureAccountConnectionStringDestination), + _configuration.AzureContainerNameDestination); - var source = new AzureStatsLogSource(cloudStorageAccountConnStringSource, _cloudStorageContainerNameSource, _executionTimeoutInSeconds/MaxFilesToProcess); - var dest = new AzureStatsLogDestination(cloudStorageAccountConnStringDest,_cloudStorageContainerNameDestination); _chinaCollector = new ChinaStatsCollector(source, dest); } @@ -47,7 +55,7 @@ public override async Task Run() CancellationTokenSource cts = new CancellationTokenSource(); cts.CancelAfter(_executionTimeoutInSeconds*1000); var aggregateExceptions = await _chinaCollector.TryProcessAsync(maxFileCount: MaxFilesToProcess, - fileNameTransform: s => $"{_destinationFilePrefix}_{s}", + fileNameTransform: s => $"{_configuration.DestinationFilePrefix}_{s}", sourceContentType: ContentType.GZip, destinationContentType: ContentType.GZip, token: cts.Token); @@ -80,5 +88,14 @@ private static CloudStorageAccount ValidateAzureCloudStorageAccount(string cloud } throw new ArgumentException("Job parameter for Azure CDN Cloud Storage Account is invalid."); } + + protected override void ConfigureAutofacServices(ContainerBuilder containerBuilder) + { + } + + protected override void ConfigureJobServices(IServiceCollection services, IConfigurationRoot configurationRoot) + { + ConfigureInitializationSection(services, configurationRoot); + } } } diff --git a/src/Stats.CollectAzureChinaCDNLogs/Scripts/Stats.CollectAzureChinaCDNLogs.cmd b/src/Stats.CollectAzureChinaCDNLogs/Scripts/Stats.CollectAzureChinaCDNLogs.cmd index 3ddf24d91..dc61f6cd4 100644 --- a/src/Stats.CollectAzureChinaCDNLogs/Scripts/Stats.CollectAzureChinaCDNLogs.cmd +++ b/src/Stats.CollectAzureChinaCDNLogs/Scripts/Stats.CollectAzureChinaCDNLogs.cmd @@ -3,24 +3,15 @@ cd bin :Top - echo "Starting job - #{Jobs.Stats.CollectAzureChinaCDNLogs.Title}" +echo "Starting job - #{Jobs.Stats.CollectAzureChinaCDNLogs.Title}" - title #{Jobs.Stats.CollectAzureChinaCDNLogs.Title} +title #{Jobs.Stats.CollectAzureChinaCDNLogs.Title} - start /w Stats.CollectAzureChinaCDNLogs.exe ^ - -VaultName "#{Deployment.Azure.KeyVault.VaultName}" ^ - -ClientId "#{Deployment.Azure.KeyVault.ClientId}" ^ - -CertificateThumbprint "#{Deployment.Azure.KeyVault.CertificateThumbprint}" ^ - -AzureAccountConnectionStringSource "#{Jobs.Stats.CollectAzureChinaCDNLogs.AzureAccountConnectionStringSource}" ^ - -AzureAccountConnectionStringDestination "#{Jobs.Stats.CollectAzureChinaCDNLogs.AzureAccountConnectionStringDestination}" ^ - -AzureContainerNameSource "#{Jobs.Stats.CollectAzureChinaCDNLogs.AzureContainerNameSource}" ^ - -AzureContainerNameDestination "#{Jobs.Stats.CollectAzureChinaCDNLogs.AzureContainerNameDestination}" ^ - -DestinationFilePrefix "#{Jobs.Stats.CollectAzureChinaCDNLogs.DestinationFilePrefix}" ^ - -ExecutionTimeoutInSeconds "#{Jobs.Stats.CollectAzureChinaCDNLogs.ExecutionTimeoutInSeconds}" ^ - -InstrumentationKey "#{Jobs.Stats.CollectAzureChinaCDNLogs.InstrumentationKey}" ^ - -verbose true ^ - -Interval #{Jobs.Stats.CollectAzureChinaCDNLogs.Interval} +start /w Stats.CollectAzureChinaCDNLogs.exe ^ +-InstrumentationKey "#{Jobs.Stats.CollectAzureChinaCDNLogs.InstrumentationKey}" ^ +-verbose true ^ +-Interval #{Jobs.Stats.CollectAzureChinaCDNLogs.Interval} - echo "Finished #{Jobs.Stats.CollectAzureChinaCDNLogs.Title}" +echo "Finished #{Jobs.Stats.CollectAzureChinaCDNLogs.Title}" - goto Top \ No newline at end of file +goto Top \ No newline at end of file diff --git a/src/Stats.CollectAzureChinaCDNLogs/Settings/dev.json b/src/Stats.CollectAzureChinaCDNLogs/Settings/dev.json new file mode 100644 index 000000000..f143fedd7 --- /dev/null +++ b/src/Stats.CollectAzureChinaCDNLogs/Settings/dev.json @@ -0,0 +1,17 @@ +{ + "Initialization": { + "AzureAccountConnectionStringSource": "#{Jobs.Stats.CollectAzureChinaCDNLogs.AzureAccountConnectionStringSource}", + "AzureAccountConnectionStringDestination": "#{Jobs.Stats.CollectAzureChinaCDNLogs.AzureAccountConnectionStringDestination}", + "AzureContainerNameSource": "#{Jobs.Stats.CollectAzureChinaCDNLogs.AzureContainerNameSource}", + "AzureContainerNameDestination": "#{Jobs.Stats.CollectAzureChinaCDNLogs.AzureContainerNameDestination}", + "DestinationFilePrefix": "#{Jobs.Stats.CollectAzureChinaCDNLogs.DestinationFilePrefix}", + "ExecutionTimeoutInSeconds": "#{Jobs.Stats.CollectAzureChinaCDNLogs.ExecutionTimeoutInSeconds}" + }, + + "KeyVault_VaultName": "#{Deployment.Azure.KeyVault.VaultName}", + "KeyVault_ClientId": "#{Deployment.Azure.KeyVault.ClientId}", + "KeyVault_CertificateThumbprint": "#{Deployment.Azure.KeyVault.CertificateThumbprint}", + "KeyVault_ValidateCertificate": true, + "KeyVault_StoreName": "My", + "KeyVault_StoreLocation": "LocalMachine" +} \ No newline at end of file diff --git a/src/Stats.CollectAzureChinaCDNLogs/Settings/int.json b/src/Stats.CollectAzureChinaCDNLogs/Settings/int.json new file mode 100644 index 000000000..f143fedd7 --- /dev/null +++ b/src/Stats.CollectAzureChinaCDNLogs/Settings/int.json @@ -0,0 +1,17 @@ +{ + "Initialization": { + "AzureAccountConnectionStringSource": "#{Jobs.Stats.CollectAzureChinaCDNLogs.AzureAccountConnectionStringSource}", + "AzureAccountConnectionStringDestination": "#{Jobs.Stats.CollectAzureChinaCDNLogs.AzureAccountConnectionStringDestination}", + "AzureContainerNameSource": "#{Jobs.Stats.CollectAzureChinaCDNLogs.AzureContainerNameSource}", + "AzureContainerNameDestination": "#{Jobs.Stats.CollectAzureChinaCDNLogs.AzureContainerNameDestination}", + "DestinationFilePrefix": "#{Jobs.Stats.CollectAzureChinaCDNLogs.DestinationFilePrefix}", + "ExecutionTimeoutInSeconds": "#{Jobs.Stats.CollectAzureChinaCDNLogs.ExecutionTimeoutInSeconds}" + }, + + "KeyVault_VaultName": "#{Deployment.Azure.KeyVault.VaultName}", + "KeyVault_ClientId": "#{Deployment.Azure.KeyVault.ClientId}", + "KeyVault_CertificateThumbprint": "#{Deployment.Azure.KeyVault.CertificateThumbprint}", + "KeyVault_ValidateCertificate": true, + "KeyVault_StoreName": "My", + "KeyVault_StoreLocation": "LocalMachine" +} \ No newline at end of file diff --git a/src/Stats.CollectAzureChinaCDNLogs/Settings/prod.json b/src/Stats.CollectAzureChinaCDNLogs/Settings/prod.json new file mode 100644 index 000000000..f143fedd7 --- /dev/null +++ b/src/Stats.CollectAzureChinaCDNLogs/Settings/prod.json @@ -0,0 +1,17 @@ +{ + "Initialization": { + "AzureAccountConnectionStringSource": "#{Jobs.Stats.CollectAzureChinaCDNLogs.AzureAccountConnectionStringSource}", + "AzureAccountConnectionStringDestination": "#{Jobs.Stats.CollectAzureChinaCDNLogs.AzureAccountConnectionStringDestination}", + "AzureContainerNameSource": "#{Jobs.Stats.CollectAzureChinaCDNLogs.AzureContainerNameSource}", + "AzureContainerNameDestination": "#{Jobs.Stats.CollectAzureChinaCDNLogs.AzureContainerNameDestination}", + "DestinationFilePrefix": "#{Jobs.Stats.CollectAzureChinaCDNLogs.DestinationFilePrefix}", + "ExecutionTimeoutInSeconds": "#{Jobs.Stats.CollectAzureChinaCDNLogs.ExecutionTimeoutInSeconds}" + }, + + "KeyVault_VaultName": "#{Deployment.Azure.KeyVault.VaultName}", + "KeyVault_ClientId": "#{Deployment.Azure.KeyVault.ClientId}", + "KeyVault_CertificateThumbprint": "#{Deployment.Azure.KeyVault.CertificateThumbprint}", + "KeyVault_ValidateCertificate": true, + "KeyVault_StoreName": "My", + "KeyVault_StoreLocation": "LocalMachine" +} \ No newline at end of file diff --git a/src/Stats.CollectAzureChinaCDNLogs/Stats.CollectAzureChinaCDNLogs.csproj b/src/Stats.CollectAzureChinaCDNLogs/Stats.CollectAzureChinaCDNLogs.csproj index c509d4c1d..c2e867581 100644 --- a/src/Stats.CollectAzureChinaCDNLogs/Stats.CollectAzureChinaCDNLogs.csproj +++ b/src/Stats.CollectAzureChinaCDNLogs/Stats.CollectAzureChinaCDNLogs.csproj @@ -42,8 +42,8 @@ - + @@ -55,6 +55,10 @@ + + + + diff --git a/src/Stats.CollectAzureChinaCDNLogs/Stats.CollectAzureChinaCDNLogs.nuspec b/src/Stats.CollectAzureChinaCDNLogs/Stats.CollectAzureChinaCDNLogs.nuspec index 99b00aeda..a483014e2 100644 --- a/src/Stats.CollectAzureChinaCDNLogs/Stats.CollectAzureChinaCDNLogs.nuspec +++ b/src/Stats.CollectAzureChinaCDNLogs/Stats.CollectAzureChinaCDNLogs.nuspec @@ -16,5 +16,7 @@ + + \ No newline at end of file diff --git a/tests/Tests.Stats.CollectAzureCdnLogs/JobTests.cs b/tests/Tests.Stats.CollectAzureCdnLogs/JobTests.cs index de30880a0..25692967c 100644 --- a/tests/Tests.Stats.CollectAzureCdnLogs/JobTests.cs +++ b/tests/Tests.Stats.CollectAzureCdnLogs/JobTests.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Generic; using System.ComponentModel.Design; +using Microsoft.Extensions.Options; +using Moq; using Stats.CollectAzureCdnLogs; using Xunit; @@ -30,115 +32,95 @@ public void InitFailsWhenEmptyArguments() } [Fact] - public void InitSucceedsWhenValidArguments() + public void InitSucceedsWhenValidConfiguration() { - var jobArgsDictionary = CreateValidJobArgsDictionary(); - var job = new Job(); - job.Init(ServiceContainer, jobArgsDictionary); - } - - [Theory] - [InlineData("")] - [InlineData(null)] - [InlineData("http://localhost")] - [InlineData("ftps://someserver/folder")] - [InlineData("ftp://")] - public void InitFailsForInvalidFtpServerUri(string serverUri) - { - var jobArgsDictionary = CreateValidJobArgsDictionary(); - jobArgsDictionary["FtpSourceUri"] = serverUri; - - var job = new Job(); - Assert.ThrowsAny(() => job.Init(ServiceContainer, jobArgsDictionary)); - } + var configuration = GetDefaultConfiguration(); - [Theory] - [InlineData("")] - [InlineData(null)] - public void InitFailsForMissingFtpUsername(string username) - { - var jobArgsDictionary = CreateValidJobArgsDictionary(); - jobArgsDictionary["FtpSourceUsername"] = username; - - var job = new Job(); - Assert.ThrowsAny(() => job.Init(ServiceContainer, jobArgsDictionary)); + job.InitializeJobConfiguration(GetMockServiceProvider(configuration)); } [Theory] - [InlineData("")] - [InlineData(null)] - public void InitFailsForMissingFtpPassword(string password) + // null values + [InlineData("AzureCdnAccountNumber", null, typeof(ArgumentException))] + [InlineData("AzureCdnCloudStorageAccount", null, typeof(ArgumentException))] + [InlineData("AzureCdnCloudStorageContainerName", null, typeof(ArgumentException))] + [InlineData("AzureCdnPlatform", null, typeof(ArgumentException))] + [InlineData("FtpSourceUri", null, typeof(ArgumentException))] + [InlineData("FtpSourceUsername", null, typeof(ArgumentException))] + [InlineData("FtpSourcePassword", null, typeof(ArgumentException))] + // empty values + [InlineData("AzureCdnAccountNumber", "", typeof(ArgumentException))] + [InlineData("AzureCdnCloudStorageAccount", "", typeof(ArgumentException))] + [InlineData("AzureCdnCloudStorageContainerName", "", typeof(ArgumentException))] + [InlineData("AzureCdnPlatform", "", typeof(ArgumentException))] + [InlineData("FtpSourceUri", "", typeof(ArgumentException))] + [InlineData("FtpSourceUsername", "", typeof(ArgumentException))] + [InlineData("FtpSourcePassword", "", typeof(ArgumentException))] + // invalid values + [InlineData("FtpSourceUri", "http://localhost", typeof(UriFormatException))] + [InlineData("FtpSourceUri", "ftps://someserver/folder", typeof(UriFormatException))] + [InlineData("FtpSourceUri", "ftp://", typeof(UriFormatException))] + [InlineData("AzureCdnPlatform", "bla", typeof(ArgumentException))] + [InlineData("AzureCdnCloudStorageAccount", "bla", typeof(ArgumentException))] + public void InitFailsWhenInvalidConfiguration(string property, object value, Type exceptionType) { - var jobArgsDictionary = CreateValidJobArgsDictionary(); - jobArgsDictionary["FtpSourcePassword"] = password; - var job = new Job(); - Assert.ThrowsAny(() => job.Init(ServiceContainer, jobArgsDictionary)); - } - - [Theory] - [InlineData("")] - [InlineData(null)] - [InlineData("bla")] - public void InitFailsForMissingOrInvalidAzureCdnPlatform(string platform) - { - var jobArgsDictionary = CreateValidJobArgsDictionary(); - jobArgsDictionary["AzureCdnPlatform"] = platform; + var configuration = GetModifiedConfiguration(property, value); - var job = new Job(); - Assert.ThrowsAny(() => job.Init(ServiceContainer, jobArgsDictionary)); + Assert.Throws(exceptionType, () => job.InitializeJobConfiguration(GetMockServiceProvider(configuration))); } - [Theory] - [InlineData("")] - [InlineData(null)] - public void InitFailsForMissingAzureCdnAccountNumber(string accountNumber) + private static CollectAzureCdnLogsConfiguration GetModifiedConfiguration(string property, object value) { - var jobArgsDictionary = CreateValidJobArgsDictionary(); - jobArgsDictionary["AzureCdnAccountNumber"] = accountNumber; - - var job = new Job(); - Assert.ThrowsAny(() => job.Init(ServiceContainer, jobArgsDictionary)); - } + var configuration = GetDefaultConfiguration(); - [Theory] - [InlineData("")] - [InlineData(null)] - [InlineData("bla")] - public void InitFailsForMissingOrInvalidAzureCdnCloudStorageAccount(string cloudStorageAccount) - { - var jobArgsDictionary = CreateValidJobArgsDictionary(); - jobArgsDictionary["AzureCdnCloudStorageAccount"] = cloudStorageAccount; + typeof(CollectAzureCdnLogsConfiguration) + .GetProperty(property) + .SetValue(configuration, value); - var job = new Job(); - Assert.ThrowsAny(() => job.Init(ServiceContainer, jobArgsDictionary)); + return configuration; } - [Theory] - [InlineData("")] - [InlineData(null)] - public void InitFailsForMissingAzureCdnCloudStorageContainerName(string containerName) + private static CollectAzureCdnLogsConfiguration GetDefaultConfiguration() { - var jobArgsDictionary = CreateValidJobArgsDictionary(); - jobArgsDictionary["AzureCdnCloudStorageContainerName"] = containerName; - - var job = new Job(); - Assert.ThrowsAny(() => job.Init(ServiceContainer, jobArgsDictionary)); + return new CollectAzureCdnLogsConfiguration + { + AzureCdnAccountNumber = "AA00", + AzureCdnCloudStorageAccount = "UseDevelopmentStorage=true;", + AzureCdnCloudStorageContainerName = "cdnLogs", + AzureCdnPlatform = "HttpLargeObject", + FtpSourceUri = "ftp://someserver/logFolder", + FtpSourceUsername = @"domain\alias", + FtpSourcePassword = "secret" + }; } - private static Dictionary CreateValidJobArgsDictionary() + private static IServiceProvider GetMockServiceProvider(CollectAzureCdnLogsConfiguration configuration) { - var jobArgsDictionary = new Dictionary(); - jobArgsDictionary.Add("FtpSourceUri", "ftp://someserver/logFolder"); - jobArgsDictionary.Add("FtpSourceUsername", @"domain\alias"); - jobArgsDictionary.Add("FtpSourcePassword", "secret"); - jobArgsDictionary.Add("AzureCdnPlatform", "HttpLargeObject"); - jobArgsDictionary.Add("AzureCdnAccountNumber", "AA00"); - jobArgsDictionary.Add("AzureCdnCloudStorageAccount", "UseDevelopmentStorage=true;"); - jobArgsDictionary.Add("AzureCdnCloudStorageContainerName", "cdnLogs"); - - return jobArgsDictionary; + var mockOptionsSnapshot = new Mock>(); + + mockOptionsSnapshot + .Setup(x => x.Value) + .Returns(configuration); + + var mockProvider = new Mock(); + + mockProvider + .Setup(sp => sp.GetService(It.IsAny())) + .Returns(serviceType => + { + if (serviceType == typeof(IOptionsSnapshot)) + { + return mockOptionsSnapshot.Object; + } + else + { + throw new InvalidOperationException($"Unexpected service lookup: {serviceType.Name}"); + } + }); + + return mockProvider.Object; } } } \ No newline at end of file diff --git a/tests/Tests.Stats.CollectAzureCdnLogs/Tests.Stats.CollectAzureCdnLogs.csproj b/tests/Tests.Stats.CollectAzureCdnLogs/Tests.Stats.CollectAzureCdnLogs.csproj index 271a8e906..f51817c41 100644 --- a/tests/Tests.Stats.CollectAzureCdnLogs/Tests.Stats.CollectAzureCdnLogs.csproj +++ b/tests/Tests.Stats.CollectAzureCdnLogs/Tests.Stats.CollectAzureCdnLogs.csproj @@ -68,6 +68,9 @@ + + 4.7.145 + 2.3.1 diff --git a/tests/Tests.Stats.CollectAzureChinaCDNLogs/JobTests.cs b/tests/Tests.Stats.CollectAzureChinaCDNLogs/JobTests.cs index 4a89a6559..0ba32657a 100644 --- a/tests/Tests.Stats.CollectAzureChinaCDNLogs/JobTests.cs +++ b/tests/Tests.Stats.CollectAzureChinaCDNLogs/JobTests.cs @@ -4,7 +4,9 @@ using System; using System.Collections.Generic; using System.ComponentModel.Design; +using Microsoft.Extensions.Options; using Microsoft.WindowsAzure.Storage; +using Moq; using Stats.CollectAzureChinaCDNLogs; using Xunit; @@ -12,7 +14,7 @@ namespace Tests.Stats.CollectAzureChinaCDNLogs { public class JobTests { - private static IServiceContainer ServiceContainer = new ServiceContainer(); + private static Mock MockServiceContainer = new Mock(); [Fact] public void InitFailsWhenEmptyArguments() @@ -20,45 +22,88 @@ public void InitFailsWhenEmptyArguments() var jobArgsDictionary = new Dictionary(); var job = new Job(); - Assert.ThrowsAny(() => job.Init(ServiceContainer, jobArgsDictionary)); + Assert.ThrowsAny(() => job.Init(MockServiceContainer.Object, jobArgsDictionary)); } [Fact] public void InitFailsWithInvalidAccount() { - var jobArgsDictionary = CreateValidJobArgsDictionary(); - var job = new Job(); - Assert.ThrowsAny(() => job.Init(ServiceContainer, jobArgsDictionary)); + var configuration = GetDefaultConfiguration(); + + Assert.ThrowsAny(() => job.InitializeJobConfiguration(GetMockServiceProvider(configuration))); } [Theory] - [InlineData("AzureAccountConStringSource")] - [InlineData("AzureAccountConStringDest")] - [InlineData("AzureContainerNameDest")] - [InlineData("AzureContainerNameSource")] - [InlineData("DestinationFilePrefix")] - public void InitMissingArgArguments(string keyToRemove) + // null values + [InlineData("AzureAccountConnectionStringSource", null, typeof(ArgumentException))] + [InlineData("AzureAccountConnectionStringDestination", null, typeof(ArgumentException))] + [InlineData("AzureContainerNameDestination", null, typeof(ArgumentNullException))] + [InlineData("AzureContainerNameSource", null, typeof(ArgumentNullException))] + [InlineData("DestinationFilePrefix", null, typeof(StorageException))] + // empty values + [InlineData("AzureAccountConnectionStringSource", "", typeof(ArgumentException))] + [InlineData("AzureAccountConnectionStringDestination", "", typeof(ArgumentException))] + [InlineData("AzureContainerNameDestination", "", typeof(ArgumentException))] + [InlineData("AzureContainerNameSource", "", typeof(ArgumentException))] + [InlineData("DestinationFilePrefix", "", typeof(StorageException))] + public void InitMissingArgArguments(string property, object value, Type exceptionType) { - var jobArgsDictionary = CreateValidJobArgsDictionary(); - jobArgsDictionary.Remove(keyToRemove); var job = new Job(); - Assert.ThrowsAny(() => job.Init(ServiceContainer, jobArgsDictionary)); + var configuration = GetModifiedConfiguration(property, value); + + Assert.Throws(exceptionType, () => job.InitializeJobConfiguration(GetMockServiceProvider(configuration))); } + private static CollectAzureChinaCdnLogsConfiguration GetModifiedConfiguration(string property, object value) + { + var configuration = GetDefaultConfiguration(); + + typeof(CollectAzureChinaCdnLogsConfiguration) + .GetProperty(property) + .SetValue(configuration, value); + + return configuration; + } - private static Dictionary CreateValidJobArgsDictionary() + private static CollectAzureChinaCdnLogsConfiguration GetDefaultConfiguration() { - var jobArgsDictionary = new Dictionary(); - jobArgsDictionary.Add("AzureAccountConnectionStringSource", "DefaultEndpointsProtocol=https;AccountName=name;AccountKey=cdummy4aadummyAAWhdummyAdummyA6A+dummydoAdummyJqdummymnm+H+2dummyA/dummygdummyqdummyKK==;EndpointSuffix=core.chinacloudapi.cn"); - jobArgsDictionary.Add("AzureAccountConnectionStringDestination", "DefaultEndpointsProtocol=https;AccountName=name;AccountKey=cdummy4aadummyAAWhdummyAdummyA6A+dummydoAdummyJqdummymnm+H+2dummyA/dummygdummyqdummyKK==;EndpointSuffix=core.windows.net"); - jobArgsDictionary.Add("AzureContainerNameDestination", "DestContainer"); - jobArgsDictionary.Add("AzureContainerNameSource", "SourceContainer"); - jobArgsDictionary.Add("DestinationFilePrefix", "SomePrfix"); - jobArgsDictionary.Add("ExecutionTimeoutInSeconds", "60"); - - return jobArgsDictionary; + return new CollectAzureChinaCdnLogsConfiguration + { + AzureAccountConnectionStringSource = "DefaultEndpointsProtocol=https;AccountName=name;AccountKey=cdummy4aadummyAAWhdummyAdummyA6A+dummydoAdummyJqdummymnm+H+2dummyA/dummygdummyqdummyKK==;EndpointSuffix=core.chinacloudapi.cn", + AzureAccountConnectionStringDestination = "DefaultEndpointsProtocol=https;AccountName=name;AccountKey=cdummy4aadummyAAWhdummyAdummyA6A+dummydoAdummyJqdummymnm+H+2dummyA/dummygdummyqdummyKK==;EndpointSuffix=core.windows.net", + AzureContainerNameDestination = "DestContainer", + AzureContainerNameSource = "SourceContainer", + DestinationFilePrefix = "SomePrfix", + ExecutionTimeoutInSeconds = 60 + }; } + private static IServiceProvider GetMockServiceProvider(CollectAzureChinaCdnLogsConfiguration configuration) + { + var mockOptionsSnapshot = new Mock>(); + + mockOptionsSnapshot + .Setup(x => x.Value) + .Returns(configuration); + + var mockProvider = new Mock(); + + mockProvider + .Setup(sp => sp.GetService(It.IsAny())) + .Returns(serviceType => + { + if (serviceType == typeof(IOptionsSnapshot)) + { + return mockOptionsSnapshot.Object; + } + else + { + throw new InvalidOperationException($"Unexpected service lookup: {serviceType.Name}"); + } + }); + + return mockProvider.Object; + } } } diff --git a/tests/Tests.Stats.CollectAzureChinaCDNLogs/Tests.Stats.CollectAzureChinaCDNLogs.csproj b/tests/Tests.Stats.CollectAzureChinaCDNLogs/Tests.Stats.CollectAzureChinaCDNLogs.csproj index 69a33bb4e..4643530ac 100644 --- a/tests/Tests.Stats.CollectAzureChinaCDNLogs/Tests.Stats.CollectAzureChinaCDNLogs.csproj +++ b/tests/Tests.Stats.CollectAzureChinaCDNLogs/Tests.Stats.CollectAzureChinaCDNLogs.csproj @@ -79,6 +79,9 @@ 5.7.0 + + 4.7.145 + 2.3.1 From e52177331d0f1bb11a4d7a0889387874a28d36f2 Mon Sep 17 00:00:00 2001 From: Christy Henriksson Date: Thu, 29 Nov 2018 08:23:20 -0800 Subject: [PATCH 4/7] formatting fixes --- src/Stats.CollectAzureCdnLogs/Job.cs | 2 -- .../Scripts/Stats.CollectAzureCdnLogs.cmd | 8 ++++---- .../Scripts/Stats.CollectAzureChinaCDNLogs.cmd | 6 +++--- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/Stats.CollectAzureCdnLogs/Job.cs b/src/Stats.CollectAzureCdnLogs/Job.cs index 14d69ee93..97403043d 100644 --- a/src/Stats.CollectAzureCdnLogs/Job.cs +++ b/src/Stats.CollectAzureCdnLogs/Job.cs @@ -64,9 +64,7 @@ public void InitializeJobConfiguration(IServiceProvider serviceProvider) } _cloudStorageAccount = ValidateAzureCloudStorageAccount(_configuration.AzureCdnCloudStorageAccount); - _azureCdnPlatform = ValidateAzureCdnPlatform(_configuration.AzureCdnPlatform); - _ftpServerUri = ValidateFtpUri(_configuration.FtpSourceUri); } diff --git a/src/Stats.CollectAzureCdnLogs/Scripts/Stats.CollectAzureCdnLogs.cmd b/src/Stats.CollectAzureCdnLogs/Scripts/Stats.CollectAzureCdnLogs.cmd index bda6afb0a..479c5b8f5 100644 --- a/src/Stats.CollectAzureCdnLogs/Scripts/Stats.CollectAzureCdnLogs.cmd +++ b/src/Stats.CollectAzureCdnLogs/Scripts/Stats.CollectAzureCdnLogs.cmd @@ -8,10 +8,10 @@ echo "Starting job - #{Jobs.stats.collectazurecdnlogs.Title}" title #{Jobs.stats.collectazurecdnlogs.Title} start /w stats.collectazurecdnlogs.exe ^ --Configuration "#{Jobs.stats.collectazurecdnlogs.Configuration}" ^ --InstrumentationKey "#{Jobs.stats.collectazurecdnlogs.InstrumentationKey}" ^ --verbose true ^ --Interval #{Jobs.stats.collectazurecdnlogs.Interval} + -Configuration "#{Jobs.stats.collectazurecdnlogs.Configuration}" ^ + -InstrumentationKey "#{Jobs.stats.collectazurecdnlogs.InstrumentationKey}" ^ + -verbose true ^ + -Interval #{Jobs.stats.collectazurecdnlogs.Interval} echo "Finished #{Jobs.stats.collectazurecdnlogs.Title}" diff --git a/src/Stats.CollectAzureChinaCDNLogs/Scripts/Stats.CollectAzureChinaCDNLogs.cmd b/src/Stats.CollectAzureChinaCDNLogs/Scripts/Stats.CollectAzureChinaCDNLogs.cmd index dc61f6cd4..e4b70026d 100644 --- a/src/Stats.CollectAzureChinaCDNLogs/Scripts/Stats.CollectAzureChinaCDNLogs.cmd +++ b/src/Stats.CollectAzureChinaCDNLogs/Scripts/Stats.CollectAzureChinaCDNLogs.cmd @@ -8,9 +8,9 @@ echo "Starting job - #{Jobs.Stats.CollectAzureChinaCDNLogs.Title}" title #{Jobs.Stats.CollectAzureChinaCDNLogs.Title} start /w Stats.CollectAzureChinaCDNLogs.exe ^ --InstrumentationKey "#{Jobs.Stats.CollectAzureChinaCDNLogs.InstrumentationKey}" ^ --verbose true ^ --Interval #{Jobs.Stats.CollectAzureChinaCDNLogs.Interval} + -InstrumentationKey "#{Jobs.Stats.CollectAzureChinaCDNLogs.InstrumentationKey}" ^ + -verbose true ^ + -Interval #{Jobs.Stats.CollectAzureChinaCDNLogs.Interval} echo "Finished #{Jobs.Stats.CollectAzureChinaCDNLogs.Title}" From 3f59f2068345775da106bf8a824d61b3fd728bdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Sharma?= Date: Thu, 29 Nov 2018 13:28:24 -0800 Subject: [PATCH 5/7] Repository sign packages owned by users with "malformed" usernames (#686) Repository sign packages that are owned by usernames with "malformed" usernames. Addresses https://github.com/NuGet/Engineering/issues/1582 and https://github.com/NuGet/Engineering/issues/1592. --- ...et.Services.Validation.Orchestrator.csproj | 1 - .../PackageSignatureValidator.cs | 41 ------------------- .../ScanAndSign/ScanAndSignProcessor.cs | 17 +------- .../UsernameHelper.cs | 21 ---------- .../PackageSignatureValidatorFacts.cs | 40 ------------------ .../ScanAndSignProcessorFacts.cs | 33 --------------- 6 files changed, 2 insertions(+), 151 deletions(-) delete mode 100644 src/NuGet.Services.Validation.Orchestrator/UsernameHelper.cs diff --git a/src/NuGet.Services.Validation.Orchestrator/NuGet.Services.Validation.Orchestrator.csproj b/src/NuGet.Services.Validation.Orchestrator/NuGet.Services.Validation.Orchestrator.csproj index 6579fb5b2..c41b271a9 100644 --- a/src/NuGet.Services.Validation.Orchestrator/NuGet.Services.Validation.Orchestrator.csproj +++ b/src/NuGet.Services.Validation.Orchestrator/NuGet.Services.Validation.Orchestrator.csproj @@ -69,7 +69,6 @@ - diff --git a/src/NuGet.Services.Validation.Orchestrator/PackageSigning/ProcessSignature/PackageSignatureValidator.cs b/src/NuGet.Services.Validation.Orchestrator/PackageSigning/ProcessSignature/PackageSignatureValidator.cs index 656bf21bf..150eb72d1 100644 --- a/src/NuGet.Services.Validation.Orchestrator/PackageSigning/ProcessSignature/PackageSignatureValidator.cs +++ b/src/NuGet.Services.Validation.Orchestrator/PackageSigning/ProcessSignature/PackageSignatureValidator.cs @@ -93,20 +93,6 @@ private IValidationResult Validate(IValidationRequest request, IValidationResult return ValidationResult.Succeeded; } - // TODO: Remove this. - // See: https://github.com/NuGet/Engineering/issues/1592 - if (HasOwnerWithInvalidUsername(request)) - { - _logger.LogWarning( - "Ignoring invalid validation result in package signature validator as the package has an owner with an invalid username. " + - "Status = {ValidationStatus}, Nupkg URL = {NupkgUrl}, validation issues = {Issues}", - result.Status, - result.NupkgUrl, - result.Issues.Select(i => i.IssueCode)); - - return ValidationResult.Succeeded; - } - _logger.LogCritical( "Unexpected validation result in package signature validator. This may be caused by an invalid repository " + "signature. Throwing an exception to force this validation to dead-letter. " + @@ -133,32 +119,5 @@ private IValidationResult Validate(IValidationRequest request, IValidationResult return result; } - - private bool HasOwnerWithInvalidUsername(IValidationRequest request) - { - var registration = _packages.FindPackageRegistrationById(request.PackageId); - - if (registration == null) - { - _logger.LogError("Attempted to validate package that has no package registration"); - - throw new InvalidOperationException($"Registration for package id {request.PackageId} does not exist"); - } - - var owners = registration.Owners.Select(o => o.Username).ToList(); - - if (owners.Any(UsernameHelper.IsInvalid)) - { - _logger.LogWarning( - "Package {PackageId} {PackageVersion} has an owner with an invalid username. {Owners}", - request.PackageId, - request.PackageVersion, - owners); - - return true; - } - - return false; - } } } \ No newline at end of file diff --git a/src/NuGet.Services.Validation.Orchestrator/PackageSigning/ScanAndSign/ScanAndSignProcessor.cs b/src/NuGet.Services.Validation.Orchestrator/PackageSigning/ScanAndSign/ScanAndSignProcessor.cs index 69c14824d..cbaac7c03 100644 --- a/src/NuGet.Services.Validation.Orchestrator/PackageSigning/ScanAndSign/ScanAndSignProcessor.cs +++ b/src/NuGet.Services.Validation.Orchestrator/PackageSigning/ScanAndSign/ScanAndSignProcessor.cs @@ -134,7 +134,7 @@ public async Task StartAsync(IValidationRequest request) var owners = FindPackageOwners(request); - if (await ShouldRepositorySignAsync(request, owners)) + if (await ShouldRepositorySignAsync(request)) { _logger.LogInformation( "Repository signing {PackageId} {PackageVersion} with {ServiceIndex} and {Owners}", @@ -198,7 +198,7 @@ private bool ShouldSkipScan(IValidationRequest request) return false; } - private async Task ShouldRepositorySignAsync(IValidationRequest request, List owners) + private async Task ShouldRepositorySignAsync(IValidationRequest request) { var hasRepositorySignature = await _validationContext .PackageSignatures @@ -216,19 +216,6 @@ private async Task ShouldRepositorySignAsync(IValidationRequest request, L return false; } - // TODO: Remove this check. - // See: https://github.com/NuGet/Engineering/issues/1582 - if (owners.Any(UsernameHelper.IsInvalid)) - { - _logger.LogWarning( - "Package {PackageId} {PackageVersion} has an owner with an invalid username. Scanning instead of signing. {Owners}", - request.PackageId, - request.PackageVersion, - owners); - - return false; - } - return true; } diff --git a/src/NuGet.Services.Validation.Orchestrator/UsernameHelper.cs b/src/NuGet.Services.Validation.Orchestrator/UsernameHelper.cs deleted file mode 100644 index 9c22e6235..000000000 --- a/src/NuGet.Services.Validation.Orchestrator/UsernameHelper.cs +++ /dev/null @@ -1,21 +0,0 @@ -// 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. - -using System; -using System.Text.RegularExpressions; - -namespace NuGet.Services.Validation.Orchestrator -{ - // TODO: Remove this type. - // See: https://github.com/NuGet/Engineering/issues/1582 - // See: https://github.com/NuGet/Engineering/issues/1592 - public static class UsernameHelper - { - private const string UsernameRegex = @"^[A-Za-z0-9][A-Za-z0-9_\.-]+[A-Za-z0-9]$"; - - public static bool IsInvalid(string username) - { - return !Regex.IsMatch(username, UsernameRegex, RegexOptions.None, TimeSpan.FromSeconds(5)); - } - } -} diff --git a/tests/NuGet.Services.Validation.Orchestrator.Tests/PackageSigning/ProcessSignature/PackageSignatureValidatorFacts.cs b/tests/NuGet.Services.Validation.Orchestrator.Tests/PackageSigning/ProcessSignature/PackageSignatureValidatorFacts.cs index 2aaa391f1..ad7b25853 100644 --- a/tests/NuGet.Services.Validation.Orchestrator.Tests/PackageSigning/ProcessSignature/PackageSignatureValidatorFacts.cs +++ b/tests/NuGet.Services.Validation.Orchestrator.Tests/PackageSigning/ProcessSignature/PackageSignatureValidatorFacts.cs @@ -153,37 +153,6 @@ public async Task WhenRepositorySigningEnabled_ThrowsExceptionIfPackageIsModifie Assert.Equal("Package signature validator has an unexpected validation result", e.Message); } - [Fact] - public async Task WhenRepositorySigningEnabled_IgnoresFailedValidationIfOwnerHasMalformedUsername() - { - // Arrange - _config.RepositorySigningEnabled = true; - - _packages - .Setup(p => p.FindPackageRegistrationById(_validationRequest.Object.PackageId)) - .Returns(_packageRegistrationWithBadUsername); - - _validatorStateService - .Setup(x => x.GetStatusAsync(It.IsAny())) - .ReturnsAsync(new ValidatorStatus - { - ValidationId = ValidationId, - PackageKey = PackageKey, - ValidatorName = ValidatorName.PackageSignatureProcessor, - State = ValidationStatus.Failed, - ValidatorIssues = new List(), - }); - - // Act - var result = await _target.GetResultAsync(_validationRequest.Object); - - // Assert - Assert.Null(result.NupkgUrl); - Assert.Empty(result.Issues); - Assert.Equal(ValidationStatus.Succeeded, result.Status); - - } - public static IEnumerable PossibleValidationStatuses => possibleValidationStatuses.Select(s => new object[] { s }); } @@ -489,7 +458,6 @@ public abstract class FactsBase protected readonly ScanAndSignConfiguration _config; protected readonly PackageRegistration _packageRegistration; - protected readonly PackageRegistration _packageRegistrationWithBadUsername; public FactsBase(ITestOutputHelper output) { @@ -520,14 +488,6 @@ public FactsBase(ITestOutputHelper output) } }; - _packageRegistrationWithBadUsername = new PackageRegistration - { - Owners = new[] - { - new User { Username = "Bad Username" } - } - }; - _target = new PackageSignatureValidator( _validatorStateService.Object, _packageSignatureVerifier.Object, diff --git a/tests/Validation.PackageSigning.ScanAndSign.Tests/ScanAndSignProcessorFacts.cs b/tests/Validation.PackageSigning.ScanAndSign.Tests/ScanAndSignProcessorFacts.cs index 62ffb1223..4438ff9bc 100644 --- a/tests/Validation.PackageSigning.ScanAndSign.Tests/ScanAndSignProcessorFacts.cs +++ b/tests/Validation.PackageSigning.ScanAndSign.Tests/ScanAndSignProcessorFacts.cs @@ -266,30 +266,6 @@ public async Task EnqueuesScanAndSignIfPackageHasNoRepositorySignature() .Verify(v => v.TryAddValidatorStatusAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); } - [Fact] - public async Task WhenUsernameInvalid_SkipsScanAndSign() - { - _config.RepositorySigningEnabled = true; - - _validationContext.Mock(); - _packageServiceMock - .Setup(p => p.FindPackageRegistrationById(_request.PackageId)) - .Returns(_packageRegistrationWithInvalidUser); - - var result = await _target.StartAsync(_request); - - _packageServiceMock - .Verify(p => p.FindPackageRegistrationById(_request.PackageId), Times.Once); - - _enqueuerMock - .Verify(e => e.EnqueueScanAsync(_request.ValidationId, _request.NupkgUrl), Times.Once); - - _validatorStateServiceMock - .Verify(v => v.TryAddValidatorStatusAsync(_request, _status, ValidationStatus.Incomplete), Times.Once); - _validatorStateServiceMock - .Verify(v => v.TryAddValidatorStatusAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); - } - [Fact] public async Task EnqueuesScanAndSignEvenIfRepositorySigningIsDisabled() { @@ -455,15 +431,6 @@ public async Task WhenPackageFitsCriteriaAndIsNotRepositorySigned_DoesNotSkipSca } }; - private PackageRegistration _packageRegistrationWithInvalidUser = new PackageRegistration - { - Owners = new List - { - new User("Billy"), - new User("Satan Claus"), - } - }; - public TheStartAsyncMethod() { _request = new ValidationRequest(Guid.NewGuid(), 42, "somepackage", "somversion", "https://example.com/package.nupkg"); From e66952fb865e10056db4d63ed17e083a3b1dd4b9 Mon Sep 17 00:00:00 2001 From: Christy Henriksson Date: Thu, 29 Nov 2018 14:13:20 -0800 Subject: [PATCH 6/7] Add configuration argument (#689) --- .../Scripts/Stats.CollectAzureChinaCDNLogs.cmd | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Stats.CollectAzureChinaCDNLogs/Scripts/Stats.CollectAzureChinaCDNLogs.cmd b/src/Stats.CollectAzureChinaCDNLogs/Scripts/Stats.CollectAzureChinaCDNLogs.cmd index e4b70026d..c2adf2f65 100644 --- a/src/Stats.CollectAzureChinaCDNLogs/Scripts/Stats.CollectAzureChinaCDNLogs.cmd +++ b/src/Stats.CollectAzureChinaCDNLogs/Scripts/Stats.CollectAzureChinaCDNLogs.cmd @@ -8,6 +8,7 @@ echo "Starting job - #{Jobs.Stats.CollectAzureChinaCDNLogs.Title}" title #{Jobs.Stats.CollectAzureChinaCDNLogs.Title} start /w Stats.CollectAzureChinaCDNLogs.exe ^ + -Configuration "#{Jobs.Stats.CollectAzureChinaCDNLogs.Configuration}" ^ -InstrumentationKey "#{Jobs.Stats.CollectAzureChinaCDNLogs.InstrumentationKey}" ^ -verbose true ^ -Interval #{Jobs.Stats.CollectAzureChinaCDNLogs.Interval} From ea7d93c9d78781855e80130af63fb83dfd87e929 Mon Sep 17 00:00:00 2001 From: Joel Verhagen Date: Wed, 31 Oct 2018 15:01:22 -0700 Subject: [PATCH 7/7] Remove legacy validation code (VCS-based) (#605) Progress on https://github.com/NuGet/Engineering/issues/123 --- NuGet.Jobs.sln | 35 -- build.ps1 | 4 - .../Configuration/JobArgumentNames.cs | 6 - .../{Vcs => Criteria}/IPackageCriteria.cs | 2 +- .../IPackageCriteriaEvaluator.cs | 2 +- .../{Vcs => Criteria}/PackageCriteria.cs | 2 +- .../PackageCriteriaEvaluator.cs | 2 +- .../Error.cs | 3 - .../Job.cs | 44 +- ...et.Services.Validation.Orchestrator.csproj | 14 +- .../PackageSigning/Scan/ScanValidator.cs | 5 +- .../ScanAndSign/ScanAndSignConfiguration.cs | 1 - .../ScanAndSign/ScanAndSignProcessor.cs | 1 - .../Symbols/SymbolCriteriaEvaluator.cs | 1 - .../Symbols/SymbolScanOnlyConfiguration.cs | 1 - .../Symbols/SymbolScanValidator.cs | 9 +- .../Vcs/VcsConfiguration.cs | 26 - .../Vcs/VcsValidator.cs | 186 ------- .../settings.json | 21 - .../Validation/ValidatorName.cs | 1 - .../Configuration/ConfigurationService.cs | 45 -- .../Configuration/EmptySecretReaderFactory.cs | 20 - .../Configuration/IConfigurationService.cs | 12 - .../Configuration/ISecretReaderFactory.cs | 14 - .../Configuration/SecretReaderFactory.cs | 54 --- .../Extensions/CloudBlobExtensions.cs | 49 -- src/Validation.Common/INotificationService.cs | 13 - .../IPackageValidationAuditor.cs | 23 - .../IPackageValidationService.cs | 24 - src/Validation.Common/NotificationService.cs | 41 -- src/Validation.Common/NuGetPackage.cs | 43 -- .../NuGetPackageQueueExtensions.cs | 34 -- .../NugetPackageExtensions.cs | 13 - src/Validation.Common/OData/NuGetV2Feed.cs | 113 ----- .../OData/NuGetV2PackageComparer.cs | 28 -- .../PackageValidationAudit.cs | 33 -- .../PackageValidationAuditEntry.cs | 16 - .../PackageValidationAuditor.cs | 255 ---------- .../PackageValidationEntity.cs | 75 --- .../PackageValidationMessage.cs | 20 - .../PackageValidationOrchestrationCursor.cs | 70 --- .../PackageValidationQueue.cs | 106 ---- .../PackageValidationService.cs | 109 ----- .../PackageValidationTable.cs | 40 -- .../Properties/AssemblyInfo.cs | 35 -- src/Validation.Common/TraceConstant.cs | 26 - src/Validation.Common/TraceEvent.cs | 28 -- src/Validation.Common/TraceHelper.cs | 87 ---- .../Validation.Common.csproj | 132 ----- src/Validation.Common/ValidationEvent.cs | 72 --- .../Validators/IValidator.cs | 16 - .../Validators/ValidationException.cs | 24 - .../Validators/ValidationResult.cs | 33 -- .../Validators/ValidatorBase.cs | 69 --- .../Validators/Vcs/VcsCallbackServer.cs | 344 ------------- .../Validators/Vcs/VcsValidator.cs | 130 ----- src/Validation.Common/app.config | 6 - src/Validation.Helper/Action.cs | 18 - src/Validation.Helper/App.config | 6 - src/Validation.Helper/Command.cs | 13 - src/Validation.Helper/CommandLineArguments.cs | 16 - src/Validation.Helper/Job.cs | 102 ---- src/Validation.Helper/MarkClean.cs | 107 ---- src/Validation.Helper/Program.cs | 28 -- .../Properties/AssemblyInfo.cs | 14 - src/Validation.Helper/Rescan.cs | 91 ---- src/Validation.Helper/Util.cs | 77 --- src/Validation.Helper/Validation.Helper.cmd | 20 - .../Validation.Helper.csproj | 93 ---- .../Validation.Helper.nuspec | 17 - src/Validation.Helper/project.json | 17 - src/Validation.Runner/App.config | 14 - src/Validation.Runner/Job.cs | 348 ------------- src/Validation.Runner/Program.cs | 14 - .../Properties/AssemblyInfo.cs | 35 -- src/Validation.Runner/Scripts/Functions.ps1 | 41 -- src/Validation.Runner/Scripts/PostDeploy.ps1 | 20 - src/Validation.Runner/Scripts/PreDeploy.ps1 | 11 - .../Scripts/Validation.Runner.cmd | 31 -- src/Validation.Runner/Scripts/nssm.exe | Bin 331264 -> 0 bytes .../Validation.Runner.csproj | 105 ---- .../Validation.Runner.nuspec | 22 - test.ps1 | 2 - .../PackageCriteriaEvaluatorFacts.cs | 2 +- ...vices.Validation.Orchestrator.Tests.csproj | 7 +- .../Symbol/SymbolScanValidatorFacts.cs | 7 +- .../ValidationProviderFacts.cs | 1 - .../Vcs/VcsValidatorFacts.cs | 458 ------------------ .../Properties/AssemblyInfo.cs | 10 - .../Validation.Common.Tests.csproj | 60 --- .../Validators/Vcs/VcsValidatorFacts.cs | 186 ------- tests/Validation.Common.Tests/project.json | 15 - .../Validation.Helper.Tests/MarkCleanFacts.cs | 51 -- .../Properties/AssemblyInfo.cs | 35 -- tests/Validation.Helper.Tests/RescanFacts.cs | 51 -- .../Validation.Helper.Tests.csproj | 65 --- tests/Validation.Helper.Tests/project.json | 16 - .../ScanAndSignProcessorFacts.cs | 5 +- .../ScanValidatorFacts.cs | 5 +- 99 files changed, 28 insertions(+), 4826 deletions(-) rename src/NuGet.Services.Validation.Orchestrator/{Vcs => Criteria}/IPackageCriteria.cs (96%) rename src/NuGet.Services.Validation.Orchestrator/{Vcs => Criteria}/IPackageCriteriaEvaluator.cs (91%) rename src/NuGet.Services.Validation.Orchestrator/{Vcs => Criteria}/PackageCriteria.cs (91%) rename src/NuGet.Services.Validation.Orchestrator/{Vcs => Criteria}/PackageCriteriaEvaluator.cs (97%) delete mode 100644 src/NuGet.Services.Validation.Orchestrator/Vcs/VcsConfiguration.cs delete mode 100644 src/NuGet.Services.Validation.Orchestrator/Vcs/VcsValidator.cs delete mode 100644 src/Validation.Common/Configuration/ConfigurationService.cs delete mode 100644 src/Validation.Common/Configuration/EmptySecretReaderFactory.cs delete mode 100644 src/Validation.Common/Configuration/IConfigurationService.cs delete mode 100644 src/Validation.Common/Configuration/ISecretReaderFactory.cs delete mode 100644 src/Validation.Common/Configuration/SecretReaderFactory.cs delete mode 100644 src/Validation.Common/Extensions/CloudBlobExtensions.cs delete mode 100644 src/Validation.Common/INotificationService.cs delete mode 100644 src/Validation.Common/IPackageValidationAuditor.cs delete mode 100644 src/Validation.Common/IPackageValidationService.cs delete mode 100644 src/Validation.Common/NotificationService.cs delete mode 100644 src/Validation.Common/NuGetPackage.cs delete mode 100644 src/Validation.Common/NuGetPackageQueueExtensions.cs delete mode 100644 src/Validation.Common/NugetPackageExtensions.cs delete mode 100644 src/Validation.Common/OData/NuGetV2Feed.cs delete mode 100644 src/Validation.Common/OData/NuGetV2PackageComparer.cs delete mode 100644 src/Validation.Common/PackageValidationAudit.cs delete mode 100644 src/Validation.Common/PackageValidationAuditEntry.cs delete mode 100644 src/Validation.Common/PackageValidationAuditor.cs delete mode 100644 src/Validation.Common/PackageValidationEntity.cs delete mode 100644 src/Validation.Common/PackageValidationMessage.cs delete mode 100644 src/Validation.Common/PackageValidationOrchestrationCursor.cs delete mode 100644 src/Validation.Common/PackageValidationQueue.cs delete mode 100644 src/Validation.Common/PackageValidationService.cs delete mode 100644 src/Validation.Common/PackageValidationTable.cs delete mode 100644 src/Validation.Common/Properties/AssemblyInfo.cs delete mode 100644 src/Validation.Common/TraceConstant.cs delete mode 100644 src/Validation.Common/TraceEvent.cs delete mode 100644 src/Validation.Common/TraceHelper.cs delete mode 100644 src/Validation.Common/Validation.Common.csproj delete mode 100644 src/Validation.Common/ValidationEvent.cs delete mode 100644 src/Validation.Common/Validators/IValidator.cs delete mode 100644 src/Validation.Common/Validators/ValidationException.cs delete mode 100644 src/Validation.Common/Validators/ValidationResult.cs delete mode 100644 src/Validation.Common/Validators/ValidatorBase.cs delete mode 100644 src/Validation.Common/Validators/Vcs/VcsCallbackServer.cs delete mode 100644 src/Validation.Common/Validators/Vcs/VcsValidator.cs delete mode 100644 src/Validation.Common/app.config delete mode 100644 src/Validation.Helper/Action.cs delete mode 100644 src/Validation.Helper/App.config delete mode 100644 src/Validation.Helper/Command.cs delete mode 100644 src/Validation.Helper/CommandLineArguments.cs delete mode 100644 src/Validation.Helper/Job.cs delete mode 100644 src/Validation.Helper/MarkClean.cs delete mode 100644 src/Validation.Helper/Program.cs delete mode 100644 src/Validation.Helper/Properties/AssemblyInfo.cs delete mode 100644 src/Validation.Helper/Rescan.cs delete mode 100644 src/Validation.Helper/Util.cs delete mode 100644 src/Validation.Helper/Validation.Helper.cmd delete mode 100644 src/Validation.Helper/Validation.Helper.csproj delete mode 100644 src/Validation.Helper/Validation.Helper.nuspec delete mode 100644 src/Validation.Helper/project.json delete mode 100644 src/Validation.Runner/App.config delete mode 100644 src/Validation.Runner/Job.cs delete mode 100644 src/Validation.Runner/Program.cs delete mode 100644 src/Validation.Runner/Properties/AssemblyInfo.cs delete mode 100644 src/Validation.Runner/Scripts/Functions.ps1 delete mode 100644 src/Validation.Runner/Scripts/PostDeploy.ps1 delete mode 100644 src/Validation.Runner/Scripts/PreDeploy.ps1 delete mode 100644 src/Validation.Runner/Scripts/Validation.Runner.cmd delete mode 100644 src/Validation.Runner/Scripts/nssm.exe delete mode 100644 src/Validation.Runner/Validation.Runner.csproj delete mode 100644 src/Validation.Runner/Validation.Runner.nuspec rename tests/NuGet.Services.Validation.Orchestrator.Tests/{Vcs => Criteria}/PackageCriteriaEvaluatorFacts.cs (99%) delete mode 100644 tests/NuGet.Services.Validation.Orchestrator.Tests/Vcs/VcsValidatorFacts.cs delete mode 100644 tests/Validation.Common.Tests/Properties/AssemblyInfo.cs delete mode 100644 tests/Validation.Common.Tests/Validation.Common.Tests.csproj delete mode 100644 tests/Validation.Common.Tests/Validators/Vcs/VcsValidatorFacts.cs delete mode 100644 tests/Validation.Common.Tests/project.json delete mode 100644 tests/Validation.Helper.Tests/MarkCleanFacts.cs delete mode 100644 tests/Validation.Helper.Tests/Properties/AssemblyInfo.cs delete mode 100644 tests/Validation.Helper.Tests/RescanFacts.cs delete mode 100644 tests/Validation.Helper.Tests/Validation.Helper.Tests.csproj delete mode 100644 tests/Validation.Helper.Tests/project.json diff --git a/NuGet.Jobs.sln b/NuGet.Jobs.sln index 4727c79e7..62d27a47b 100644 --- a/NuGet.Jobs.sln +++ b/NuGet.Jobs.sln @@ -55,10 +55,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stats.RollUpDownloadFacts", EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Validation", "Validation", "{678D7B14-F8BC-4193-99AF-2EE8AA390A02}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Validation.Common", "src\Validation.Common\Validation.Common.csproj", "{2539DDF3-0CC5-4A03-B5F9-39B47744A7BD}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Validation.Runner", "src\Validation.Runner\Validation.Runner.csproj", "{1EB7FF94-9B4A-4008-8F8E-5F867C0B00DE}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Gallery", "Gallery", "{88725659-D5F8-49F9-9B7E-D87C5B9917D7}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gallery.CredentialExpiration", "src\Gallery.CredentialExpiration\Gallery.CredentialExpiration.csproj", "{FA8C7905-985F-4919-AAA9-4B9A252F4977}" @@ -71,10 +67,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gallery.Maintenance", "src\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CopyAzureContainer", "src\CopyAzureContainer\CopyAzureContainer.csproj", "{A07F7D0C-F269-43D5-A812-3ABC47090885}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Validation.Helper", "src\Validation.Helper\Validation.Helper.csproj", "{305FB2C1-01FD-413D-B788-7B85BAD85A41}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Validation.Helper.Tests", "tests\Validation.Helper.Tests\Validation.Helper.Tests.csproj", "{8336BEBE-EC6A-4E40-A1C9-8C34A507E62D}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NuGetCDNRedirect", "src\NuGetCDNRedirect\NuGetCDNRedirect.csproj", "{BC9EA7CE-AD21-4D17-B581-F8ED8CBD7191}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.Search.GenerateAuxiliaryData", "tests\Tests.Search.GenerateAuxiliaryData\Tests.Search.GenerateAuxiliaryData.csproj", "{BA59047E-846A-472C-8587-73719DDB29CE}" @@ -89,8 +81,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NuGet.Services.Validation.O EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NuGet.Services.Validation.Orchestrator.Tests", "tests\NuGet.Services.Validation.Orchestrator.Tests\NuGet.Services.Validation.Orchestrator.Tests.csproj", "{A3B0B15D-22D9-4F1F-94C4-B24B28ECF632}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Validation.Common.Tests", "tests\Validation.Common.Tests\Validation.Common.Tests.csproj", "{F9690B52-3C92-42A0-B41F-1A6040C2D2EE}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Validation.PackageSigning.Core", "src\Validation.PackageSigning.Core\Validation.PackageSigning.Core.csproj", "{91C060DA-736F-4DA9-A57F-CB3AC0E6CB10}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Validation.PackageSigning.ValidateCertificate", "src\Validation.PackageSigning.ValidateCertificate\Validation.PackageSigning.ValidateCertificate.csproj", "{A245E448-8AE0-452B-9338-8C0E0B637D72}" @@ -219,14 +209,6 @@ Global {3E0A20C8-C6D2-4762-955D-C7BF35C2C9A7}.Debug|Any CPU.Build.0 = Debug|Any CPU {3E0A20C8-C6D2-4762-955D-C7BF35C2C9A7}.Release|Any CPU.ActiveCfg = Release|Any CPU {3E0A20C8-C6D2-4762-955D-C7BF35C2C9A7}.Release|Any CPU.Build.0 = Release|Any CPU - {2539DDF3-0CC5-4A03-B5F9-39B47744A7BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2539DDF3-0CC5-4A03-B5F9-39B47744A7BD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2539DDF3-0CC5-4A03-B5F9-39B47744A7BD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2539DDF3-0CC5-4A03-B5F9-39B47744A7BD}.Release|Any CPU.Build.0 = Release|Any CPU - {1EB7FF94-9B4A-4008-8F8E-5F867C0B00DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1EB7FF94-9B4A-4008-8F8E-5F867C0B00DE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1EB7FF94-9B4A-4008-8F8E-5F867C0B00DE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1EB7FF94-9B4A-4008-8F8E-5F867C0B00DE}.Release|Any CPU.Build.0 = Release|Any CPU {FA8C7905-985F-4919-AAA9-4B9A252F4977}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FA8C7905-985F-4919-AAA9-4B9A252F4977}.Debug|Any CPU.Build.0 = Debug|Any CPU {FA8C7905-985F-4919-AAA9-4B9A252F4977}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -243,14 +225,6 @@ Global {A07F7D0C-F269-43D5-A812-3ABC47090885}.Debug|Any CPU.Build.0 = Debug|Any CPU {A07F7D0C-F269-43D5-A812-3ABC47090885}.Release|Any CPU.ActiveCfg = Release|Any CPU {A07F7D0C-F269-43D5-A812-3ABC47090885}.Release|Any CPU.Build.0 = Release|Any CPU - {305FB2C1-01FD-413D-B788-7B85BAD85A41}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {305FB2C1-01FD-413D-B788-7B85BAD85A41}.Debug|Any CPU.Build.0 = Debug|Any CPU - {305FB2C1-01FD-413D-B788-7B85BAD85A41}.Release|Any CPU.ActiveCfg = Release|Any CPU - {305FB2C1-01FD-413D-B788-7B85BAD85A41}.Release|Any CPU.Build.0 = Release|Any CPU - {8336BEBE-EC6A-4E40-A1C9-8C34A507E62D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8336BEBE-EC6A-4E40-A1C9-8C34A507E62D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8336BEBE-EC6A-4E40-A1C9-8C34A507E62D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8336BEBE-EC6A-4E40-A1C9-8C34A507E62D}.Release|Any CPU.Build.0 = Release|Any CPU {BC9EA7CE-AD21-4D17-B581-F8ED8CBD7191}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BC9EA7CE-AD21-4D17-B581-F8ED8CBD7191}.Debug|Any CPU.Build.0 = Debug|Any CPU {BC9EA7CE-AD21-4D17-B581-F8ED8CBD7191}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -279,10 +253,6 @@ Global {A3B0B15D-22D9-4F1F-94C4-B24B28ECF632}.Debug|Any CPU.Build.0 = Debug|Any CPU {A3B0B15D-22D9-4F1F-94C4-B24B28ECF632}.Release|Any CPU.ActiveCfg = Release|Any CPU {A3B0B15D-22D9-4F1F-94C4-B24B28ECF632}.Release|Any CPU.Build.0 = Release|Any CPU - {F9690B52-3C92-42A0-B41F-1A6040C2D2EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F9690B52-3C92-42A0-B41F-1A6040C2D2EE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F9690B52-3C92-42A0-B41F-1A6040C2D2EE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F9690B52-3C92-42A0-B41F-1A6040C2D2EE}.Release|Any CPU.Build.0 = Release|Any CPU {91C060DA-736F-4DA9-A57F-CB3AC0E6CB10}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {91C060DA-736F-4DA9-A57F-CB3AC0E6CB10}.Debug|Any CPU.Build.0 = Debug|Any CPU {91C060DA-736F-4DA9-A57F-CB3AC0E6CB10}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -419,14 +389,10 @@ Global {B5C01B7A-933D-483E-AF07-6AA266B0EB49} = {B9D03824-A9CA-43AC-86D6-8BB399B9A228} {CF7645A4-B4C0-4827-B6B0-AE42108071C6} = {57F34E9A-3A98-4E48-BF76-D49CB9CB3323} {3E0A20C8-C6D2-4762-955D-C7BF35C2C9A7} = {B9D03824-A9CA-43AC-86D6-8BB399B9A228} - {2539DDF3-0CC5-4A03-B5F9-39B47744A7BD} = {678D7B14-F8BC-4193-99AF-2EE8AA390A02} - {1EB7FF94-9B4A-4008-8F8E-5F867C0B00DE} = {678D7B14-F8BC-4193-99AF-2EE8AA390A02} {FA8C7905-985F-4919-AAA9-4B9A252F4977} = {88725659-D5F8-49F9-9B7E-D87C5B9917D7} {12719498-B87E-4E92-8C2B-30046393CF85} = {BEC3DF4D-9A04-42C8-8B4F-D42750202B4D} {EFF021CA-1BF4-4C09-BFB8-D314EAAD24D2} = {88725659-D5F8-49F9-9B7E-D87C5B9917D7} {A07F7D0C-F269-43D5-A812-3ABC47090885} = {FA5644B5-4F08-43F6-86B3-039374312A47} - {305FB2C1-01FD-413D-B788-7B85BAD85A41} = {678D7B14-F8BC-4193-99AF-2EE8AA390A02} - {8336BEBE-EC6A-4E40-A1C9-8C34A507E62D} = {6A776396-02B1-475D-A104-26940ADB04AB} {BC9EA7CE-AD21-4D17-B581-F8ED8CBD7191} = {FA5644B5-4F08-43F6-86B3-039374312A47} {BA59047E-846A-472C-8587-73719DDB29CE} = {6A776396-02B1-475D-A104-26940ADB04AB} {147A757D-864B-4C74-B8CF-14DFF9793605} = {6A776396-02B1-475D-A104-26940ADB04AB} @@ -434,7 +400,6 @@ Global {0C887292-C5AB-4107-946C-A53B18A38D22} = {6A776396-02B1-475D-A104-26940ADB04AB} {E6D094FB-9068-4578-B176-116F97E7506B} = {678D7B14-F8BC-4193-99AF-2EE8AA390A02} {A3B0B15D-22D9-4F1F-94C4-B24B28ECF632} = {6A776396-02B1-475D-A104-26940ADB04AB} - {F9690B52-3C92-42A0-B41F-1A6040C2D2EE} = {6A776396-02B1-475D-A104-26940ADB04AB} {91C060DA-736F-4DA9-A57F-CB3AC0E6CB10} = {678D7B14-F8BC-4193-99AF-2EE8AA390A02} {A245E448-8AE0-452B-9338-8C0E0B637D72} = {678D7B14-F8BC-4193-99AF-2EE8AA390A02} {5ACE7756-F8D0-4D90-9957-872DE4A1381E} = {6A776396-02B1-475D-A104-26940ADB04AB} diff --git a/build.ps1 b/build.ps1 index 28f576599..30b2faed9 100644 --- a/build.ps1 +++ b/build.ps1 @@ -87,7 +87,6 @@ Invoke-BuildStep 'Clearing artifacts' { Clear-Artifacts } ` Invoke-BuildStep 'Set version metadata in AssemblyInfo.cs' { ` $versionMetadata = - "$PSScriptRoot\src\Validation.Helper\Properties\AssemblyInfo.g.cs", "$PSScriptRoot\src\CopyAzureContainer\Properties\AssemblyInfo.g.cs", "$PSScriptRoot\src\NuGetCDNRedirect\Properties\AssemblyInfo.g.cs", "$PSScriptRoot\src\NuGet.Services.Validation.Orchestrator\Properties\AssemblyInfo.g.cs", @@ -96,7 +95,6 @@ Invoke-BuildStep 'Set version metadata in AssemblyInfo.cs' { ` "$PSScriptRoot\src\Validation.PackageSigning.ProcessSignature\Properties\AssemblyInfo.g.cs", "$PSScriptRoot\src\Validation.PackageSigning.ValidateCertificate\Properties\AssemblyInfo.g.cs", "$PSScriptRoot\src\Validation.PackageSigning.RevalidateCertificate\Properties\AssemblyInfo.g.cs", - "$PSScriptRoot\src\NuGet.Jobs.Common\Properties\AssemblyInfo.g.cs", "$PSScriptRoot\src\Validation.Common.Job\Properties\AssemblyInfo.g.cs", "$PSScriptRoot\src\Validation.ScanAndSign.Core\Properties\AssemblyInfo.g.cs", "$PSScriptRoot\src\PackageLagMonitor\Properties\AssemblyInfo.g.cs", @@ -150,9 +148,7 @@ Invoke-BuildStep 'Creating artifacts' { "src/ArchivePackages/ArchivePackages.csproj", ` "src/Search.GenerateAuxiliaryData/Search.GenerateAuxiliaryData.csproj", ` "src/Stats.RollUpDownloadFacts/Stats.RollUpDownloadFacts.csproj", ` - "src/Validation.Runner/Validation.Runner.csproj", ` "src/NuGet.SupportRequests.Notifications/NuGet.SupportRequests.Notifications.csproj", ` - "src/Validation.Helper/Validation.Helper.csproj", ` "src/CopyAzureContainer/CopyAzureContainer.csproj", ` "src/NuGetCDNRedirect/NuGetCDNRedirect.csproj", ` "src/NuGet.Services.Validation.Orchestrator/NuGet.Services.Validation.Orchestrator.csproj", ` diff --git a/src/NuGet.Jobs.Common/Configuration/JobArgumentNames.cs b/src/NuGet.Jobs.Common/Configuration/JobArgumentNames.cs index 2c0959ef1..79ba8df9d 100644 --- a/src/NuGet.Jobs.Common/Configuration/JobArgumentNames.cs +++ b/src/NuGet.Jobs.Common/Configuration/JobArgumentNames.cs @@ -99,12 +99,6 @@ public static class JobArgumentNames public const string PackageUrlTemplate = "PackageUrlTemplate"; public const string BatchSize = "BatchSize"; - // Arguments specific to VCS validation task - public const string VcsValidatorServiceUrl = "VcsValidatorServiceUrl"; - public const string VcsValidatorCallbackUrl = "VcsValidatorCallbackUrl"; - public const string VcsContactAlias = "VcsContactAlias"; - public const string VcsValidatorSubmitterAlias = "VcsValidatorAlias"; - // Key Vault public const string VaultName = "VaultName"; public const string ClientId = "ClientId"; diff --git a/src/NuGet.Services.Validation.Orchestrator/Vcs/IPackageCriteria.cs b/src/NuGet.Services.Validation.Orchestrator/Criteria/IPackageCriteria.cs similarity index 96% rename from src/NuGet.Services.Validation.Orchestrator/Vcs/IPackageCriteria.cs rename to src/NuGet.Services.Validation.Orchestrator/Criteria/IPackageCriteria.cs index b6cac5470..9635343ac 100644 --- a/src/NuGet.Services.Validation.Orchestrator/Vcs/IPackageCriteria.cs +++ b/src/NuGet.Services.Validation.Orchestrator/Criteria/IPackageCriteria.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; -namespace NuGet.Services.Validation.Vcs +namespace NuGet.Services.Validation { /// /// Generic criteria used to include or exclude packages from a process. diff --git a/src/NuGet.Services.Validation.Orchestrator/Vcs/IPackageCriteriaEvaluator.cs b/src/NuGet.Services.Validation.Orchestrator/Criteria/IPackageCriteriaEvaluator.cs similarity index 91% rename from src/NuGet.Services.Validation.Orchestrator/Vcs/IPackageCriteriaEvaluator.cs rename to src/NuGet.Services.Validation.Orchestrator/Criteria/IPackageCriteriaEvaluator.cs index 41ab63b26..ea7151e24 100644 --- a/src/NuGet.Services.Validation.Orchestrator/Vcs/IPackageCriteriaEvaluator.cs +++ b/src/NuGet.Services.Validation.Orchestrator/Criteria/IPackageCriteriaEvaluator.cs @@ -3,7 +3,7 @@ using NuGet.Services.Entities; -namespace NuGet.Services.Validation.Vcs +namespace NuGet.Services.Validation { /// /// Evaluates whether a given entity matches some criteria. diff --git a/src/NuGet.Services.Validation.Orchestrator/Vcs/PackageCriteria.cs b/src/NuGet.Services.Validation.Orchestrator/Criteria/PackageCriteria.cs similarity index 91% rename from src/NuGet.Services.Validation.Orchestrator/Vcs/PackageCriteria.cs rename to src/NuGet.Services.Validation.Orchestrator/Criteria/PackageCriteria.cs index 7e00fe566..054a79be2 100644 --- a/src/NuGet.Services.Validation.Orchestrator/Vcs/PackageCriteria.cs +++ b/src/NuGet.Services.Validation.Orchestrator/Criteria/PackageCriteria.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; -namespace NuGet.Services.Validation.Vcs +namespace NuGet.Services.Validation { public class PackageCriteria : ICriteria { diff --git a/src/NuGet.Services.Validation.Orchestrator/Vcs/PackageCriteriaEvaluator.cs b/src/NuGet.Services.Validation.Orchestrator/Criteria/PackageCriteriaEvaluator.cs similarity index 97% rename from src/NuGet.Services.Validation.Orchestrator/Vcs/PackageCriteriaEvaluator.cs rename to src/NuGet.Services.Validation.Orchestrator/Criteria/PackageCriteriaEvaluator.cs index 77e5df81b..2ac7e9e35 100644 --- a/src/NuGet.Services.Validation.Orchestrator/Vcs/PackageCriteriaEvaluator.cs +++ b/src/NuGet.Services.Validation.Orchestrator/Criteria/PackageCriteriaEvaluator.cs @@ -6,7 +6,7 @@ using System.Text.RegularExpressions; using NuGet.Services.Entities; -namespace NuGet.Services.Validation.Vcs +namespace NuGet.Services.Validation { public class PackageCriteriaEvaluator : ICriteriaEvaluator { diff --git a/src/NuGet.Services.Validation.Orchestrator/Error.cs b/src/NuGet.Services.Validation.Orchestrator/Error.cs index d0498b1e8..7bb32611c 100644 --- a/src/NuGet.Services.Validation.Orchestrator/Error.cs +++ b/src/NuGet.Services.Validation.Orchestrator/Error.cs @@ -9,9 +9,6 @@ public static class Error { public static EventId ConfigurationReadFailure = new EventId(1, "Failed to process configuration"); public static EventId ConfigurationValidationFailure = new EventId(2, "Configuration is invalid"); - public static EventId VcsValidationAlreadyStarted = new EventId(3, "VCS validation already started"); - public static EventId VcsValidationFailureAuditFound = new EventId(4, "VCS validation failure audit found"); - public static EventId VcsValidationUnexpectedAuditFound = new EventId(5, "VCS validation unexpected audit found"); public static EventId OrchestratorOnMessageException = new EventId(6, "Failed to process orchestrator message"); public static EventId UpdatingPackageDbStatusFailed = new EventId(7, "Failed to update package status in DB"); diff --git a/src/NuGet.Services.Validation.Orchestrator/Job.cs b/src/NuGet.Services.Validation.Orchestrator/Job.cs index 7eb10a3df..3d4659d6b 100644 --- a/src/NuGet.Services.Validation.Orchestrator/Job.cs +++ b/src/NuGet.Services.Validation.Orchestrator/Job.cs @@ -17,31 +17,28 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.WindowsAzure.Storage; using NuGet.Jobs; using NuGet.Jobs.Configuration; using NuGet.Jobs.Validation; -using NuGet.Jobs.Validation.Common; using NuGet.Jobs.Validation.PackageSigning.Messages; using NuGet.Jobs.Validation.ScanAndSign; using NuGet.Jobs.Validation.Storage; -using NuGet.Services.Validation.Symbols; using NuGet.Jobs.Validation.Symbols.Core; using NuGet.Services.Configuration; +using NuGet.Services.Entities; using NuGet.Services.KeyVault; using NuGet.Services.Logging; +using NuGet.Services.Messaging; +using NuGet.Services.Messaging.Email; using NuGet.Services.ServiceBus; using NuGet.Services.Sql; using NuGet.Services.Validation.Orchestrator.PackageSigning.ScanAndSign; using NuGet.Services.Validation.Orchestrator.Telemetry; using NuGet.Services.Validation.PackageSigning.ProcessSignature; using NuGet.Services.Validation.PackageSigning.ValidateCertificate; -using NuGet.Services.Validation.Vcs; -using NuGet.Services.Messaging.Email; +using NuGet.Services.Validation.Symbols; using NuGetGallery; using NuGetGallery.Diagnostics; -using NuGet.Services.Entities; -using NuGet.Services.Messaging; namespace NuGet.Services.Validation.Orchestrator { @@ -56,7 +53,6 @@ public class Job : JobBase private const string ValidateArgument = "Validate"; private const string ConfigurationSectionName = "Configuration"; - private const string VcsSectionName = "Vcs"; private const string PackageSigningSectionName = "PackageSigning"; private const string PackageCertificatesSectionName = "PackageCertificates"; private const string ScanAndSignSectionName = "ScanAndSign"; @@ -70,7 +66,6 @@ public class Job : JobBase private const string PackageDownloadTimeoutName = "PackageDownloadTimeout"; private const string EmailBindingKey = EmailConfigurationSectionName; - private const string VcsBindingKey = VcsSectionName; private const string PackageVerificationTopicClientBindingKey = "PackageVerificationTopicClient"; private const string PackageSignatureBindingKey = PackageSigningSectionName; private const string PackageCertificatesBindingKey = PackageCertificatesSectionName; @@ -188,7 +183,6 @@ private DbConnection CreateDbConnection(IServiceProvider serviceProvider) whe private void ConfigureJobServices(IServiceCollection services, IConfigurationRoot configurationRoot) { services.Configure(configurationRoot.GetSection(ConfigurationSectionName)); - services.Configure(configurationRoot.GetSection(VcsSectionName)); services.Configure(configurationRoot.GetSection(PackageSigningSectionName)); services.Configure(configurationRoot.GetSection(PackageCertificatesSectionName)); services.Configure(configurationRoot.GetSection(RunnerConfigurationSectionName)); @@ -239,7 +233,6 @@ private void ConfigureJobServices(IServiceCollection services, IConfigurationRoo services.AddTransient(); services.AddTransient, PackageValidationMessageDataSerializationAdapter>(); services.AddTransient, PackageCriteriaEvaluator>(); - services.AddTransient(); services.AddTransient(); services.AddTransient(c => { @@ -298,17 +291,6 @@ private static IServiceProvider CreateProvider(IServiceCollection services, ICon var containerBuilder = new ContainerBuilder(); containerBuilder.Populate(services); - /// Initialize dependencies for the . There is some additional complexity here - /// because the implementations require ambiguous types (such as a and a - /// which there may be more than one configuration of). - containerBuilder - .Register(c => - { - var vcsConfiguration = c.Resolve>(); - var cloudStorageAccount = CloudStorageAccount.Parse(vcsConfiguration.Value.DataStorageAccount); - return cloudStorageAccount; - }) - .Keyed(VcsBindingKey); containerBuilder .Register(c => { @@ -317,23 +299,7 @@ private static IServiceProvider CreateProvider(IServiceCollection services, ICon return topicClient; }) .Keyed(PackageVerificationTopicClientBindingKey); - - containerBuilder - .RegisterType() - .WithKeyedParameter(typeof(CloudStorageAccount), VcsBindingKey) - .WithParameter(new ResolvedParameter( - (pi, ctx) => pi.ParameterType == typeof(string), - (pi, ctx) => ctx.Resolve>().Value.ContainerName)) - .As(); - - containerBuilder - .RegisterType() - .WithKeyedParameter(typeof(CloudStorageAccount), VcsBindingKey) - .WithParameter(new ResolvedParameter( - (pi, ctx) => pi.ParameterType == typeof(string), - (pi, ctx) => ctx.Resolve>().Value.ContainerName)) - .As(); - + containerBuilder .RegisterType() .WithParameter(new ResolvedParameter( diff --git a/src/NuGet.Services.Validation.Orchestrator/NuGet.Services.Validation.Orchestrator.csproj b/src/NuGet.Services.Validation.Orchestrator/NuGet.Services.Validation.Orchestrator.csproj index c41b271a9..fba087900 100644 --- a/src/NuGet.Services.Validation.Orchestrator/NuGet.Services.Validation.Orchestrator.csproj +++ b/src/NuGet.Services.Validation.Orchestrator/NuGet.Services.Validation.Orchestrator.csproj @@ -107,12 +107,10 @@ - - - - - - + + + + @@ -142,10 +140,6 @@ {fa87d075-a934-4443-8d0b-5db32640b6d7} Validation.Common.Job - - {2539ddf3-0cc5-4a03-b5f9-39b47744a7bd} - Validation.Common - {91C060DA-736F-4DA9-A57F-CB3AC0E6CB10} Validation.PackageSigning.Core diff --git a/src/NuGet.Services.Validation.Orchestrator/PackageSigning/Scan/ScanValidator.cs b/src/NuGet.Services.Validation.Orchestrator/PackageSigning/Scan/ScanValidator.cs index 0e6074d79..bf41dc60b 100644 --- a/src/NuGet.Services.Validation.Orchestrator/PackageSigning/Scan/ScanValidator.cs +++ b/src/NuGet.Services.Validation.Orchestrator/PackageSigning/Scan/ScanValidator.cs @@ -6,11 +6,10 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NuGet.Jobs.Validation; -using NuGet.Jobs.Validation.Storage; using NuGet.Jobs.Validation.ScanAndSign; -using NuGet.Services.Validation.Vcs; -using NuGetGallery; +using NuGet.Jobs.Validation.Storage; using NuGet.Services.Entities; +using NuGetGallery; namespace NuGet.Services.Validation.Orchestrator.PackageSigning.ScanAndSign { diff --git a/src/NuGet.Services.Validation.Orchestrator/PackageSigning/ScanAndSign/ScanAndSignConfiguration.cs b/src/NuGet.Services.Validation.Orchestrator/PackageSigning/ScanAndSign/ScanAndSignConfiguration.cs index d6182a5e4..5fd1e0d7f 100644 --- a/src/NuGet.Services.Validation.Orchestrator/PackageSigning/ScanAndSign/ScanAndSignConfiguration.cs +++ b/src/NuGet.Services.Validation.Orchestrator/PackageSigning/ScanAndSign/ScanAndSignConfiguration.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using NuGet.Jobs.Configuration; -using NuGet.Services.Validation.Vcs; namespace NuGet.Services.Validation.Orchestrator.PackageSigning.ScanAndSign { diff --git a/src/NuGet.Services.Validation.Orchestrator/PackageSigning/ScanAndSign/ScanAndSignProcessor.cs b/src/NuGet.Services.Validation.Orchestrator/PackageSigning/ScanAndSign/ScanAndSignProcessor.cs index cbaac7c03..d9b52e571 100644 --- a/src/NuGet.Services.Validation.Orchestrator/PackageSigning/ScanAndSign/ScanAndSignProcessor.cs +++ b/src/NuGet.Services.Validation.Orchestrator/PackageSigning/ScanAndSign/ScanAndSignProcessor.cs @@ -12,7 +12,6 @@ using NuGet.Jobs.Validation.ScanAndSign; using NuGet.Jobs.Validation.Storage; using NuGet.Services.Entities; -using NuGet.Services.Validation.Vcs; using NuGetGallery; namespace NuGet.Services.Validation.Orchestrator.PackageSigning.ScanAndSign diff --git a/src/NuGet.Services.Validation.Orchestrator/Symbols/SymbolCriteriaEvaluator.cs b/src/NuGet.Services.Validation.Orchestrator/Symbols/SymbolCriteriaEvaluator.cs index f8a3c8cb1..c8edcd8de 100644 --- a/src/NuGet.Services.Validation.Orchestrator/Symbols/SymbolCriteriaEvaluator.cs +++ b/src/NuGet.Services.Validation.Orchestrator/Symbols/SymbolCriteriaEvaluator.cs @@ -3,7 +3,6 @@ using System; using NuGet.Services.Entities; -using NuGet.Services.Validation.Vcs; namespace NuGet.Services.Validation { diff --git a/src/NuGet.Services.Validation.Orchestrator/Symbols/SymbolScanOnlyConfiguration.cs b/src/NuGet.Services.Validation.Orchestrator/Symbols/SymbolScanOnlyConfiguration.cs index 677597979..6910fa24a 100644 --- a/src/NuGet.Services.Validation.Orchestrator/Symbols/SymbolScanOnlyConfiguration.cs +++ b/src/NuGet.Services.Validation.Orchestrator/Symbols/SymbolScanOnlyConfiguration.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using NuGet.Jobs.Configuration; -using NuGet.Services.Validation.Vcs; namespace NuGet.Services.Validation.Symbols { diff --git a/src/NuGet.Services.Validation.Orchestrator/Symbols/SymbolScanValidator.cs b/src/NuGet.Services.Validation.Orchestrator/Symbols/SymbolScanValidator.cs index 4bfeef367..994b65564 100644 --- a/src/NuGet.Services.Validation.Orchestrator/Symbols/SymbolScanValidator.cs +++ b/src/NuGet.Services.Validation.Orchestrator/Symbols/SymbolScanValidator.cs @@ -1,19 +1,18 @@ // 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. -using System.Threading.Tasks; using System; -using System.Linq; using System.IO; +using System.Linq; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NuGet.Jobs.Validation; +using NuGet.Jobs.Validation.ScanAndSign; +using NuGet.Jobs.Validation.Storage; using NuGet.Services.Entities; using NuGet.Services.Validation.Orchestrator; using NuGet.Services.Validation.Orchestrator.PackageSigning.ScanAndSign; -using NuGet.Jobs.Validation.Storage; -using NuGet.Jobs.Validation.ScanAndSign; -using NuGet.Services.Validation.Vcs; using NuGetGallery; namespace NuGet.Services.Validation.Symbols diff --git a/src/NuGet.Services.Validation.Orchestrator/Vcs/VcsConfiguration.cs b/src/NuGet.Services.Validation.Orchestrator/Vcs/VcsConfiguration.cs deleted file mode 100644 index 2bba78fe3..000000000 --- a/src/NuGet.Services.Validation.Orchestrator/Vcs/VcsConfiguration.cs +++ /dev/null @@ -1,26 +0,0 @@ -// 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. - -namespace NuGet.Services.Validation.Vcs -{ - /// - /// Configuration for initializing the . - /// - public class VcsConfiguration - { - /// - /// The container name to use for VCS storage resources (table, queue, and blob storage). - /// - public string ContainerName { get; set; } - - /// - /// The connection string to use to connect to an Azure Storage account. - /// - public string DataStorageAccount { get; set; } - - /// - /// The criteria used to determine if a package should be scanned by VCS. - /// - public PackageCriteria PackageCriteria { get; set; } = new PackageCriteria(); - } -} diff --git a/src/NuGet.Services.Validation.Orchestrator/Vcs/VcsValidator.cs b/src/NuGet.Services.Validation.Orchestrator/Vcs/VcsValidator.cs deleted file mode 100644 index 8f77b2bf3..000000000 --- a/src/NuGet.Services.Validation.Orchestrator/Vcs/VcsValidator.cs +++ /dev/null @@ -1,186 +0,0 @@ -// 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. - -using System; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.WindowsAzure.Storage; -using NuGet.Jobs.Validation; -using NuGet.Jobs.Validation.Common; -using NuGet.Services.Entities; -using NuGet.Services.Validation.Orchestrator; -using NuGet.Versioning; -using Error = NuGet.Services.Validation.Orchestrator.Error; - -namespace NuGet.Services.Validation.Vcs -{ - [ValidatorName(ValidatorName.Vcs)] - public class VcsValidator : BaseValidator, IValidator - { - private const string InternalValidatorName = Jobs.Validation.Common.Validators.Vcs.VcsValidator.ValidatorName; - - private readonly IPackageValidationService _validationService; - private readonly IPackageValidationAuditor _validationAuditor; - private readonly IEntityService _packageService; - private readonly ICriteriaEvaluator _criteriaEvaluator; - private readonly IOptionsSnapshot _config; - private readonly ILogger _logger; - - public VcsValidator( - IPackageValidationService validationService, - IPackageValidationAuditor validationAuditor, - IEntityService packageService, - ICriteriaEvaluator criteriaEvaluator, - IOptionsSnapshot config, - ILogger logger) - { - _validationService = validationService ?? throw new ArgumentNullException(nameof(validationService)); - _validationAuditor = validationAuditor ?? throw new ArgumentNullException(nameof(validationAuditor)); - _packageService = packageService ?? throw new ArgumentNullException(nameof(packageService)); - _criteriaEvaluator = criteriaEvaluator ?? throw new ArgumentNullException(nameof(criteriaEvaluator)); - _config = config ?? throw new ArgumentNullException(nameof(config)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - public async Task GetResultAsync(IValidationRequest request) - { - if (ShouldSkip(request)) - { - return ValidationResult.Succeeded; - } - - var audit = await _validationAuditor.ReadAuditAsync( - request.ValidationId, - NormalizePackageId(request.PackageId), - NormalizePackageVersion(request.PackageVersion)); - - if (audit == null) - { - return ValidationResult.NotStarted; - } - - var validationStatusList = audit - .Entries - .Where(x => x.ValidatorName == InternalValidatorName) - .Select(x => GetValidationStatus(request, x.EventId)) - .ToList(); - - var result = validationStatusList.FirstOrDefault(x => x == ValidationStatus.Failed) ?? - validationStatusList.FirstOrDefault(x => x == ValidationStatus.Succeeded) ?? - ValidationStatus.Incomplete; - - return new ValidationResult(result); - } - - private ValidationStatus? GetValidationStatus(IValidationRequest request, ValidationEvent validationEvent) - { - switch (validationEvent) - { - case ValidationEvent.ValidatorException: - case ValidationEvent.BeforeVirusScanRequest: - case ValidationEvent.VirusScanRequestSent: - case ValidationEvent.VirusScanRequestFailed: - return ValidationStatus.Incomplete; - case ValidationEvent.PackageClean: - return ValidationStatus.Succeeded; - case ValidationEvent.PackageNotClean: - case ValidationEvent.NotCleanReason: - case ValidationEvent.ScanFailed: - case ValidationEvent.ScanFailureReason: - _logger.LogError( - Error.VcsValidationFailureAuditFound, - "A failed audit result was found for {validationId} ({packageId} {packageVersion}): {validationEvent}.", - request.ValidationId, - request.PackageId, - request.PackageVersion, - validationEvent); - return ValidationStatus.Failed; - default: - _logger.LogError( - Error.VcsValidationUnexpectedAuditFound, - "An unexpected audit result was found for {validationId} ({packageId} {packageVersion}): {validationEvent}.", - request.ValidationId, - request.PackageId, - request.PackageVersion, - validationEvent); - return ValidationStatus.Failed; - } - } - - public async Task StartAsync(IValidationRequest request) - { - if (ShouldSkip(request)) - { - return ValidationResult.Succeeded; - } - - var normalizedPackageId = NormalizePackageId(request.PackageId); - var normalizedPackageVerison = NormalizePackageVersion(request.PackageVersion); - - try - { - await _validationService.StartValidationProcessAsync( - new NuGetPackage - { - Id = normalizedPackageId, - NormalizedVersion = normalizedPackageVerison, - Version = normalizedPackageVerison, - DownloadUrl = new Uri(request.NupkgUrl), - }, - validators: new[] { InternalValidatorName }, - validationId: request.ValidationId); - } - catch (StorageException e) when (e.RequestInformation?.HttpStatusCode == (int)HttpStatusCode.Conflict - || e.RequestInformation?.HttpStatusCode == (int)HttpStatusCode.PreconditionFailed) - { - // This means the validation has already started. This is acceptable so we should move on. - _logger.LogWarning( - Error.VcsValidationAlreadyStarted, - e, - "The VCS validation for {validationId} ({packageId} {packageVersion}) has already been started.", - request.ValidationId, - request.PackageId, - request.PackageVersion); - } - - return await GetResultAsync(request); - } - - private static string NormalizePackageVersion(string packageVersion) - { - return NuGetVersion - .Parse(packageVersion) - .ToNormalizedString() - .ToLowerInvariant(); - } - - private static string NormalizePackageId(string packageId) - { - return packageId.ToLowerInvariant(); - } - - private bool ShouldSkip(IValidationRequest request) - { - var package = _packageService.FindPackageByIdAndVersionStrict( - request.PackageId, - request.PackageVersion)?.EntityRecord; - - if (!_criteriaEvaluator.IsMatch(_config.Value.PackageCriteria, package)) - { - // This means the validation has already started. This is acceptable so we should move on. - _logger.LogInformation( - "The VCS validation for {validationId} ({packageId} {packageVersion}) was skipped due to package criteria configuration.", - request.ValidationId, - request.PackageId, - request.PackageVersion); - - return true; - } - - return false; - } - } -} diff --git a/src/NuGet.Services.Validation.Orchestrator/settings.json b/src/NuGet.Services.Validation.Orchestrator/settings.json index a3bffb825..1b24eeeab 100644 --- a/src/NuGet.Services.Validation.Orchestrator/settings.json +++ b/src/NuGet.Services.Validation.Orchestrator/settings.json @@ -1,15 +1,6 @@ { "Configuration": { "Validations": [ - { - "name": "VcsValidator", - "TrackAfter": "1:00:00:00", - "requiredValidations": [ - "PackageSigningValidator" - ], - "ShouldStart": true, - "FailureBehavior": "MustSucceed" - }, { "name": "PackageSigningValidator", "TrackAfter": "00:10:00", @@ -43,18 +34,6 @@ "ValidationSetNotificationTimeout": "00:50:00", "TimeoutValidationSetAfter": "1:00:00:00" }, - "Vcs": { - "ContainerName": "validation", - "DataStorageAccount": "UseDevelopmentStorage=true", - "PackageCriteria": { - "ExcludeOwners": [ - "NugetTestAccount" - ], - "IncludeIdPatterns": [ - "E2E.SemVer1Stable.*" - ] - } - }, "PackageSigning": { "ServiceBus": { "ConnectionString": "", diff --git a/src/Validation.Common.Job/Validation/ValidatorName.cs b/src/Validation.Common.Job/Validation/ValidatorName.cs index 4219741fc..2638bd8e2 100644 --- a/src/Validation.Common.Job/Validation/ValidatorName.cs +++ b/src/Validation.Common.Job/Validation/ValidatorName.cs @@ -5,7 +5,6 @@ namespace NuGet.Jobs.Validation { public static class ValidatorName { - public const string Vcs = "VcsValidator"; public const string PackageCertificate = "PackageCertificatesValidator"; public const string ScanAndSign = "ScanAndSign"; public const string ScanOnly = "ScanOnly"; diff --git a/src/Validation.Common/Configuration/ConfigurationService.cs b/src/Validation.Common/Configuration/ConfigurationService.cs deleted file mode 100644 index 1118054ef..000000000 --- a/src/Validation.Common/Configuration/ConfigurationService.cs +++ /dev/null @@ -1,45 +0,0 @@ -// 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. - -using System; -using System.Configuration; -using System.Threading.Tasks; -using NuGet.Services.KeyVault; - -namespace NuGet.Jobs.Validation.Common -{ - public class ConfigurationService : IConfigurationService - { - private ISecretReaderFactory _secretReaderFactory; - private Lazy _secretInjector; - - public ConfigurationService(ISecretReaderFactory secretReaderFactory) - { - if (secretReaderFactory == null) - { - throw new ArgumentNullException(nameof(secretReaderFactory)); - } - - _secretReaderFactory = secretReaderFactory; - _secretInjector = new Lazy(InitSecretInjector, isThreadSafe: false); - } - - public async Task Get(string key) - { - var value = ConfigurationManager.AppSettings[key]; - - if (!string.IsNullOrEmpty(value)) - { - value = await _secretInjector.Value.InjectAsync(value); - } - - return value; - } - - private ISecretInjector InitSecretInjector() - { - return _secretReaderFactory.CreateSecretInjector(_secretReaderFactory.CreateSecretReader(new ConfigurationService(new EmptySecretReaderFactory()))); - } - - } -} \ No newline at end of file diff --git a/src/Validation.Common/Configuration/EmptySecretReaderFactory.cs b/src/Validation.Common/Configuration/EmptySecretReaderFactory.cs deleted file mode 100644 index f57ebb205..000000000 --- a/src/Validation.Common/Configuration/EmptySecretReaderFactory.cs +++ /dev/null @@ -1,20 +0,0 @@ -// 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. - -using NuGet.Services.KeyVault; - -namespace NuGet.Jobs.Validation.Common -{ - public class EmptySecretReaderFactory : ISecretReaderFactory - { - public ISecretInjector CreateSecretInjector(ISecretReader secretReader) - { - return new SecretInjector(secretReader); - } - - public ISecretReader CreateSecretReader(IConfigurationService configuration) - { - return new EmptySecretReader(); - } - } -} \ No newline at end of file diff --git a/src/Validation.Common/Configuration/IConfigurationService.cs b/src/Validation.Common/Configuration/IConfigurationService.cs deleted file mode 100644 index a1b80aa9c..000000000 --- a/src/Validation.Common/Configuration/IConfigurationService.cs +++ /dev/null @@ -1,12 +0,0 @@ -// 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. - -using System.Threading.Tasks; - -namespace NuGet.Jobs.Validation.Common -{ - public interface IConfigurationService - { - Task Get(string key); - } -} diff --git a/src/Validation.Common/Configuration/ISecretReaderFactory.cs b/src/Validation.Common/Configuration/ISecretReaderFactory.cs deleted file mode 100644 index 42b613d63..000000000 --- a/src/Validation.Common/Configuration/ISecretReaderFactory.cs +++ /dev/null @@ -1,14 +0,0 @@ -// 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. - -using NuGet.Services.KeyVault; - -namespace NuGet.Jobs.Validation.Common -{ - public interface ISecretReaderFactory - { - ISecretInjector CreateSecretInjector(ISecretReader secretReader); - - ISecretReader CreateSecretReader(IConfigurationService configurationService); - } -} diff --git a/src/Validation.Common/Configuration/SecretReaderFactory.cs b/src/Validation.Common/Configuration/SecretReaderFactory.cs deleted file mode 100644 index df66819d0..000000000 --- a/src/Validation.Common/Configuration/SecretReaderFactory.cs +++ /dev/null @@ -1,54 +0,0 @@ -// 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. - -using System; -using System.Security.Cryptography.X509Certificates; -using NuGet.Services.KeyVault; - -namespace NuGet.Jobs.Validation.Common -{ - public class SecretReaderFactory : ISecretReaderFactory - { - public const string VaultNameKey = "KeyVault:VaultName"; - public const string ClientIdKey = "KeyVault:ClientId"; - public const string CertificateThumbprintKey = "KeyVault:CertificateThumbprint"; - public const string StoreNameKey = "KeyVault:StoreName"; - public const string StoreLocationKey = "KeyVault:StoreLocation"; - public const string ValidateCertificateKey = "KeyVault:ValidateCertificate"; - - public ISecretReader CreateSecretReader(IConfigurationService configurationService) - { - var vaultName = configurationService.Get(VaultNameKey).Result; - ISecretReader secretReader; - - // Is key vault configured? - if (string.IsNullOrEmpty(vaultName)) - { - secretReader = new EmptySecretReader(); - } - else - { - var clientId = configurationService.Get(ClientIdKey).Result; - var certificateThumbprint = configurationService.Get(CertificateThumbprintKey).Result; - var storeLocation = (StoreLocation)Enum.Parse(typeof(StoreLocation), configurationService.Get(StoreLocationKey).Result); - var storeName = (StoreName)Enum.Parse(typeof(StoreName), configurationService.Get(StoreNameKey).Result); - var validateCertificate = bool.Parse(configurationService.Get(ValidateCertificateKey).Result); - - var certificate = CertificateUtility.FindCertificateByThumbprint(storeName, storeLocation, certificateThumbprint, validateCertificate); - - secretReader = new KeyVaultReader( - new KeyVaultConfiguration( - vaultName, - clientId, - certificate)); - } - - return secretReader; - } - - public ISecretInjector CreateSecretInjector(ISecretReader secretReader) - { - return new SecretInjector(secretReader); - } - } -} \ No newline at end of file diff --git a/src/Validation.Common/Extensions/CloudBlobExtensions.cs b/src/Validation.Common/Extensions/CloudBlobExtensions.cs deleted file mode 100644 index ef405eab3..000000000 --- a/src/Validation.Common/Extensions/CloudBlobExtensions.cs +++ /dev/null @@ -1,49 +0,0 @@ -// 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. - -using System; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.WindowsAzure.Storage; -using Microsoft.WindowsAzure.Storage.Blob; - -namespace NuGet.Jobs.Validation.Common.Extensions -{ - public static class CloudBlobExtensions - { - public static async Task TryAcquireLeaseAsync(this ICloudBlob blob, TimeSpan leaseTime, CancellationToken cancellationToken) - { - string leaseId; - try - { - var sourceBlobExists = await blob.ExistsAsync(cancellationToken); - if (!sourceBlobExists) - { - return null; - } - - leaseId = await blob.AcquireLeaseAsync(leaseTime, null, cancellationToken); - } - catch (StorageException storageException) - { - // check if this is a 409 Conflict with a StatusDescription stating that "There is already a lease present." - // or 404 NotFound (might have been removed by another other instance of this job) - var webException = storageException.InnerException as WebException; - var httpWebResponse = webException?.Response as HttpWebResponse; - if (httpWebResponse != null) - { - if ((httpWebResponse.StatusCode == HttpStatusCode.Conflict - && httpWebResponse.StatusDescription == "There is already a lease present.") || httpWebResponse.StatusCode == HttpStatusCode.NotFound) - { - return null; - } - } - - throw; - } - - return leaseId; - } - } -} \ No newline at end of file diff --git a/src/Validation.Common/INotificationService.cs b/src/Validation.Common/INotificationService.cs deleted file mode 100644 index 5e972be19..000000000 --- a/src/Validation.Common/INotificationService.cs +++ /dev/null @@ -1,13 +0,0 @@ -// 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. - -using System.Threading.Tasks; - -namespace NuGet.Jobs.Validation.Common -{ - public interface INotificationService - { - Task SendNotificationAsync(string subject, string body); - Task SendNotificationAsync(string category, string subject, string body); - } -} \ No newline at end of file diff --git a/src/Validation.Common/IPackageValidationAuditor.cs b/src/Validation.Common/IPackageValidationAuditor.cs deleted file mode 100644 index 0058de00a..000000000 --- a/src/Validation.Common/IPackageValidationAuditor.cs +++ /dev/null @@ -1,23 +0,0 @@ -// 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. - -using System; -using System.Threading.Tasks; - -namespace NuGet.Jobs.Validation.Common -{ - /// - /// The interface for reading package audit information. Only the VCS validation is initiated using this interface. - /// - public interface IPackageValidationAuditor - { - /// - /// Reads the validation audit information. The three parameters comprise the audit key. - /// - /// The validation ID. - /// The package ID. - /// The package version. - /// The package audit information. null if the audit does not exist. - Task ReadAuditAsync(Guid validationId, string packageId, string packageVersion); - } -} \ No newline at end of file diff --git a/src/Validation.Common/IPackageValidationService.cs b/src/Validation.Common/IPackageValidationService.cs deleted file mode 100644 index febb0f6eb..000000000 --- a/src/Validation.Common/IPackageValidationService.cs +++ /dev/null @@ -1,24 +0,0 @@ -// 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. - -using System; -using System.Threading.Tasks; - -namespace NuGet.Jobs.Validation.Common -{ - /// - /// The interface for starting validations. Only the VCS validation is initiated using this interface. - /// - public interface IPackageValidationService - { - /// - /// Start a validation of the specified package. The validation ID is provided by the caller. - /// - /// The package to validate. - /// The well-known names of validators to run. - /// The validation ID to use. - /// A task that completes when the validation has started. - /// Thrown when the validation has already been started. - Task StartValidationProcessAsync(NuGetPackage package, string[] validators, Guid validationId); - } -} \ No newline at end of file diff --git a/src/Validation.Common/NotificationService.cs b/src/Validation.Common/NotificationService.cs deleted file mode 100644 index 66f307ef7..000000000 --- a/src/Validation.Common/NotificationService.cs +++ /dev/null @@ -1,41 +0,0 @@ -// 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. - -using System; -using System.Threading.Tasks; -using Microsoft.WindowsAzure.Storage; -using Microsoft.WindowsAzure.Storage.Blob; - -namespace NuGet.Jobs.Validation.Common -{ - public class NotificationService - : INotificationService - { - private readonly CloudBlobContainer _notificationContainer; - - public NotificationService(CloudStorageAccount cloudStorageAccount, string containerNamePrefix) - { - var cloudBlobClient = cloudStorageAccount.CreateCloudBlobClient(); - _notificationContainer = cloudBlobClient.GetContainerReference(containerNamePrefix + "-notification"); - _notificationContainer.CreateIfNotExists(); - } - - public async Task SendNotificationAsync(string subject, string body) - { - await SendNotificationAsync(null, subject, body); - } - - public async Task SendNotificationAsync(string category, string subject, string body) - { - var fileName = $"{DateTime.UtcNow.ToString("O")}.txt"; - if (!string.IsNullOrEmpty(category)) - { - fileName = $"{category}/{fileName}"; - } - - var notificationBlob = _notificationContainer.GetBlockBlobReference(fileName); - - await notificationBlob.UploadTextAsync($"{subject}\r\n\r\n{body}"); - } - } -} \ No newline at end of file diff --git a/src/Validation.Common/NuGetPackage.cs b/src/Validation.Common/NuGetPackage.cs deleted file mode 100644 index 19539b96c..000000000 --- a/src/Validation.Common/NuGetPackage.cs +++ /dev/null @@ -1,43 +0,0 @@ -// 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. - -using System; - -namespace NuGet.Jobs.Validation.Common -{ - public class NuGetPackage - { - public string Id { get; set; } - public string Version { get; set; } - public string NormalizedVersion { get; set; } - public Uri DownloadUrl { get; set; } - public string Copyright { get; set; } - public DateTimeOffset Created { get; set; } - public string Dependencies { get; set; } - public string Description { get; set; } - public int DownloadCount { get; set; } - public int VersionDownloadCount { get; set; } - public Uri GalleryDetailsUrl { get; set; } - public Uri IconUrl { get; set; } - public bool IsAbsoluteLatestVersion { get; set; } - public bool IsLatestVersion { get; set; } - public bool IsPrerelease { get; set; } - public string Language { get; set; } - public DateTimeOffset? LastEdited { get; set; } - public string LicenseNames { get; set; } - public Uri LicenseReportUrl { get; set; } - public Uri LicenseUrl { get; set; } - public string MinClientVersion { get; set; } - public string PackageHash { get; set; } - public string PackageHashAlgorithm { get; set; } - public long PackageSize { get; set; } - public Uri ProjectUrl { get; set; } - public DateTimeOffset? Published { get; set; } - public string ReleaseNotes { get; set; } - public Uri ReportAbuseUrl { get; set; } - public bool RequireLicenseAcceptance { get; set; } - public string Summary { get; set; } - public string Tags { get; set; } - public string Title { get; set; } - } -} \ No newline at end of file diff --git a/src/Validation.Common/NuGetPackageQueueExtensions.cs b/src/Validation.Common/NuGetPackageQueueExtensions.cs deleted file mode 100644 index 67002b9f4..000000000 --- a/src/Validation.Common/NuGetPackageQueueExtensions.cs +++ /dev/null @@ -1,34 +0,0 @@ -// 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. - -using Newtonsoft.Json; - -namespace NuGet.Jobs.Validation.Common -{ - public static class NuGetPackageQueueExtensions - { - private const string Truncated = "(truncated)"; - - /// - /// Azure Queues have a max message length of 65536 bytes. - /// This method truncates potentially long fields so that a serialized representation - /// of the package falls within that boundary. - /// - /// The package to truncate - /// Truncated package - public static NuGetPackage TruncateForAzureQueue(this NuGetPackage package) - { - // Clone the package - var clone = JsonConvert.DeserializeObject( - JsonConvert.SerializeObject(package)); - - // Truncate long properties (https://github.com/NuGet/NuGet.Jobs/pull/54/files/228105a40129c076afc9b9e21551ffadef315f92#r70679869) - clone.Description = Truncated; - clone.ReleaseNotes = Truncated; - clone.Summary = Truncated; - clone.Tags = Truncated; - - return clone; - } - } -} \ No newline at end of file diff --git a/src/Validation.Common/NugetPackageExtensions.cs b/src/Validation.Common/NugetPackageExtensions.cs deleted file mode 100644 index 22128ca21..000000000 --- a/src/Validation.Common/NugetPackageExtensions.cs +++ /dev/null @@ -1,13 +0,0 @@ -// 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. - -namespace NuGet.Jobs.Validation.Common -{ - public static class NugetPackageExtensions - { - public static string GetVersion(this NuGetPackage package) - { - return package.NormalizedVersion ?? package.Version; - } - } -} diff --git a/src/Validation.Common/OData/NuGetV2Feed.cs b/src/Validation.Common/OData/NuGetV2Feed.cs deleted file mode 100644 index 890156fe0..000000000 --- a/src/Validation.Common/OData/NuGetV2Feed.cs +++ /dev/null @@ -1,113 +0,0 @@ -// 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. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; -using System.Xml.Linq; -using Microsoft.Extensions.Logging; - -namespace NuGet.Jobs.Validation.Common.OData -{ - public class NuGetV2Feed - { - private readonly HttpClient _httpClient; - private readonly ILogger _logger; - - public NuGetV2Feed(HttpClient httpClient, ILogger logger) - { - _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - public async Task> GetPackagesAsync(Uri uri, bool includeDownloadUrl, int continuationsToFollow = 0) - { - _logger.LogInformation($"Start retrieving packages from URL {{{TraceConstant.Url}}}...", uri); - - var result = new List(); - - XElement feed; - using (var stream = await _httpClient.GetStreamAsync(uri)) - { - feed = XElement.Load(stream); - } - - XNamespace atom = "http://www.w3.org/2005/Atom"; - XNamespace dataservices = "http://schemas.microsoft.com/ado/2007/08/dataservices"; - XNamespace metadata = "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"; - - foreach (var entry in feed.Elements(atom + "entry")) - { - var propertiesElement = entry.Element(metadata + "properties"); - - var package = new NuGetPackage - { - Id = entry.GetString(atom + "title", null), - Version = propertiesElement.GetString(dataservices + "Version", null), - NormalizedVersion = propertiesElement.GetString(dataservices + "NormalizedVersion", null), - DownloadUrl = new Uri(entry.Element(atom + "content").Attribute("src").Value), - Copyright = propertiesElement.GetString(dataservices + "Copyright", null), - Created = propertiesElement.GetDateTimeOffset(dataservices + "Created"), - Dependencies = propertiesElement.GetString(dataservices + "Dependencies", null), - Description = propertiesElement.GetString(dataservices + "Description", null), - DownloadCount = propertiesElement.GetInt32(dataservices + "DownloadCount", 0), - VersionDownloadCount = propertiesElement.GetInt32(dataservices + "VersionDownloadCount", 0), - GalleryDetailsUrl = propertiesElement.GetUri(dataservices + "GalleryDetailsUrl", null), - IconUrl = propertiesElement.GetUri(dataservices + "IconUrl", null), - IsAbsoluteLatestVersion = propertiesElement.GetBool(dataservices + "IsAbsoluteLatestVersion", false), - IsLatestVersion = propertiesElement.GetBool(dataservices + "IsLatestVersion", false), - IsPrerelease = propertiesElement.GetBool(dataservices + "IsPrerelease", false), - Language = propertiesElement.GetString(dataservices + "Language", null), - LastEdited = propertiesElement.GetDateTimeOffset(dataservices + "LastEdited", null), - LicenseNames = propertiesElement.GetString(dataservices + "LicenseNames", null), - LicenseReportUrl = propertiesElement.GetUri(dataservices + "LicenseReportUrl", null), - LicenseUrl = propertiesElement.GetUri(dataservices + "LicenseUrl", null), - MinClientVersion = propertiesElement.GetString(dataservices + "MinClientVersion", null), - PackageHash = propertiesElement.GetString(dataservices + "PackageHash", null), - PackageHashAlgorithm = propertiesElement.GetString(dataservices + "PackageHashAlgorithm", null), - PackageSize = propertiesElement.GetInt64(dataservices + "PackageSize", 0), - ProjectUrl = propertiesElement.GetUri(dataservices + "ProjectUrl", null), - Published = propertiesElement.GetDateTimeOffset(dataservices + "Published", null), - ReleaseNotes = propertiesElement.GetString(dataservices + "ReleaseNotes", null), - ReportAbuseUrl = propertiesElement.GetUri(dataservices + "ReportAbuseUrl", null), - RequireLicenseAcceptance = propertiesElement.GetBool(dataservices + "RequireLicenseAcceptance", false), - Summary = entry.GetString(atom + "summary", null), - Tags = propertiesElement.GetString(dataservices + "Tags", null), - Title = propertiesElement.GetString(dataservices + "Title", null) - }; - - if (!includeDownloadUrl) - { - // Don't read the package URL from OData. Instead, allow the callers to build the package URL - // themselves. This is important because the URL in the OData feed is not pointing directly to - // Azure Blob Storage, which is required by the VCS validator. - package.DownloadUrl = null; - } - - result.Add(package); - } - - _logger.LogInformation($"Finished retrieving packages from URL {{{TraceConstant.Url}}}.", uri); - - if (continuationsToFollow > 0) - { - var links = feed.Elements(atom + "link"); - var continuationLink = links.FirstOrDefault( - link => link.Attribute("rel") != null && link.Attribute("rel").Value == "next"); - - if (continuationLink != null) - { - var href = continuationLink.Attribute("href").Value; - - _logger.LogInformation($"Start following continuation token {{{TraceConstant.Url}}}...", href); - result.AddRange(await GetPackagesAsync(new Uri(href), includeDownloadUrl, continuationsToFollow - 1)); - _logger.LogInformation($"Finished following continuation token {{{TraceConstant.Url}}}.", href); - } - } - - return result; - } - } -} \ No newline at end of file diff --git a/src/Validation.Common/OData/NuGetV2PackageComparer.cs b/src/Validation.Common/OData/NuGetV2PackageComparer.cs deleted file mode 100644 index 71517438a..000000000 --- a/src/Validation.Common/OData/NuGetV2PackageComparer.cs +++ /dev/null @@ -1,28 +0,0 @@ -// 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. - -using System; -using System.Collections.Generic; - -namespace NuGet.Jobs.Validation.Common.OData -{ - public class NuGetV2PackageEqualityComparer - : IEqualityComparer - { - public bool Equals(NuGetPackage x, NuGetPackage y) - { - return String.Equals(x.Id, y.Id, StringComparison.OrdinalIgnoreCase) - && String.Equals(x.NormalizedVersion, y.NormalizedVersion, StringComparison.OrdinalIgnoreCase); - } - - public int GetHashCode(NuGetPackage obj) - { - unchecked - { - var hashCode = (obj.Id != null ? StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Id) : 0); - hashCode = (hashCode * 397) ^ (obj.NormalizedVersion != null ? StringComparer.OrdinalIgnoreCase.GetHashCode(obj.NormalizedVersion) : 0); - return hashCode; - } - } - } -} \ No newline at end of file diff --git a/src/Validation.Common/PackageValidationAudit.cs b/src/Validation.Common/PackageValidationAudit.cs deleted file mode 100644 index 01241094e..000000000 --- a/src/Validation.Common/PackageValidationAudit.cs +++ /dev/null @@ -1,33 +0,0 @@ -// 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. - -using System; -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace NuGet.Jobs.Validation.Common -{ - public class PackageValidationAudit - { - public PackageValidationAudit() - { - Entries = new List(); - } - - public Guid ValidationId { get; set; } - public string PackageId { get; set; } - public string PackageVersion { get; set; } - public string[] Validators { get; set; } - public DateTimeOffset Started { get; set; } - public DateTimeOffset? Completed { get; set; } - - public NuGetPackage Package { get; set; } - - public List Entries { get; set; } - - public string Humanize() - { - return JsonConvert.SerializeObject(this, Formatting.Indented); - } - } -} \ No newline at end of file diff --git a/src/Validation.Common/PackageValidationAuditEntry.cs b/src/Validation.Common/PackageValidationAuditEntry.cs deleted file mode 100644 index c990d55d7..000000000 --- a/src/Validation.Common/PackageValidationAuditEntry.cs +++ /dev/null @@ -1,16 +0,0 @@ -// 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. - -using System; - -namespace NuGet.Jobs.Validation.Common -{ - public class PackageValidationAuditEntry - { - public string ValidatorName { get; set; } - public ValidationEvent EventId { get; set; } - public string EventName => EventId.ToString(); - public string Message { get; set; } - public DateTimeOffset Timestamp { get; set; } - } -} \ No newline at end of file diff --git a/src/Validation.Common/PackageValidationAuditor.cs b/src/Validation.Common/PackageValidationAuditor.cs deleted file mode 100644 index bb82a3868..000000000 --- a/src/Validation.Common/PackageValidationAuditor.cs +++ /dev/null @@ -1,255 +0,0 @@ -// 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. - -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.WindowsAzure.Storage; -using Microsoft.WindowsAzure.Storage.Blob; -using Newtonsoft.Json; -using NuGet.Jobs.Validation.Common.Extensions; - -namespace NuGet.Jobs.Validation.Common -{ - public class PackageValidationAuditor : IPackageValidationAuditor - { - private readonly CloudBlobContainer _auditsContainer; - private readonly ILogger _logger; - - public PackageValidationAuditor(CloudStorageAccount cloudStorageAccount, string containerNamePrefix, ILoggerFactory loggerFactory) - { - var cloudBlobClient = cloudStorageAccount.CreateCloudBlobClient(); - _auditsContainer = cloudBlobClient.GetContainerReference(containerNamePrefix + "-audit"); - _auditsContainer.CreateIfNotExists(BlobContainerPublicAccessType.Blob); - _logger = loggerFactory.CreateLogger(); - } - - public async Task StartAuditAsync(Guid validationId, string[] validators, DateTimeOffset started, string packageId, string packageVersion, NuGetPackage package) - { - _logger.LogInformation("Start writing Start PackageValidationAudit for " + - $"validation {{{TraceConstant.ValidationId}}} " + - $"- package {{{TraceConstant.PackageId}}} " + - $"v. {{{TraceConstant.PackageVersion}}}...", - validationId, - package.Id, - packageVersion); - - var packageValidationAudit = new PackageValidationAudit(); - packageValidationAudit.ValidationId = validationId; - packageValidationAudit.PackageId = packageId; - packageValidationAudit.PackageVersion = packageVersion; - packageValidationAudit.Package = package; - packageValidationAudit.Started = started; - packageValidationAudit.Validators = validators; - - await StoreAuditAsync( - validationId, - packageValidationAudit.PackageId, - packageValidationAudit.PackageVersion, - _ => packageValidationAudit, - uploadAccessCondition: AccessCondition.GenerateIfNoneMatchCondition("*")); - - _logger.LogInformation("Finished writing Start PackageValidationAudit for " + - $"validation {{{TraceConstant.ValidationId}}} " + - $"- package {{{TraceConstant.PackageId}}} " + - $"v. {{{TraceConstant.PackageVersion}}}.", - validationId, - package.Id, - packageVersion); - } - - public async Task WriteAuditEntryAsync(Guid validationId, string packageId, string packageVersion, PackageValidationAuditEntry entry) - { - await WriteAuditEntriesAsync(validationId, packageId, packageVersion, new[] { entry }); - } - - public async Task WriteAuditEntriesAsync(Guid validationId, string packageId, string packageVersion, IEnumerable entries) - { - _logger.LogInformation("Start writing AuditEntry PackageValidationAudit for " + - $"validation {{{TraceConstant.ValidationId}}} " + - $"- package {{{TraceConstant.PackageId}}} " + - $"v. {{{TraceConstant.PackageVersion}}}...", - validationId, - packageId, - packageVersion); - - await StoreAuditAsync(validationId, packageId, packageVersion, - packageValidationAudit => - { - packageValidationAudit.Entries.AddRange(entries); - return packageValidationAudit; - }); - - _logger.LogInformation("Finished writing AuditEntry PackageValidationAudit for " + - $"validation {{{TraceConstant.ValidationId}}} " + - $"- package {{{TraceConstant.PackageId}}} " + - $"v. {{{TraceConstant.PackageVersion}}}.", - validationId, - packageId, - packageVersion); - } - - public async Task CompleteAuditAsync(Guid validationId, DateTimeOffset completed, string packageId, string packageVersion) - { - _logger.LogInformation("Start writing Complete PackageValidationAudit for " + - $"validation {{{TraceConstant.ValidationId}}} " + - $"- package {{{TraceConstant.PackageId}}} " + - $"v. {{{TraceConstant.PackageVersion}}}...", - validationId, - packageId, - packageVersion); - - try - { - await StoreAuditAsync(validationId, packageId, packageVersion, - packageValidationAudit => - { - packageValidationAudit.Completed = completed; - return packageValidationAudit; - }); - } - catch (Exception e) - { - _logger.LogError(TraceEvent.PackageValidationAuditorException, e, "Failed to update the audit blob for " + - $"validation {{{TraceConstant.ValidationId}}} " + - $"- package {{{TraceConstant.PackageId}}} " + - $"{{{TraceConstant.PackageVersion}}}", - validationId, - packageId, - packageVersion); - throw; - } - - _logger.LogInformation("Finished writing Complete PackageValidationAudit for " + - $"validation {{{TraceConstant.ValidationId}}} " + - $"- package {{{TraceConstant.PackageId}}} " + - $"{{{TraceConstant.PackageVersion}}}.", - validationId, - packageId, - packageVersion); - } - - public Task StoreAuditAsync( - Guid validationId, - string packageId, - string packageVersion, - Func updateAudit) - { - return StoreAuditAsync( - validationId, - packageId, - packageVersion, - updateAudit, - uploadAccessCondition: null); - } - - public async Task StoreAuditAsync( - Guid validationId, - string packageId, - string packageVersion, - Func updateAudit, - AccessCondition uploadAccessCondition) - { - _logger.LogInformation($"Started updating audit blob for validation {{{TraceConstant.ValidationId}}} " + - $"- package {{{TraceConstant.PackageId}}} " + - $"{{{TraceConstant.PackageVersion}}}", - validationId, - packageId, - packageVersion); - - var blob = _auditsContainer.GetBlockBlobReference( - GenerateAuditFileName( - validationId, - packageId, - packageVersion)); - - var leaseId = await blob.TryAcquireLeaseAsync(TimeSpan.FromSeconds(30), CancellationToken.None); - _logger.LogInformation($"Got blob lease: {{LeaseId}} for validation {{{TraceConstant.ValidationId}}}", - leaseId, - validationId); - - var accessCondition = !string.IsNullOrEmpty(leaseId) - ? AccessCondition.GenerateLeaseCondition(leaseId) - : null; - - PackageValidationAudit packageValidationAudit; - - if (await blob.ExistsAsync()) - { - _logger.LogInformation($"Updating existing auditing blob for validation {{{TraceConstant.ValidationId}}}", - validationId); - var json = await blob.DownloadTextAsync(Encoding.UTF8, accessCondition, null, null); - packageValidationAudit = JsonConvert.DeserializeObject(json); - packageValidationAudit = updateAudit(packageValidationAudit); - } - else - { - _logger.LogInformation($"Creating new auditing blob for validation {{{TraceConstant.ValidationId}}}", - validationId); - packageValidationAudit = updateAudit(null); - } - - _logger.LogInformation($"Saving updated audit blob for validation {{{TraceConstant.ValidationId}}}", - validationId); - await blob.UploadTextAsync( - JsonConvert.SerializeObject(packageValidationAudit), - Encoding.UTF8, - uploadAccessCondition ?? accessCondition, - null, - null); - - blob.Properties.ContentType = "application/json"; - _logger.LogInformation($"Setting blob properties for validation {{{TraceConstant.ValidationId}}}", - validationId); - await blob.SetPropertiesAsync(accessCondition, null, null); - - if (accessCondition != null) - { - try - { - _logger.LogInformation($"Releasing lease for validation {{{TraceConstant.ValidationId}}}", - validationId); - await blob.ReleaseLeaseAsync(AccessCondition.GenerateLeaseCondition(leaseId)); - } - catch (Exception e) - { - // intentional, lease may already have been expired - _logger.LogInformation( - TraceEvent.AuditBlobLeaseReleaseFailed, - e, - $"Exception occurred while releasing the lease for validation {{{TraceConstant.ValidationId}}}", - validationId); - } - } - _logger.LogInformation($"Completed updating audit blob for validation {{{TraceConstant.ValidationId}}}", - validationId); - } - - public async Task ReadAuditAsync(Guid validationId, string packageId, string packageVersion) - { - var blob = _auditsContainer.GetBlockBlobReference( - GenerateAuditFileName( - validationId, - packageId, - packageVersion)); - - if (await blob.ExistsAsync()) - { - var json = await blob.DownloadTextAsync(); - return JsonConvert.DeserializeObject(json); - } - else - { - return null; - } - } - - private static string GenerateAuditFileName(Guid validationId, string packageId, string packageVersion) - { - return $"{packageId}/{packageVersion}/{validationId}.json"; - } - } -} diff --git a/src/Validation.Common/PackageValidationEntity.cs b/src/Validation.Common/PackageValidationEntity.cs deleted file mode 100644 index 1b129d396..000000000 --- a/src/Validation.Common/PackageValidationEntity.cs +++ /dev/null @@ -1,75 +0,0 @@ -// 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. - -using System; -using System.Linq; -using System.Collections.Generic; -using Microsoft.WindowsAzure.Storage.Table; -using NuGet.Jobs.Validation.Common.Validators; - -namespace NuGet.Jobs.Validation.Common -{ - public class PackageValidationEntity - : TableEntity - { - private DateTimeOffset _created; - private Guid _validationId; - - public Guid ValidationId - { - get { return _validationId; } - set - { - _validationId = value; - - // RowKey is our validation id - RowKey = value.ToString(); - } - } - - public string PackageId { get; set; } - public string PackageVersion { get; set; } - - public DateTimeOffset Created - { - get { return _created; } - set - { - _created = value; - - // PartitionKey is a reversed timestamp (entries ordered by date descending) - PartitionKey = string.Format("{0:D19}", DateTimeOffset.MaxValue.Ticks - _created.Ticks); - } - } - - public DateTimeOffset? Finished { get; set; } - public string RequestedValidators { get; set; } - public string CompletedValidators { get; set; } - public int ValidationResult { get; set; } - - public void ValidatorCompleted(string validator, ValidationResult result) - { - var completedValidators = GetCompletedValidatorsList(); - if (!completedValidators.Contains(validator)) - { - completedValidators.Add(validator); - CompletedValidators = string.Join(";", completedValidators.OrderBy(v => v)); - } - - if (ValidationResult >= 0) - { - ValidationResult = (int)result; - } - - if (RequestedValidators == CompletedValidators) - { - Finished = DateTimeOffset.UtcNow; - } - } - - public List GetCompletedValidatorsList() - { - return CompletedValidators.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries).ToList(); - } - } -} \ No newline at end of file diff --git a/src/Validation.Common/PackageValidationMessage.cs b/src/Validation.Common/PackageValidationMessage.cs deleted file mode 100644 index e051506fc..000000000 --- a/src/Validation.Common/PackageValidationMessage.cs +++ /dev/null @@ -1,20 +0,0 @@ -// 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. - -using System; - -namespace NuGet.Jobs.Validation.Common -{ - public class PackageValidationMessage - { - public Guid ValidationId { get; set; } - public string PackageId { get; set; } - public string PackageVersion { get; set; } - public NuGetPackage Package { get; set; } - - public string MessageId { get; set; } - public DateTimeOffset? InsertionTime { get; set; } - public string PopReceipt { get; set; } - public int DequeueCount { get; set; } - } -} \ No newline at end of file diff --git a/src/Validation.Common/PackageValidationOrchestrationCursor.cs b/src/Validation.Common/PackageValidationOrchestrationCursor.cs deleted file mode 100644 index 75eea40a6..000000000 --- a/src/Validation.Common/PackageValidationOrchestrationCursor.cs +++ /dev/null @@ -1,70 +0,0 @@ -// 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. - -using System; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.WindowsAzure.Storage; -using Microsoft.WindowsAzure.Storage.Blob; -using Newtonsoft.Json.Linq; - -namespace NuGet.Jobs.Validation.Common -{ - public class PackageValidationOrchestrationCursor - { - private readonly CloudBlockBlob _cursorBlob; - private readonly ILogger _logger; - - public PackageValidationOrchestrationCursor(CloudStorageAccount cloudStorageAccount, string containerName, string cursorName, ILoggerFactory loggerFactory) - { - var cloudBlobClient = cloudStorageAccount.CreateCloudBlobClient(); - var container = cloudBlobClient.GetContainerReference(containerName); - container.CreateIfNotExists(); - - _cursorBlob = container.GetBlockBlobReference(cursorName); - _logger = loggerFactory.CreateLogger(); - } - - public DateTimeOffset? LastCreated { get; set; } - public DateTimeOffset? LastEdited { get; set; } - - public async Task LoadAsync() - { - _logger.LogInformation($"Start loading cursor from {{{TraceConstant.Url}}}...", _cursorBlob.Uri); - - if (await _cursorBlob.ExistsAsync()) - { - var json = await _cursorBlob.DownloadTextAsync(); - - var cursorObject = JObject.Parse(json); - LastCreated = cursorObject["lastCreated"].ToObject(); - LastEdited = cursorObject["lastEdited"].ToObject(); - - _logger.LogInformation($"Cursor value: {{{TraceConstant.CursorValue}}}", json); - } - - _logger.LogInformation($"Finished loading cursor from {{{TraceConstant.Url}}}.", _cursorBlob.Uri); - } - - public async Task SaveAsync() - { - _logger.LogInformation($"Start saving cursor to {{{TraceConstant.Url}}}...", _cursorBlob.Uri); - - var cursorObject = new JObject - { - { "lastCreated", LastCreated?.ToString("O") }, - { "lastEdited", LastEdited?.ToString("O") }, - }; - - var json = cursorObject.ToString(); - - await _cursorBlob.UploadTextAsync(json); - - _cursorBlob.Properties.ContentType = "application/json"; - await _cursorBlob.SetPropertiesAsync(); - - _logger.LogInformation($"Cursor value: {{{TraceConstant.CursorValue}}}", json); - _logger.LogInformation($"Finished saving cursor to {{{TraceConstant.Url}}}.", _cursorBlob.Uri); - } - } -} \ No newline at end of file diff --git a/src/Validation.Common/PackageValidationQueue.cs b/src/Validation.Common/PackageValidationQueue.cs deleted file mode 100644 index 84592994a..000000000 --- a/src/Validation.Common/PackageValidationQueue.cs +++ /dev/null @@ -1,106 +0,0 @@ -// 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. - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.WindowsAzure.Storage; -using Microsoft.WindowsAzure.Storage.Queue; -using Newtonsoft.Json; - -namespace NuGet.Jobs.Validation.Common -{ - public class PackageValidationQueue - { - private readonly ConcurrentDictionary _queues = new ConcurrentDictionary(); - private readonly string _containerNamePrefix; - private readonly CloudQueueClient _cloudQueueClient; - private readonly ILogger _logger; - - public PackageValidationQueue(CloudStorageAccount cloudStorageAccount, string containerNamePrefix, ILoggerFactory loggerFactory) - { - _containerNamePrefix = containerNamePrefix; - _cloudQueueClient = cloudStorageAccount.CreateCloudQueueClient(); - _logger = loggerFactory.CreateLogger(); - } - - private async Task GetQueueAsync(string validatorName) - { - var queueName = (_containerNamePrefix + validatorName).ToLowerInvariant(); - - CloudQueue queue; - if (!_queues.TryGetValue(queueName, out queue)) - { - queue = _cloudQueueClient.GetQueueReference(queueName); - await queue.CreateIfNotExistsAsync(); - _queues.TryAdd(queueName, queue); - } - return queue; - } - - public async Task EnqueueAsync(string validatorName, PackageValidationMessage message) - { - message.Package = message.Package.TruncateForAzureQueue(); - - _logger.LogInformation($"Start enqueue validation {{{TraceConstant.ValidatorName}}} " + - $"{{{TraceConstant.ValidationId}}} " + - $"- package {{{TraceConstant.PackageId}}} " + - $"v. {{{TraceConstant.PackageVersion}}}...", - validatorName, - message.ValidationId, - message.PackageId, - message.PackageVersion); - - var queue = await GetQueueAsync(validatorName); - await queue.AddMessageAsync(new CloudQueueMessage(JsonConvert.SerializeObject(message))); - - _logger.LogInformation($"Finished enqueue validation {{{TraceConstant.ValidatorName}}} " + - $"{{{TraceConstant.ValidationId}}} " + - $"- package {{{TraceConstant.PackageId}}} " + - $"v. {{{TraceConstant.PackageVersion}}}.", - validatorName, - message.ValidationId, - message.PackageId, - message.PackageVersion); - } - - public async Task> DequeueAsync(string validatorName, int messageCount, TimeSpan visibilityTimeout) - { - _logger.LogInformation($"Start dequeue validation {{{TraceConstant.ValidatorName}}} " + - $"(maximum {{{TraceConstant.MessageCount}}} items)...", - validatorName, - messageCount); - - var results = new List(); - var queue = await GetQueueAsync(validatorName); - var messages = await queue.GetMessagesAsync(messageCount, visibilityTimeout, null, null); - foreach (var message in messages) - { - var deserializedMessage = JsonConvert.DeserializeObject(message.AsString); - deserializedMessage.MessageId = message.Id; - deserializedMessage.InsertionTime = message.InsertionTime; - deserializedMessage.PopReceipt = message.PopReceipt; - deserializedMessage.DequeueCount = message.DequeueCount; - - results.Add(deserializedMessage); - } - - _logger.LogInformation($"Finished dequeue validation {{{TraceConstant.ValidatorName}}} " + - $"({{{TraceConstant.ResultCount}}} items).", validatorName, results.Count); - - return results; - } - - public async Task DeleteAsync(string validatorName, PackageValidationMessage message) - { - _logger.LogInformation($"Start complete validation {{{TraceConstant.ValidatorName}}}...", validatorName); - - var queue = await GetQueueAsync(validatorName); - await queue.DeleteMessageAsync(message.MessageId, message.PopReceipt); - - _logger.LogInformation($"Finished complete validation {{{TraceConstant.ValidatorName}}}.", validatorName); - } - } -} \ No newline at end of file diff --git a/src/Validation.Common/PackageValidationService.cs b/src/Validation.Common/PackageValidationService.cs deleted file mode 100644 index 6e96cf0cf..000000000 --- a/src/Validation.Common/PackageValidationService.cs +++ /dev/null @@ -1,109 +0,0 @@ -// 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. - -using System; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.WindowsAzure.Storage; - -namespace NuGet.Jobs.Validation.Common -{ - public class PackageValidationService : IPackageValidationService - { - private readonly PackageValidationTable _packageValidationTable; - private readonly PackageValidationQueue _packageValidationQueue; - private readonly PackageValidationAuditor _packageValidationAuditor; - private readonly INotificationService _notificationService; - private readonly ILogger _logger; - - public PackageValidationService(CloudStorageAccount cloudStorageAccount, string containerNamePrefix, ILoggerFactory loggerFactory) - { - _packageValidationTable = new PackageValidationTable(cloudStorageAccount, containerNamePrefix); - _packageValidationQueue = new PackageValidationQueue(cloudStorageAccount, containerNamePrefix, loggerFactory); - _packageValidationAuditor = new PackageValidationAuditor(cloudStorageAccount, containerNamePrefix, loggerFactory); - _notificationService = new NotificationService(cloudStorageAccount, containerNamePrefix); - _logger = loggerFactory.CreateLogger(); - } - - public Task StartValidationProcessAsync(NuGetPackage package, string[] validators) - { - return StartValidationProcessAsync( - package, - validators, - Guid.NewGuid()); - } - - public async Task StartValidationProcessAsync(NuGetPackage package, string[] validators, Guid validationId) - { - var packageId = package.Id; - var packageVersion = package.GetVersion(); - var created = DateTimeOffset.UtcNow; - - _logger.LogInformation( - $"Starting validation process for validation {{{TraceConstant.ValidationId}}} " + - $"- package {{{TraceConstant.PackageId}}} " + - $"v. {{{TraceConstant.PackageVersion}}}...", - validationId, - packageId, - packageVersion); - - // Write a tracking entity - await _packageValidationTable.StoreAsync(new PackageValidationEntity - { - ValidationId = validationId, - PackageId = packageId, - PackageVersion = packageVersion, - RequestedValidators = string.Join(";", validators.OrderBy(v => v)), - CompletedValidators = string.Empty, - Created = created - }); - - // Enqueue validations - foreach (var validator in validators) - { - var message = new PackageValidationMessage - { - ValidationId = validationId, - PackageId = packageId, - PackageVersion = packageVersion, - Package = package - }; - - await _packageValidationQueue.EnqueueAsync(validator, message); - } - - // Write audit entry so we can get all the nitty-gritty details on our validation process - try - { - await _packageValidationAuditor.StartAuditAsync(validationId, validators, created, packageId, packageVersion, package); - } - catch (Exception ex) - { - var logMessage = $"Error while starting validation process for validation {validationId} - package {packageId} {packageVersion}: {ex.Message} {ex.StackTrace}"; - - _logger.LogError(TraceEvent.StartValidationAuditFailed, ex, - $"Error while starting validation process for validation {{{TraceConstant.ValidationId}}} " + - $"- package {{{TraceConstant.PackageId}}} " + - $"v. {{{TraceConstant.PackageVersion}}}", - validationId, - packageId, - packageVersion); - - await _notificationService.SendNotificationAsync( - "exception", - "Error while starting validation process for validation", - logMessage); - - throw; - } - - _logger.LogInformation($"Started validation process for validation {{{TraceConstant.ValidationId}}} " + - $"- package {{{TraceConstant.PackageId}}} " + - $"v. {{{TraceConstant.PackageVersion}}}", - validationId, - packageId, - packageVersion); - } - } -} \ No newline at end of file diff --git a/src/Validation.Common/PackageValidationTable.cs b/src/Validation.Common/PackageValidationTable.cs deleted file mode 100644 index 3f7701bfc..000000000 --- a/src/Validation.Common/PackageValidationTable.cs +++ /dev/null @@ -1,40 +0,0 @@ -// 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. - -using System; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.WindowsAzure.Storage; -using Microsoft.WindowsAzure.Storage.Table; - -namespace NuGet.Jobs.Validation.Common -{ - public class PackageValidationTable - { - private readonly CloudTable _validationTable; - - public PackageValidationTable(CloudStorageAccount cloudStorageAccount, string containerNamePrefix) - { - var cloudTableClient = cloudStorageAccount.CreateCloudTableClient(); - _validationTable = cloudTableClient.GetTableReference(containerNamePrefix + "validation"); - _validationTable.CreateIfNotExists(); - } - - public async Task StoreAsync(PackageValidationEntity entity) - { - await _validationTable.ExecuteAsync(TableOperation.InsertOrReplace(entity)); - } - - public async Task GetValidationAsync(Guid validationId) - { - var result = await _validationTable.ExecuteQuerySegmentedAsync( - new TableQuery - { - FilterString = $"RowKey eq '{validationId}'", - TakeCount = 1 - }, null); - - return result.FirstOrDefault(); - } - } -} \ No newline at end of file diff --git a/src/Validation.Common/Properties/AssemblyInfo.cs b/src/Validation.Common/Properties/AssemblyInfo.cs deleted file mode 100644 index a0bf2b83e..000000000 --- a/src/Validation.Common/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Validation.Common")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Validation.Common")] -[assembly: AssemblyCopyright("Copyright © 2016")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("2539ddf3-0cc5-4a03-b5f9-39b47744a7bd")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/Validation.Common/TraceConstant.cs b/src/Validation.Common/TraceConstant.cs deleted file mode 100644 index bb34be972..000000000 --- a/src/Validation.Common/TraceConstant.cs +++ /dev/null @@ -1,26 +0,0 @@ -// 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. - -namespace NuGet.Jobs.Validation.Common -{ - public static class TraceConstant - { - public const string EventName = "EventName"; - public const string ValidatorName = "ValidatorName"; - public const string ValidationId = "ValidationId"; - public const string ValidationResult = "ValidationResult"; - public const string PackageId = "PackageId"; - public const string PackageVersion = "PackageVersion"; - - public const string PackageUnclean = "PackageUnclean"; - public const string RequestNotFound = "RequestNotFound"; - public const string InvestigationNeeded = "InvestigationNeeded"; - public const string Url = "Url"; - public const string CursorValue = "CursorValue"; - public const string MessageCount = "MessageCount"; - public const string ResultCount = "ResultCount"; - - public const string Comment = "Comment"; - public const string Alias = "Alias"; - } -} diff --git a/src/Validation.Common/TraceEvent.cs b/src/Validation.Common/TraceEvent.cs deleted file mode 100644 index 2580d7b95..000000000 --- a/src/Validation.Common/TraceEvent.cs +++ /dev/null @@ -1,28 +0,0 @@ -// 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. - -using Microsoft.Extensions.Logging; - -namespace NuGet.Jobs.Validation.Common -{ - public static class TraceEvent - { - public static readonly EventId ValidatorException = CreateEventId(0, "Validator exception"); - public static readonly EventId CommandLineProcessingFailed = CreateEventId(1, "Failed to process Job's command line arguments"); - public static readonly EventId StartValidationAuditFailed = CreateEventId(2, "Failed to save audit info regarding validation queueing"); - public static readonly EventId FailedToProcessArguments = CreateEventId(3, "Failed to process arguments"); - public static readonly EventId HelperFailed = CreateEventId(4, "Failed to run helper action"); - public static readonly EventId AuditBlobLeaseReleaseFailed = CreateEventId(5, "Exception occurred while trying to release the lease on audit blob"); - public static readonly EventId PackageValidationAuditorException = CreateEventId(6, "Exception occurred while trying to update the package validation auditing blob"); - - /// - /// Random number used as a base for EventIds and to make sure they don't clash with - /// other Ids across the project. - /// - private const int StartId = 1186511685; - private static EventId CreateEventId(int offset, string name) - { - return new EventId(StartId + offset, name); - } - } -} diff --git a/src/Validation.Common/TraceHelper.cs b/src/Validation.Common/TraceHelper.cs deleted file mode 100644 index 1083edf65..000000000 --- a/src/Validation.Common/TraceHelper.cs +++ /dev/null @@ -1,87 +0,0 @@ -// 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. - -using System; -using Microsoft.Extensions.Logging; - -namespace NuGet.Jobs.Validation.Common -{ - public static class TraceHelper - { - /// - /// Tracks the result of the validation. - /// - /// Logger object to use - /// The name of validator attempted - /// Validation ID of the finished validator - /// Validation result - /// Package ID - /// Package name - public static void TrackValidatorResult(this ILogger logger, string validatorName, Guid validationId, string result, string packageId, string packageVersion) - { - logger.LogInformation($"{{{TraceConstant.EventName}}}: " + - $"{{{TraceConstant.ValidatorName}}} " + - $"ValidationId: {{{TraceConstant.ValidationId}}} " + - $"for package {{{TraceConstant.PackageId}}} " + - $"{{{TraceConstant.PackageVersion}}} " + - $"resulted in {{Result}}", - "ValidatorResult", - validatorName, - validationId, - packageId, - packageVersion, - result); - } - - /// - /// Tracks the result of the validation with additional information - /// - /// Logger object to use - /// The name of validator attempted - /// Validation ID of the finished validator - /// Validation result - /// Package ID - /// Package name - /// Additional information you'd want logged - public static void TrackValidatorResult(this ILogger logger, string validatorName, Guid validationId, string result, string packageId, string packageVersion, string additionalInformation) - { - logger.LogInformation($"{{{TraceConstant.EventName}}}: " + - $"{{{TraceConstant.ValidatorName}}} " + - $"ValidationId: {{{TraceConstant.ValidationId}}} " + - $"for package {{{TraceConstant.PackageId}}} " + - $"{{{TraceConstant.PackageVersion}}} " + - $"resulted in {{Result}}, " + - $"additional info: {{AdditionalInformation}}", - "ValidatorResult", - validatorName, - validationId, - packageId, - packageVersion, - result, - additionalInformation); - } - - /// - /// Tracks the exception occured during validation - /// - /// Logger object to use - /// The name of the validator that was running when exception happened - /// Validation ID that was being processed when exception happened - /// The exception to track - /// Package ID - /// Package name - public static void TrackValidatorException(this ILogger logger, string validatorName, Guid validationId, Exception ex, string packageId, string packageVersion) - { - logger.LogError(TraceEvent.ValidatorException, ex, - $"{{{TraceConstant.EventName}}} " + - $"occurred while running {{{TraceConstant.ValidatorName}}} {{{TraceConstant.ValidationId}}}" + - $"on package {{{TraceConstant.PackageId}}}" + - $"{{{TraceConstant.PackageVersion}}}", - "ValidatorException", - validationId, - validatorName, - packageId, - packageVersion); - } - } -} diff --git a/src/Validation.Common/Validation.Common.csproj b/src/Validation.Common/Validation.Common.csproj deleted file mode 100644 index b229a5777..000000000 --- a/src/Validation.Common/Validation.Common.csproj +++ /dev/null @@ -1,132 +0,0 @@ - - - - - Debug - AnyCPU - {2539DDF3-0CC5-4A03-B5F9-39B47744A7BD} - Library - Properties - NuGet.Jobs.Validation.Common - Validation.Common - v4.6.2 - 512 - true - - - - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Designer - - - - - {4b4b1efb-8f33-42e6-b79f-54e7f3293d31} - NuGet.Jobs.Common - - - - - 2.2.0 - - - 1.0.0 - - - 3.0.1 - - - 4.1.0 - - - 3.2.0 - - - 7.1.2 - - - 0.3.0 - runtime; build; native; contentfiles; analyzers - all - - - - - ..\..\build - $(BUILD_SOURCESDIRECTORY)\build - $(NuGetBuildPath) - none - - - - - \ No newline at end of file diff --git a/src/Validation.Common/ValidationEvent.cs b/src/Validation.Common/ValidationEvent.cs deleted file mode 100644 index 94c39e59d..000000000 --- a/src/Validation.Common/ValidationEvent.cs +++ /dev/null @@ -1,72 +0,0 @@ -// 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. - -using System; - -namespace NuGet.Jobs.Validation.Common -{ - public enum ValidationEvent - { - /// - /// Virus scan request is about to be sent - /// - BeforeVirusScanRequest, - - /// - /// The validation queue item was deadlettered after several attempts of processing - /// - Deadlettered, - - /// - /// The detail item passed with a result - /// - NotCleanReason, - - /// - /// Virus scan service reported package as clean - /// - PackageClean, - - /// - /// Deprecated when the unzip validator was removed. - /// - [Obsolete] - PackageDownloaded, - - /// - /// Virus scan service reported package as not clean - /// - PackageNotClean, - - /// - /// Virus scan service reported its failure to scan package (it does *not* mean package is not clean) - /// - ScanFailed, - - /// - /// The detail item passed with result - /// - ScanFailureReason, - - /// - /// An exception was thrown during validator execution - /// - ValidatorException, - - /// - /// The virus scan request was submitted - /// - VirusScanRequestSent, - - /// - /// Sending the virus scanning request had failed - /// - VirusScanRequestFailed, - - /// - /// Deprecated when the unzip validator was removed. - /// - [Obsolete] - UnzipSucceeeded, - } -} diff --git a/src/Validation.Common/Validators/IValidator.cs b/src/Validation.Common/Validators/IValidator.cs deleted file mode 100644 index 05599e6d4..000000000 --- a/src/Validation.Common/Validators/IValidator.cs +++ /dev/null @@ -1,16 +0,0 @@ -// 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. - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace NuGet.Jobs.Validation.Common.Validators -{ - public interface IValidator - { - string Name { get; } - TimeSpan VisibilityTimeout { get; } - Task ValidateAsync(PackageValidationMessage message, List auditEntries); - } -} \ No newline at end of file diff --git a/src/Validation.Common/Validators/ValidationException.cs b/src/Validation.Common/Validators/ValidationException.cs deleted file mode 100644 index 7c3f82f2c..000000000 --- a/src/Validation.Common/Validators/ValidationException.cs +++ /dev/null @@ -1,24 +0,0 @@ -// 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. - -using System; -using System.Runtime.Serialization; - -namespace NuGet.Jobs.Validation.Common.Validators.Vcs -{ - [Serializable] - public class ValidationException : Exception - { - public ValidationException(string message) : base(message) - { - } - - public ValidationException(string message, Exception innerException) : base(message, innerException) - { - } - - protected ValidationException(SerializationInfo info, StreamingContext context) : base(info, context) - { - } - } -} \ No newline at end of file diff --git a/src/Validation.Common/Validators/ValidationResult.cs b/src/Validation.Common/Validators/ValidationResult.cs deleted file mode 100644 index 3c9ee1db5..000000000 --- a/src/Validation.Common/Validators/ValidationResult.cs +++ /dev/null @@ -1,33 +0,0 @@ -// 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. - -namespace NuGet.Jobs.Validation.Common.Validators -{ - public enum ValidationResult - { - /// - /// Validation deadlettered. This value has to be negative. - /// - Deadlettered = -2, - - /// - /// Validation failed. This value has to be negative. - /// - Failed = -1, - - /// - /// Unknown. - /// - Unknown = 0, - - /// - /// Validation succeeded. - /// - Succeeded = 1, - - /// - /// Validation is dispatched on an asynchronous process that will report back on its own. - /// - Asynchronous = 2 - } -} \ No newline at end of file diff --git a/src/Validation.Common/Validators/ValidatorBase.cs b/src/Validation.Common/Validators/ValidatorBase.cs deleted file mode 100644 index b630fec32..000000000 --- a/src/Validation.Common/Validators/ValidatorBase.cs +++ /dev/null @@ -1,69 +0,0 @@ -// 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. - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace NuGet.Jobs.Validation.Common.Validators -{ - public abstract class ValidatorBase - : IValidator - { - private readonly string _packageUrlTemplate; - - public ValidatorBase(string packageUrlTemplate) - { - _packageUrlTemplate = packageUrlTemplate; - } - - public abstract string Name { get; } - - public virtual TimeSpan VisibilityTimeout - { - get - { - return TimeSpan.FromMinutes(5); - } - } - - public abstract Task ValidateAsync(PackageValidationMessage message, List auditEntries); - - protected void WriteAuditEntry(List auditEntries, string message, ValidationEvent validationEvent) - { - auditEntries.Add(new PackageValidationAuditEntry - { - Timestamp = DateTimeOffset.UtcNow, - ValidatorName = Name, - Message = message, - EventId = validationEvent, - }); - } - - protected string GetPackageUrl(PackageValidationMessage message) - { - string packageUrl; - if (message.Package.DownloadUrl != null) - { - packageUrl = message.Package.DownloadUrl.AbsoluteUri; - } - else - { - packageUrl = BuildStorageUrl(message.Package.Id, message.PackageVersion); - } - - return packageUrl; - } - - private string BuildStorageUrl(string packageId, string packageVersion) - { - // The VCS service needs a blob storage URL, which the NuGet API does not expose. - // Build one from a template here. - // Guarantee all URL transformations (such as URL encoding) are performed. - return new Uri(_packageUrlTemplate - .Replace("{id}", packageId) - .Replace("{version}", packageVersion) - .ToLowerInvariant()).AbsoluteUri; - } - } -} \ No newline at end of file diff --git a/src/Validation.Common/Validators/Vcs/VcsCallbackServer.cs b/src/Validation.Common/Validators/Vcs/VcsCallbackServer.cs deleted file mode 100644 index 3b3f1201a..000000000 --- a/src/Validation.Common/Validators/Vcs/VcsCallbackServer.cs +++ /dev/null @@ -1,344 +0,0 @@ -// 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. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.Owin; -using Microsoft.WindowsAzure.Storage; -using NuGet.ApplicationInsights.Owin; -using NuGet.Jobs.Validation.Common.Validators.Vcs; -using NuGet.Services.VirusScanning.Vcs.Callback; -using Owin; - -[assembly: OwinStartup(typeof(VcsCallbackServerStartup))] - -namespace NuGet.Jobs.Validation.Common.Validators.Vcs -{ - public class VcsCallbackServerStartup - { - private readonly VcsStatusCallbackParser _callbackParser = new VcsStatusCallbackParser(); - private readonly HashSet SuccessResults = new HashSet - { - "Pass", - "PassWithInfo", - "PassManual", - "PassWithWarn", - }; - - private readonly PackageValidationTable _packageValidationTable; - private readonly PackageValidationAuditor _packageValidationAuditor; - private readonly INotificationService _notificationService; - private readonly ILogger _logger; - - /// - /// Number of body characters to take for logging. - /// - /// - /// Callback service is available to be queried from anywhere and hence the body may be of any size. - /// In situations when we want to log the body, we don't want to log potentially Multi-MB bodies, so, - /// we'll only take a "reasonable" (that would fit most of the calls we really expect) amount from - /// the beginning. - /// - private const int ReasonableBodySize = 2048; - - private static class State - { - public const string Complete = "Complete"; - public const string Released = "Released"; - } - - public VcsCallbackServerStartup() - { - // Configure to get values from keyvault - var configurationService = new ConfigurationService(new SecretReaderFactory()); - - // Get configuration - var cloudStorageAccount = CloudStorageAccount.Parse(configurationService.Get("DataStorageAccount").Result); - var containerName = configurationService.Get("ContainerName").Result; - - string instrumentationKey = configurationService.Get("ApplicationInsightsInstrumentationKey").Result; - Services.Logging.ApplicationInsights.Initialize(instrumentationKey); - ILoggerFactory loggerFactory = Services.Logging.LoggingSetup.CreateLoggerFactory(); - _logger = loggerFactory.CreateLogger(); - - // Services - _packageValidationTable = new PackageValidationTable(cloudStorageAccount, containerName); - _packageValidationAuditor = new PackageValidationAuditor(cloudStorageAccount, containerName, loggerFactory); - _notificationService = new NotificationService(cloudStorageAccount, containerName); - } - - public void Configuration(IAppBuilder app) - { - // Ensure that SSLv3 is disabled and that Tls v1.2 is enabled. - ServicePointManager.SecurityProtocol &= ~SecurityProtocolType.Ssl3; - ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; - - if (Services.Logging.ApplicationInsights.Initialized) - { - app.Use(); - } - - app.Run(Invoke); - } - - public async Task Invoke(IOwinContext context) - { - if (context.Request.Method == "POST" && context.Request.ContentType.Contains("text/xml")) - { - // VCS callback request - await ProcessRequest(context.Request.Body); - - // The VCS caller requires a SOAP response. - context.Response.ContentType = "text/xml"; - await context.Response.WriteAsync(@" - - - - -"); - } - else if (context.Request.Method == "GET") - { - context.Response.ContentType = "text/plain"; - await context.Response.WriteAsync("OK"); - } - else - { - context.Response.StatusCode = (int)HttpStatusCode.BadRequest; - } - } - - private async Task ProcessRequest(Stream requestBody) - { - using (var bodyStreamReader = new StreamReader(requestBody)) - { - bool processedRequest = false; - - var body = await bodyStreamReader.ReadToEndAsync(); - var result = _callbackParser.ParseSoapMessage(body); - - // Find our validation - Guid validationId; - PackageValidationEntity validationEntity = null; - if (Guid.TryParse(result.SrcId, out validationId)) - { - validationEntity = await _packageValidationTable.GetValidationAsync(validationId); - if (validationEntity == null) - { - processedRequest = true; - _logger.TrackValidatorResult(VcsValidator.ValidatorName, validationId, TraceConstant.RequestNotFound, validationEntity.PackageId, validationEntity.PackageVersion); - - // Notify us about the fact that no valiation was found - await _notificationService.SendNotificationAsync( - "vcscallback-notfound", - "Validation " + validationId + " was not found.", - body); - } - else - { - if (validationEntity.GetCompletedValidatorsList().Contains(VcsValidator.ValidatorName)) - { - _logger.LogInformation($"Package already processed for validation {{{TraceConstant.ValidationId}}} " + - "with state={State}, result={Result} " + - $"for package {{{TraceConstant.PackageId}}} " + - $"v. {{{TraceConstant.PackageVersion}}}", - validationId, - result.State, - result.Result, - validationEntity.PackageId, - validationEntity.PackageVersion); - return; - } - } - } - - // Determine state of the VCS callback - if (validationEntity != null) - { - _logger.LogInformation($"Got VCS callback for validation {{{TraceConstant.ValidationId}}} " + - "with state={State}, result={Result} " + - $"for package {{{TraceConstant.PackageId}}} " + - $"v. {{{TraceConstant.PackageVersion}}}", - validationId, - result.State, - result.Result, - validationEntity.PackageId, - validationEntity.PackageVersion); - - // "The Request is in Manual State and the Request is cancelled." - // This denotes a manual verification is being carried out or has been carried out. - if ((result.State == State.Complete || result.State == State.Released) - && (result.Result == "Canceled" || result.Result == "Cancelled")) - { - processedRequest = true; - var services = result.Services?.Service; - if (services != null && services.Any(s => s.Name == "Scan" && s.State == "Complete" && s.Result == "Canceled")) - { - // Package scanned unclean - validationEntity.ValidatorCompleted(VcsValidator.ValidatorName, ValidationResult.Failed); - await _packageValidationTable.StoreAsync(validationEntity); - - _logger.TrackValidatorResult(VcsValidator.ValidatorName, validationId, TraceConstant.PackageUnclean, validationEntity.PackageId, validationEntity.PackageVersion); - var auditEntries = new List(); - auditEntries.Add(new PackageValidationAuditEntry - { - Timestamp = DateTimeOffset.UtcNow, - ValidatorName = VcsValidator.ValidatorName, - Message = "Package did not scan clean.", - EventId = ValidationEvent.PackageNotClean, - }); - - if (result.ResultReasons?.ResultReason != null) - { - foreach (var resultReason in result.ResultReasons.ResultReason) - { - auditEntries.Add(new PackageValidationAuditEntry - { - Timestamp = DateTimeOffset.UtcNow, - ValidatorName = VcsValidator.ValidatorName, - Message = resultReason.RefId + " " + resultReason.Result + " " + resultReason.Determination, - EventId = ValidationEvent.NotCleanReason, - }); - } - } - - await _packageValidationAuditor.WriteAuditEntriesAsync( - validationEntity.ValidationId, validationEntity.PackageId, validationEntity.PackageVersion, auditEntries); - - // Notify - await _notificationService.SendNotificationAsync( - $"vcscallback-notclean/{validationEntity.Created.ToString("yyyy-MM-dd")}", - $"Validation {validationId} ({validationEntity.PackageId} {validationEntity.PackageVersion}) returned {result.State} {result.Result}.", - body); - } - else - { - _logger.TrackValidatorResult(VcsValidator.ValidatorName, validationId, TraceConstant.InvestigationNeeded, validationEntity.PackageId, validationEntity.PackageVersion); - // To investigate - await _notificationService.SendNotificationAsync( - $"vcscallback-investigate/{validationEntity.Created.ToString("yyyy-MM-dd")}", - $"Validation {validationId} ({validationEntity.PackageId} {validationEntity.PackageVersion}) returned {result.State} {result.Result}.", - body); - } - } - - // "The Request is completed, with either of these four states: Results, Pass, PassWithInfo, PassManual" - // This denotes scan has completed and we have a pass (or results) - if (result.State == State.Complete || result.State == State.Released) - { - if (SuccessResults.Contains(result.Result)) - { - // The result is clean. - processedRequest = true; - validationEntity.ValidatorCompleted(VcsValidator.ValidatorName, ValidationResult.Succeeded); - await _packageValidationTable.StoreAsync(validationEntity); - - _logger.TrackValidatorResult(VcsValidator.ValidatorName, validationId, ValidationResult.Succeeded.ToString(), validationEntity.PackageId, validationEntity.PackageVersion); - await _packageValidationAuditor.WriteAuditEntryAsync(validationEntity.ValidationId, validationEntity.PackageId, validationEntity.PackageVersion, - new PackageValidationAuditEntry - { - Timestamp = DateTimeOffset.UtcNow, - ValidatorName = VcsValidator.ValidatorName, - Message = "Package scanned clean.", - EventId = ValidationEvent.PackageClean, - }); - } - else if (result.Result == "Results" || result.Result == "Fail") - { - // Potential issue, report back - processedRequest = true; - validationEntity.ValidatorCompleted(VcsValidator.ValidatorName, ValidationResult.Failed); - await _packageValidationTable.StoreAsync(validationEntity); - - _logger.TrackValidatorResult(VcsValidator.ValidatorName, - validationId, - ValidationResult.Failed.ToString(), - validationEntity.PackageId, - validationEntity.PackageVersion, - TruncateString(body, ReasonableBodySize)); - var auditEntries = new List(); - auditEntries.Add(new PackageValidationAuditEntry - { - Timestamp = DateTimeOffset.UtcNow, - ValidatorName = VcsValidator.ValidatorName, - Message = $"Package scan failed. Response: {body}", - EventId = ValidationEvent.ScanFailed, - }); - - if (result.ResultReasons?.ResultReason != null) - { - foreach (var resultReason in result.ResultReasons.ResultReason) - { - auditEntries.Add(new PackageValidationAuditEntry - { - Timestamp = DateTimeOffset.UtcNow, - ValidatorName = VcsValidator.ValidatorName, - Message = resultReason.RefId + " " + resultReason.Result + " " + resultReason.Determination, - EventId = ValidationEvent.ScanFailureReason, - }); - } - } - - await _packageValidationAuditor.WriteAuditEntriesAsync( - validationEntity.ValidationId, validationEntity.PackageId, validationEntity.PackageVersion, auditEntries); - - // Notify - await _notificationService.SendNotificationAsync( - $"vcscallback-failed/{validationEntity.Created.ToString("yyyy-MM-dd")}", - $"Validation {validationId} ({validationEntity.PackageId} {validationEntity.PackageVersion}) did not scan clean.", - body); - } - } - } - - if (!processedRequest) - { - if (validationEntity == null) - { - _logger.LogWarning( - "Callback was not handled for State={State}, Result={Result}. " + - "Request body: {RequestBody}", - result?.State, result?.Result, TruncateString(body, ReasonableBodySize)); - } - else - { - _logger.LogWarning( - "Callback was not handled for State={State}, Result={Result}," + - $"{{{TraceConstant.ValidationId}}}, " + - $"Package: {{{TraceConstant.PackageId}}} {{{TraceConstant.PackageVersion}}}. " + - "Request body: {RequestBody}", - result?.State, - result?.Result, - validationEntity.ValidationId, - validationEntity.PackageId, - validationEntity.PackageVersion, - TruncateString(body, ReasonableBodySize)); - } - } - } - } - - /// - /// Truncates the string leaving at most specified amount of characters and adds a "(truncated)" at the end - /// if it removes any portion of the string - /// - /// String to truncate - /// Max amount of characters to keep if truncated - /// Original string if it's length was less than specified length, otherwise, first 'length' characters of the string - /// with "(truncated)" appended. - private static string TruncateString(string str, int length) - { - if (str.Length <= length) - { - return str; - } - - return str.Substring(0, length) + "(truncated)"; - } - } -} \ No newline at end of file diff --git a/src/Validation.Common/Validators/Vcs/VcsValidator.cs b/src/Validation.Common/Validators/Vcs/VcsValidator.cs deleted file mode 100644 index edc56fa2a..000000000 --- a/src/Validation.Common/Validators/Vcs/VcsValidator.cs +++ /dev/null @@ -1,130 +0,0 @@ -// 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. - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using NuGet.Services.VirusScanning.Core; - -namespace NuGet.Jobs.Validation.Common.Validators.Vcs -{ - public class VcsValidator - : ValidatorBase, IValidator - { - public const string ValidatorName = "validator-vcs"; - - private readonly Uri _callbackUrl; - private readonly IVirusScanningService _scanningService; - private readonly ILogger _logger; - - public VcsValidator( - Uri callbackUrl, - string packageUrlTemplate, - IVirusScanningService scanningService, - ILogger logger) : base(packageUrlTemplate) - { - _callbackUrl = callbackUrl; - _scanningService = scanningService; - _logger = logger; - } - - public override string Name - { - get - { - return ValidatorName; - } - } - - public override async Task ValidateAsync(PackageValidationMessage message, List auditEntries) - { - var description = $"NuGet - {message.ValidationId} - {message.PackageId} {message.PackageVersion}"; - _logger.LogInformation("Submitting virus scan job with description {description}, " + - $" {{{TraceConstant.ValidatorName}}} {{{TraceConstant.ValidationId}}} " + - $" for package {{{TraceConstant.PackageId}}} " + - $"v. {{{TraceConstant.PackageVersion}}}", - description, - Name, - message.ValidationId, - message.PackageId, - message.PackageVersion); - WriteAuditEntry(auditEntries, $"Submitting virus scan job with description \"{description}\"...", - ValidationEvent.BeforeVirusScanRequest); - - string errorMessage; - try - { - var packageUrl = GetPackageUrl(message); - - // VCS requires a package URL that is either a direct URL to Azure Blob Storage or a UNC share file - // path. Azure Blob Storage URLs with SAS tokens in them are accepted. - var result = await _scanningService.CreateVirusScanJobAsync( - packageUrl, - _callbackUrl, - description, - message.ValidationId); - - if (IsValidJobResult(result)) - { - _logger.LogInformation("Submission completed for " + - $"{{{TraceConstant.ValidatorName}}} {{{TraceConstant.ValidationId}}}. " + - $"package {{{TraceConstant.PackageId}}} " + - $"{{{TraceConstant.PackageVersion}}} " + - "Request id: {RequestId} - job id: {JobId} - region code: {RegionCode}", - Name, - message.ValidationId, - message.PackageId, - message.PackageVersion, - result.RequestId, - result.JobId, - result.RegionCode); - WriteAuditEntry(auditEntries, $"Submission completed. Request id: {result.RequestId} " + - $"- job id: {result.JobId} " + - $"- region code: {result.RegionCode}", - ValidationEvent.VirusScanRequestSent); - return ValidationResult.Asynchronous; - } - else - { - errorMessage = result.ErrorMessage ?? "The request had no request ID, job ID, and region code."; - - _logger.LogError($"Submission failed for {{{TraceConstant.ValidatorName}}} {{{TraceConstant.ValidationId}}} " + - $"package {{{TraceConstant.PackageId}}} " + - $"v. {{{TraceConstant.PackageVersion}}} " + - "with: {ErrorMessage}", - Name, - message.ValidationId, - message.PackageId, - message.PackageVersion, - errorMessage); - WriteAuditEntry(auditEntries, $"Submission failed. Error message: {errorMessage}", - ValidationEvent.VirusScanRequestFailed); - - throw new ValidationException(errorMessage); - } - } - catch (Exception ex) - { - errorMessage = ex.Message; - _logger.TrackValidatorException(ValidatorName, message.ValidationId, ex, message.PackageId, message.PackageVersion); - WriteAuditEntry(auditEntries, $"Submission failed. Error message: {errorMessage}", - ValidationEvent.VirusScanRequestFailed); - throw; - } - } - - private static bool IsValidJobResult(VirusScanJob result) - { - return string.IsNullOrEmpty(result.ErrorMessage) - && !AreAllJobIdsEmpty(result); - } - - private static bool AreAllJobIdsEmpty(VirusScanJob result) - { - return string.IsNullOrEmpty(result.RequestId) - && string.IsNullOrEmpty(result.JobId) - && string.IsNullOrEmpty(result.RegionCode); - } - } -} \ No newline at end of file diff --git a/src/Validation.Common/app.config b/src/Validation.Common/app.config deleted file mode 100644 index c76c89bb8..000000000 --- a/src/Validation.Common/app.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src/Validation.Helper/Action.cs b/src/Validation.Helper/Action.cs deleted file mode 100644 index 843355619..000000000 --- a/src/Validation.Helper/Action.cs +++ /dev/null @@ -1,18 +0,0 @@ -// 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. - -namespace NuGet.Jobs.Validation.Helper -{ - public enum Action - { - /// - /// Request to rescan the package - /// - Rescan, - - /// - /// Mark package as clean manually - /// - MarkClean - } -} diff --git a/src/Validation.Helper/App.config b/src/Validation.Helper/App.config deleted file mode 100644 index c76c89bb8..000000000 --- a/src/Validation.Helper/App.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src/Validation.Helper/Command.cs b/src/Validation.Helper/Command.cs deleted file mode 100644 index d66ceb85b..000000000 --- a/src/Validation.Helper/Command.cs +++ /dev/null @@ -1,13 +0,0 @@ -// 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. - -using System.Threading.Tasks; - -namespace NuGet.Jobs.Validation.Helper -{ - internal interface ICommand - { - Action Action { get; } - Task Run(); - } -} diff --git a/src/Validation.Helper/CommandLineArguments.cs b/src/Validation.Helper/CommandLineArguments.cs deleted file mode 100644 index d868af9f6..000000000 --- a/src/Validation.Helper/CommandLineArguments.cs +++ /dev/null @@ -1,16 +0,0 @@ -// 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. - - -namespace NuGet.Jobs.Validation.Helper -{ - public class CommandLineArguments - { - public const string Action = "Action"; - public const string PackageId = "PackageId"; - public const string PackageVersion = "PackageVersion"; - public const string Comment = "Comment"; - public const string ValidationId = "ValidationId"; - public const string Alias = "Alias"; - } -} diff --git a/src/Validation.Helper/Job.cs b/src/Validation.Helper/Job.cs deleted file mode 100644 index d6a348713..000000000 --- a/src/Validation.Helper/Job.cs +++ /dev/null @@ -1,102 +0,0 @@ -// 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. - -using System; -using System.Collections.Generic; -using System.ComponentModel.Design; -using System.Diagnostics; -using System.IO; -using System.Net.Http; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.WindowsAzure.Storage; -using NuGet.Jobs.Validation.Common; -using NuGet.Jobs.Validation.Common.OData; - -namespace NuGet.Jobs.Validation.Helper -{ - public class Job : JobBase - { - private ICommand _command; - - public override void Init(IServiceContainer serviceContainer, IDictionary jobArgsDictionary) - { - var action = ParseEnum(JobConfigurationManager.GetArgument(jobArgsDictionary, CommandLineArguments.Action)); - - var azureStorageConnectionString = JobConfigurationManager.GetArgument(jobArgsDictionary, JobArgumentNames.DataStorageAccount); - var containerName = JobConfigurationManager.GetArgument(jobArgsDictionary, JobArgumentNames.ContainerName); - var cloudStorageAccount = CloudStorageAccount.Parse(azureStorageConnectionString); - var galleryBaseAddress = JobConfigurationManager.GetArgument(jobArgsDictionary, JobArgumentNames.GalleryBaseAddress); - - switch (action) - { - case Action.Rescan: - _command = new Rescan( - jobArgsDictionary, - LoggerFactory.CreateLogger(), - cloudStorageAccount, - containerName, - new NuGetV2Feed(new HttpClient(), LoggerFactory.CreateLogger()), - new PackageValidationService(cloudStorageAccount, containerName, LoggerFactory), - galleryBaseAddress); - break; - - case Action.MarkClean: - _command = new MarkClean( - jobArgsDictionary, - LoggerFactory.CreateLogger(), - cloudStorageAccount, - containerName, - new NuGetV2Feed(new HttpClient(), LoggerFactory.CreateLogger()), - new PackageValidationAuditor(cloudStorageAccount, containerName, LoggerFactory), - galleryBaseAddress); - break; - } - } - - public async override Task Run() - { - using (Logger.BeginScope("Processing action {Action} scope id: {RunTraceId}", _command.Action, Guid.NewGuid())) - { - await _command.Run(); - } - } - - public static void PrintUsage() - { - Console.WriteLine("Usage: {0} " + - $"-{JobArgumentNames.VaultName} " + - $"-{JobArgumentNames.ClientId} " + - $"-{JobArgumentNames.CertificateThumbprint} " + - $"-{JobArgumentNames.DataStorageAccount} " + - $"-{JobArgumentNames.ContainerName} " + - $"-{JobArgumentNames.GalleryBaseAddress} " + - $"-{CommandLineArguments.Action} ({Action.Rescan.ToString()}|{Action.MarkClean.ToString()}) " + - $"[-{JobArgumentNames.StoreName} (My|Root|TrustedPeople|TrustedPublisher|AddressBook|AuthRoot|CertificateAuthority|Disallowed)] " + - $"[-{JobArgumentNames.StoreLocation} (LocalMachine|CurrentUser)] " + - $"[-{JobArgumentNames.ValidateCertificate} (true|false)] " + - $"[-{JobArgumentNames.InstrumentationKey} ]", - Path.GetFileName(Process.GetCurrentProcess().MainModule.FileName)); - - Console.WriteLine(); - Console.WriteLine($"'-{CommandLineArguments.Action} {Action.Rescan.ToString()}' specific arguments: "); - Console.WriteLine($"\t-{CommandLineArguments.PackageId} "); - Console.WriteLine($"\t-{CommandLineArguments.PackageVersion} "); - Console.WriteLine(); - Console.WriteLine($"'-{CommandLineArguments.Action} {Action.MarkClean.ToString()}' specific arguments: "); - Console.WriteLine($"\t-{CommandLineArguments.PackageId} "); - Console.WriteLine($"\t-{CommandLineArguments.PackageVersion} "); - Console.WriteLine($"\t-{CommandLineArguments.ValidationId} "); - Console.WriteLine($"\t-{CommandLineArguments.Alias} "); - Console.WriteLine($"\t-{CommandLineArguments.Comment} "); - Console.WriteLine(); - Console.WriteLine("Package Id and version are supposed to be urlencoded (https://www.bing.com/search?q=urlencode)."); - Console.WriteLine("You need it for package ids and versions containing non-latin unicode characters and versions containing '+'."); - } - - private static T ParseEnum(string value) - { - return (T)Enum.Parse(typeof(T), value); - } - } -} diff --git a/src/Validation.Helper/MarkClean.cs b/src/Validation.Helper/MarkClean.cs deleted file mode 100644 index ae425e571..000000000 --- a/src/Validation.Helper/MarkClean.cs +++ /dev/null @@ -1,107 +0,0 @@ -// 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. - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using System.Web; -using Microsoft.Extensions.Logging; -using Microsoft.WindowsAzure.Storage; -using NuGet.Jobs.Validation.Common; -using NuGet.Jobs.Validation.Common.OData; -using NuGet.Jobs.Validation.Common.Validators.Vcs; - -namespace NuGet.Jobs.Validation.Helper -{ - public class MarkClean : ICommand - { - private readonly ILogger _logger; - private readonly CloudStorageAccount _cloudStorageAccount; - private readonly string _containerName; - private readonly Guid _validationId; - private readonly string _comment; - private readonly string _alias; - private readonly NuGetV2Feed _feed; - private readonly PackageValidationAuditor _packageValidationAuditor; - private readonly string _galleryBaseAddress; - - public Action Action => Action.MarkClean; - - public string PackageId { get; } - public string PackageVersion { get; } - - public MarkClean( - IDictionary jobArgsDictionary, - ILogger logger, - CloudStorageAccount cloudStorageAccount, - string containerName, - NuGetV2Feed feed, - PackageValidationAuditor packageValidationAuditor, - string galleryBaseAddress) - { - _logger = logger; - _cloudStorageAccount = cloudStorageAccount; - _containerName = containerName; - - PackageId = JobConfigurationManager.GetArgument(jobArgsDictionary, CommandLineArguments.PackageId); - PackageId = HttpUtility.UrlDecode(PackageId); - PackageVersion = JobConfigurationManager.GetArgument(jobArgsDictionary, CommandLineArguments.PackageVersion); - PackageVersion = HttpUtility.UrlDecode(PackageVersion); - var validationIdStr = JobConfigurationManager.GetArgument(jobArgsDictionary, CommandLineArguments.ValidationId); - _validationId = Guid.Parse(validationIdStr); - _comment = JobConfigurationManager.GetArgument(jobArgsDictionary, CommandLineArguments.Comment); - _alias = JobConfigurationManager.GetArgument(jobArgsDictionary, CommandLineArguments.Alias); - _feed = feed; - _packageValidationAuditor = packageValidationAuditor; - _galleryBaseAddress = galleryBaseAddress; - } - - public async Task Run() - { - _logger.LogInformation($"Starting creating successful scan entry for the {{{TraceConstant.PackageId}}} " + - $"{{{TraceConstant.PackageVersion}}}", - PackageId, - PackageVersion); - - var package = await Util.GetPackage( - _galleryBaseAddress, - _feed, - PackageId, - PackageVersion, - includeDownloadUrl: false); - - if (package == null) - { - _logger.LogError($"Unable to find {{{TraceConstant.PackageId}}} " + - $"{{{TraceConstant.PackageVersion}}}. Terminating.", - PackageId, - PackageVersion); - return false; - } - _logger.LogInformation($"Found package {{{TraceConstant.PackageId}}} " + - $"{{{TraceConstant.PackageVersion}}}", - package.Id, - package.Version); - - string packageVersion = package.GetVersion(); - - PackageValidationAuditEntry[] entries = new[] {new PackageValidationAuditEntry { - Timestamp = DateTimeOffset.UtcNow, - ValidatorName = VcsValidator.ValidatorName, - Message = $"{_alias} marked the package as scanned clean, comment: {_comment}", - EventId = ValidationEvent.PackageClean, - }}; - - _logger.LogInformation($"Marking the {{{TraceConstant.PackageId}}} " + - $"{{{TraceConstant.PackageVersion}}} " + - $"as clean with comment: {{{TraceConstant.Comment}}}. " + - $"Requested by {{{TraceConstant.Alias}}}", - package.Id, - packageVersion, - _comment, - _alias); - await _packageValidationAuditor.WriteAuditEntriesAsync(_validationId, package.Id, packageVersion, entries); - return true; - } - } -} diff --git a/src/Validation.Helper/Program.cs b/src/Validation.Helper/Program.cs deleted file mode 100644 index a0a86caca..000000000 --- a/src/Validation.Helper/Program.cs +++ /dev/null @@ -1,28 +0,0 @@ -// 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. - -using System.Linq; - -namespace NuGet.Jobs.Validation.Helper -{ - class Program - { - private static void Main(string[] args) - { - if (args.Count() < 1) - { - Job.PrintUsage(); - return; - } - - // force running once - if (!args.Contains("-Once")) - { - args = args.Concat(new[] { "-Once" }).ToArray(); - } - - var job = new Job(); - JobRunner.Run(job, args).Wait(); - } - } -} diff --git a/src/Validation.Helper/Properties/AssemblyInfo.cs b/src/Validation.Helper/Properties/AssemblyInfo.cs deleted file mode 100644 index 13287809c..000000000 --- a/src/Validation.Helper/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Validation.Helper")] -[assembly: AssemblyDescription("Validation helper tool")] -[assembly: AssemblyCompany(".NET Foundation")] -[assembly: AssemblyProduct("Validation.Helper")] -[assembly: AssemblyCopyright("Copyright © .NET Foundation 2017")] -[assembly: ComVisible(false)] -[assembly: Guid("305fb2c1-01fd-413d-b788-7b85bad85a41")] - diff --git a/src/Validation.Helper/Rescan.cs b/src/Validation.Helper/Rescan.cs deleted file mode 100644 index 3b02352df..000000000 --- a/src/Validation.Helper/Rescan.cs +++ /dev/null @@ -1,91 +0,0 @@ -// 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. - -using System.Collections.Generic; -using System.Threading.Tasks; -using System.Web; -using Microsoft.Extensions.Logging; -using Microsoft.WindowsAzure.Storage; -using NuGet.Jobs.Validation.Common; -using NuGet.Jobs.Validation.Common.OData; -using NuGet.Jobs.Validation.Common.Validators.Vcs; - -namespace NuGet.Jobs.Validation.Helper -{ - public class Rescan : ICommand - { - private readonly ILogger _logger; - private readonly NuGetV2Feed _feed; - private readonly string _containerName; - private readonly CloudStorageAccount _cloudStorageAccount; - private readonly PackageValidationService _packageValidationService; - private readonly string _galleryBaseAddress; - - public Action Action => Action.Rescan; - - public string PackageId { get; } - public string PackageVersion { get; } - - public Rescan( - IDictionary jobArgsDictionary, - ILogger logger, - CloudStorageAccount cloudStorageAccount, - string containerName, - NuGetV2Feed feed, - PackageValidationService packageValidationService, - string galleryBaseAddress) - { - _logger = logger; - _feed = feed; - - _containerName = containerName; - _cloudStorageAccount = cloudStorageAccount; - - PackageId = JobConfigurationManager.GetArgument(jobArgsDictionary, CommandLineArguments.PackageId); - PackageId = HttpUtility.UrlDecode(PackageId); - PackageVersion = JobConfigurationManager.GetArgument(jobArgsDictionary, CommandLineArguments.PackageVersion); - PackageVersion = HttpUtility.UrlDecode(PackageVersion); - _packageValidationService = packageValidationService; - _galleryBaseAddress = galleryBaseAddress; - } - - public async Task Run() - { - _logger.LogInformation($"Creating rescan request for {{{TraceConstant.PackageId}}} " + - $"{{{TraceConstant.PackageVersion}}}", - PackageId, - PackageVersion); - - var package = await Util.GetPackage( - _galleryBaseAddress, - _feed, - PackageId, - PackageVersion, - includeDownloadUrl: false); - if (package == null) - { - _logger.LogError($"Unable to find {{{TraceConstant.PackageId}}} " + - $"{{{TraceConstant.PackageVersion}}}. Terminating.", - PackageId, - PackageVersion); - return false; - } - _logger.LogInformation($"Found package {{{TraceConstant.PackageId}}} " + - $"{{{TraceConstant.PackageVersion}}}", - package.Id, - package.Version); - - _logger.LogInformation($"Submitting rescan request for {{{TraceConstant.PackageId}}} " + - $"{{{TraceConstant.PackageVersion}}}", - package.Id, - package.Version); - await _packageValidationService.StartValidationProcessAsync(package, new[] { VcsValidator.ValidatorName }); - _logger.LogInformation($"Done submitting rescan request for {{{TraceConstant.PackageId}}} " + - $"{{{TraceConstant.PackageVersion}}}", - package.Id, - package.Version); - - return true; - } - } -} diff --git a/src/Validation.Helper/Util.cs b/src/Validation.Helper/Util.cs deleted file mode 100644 index c777d372d..000000000 --- a/src/Validation.Helper/Util.cs +++ /dev/null @@ -1,77 +0,0 @@ -// 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. - -using System; -using System.Linq; -using System.Threading.Tasks; -using NuGet.Jobs.Validation.Common; -using NuGet.Jobs.Validation.Common.OData; - -namespace NuGet.Jobs.Validation.Helper -{ - internal static class Util - { - public static async Task GetPackage( - string galleryBaseAddress, - NuGetV2Feed feed, - string packageId, - string packageVersion, - bool includeDownloadUrl) - { - // We'll try the normalized version first, then fall back to non-normalized - // one if it fails. - var url = GetNormalizedPackageUrl(galleryBaseAddress, packageId, packageVersion); - var package = await GetPackage(feed, url, includeDownloadUrl); - if (package != null) - { - return package; - } - url = GetPackageUrl(galleryBaseAddress, packageId, packageVersion); - return await GetPackage(feed, url, includeDownloadUrl); - } - - private static async Task GetPackage(NuGetV2Feed feed, Uri url, bool includeDownloadUrl) - { - var packages = await feed.GetPackagesAsync( - url, - includeDownloadUrl, - continuationsToFollow: 0); - - return packages.FirstOrDefault(); - } - - /// - /// Returns the URL of the OData request to the NuGet service that - /// would provide the package information for the specified package - /// by its ID and (non-normalized) version. - /// - /// - /// Base address of the NuGet Gallery V2 API (https://www.nuget.org/api/v2). - /// - /// Package ID. - /// Non-normalized package version. - /// URL of the OData request providing package information for the requested package. - private static Uri GetPackageUrl(string galleryBaseAddress, string packageId, string packageVersion) - { - return new Uri($"{galleryBaseAddress}/Packages?" + - $"$filter=Id eq '{packageId}' and Version eq '{packageVersion}' and true"); - } - - /// - /// Returns the URL of the OData request to the NuGet service that - /// would provide the package information for the specified package - /// by its ID and normalized version - /// - /// - /// Base address of the NuGet Gallery V2 API (https://www.nuget.org/api/v2). - /// - /// Package ID. - /// Normalized package version. - /// URL of the OData request providing package information for the requested package. - private static Uri GetNormalizedPackageUrl(string galleryBaseAddress, string packageId, string normalizedPackageVersion) - { - return new Uri($"{galleryBaseAddress}/Packages?" + - $"$filter=Id eq '{packageId}' and NormalizedVersion eq '{normalizedPackageVersion}' and true"); - } - } -} diff --git a/src/Validation.Helper/Validation.Helper.cmd b/src/Validation.Helper/Validation.Helper.cmd deleted file mode 100644 index 99f62cb51..000000000 --- a/src/Validation.Helper/Validation.Helper.cmd +++ /dev/null @@ -1,20 +0,0 @@ -@echo off - -if "%1"=="" ( - echo Usage: - echo %~nx0 -Action Rescan -PackageId ^ -PackageVersion ^ - echo %~nx0 -Action MarkClean -PackageId ^ -PackageVersion ^ -ValidationId ^ -Alias ^ -Comment ^ - exit 1 -) - -SET vn=#{Deployment.Azure.KeyVault.VaultName} -SET clientid=#{Deployment.Azure.KeyVault.ClientId} -SET tp=#{Deployment.Azure.KeyVault.CertificateThumbprint} -SET la=#{Jobs.validation.DataStorageAccount} -SET dsa=#{Jobs.validation.DataStorageAccount} -SET cn=#{Jobs.validation.ContainerName} -SET ik=#{Jobs.validation.VcsValidatorInstrumentationKey} -SET gba=#{Jobs.validation.GalleryBaseAddress} - -NuGet.Jobs.Validation.Helper.exe -VaultName "%vn%" -ClientId "%clientid%" -CertificateThumbprint "%tp%" -DataStorageAccount "%dsa%" -ContainerName "%cn%" -InstrumentationKey "%ik%" -GalleryBaseAddress "%gba%" %* - diff --git a/src/Validation.Helper/Validation.Helper.csproj b/src/Validation.Helper/Validation.Helper.csproj deleted file mode 100644 index ac67de677..000000000 --- a/src/Validation.Helper/Validation.Helper.csproj +++ /dev/null @@ -1,93 +0,0 @@ - - - - - Debug - AnyCPU - {305FB2C1-01FD-413D-B788-7B85BAD85A41} - Exe - NuGet.Jobs.Validation.Helper - NuGet.Jobs.Validation.Helper - v4.6.2 - 512 - true - - - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - - - - - - - - - - - - - Designer - - - - PreserveNewest - - - - - {4b4b1efb-8f33-42e6-b79f-54e7f3293d31} - NuGet.Jobs.Common - - - {2539ddf3-0cc5-4a03-b5f9-39b47744a7bd} - Validation.Common - - - - - 0.3.0 - runtime; build; native; contentfiles; analyzers - all - - - - - ..\..\build - $(BUILD_SOURCESDIRECTORY)\build - $(NuGetBuildPath) - none - - - - - \ No newline at end of file diff --git a/src/Validation.Helper/Validation.Helper.nuspec b/src/Validation.Helper/Validation.Helper.nuspec deleted file mode 100644 index cd3dd017d..000000000 --- a/src/Validation.Helper/Validation.Helper.nuspec +++ /dev/null @@ -1,17 +0,0 @@ - - - - Validation.Helper - $version$ - Validation.Helper - .NET Foundation - .NET Foundation - Validation helper tool - Copyright .NET Foundation - - - - - - - \ No newline at end of file diff --git a/src/Validation.Helper/project.json b/src/Validation.Helper/project.json deleted file mode 100644 index c8ba17033..000000000 --- a/src/Validation.Helper/project.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "dependencies": { - "Microsoft.Data.Services.Client": "5.7.0", - "Microsoft.Extensions.Logging.Abstractions": "1.0.0", - "Newtonsoft.Json": { - "version": "9.0.1", - "comment": "Not a direct dependency, workaround to have correct version of json.dll to be put in the output directory. Probably could be removed once all dependent projects are upgraded to use project.json" - }, - "WindowsAzure.Storage": "7.1.2" - }, - "frameworks": { - "net462": {} - }, - "runtimes": { - "win": {} - } -} \ No newline at end of file diff --git a/src/Validation.Runner/App.config b/src/Validation.Runner/App.config deleted file mode 100644 index b7c435af2..000000000 --- a/src/Validation.Runner/App.config +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/src/Validation.Runner/Job.cs b/src/Validation.Runner/Job.cs deleted file mode 100644 index 810ba2e64..000000000 --- a/src/Validation.Runner/Job.cs +++ /dev/null @@ -1,348 +0,0 @@ -// 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. - -using System; -using System.Collections.Generic; -using System.ComponentModel.Design; -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.WindowsAzure.Storage; -using NuGet.Jobs.Validation.Common; -using NuGet.Jobs.Validation.Common.OData; -using NuGet.Jobs.Validation.Common.Validators; -using NuGet.Jobs.Validation.Common.Validators.Vcs; -using NuGet.Services.VirusScanning.Vcs; - -namespace NuGet.Jobs.Validation.Runner -{ - public class Job - : JobBase - { - private const int DefaultBatchSize = 10; - private readonly List _validators = new List(); - - private string _galleryBaseAddress; - private CloudStorageAccount _cloudStorageAccount; - private string _containerName; - private string[] _runValidationTasks; - private string[] _requestValidationTasks; - private int _batchSize; - - public override void Init(IServiceContainer serviceContainer, IDictionary jobArgsDictionary) - { - _galleryBaseAddress = JobConfigurationManager.GetArgument(jobArgsDictionary, JobArgumentNames.GalleryBaseAddress); - - var storageConnectionString = JobConfigurationManager.GetArgument(jobArgsDictionary, JobArgumentNames.DataStorageAccount); - _cloudStorageAccount = CreateCloudStorageAccount(JobArgumentNames.DataStorageAccount, storageConnectionString); - - _containerName = JobConfigurationManager.GetArgument(jobArgsDictionary, JobArgumentNames.ContainerName); - - _runValidationTasks = JobConfigurationManager.GetArgument(jobArgsDictionary, JobArgumentNames.RunValidationTasks).Split(';'); - _requestValidationTasks = JobConfigurationManager.GetArgument(jobArgsDictionary, JobArgumentNames.RequestValidationTasks).Split(';'); - _batchSize = JobConfigurationManager.TryGetIntArgument(jobArgsDictionary, JobArgumentNames.BatchSize) ?? DefaultBatchSize; - - // Add validators - if (_runValidationTasks.Contains(VcsValidator.ValidatorName)) - { - var serviceUrl = new Uri(JobConfigurationManager.GetArgument(jobArgsDictionary, JobArgumentNames.VcsValidatorServiceUrl)); - var consumerCode = "DIRECT"; - var callbackUrl = new Uri(JobConfigurationManager.GetArgument(jobArgsDictionary, JobArgumentNames.VcsValidatorCallbackUrl)); - var packageUrlTemplate = JobConfigurationManager.GetArgument(jobArgsDictionary, JobArgumentNames.PackageUrlTemplate); - var submitterAlias = JobConfigurationManager.GetArgument(jobArgsDictionary, JobArgumentNames.VcsValidatorSubmitterAlias); - - // if contact alias set, use it, if not, use submitter alias. - var contactAlias = JobConfigurationManager.TryGetArgument(jobArgsDictionary, JobArgumentNames.VcsContactAlias) ?? submitterAlias; - - var scanningService = new VcsVirusScanningService( - serviceUrl, - consumerCode, - contactAlias, - submitterAlias, - LoggerFactory); - - _validators.Add(new VcsValidator( - callbackUrl, - packageUrlTemplate, - scanningService, - LoggerFactory.CreateLogger())); - } - } - - private static CloudStorageAccount CreateCloudStorageAccount(string argumentName, string storageConnectionString) - { - if (string.IsNullOrEmpty(storageConnectionString)) - { - throw new ArgumentException("Job parameter " + argumentName + " for Azure Cloud Storage Account is not defined."); - } - - CloudStorageAccount account; - if (CloudStorageAccount.TryParse(storageConnectionString, out account)) - { - return account; - } - - throw new ArgumentException("Job parameter " + argumentName + " for Azure Cloud Storage Account is invalid."); - } - - public override async Task Run() - { - if (!_runValidationTasks.Any()) - { - throw new ArgumentException("Job parameter " + JobArgumentNames.RunValidationTasks + " must be specified."); - } - - if (!_requestValidationTasks.Any()) - { - throw new ArgumentException("Job parameter " + JobArgumentNames.RequestValidationTasks + " must be specified."); - } - - // Run any of the subcommands specified in the "RunValidationTasks" argument - foreach (var validationTask in _runValidationTasks) - { - if (validationTask.StartsWith("validator", StringComparison.OrdinalIgnoreCase)) - { - var validator = _validators.FirstOrDefault( - v => String.Equals(v.Name, validationTask, StringComparison.OrdinalIgnoreCase)); - - if (validator != null) - { - await RunValidationsAsync(validator); - } - } - else if (string.Equals(validationTask, "orchestrate", StringComparison.OrdinalIgnoreCase)) - { - await RunOrchestrateAsync(); - } - } - } - - private async Task RunValidationsAsync(IValidator validator) - { - Logger.LogInformation($"{{{TraceConstant.EventName}}}: " + - $"Checking the queue of {{{TraceConstant.ValidatorName}}}", - "ValidatorQueueCheck", - validator.Name); - - // Services - var packageValidationTable = new PackageValidationTable(_cloudStorageAccount, _containerName); - var packageValidationAuditor = new PackageValidationAuditor(_cloudStorageAccount, _containerName, LoggerFactory); - var packageValidationQueue = new PackageValidationQueue(_cloudStorageAccount, _containerName, LoggerFactory); - var notificationService = new NotificationService(_cloudStorageAccount, _containerName); - - // Get messages to process - var messages = await packageValidationQueue.DequeueAsync(validator.Name, _batchSize, validator.VisibilityTimeout); - foreach (var message in messages) - { - // Audit entry collection to which our validator can write - var auditEntries = new List(); - var validationResult = ValidationResult.Unknown; - - // Deadlettering - if (message.DequeueCount > 10) - { - validationResult = ValidationResult.Deadlettered; - - auditEntries.Add(new PackageValidationAuditEntry - { - Timestamp = DateTimeOffset.UtcNow, - ValidatorName = validator.Name, - Message = $"Message has been attempted too many times and is being deadlettered. Aborting validator.", - EventId = ValidationEvent.Deadlettered, - }); - } - - if (validationResult != ValidationResult.Deadlettered) - { - try - { - // Perform the validation - Logger.LogInformation($"Starting validator {{{TraceConstant.ValidatorName}}} " + - $"for validation {{{TraceConstant.ValidationId}}} " + - $"- package {{{TraceConstant.PackageId}}} " + - $"v. {{{TraceConstant.PackageVersion}}}...", - validator.Name, - message.ValidationId, - message.PackageId, - message.PackageVersion); - - validationResult = await validator.ValidateAsync(message, auditEntries); - - Logger.LogInformation($"Finished running validator {{{TraceConstant.ValidatorName}}} " + - $"for validation {{{TraceConstant.ValidationId}}} " + - $"- package {{{TraceConstant.PackageId}}} " + - $"v. {{{TraceConstant.PackageVersion}}}. " + - $"Result: {{{TraceConstant.ValidationResult}}}", - validator.Name, - message.ValidationId, - message.PackageId, - message.PackageVersion, - validationResult); - } - catch (Exception ex) - { - // Audit the exception, but do not remove the message yet. - // We want to retry validation on next run. - auditEntries.Add(new PackageValidationAuditEntry - { - Timestamp = DateTimeOffset.UtcNow, - ValidatorName = validator.Name, - Message = $"Exception thrown during validation - {ex.Message}\r\n{ex.StackTrace}", - EventId = ValidationEvent.ValidatorException, - }); - - Logger.LogError(TraceEvent.ValidatorException, ex, - $"Exception while running validator {{{TraceConstant.ValidatorName}}} " + - $"for validation {{{TraceConstant.ValidationId}}} " + - $"- package {{{TraceConstant.PackageId}}} " + - $"v. {{{TraceConstant.PackageVersion}}}", - validator.Name, - message.ValidationId, - message.PackageId, - message.PackageVersion); - } - } - - // Process message - if (validationResult != ValidationResult.Unknown) - { - TrackValidatorResult(validator.Name, message.ValidationId, validationResult.ToString(), message.PackageId, message.PackageVersion); - - // Update our tracking entity - var packageValidationEntity = await packageValidationTable.GetValidationAsync(message.ValidationId); - if (packageValidationEntity != null && validationResult != ValidationResult.Asynchronous) - { - packageValidationEntity.ValidatorCompleted(validator.Name, validationResult); - await packageValidationTable.StoreAsync(packageValidationEntity); - } - - // Remove the message - await packageValidationQueue.DeleteAsync(validator.Name, message); - } - - // Write audit entries - await packageValidationAuditor.WriteAuditEntriesAsync(message.ValidationId, message.PackageId, message.PackageVersion, auditEntries); - - // Process failure - if (validationResult == ValidationResult.Failed || validationResult == ValidationResult.Deadlettered) - { - var audit = await packageValidationAuditor.ReadAuditAsync(message.ValidationId, message.PackageId, message.PackageVersion); - - await notificationService.SendNotificationAsync( - "validation", - $"Validation {message.ValidationId} ({message.PackageId} {message.PackageVersion}) returned '{validationResult}'", - audit.Humanize()); - } - } - - Logger.LogInformation($"Done checking the queue of {{{TraceConstant.ValidatorName}}}", validator.Name); - } - - private async Task RunOrchestrateAsync() - { - Logger.LogInformation($"{{{TraceConstant.EventName}}}: Attempting orchestration", - "OrchestrationAttempt"); - - // Retrieve cursor (last created / last edited) - var cursor = new PackageValidationOrchestrationCursor(_cloudStorageAccount, _containerName + "-audit", "cursor.json", LoggerFactory); - await cursor.LoadAsync(); - - // Setup package validation service - var packageValidationService = new PackageValidationService(_cloudStorageAccount, _containerName, LoggerFactory); - - // Get reference timestamps - var referenceLastCreated = cursor.LastCreated ?? DateTimeOffset.UtcNow.AddMinutes(-15); - var referenceLastEdited = cursor.LastEdited ?? DateTimeOffset.UtcNow.AddMinutes(-15); - - // Fetch newly added / edited packages and enqueue validations - using (var client = new HttpClient()) - { - var packages = new HashSet(new NuGetV2PackageEqualityComparer()); - - var feed = new NuGetV2Feed(client, LoggerFactory.CreateLogger()); - - var createdPackagesUrl = MakePackageQueryUrl(_galleryBaseAddress, "Created", referenceLastCreated); - Logger.LogInformation("Querying packages created since {StartTime}, URL: {QueryUrl}", referenceLastCreated, createdPackagesUrl); - var createdPackages = await feed.GetPackagesAsync( - createdPackagesUrl, - includeDownloadUrl: false, - continuationsToFollow: 0); - - foreach (var package in createdPackages) - { - packages.Add(package); - - if (package.Created > cursor.LastCreated || cursor.LastCreated == null) - { - cursor.LastCreated = package.Created; - } - } - - // todo: do we also want to check edited packages again? - //var editedPackagesUrl = MakePackageQueryUrl(_galleryBaseAddress, "LastEdited", referenceLastEdited); - //var editedPackages = await feed.GetPackagesAsync(editedPackagesUrl, followContinuations: true); - //foreach (var package in editedPackages) - //{ - // //packages.Add(package); - - // if (package.LastEdited > cursor.LastEdited || cursor.LastEdited == null) - // { - // cursor.LastEdited = package.LastEdited; - // } - //} - - // Start the validation process for each package - foreach (var package in packages) - { - await packageValidationService.StartValidationProcessAsync(package, _requestValidationTasks); - } - - // Store cursor - await cursor.SaveAsync(); - } - - // TODO: check for validations that have never been executed? - } - - private static Uri MakePackageQueryUrl(string source, string propertyName, DateTimeOffset since) - { - var address = string.Format("{0}/Packages?$filter={1} gt DateTime'{2}'&$orderby={1}", - source.Trim('/'), - propertyName, - since.UtcDateTime.ToString("O")); - - return new Uri(address); - } - - /// - /// Tracks the result of validation. If result is then tracks it in - /// a separate event (since it is non-terminal result, want to make it trivially distinguishable from terminal). - /// - /// The name of the validator - /// Validation ID of the finished validator - /// String representation of the outcome - /// Package ID - /// Package version - private void TrackValidatorResult(string validatorName, Guid validationId, string result, string packageId, string packageVersion) - { - if (result == ValidationResult.Asynchronous.ToString()) - { - Logger.LogInformation($"{{{TraceConstant.EventName}}}: " + - $"running a {{{TraceConstant.ValidatorName}}} " + - $"ValidationID: {{{TraceConstant.ValidationId}}} " + - $"for package {{{TraceConstant.PackageId}}} " + - $"v.{{{TraceConstant.PackageVersion}}} resulted in starting async task", - "ValidatorAsync", - validatorName, - validationId, - packageId, - packageVersion); - } - else - { - Logger.TrackValidatorResult(validatorName, validationId, result, packageId, packageVersion); - } - } - } -} \ No newline at end of file diff --git a/src/Validation.Runner/Program.cs b/src/Validation.Runner/Program.cs deleted file mode 100644 index 15e62c389..000000000 --- a/src/Validation.Runner/Program.cs +++ /dev/null @@ -1,14 +0,0 @@ -// 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. - -namespace NuGet.Jobs.Validation.Runner -{ - class Program - { - static void Main(string[] args) - { - var job = new Job(); - JobRunner.Run(job, args).Wait(); - } - } -} diff --git a/src/Validation.Runner/Properties/AssemblyInfo.cs b/src/Validation.Runner/Properties/AssemblyInfo.cs deleted file mode 100644 index 4c956ef18..000000000 --- a/src/Validation.Runner/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Validation.Runner")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Validation.Runner")] -[assembly: AssemblyCopyright("Copyright © 2016")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("1eb7ff94-9b4a-4008-8f8e-5f867c0b00de")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/Validation.Runner/Scripts/Functions.ps1 b/src/Validation.Runner/Scripts/Functions.ps1 deleted file mode 100644 index 1fce22113..000000000 --- a/src/Validation.Runner/Scripts/Functions.ps1 +++ /dev/null @@ -1,41 +0,0 @@ -Function Uninstall-NuGetService() { - Param ([string]$ServiceName) - - if (Get-Service $ServiceName -ErrorAction SilentlyContinue) - { - Write-Host Removing service $ServiceName... - Stop-Service $ServiceName -Force - sc.exe delete $ServiceName - Write-Host Removed service $ServiceName. - } else { - Write-Host Skipping removal of service $ServiceName - no such service exists. - } -} - -Function Install-NuGetService() { - Param ( - [string]$ServiceName, - [string]$ServiceTitle, - [string]$ScriptToRun, - [Parameter(Mandatory=$false)][string]$Username, - [Parameter(Mandatory=$false)][string]$Password - ) - - Write-Host Installing service $ServiceName... - - $installService = "nssm install $ServiceName $ScriptToRun" - cmd /C $installService - - Set-Service -Name $ServiceName -DisplayName "$ServiceTitle - $ServiceName" -Description "Runs $ServiceTitle." -StartupType Automatic - sc.exe failure $ServiceName reset= 30 actions= restart/5000 - - if ($Username) { - Write-Host Running service under specific credentials. - sc.exe config "$ServiceName" obj= "$Username" password= "$Password" - } - - # Run service - net start $ServiceName - - Write-Host Installed service $ServiceName. -} \ No newline at end of file diff --git a/src/Validation.Runner/Scripts/PostDeploy.ps1 b/src/Validation.Runner/Scripts/PostDeploy.ps1 deleted file mode 100644 index d56a30b1c..000000000 --- a/src/Validation.Runner/Scripts/PostDeploy.ps1 +++ /dev/null @@ -1,20 +0,0 @@ -. .\Functions.ps1 - -$jobsToInstall = $OctopusParameters["Jobs.ServiceNames"].Split("{,}") - -Write-Host Installing services... - -$currentDirectory = [string](Get-Location) - -$jobsToInstall.Split("{;}") | %{ - $serviceName = $_ - $serviceTitle = $OctopusParameters["Jobs.$serviceName.Title"] - $serviceUsername = $OctopusParameters["Jobs.$serviceName.Username"] - $servicePassword = $OctopusParameters["Jobs.$serviceName.Password"] - $scriptToRun = $OctopusParameters["Jobs.$serviceName.Script"] - $scriptToRun = "$currentDirectory\$scriptToRun" - - Install-NuGetService -ServiceName $serviceName -ServiceTitle $serviceTitle -ScriptToRun $scriptToRun -Username $serviceUsername -Password $servicePassword -} - -Write-Host Installed services. \ No newline at end of file diff --git a/src/Validation.Runner/Scripts/PreDeploy.ps1 b/src/Validation.Runner/Scripts/PreDeploy.ps1 deleted file mode 100644 index ef711a912..000000000 --- a/src/Validation.Runner/Scripts/PreDeploy.ps1 +++ /dev/null @@ -1,11 +0,0 @@ -. .\Functions.ps1 - -$jobsToInstall = $OctopusParameters["Jobs.ServiceNames"].Split("{,}") - -Write-Host Removing services... - -$jobsToInstall.Split("{;}") | %{ - Uninstall-NuGetService -ServiceName $_ -} - -Write-Host Removed services. \ No newline at end of file diff --git a/src/Validation.Runner/Scripts/Validation.Runner.cmd b/src/Validation.Runner/Scripts/Validation.Runner.cmd deleted file mode 100644 index dd5200314..000000000 --- a/src/Validation.Runner/Scripts/Validation.Runner.cmd +++ /dev/null @@ -1,31 +0,0 @@ -@echo OFF - -cd bin - -:Top -echo "Starting job - #{Jobs.validation.Title}" - -title #{Jobs.validation.Title} - -start /w Validation.Runner.exe ^ - -VaultName "#{Deployment.Azure.KeyVault.VaultName}" ^ - -ClientId "#{Deployment.Azure.KeyVault.ClientId}" ^ - -CertificateThumbprint "#{Deployment.Azure.KeyVault.CertificateThumbprint}" ^ - -RunValidationTasks "#{Jobs.validation.RunValidationTasks}" ^ - -RequestValidationTasks "#{Jobs.validation.RequestValidationTasks}" ^ - -GalleryBaseAddress "#{Jobs.validation.GalleryBaseAddress}" ^ - -DataStorageAccount "#{Jobs.validation.DataStorageAccount}" ^ - -ContainerName "#{Jobs.validation.ContainerName}" ^ - -PackageUrlTemplate "#{Jobs.validation.PackageUrlTemplate}" ^ - -VcsValidatorServiceUrl "#{Jobs.validation.VcsValidatorServiceUrl}" ^ - -VcsValidatorCallbackUrl "#{Jobs.validation.VcsValidatorCallbackUrl}" ^ - -VcsValidatorAlias "#{Jobs.validation.VcsValidatorAlias}" ^ - -verbose true ^ - -Interval #{Jobs.validation.Interval} ^ - -InstrumentationKey "#{Jobs.validation.ApplicationInsightsInstrumentationKey}" ^ - -VcsContactAlias "#{Jobs.validation.NugetVcsContactAlias}" ^ - -BatchSize "#{Jobs.validation.BatchSize}" - -echo "Finished #{Jobs.validation.Title}" - -goto Top diff --git a/src/Validation.Runner/Scripts/nssm.exe b/src/Validation.Runner/Scripts/nssm.exe deleted file mode 100644 index 6ccfe3cfb85f0f7126648dceb7ac93d58b025a0f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 331264 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 z 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 - - - - Debug - AnyCPU - {1EB7FF94-9B4A-4008-8F8E-5F867C0B00DE} - Exe - Properties - NuGet.Jobs.Validation.Runner - Validation.Runner - v4.6.2 - 512 - true - - - - - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - - - - - - - Designer - - - - - - - {2539DDF3-0CC5-4A03-B5F9-39B47744A7BD} - Validation.Common - - - {4B4B1EFB-8F33-42E6-B79F-54E7F3293D31} - NuGet.Jobs.Common - - - {305fb2c1-01fd-413d-b788-7b85bad85a41} - Validation.Helper - - - - - - - - 0.3.0 - runtime; build; native; contentfiles; analyzers - all - - - 5.7.0 - - - 3.2.0 - - - 7.1.2 - - - - - ..\..\build - $(BUILD_SOURCESDIRECTORY)\build - $(NuGetBuildPath) - none - - - - - - \ No newline at end of file diff --git a/src/Validation.Runner/Validation.Runner.nuspec b/src/Validation.Runner/Validation.Runner.nuspec deleted file mode 100644 index 4b3b6654e..000000000 --- a/src/Validation.Runner/Validation.Runner.nuspec +++ /dev/null @@ -1,22 +0,0 @@ - - - - Validation.Runner.$branch$ - $version$ - Validation.Runner - .NET Foundation - .NET Foundation - Validation.Runner - Copyright .NET Foundation - - - - - - - - - - - - \ No newline at end of file diff --git a/test.ps1 b/test.ps1 index ec03444b2..2205d883e 100644 --- a/test.ps1 +++ b/test.ps1 @@ -37,8 +37,6 @@ Function Run-Tests { "tests\Tests.Stats.CollectAzureChinaCdnLogs\bin\$Configuration\Tests.Stats.CollectAzureChinaCdnLogs.dll", ` "tests\Tests.Stats.ImportAzureCdnStatistics\bin\$Configuration\Tests.Stats.ImportAzureCdnStatistics.dll", ` "tests\Validation.Common.Job.Tests\bin\$Configuration\Validation.Common.Job.Tests.dll", ` - "tests\Validation.Common.Tests\bin\$Configuration\Validation.Common.Tests.dll", ` - "tests\Validation.Helper.Tests\bin\$Configuration\Validation.Helper.Tests.dll",` "tests\Validation.PackageSigning.Core.Tests\bin\$Configuration\Validation.PackageSigning.Core.Tests.dll", ` "tests\Validation.PackageSigning.ProcessSignature.Tests\bin\$Configuration\Validation.PackageSigning.ProcessSignature.Tests.dll", ` "tests\Validation.PackageSigning.RevalidateCertificate.Tests\bin\$Configuration\Validation.PackageSigning.RevalidateCertificate.Tests.dll", ` diff --git a/tests/NuGet.Services.Validation.Orchestrator.Tests/Vcs/PackageCriteriaEvaluatorFacts.cs b/tests/NuGet.Services.Validation.Orchestrator.Tests/Criteria/PackageCriteriaEvaluatorFacts.cs similarity index 99% rename from tests/NuGet.Services.Validation.Orchestrator.Tests/Vcs/PackageCriteriaEvaluatorFacts.cs rename to tests/NuGet.Services.Validation.Orchestrator.Tests/Criteria/PackageCriteriaEvaluatorFacts.cs index f5b32a7d9..829327f81 100644 --- a/tests/NuGet.Services.Validation.Orchestrator.Tests/Vcs/PackageCriteriaEvaluatorFacts.cs +++ b/tests/NuGet.Services.Validation.Orchestrator.Tests/Criteria/PackageCriteriaEvaluatorFacts.cs @@ -5,7 +5,7 @@ using NuGet.Services.Entities; using Xunit; -namespace NuGet.Services.Validation.Vcs +namespace NuGet.Services.Validation { public class PackageCriteriaEvaluatorFacts { diff --git a/tests/NuGet.Services.Validation.Orchestrator.Tests/NuGet.Services.Validation.Orchestrator.Tests.csproj b/tests/NuGet.Services.Validation.Orchestrator.Tests/NuGet.Services.Validation.Orchestrator.Tests.csproj index b6795d9f4..42bd9cd5f 100644 --- a/tests/NuGet.Services.Validation.Orchestrator.Tests/NuGet.Services.Validation.Orchestrator.Tests.csproj +++ b/tests/NuGet.Services.Validation.Orchestrator.Tests/NuGet.Services.Validation.Orchestrator.Tests.csproj @@ -75,8 +75,7 @@ - - + @@ -107,10 +106,6 @@ {fa87d075-a934-4443-8d0b-5db32640b6d7} Validation.Common.Job - - {2539DDF3-0CC5-4A03-B5F9-39B47744A7BD} - Validation.Common - {91C060DA-736F-4DA9-A57F-CB3AC0E6CB10} Validation.PackageSigning.Core diff --git a/tests/NuGet.Services.Validation.Orchestrator.Tests/Symbol/SymbolScanValidatorFacts.cs b/tests/NuGet.Services.Validation.Orchestrator.Tests/Symbol/SymbolScanValidatorFacts.cs index 6fa111c57..36a7c9a68 100644 --- a/tests/NuGet.Services.Validation.Orchestrator.Tests/Symbol/SymbolScanValidatorFacts.cs +++ b/tests/NuGet.Services.Validation.Orchestrator.Tests/Symbol/SymbolScanValidatorFacts.cs @@ -3,19 +3,18 @@ using System; using System.Collections.Generic; +using System.IO; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; -using NuGet.Services.Validation.Symbols; using NuGet.Jobs.Validation.ScanAndSign; using NuGet.Jobs.Validation.Storage; +using NuGet.Services.Entities; using NuGet.Services.Validation.Orchestrator.PackageSigning.ScanAndSign; -using NuGet.Services.Validation.Vcs; +using NuGet.Services.Validation.Symbols; using NuGetGallery; using Xunit; -using System.IO; -using NuGet.Services.Entities; namespace NuGet.Services.Validation.Orchestrator.Tests.Symbol { diff --git a/tests/NuGet.Services.Validation.Orchestrator.Tests/ValidationProviderFacts.cs b/tests/NuGet.Services.Validation.Orchestrator.Tests/ValidationProviderFacts.cs index d2d4c5bae..b6d4263bc 100644 --- a/tests/NuGet.Services.Validation.Orchestrator.Tests/ValidationProviderFacts.cs +++ b/tests/NuGet.Services.Validation.Orchestrator.Tests/ValidationProviderFacts.cs @@ -84,7 +84,6 @@ public class GetValidator : BaseFacts /// or data fix. These names are encoded into DB tables used for orchestrator bookkeeping. ///

[Theory] - [InlineData("VcsValidator", false)] [InlineData("PackageSigningValidator", true)] [InlineData("PackageCertificatesValidator", false)] public void KnownValidatorsDoNotChangeNames(string validatorName, bool isProcessor) diff --git a/tests/NuGet.Services.Validation.Orchestrator.Tests/Vcs/VcsValidatorFacts.cs b/tests/NuGet.Services.Validation.Orchestrator.Tests/Vcs/VcsValidatorFacts.cs deleted file mode 100644 index 474925f8e..000000000 --- a/tests/NuGet.Services.Validation.Orchestrator.Tests/Vcs/VcsValidatorFacts.cs +++ /dev/null @@ -1,458 +0,0 @@ -// 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. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.WindowsAzure.Storage; -using Moq; -using NuGet.Jobs.Validation.Common; -using NuGet.Services.Entities; -using NuGet.Services.Validation.Orchestrator; -using Xunit; - -namespace NuGet.Services.Validation.Vcs -{ - public class VcsValidatorFacts - { - private const int PackageKey = 1001; - private const string PackageId = "NuGet.Versioning"; - private const string PackageVersion = "4.3.0.0-ALPHA+git"; - private static readonly Guid ValidationId = new Guid("fb9c0bac-3d4d-4cc7-ac2d-b3940e15b94d"); - private const string NupkgUrl = "https://example/nuget.versioning/4.3.0/package.nupkg"; - - private const string ValidatorName = "validator-vcs"; - - private const string NormalizedPackageId = "nuget.versioning"; - private const string NormalizedPackageVersion = "4.3.0-alpha"; - - public class TheGetStatusMethod : FactsBase - { - private static readonly ISet IncompleteEvents = new HashSet - { - ValidationEvent.ValidatorException, - ValidationEvent.BeforeVirusScanRequest, - ValidationEvent.VirusScanRequestSent, - ValidationEvent.VirusScanRequestFailed, - }; - - private static readonly ISet SucceededEvents = new HashSet - { - ValidationEvent.PackageClean, - }; - - private static readonly ISet FailedEvents = new HashSet( - new[] { (ValidationEvent)(-1) }.Concat(Enum - .GetValues(typeof(ValidationEvent)) - .Cast() - .Except(IncompleteEvents) - .Except(SucceededEvents))); - - [Fact] - public async Task ReturnsNotStartedForNullAudit() - { - // Arrange & Act - var actual = await _target.GetResultAsync(_validationRequest.Object); - - // Assert - Assert.Equal(ValidationStatus.NotStarted, actual.Status); - _validationAuditor.Verify( - x => x.ReadAuditAsync(It.IsAny(), It.IsAny(), It.IsAny()), - Times.Once); - _validationAuditor.Verify( - x => x.ReadAuditAsync(ValidationId, NormalizedPackageId, NormalizedPackageVersion), - Times.Once); - _validationService.Verify( - x => x.StartValidationProcessAsync(It.IsAny(), It.IsAny(), It.IsAny()), - Times.Never); - } - - [Theory] - [MemberData(nameof(FailedTestData))] - public async Task ReturnsFailedIfAuditHasAnyFailedEvents(ValidationEvent validationEvent) - { - // Arrange - _validationAuditor - .Setup(x => x.ReadAuditAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(new PackageValidationAudit - { - Entries = new List - { - new PackageValidationAuditEntry - { - ValidatorName = ValidatorName, - EventId = IncompleteEvents.First(), - }, - new PackageValidationAuditEntry - { - ValidatorName = ValidatorName, - EventId = SucceededEvents.First(), - }, - new PackageValidationAuditEntry - { - ValidatorName = ValidatorName, - EventId = validationEvent, - }, - }, - }); - - // Act - var actual = await _target.GetResultAsync(_validationRequest.Object); - - // Assert - Assert.Equal(ValidationStatus.Failed, actual.Status); - _validationAuditor.Verify( - x => x.ReadAuditAsync(It.IsAny(), It.IsAny(), It.IsAny()), - Times.Once); - _validationAuditor.Verify( - x => x.ReadAuditAsync(ValidationId, NormalizedPackageId, NormalizedPackageVersion), - Times.Once); - _validationService.Verify( - x => x.StartValidationProcessAsync(It.IsAny(), It.IsAny(), It.IsAny()), - Times.Never); - } - - [Theory] - [MemberData(nameof(SucceededTestData))] - public async Task ReturnsSucceededIfAuditHasNoFailedEvents(ValidationEvent validationEvent) - { - // Arrange - _validationAuditor - .Setup(x => x.ReadAuditAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(new PackageValidationAudit - { - Entries = new List - { - new PackageValidationAuditEntry - { - ValidatorName = ValidatorName, - EventId = IncompleteEvents.First(), - }, - new PackageValidationAuditEntry - { - ValidatorName = ValidatorName, - EventId = validationEvent, - }, - }, - }); - - // Act - var actual = await _target.GetResultAsync(_validationRequest.Object); - - // Assert - Assert.Equal(ValidationStatus.Succeeded, actual.Status); - _validationAuditor.Verify( - x => x.ReadAuditAsync(It.IsAny(), It.IsAny(), It.IsAny()), - Times.Once); - _validationAuditor.Verify( - x => x.ReadAuditAsync(ValidationId, NormalizedPackageId, NormalizedPackageVersion), - Times.Once); - _validationService.Verify( - x => x.StartValidationProcessAsync(It.IsAny(), It.IsAny(), It.IsAny()), - Times.Never); - } - - [Theory] - [MemberData(nameof(IncompleteTestData))] - public async Task ReturnsIncompleteIfAuditHasNoFailedOrSucceededEvents(ValidationEvent validationEvent) - { - // Arrange - _validationAuditor - .Setup(x => x.ReadAuditAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(new PackageValidationAudit - { - Entries = new List - { - new PackageValidationAuditEntry - { - ValidatorName = ValidatorName, - EventId = validationEvent, - }, - }, - }); - - // Act - var actual = await _target.GetResultAsync(_validationRequest.Object); - - // Assert - Assert.Equal(ValidationStatus.Incomplete, actual.Status); - _validationAuditor.Verify( - x => x.ReadAuditAsync(It.IsAny(), It.IsAny(), It.IsAny()), - Times.Once); - _validationAuditor.Verify( - x => x.ReadAuditAsync(ValidationId, NormalizedPackageId, NormalizedPackageVersion), - Times.Once); - _validationService.Verify( - x => x.StartValidationProcessAsync(It.IsAny(), It.IsAny(), It.IsAny()), - Times.Never); - } - - [Fact] - public async Task ReturnsIncompleteIfAuditHasEventsWithCorrectValidatorName() - { - // Arrange - var someOtherValidatorName = "some-other-validator"; - _validationAuditor - .Setup(x => x.ReadAuditAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(new PackageValidationAudit - { - Entries = new List - { - new PackageValidationAuditEntry - { - ValidatorName = someOtherValidatorName, - EventId = ValidationEvent.ScanFailed, - }, - new PackageValidationAuditEntry - { - ValidatorName = someOtherValidatorName, -#pragma warning disable CS0612 // Type or member is obsolete - EventId = ValidationEvent.UnzipSucceeeded, -#pragma warning restore CS0612 // Type or member is obsolete - }, - new PackageValidationAuditEntry - { - ValidatorName = someOtherValidatorName, - EventId = ValidationEvent.BeforeVirusScanRequest, - }, - }, - }); - - // Act - var actual = await _target.GetResultAsync(_validationRequest.Object); - - // Assert - Assert.Equal(ValidationStatus.Incomplete, actual.Status); - _validationAuditor.Verify( - x => x.ReadAuditAsync(It.IsAny(), It.IsAny(), It.IsAny()), - Times.Once); - _validationAuditor.Verify( - x => x.ReadAuditAsync(ValidationId, NormalizedPackageId, NormalizedPackageVersion), - Times.Once); - _validationService.Verify( - x => x.StartValidationProcessAsync(It.IsAny(), It.IsAny(), It.IsAny()), - Times.Never); - } - - [Fact] - public async Task ReturnsSuccessIfPackageIsExcludedByCriteria() - { - // Arrange - _criteriaEvaluator - .Setup(x => x.IsMatch(It.IsAny(), It.IsAny())) - .Returns(false); - - // Act - var actual = await _target.GetResultAsync(_validationRequest.Object); - - // Assert - Assert.Equal(ValidationStatus.Succeeded, actual.Status); - _packageService.Verify( - x => x.FindPackageByIdAndVersionStrict(PackageId, PackageVersion), - Times.Once); - _criteriaEvaluator.Verify( - x => x.IsMatch(It.IsAny(), It.IsAny()), - Times.Once); - _validationAuditor.Verify( - x => x.ReadAuditAsync(It.IsAny(), It.IsAny(), It.IsAny()), - Times.Never); - } - - public static IEnumerable IncompleteTestData => IncompleteEvents.Select(e => new object[] { e }); - public static IEnumerable SucceededTestData => SucceededEvents.Select(e => new object[] { e }); - public static IEnumerable FailedTestData => FailedEvents.Select(e => new object[] { e }); - } - - public class TheStartValidationMethod : FactsBase - { - private readonly IList _started = new List(); - - public TheStartValidationMethod() - { - _validationService - .Setup(x => x.StartValidationProcessAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(0)) - .Callback((p, v, i) => _started.Add(new StartedValidation(p, v, i))); - - _validationAuditor - .Setup(x => x.ReadAuditAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(new PackageValidationAudit()); - } - - [Fact] - public async Task UsesTheCorrectPackageForValidation() - { - // Arrange & Act - var status = await _target.StartAsync(_validationRequest.Object); - - // Assert - Assert.Equal(1, _started.Count); - var started = _started[0]; - Assert.Equal(ValidationId, started.ValidationId); - Assert.NotNull(started.Package); - Assert.Equal(NormalizedPackageId, started.Package.Id); - Assert.Equal(NormalizedPackageVersion, started.Package.Version); - Assert.Equal(NormalizedPackageVersion, started.Package.NormalizedVersion); - Assert.Equal(NupkgUrl, started.Package.DownloadUrl.ToString()); - Assert.Equal(new[] { ValidatorName }, started.Validators); - Assert.Equal(ValidationStatus.Incomplete, status.Status); - _validationService.Verify( - x => x.StartValidationProcessAsync( - It.IsAny(), - It.IsAny(), - It.IsAny()), - Times.Once); - _validationAuditor.Verify( - x => x.ReadAuditAsync(It.IsAny(), It.IsAny(), It.IsAny()), - Times.Once); - } - - [Theory] - [InlineData(HttpStatusCode.Conflict)] - [InlineData(HttpStatusCode.PreconditionFailed)] - public async Task IgnoresExceptionsFromAlreadyStartedValidation(HttpStatusCode statusCode) - { - // Arrange - _validationService - .Setup(x => x.StartValidationProcessAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .Throws(new StorageException( - new RequestResult { HttpStatusCode = (int)statusCode }, - "Storage exception", - inner: null)); - - // Act - var result = await _target.StartAsync(_validationRequest.Object); - - // Assert - Assert.Equal(ValidationStatus.Incomplete, result.Status); - _validationService.Verify( - x => x.StartValidationProcessAsync( - It.IsAny(), - It.IsAny(), - It.IsAny()), - Times.Once); - _validationAuditor.Verify( - x => x.ReadAuditAsync(It.IsAny(), It.IsAny(), It.IsAny()), - Times.Once); - } - - [Fact] - public async Task DoesNotSwallowUnexpectedExceptions() - { - // Arrange - var expected = new FormatException("Something!"); - _validationService - .Setup(x => x.StartValidationProcessAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .Throws(expected); - - // Act & Assert - var actual = await Assert.ThrowsAsync( - expected.GetType(), - () => _target.StartAsync(_validationRequest.Object)); - Assert.Same(expected, actual); - - _validationService.Verify( - x => x.StartValidationProcessAsync( - It.IsAny(), - It.IsAny(), - It.IsAny()), - Times.Once); - _validationAuditor.Verify( - x => x.ReadAuditAsync(It.IsAny(), It.IsAny(), It.IsAny()), - Times.Never); - } - - [Fact] - public async Task ReturnsSuccessIfPackageIsExcludedByCriteria() - { - // Arrange - _criteriaEvaluator - .Setup(x => x.IsMatch(It.IsAny(), It.IsAny())) - .Returns(false); - - // Act - var actual = await _target.StartAsync(_validationRequest.Object); - - // Assert - Assert.Equal(ValidationStatus.Succeeded, actual.Status); - _packageService.Verify( - x => x.FindPackageByIdAndVersionStrict(PackageId, PackageVersion), - Times.Once); - _criteriaEvaluator.Verify( - x => x.IsMatch(It.IsAny(), It.IsAny()), - Times.Once); - _validationAuditor.Verify( - x => x.ReadAuditAsync(It.IsAny(), It.IsAny(), It.IsAny()), - Times.Never); - _validationService.Verify( - x => x.StartValidationProcessAsync(It.IsAny(), It.IsAny(), It.IsAny()), - Times.Never); - } - - private class StartedValidation - { - public StartedValidation(NuGetPackage package, string[] validators, Guid validationId) - { - Package = package; - Validators = validators; - ValidationId = validationId; - } - - public NuGetPackage Package { get; } - public string[] Validators { get; } - public Guid ValidationId { get; } - } - } - - public abstract class FactsBase - { - protected readonly VcsConfiguration _config; - protected readonly Mock _validationRequest; - protected readonly Mock _validationService; - protected readonly Mock _validationAuditor; - protected readonly Mock> _packageService; - protected readonly Mock> _criteriaEvaluator; - protected readonly Mock> _options; - protected readonly Mock> _logger; - protected readonly VcsValidator _target; - - public FactsBase() - { - _config = new VcsConfiguration(); - _validationRequest = new Mock(); - _validationRequest.Setup(x => x.NupkgUrl).Returns(NupkgUrl); - _validationRequest.Setup(x => x.PackageId).Returns(PackageId); - _validationRequest.Setup(x => x.PackageKey).Returns(PackageKey); - _validationRequest.Setup(x => x.PackageVersion).Returns(PackageVersion); - _validationRequest.Setup(x => x.ValidationId).Returns(ValidationId); - - _validationService = new Mock(); - _validationAuditor = new Mock(); - _packageService = new Mock>(); - _criteriaEvaluator = new Mock>(); - _options = new Mock>(); - _logger = new Mock>(); - - _criteriaEvaluator - .Setup(x => x.IsMatch(It.IsAny(), It.IsAny())) - .Returns(true); - - _options - .Setup(x => x.Value) - .Returns(() => _config); - - _target = new VcsValidator( - _validationService.Object, - _validationAuditor.Object, - _packageService.Object, - _criteriaEvaluator.Object, - _options.Object, - _logger.Object); - } - } - } -} diff --git a/tests/Validation.Common.Tests/Properties/AssemblyInfo.cs b/tests/Validation.Common.Tests/Properties/AssemblyInfo.cs deleted file mode 100644 index 120bf7d62..000000000 --- a/tests/Validation.Common.Tests/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,10 +0,0 @@ -// 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. - -using System.Reflection; -using System.Runtime.InteropServices; - -[assembly: AssemblyTitle("Validation.Common.Tests")] -[assembly: AssemblyProduct("Validation.Common.Tests")] -[assembly: ComVisible(false)] -[assembly: Guid("f9690b52-3c92-42a0-b41f-1a6040c2d2ee")] diff --git a/tests/Validation.Common.Tests/Validation.Common.Tests.csproj b/tests/Validation.Common.Tests/Validation.Common.Tests.csproj deleted file mode 100644 index f8c1d95bd..000000000 --- a/tests/Validation.Common.Tests/Validation.Common.Tests.csproj +++ /dev/null @@ -1,60 +0,0 @@ - - - - - Debug - AnyCPU - {F9690B52-3C92-42A0-B41F-1A6040C2D2EE} - Library - Properties - Validation.Common.Tests - Validation.Common.Tests - v4.6.2 - 512 - true - - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - - - - - - - {2539ddf3-0cc5-4a03-b5f9-39b47744a7bd} - Validation.Common - - - - - - - \ No newline at end of file diff --git a/tests/Validation.Common.Tests/Validators/Vcs/VcsValidatorFacts.cs b/tests/Validation.Common.Tests/Validators/Vcs/VcsValidatorFacts.cs deleted file mode 100644 index d2976c126..000000000 --- a/tests/Validation.Common.Tests/Validators/Vcs/VcsValidatorFacts.cs +++ /dev/null @@ -1,186 +0,0 @@ -// 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. - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Moq; -using NuGet.Jobs.Validation.Common; -using NuGet.Jobs.Validation.Common.Validators; -using NuGet.Jobs.Validation.Common.Validators.Vcs; -using NuGet.Services.VirusScanning.Core; -using Xunit; - -namespace Validation.Common.Tests -{ - public class VcsValidatorFacts - { - public class TheNameProperty : FactsBase - { - [Fact] - public void NeverChanges() - { - Assert.Equal("validator-vcs", _target.Name); - } - } - - public class TheValidateMethod : FactsBase - { - [Fact] - public async Task PrefersTheDownloadUrlOverBuildingTheUrl() - { - // Arrange - _message.Package.DownloadUrl = new Uri("http://example/some-custom-url"); - - // Act - var actual = await _target.ValidateAsync(_message, _auditEntries); - - // Assert - Assert.Equal(ValidationResult.Asynchronous, actual); - _scanningService.Verify( - x => x.CreateVirusScanJobAsync( - _message.Package.DownloadUrl.AbsoluteUri, - _callbackUrl, - $"NuGet - f470b9fb-0243-4f65-8aef-90d93dfe1a03 - NuGet.Versioning 3.4.0-ALPHA", - _message.ValidationId), - Times.Once); - } - - [Fact] - public async Task ReturnsAsynchronousOnSuccess() - { - // Arrange & Act - var actual = await _target.ValidateAsync(_message, _auditEntries); - - // Assert - Assert.Equal(ValidationResult.Asynchronous, actual); - _scanningService.Verify( - x => x.CreateVirusScanJobAsync( - "http://example/packages/nuget.versioning/3.4.0-alpha/nuget.versioning.3.4.0-alpha.nupkg", - _callbackUrl, - $"NuGet - f470b9fb-0243-4f65-8aef-90d93dfe1a03 - NuGet.Versioning 3.4.0-ALPHA", - _message.ValidationId), - Times.Once); - _scanningService.Verify( - x => x.CreateVirusScanJobAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), - Times.Once); - } - - [Fact] - public async Task FailsWhenThereIsAnErrorMessage() - { - // Arrange - var errorMessage = "Error!"; - _virusScanJob.ErrorMessage = errorMessage; - - // Act & Assert - var exception = await Assert.ThrowsAsync( - () => _target.ValidateAsync(_message, _auditEntries)); - Assert.Equal(errorMessage, exception.Message); - } - - [Fact] - public async Task FailsIfAllResponseValuesAreNull() - { - // Arrange - _virusScanJob.RequestId = null; - _virusScanJob.JobId = null; - _virusScanJob.RegionCode = null; - - // Act & Assert - var exception = await Assert.ThrowsAsync( - () => _target.ValidateAsync(_message, _auditEntries)); - Assert.Equal("The request had no request ID, job ID, and region code.", exception.Message); - } - - [Fact] - public async Task SucceedsIfOnlyRequestIdIsNull() - { - // Arrange - _virusScanJob.RequestId = null; - - // Act - var actual = await _target.ValidateAsync(_message, _auditEntries); - - // Assert - Assert.Equal(ValidationResult.Asynchronous, actual); - } - - [Fact] - public async Task SucceedsIfOnlyJobIdIsNull() - { - // Arrange - _virusScanJob.JobId = null; - - // Act - var actual = await _target.ValidateAsync(_message, _auditEntries); - - // Assert - Assert.Equal(ValidationResult.Asynchronous, actual); - } - - [Fact] - public async Task SucceedsIfOnlyRegionCodeIsNull() - { - // Arrange - _virusScanJob.RegionCode = null; - - // Act - var actual = await _target.ValidateAsync(_message, _auditEntries); - - // Assert - Assert.Equal(ValidationResult.Asynchronous, actual); - } - } - - public abstract class FactsBase - { - protected readonly Uri _callbackUrl; - protected readonly string _packageUrlTemplate; - protected readonly Mock _scanningService; - protected readonly Mock> _logger; - protected PackageValidationMessage _message; - protected List _auditEntries; - protected VirusScanJob _virusScanJob; - protected readonly VcsValidator _target; - - public FactsBase() - { - _callbackUrl = new Uri("http://example/callback"); - _packageUrlTemplate = "http://example/packages/{id}/{version}/{id}.{version}.nupkg"; - _scanningService = new Mock(); - _logger = new Mock>(); - - _message = new PackageValidationMessage - { - PackageId = "NuGet.Versioning", - PackageVersion = "3.4.0-ALPHA", - ValidationId = new Guid("f470b9fb-0243-4f65-8aef-90d93dfe1a03"), - Package = new NuGetPackage - { - Id = "NuGet.Versioning", - NormalizedVersion = "3.4.0-ALPHA" - } - }; - _auditEntries = new List(); - _virusScanJob = new VirusScanJob - { - JobId = "123", - RequestId = "456", - RegionCode = "USW", - }; - - _scanningService - .Setup(x => x.CreateVirusScanJobAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(() => Task.FromResult(_virusScanJob)); - - _target = new VcsValidator( - _callbackUrl, - _packageUrlTemplate, - _scanningService.Object, - _logger.Object); - } - } - } -} diff --git a/tests/Validation.Common.Tests/project.json b/tests/Validation.Common.Tests/project.json deleted file mode 100644 index 715251ae9..000000000 --- a/tests/Validation.Common.Tests/project.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "1.0.0", - "Moq": "4.7.145", - "NuGet.Services.VirusScanning.Vcs": "3.2.0", - "xunit": "2.3.1", - "xunit.runner.visualstudio": "2.3.1" - }, - "frameworks": { - "net462": {} - }, - "runtimes": { - "win": {} - } -} \ No newline at end of file diff --git a/tests/Validation.Helper.Tests/MarkCleanFacts.cs b/tests/Validation.Helper.Tests/MarkCleanFacts.cs deleted file mode 100644 index b951a01b4..000000000 --- a/tests/Validation.Helper.Tests/MarkCleanFacts.cs +++ /dev/null @@ -1,51 +0,0 @@ -// 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. - -using System; -using System.Collections.Generic; -using Microsoft.Extensions.Logging; -using Moq; -using Xunit; - -namespace NuGet.Jobs.Validation.Helper.Tests -{ - public class MarkCleanFacts - { - [Theory] - [InlineData("Newtonsoft.Json", "Newtonsoft.Json")] - [InlineData("%E8%8B%8F%E5%AE%81%E5%BC%80%E6%94%BE%E5%B9%B3%E5%8F%B0", "苏宁开放平台")] - public void PackageIdUrlDecoded(string argument, string expectedPackageid) - { - MarkClean mc = CreateMarkClean(argument, "1.0.0"); - - Assert.Equal(expectedPackageid, mc.PackageId); - } - - [Theory] - [InlineData("1.0.0", "1.0.0")] - [InlineData("1.0.0+msbuild", "1.0.0 msbuild")] - [InlineData("1.0.0%2Bmsbuild", "1.0.0+msbuild")] - public void PackageVersionUrlDecoded(string argument, string expectedPackageVersion) - { - MarkClean mc = CreateMarkClean("Newtonsoft.Json", argument); - - Assert.Equal(expectedPackageVersion, mc.PackageVersion); - } - - private static MarkClean CreateMarkClean(string packageId, string packageVersion) - { - var loggerMock = new Mock>(); - var args = new Dictionary - { - { CommandLineArguments.PackageId, packageId }, - { CommandLineArguments.PackageVersion, packageVersion }, - { CommandLineArguments.ValidationId, Guid.NewGuid().ToString() }, - { CommandLineArguments.Comment, "comment" }, - { CommandLineArguments.Alias, "angrigor" }, - }; - - var mc = new MarkClean(args, loggerMock.Object, null, "container", null, null, "https://www.nuget.org/"); - return mc; - } - } -} diff --git a/tests/Validation.Helper.Tests/Properties/AssemblyInfo.cs b/tests/Validation.Helper.Tests/Properties/AssemblyInfo.cs deleted file mode 100644 index 980a80fd8..000000000 --- a/tests/Validation.Helper.Tests/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Validation.Helper.Tests")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Validation.Helper.Tests")] -[assembly: AssemblyCopyright("Copyright © 2017")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("8336bebe-ec6a-4e40-a1c9-8c34a507e62d")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/tests/Validation.Helper.Tests/RescanFacts.cs b/tests/Validation.Helper.Tests/RescanFacts.cs deleted file mode 100644 index f44b96560..000000000 --- a/tests/Validation.Helper.Tests/RescanFacts.cs +++ /dev/null @@ -1,51 +0,0 @@ -// 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. - -using System; -using System.Collections.Generic; -using Microsoft.Extensions.Logging; -using Moq; -using Xunit; - -namespace NuGet.Jobs.Validation.Helper.Tests -{ - public class RescanFacts - { - [Theory] - [InlineData("Newtonsoft.Json", "Newtonsoft.Json")] - [InlineData("%E8%8B%8F%E5%AE%81%E5%BC%80%E6%94%BE%E5%B9%B3%E5%8F%B0", "苏宁开放平台")] - public void PackageIdUrlDecoded(string argument, string expectedPackageid) - { - Rescan mc = CreateRescan(argument, "1.0.0"); - - Assert.Equal(expectedPackageid, mc.PackageId); - } - - [Theory] - [InlineData("1.0.0", "1.0.0")] - [InlineData("1.0.0+msbuild", "1.0.0 msbuild")] - [InlineData("1.0.0%2Bmsbuild", "1.0.0+msbuild")] - public void PackageVersionUrlDecoded(string argument, string expectedPackageVersion) - { - Rescan mc = CreateRescan("Newtonsoft.Json", argument); - - Assert.Equal(expectedPackageVersion, mc.PackageVersion); - } - - private static Rescan CreateRescan(string packageId, string packageVersion) - { - var loggerMock = new Mock>(); - var args = new Dictionary - { - { CommandLineArguments.PackageId, packageId }, - { CommandLineArguments.PackageVersion, packageVersion }, - { CommandLineArguments.ValidationId, Guid.NewGuid().ToString() }, - { CommandLineArguments.Comment, "comment" }, - { CommandLineArguments.Alias, "angrigor" }, - }; - - var mc = new Rescan(args, loggerMock.Object, null, "container", null, null, "https://www.nuget.org/"); - return mc; - } - } -} \ No newline at end of file diff --git a/tests/Validation.Helper.Tests/Validation.Helper.Tests.csproj b/tests/Validation.Helper.Tests/Validation.Helper.Tests.csproj deleted file mode 100644 index d975b5809..000000000 --- a/tests/Validation.Helper.Tests/Validation.Helper.Tests.csproj +++ /dev/null @@ -1,65 +0,0 @@ - - - - - Debug - AnyCPU - {8336BEBE-EC6A-4E40-A1C9-8C34A507E62D} - Library - Properties - NuGet.Jobs.Validation.Helper.Tests - Validation.Helper.Tests - v4.6.2 - 512 - true - - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - - - - - - - - - - - {2539DDF3-0CC5-4A03-B5F9-39B47744A7BD} - Validation.Common - - - {305fb2c1-01fd-413d-b788-7b85bad85a41} - Validation.Helper - - - - \ No newline at end of file diff --git a/tests/Validation.Helper.Tests/project.json b/tests/Validation.Helper.Tests/project.json deleted file mode 100644 index a71a70525..000000000 --- a/tests/Validation.Helper.Tests/project.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "dependencies": { - "Microsoft.Data.OData": "5.7.0", - "Microsoft.Data.Services.Client": "5.7.0", - "Microsoft.Extensions.Logging.Abstractions": "1.0.0", - "Moq": "4.7.145", - "xunit": "2.3.1", - "xunit.runner.visualstudio": "2.3.1" - }, - "frameworks": { - "net462": {} - }, - "runtimes": { - "win": {} - } -} \ No newline at end of file diff --git a/tests/Validation.PackageSigning.ScanAndSign.Tests/ScanAndSignProcessorFacts.cs b/tests/Validation.PackageSigning.ScanAndSign.Tests/ScanAndSignProcessorFacts.cs index 4438ff9bc..21ee211f4 100644 --- a/tests/Validation.PackageSigning.ScanAndSign.Tests/ScanAndSignProcessorFacts.cs +++ b/tests/Validation.PackageSigning.ScanAndSign.Tests/ScanAndSignProcessorFacts.cs @@ -8,16 +8,15 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; -using NuGet.Jobs.Validation.Storage; using NuGet.Jobs.Validation.ScanAndSign; +using NuGet.Jobs.Validation.Storage; +using NuGet.Services.Entities; using NuGet.Services.Validation; using NuGet.Services.Validation.Orchestrator; using NuGet.Services.Validation.Orchestrator.PackageSigning.ScanAndSign; -using NuGet.Services.Validation.Vcs; using NuGetGallery; using Tests.ContextHelpers; using Xunit; -using NuGet.Services.Entities; namespace Validation.PackageSigning.ScanAndSign.Tests { diff --git a/tests/Validation.PackageSigning.ScanAndSign.Tests/ScanValidatorFacts.cs b/tests/Validation.PackageSigning.ScanAndSign.Tests/ScanValidatorFacts.cs index bb4854b8f..f7c632bcd 100644 --- a/tests/Validation.PackageSigning.ScanAndSign.Tests/ScanValidatorFacts.cs +++ b/tests/Validation.PackageSigning.ScanAndSign.Tests/ScanValidatorFacts.cs @@ -7,16 +7,15 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; -using NuGet.Jobs.Validation.Storage; using NuGet.Jobs.Validation.ScanAndSign; +using NuGet.Jobs.Validation.Storage; +using NuGet.Services.Entities; using NuGet.Services.Validation; using NuGet.Services.Validation.Orchestrator; using NuGet.Services.Validation.Orchestrator.PackageSigning.ScanAndSign; -using NuGet.Services.Validation.Vcs; using NuGetGallery; using Tests.ContextHelpers; using Xunit; -using NuGet.Services.Entities; namespace Validation.PackageSigning.ScanAndSign.Tests {