diff --git a/src/code/ContainerRegistryServerAPICalls.cs b/src/code/ContainerRegistryServerAPICalls.cs index 9c17c0db0..c3e6c3d17 100644 --- a/src/code/ContainerRegistryServerAPICalls.cs +++ b/src/code/ContainerRegistryServerAPICalls.cs @@ -31,6 +31,7 @@ internal class ContainerRegistryServerAPICalls : ServerApiCall #region Members public override PSRepositoryInfo Repository { get; set; } + internal override bool WriteWarnings { get; set; } public String Registry { get; set; } private readonly PSCmdlet _cmdletPassedIn; private HttpClient _sessionClient { get; set; } diff --git a/src/code/FindHelper.cs b/src/code/FindHelper.cs index d8287c689..365aa7b49 100644 --- a/src/code/FindHelper.cs +++ b/src/code/FindHelper.cs @@ -206,7 +206,10 @@ public IEnumerable FindByResourceName( // Set network credentials via passed in credentials, AzArtifacts CredentialProvider, or SecretManagement. _networkCredential = currentRepository.SetNetworkCredentials(_networkCredential, _cmdletPassedIn); - ServerApiCall currentServer = ServerFactory.GetServer(currentRepository, _cmdletPassedIn, _networkCredential); + bool shouldReportErrorForEachRepo = !suppressErrors && !_repositoryNameContainsWildcard; + bool shouldWriteWarningsForRepo = !shouldReportErrorForEachRepo; // Only write warnings for a repository if we are not writing errors for each repository. + + ServerApiCall currentServer = ServerFactory.GetServer(currentRepository, _cmdletPassedIn, _networkCredential, writeWarnings: shouldWriteWarningsForRepo); if (currentServer == null) { // this indicates that PSRepositoryInfo.APIVersion = PSRepositoryInfo.APIVersion.unknown @@ -222,7 +225,6 @@ public IEnumerable FindByResourceName( ResponseUtil currentResponseUtil = ResponseUtilFactory.GetResponseUtil(currentRepository); _cmdletPassedIn.WriteDebug($"Searching through repository '{currentRepository.Name}'"); - bool shouldReportErrorForEachRepo = !suppressErrors && !_repositoryNameContainsWildcard; foreach (PSResourceInfo currentPkg in SearchByNames(currentServer, currentResponseUtil, currentRepository, shouldReportErrorForEachRepo)) { if (currentPkg == null) diff --git a/src/code/InstallHelper.cs b/src/code/InstallHelper.cs index fef419a4f..062b8d76c 100644 --- a/src/code/InstallHelper.cs +++ b/src/code/InstallHelper.cs @@ -184,6 +184,7 @@ private List ProcessRepositories( { _cmdletPassedIn.WriteDebug("In InstallHelper::ProcessRepositories()"); List allPkgsInstalled = new(); + bool containsWildcard = false; if (repository != null && repository.Length != 0) { // Write error and disregard repository entries containing wildcards. @@ -199,7 +200,6 @@ private List ProcessRepositories( // If repository entries includes wildcards and non-wildcard names, write terminating error // Ex: -Repository *Gallery, localRepo - bool containsWildcard = false; bool containsNonWildcard = false; foreach (string repoName in repository) { @@ -222,6 +222,10 @@ private List ProcessRepositories( _cmdletPassedIn)); } } + else + { + containsWildcard = true; + } // Get repositories to search. List repositoriesToSearch; @@ -289,7 +293,8 @@ private List ProcessRepositories( // Set network credentials via passed in credentials, AzArtifacts CredentialProvider, or SecretManagement. _networkCredential = currentRepository.SetNetworkCredentials(_networkCredential, _cmdletPassedIn); - ServerApiCall currentServer = ServerFactory.GetServer(currentRepository, _cmdletPassedIn, _networkCredential); + bool writeWarningsForRepo = containsWildcard; + ServerApiCall currentServer = ServerFactory.GetServer(currentRepository, _cmdletPassedIn, _networkCredential, writeWarningsForRepo); if (currentServer == null) { @@ -537,7 +542,7 @@ private List InstallPackages( errRecord: out ErrorRecord errRecord); // At this point parent package is installed to temp path. - if (errRecord != null) + if (errRecord != null && !currentServer.WriteWarnings) { if (errRecord.FullyQualifiedErrorId.Equals("PackageNotFound")) { diff --git a/src/code/LocalServerApiCalls.cs b/src/code/LocalServerApiCalls.cs index cc43c340d..32a7188dd 100644 --- a/src/code/LocalServerApiCalls.cs +++ b/src/code/LocalServerApiCalls.cs @@ -24,14 +24,16 @@ internal class LocalServerAPICalls : ServerApiCall private readonly PSCmdlet _cmdletPassedIn; private readonly FindResponseType _localServerFindResponseType = FindResponseType.ResponseHashtable; private readonly string _fileTypeKey = "filetype"; + internal override bool WriteWarnings { get; set; } #endregion #region Constructor - public LocalServerAPICalls (PSRepositoryInfo repository, PSCmdlet cmdletPassedIn, NetworkCredential networkCredential) : base (repository, networkCredential) + public LocalServerAPICalls (PSRepositoryInfo repository, PSCmdlet cmdletPassedIn, NetworkCredential networkCredential, bool writeWarnings = false) : base (repository, networkCredential, writeWarnings) { this.Repository = repository; + this.WriteWarnings = writeWarnings; _cmdletPassedIn = cmdletPassedIn; } @@ -263,7 +265,28 @@ private FindResults FindNameHelper(string packageName, string[] tags, bool inclu string regexPattern = $"{packageName}" + @"(\.\d+){1,3}(?:[a-zA-Z0-9-.]+|.\d)?\.nupkg"; _cmdletPassedIn.WriteDebug($"package file name pattern to be searched for is: {regexPattern}"); - foreach (string path in Directory.GetFiles(Repository.Uri.LocalPath)) + string[] foundFiles = Utils.EmptyStrArray; + try + { + foundFiles = Directory.GetFiles(Repository.Uri.LocalPath); + } + catch (Exception e) + { + if (WriteWarnings) + { + _cmdletPassedIn.WriteWarning($"Unable to resolve repository '{Repository.Name}' with source '{Repository.Uri.LocalPath}' due to exception: {e.Message}"); + } + + errRecord = new ErrorRecord( + exception: e, + "FileAccessFailure", + ErrorCategory.ReadError, + this); + + return findResponse; + } + + foreach (string path in foundFiles) { string packageFullName = Path.GetFileName(path); bool isMatch = Regex.IsMatch(packageFullName, regexPattern, RegexOptions.IgnoreCase); @@ -333,7 +356,12 @@ private FindResults FindNameGlobbingHelper(string packageName, string[] tags, bo List pkgsFound = new List(); errRecord = null; - Hashtable pkgVersionsFound = GetMatchingFilesGivenNamePattern(packageNameWithWildcard: packageName, includePrerelease: includePrerelease); + Hashtable pkgVersionsFound = GetMatchingFilesGivenNamePattern(packageNameWithWildcard: packageName, includePrerelease: includePrerelease, out errRecord); + if (errRecord != null) + { + // ErrorRecord errRecord is only set if directory access to retrieve files failed (i.e network error when accessing file share, incorrect directory path, etc), not if desired files within directory are not found since this is a wildcard scenario. + return findResponse; + } List pkgNamesList = pkgVersionsFound.Keys.Cast().ToList(); foreach(string pkgFound in pkgNamesList) @@ -382,7 +410,28 @@ private FindResults FindVersionHelper(string packageName, string version, string string pkgPath = String.Empty; string actualPkgName = String.Empty; - foreach (string path in Directory.GetFiles(Repository.Uri.LocalPath)) + string[] foundFiles = Utils.EmptyStrArray; + try + { + foundFiles = Directory.GetFiles(Repository.Uri.LocalPath); + } + catch (Exception e) + { + if (WriteWarnings) + { + _cmdletPassedIn.WriteWarning($"Unable to resolve repository '{Repository.Name}' with source '{Repository.Uri.LocalPath}' due to exception: {e.Message}"); + } + + errRecord = new ErrorRecord( + exception: e, + "FileAccessFailure", + ErrorCategory.ReadError, + this); + + return findResponse; + } + + foreach (string path in foundFiles) { string packageFullName = Path.GetFileName(path); bool isMatch = Regex.IsMatch(packageFullName, regexPattern, RegexOptions.IgnoreCase); @@ -450,7 +499,12 @@ private FindResults FindTagsHelper(string[] tags, bool includePrerelease, out Er List pkgsFound = new List(); errRecord = null; - Hashtable pkgVersionsFound = GetMatchingFilesGivenNamePattern(packageNameWithWildcard: String.Empty, includePrerelease: includePrerelease); + Hashtable pkgVersionsFound = GetMatchingFilesGivenNamePattern(packageNameWithWildcard: String.Empty, includePrerelease: includePrerelease, errRecord: out errRecord); + if (errRecord != null) + { + // ErrorRecord errRecord is only set if directory access to retrieve files failed (i.e network error when accessing file share, incorrect directory path, etc), not if desired files within directory are not found since this is a wildcard scenario. + return findResponse; + } List pkgNamesList = pkgVersionsFound.Keys.Cast().ToList(); foreach(string pkgFound in pkgNamesList) @@ -496,7 +550,23 @@ private Stream InstallName(string packageName, bool includePrerelease, out Error NuGetVersion latestVersion = new NuGetVersion("0.0.0.0"); String latestVersionPath = String.Empty; - foreach (string path in Directory.GetFiles(Repository.Uri.LocalPath)) + string[] foundFiles = Utils.EmptyStrArray; + try + { + foundFiles = Directory.GetFiles(Repository.Uri.LocalPath); + } + catch (Exception e) + { + errRecord = new ErrorRecord( + exception: e, + "FileAccessFailure", + ErrorCategory.ReadError, + this); + + return fs; + } + + foreach (string path in foundFiles) { string packageFullName = Path.GetFileName(path); @@ -581,7 +651,28 @@ private Stream InstallVersion(string packageName, string version, out ErrorRecor WildcardPattern pkgNamePattern = new WildcardPattern($"{packageName}.{version}.nupkg*", WildcardOptions.IgnoreCase); String pkgVersionPath = String.Empty; - foreach (string path in Directory.GetFiles(Repository.Uri.LocalPath)) + string[] foundFiles = Utils.EmptyStrArray; + try + { + foundFiles = Directory.GetFiles(Repository.Uri.LocalPath); + } + catch (Exception e) + { + if (WriteWarnings) + { + _cmdletPassedIn.WriteWarning($"Unable to resolve repository '{Repository.Name}' with source '{Repository.Uri.LocalPath}' due to exception: {e.Message}"); + } + + errRecord = new ErrorRecord( + exception: e, + "FileAccessFailure", + ErrorCategory.ReadError, + this); + + return fs; + } + + foreach (string path in foundFiles) { string packageFullName = Path.GetFileName(path); @@ -778,7 +869,23 @@ private Hashtable GetMatchingFilesGivenSpecificName(string packageName, bool inc Hashtable pkgVersionsFound = new Hashtable(StringComparer.OrdinalIgnoreCase); errRecord = null; - foreach (string path in Directory.GetFiles(Repository.Uri.LocalPath)) + string[] foundFiles = Utils.EmptyStrArray; + try + { + foundFiles = Directory.GetFiles(Repository.Uri.LocalPath); + } + catch (Exception e) + { + errRecord = new ErrorRecord( + exception: e, + "FileAccessFailure", + ErrorCategory.ReadError, + this); + + return pkgVersionsFound; + } + + foreach (string path in foundFiles) { string packageFullName = Path.GetFileName(path); @@ -809,9 +916,10 @@ private Hashtable GetMatchingFilesGivenSpecificName(string packageName, bool inc /// hashtable with those that match the name wildcard pattern and prerelease requirements provided. /// This helper method is called for FindAll(), FindTags(), FindNameGlobbing() scenarios. /// - private Hashtable GetMatchingFilesGivenNamePattern(string packageNameWithWildcard, bool includePrerelease) + private Hashtable GetMatchingFilesGivenNamePattern(string packageNameWithWildcard, bool includePrerelease, out ErrorRecord errRecord) { _cmdletPassedIn.WriteDebug("In LocalServerApiCalls::GetMatchingFilesGivenNamePattern()"); + errRecord = null; bool isNameFilteringRequired = !String.IsNullOrEmpty(packageNameWithWildcard); // wildcard name possibilities: power*, *get, power*get @@ -820,7 +928,23 @@ private Hashtable GetMatchingFilesGivenNamePattern(string packageNameWithWildcar Regex rx = new Regex(@"\.\d+\.", RegexOptions.Compiled | RegexOptions.IgnoreCase); Hashtable pkgVersionsFound = new Hashtable(StringComparer.OrdinalIgnoreCase); - foreach (string path in Directory.GetFiles(Repository.Uri.LocalPath)) + string[] foundFiles = Utils.EmptyStrArray; + try + { + foundFiles = Directory.GetFiles(Repository.Uri.LocalPath); + } + catch (Exception e) + { + errRecord = new ErrorRecord( + exception: e, + "FileAccessFailure", + ErrorCategory.ReadError, + this); + + return pkgVersionsFound; + } + + foreach (string path in foundFiles) { string packageFullName = Path.GetFileName(path); MatchCollection matches = rx.Matches(packageFullName); diff --git a/src/code/NuGetServerAPICalls.cs b/src/code/NuGetServerAPICalls.cs index 1497c83da..ba4cc6932 100644 --- a/src/code/NuGetServerAPICalls.cs +++ b/src/code/NuGetServerAPICalls.cs @@ -21,6 +21,7 @@ internal class NuGetServerAPICalls : ServerApiCall #region Members public override PSRepositoryInfo Repository { get; set; } + internal override bool WriteWarnings { get; set; } private readonly PSCmdlet _cmdletPassedIn; private HttpClient _sessionClient { get; set; } private static readonly Hashtable[] emptyHashResponses = new Hashtable[]{}; diff --git a/src/code/ServerApiCall.cs b/src/code/ServerApiCall.cs index 4580e362e..98357debe 100644 --- a/src/code/ServerApiCall.cs +++ b/src/code/ServerApiCall.cs @@ -18,12 +18,20 @@ internal abstract class ServerApiCall : IServerAPICalls #region Members public abstract PSRepositoryInfo Repository { get; set; } + internal abstract bool WriteWarnings { get; set; } private HttpClient _sessionClient { get; set; } #endregion #region Constructor + public ServerApiCall(PSRepositoryInfo repository, NetworkCredential networkCredential, bool writeWarnings) + : this(repository, networkCredential) + { + this.WriteWarnings = writeWarnings; + } + + public ServerApiCall(PSRepositoryInfo repository, NetworkCredential networkCredential) { this.Repository = repository; diff --git a/src/code/ServerFactory.cs b/src/code/ServerFactory.cs index 7be9d1ca1..7f0aa8949 100644 --- a/src/code/ServerFactory.cs +++ b/src/code/ServerFactory.cs @@ -30,7 +30,7 @@ internal static string UserAgentString() internal class ServerFactory { - public static ServerApiCall GetServer(PSRepositoryInfo repository, PSCmdlet cmdletPassedIn, NetworkCredential networkCredential) + public static ServerApiCall GetServer(PSRepositoryInfo repository, PSCmdlet cmdletPassedIn, NetworkCredential networkCredential, bool writeWarnings = false) { PSRepositoryInfo.APIVersion repoApiVersion = repository.ApiVersion; ServerApiCall currentServer = null; @@ -47,7 +47,7 @@ public static ServerApiCall GetServer(PSRepositoryInfo repository, PSCmdlet cmdl break; case PSRepositoryInfo.APIVersion.Local: - currentServer = new LocalServerAPICalls(repository, cmdletPassedIn, networkCredential); + currentServer = new LocalServerAPICalls(repository, cmdletPassedIn, networkCredential, writeWarnings); break; case PSRepositoryInfo.APIVersion.NugetServer: diff --git a/src/code/V2ServerAPICalls.cs b/src/code/V2ServerAPICalls.cs index 94d0b3a0b..c5654ae94 100644 --- a/src/code/V2ServerAPICalls.cs +++ b/src/code/V2ServerAPICalls.cs @@ -37,6 +37,7 @@ internal class V2ServerAPICalls : ServerApiCall #region Members public override PSRepositoryInfo Repository { get; set; } + internal override bool WriteWarnings { get; set; } private readonly PSCmdlet _cmdletPassedIn; private HttpClient _sessionClient { get; set; } private static readonly Hashtable[] emptyHashResponses = new Hashtable[]{}; diff --git a/src/code/V3ServerAPICalls.cs b/src/code/V3ServerAPICalls.cs index 903b1da55..ca32227f5 100644 --- a/src/code/V3ServerAPICalls.cs +++ b/src/code/V3ServerAPICalls.cs @@ -21,6 +21,7 @@ internal class V3ServerAPICalls : ServerApiCall { #region Members public override PSRepositoryInfo Repository { get; set; } + internal override bool WriteWarnings { get; set; } private readonly PSCmdlet _cmdletPassedIn; private HttpClient _sessionClient { get; set; } private bool _isNuGetRepo { get; set; } diff --git a/test/FindPSResourceTests/FindPSResourceLocal.Tests.ps1 b/test/FindPSResourceTests/FindPSResourceLocal.Tests.ps1 index a3cbe2335..961e975a9 100644 --- a/test/FindPSResourceTests/FindPSResourceLocal.Tests.ps1 +++ b/test/FindPSResourceTests/FindPSResourceLocal.Tests.ps1 @@ -12,6 +12,7 @@ Describe 'Test Find-PSResource for local repositories' -tags 'CI' { BeforeAll{ $localRepo = "psgettestlocal" $localUNCRepo = 'psgettestlocal3' + $localPrivateRepo = "psgettestlocal5" $testModuleName = "test_local_mod" $testModuleName2 = "test_local_mod2" $testModuleName3 = "Test_Local_Mod3" @@ -347,4 +348,22 @@ Describe 'Test Find-PSResource for local repositories' -tags 'CI' { $res = Find-PSResource -Name 'Az.KeyVault' -Repository $localRepo $res.Version | Should -Be "6.3.1" } + + It "Find should not silently fail if network connection to local private repository cannot be established and remainder repositories should be searched" { + $privateRepo = Get-PSResourceRepository $localPrivateRepo + $res = Find-PSResource -Name $testModuleName -WarningVariable WarningVar -WarningAction SilentlyContinue + $WarningVar | Should -Not -BeNullOrEmpty + $WarningVar[0] | Should -Match "$($privateRepo.Uri.LocalPath)" + $res.Name | Should -Contain $testModuleName + $res.Version | Should -Be "1.0.0" + } + + It "Find should not silently fail if network connection to local private repository cannot be established and package version was provided and remainder repositories should be searched" { + $privateRepo = Get-PSResourceRepository $localPrivateRepo + $res = Find-PSResource -Name $testModuleName -Version "1.0.0" -WarningVariable WarningVar -WarningAction SilentlyContinue + $WarningVar | Should -Not -BeNullOrEmpty + $WarningVar[0] | Should -Match "$($privateRepo.Uri.LocalPath)" + $res.Name | Should -Contain $testModuleName + $res.Version | Should -Be "1.0.0" + } } diff --git a/test/InstallPSResourceTests/InstallPSResourceLocal.Tests.ps1 b/test/InstallPSResourceTests/InstallPSResourceLocal.Tests.ps1 index 77c8766ad..030d43ddf 100644 --- a/test/InstallPSResourceTests/InstallPSResourceLocal.Tests.ps1 +++ b/test/InstallPSResourceTests/InstallPSResourceLocal.Tests.ps1 @@ -16,6 +16,7 @@ Describe 'Test Install-PSResource for local repositories' -tags 'CI' { BeforeAll { $localRepo = "psgettestlocal" $localUNCRepo = "psgettestlocal3" + $localPrivateRepo = "psgettestlocal5" $localNupkgRepo = "localNupkgRepo" $testModuleName = "test_local_mod" $testModuleName2 = "test_local_mod2" @@ -296,4 +297,22 @@ Describe 'Test Install-PSResource for local repositories' -tags 'CI' { $pkg.Name | Should -Be $nupkgName $pkg.Version | Should -Be $nupkgVersion } + + It "Install should not silently fail if network connection to local private repository cannot be established and remainder repositories should be searched" { + $privateRepo = Get-PSResourceRepository $localPrivateRepo + $res = Install-PSResource -Name $testModuleName -TrustRepository -PassThru -WarningVariable WarningVar -WarningAction SilentlyContinue + $WarningVar | Should -Not -BeNullOrEmpty + $WarningVar[0] | Should -Match "$($privateRepo.Uri.LocalPath)" + $res.Name | Should -Contain $testModuleName + $res.Version | Should -Be "1.0.0" + } + + It "Install should not silently fail if network connection to local private repository cannot be established and package version was provided and remainder repositories should be searched" { + $privateRepo = Get-PSResourceRepository $localPrivateRepo + $res = Install-PSResource -Name $testModuleName -Version "1.0.0" -TrustRepository -PassThru -WarningVariable WarningVar -WarningAction SilentlyContinue + $WarningVar | Should -Not -BeNullOrEmpty + $WarningVar[0] | Should -Match "$($privateRepo.Uri.LocalPath)" + $res.Name | Should -Contain $testModuleName + $res.Version | Should -Be "1.0.0" + } } diff --git a/test/PSGetTestUtils.psm1 b/test/PSGetTestUtils.psm1 index 6a384c17c..194bb7c4b 100644 --- a/test/PSGetTestUtils.psm1 +++ b/test/PSGetTestUtils.psm1 @@ -270,7 +270,16 @@ function Register-LocalRepos { } Register-PSResourceRepository @localRepoParams2 - Write-Verbose "registered psgettestlocal, psgettestlocal2, psgettestlocal3, psgettestlocal4" + $path4 = "\\localhost\PSRepoLocal" + $localRepoParams2 = @{ + Name = "psgettestlocal5" + Uri = $path4 + Priority = 30 + Trusted = $false + } + Register-PSResourceRepository @localRepoParams2 + + Write-Verbose "registered psgettestlocal, psgettestlocal2, psgettestlocal3, psgettestlocal4, psgettestlocal5" } function Register-LocalTestNupkgsRepo {