diff --git a/src/code/RegisterPSResourceRepository.cs b/src/code/RegisterPSResourceRepository.cs index a4994df8a..b53f6d656 100644 --- a/src/code/RegisterPSResourceRepository.cs +++ b/src/code/RegisterPSResourceRepository.cs @@ -29,14 +29,19 @@ class RegisterPSResourceRepository : PSCmdlet, IDynamicParameters #region Members private readonly string PSGalleryRepoName = "PSGallery"; + private readonly string MicrosoftArtifactRegistryRepoName = "MicrosoftArtifactRegistry"; + private readonly string MicrosoftArtifactRegistryRepoUri = "https://mcr.microsoft.com"; private readonly string PSGalleryRepoUri = "https://www.powershellgallery.com/api/v2"; private const int DefaultPriority = 50; private const bool DefaultTrusted = false; + private const bool MARDefaultTrusted = true; + private const int MARDefaultPriority = 40; private const string NameParameterSet = "NameParameterSet"; private const string PSGalleryParameterSet = "PSGalleryParameterSet"; private const string RepositoriesParameterSet = "RepositoriesParameterSet"; private Uri _uri; private CredentialProviderDynamicParameters _credentialProvider; + private const string MARParameterSet = "MARParameterSet"; #endregion @@ -62,6 +67,13 @@ class RegisterPSResourceRepository : PSCmdlet, IDynamicParameters [Parameter(Mandatory = true, ParameterSetName = PSGalleryParameterSet, HelpMessage = "PSGallery switch used to indicate registering PSGallery repository.")] public SwitchParameter PSGallery { get; set; } + /// + /// When specified, registers Microsoft Artifact Registry repository. + /// + [Parameter(Mandatory = true, ParameterSetName = MARParameterSet, HelpMessage = "Switch used to indicate registering Microsoft Artifact Registry repository.")] + [Alias("MAR")] + public SwitchParameter MicrosoftArtifactRegistry { get; set; } + /// /// Specifies a hashtable of repositories and is used to register multiple repositories at once. /// @@ -74,6 +86,7 @@ class RegisterPSResourceRepository : PSCmdlet, IDynamicParameters /// [Parameter(ParameterSetName = NameParameterSet)] [Parameter(ParameterSetName = PSGalleryParameterSet)] + [Parameter(ParameterSetName = MARParameterSet)] public SwitchParameter Trusted { get; set; } /// @@ -84,8 +97,9 @@ class RegisterPSResourceRepository : PSCmdlet, IDynamicParameters /// [Parameter(ParameterSetName = NameParameterSet)] [Parameter(ParameterSetName = PSGalleryParameterSet)] + [Parameter(ParameterSetName = MARParameterSet)] [ValidateRange(0, 100)] - public int Priority { get; set; } = DefaultPriority; + public int Priority { get; set; } /// /// Specifies the Api version of the repository to be set. @@ -122,6 +136,7 @@ public object GetDynamicParameters() // It should also not appear when using the 'Repositories' parameter set. if (ParameterSetName.Equals(PSGalleryParameterSet) || ParameterSetName.Equals(RepositoriesParameterSet) || + ParameterSetName.Equals(MARParameterSet) || PSRepositoryInfo.IsValidContainerRegistryURL(Uri)) { return null; @@ -149,6 +164,18 @@ protected override void ProcessRecord() repoApiVersion = ApiVersion; } + if (!MyInvocation.BoundParameters.ContainsKey(nameof(Priority))) + { + if (ParameterSetName.Equals(MARParameterSet, StringComparison.OrdinalIgnoreCase)) + { + Priority = MARDefaultPriority; + } + else + { + Priority = DefaultPriority; + } + } + PSRepositoryInfo.CredentialProviderType? credentialProvider = _credentialProvider?.CredentialProvider; switch (ParameterSetName) @@ -218,6 +245,25 @@ protected override void ProcessRecord() } break; + case MARParameterSet: + if (MicrosoftArtifactRegistry) + { + try + { + bool trustedValue = Trusted.IsPresent ? Trusted : MARDefaultTrusted; + items.Add(MicrosoftArtifactRegistryParameterSetHelper(Priority, trustedValue)); + } + catch (Exception e) + { + ThrowTerminatingError(new ErrorRecord( + new PSInvalidOperationException(e.Message), + "ErrorInMARParameterSet", + ErrorCategory.InvalidArgument, + this)); + } + } + break; + default: Dbg.Assert(false, "Invalid parameter set"); break; @@ -262,6 +308,34 @@ private PSRepositoryInfo PSGalleryParameterSetHelper(int repoPriority, bool repo return addedRepo; } + private PSRepositoryInfo MicrosoftArtifactRegistryParameterSetHelper(int repoPriority, bool repoTrusted) + { + WriteDebug("In RegisterPSResourceRepository::MicrosoftArtifactRegistryParameterSetHelper()"); + Uri marUri = new Uri(MicrosoftArtifactRegistryRepoUri); + WriteDebug("Internal name and uri values for Microsoft Artifact Registry are hardcoded and validated. Priority and trusted values, if passed in, also validated"); + var addedRepo = RepositorySettings.AddToRepositoryStore(MicrosoftArtifactRegistryRepoName, + marUri, + repoPriority, + repoTrusted, + apiVersion: null, + repoCredentialInfo: null, + credentialProvider: null, + Force, + this, + out string errorMsg); + + if (!string.IsNullOrEmpty(errorMsg)) + { + ThrowTerminatingError(new ErrorRecord( + new PSInvalidOperationException(errorMsg), + "RepositoryCredentialSecretManagementUnavailableModule", + ErrorCategory.ResourceUnavailable, + this)); + } + + return addedRepo; + } + private List RepositoriesParameterSetHelper() { WriteDebug("In RegisterPSResourceRepository::RepositoriesParameterSetHelper()"); @@ -323,7 +397,7 @@ private PSRepositoryInfo RepoValidationHelper(Hashtable repo) return null; } - if (repo["Name"].ToString().Equals("PSGallery")) + if (repo["Name"].ToString().Equals("PSGallery", StringComparison.OrdinalIgnoreCase)) { WriteError(new ErrorRecord( new PSInvalidOperationException("Cannot register PSGallery with -Name parameter. Try: Register-PSResourceRepository -PSGallery"), @@ -334,6 +408,17 @@ private PSRepositoryInfo RepoValidationHelper(Hashtable repo) return null; } + if (repo["Name"].ToString().Equals("MAR", StringComparison.OrdinalIgnoreCase)) + { + WriteError(new ErrorRecord( + new PSInvalidOperationException("Cannot register MAR with -Name parameter. The MAR repository is automatically registered. Try: Reset-PSResourceRepository to restore default repositories."), + "MARProvidedAsNameRepoPSet", + ErrorCategory.InvalidArgument, + this)); + + return null; + } + if (!repo.ContainsKey("Uri") || repo["Uri"] == null || String.IsNullOrEmpty(repo["Uri"].ToString())) { WriteError(new ErrorRecord( diff --git a/src/code/RepositorySettings.cs b/src/code/RepositorySettings.cs index 7cf4f9261..40893d529 100644 --- a/src/code/RepositorySettings.cs +++ b/src/code/RepositorySettings.cs @@ -26,8 +26,12 @@ internal static class RepositorySettings // The repository store file's location is currently only at '%LOCALAPPDATA%\PSResourceGet' for the user account. private const string PSGalleryRepoName = "PSGallery"; private const string PSGalleryRepoUri = "https://www.powershellgallery.com/api/v2"; + private const string MARRepoName = "MicrosoftArtifactRegistry"; + private const string MARRepoUri = "https://mcr.microsoft.com"; private const int DefaultPriority = 50; + private const int MARDefaultPriority = 40; private const bool DefaultTrusted = false; + private const bool MARDefaultTrusted = true; private const string RepositoryFileName = "PSResourceRepository.xml"; private static readonly string RepositoryPath = Path.Combine(Environment.GetFolderPath( Environment.SpecialFolder.LocalApplicationData), "PSResourceGet"); @@ -63,6 +67,10 @@ public static void CheckRepositoryStore() // Add PSGallery to the newly created store Uri psGalleryUri = new Uri(PSGalleryRepoUri); Add(PSGalleryRepoName, psGalleryUri, DefaultPriority, DefaultTrusted, repoCredentialInfo: null, repoCredentialProvider: CredentialProviderType.None, APIVersion.V2, force: false); + + // Add MAR to the newly created store + Uri marUri = new Uri(MARRepoUri); + Add(MARRepoName, marUri, MARDefaultPriority, MARDefaultTrusted, repoCredentialInfo: null, repoCredentialProvider: CredentialProviderType.None, APIVersion.ContainerRegistry, force: false); } // Open file (which should exist now), if cannot/is corrupted then throw error @@ -85,6 +93,12 @@ public static PSRepositoryInfo AddRepository(string repoName, Uri repoUri, int r return null; } + if (repoName.Equals("MicrosoftArtifactRegistry", StringComparison.OrdinalIgnoreCase)) + { + errorMsg = "Cannot register MAR with -Name parameter. Try: Register-PSResourceRepository -MicrosoftArtifactRegistry."; + return null; + } + return AddToRepositoryStore(repoName, repoUri, repoPriority, repoTrusted, apiVersion, repoCredentialInfo, repoCredentialProvider, force, cmdletPassedIn, out errorMsg); } @@ -187,6 +201,20 @@ public static PSRepositoryInfo UpdateRepositoryStore(string repoName, Uri repoUr return null; } + // check MAR Uri is not trying to be set + if (repoName.Equals("MicrosoftArtifactRegistry", StringComparison.OrdinalIgnoreCase) && repoUri != null) + { + errorMsg = "The MAR repository has a predefined Uri. Setting the -Uri parameter for this repository is not allowed. Please run 'Reset-PSResourceRepository' to restore default repositories."; + return null; + } + + // check MAR CredentialInfo is not trying to be set + if (repoName.Equals("MicrosoftArtifactRegistry", StringComparison.OrdinalIgnoreCase) && repoCredentialInfo != null) + { + errorMsg = "Setting the -CredentialInfo parameter for MAR is not allowed. Run 'Reset-PSResourceRepository' to restore default repositories."; + return null; + } + // determine trusted value to pass in (true/false if set, null otherwise, hence the nullable bool variable) bool? _trustedNullable = isSet ? new bool?(repoTrusted) : new bool?(); @@ -908,6 +936,10 @@ public static PSRepositoryInfo Reset(out string errorMsg) Uri psGalleryUri = new Uri(PSGalleryRepoUri); PSRepositoryInfo psGalleryRepo = Add(PSGalleryRepoName, psGalleryUri, DefaultPriority, DefaultTrusted, repoCredentialInfo: null, repoCredentialProvider: CredentialProviderType.None, APIVersion.V2, force: false); + // Add MAR to the newly created store + Uri marUri = new Uri(MARRepoUri); + Add(MARRepoName, marUri, MARDefaultPriority, MARDefaultTrusted, repoCredentialInfo: null, repoCredentialProvider: CredentialProviderType.None, APIVersion.ContainerRegistry, force: false); + // Clean up backup file on success if (!string.IsNullOrEmpty(backupFilePath) && File.Exists(backupFilePath)) { diff --git a/src/code/ResetPSResourceRepository.cs b/src/code/ResetPSResourceRepository.cs index 2c217aab3..bc1d4848f 100644 --- a/src/code/ResetPSResourceRepository.cs +++ b/src/code/ResetPSResourceRepository.cs @@ -10,7 +10,7 @@ namespace Microsoft.PowerShell.PSResourceGet.Cmdlets /// /// The Reset-PSResourceRepository cmdlet resets the repository store by creating a new PSRepositories.xml file. /// This is useful when the repository store becomes corrupted. - /// It will create a new repository store with only the PSGallery repository registered. + /// It will create a new repository store with PSGallery and MAR repositories registered. /// [Cmdlet(VerbsCommon.Reset, "PSResourceRepository", @@ -39,8 +39,8 @@ protected override void ProcessRecord() "PSResourceRepository.xml"); WriteVerbose($"Resetting repository store at: {repositoryStorePath}"); - - if (!ShouldProcess(repositoryStorePath, "Reset repository store and create new PSRepositories.xml file with PSGallery registered")) + + if (!ShouldProcess(repositoryStorePath, "Reset repository store and create new PSRepositories.xml file with PSGallery and MAR registered")) { return; } @@ -57,7 +57,7 @@ protected override void ProcessRecord() return; } - WriteVerbose("Repository store reset successfully. PSGallery has been registered."); + WriteVerbose("Repository store reset successfully. PSGallery and MAR have been registered."); if (PassThru) { diff --git a/src/code/SetPSResourceRepository.cs b/src/code/SetPSResourceRepository.cs index 8dc17fe21..293db33e9 100644 --- a/src/code/SetPSResourceRepository.cs +++ b/src/code/SetPSResourceRepository.cs @@ -110,10 +110,11 @@ public SwitchParameter Trusted public object GetDynamicParameters() { PSRepositoryInfo repository = RepositorySettings.Read(new[] { Name }, out string[] _).FirstOrDefault(); - // Dynamic parameter '-CredentialProvider' should not appear for PSGallery, or any container registry repository. + // Dynamic parameter '-CredentialProvider' should not appear for PSGallery, MAR, or any container registry repository. // It should also not appear when using the 'Repositories' parameter set. if (repository is not null && (repository.Name.Equals("PSGallery", StringComparison.OrdinalIgnoreCase) || + repository.Name.Equals("MicrosoftArtifactRegistry", StringComparison.OrdinalIgnoreCase) || ParameterSetName.Equals(RepositoriesParameterSet) || repository.IsContainerRegistry())) { diff --git a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 index e372720b0..c676fedcb 100644 --- a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 +++ b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 @@ -7,6 +7,7 @@ Import-Module $modPath -Force -Verbose Describe 'Test HTTP Find-PSResource for ACR Server Protocol' -tags 'CI' { BeforeAll { + $MARName = Get-MarName $testModuleName = "test-module" $testModuleWith2DigitVersion = "test-2DigitPkg" $testModuleParentName = "test_parent_mod" @@ -273,50 +274,51 @@ Describe 'Test HTTP Find-PSResource for ACR Server Protocol' -tags 'CI' { } Describe 'Test Find-PSResource for MAR Repository' -tags 'CI' { + BeforeAll { - Register-PSResourceRepository -Name "MAR" -Uri "https://mcr.microsoft.com" -ApiVersion "ContainerRegistry" + Get-NewPSResourceRepositoryFile } AfterAll { - Unregister-PSResourceRepository -Name "MAR" + Get-RevertPSResourceRepositoryFile } It "Should find resource given specific Name, Version null" { - $res = Find-PSResource -Name "Az.Accounts" -Repository "MAR" + $res = Find-PSResource -Name "Az.Accounts" -Repository $MarName $res.Name | Should -Be "Az.Accounts" $res.Version | Should -BeGreaterThan ([Version]"4.0.0") } It "Should find resource and its dependency given specific Name and Version" { - $res = Find-PSResource -Name "Az.Storage" -Version "8.0.0" -Repository "MAR" + $res = Find-PSResource -Name "Az.Storage" -Version "8.0.0" -Repository $MarName $res.Dependencies.Length | Should -Be 1 $res.Dependencies[0].Name | Should -Be "Az.Accounts" } It "Should find Azpreview resource and it's dependency given specific Name and Version" { - $res = Find-PSResource -Name "Azpreview" -Version "13.2.0" -Repository "MAR" + $res = Find-PSResource -Name "Azpreview" -Version "13.2.0" -Repository $MarName $res.Dependencies.Length | Should -Not -Be 0 } It "Should find resource with wildcard in Name" { - $res = Find-PSResource -Name "Az.App*" -Repository "MAR" + $res = Find-PSResource -Name "Az.App*" -Repository $MarName $res | Should -Not -BeNullOrEmpty $res.Count | Should -BeGreaterThan 1 } It "Should find all resource with wildcard in Name" { - $res = Find-PSResource -Name "*" -Repository "MAR" + $res = Find-PSResource -Name "*" -Repository $MarName $res | Should -Not -BeNullOrEmpty $res.Count | Should -BeGreaterThan 1 } It "Should find version range for Az dependencies" { # Target known version to know the output from the API won't change - $res = Find-PSResource -Repository 'MAR' -Name 'Az' -Version '14.4.0' - + $res = Find-PSResource -Repository $MarName -Name 'Az' -Version '14.4.0' + # Version defined by "ModuleVersion" $res.Dependencies.Where{$_.'Name' -eq 'Az.Accounts'}.'VersionRange'.ToString() | Should -Be '[5.3.0, )' - + # Version defined by "RequiredVersion" $res.Dependencies.Where{$_.'Name' -eq 'Az.Resources'}.'VersionRange'.ToString() | Should -Be '[8.1.0, 8.1.0]' } diff --git a/test/InstallPSResourceTests/InstallPSResourceContainerRegistryServer.Tests.ps1 b/test/InstallPSResourceTests/InstallPSResourceContainerRegistryServer.Tests.ps1 index afe81d2c3..0c66038b7 100644 --- a/test/InstallPSResourceTests/InstallPSResourceContainerRegistryServer.Tests.ps1 +++ b/test/InstallPSResourceTests/InstallPSResourceContainerRegistryServer.Tests.ps1 @@ -351,25 +351,24 @@ Describe 'Test Install-PSResource for Container Registry scenarios - Manual Vali } Describe 'Test Install-PSResource for MAR Repository' -tags 'CI' { + BeforeAll { - [Microsoft.PowerShell.PSResourceGet.UtilClasses.InternalHooks]::SetTestHook("MARPrefix", "azure-powershell/"); - Register-PSResourceRepository -Name "MAR" -Uri "https://mcr.microsoft.com" -ApiVersion "ContainerRegistry" + Get-NewPSResourceRepositoryFile } - AfterAll { - [Microsoft.PowerShell.PSResourceGet.UtilClasses.InternalHooks]::SetTestHook("MARPrefix", $null); - Unregister-PSResourceRepository -Name "MAR" + Get-RevertPSResourceRepositoryFile } - It "Should find resource given specific Name, Version null" { + It "Should install resource given specific Name, Version null" { try { - $pkg = Install-PSResource -Name "Az.Accounts" -Repository "MAR" -PassThru -TrustRepository -Reinstall + $pkg = Install-PSResource -Name "Az.Accounts" -Repository 'MicrosoftArtifactRegistry' -PassThru -TrustRepository -Reinstall $pkg.Name | Should -Be "Az.Accounts" - $pkg.Version | Should -Be "3.0.4" + $pkg.Version.Major | Should -BeGreaterOrEqual 5 + } finally { if ($pkg) { - Uninstall-PSResource -Name "Az.Accounts" -Version "3.0.4" + Uninstall-PSResource -Name "Az.Accounts" -Version $pkg.Version } } } diff --git a/test/InstallPSResourceTests/InstallPSResourceRepositorySearching.Tests.ps1 b/test/InstallPSResourceTests/InstallPSResourceRepositorySearching.Tests.ps1 index eea4dae94..86b981a7c 100644 --- a/test/InstallPSResourceTests/InstallPSResourceRepositorySearching.Tests.ps1 +++ b/test/InstallPSResourceTests/InstallPSResourceRepositorySearching.Tests.ps1 @@ -36,8 +36,9 @@ Describe 'Test Install-PSResource for searching and looping through repositories It "install resources from highest priority repository where it exists (without -Repository specified)" { # Package "test_module" exists in the following repositories (in this order): localRepo, PSGallery, NuGetGallery + # Package does not exist on MAR to we need to skip when validating that the highest priority repository is used $res = Install-PSResource -Name $testModuleName -TrustRepository -SkipDependencyCheck -ErrorVariable err -ErrorAction SilentlyContinue -PassThru - $err | Should -HaveCount 0 + $err | Should -HaveCount 1 ## This comes from MAR $res | Should -Not -BeNullOrEmpty $res.Repository | Should -Be $localRepoName @@ -45,8 +46,9 @@ Describe 'Test Install-PSResource for searching and looping through repositories It "install resources from hightest priority repository where it exists and not write errors for repositories where it does not exist (without -Repository specified)" { # Package "test_script" exists in the following repositories: PSGallery, NuGetGallery + # Package does not exist on MAR to we need to skip when validating that the highest priority repository is used Install-PSResource -Name $testScriptName -TrustRepository -SkipDependencyCheck -ErrorVariable err -ErrorAction SilentlyContinue - $err | Should -HaveCount 0 + $err | Should -HaveCount 1 ## This comes from MAR $res = Get-InstalledPSResource $testScriptName $res | Should -Not -BeNullOrEmpty @@ -55,7 +57,7 @@ Describe 'Test Install-PSResource for searching and looping through repositories It "should install resources that exist and not install ones that do not exist while reporting error (without -Repository specified)" { Install-PSResource -Name $testScriptName,"NonExistentModule" -TrustRepository -SkipDependencyCheck -ErrorVariable err -ErrorAction SilentlyContinue - $err[0].FullyQualifiedErrorId | Should -BeExactly "InstallPackageFailure,Microsoft.PowerShell.PSResourceGet.Cmdlets.InstallPSResource" + "InstallPackageFailure,Microsoft.PowerShell.PSResourceGet.Cmdlets.InstallPSResource" | Should -BeIn $err.FullyQualifiedErrorId $res = Get-InstalledPSResource $testScriptName $res | Should -Not -BeNullOrEmpty @@ -64,13 +66,13 @@ Describe 'Test Install-PSResource for searching and looping through repositories It "should not install resource given nonexistent Name (without -Repository specified)" { Install-PSResource -Name "NonExistentModule" -TrustRepository -SkipDependencyCheck -ErrorVariable err -ErrorAction SilentlyContinue - $err | Should -HaveCount 1 - $err[0].FullyQualifiedErrorId | Should -BeExactly "InstallPackageFailure,Microsoft.PowerShell.PSResourceGet.Cmdlets.InstallPSResource" + $err | Should -HaveCount 2 ## One error comes from MAR + "InstallPackageFailure,Microsoft.PowerShell.PSResourceGet.Cmdlets.InstallPSResource" | Should -BeIn $err.FullyQualifiedErrorId } It "install multiple resources from highest priority repository where it exists (without -Repository specified)" { $res = Install-PSResource -Name "test_module","test_module2" -TrustRepository -SkipDependencyCheck -ErrorVariable err -ErrorAction SilentlyContinue -PassThru - $err | Should -HaveCount 0 + $err | Should -HaveCount 2 ## This comes from MAR $res | Should -Not -BeNullOrEmpty $pkg1 = $res[0] diff --git a/test/PSGetTestUtils.psm1 b/test/PSGetTestUtils.psm1 index 6a384c17c..44202a58d 100644 --- a/test/PSGetTestUtils.psm1 +++ b/test/PSGetTestUtils.psm1 @@ -19,6 +19,9 @@ $script:IsCoreCLR = $PSVersionTable.ContainsKey('PSEdition') -and $PSVersionTabl $script:PSGalleryName = 'PSGallery' $script:PSGalleryLocation = 'https://www.powershellgallery.com/api/v2' +$script:MARName = 'MicrosoftArtifactRegistry' +$script:MARLocation = 'https://mcr.microsoft.com/' + $script:NuGetGalleryName = 'NuGetGallery' $script:NuGetGalleryLocation = 'https://api.nuget.org/v3/index.json' @@ -153,6 +156,16 @@ function Get-PSGalleryName function Get-PSGalleryLocation { return $script:PSGalleryLocation } + +function Get-MARName +{ + return $script:MARName +} + +function Get-MARLocation { + return $script:MARLocation +} + function Get-NewTestDirs { Param( [string[]] diff --git a/test/PublishPSResourceTests/PublishPSResource.Tests.ps1 b/test/PublishPSResourceTests/PublishPSResource.Tests.ps1 index ed0adf1b8..bb58ddeed 100644 --- a/test/PublishPSResourceTests/PublishPSResource.Tests.ps1 +++ b/test/PublishPSResourceTests/PublishPSResource.Tests.ps1 @@ -101,6 +101,7 @@ Describe "Test Publish-PSResource" -tags 'CI' { CreateTestModule -Path $TestDrive -ModuleName 'ModuleWithMissingRequiredModule' $script:PSGalleryName = 'PSGallery' + $script:MARName = 'MicrosoftArtifactRegistry' } AfterAll { Get-RevertPSResourceRepositoryFile @@ -802,8 +803,9 @@ Describe "Test Publish-PSResource with Module Prefix" -tags 'CI' { $err[0].FullyQualifiedErrorId | Should -Be "RepositoryNotFound,Microsoft.PowerShell.PSResourceGet.Cmdlets.PublishPSResource" $registeredRepos = Get-PSResourceRepository - $registeredRepos.Count | Should -Be 1 - $registeredRepos[0].Name | Should -Be $script:PSGalleryName + $registeredRepos.Count | Should -Be 2 + $registeredRepos[0].Name | Should -Be $script:MARName + $registeredRepos[1].Name | Should -Be $script:PSGalleryName } finally { # Cleanup diff --git a/test/PublishPSResourceTests/PublishPSResourceContainerRegistryServer.Tests.ps1 b/test/PublishPSResourceTests/PublishPSResourceContainerRegistryServer.Tests.ps1 index bd59d69d0..abde6338e 100644 --- a/test/PublishPSResourceTests/PublishPSResourceContainerRegistryServer.Tests.ps1 +++ b/test/PublishPSResourceTests/PublishPSResourceContainerRegistryServer.Tests.ps1 @@ -590,14 +590,12 @@ Describe "Test Publish-PSResource" -tags 'CI' { } Describe 'Test Publish-PSResource for MAR Repository' -tags 'CI' { + BeforeAll { - [Microsoft.PowerShell.PSResourceGet.UtilClasses.InternalHooks]::SetTestHook("MARPrefix", "azure-powershell/"); - Register-PSResourceRepository -Name "MAR" -Uri "https://mcr.microsoft.com" -ApiVersion "ContainerRegistry" + Get-NewPSResourceRepositoryFile } - AfterAll { - [Microsoft.PowerShell.PSResourceGet.UtilClasses.InternalHooks]::SetTestHook("MARPrefix", $null); - Unregister-PSResourceRepository -Name "MAR" + Get-RevertPSResourceRepositoryFile } It "Should find resource given specific Name, Version null" { @@ -606,6 +604,6 @@ Describe 'Test Publish-PSResource for MAR Repository' -tags 'CI' { $psd1Path = Join-Path -Path $modulePath -ChildPath $fileName New-ModuleManifest -Path $psd1Path -ModuleVersion "1.0.0" -Description "NonExistent module" - { Publish-PSResource -Path $modulePath -Repository "MAR" -ErrorAction Stop } | Should -Throw -ErrorId "MARRepositoryPublishError,Microsoft.PowerShell.PSResourceGet.Cmdlets.PublishPSResource" + { Publish-PSResource -Path $modulePath -Repository "MicrosoftArtifactRegistry" -ErrorAction Stop } | Should -Throw -ErrorId "MARRepositoryPublishError,Microsoft.PowerShell.PSResourceGet.Cmdlets.PublishPSResource" } } diff --git a/test/ResourceRepositoryTests/MARRepository.Tests.ps1 b/test/ResourceRepositoryTests/MARRepository.Tests.ps1 new file mode 100644 index 000000000..06f49931c --- /dev/null +++ b/test/ResourceRepositoryTests/MARRepository.Tests.ps1 @@ -0,0 +1,142 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +$modPath = "$psscriptroot/../PSGetTestUtils.psm1" +Write-Verbose -Verbose -Message "PSGetTestUtils path: $modPath" +Import-Module $modPath -Force -Verbose + +Describe "Test MAR Repository Registration" -tags 'CI' { + BeforeEach { + $MARName = Get-MARName + $MARUri = Get-MARLocation + $PSGalleryName = Get-PSGalleryName + $PSGalleryUri = Get-PSGalleryLocation + Get-NewPSResourceRepositoryFile + } + AfterEach { + Get-RevertPSResourceRepositoryFile + } + + Context "MAR is auto-registered with expected values" { + It "MAR repository should be present with expected default values" { + $res = Get-PSResourceRepository -Name $MARName + $res | Should -Not -BeNullOrEmpty + $res.Name | Should -Be $MARName + $res.Uri | Should -Be "$MARUri" + $res.Trusted | Should -Be True + $res.Priority | Should -Be 40 + $res.ApiVersion | Should -Be 'ContainerRegistry' + } + + It "MAR repository should have lower priority number (higher priority) than PSGallery" { + $mar = Get-PSResourceRepository -Name $MARName + $psGallery = Get-PSResourceRepository -Name $PSGalleryName + $mar.Priority | Should -BeLessThan $psGallery.Priority + } + } + + Context "MAR name protection" { + It "should not allow registering MAR with -Name parameter" { + { Register-PSResourceRepository -Name $MARName -Uri "https://mcr.microsoft.com" -ErrorAction Stop } | Should -Throw -ErrorId "ErrorInNameParameterSet,Microsoft.PowerShell.PSResourceGet.Cmdlets.RegisterPSResourceRepository" + } + + It "should not allow registering MAR (case insensitive) with -Name parameter" { + { Register-PSResourceRepository -Name $MARName -Uri "https://mcr.microsoft.com" -ErrorAction Stop } | Should -Throw -ErrorId "ErrorInNameParameterSet,Microsoft.PowerShell.PSResourceGet.Cmdlets.RegisterPSResourceRepository" + } + + It "should not allow registering MAR with -Name parameter in hashtable" { + Unregister-PSResourceRepository -Name $MARName + $hashtable = @{Name = "MAR"; Uri = "https://mcr.microsoft.com"} + Register-PSResourceRepository -Repository $hashtable -ErrorVariable err -ErrorAction SilentlyContinue + $err.Count | Should -BeGreaterThan 0 + $err[0].FullyQualifiedErrorId | Should -BeExactly "MARProvidedAsNameRepoPSet,Microsoft.PowerShell.PSResourceGet.Cmdlets.RegisterPSResourceRepository" + } + + It "should not allow setting Uri for MAR repository" { + { Set-PSResourceRepository -Name $MARName -Uri "https://example.com" -ErrorAction Stop } | Should -Throw -ErrorId "ErrorInNameParameterSet,Microsoft.PowerShell.PSResourceGet.Cmdlets.SetPSResourceRepository" + } + + It "should not allow setting CredentialInfo for MAR repository" { + $randomSecret = [System.IO.Path]::GetRandomFileName() + $credentialInfo = New-Object Microsoft.PowerShell.PSResourceGet.UtilClasses.PSCredentialInfo ("testvault", $randomSecret) + { Set-PSResourceRepository -Name $MARName -CredentialInfo $credentialInfo -ErrorAction Stop } | Should -Throw -ErrorId "ErrorInNameParameterSet,Microsoft.PowerShell.PSResourceGet.Cmdlets.SetPSResourceRepository" + } + + It "should allow setting Trusted for MAR repository" { + Set-PSResourceRepository -Name $MARName -Trusted:$false + $res = Get-PSResourceRepository -Name $MARName + $res.Trusted | Should -Be False + } + + It "should allow setting Priority for MAR repository" { + Set-PSResourceRepository -Name $MARName -Priority 10 + $res = Get-PSResourceRepository -Name $MARName + $res.Priority | Should -Be 10 + } + } + + Context "Reset repository store includes MAR" { + It "Reset-PSResourceRepository should register MAR alongside PSGallery" { + Reset-PSResourceRepository -Confirm:$false + $res = Get-PSResourceRepository -Name $MARName + $res | Should -Not -BeNullOrEmpty + $res.Name | Should -Be $MARName + $res.Uri | Should -Be "$MARUri" + $res.Trusted | Should -Be True + $res.Priority | Should -Be 40 + $res.ApiVersion | Should -Be 'ContainerRegistry' + + $psGallery = Get-PSResourceRepository -Name $PSGalleryName + $psGallery | Should -Not -BeNullOrEmpty + } + + It "Reset-PSResourceRepository should restore MAR after unregistration" { + Unregister-PSResourceRepository -Name $MARName + $res = Get-PSResourceRepository -Name $MARName -ErrorAction SilentlyContinue + $res | Should -BeNullOrEmpty + + Reset-PSResourceRepository -Confirm:$false + $res = Get-PSResourceRepository -Name $MARName + $res | Should -Not -BeNullOrEmpty + $res.Name | Should -Be $MARName + $res.Uri | Should -Be "$MARUri" + $res.Trusted | Should -Be True + $res.Priority | Should -Be 40 + } + + It "Reset-PSResourceRepository should restore both PSGallery and MAR" { + Unregister-PSResourceRepository -Name $MARName + Unregister-PSResourceRepository -Name $PSGalleryName + Reset-PSResourceRepository -Confirm:$false + + $mar = Get-PSResourceRepository -Name $MARName + $mar | Should -Not -BeNullOrEmpty + $mar.Priority | Should -Be 40 + $mar.Trusted | Should -Be True + $mar.ApiVersion | Should -Be 'ContainerRegistry' + + $psGallery = Get-PSResourceRepository -Name $PSGalleryName + $psGallery | Should -Not -BeNullOrEmpty + $psGallery.Priority | Should -Be 50 + } + } + + Context "MAR first due to higher priority" { + It "Find-PSResource Az.Accounts module from MAR" { + $res = Find-PSResource -Name "Az.Accounts" + $res | Should -Not -BeNullOrEmpty + $res.Count | Should -BeGreaterThan 0 + $firstRes = $res | Select-Object -First 1 + $firstRes.Name | Should -Be "Az.Accounts" + $firstRes.Repository | Should -Be $MARName + } + + It 'Find-PSResource fallback to PSGallery if module not in MAR' { + $res = Find-PSResource -Name "Pscx" + $res | Should -Not -BeNullOrEmpty + $firstRes = $res | Select-Object -First 1 + $firstRes.Name | Should -Be "Pscx" + $firstRes.Repository | Should -Be $PSGalleryName + } + } +} diff --git a/test/ResourceRepositoryTests/RegisterPSResourceRepository.Tests.ps1 b/test/ResourceRepositoryTests/RegisterPSResourceRepository.Tests.ps1 index f9c350356..332921c31 100644 --- a/test/ResourceRepositoryTests/RegisterPSResourceRepository.Tests.ps1 +++ b/test/ResourceRepositoryTests/RegisterPSResourceRepository.Tests.ps1 @@ -7,6 +7,8 @@ Import-Module $modPath -Force -Verbose Describe "Test Register-PSResourceRepository" -tags 'CI' { BeforeEach { + $MARName = Get-MARName + $MARUri = Get-MARLocation $PSGalleryName = Get-PSGalleryName $PSGalleryUri = Get-PSGalleryLocation $TestRepoName1 = "testRepository" @@ -85,6 +87,15 @@ Describe "Test Register-PSResourceRepository" -tags 'CI' { $res.Priority | Should -Be 50 } + It "register repository with MicrosoftArtifactRegistry parameter (MicrosoftArtifactRegistryParameterSet)" { + Unregister-PSResourceRepository -Name $MARName + $res = Register-PSResourceRepository -MicrosoftArtifactRegistry -PassThru + $res.Name | Should -Be $MARName + $res.Uri | Should -Be $MARUri + $res.Trusted | Should -Be True + $res.Priority | Should -Be 40 + } + It "register repository with PSGallery switch parameter value of false (PSGalleryParameterSet)" { Unregister-PSResourceRepository -Name $PSGalleryName $res = Register-PSResourceRepository -PSGallery:$false -PassThru @@ -412,7 +423,7 @@ Describe "Test Register-PSResourceRepository" -tags 'CI' { It "should throw error when trying to register repository with ApiVersion unknown" { {Register-PSResourceRepository -Name $TestRepoName1 -Uri $tmpDir1Path -ApiVersion "unknown" -ErrorAction Stop} | Should -Throw -ErrorId "ParameterArgumentValidationError,Microsoft.PowerShell.PSResourceGet.Cmdlets.RegisterPSResourceRepository" - + # Verify the repository was not created $repo = Get-PSResourceRepository $TestRepoName1 -ErrorAction SilentlyContinue $repo | Should -BeNullOrEmpty diff --git a/test/ResourceRepositoryTests/ResetPSResourceRepository.Tests.ps1 b/test/ResourceRepositoryTests/ResetPSResourceRepository.Tests.ps1 index f49570229..f658fac6b 100644 --- a/test/ResourceRepositoryTests/ResetPSResourceRepository.Tests.ps1 +++ b/test/ResourceRepositoryTests/ResetPSResourceRepository.Tests.ps1 @@ -7,6 +7,8 @@ Import-Module $modPath -Force -Verbose Describe "Test Reset-PSResourceRepository" -tags 'CI' { BeforeEach { + $MARName = Get-MARName + $MARUri = Get-MARLocation $PSGalleryName = Get-PSGalleryName $PSGalleryUri = Get-PSGalleryLocation Get-NewPSResourceRepositoryFile @@ -22,19 +24,19 @@ Describe "Test Reset-PSResourceRepository" -tags 'CI' { $tmpDirPath = Join-Path -Path $TestDrive -ChildPath "tmpDir1" New-Item -ItemType Directory -Path $tmpDirPath -Force | Out-Null Register-PSResourceRepository -Name $TestRepoName -Uri $tmpDirPath - + # Verify repository was added $repos = Get-PSResourceRepository $repos.Count | Should -BeGreaterThan 1 - + # Act: Reset repository store Reset-PSResourceRepository -Confirm:$false - + # Assert: Only PSGallery should exist $repos = Get-PSResourceRepository - $repos.Count | Should -Be 1 - $repos.Name | Should -Be $PSGalleryName - $repos.Uri | Should -Be $PSGalleryUri + $repos.Count | Should -Be 2 + $repos.Name | ForEach-Object { $_ | Should -BeIn @($PSGalleryName, $MARName) } + $repos.Uri | ForEach-Object { $_ | Should -BeIn @($PSGalleryUri, $MARUri) } } It "Reset repository store with PassThru parameter returns PSGallery" { @@ -43,20 +45,22 @@ Describe "Test Reset-PSResourceRepository" -tags 'CI' { $tmpDirPath = Join-Path -Path $TestDrive -ChildPath "tmpDir1" New-Item -ItemType Directory -Path $tmpDirPath -Force | Out-Null Register-PSResourceRepository -Name $TestRepoName -Uri $tmpDirPath - + # Act: Reset repository store with PassThru $result = Reset-PSResourceRepository -Confirm:$false -PassThru - + # Assert: Result should be PSGallery repository info $result | Should -Not -BeNullOrEmpty $result.Name | Should -Be $PSGalleryName $result.Uri | Should -Be $PSGalleryUri $result.Trusted | Should -Be $false $result.Priority | Should -Be 50 - + # Verify only PSGallery exists $repos = Get-PSResourceRepository - $repos.Count | Should -Be 1 + $repos.Count | Should -Be 2 + $repos.Name | ForEach-Object { $_ | Should -BeIn @($PSGalleryName, $MARName) } + $repos.Uri | ForEach-Object { $_ | Should -BeIn @($PSGalleryUri, $MARUri) } } It "Reset repository store should support -WhatIf" { @@ -65,14 +69,14 @@ Describe "Test Reset-PSResourceRepository" -tags 'CI' { $tmpDirPath = Join-Path -Path $TestDrive -ChildPath "tmpDir1" New-Item -ItemType Directory -Path $tmpDirPath -Force | Out-Null Register-PSResourceRepository -Name $TestRepoName -Uri $tmpDirPath - + # Capture repository count before WhatIf $reposBefore = Get-PSResourceRepository $countBefore = $reposBefore.Count - + # Act: Run with WhatIf Reset-PSResourceRepository -WhatIf - + # Assert: Repositories should not have changed $reposAfter = Get-PSResourceRepository $reposAfter.Count | Should -Be $countBefore @@ -82,43 +86,43 @@ Describe "Test Reset-PSResourceRepository" -tags 'CI' { # Arrange: Corrupt the repository file $powerShellGetPath = Join-Path -Path ([Environment]::GetFolderPath([System.Environment+SpecialFolder]::LocalApplicationData)) -ChildPath "PSResourceGet" $repoFilePath = Join-Path -Path $powerShellGetPath -ChildPath "PSResourceRepository.xml" - + # Write invalid XML to corrupt the file "This is not valid XML" | Set-Content -Path $repoFilePath -Force - + # Act: Reset the repository store $result = Reset-PSResourceRepository -Confirm:$false -PassThru - + # Assert: Should successfully reset and return PSGallery $result | Should -Not -BeNullOrEmpty $result.Name | Should -Be $PSGalleryName - + # Verify we can now read repositories $repos = Get-PSResourceRepository - $repos.Count | Should -Be 1 - $repos.Name | Should -Be $PSGalleryName + $repos.Count | Should -Be 2 + $repos.Name | ForEach-Object { $_ | Should -BeIn @($PSGalleryName, $MARName) } } It "Reset repository store when file doesn't exist should succeed" { # Arrange: Delete the repository file $powerShellGetPath = Join-Path -Path ([Environment]::GetFolderPath([System.Environment+SpecialFolder]::LocalApplicationData)) -ChildPath "PSResourceGet" $repoFilePath = Join-Path -Path $powerShellGetPath -ChildPath "PSResourceRepository.xml" - + if (Test-Path -Path $repoFilePath) { Remove-Item -Path $repoFilePath -Force } - + # Act: Reset the repository store $result = Reset-PSResourceRepository -Confirm:$false -PassThru - + # Assert: Should successfully reset and return PSGallery $result | Should -Not -BeNullOrEmpty $result.Name | Should -Be $PSGalleryName - + # Verify PSGallery is registered $repos = Get-PSResourceRepository - $repos.Count | Should -Be 1 - $repos.Name | Should -Be $PSGalleryName + $repos.Count | Should -Be 2 + $repos.Name | ForEach-Object { $_ | Should -BeIn @($PSGalleryName, $MARName) } } It "Reset repository store with multiple repositories should only keep PSGallery" { @@ -129,21 +133,21 @@ Describe "Test Reset-PSResourceRepository" -tags 'CI' { New-Item -ItemType Directory -Path $tmpDir1Path -Force | Out-Null New-Item -ItemType Directory -Path $tmpDir2Path -Force | Out-Null New-Item -ItemType Directory -Path $tmpDir3Path -Force | Out-Null - + Register-PSResourceRepository -Name "testRepo1" -Uri $tmpDir1Path Register-PSResourceRepository -Name "testRepo2" -Uri $tmpDir2Path Register-PSResourceRepository -Name "testRepo3" -Uri $tmpDir3Path - + # Verify multiple repositories exist $reposBefore = Get-PSResourceRepository $reposBefore.Count | Should -BeGreaterThan 1 - + # Act: Reset repository store Reset-PSResourceRepository -Confirm:$false - + # Assert: Only PSGallery should remain $reposAfter = Get-PSResourceRepository - $reposAfter.Count | Should -Be 1 - $reposAfter.Name | Should -Be $PSGalleryName + $reposAfter.Count | Should -Be 2 + $reposAfter.Name | ForEach-Object { $_ | Should -BeIn @($PSGalleryName, $MARName) } } } diff --git a/test/SavePSResourceTests/SavePSResourceLocal.Tests.ps1 b/test/SavePSResourceTests/SavePSResourceLocal.Tests.ps1 index b25a6c53b..3b22f8f9e 100644 --- a/test/SavePSResourceTests/SavePSResourceLocal.Tests.ps1 +++ b/test/SavePSResourceTests/SavePSResourceLocal.Tests.ps1 @@ -157,8 +157,8 @@ Describe 'Test Save-PSResource for local repositories' -tags 'CI' { } It "Save module, should search through all repositories and only install from the first repo containing the package" { - Save-PSResource -Name $moduleName3 -Version "0.0.93" -Path $SaveDir -TrustRepository -ErrorVariable ev - $ev | Should -BeNullOrEmpty + Save-PSResource -Name $moduleName3 -Version "0.0.93" -Path $SaveDir -TrustRepository -ErrorVariable ev -ErrorAction SilentlyContinue + $ev | Should -HaveCount 1 ## This comes from MAR not having the module so the error is thrown from that repository before falling back to the next repository $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq "$moduleName3" $pkgDir | Should -Not -BeNullOrEmpty } diff --git a/test/testRepositories.xml b/test/testRepositories.xml index 1e330dda9..07d04d8e4 100644 --- a/test/testRepositories.xml +++ b/test/testRepositories.xml @@ -1,5 +1,6 @@ +