From 3b491fee9712cb478d016a649b5bd90e573abeac Mon Sep 17 00:00:00 2001 From: Afroz Mohammed Date: Fri, 24 Jan 2025 13:27:23 -0800 Subject: [PATCH 01/11] Fix #1777: Fixes bug with nuspec dependency version range when versions are specified in RequiredModules section Fixes bug with generated nuspec dependency version range when RequiredVersion,MaxiumumVersion and ModuleVersion are specified in RequiredModules section --- src/code/PublishHelper.cs | 58 ++++-- .../CompressPSResource.Tests.ps1 | 178 ++++++++++++++++++ 2 files changed, 224 insertions(+), 12 deletions(-) diff --git a/src/code/PublishHelper.cs b/src/code/PublishHelper.cs index 66daea84e..4cbfb0f4a 100644 --- a/src/code/PublishHelper.cs +++ b/src/code/PublishHelper.cs @@ -1122,20 +1122,47 @@ private string CreateNuspec( if (requiredModules != null) { XmlElement dependenciesElement = doc.CreateElement("dependencies", nameSpaceUri); - foreach (string dependencyName in requiredModules.Keys) { XmlElement element = doc.CreateElement("dependency", nameSpaceUri); - element.SetAttribute("id", dependencyName); + string dependencyVersion = requiredModules[dependencyName].ToString(); if (!string.IsNullOrEmpty(dependencyVersion)) { - element.SetAttribute("version", requiredModules[dependencyName].ToString()); + var requiredModulesVersionInfo = (Hashtable)requiredModules[dependencyName]; + string versionRange = String.Empty; + if (requiredModulesVersionInfo.ContainsKey("RequiredVersion")) + { + // For RequiredVersion, use exact version notation [x.x.x] + string requiredModulesVersion = requiredModulesVersionInfo["RequiredVersion"].ToString(); + versionRange = $"[{requiredModulesVersion}]"; + } + else if (requiredModulesVersionInfo.ContainsKey("ModuleVersion") && requiredModulesVersionInfo.ContainsKey("MaximumVersion")) + { + // Version range when both min and max specified: [min,max] + versionRange = $"[{requiredModulesVersionInfo["ModuleVersion"]}, {requiredModulesVersionInfo["MaximumVersion"]}]"; + } + else if (requiredModulesVersionInfo.ContainsKey("ModuleVersion")) + { + // Only min specified: min (which means ≥ min) + versionRange = requiredModulesVersionInfo["ModuleVersion"].ToString(); + } + else if (requiredModulesVersionInfo.ContainsKey("MaximumVersion")) + { + // Only max specified: (, max] + versionRange = $"(, {requiredModulesVersionInfo["MaximumVersion"]}]"; + } + + if (!string.IsNullOrEmpty(versionRange)) + { + element.SetAttribute("version", versionRange); + } } dependenciesElement.AppendChild(element); } + metadataElement.AppendChild(dependenciesElement); } @@ -1173,19 +1200,26 @@ private Hashtable ParseRequiredModules(Hashtable parsedMetadataHash) if (LanguagePrimitives.TryConvertTo(reqModule, out Hashtable moduleHash)) { string moduleName = moduleHash["ModuleName"] as string; - - if (moduleHash.ContainsKey("ModuleVersion")) + var versionInfo = new Hashtable(); + + // RequiredVersion cannot be used with ModuleVersion or MaximumVersion + if (moduleHash.ContainsKey("RequiredVersion")) { - dependenciesHash.Add(moduleName, moduleHash["ModuleVersion"]); + versionInfo["RequiredVersion"] = moduleHash["RequiredVersion"].ToString(); } - else if (moduleHash.ContainsKey("RequiredVersion")) + else { - dependenciesHash.Add(moduleName, moduleHash["RequiredVersion"]); - } - else - { - dependenciesHash.Add(moduleName, string.Empty); + // ModuleVersion and MaximumVersion can be used together + if (moduleHash.ContainsKey("ModuleVersion")) + { + versionInfo["ModuleVersion"] = moduleHash["ModuleVersion"].ToString(); + } + if (moduleHash.ContainsKey("MaximumVersion")) + { + versionInfo["MaximumVersion"] = moduleHash["MaximumVersion"].ToString(); + } } + dependenciesHash.Add(moduleName, versionInfo); } else if (LanguagePrimitives.TryConvertTo(reqModule, out string moduleName)) { diff --git a/test/PublishPSResourceTests/CompressPSResource.Tests.ps1 b/test/PublishPSResourceTests/CompressPSResource.Tests.ps1 index 7d9fe9770..28f74a742 100644 --- a/test/PublishPSResourceTests/CompressPSResource.Tests.ps1 +++ b/test/PublishPSResourceTests/CompressPSResource.Tests.ps1 @@ -42,6 +42,39 @@ function CreateTestModule '@ | Out-File -FilePath $moduleSrc } +function CompressExpandRetrieveNuspec +{ + param( + [string]$PublishModuleBase, + [string]$PublishModuleName, + [string]$ModuleVersion, + [string]$RepositoryPath, + [string]$ModuleBasePath, + [string]$TestDrive, + [object[]]$RequiredModules, + [switch]$SkipModuleManifestValidate + ) + + $testFile = Join-Path -Path "TestSubDirectory" -ChildPath "TestSubDirFile.ps1" + $null = New-ModuleManifest -Path (Join-Path -Path $PublishModuleBase -ChildPath "$PublishModuleName.psd1") -ModuleVersion $version -Description "$PublishModuleName module" -RequiredModules $RequiredModules + $null = New-Item -Path (Join-Path -Path $PublishModuleBase -ChildPath $testFile) -Force + + $null = Compress-PSResource -Path $PublishModuleBase -DestinationPath $repositoryPath -SkipModuleManifestValidate:$SkipModuleManifestValidate + + # Must change .nupkg to .zip so that Expand-Archive can work on Windows PowerShell + $nupkgPath = Join-Path -Path $RepositoryPath -ChildPath "$PublishModuleName.$version.nupkg" + $zipPath = Join-Path -Path $RepositoryPath -ChildPath "$PublishModuleName.$version.zip" + Rename-Item -Path $nupkgPath -NewName $zipPath + $unzippedPath = Join-Path -Path $TestDrive -ChildPath "$PublishModuleName" + $null = New-Item $unzippedPath -Itemtype directory -Force + $null = Expand-Archive -Path $zipPath -DestinationPath $unzippedPath + + $nuspecPath = Join-Path -Path $unzippedPath -ChildPath "$PublishModuleName.nuspec" + $nuspecxml = [xml](Get-Content $nuspecPath) + $null = Remove-Item $unzippedPath -Force -Recurse + return $nuspecxml +} + Describe "Test Compress-PSResource" -tags 'CI' { BeforeAll { Get-NewPSResourceRepositoryFile @@ -152,6 +185,7 @@ Describe "Test Compress-PSResource" -tags 'CI' { Expand-Archive -Path $zipPath -DestinationPath $unzippedPath Test-Path -Path (Join-Path -Path $unzippedPath -ChildPath $testFile) | Should -Be $True + $null = Remove-Item $unzippedPath -Force -Recurse } It "Compresses a script" { @@ -218,6 +252,150 @@ Describe "Test Compress-PSResource" -tags 'CI' { $fileInfoObject.Name | Should -Be "$script:PublishModuleName.$version.nupkg" } + It "Compress-PSResource creates nuspec dependecy version range when RequiredVersion is in RequiredModules section" { + $version = "1.0.0" + $requiredModules = @( + @{ + 'ModuleName' = 'PSGetTestRequiredModule' + 'GUID' = (New-Guid).Guid + 'RequiredVersion' = '2.0.0' + } + ) + $compressParams = @{ + 'PublishModuleBase' = $script:PublishModuleBase + 'PublishModuleName' = $script:PublishModuleName + 'ModuleVersion' = $version + 'RepositoryPath' = $script:repositoryPath + 'TestDrive' = $TestDrive + 'RequiredModules' = $requiredModules + 'SkipModuleManifestValidate' = $true + } + $nuspecxml = CompressExpandRetrieveNuspec @compressParams + # removing spaces as the nuget packaging is formatting the version range and adding spaces even when the original nuspec file doesn't have spaces. + # e.g (,2.0.0] is being formatted to (, 2.0.0] + $nuspecxml.package.metadata.dependencies.dependency.version.replace(' ', '') | Should -BeExactly '[2.0.0]' + } + + It "Compress-PSResource creates nuspec dependecy version range when ModuleVersion is in RequiredModules section" { + $version = "1.0.0" + $requiredModules = @( + @{ + 'ModuleName' = 'PSGetTestRequiredModule' + 'GUID' = (New-Guid).Guid + 'ModuleVersion' = '2.0.0' + } + ) + $compressParams = @{ + 'PublishModuleBase' = $script:PublishModuleBase + 'PublishModuleName' = $script:PublishModuleName + 'ModuleVersion' = $version + 'RepositoryPath' = $script:repositoryPath + 'TestDrive' = $TestDrive + 'RequiredModules' = $requiredModules + 'SkipModuleManifestValidate' = $true + } + $nuspecxml = CompressExpandRetrieveNuspec @compressParams + $nuspecxml.package.metadata.dependencies.dependency.version.replace(' ', '') | Should -BeExactly '2.0.0' + } + + It "Compress-PSResource creates nuspec dependecy version range when MaximumVersion is in RequiredModules section" { + $version = "1.0.0" + $requiredModules = @( + @{ + 'ModuleName' = 'PSGetTestRequiredModule' + 'GUID' = (New-Guid).Guid + 'MaximumVersion' = '2.0.0' + } + ) + $compressParams = @{ + 'PublishModuleBase' = $script:PublishModuleBase + 'PublishModuleName' = $script:PublishModuleName + 'ModuleVersion' = $version + 'RepositoryPath' = $script:repositoryPath + 'TestDrive' = $TestDrive + 'RequiredModules' = $requiredModules + 'SkipModuleManifestValidate' = $true + } + $nuspecxml = CompressExpandRetrieveNuspec @compressParams + $nuspecxml.package.metadata.dependencies.dependency.version.replace(' ', '') | Should -BeExactly '(,2.0.0]' + } + + It "Compress-PSResource creates nuspec dependecy version range when ModuleVersion and MaximumVersion are in RequiredModules section" { + $version = "1.0.0" + $requiredModules = @( + @{ + 'ModuleName' = 'PSGetTestRequiredModule' + 'GUID' = (New-Guid).Guid + 'ModuleVersion' = '1.0.0' + 'MaximumVersion' = '2.0.0' + } + ) + $compressParams = @{ + 'PublishModuleBase' = $script:PublishModuleBase + 'PublishModuleName' = $script:PublishModuleName + 'ModuleVersion' = $version + 'RepositoryPath' = $script:repositoryPath + 'TestDrive' = $TestDrive + 'RequiredModules' = $requiredModules + 'SkipModuleManifestValidate' = $true + } + $nuspecxml = CompressExpandRetrieveNuspec @compressParams + $nuspecxml.package.metadata.dependencies.dependency.version.replace(' ', '') | Should -BeExactly '[1.0.0,2.0.0]' + } + + It "Compress-PSResource creates nuspec dependecy version range when there are multiple modules in RequiredModules section" { + $version = "1.0.0" + $requiredModules = @( + @{ + 'ModuleName' = 'PSGetTestRequiredModuleRequiredVersion' + 'GUID' = (New-Guid).Guid + 'RequiredVersion' = '1.0.0' + }, + @{ + 'ModuleName' = 'PSGetTestRequiredModuleModuleVersion' + 'GUID' = (New-Guid).Guid + 'ModuleVersion' = '2.0.0' + }, + @{ + 'ModuleName' = 'PSGetTestRequiredModuleMaximumVersion' + 'GUID' = (New-Guid).Guid + 'MaximumVersion' = '3.0.0' + }, + @{ + 'ModuleName' = 'PSGetTestRequiredModuleModuleAndMaximumVersion' + 'GUID' = (New-Guid).Guid + 'ModuleVersion' = '4.0.0' + 'MaximumVersion' = '5.0.0' + } + ) + $compressParams = @{ + 'PublishModuleBase' = $script:PublishModuleBase + 'PublishModuleName' = $script:PublishModuleName + 'ModuleVersion' = $version + 'RepositoryPath' = $script:repositoryPath + 'TestDrive' = $TestDrive + 'RequiredModules' = $requiredModules + 'SkipModuleManifestValidate' = $true + } + $nuspecxml = CompressExpandRetrieveNuspec @compressParams + foreach ($dependency in $nuspecxml.package.metadata.dependencies.dependency) { + switch ($dependency.id) { + "PSGetTestRequiredModuleRequiredVersion" { + $dependency.version.replace(' ', '') | Should -BeExactly '[1.0.0]' + } + "PSGetTestRequiredModuleModuleVersion" { + $dependency.version.replace(' ', '') | Should -BeExactly '2.0.0' + } + "PSGetTestRequiredModuleMaximumVersion" { + $dependency.version.replace(' ', '') | Should -BeExactly '(,3.0.0]' + } + "PSGetTestRequiredModuleModuleAndMaximumVersion" { + $dependency.version.replace(' ', '') | Should -BeExactly '[4.0.0,5.0.0]' + } + } + } + } + <# Test for Signing the nupkg. Signing doesn't work It "Compressed Module is able to be signed with a certificate" { $version = "1.0.0" From cf7ee196805868d536ed8737e4e985935e162879 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Wed, 19 Feb 2025 11:40:36 -0800 Subject: [PATCH 02/11] Allow wildcard for MAR repository for FindAll and FindByName (#1786) --- src/code/ContainerRegistryServerAPICalls.cs | 92 ++++++++++++++++--- ...SResourceContainerRegistryServer.Tests.ps1 | 14 ++- 2 files changed, 92 insertions(+), 14 deletions(-) diff --git a/src/code/ContainerRegistryServerAPICalls.cs b/src/code/ContainerRegistryServerAPICalls.cs index d9175c0b0..973525daa 100644 --- a/src/code/ContainerRegistryServerAPICalls.cs +++ b/src/code/ContainerRegistryServerAPICalls.cs @@ -46,6 +46,7 @@ internal class ContainerRegistryServerAPICalls : ServerApiCall const string containerRegistryFindImageVersionUrlTemplate = "https://{0}/v2/{1}/tags/list"; // 0 - registry, 1 - repo(modulename) const string containerRegistryStartUploadTemplate = "https://{0}/v2/{1}/blobs/uploads/"; // 0 - registry, 1 - packagename const string containerRegistryEndUploadTemplate = "https://{0}{1}&digest=sha256:{2}"; // 0 - registry, 1 - location, 2 - digest + const string containerRegistryRepositoryListTemplate = "https://{0}/v2/_catalog"; // 0 - registry #endregion @@ -76,13 +77,13 @@ public ContainerRegistryServerAPICalls(PSRepositoryInfo repository, PSCmdlet cmd public override FindResults FindAll(bool includePrerelease, ResourceType type, out ErrorRecord errRecord) { _cmdletPassedIn.WriteDebug("In ContainerRegistryServerAPICalls::FindAll()"); - errRecord = new ErrorRecord( - new InvalidOperationException($"Find all is not supported for the ContainerRegistry server protocol repository '{Repository.Name}'"), - "FindAllFailure", - ErrorCategory.InvalidOperation, - this); + var findResult = FindPackages("*", includePrerelease, out errRecord); + if (errRecord != null) + { + return emptyResponseResults; + } - return emptyResponseResults; + return findResult; } /// @@ -161,13 +162,13 @@ public override FindResults FindNameWithTag(string packageName, string[] tags, b public override FindResults FindNameGlobbing(string packageName, bool includePrerelease, ResourceType type, out ErrorRecord errRecord) { _cmdletPassedIn.WriteDebug("In ContainerRegistryServerAPICalls::FindNameGlobbing()"); - errRecord = new ErrorRecord( - new InvalidOperationException($"FindNameGlobbing all is not supported for the ContainerRegistry server protocol repository '{Repository.Name}'"), - "FindNameGlobbingFailure", - ErrorCategory.InvalidOperation, - this); + var findResult = FindPackages(packageName, includePrerelease, out errRecord); + if (errRecord != null) + { + return emptyResponseResults; + } - return emptyResponseResults; + return findResult; } /// @@ -591,6 +592,20 @@ internal JObject FindContainerRegistryImageTags(string packageName, string versi return GetHttpResponseJObjectUsingDefaultHeaders(findImageUrl, HttpMethod.Get, defaultHeaders, out errRecord); } + /// + /// Helper method to find all packages on container registry + /// + /// + /// + /// + internal JObject FindAllRepositories(string containerRegistryAccessToken, out ErrorRecord errRecord) + { + _cmdletPassedIn.WriteDebug("In ContainerRegistryServerAPICalls::FindAllRepositories()"); + string repositoryListUrl = string.Format(containerRegistryRepositoryListTemplate, Registry); + var defaultHeaders = GetDefaultHeaders(containerRegistryAccessToken); + return GetHttpResponseJObjectUsingDefaultHeaders(repositoryListUrl, HttpMethod.Get, defaultHeaders, out errRecord); + } + /// /// Get metadata for a package version. /// @@ -1705,12 +1720,63 @@ private string PrependMARPrefix(string packageName) // If the repostitory is MAR and its not a wildcard search, we need to prefix the package name with MAR prefix. string updatedPackageName = Repository.IsMARRepository() && packageName.Trim() != "*" - ? string.Concat(prefix, packageName) + ? packageName.StartsWith(prefix) ? packageName : string.Concat(prefix, packageName) : packageName; return updatedPackageName; } + private FindResults FindPackages(string packageName, bool includePrerelease, out ErrorRecord errRecord) + { + _cmdletPassedIn.WriteDebug("In ContainerRegistryServerAPICalls::FindPackages()"); + errRecord = null; + string containerRegistryAccessToken = GetContainerRegistryAccessToken(out errRecord); + if (errRecord != null) + { + return emptyResponseResults; + } + + var pkgResult = FindAllRepositories(containerRegistryAccessToken, out errRecord); + if (errRecord != null) + { + return emptyResponseResults; + } + + List repositoriesList = new List(); + var isMAR = Repository.IsMARRepository(); + + // Convert the list of repositories to a list of hashtables + foreach (var repository in pkgResult["repositories"].ToList()) + { + string repositoryName = repository.ToString(); + + if (isMAR && !repositoryName.StartsWith(PSRepositoryInfo.MARPrefix)) + { + continue; + } + + // This remove the 'psresource/' prefix from the repository name for comparison with wildcard. + string moduleName = repositoryName.Substring(11); + + WildcardPattern wildcardPattern = new WildcardPattern(packageName, WildcardOptions.IgnoreCase); + + if (!wildcardPattern.IsMatch(moduleName)) + { + continue; + } + + _cmdletPassedIn.WriteDebug($"Found repository: {repositoryName}"); + + repositoriesList.AddRange(FindPackagesWithVersionHelper(repositoryName, VersionType.VersionRange, versionRange: VersionRange.All, requiredVersion: null, includePrerelease, getOnlyLatest: true, out errRecord)); + if (errRecord != null) + { + return emptyResponseResults; + } + } + + return new FindResults(stringResponse: new string[] { }, hashtableResponse: repositoriesList.ToArray(), responseType: containerRegistryFindResponseType); + } + #endregion } } diff --git a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 index b7ffdfb8e..3b403b24d 100644 --- a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 +++ b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 @@ -247,7 +247,7 @@ Describe 'Test Find-PSResource for MAR Repository' -tags 'CI' { It "Should find resource given specific Name, Version null" { $res = Find-PSResource -Name "Az.Accounts" -Repository "MAR" $res.Name | Should -Be "Az.Accounts" - $res.Version | Should -Be "4.0.0" + $res.Version | Should -BeGreaterThan ([Version]"4.0.0") } It "Should find resource and its dependency given specific Name and Version" { @@ -255,4 +255,16 @@ Describe 'Test Find-PSResource for MAR Repository' -tags 'CI' { $res.Dependencies.Length | Should -Be 1 $res.Dependencies[0].Name | Should -Be "Az.Accounts" } + + It "Should find resource with wildcard in Name" { + $res = Find-PSResource -Name "Az.App*" -Repository "MAR" + $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 | Should -Not -BeNullOrEmpty + $res.Count | Should -BeGreaterThan 1 + } } From a7873b7e2ec9cee309e42c58dd680459b23d8b56 Mon Sep 17 00:00:00 2001 From: Anam Navied Date: Wed, 12 Feb 2025 13:09:10 -0500 Subject: [PATCH 03/11] Update Dependency parsing logic to account for Az packages with Az* naming --- src/code/PSResourceInfo.cs | 2 +- .../FindPSResourceContainerRegistryServer.Tests.ps1 | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/code/PSResourceInfo.cs b/src/code/PSResourceInfo.cs index 409665edc..547828393 100644 --- a/src/code/PSResourceInfo.cs +++ b/src/code/PSResourceInfo.cs @@ -972,7 +972,7 @@ public static bool TryConvertFromContainerRegistryJson( metadata["Dependencies"] = ParseContainerRegistryDependencies(requiredModulesElement, out errorMsg).ToArray(); } - if (string.Equals(packageName, "Az", StringComparison.OrdinalIgnoreCase) || packageName.StartsWith("Az.", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(packageName, "Az", StringComparison.OrdinalIgnoreCase) || string.Equals(packageName, "Azpreview", StringComparison.OrdinalIgnoreCase) || packageName.StartsWith("Az.", StringComparison.OrdinalIgnoreCase)) { if (rootDom.TryGetProperty("ModuleList", out JsonElement moduleListDepsElement)) { diff --git a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 index 3b403b24d..618cb05f4 100644 --- a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 +++ b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 @@ -266,5 +266,8 @@ Describe 'Test Find-PSResource for MAR Repository' -tags 'CI' { $res = Find-PSResource -Name "*" -Repository "MAR" $res | Should -Not -BeNullOrEmpty $res.Count | Should -BeGreaterThan 1 + 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.Dependencies.Length | Should -Not -Be 0 } } From b59dd8c5123392e9b9942a6fee371da9c0b2863e Mon Sep 17 00:00:00 2001 From: Anam Navied Date: Tue, 18 Feb 2025 01:18:33 -0500 Subject: [PATCH 04/11] fix install for a package from MAR that may have different version than its normalized version --- src/code/InstallHelper.cs | 8 ++++ src/code/Utils.cs | 9 ++-- ...SResourceContainerRegistryServer.Tests.ps1 | 20 +++++++++ ...SResourceContainerRegistryServer.Tests.ps1 | 44 ++++++++++++++++++- 4 files changed, 77 insertions(+), 4 deletions(-) diff --git a/src/code/InstallHelper.cs b/src/code/InstallHelper.cs index e31c2b86c..3e0d8ae9a 100644 --- a/src/code/InstallHelper.cs +++ b/src/code/InstallHelper.cs @@ -824,6 +824,14 @@ private Hashtable BeginPackageInstall( pkgVersion += $"-{pkgToInstall.Prerelease}"; } } + + // For most repositories/providers the server will use the normalized version, which pkgVersion originally reflects + // However, for container registries the version must exactly match what was in the artifact manifest and then reflected in PSResourceInfo.Version.ToString() + if (currentServer.Repository.ApiVersion == PSRepositoryInfo.APIVersion.ContainerRegistry) + { + pkgVersion = String.IsNullOrEmpty(pkgToInstall.Prerelease) ? pkgToInstall.Version.ToString() : $"{pkgToInstall.Version.ToString()}-{pkgToInstall.Prerelease}"; + } + // Check to see if the pkg is already installed (ie the pkg is installed and the version satisfies the version range provided via param) if (!_reinstall) { diff --git a/src/code/Utils.cs b/src/code/Utils.cs index d51ba0fdb..89de7cd10 100644 --- a/src/code/Utils.cs +++ b/src/code/Utils.cs @@ -316,9 +316,11 @@ public static string GetNormalizedVersionString( string versionString, string prerelease) { - // versionString may be like 1.2.0.0 or 1.2.0 + // versionString may be like 1.2.0.0 or 1.2.0 or 1.2 // prerelease may be null or "alpha1" // possible passed in examples: + // versionString: "1.2" <- container registry 2 digit version + // versionString: "1.2" prerelease: "alpha1" <- container registry 2 digit version // versionString: "1.2.0" prerelease: "alpha1" // versionString: "1.2.0" prerelease: "" <- doubtful though // versionString: "1.2.0.0" prerelease: "alpha1" @@ -331,9 +333,10 @@ public static string GetNormalizedVersionString( int numVersionDigits = versionString.Split('.').Count(); - if (numVersionDigits == 3) + if (numVersionDigits == 2 || numVersionDigits == 3) { - // versionString: "1.2.0" prerelease: "alpha1" + // versionString: "1.2.0" prerelease: "alpha1" -> 1.2.0-alpha1 + // versionString: "1.2" prerelease: "alpha1" -> 1.2-alpha1 return versionString + "-" + prerelease; } else if (numVersionDigits == 4) diff --git a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 index 618cb05f4..4295fccfd 100644 --- a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 +++ b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 @@ -8,6 +8,7 @@ Describe 'Test HTTP Find-PSResource for ACR Server Protocol' -tags 'CI' { BeforeAll{ $testModuleName = "test-module" + $testModuleWith2DigitVersion = "test-2DigitPkg" $testModuleParentName = "test_parent_mod" $testModuleDependencyName = "test_dependency_mod" $testScriptName = "test-script" @@ -82,6 +83,25 @@ Describe 'Test HTTP Find-PSResource for ACR Server Protocol' -tags 'CI' { $res.Count | Should -BeGreaterOrEqual 1 } + It "Find resource when version contains different number of digits than the normalized version" { + # the resource has version "1.0", but querying with any equivalent version should work + $res1DigitVersion = Find-PSResource -Name $testModuleWith2DigitVersion -Version "1" -Repository $ACRRepoName + $res1DigitVersion | Should -Not -BeNullOrEmpty + $res1DigitVersion.Version | Should -Be "1.0" + + $res2DigitVersion = Find-PSResource -Name $testModuleWith2DigitVersion -Version "1.0" -Repository $ACRRepoName + $res2DigitVersion | Should -Not -BeNullOrEmpty + $res2DigitVersion.Version | Should -Be "1.0" + + $res3DigitVersion = Find-PSResource -Name $testModuleWith2DigitVersion -Version "1.0.0" -Repository $ACRRepoName + $res3DigitVersion | Should -Not -BeNullOrEmpty + $res3DigitVersion.Version | Should -Be "1.0" + + $res4DigitVersion = Find-PSResource -Name $testModuleWith2DigitVersion -Version "1.0.0.0" -Repository $ACRRepoName + $res4DigitVersion | Should -Not -BeNullOrEmpty + $res4DigitVersion.Version | Should -Be "1.0" + } + It "Find module and dependencies when -IncludeDependencies is specified" { $res = Find-PSResource -Name $testModuleParentName -Repository $ACRRepoName -IncludeDependencies $res | Should -Not -BeNullOrEmpty diff --git a/test/InstallPSResourceTests/InstallPSResourceContainerRegistryServer.Tests.ps1 b/test/InstallPSResourceTests/InstallPSResourceContainerRegistryServer.Tests.ps1 index 2ade007f2..5f80ace08 100644 --- a/test/InstallPSResourceTests/InstallPSResourceContainerRegistryServer.Tests.ps1 +++ b/test/InstallPSResourceTests/InstallPSResourceContainerRegistryServer.Tests.ps1 @@ -10,6 +10,7 @@ Describe 'Test Install-PSResource for ACR scenarios' -tags 'CI' { BeforeAll { $testModuleName = "test-module" $testModuleName2 = "test-module2" + $testModuleWith2DigitVersion = "test-2DigitPkg" $testCamelCaseModuleName = "test-camelCaseModule" $testCamelCaseScriptName = "test-camelCaseScript" $testModuleParentName = "test_parent_mod" @@ -33,7 +34,7 @@ Describe 'Test Install-PSResource for ACR scenarios' -tags 'CI' { } AfterEach { - Uninstall-PSResource $testModuleName, $testModuleName2, $testCamelCaseModuleName, $testScriptName, $testCamelCaseScriptName -Version "*" -SkipDependencyCheck -ErrorAction SilentlyContinue + Uninstall-PSResource $testModuleName, $testModuleName2, $testCamelCaseModuleName, $testScriptName, $testCamelCaseScriptName, $testModuleWith2DigitVersion -Version "*" -SkipDependencyCheck -ErrorAction SilentlyContinue } AfterAll { @@ -75,6 +76,47 @@ Describe 'Test Install-PSResource for ACR scenarios' -tags 'CI' { $pkg.Version | Should -BeExactly "1.0.0" } + It "Install resource when version contains different number of digits than the normalized version- 1 digit specified" { + # the resource has version "1.0", but querying with any equivalent version should work + Install-PSResource -Name $testModuleWith2DigitVersion -Version "1" -Repository $ACRRepoName -TrustRepository + $res = Get-InstalledPSResource -Name $testModuleWith2DigitVersion + $res | Should -Not -BeNullOrEmpty + $res.Version | Should -Be "1.0" + } + + It "Install resource when version contains different number of digits than the normalized version- 2 digits specified" { + # the resource has version "1.0", but querying with any equivalent version should work + Install-PSResource -Name $testModuleWith2DigitVersion -Version "1.0" -Repository $ACRRepoName -TrustRepository + $res = Get-InstalledPSResource -Name $testModuleWith2DigitVersion + $res | Should -Not -BeNullOrEmpty + $res.Version | Should -Be "1.0" + } + + It "Install resource when version contains different number of digits than the normalized version- 3 digits specified" { + # the resource has version "1.0", but querying with any equivalent version should work + Install-PSResource -Name $testModuleWith2DigitVersion -Version "1.0.0" -Repository $ACRRepoName -TrustRepository + $res = Get-InstalledPSResource -Name $testModuleWith2DigitVersion + $res | Should -Not -BeNullOrEmpty + $res.Version | Should -Be "1.0" + } + + It "Install resource when version contains different number of digits than the normalized version- 4 digits specified" { + # the resource has version "1.0", but querying with any equivalent version should work + Install-PSResource -Name $testModuleWith2DigitVersion -Version "1.0.0.0" -Repository $ACRRepoName -TrustRepository + $res = Get-InstalledPSResource -Name $testModuleWith2DigitVersion + $res | Should -Not -BeNullOrEmpty + $res.Version | Should -Be "1.0" + } + + It "Install resource where version specified is a prerelease version" { + # the resource has version "1.0", but querying with any equivalent version should work + Install-PSResource -Name $testModuleWith2DigitVersion -Version "1.5-alpha" -Prerelease -Repository $ACRRepoName -TrustRepository + $res = Get-InstalledPSResource -Name $testModuleWith2DigitVersion + $res | Should -Not -BeNullOrEmpty + $res.Version | Should -Be "1.5" + $res.Prerelease | Should -Be "alpha" + } + It "Install multiple resources by name" { $pkgNames = @($testModuleName, $testModuleName2) Install-PSResource -Name $pkgNames -Repository $ACRRepoName -TrustRepository From 5b08a8635231840812f8acf52104705d1db8ca77 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Fri, 21 Feb 2025 10:27:14 -0800 Subject: [PATCH 05/11] Use authentication challenge for unauthenticated container registry repository (#1797) --- src/code/ContainerRegistryServerAPICalls.cs | 82 ++++++++++++++++++- ...SResourceContainerRegistryServer.Tests.ps1 | 36 +++++++- 2 files changed, 110 insertions(+), 8 deletions(-) diff --git a/src/code/ContainerRegistryServerAPICalls.cs b/src/code/ContainerRegistryServerAPICalls.cs index 973525daa..785c7aeae 100644 --- a/src/code/ContainerRegistryServerAPICalls.cs +++ b/src/code/ContainerRegistryServerAPICalls.cs @@ -38,7 +38,7 @@ internal class ContainerRegistryServerAPICalls : ServerApiCall private static readonly FindResults emptyResponseResults = new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: containerRegistryFindResponseType); const string containerRegistryRefreshTokenTemplate = "grant_type=access_token&service={0}&tenant={1}&access_token={2}"; // 0 - registry, 1 - tenant, 2 - access token - const string containerRegistryAccessTokenTemplate = "grant_type=refresh_token&service={0}&scope=repository:*:*&refresh_token={1}"; // 0 - registry, 1 - refresh token + const string containerRegistryAccessTokenTemplate = "grant_type=refresh_token&service={0}&scope=repository:*:*&scope=registry:catalog:*&refresh_token={1}"; // 0 - registry, 1 - refresh token const string containerRegistryOAuthExchangeUrlTemplate = "https://{0}/oauth2/exchange"; // 0 - registry const string containerRegistryOAuthTokenUrlTemplate = "https://{0}/oauth2/token"; // 0 - registry const string containerRegistryManifestUrlTemplate = "https://{0}/v2/{1}/manifests/{2}"; // 0 - registry, 1 - repo(modulename), 2 - tag(version) @@ -46,6 +46,7 @@ internal class ContainerRegistryServerAPICalls : ServerApiCall const string containerRegistryFindImageVersionUrlTemplate = "https://{0}/v2/{1}/tags/list"; // 0 - registry, 1 - repo(modulename) const string containerRegistryStartUploadTemplate = "https://{0}/v2/{1}/blobs/uploads/"; // 0 - registry, 1 - packagename const string containerRegistryEndUploadTemplate = "https://{0}{1}&digest=sha256:{2}"; // 0 - registry, 1 - location, 2 - digest + const string defaultScope = "&scope=repository:*:*&scope=registry:catalog:*"; const string containerRegistryRepositoryListTemplate = "https://{0}/v2/_catalog"; // 0 - registry #endregion @@ -392,12 +393,18 @@ internal string GetContainerRegistryAccessToken(out ErrorRecord errRecord) } else { - bool isRepositoryUnauthenticated = IsContainerRegistryUnauthenticated(Repository.Uri.ToString(), out errRecord); + bool isRepositoryUnauthenticated = IsContainerRegistryUnauthenticated(Repository.Uri.ToString(), out errRecord, out accessToken); if (errRecord != null) { return null; } + if (!string.IsNullOrEmpty(accessToken)) + { + _cmdletPassedIn.WriteVerbose("Anonymous access token retrieved."); + return accessToken; + } + if (!isRepositoryUnauthenticated) { accessToken = Utils.GetAzAccessToken(); @@ -437,15 +444,82 @@ internal string GetContainerRegistryAccessToken(out ErrorRecord errRecord) /// /// Checks if container registry repository is unauthenticated. /// - internal bool IsContainerRegistryUnauthenticated(string containerRegistyUrl, out ErrorRecord errRecord) + internal bool IsContainerRegistryUnauthenticated(string containerRegistyUrl, out ErrorRecord errRecord, out string anonymousAccessToken) { _cmdletPassedIn.WriteDebug("In ContainerRegistryServerAPICalls::IsContainerRegistryUnauthenticated()"); errRecord = null; + anonymousAccessToken = string.Empty; string endpoint = $"{containerRegistyUrl}/v2/"; HttpResponseMessage response; try { response = _sessionClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, endpoint)).Result; + + if (response.StatusCode == HttpStatusCode.Unauthorized) + { + // check if there is a auth challenge header + if (response.Headers.WwwAuthenticate.Count() > 0) + { + var authHeader = response.Headers.WwwAuthenticate.First(); + if (authHeader.Scheme == "Bearer") + { + // check if there is a realm + if (authHeader.Parameter.Contains("realm")) + { + // get the realm + var realm = authHeader.Parameter.Split(',')?.Where(x => x.Contains("realm"))?.FirstOrDefault()?.Split('=')[1]?.Trim('"'); + // get the service + var service = authHeader.Parameter.Split(',')?.Where(x => x.Contains("service"))?.FirstOrDefault()?.Split('=')[1]?.Trim('"'); + + if (string.IsNullOrEmpty(realm) || string.IsNullOrEmpty(service)) + { + errRecord = new ErrorRecord( + new InvalidOperationException("Failed to get realm or service from the auth challenge header."), + "RegistryUnauthenticationCheckError", + ErrorCategory.InvalidResult, + this); + + return false; + } + + string content = "grant_type=access_token&service=" + service + defaultScope; + var contentHeaders = new Collection> { new KeyValuePair("Content-Type", "application/x-www-form-urlencoded") }; + + // get the anonymous access token + var url = $"{realm}?service={service}{defaultScope}"; + + // we dont check the errorrecord here because we want to return false if we get a 401 and not throw an error + var results = GetHttpResponseJObjectUsingContentHeaders(url, HttpMethod.Get, content, contentHeaders, out _); + + if (results == null) + { + _cmdletPassedIn.WriteDebug("Failed to get access token from the realm. results is null."); + return false; + } + + if (results["access_token"] == null) + { + _cmdletPassedIn.WriteDebug($"Failed to get access token from the realm. access_token is null. results: {results}"); + return false; + } + + anonymousAccessToken = results["access_token"].ToString(); + _cmdletPassedIn.WriteDebug("Anonymous access token retrieved"); + return true; + } + } + } + } + } + catch (HttpRequestException hre) + { + errRecord = new ErrorRecord( + hre, + "RegistryAnonymousAcquireError", + ErrorCategory.ConnectionError, + this); + + return false; } catch (Exception e) { @@ -1756,7 +1830,7 @@ private FindResults FindPackages(string packageName, bool includePrerelease, out } // This remove the 'psresource/' prefix from the repository name for comparison with wildcard. - string moduleName = repositoryName.Substring(11); + string moduleName = repositoryName.StartsWith("psresource/") ? repositoryName.Substring(11) : repositoryName; WildcardPattern wildcardPattern = new WildcardPattern(packageName, WildcardOptions.IgnoreCase); diff --git a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 index 4295fccfd..76c6e64b6 100644 --- a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 +++ b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 @@ -171,12 +171,11 @@ Describe 'Test HTTP Find-PSResource for ACR Server Protocol' -tags 'CI' { $err[0].FullyQualifiedErrorId | Should -BeExactly "FindCommandOrDscResourceFailure,Microsoft.PowerShell.PSResourceGet.Cmdlets.FindPSResource" } - It "Should not find all resources given Name '*'" { + It "Should find all resources given Name '*'" { # FindAll() $res = Find-PSResource -Name "*" -Repository $ACRRepoName -ErrorVariable err -ErrorAction SilentlyContinue - $res | Should -BeNullOrEmpty - $err.Count | Should -BeGreaterThan 0 - $err[0].FullyQualifiedErrorId | Should -BeExactly "FindAllFailure,Microsoft.PowerShell.PSResourceGet.Cmdlets.FindPSResource" + $res | Should -Not -BeNullOrEmpty + $res.Count | Should -BeGreaterThan 0 } It "Should find script given Name" { @@ -291,3 +290,32 @@ Describe 'Test Find-PSResource for MAR Repository' -tags 'CI' { $res.Dependencies.Length | Should -Not -Be 0 } } + +# Skip this test fo +Describe 'Test Find-PSResource for unauthenticated ACR repository' -tags 'CI' { + BeforeAll { + $skipOnWinPS = $PSVersionTable.PSVersion.Major -eq 5 + + if (-not $skipOnWinPS) { + Register-PSResourceRepository -Name "Unauthenticated" -Uri "https://psresourcegetnoauth.azurecr.io/" -ApiVersion "ContainerRegistry" + } + } + + AfterAll { + if (-not $skipOnWinPS) { + Unregister-PSResourceRepository -Name "Unauthenticated" + } + } + + It "Should find resource given specific Name, Version null" { + + if ($skipOnWinPS) { + Set-ItResult -Pending -Because "Skipping test on Windows PowerShell" + return + } + + $res = Find-PSResource -Name "hello-world" -Repository "Unauthenticated" + $res.Name | Should -Be "hello-world" + $res.Version | Should -Be "5.0.0" + } +} From 2a8c2e43b2152516c36b4005e4899b69b0ffb795 Mon Sep 17 00:00:00 2001 From: Anam Navied Date: Thu, 6 Mar 2025 10:30:02 -0500 Subject: [PATCH 06/11] Merge pull request #1798 from PowerShell/alerickson-patch-1 Update README.md --- README.md | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index e94344ae1..a0ca6c443 100644 --- a/README.md +++ b/README.md @@ -52,25 +52,22 @@ Please use the [PowerShell Gallery](https://www.powershellgallery.com) to get th ### Build the project -```powershell -# Build for the net472 framework -PS C:\Repos\PSResourceGet> .\build.ps1 -Clean -Build -BuildConfiguration Debug -BuildFramework net472 - -# Build for the netstandard2.0 framework -PS C:\Repos\PSResourceGet> .\build.ps1 -Clean -Build -BuildConfiguration Debug -BuildFramework netstandard2.0 -``` - -### Publish the module to a local repository -======= -* Run functional tests +Note: Please ensure you have the exact version of the .NET SDK installed. The current version can be found in the [global.json](https://github.com/PowerShell/PSResourceGet/blob/master/global.json) and installed from the [.NET website](https://dotnet.microsoft.com/en-us/download). + ```powershell + # Build for the net472 framework + PS C:\Repos\PSResourceGet> .\build.ps1 -Clean -Build -BuildConfiguration Debug -BuildFramework net472 + ``` -```powershell -PS C:\Repos\PSResourceGet> Invoke-Pester -``` +### Run functional tests -```powershell -PS C:\Repos\PSResourceGet> Invoke-Pester -``` +* Run all tests + ```powershell + PS C:\Repos\PSResourceGet> Invoke-Pester + ``` +* Run an individual test + ```powershell + PS C:\Repos\PSResourceGet> Invoke-Pester + ``` ### Import the built module into a new PowerShell session From ba53100481ddec6a51a081f2d32682e887c07314 Mon Sep 17 00:00:00 2001 From: Anam Navied Date: Tue, 25 Feb 2025 13:09:55 -0500 Subject: [PATCH 07/11] Get metadata properties when finding a PSResource from a ContainerRegistry --- src/code/PSResourceInfo.cs | 70 ++++++++++++++++--- ...SResourceContainerRegistryServer.Tests.ps1 | 10 +++ 2 files changed, 71 insertions(+), 9 deletions(-) diff --git a/src/code/PSResourceInfo.cs b/src/code/PSResourceInfo.cs index 547828393..dd840b62d 100644 --- a/src/code/PSResourceInfo.cs +++ b/src/code/PSResourceInfo.cs @@ -852,9 +852,9 @@ public static bool TryConvertFromContainerRegistryJson( pkgVersion = ParseHttpVersion(versionValue, out string prereleaseLabel); metadata["Version"] = pkgVersion; - if (rootDom.TryGetProperty("PrivateData", out JsonElement privateDataElement) && privateDataElement.TryGetProperty("PSData", out JsonElement psDataElement)) + if (rootDom.TryGetProperty("PrivateData", out JsonElement versionPrivateDataElement) && versionPrivateDataElement.TryGetProperty("PSData", out JsonElement versionPSDataElement)) { - if (psDataElement.TryGetProperty("Prerelease", out JsonElement pkgPrereleaseLabelElement) && !String.IsNullOrEmpty(pkgPrereleaseLabelElement.ToString().Trim())) + if (versionPSDataElement.TryGetProperty("Prerelease", out JsonElement pkgPrereleaseLabelElement) && !String.IsNullOrEmpty(pkgPrereleaseLabelElement.ToString().Trim())) { prereleaseLabel = pkgPrereleaseLabelElement.ToString().Trim(); versionValue += $"-{prereleaseLabel}"; @@ -938,14 +938,19 @@ public static bool TryConvertFromContainerRegistryJson( } // Author - if (rootDom.TryGetProperty("Authors", out JsonElement authorsElement) || rootDom.TryGetProperty("authors", out authorsElement)) + if (rootDom.TryGetProperty("Authors", out JsonElement authorsElement) || rootDom.TryGetProperty("authors", out authorsElement) || rootDom.TryGetProperty("Author", out authorsElement)) { metadata["Authors"] = authorsElement.ToString(); + } - // CompanyName - // CompanyName is not provided in v3 pkg metadata response, so we've just set it to the author, - // which is often the company - metadata["CompanyName"] = authorsElement.ToString(); + if (rootDom.TryGetProperty("CompanyName", out JsonElement companyNameElement)) + { + metadata["CompanyName"] = companyNameElement.ToString(); + } + else + { + // if CompanyName property is not provided set it to the Author value which is often the same. + metadata["CompanyName"] = metadata["Authors"]; } // Copyright @@ -978,15 +983,62 @@ public static bool TryConvertFromContainerRegistryJson( { metadata["Dependencies"] = ParseContainerRegistryDependencies(moduleListDepsElement, out errorMsg).ToArray(); } - else if (rootDom.TryGetProperty("PrivateData", out JsonElement privateDataElement) && privateDataElement.TryGetProperty("PSData", out JsonElement psDataElement)) + else if (rootDom.TryGetProperty("PrivateData", out JsonElement depsPrivateDataElement) && depsPrivateDataElement.TryGetProperty("PSData", out JsonElement depsPSDataElement)) { - if (psDataElement.TryGetProperty("ModuleList", out JsonElement privateDataModuleListDepsElement)) + if (depsPSDataElement.TryGetProperty("ModuleList", out JsonElement privateDataModuleListDepsElement)) { metadata["Dependencies"] = ParseContainerRegistryDependencies(privateDataModuleListDepsElement, out errorMsg).ToArray(); } } } + if (rootDom.TryGetProperty("PrivateData", out JsonElement privateDataElement) && privateDataElement.ValueKind == JsonValueKind.Object && privateDataElement.TryGetProperty("PSData", out JsonElement psDataElement)) + { + // some properties that may be in PrivateData.PSData: LicenseUri, ProjectUri, IconUri, ReleaseNotes + if (!metadata.ContainsKey("LicenseUrl") && psDataElement.TryGetProperty("LicenseUri", out JsonElement psDataLicenseUriElement)) + { + metadata["LicenseUrl"] = ParseHttpUrl(psDataLicenseUriElement.ToString()) as Uri; + } + + if (!metadata.ContainsKey("ProjectUrl") && psDataElement.TryGetProperty("ProjectUri", out JsonElement psDataProjectUriElement)) + { + metadata["ProjectUrl"] = ParseHttpUrl(psDataProjectUriElement.ToString()) as Uri; + } + + if (!metadata.ContainsKey("IconUrl") && psDataElement.TryGetProperty("IconUri", out JsonElement psDataIconUriElement)) + { + metadata["IconUrl"] = ParseHttpUrl(psDataIconUriElement.ToString()) as Uri; + } + + if (!metadata.ContainsKey("ReleaseNotes") && psDataElement.TryGetProperty("ReleaseNotes", out JsonElement psDataReleaseNotesElement)) + { + metadata["ReleaseNotes"] = psDataReleaseNotesElement.ToString(); + } + + if (!metadata.ContainsKey("Tags") && psDataElement.TryGetProperty("Tags", out JsonElement psDataTagsElement)) + { + string[] pkgTags = Utils.EmptyStrArray; + if (psDataTagsElement.ValueKind == JsonValueKind.Array) + { + var arrayLength = psDataTagsElement.GetArrayLength(); + List tags = new List(arrayLength); + foreach (var tag in psDataTagsElement.EnumerateArray()) + { + tags.Add(tag.ToString()); + } + + pkgTags = tags.ToArray(); + } + else if (psDataTagsElement.ValueKind == JsonValueKind.String) + { + string tagStr = psDataTagsElement.ToString(); + pkgTags = tagStr.Split(Utils.WhitespaceSeparator, StringSplitOptions.RemoveEmptyEntries); + } + + metadata["Tags"] = pkgTags; + } + } + var additionalMetadataHashtable = new Dictionary { { "NormalizedVersion", metadata["NormalizedVersion"].ToString() } diff --git a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 index 76c6e64b6..ef3d1b7d2 100644 --- a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 +++ b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 @@ -252,6 +252,16 @@ Describe 'Test HTTP Find-PSResource for ACR Server Protocol' -tags 'CI' { $res.Dependencies.Length | Should -Be 1 $res.Dependencies[0].Name | Should -Be "Az.Accounts" } + + It "Should find resource and its associated author, licenseUri, projectUri, releaseNotes, etc properties" { + $res = Find-PSResource -Name "Az.Storage" -Version "8.0.0" -Repository $ACRRepoName + $res.Author | Should -Be "Microsoft Corporation" + $res.CompanyName | Should -Be "Microsoft Corporation" + $res.LicenseUri | Should -Be "https://aka.ms/azps-license" + $res.ProjectUri | Should -Be "https://github.com/Azure/azure-powershell" + $res.ReleaseNotes.Length | Should -Not -Be 0 + $res.Tags.Length | Should -Be 5 + } } Describe 'Test Find-PSResource for MAR Repository' -tags 'CI' { From 330201fe06485ecb085ba97640e46b65965dc74c Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Wed, 26 Feb 2025 10:01:27 -0800 Subject: [PATCH 08/11] Use deploybox to release to Gallery (#1800) --- .pipelines/PSResourceGet-Official.yml | 68 ++++++++++++--------------- 1 file changed, 31 insertions(+), 37 deletions(-) diff --git a/.pipelines/PSResourceGet-Official.yml b/.pipelines/PSResourceGet-Official.yml index d096cf33e..ae6e2d484 100644 --- a/.pipelines/PSResourceGet-Official.yml +++ b/.pipelines/PSResourceGet-Official.yml @@ -29,7 +29,7 @@ variables: value: onebranch.azurecr.io/windows/ltsc2022/vse2022:latest # Docker image which is used to build the project https://aka.ms/obpipelines/containers resources: - repositories: + repositories: - repository: onebranchTemplates type: git name: OneBranch.Pipelines/GovernedTemplates @@ -41,6 +41,8 @@ extends: featureFlags: WindowsHostVersion: '1ESWindows2022' customTags: 'ES365AIMigrationTooling' + release: + category: NonAzure globalSdl: disableLegacyManifest: true sbom: @@ -58,7 +60,7 @@ extends: binskim: enabled: true apiscan: - enabled: false + enabled: false stages: - stage: stagebuild @@ -125,15 +127,6 @@ extends: AnalyzeInPipeline: true Language: csharp - - pwsh: | - $module = 'Microsoft.PowerShell.PSResourceGet' - Write-Verbose "installing $module..." -verbose - $ProgressPreference = 'SilentlyContinue' - Install-Module $module -AllowClobber -Force - displayName: Install PSResourceGet 0.9.0 or above for build.psm1 - env: - ob_restore_phase: true # Set ob_restore_phase to run this step before '🔒 Setup Signing' step. - # this is installing .NET - pwsh: | Set-Location "$(repoRoot)" @@ -167,14 +160,14 @@ extends: } } displayName: Find all 3rd party files that need to be signed - + - task: onebranch.pipeline.signing@1 displayName: Sign 3rd Party files inputs: command: 'sign' signing_profile: 135020002 files_to_sign: '*.dll' - search_root: $(signSrcPath)/Microsoft.PowerShell.PSResourceGet/UnsignedDependencies + search_root: $(signSrcPath)/Microsoft.PowerShell.PSResourceGet/UnsignedDependencies - pwsh: | $newlySignedDepsPath = Join-Path -Path $(signSrcPath) -ChildPath "Microsoft.PowerShell.PSResourceGet" -AdditionalChildPath "UnsignedDependencies" @@ -216,7 +209,7 @@ extends: value: $(Build.SourcesDirectory)\PSResourceGet\.config\tsaoptions.json # Disable because SBOM was already built in the previous job - name: ob_sdl_sbom_enabled - value: false + value: true - name: signOutPath value: $(repoRoot)/signed - name: ob_signing_setup_enabled @@ -250,15 +243,12 @@ extends: displayName: Capture artifacts directory structure - pwsh: | - $module = 'Microsoft.PowerShell.PSResourceGet' - Write-Verbose "installing $module..." -verbose - $ProgressPreference = 'SilentlyContinue' - Install-Module $module -AllowClobber -Force - displayName: Install PSResourceGet 0.9.0 or above for build.psm1 + # This need to be done before set-location so the module from PSHome is loaded + Import-Module -Name Microsoft.PowerShell.PSResourceGet -Force - - pwsh: | Set-Location "$(signOutPath)\Microsoft.PowerShell.PSResourceGet" - New-Item -ItemType Directory -Path "$(signOutPath)\PublishedNupkg" -Force + $null = New-Item -ItemType Directory -Path "$(signOutPath)\PublishedNupkg" -Force + Register-PSResourceRepository -Name 'localRepo' -Uri "$(signOutPath)\PublishedNupkg" Publish-PSResource -Path "$(signOutPath)\Microsoft.PowerShell.PSResourceGet" -Repository 'localRepo' -Verbose displayName: Create nupkg for publishing @@ -274,7 +264,7 @@ extends: - pwsh: | Set-Location "$(signOutPath)\PublishedNupkg" Write-Host "Contents of signOutPath:" - Get-ChildItem "$(signOutPath)" -Recurse + Get-ChildItem "$(signOutPath)" -Recurse displayName: Find Nupkg - task: CopyFiles@2 @@ -282,10 +272,10 @@ extends: inputs: Contents: $(signOutPath)\PublishedNupkg\Microsoft.PowerShell.PSResourceGet.*.nupkg TargetFolder: $(ob_outputDirectory) - + - pwsh: | Write-Host "Contents of ob_outputDirectory:" - Get-ChildItem "$(ob_outputDirectory)" -Recurse + Get-ChildItem "$(ob_outputDirectory)" -Recurse displayName: Find Signed Nupkg - stage: release @@ -293,12 +283,14 @@ extends: dependsOn: stagebuild variables: version: $[ stageDependencies.build.main.outputs['package.version'] ] - drop: $(Pipeline.Workspace)/drop_build_main + drop: $(Pipeline.Workspace)/drop_stagebuild_nupkg + ob_release_environment: 'Production' + jobs: - job: validation displayName: Manual validation pool: - type: agentless + type: server timeoutInMinutes: 1440 steps: - task: ManualValidation@0 @@ -306,29 +298,31 @@ extends: inputs: instructions: Please validate the release timeoutInMinutes: 1440 + - job: PSGalleryPublish displayName: Publish to PSGallery dependsOn: validation + templateContext: + inputs: + - input: pipelineArtifact + artifactName: drop_stagebuild_nupkg pool: - type: windows + type: release + os: windows variables: ob_outputDirectory: '$(Build.ArtifactStagingDirectory)/ONEBRANCH_ARTIFACT' steps: - - download: current - displayName: Download artifact - - - pwsh: | - Get-ChildItem $(Pipeline.Workspace) -Recurse - displayName: Capture environment - - - pwsh: | - Get-ChildItem "$(Pipeline.Workspace)/drop_stagebuild_nupkg" -Recurse + - task: PowerShell@2 + inputs: + targetType: 'inline' + script: | + Get-ChildItem "$(Pipeline.Workspace)/" -Recurse displayName: Find signed Nupkg - task: NuGetCommand@2 displayName: Push PowerShellGet module artifacts to PSGallery feed inputs: command: push - packagesToPush: '$(Pipeline.Workspace)\drop_stagebuild_nupkg\PSResourceGet\signed\PublishedNupkg\Microsoft.PowerShell.PSResourceGet.*.nupkg' + packagesToPush: '$(Pipeline.Workspace)\PSResourceGet\signed\PublishedNupkg\Microsoft.PowerShell.PSResourceGet.*.nupkg' nuGetFeedType: external publishFeedCredentials: PSGet-PSGalleryPush From 35eac2220722d509a4039be1fc809351660566e5 Mon Sep 17 00:00:00 2001 From: Anam Navied Date: Thu, 6 Mar 2025 12:30:00 -0500 Subject: [PATCH 09/11] Update changelog, release notes, version for v1.1.1 release (#1801) --- CHANGELOG/1.1.md | 12 ++++++++++++ src/Microsoft.PowerShell.PSResourceGet.psd1 | 13 ++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/CHANGELOG/1.1.md b/CHANGELOG/1.1.md index ba6cce61e..4fb078eda 100644 --- a/CHANGELOG/1.1.md +++ b/CHANGELOG/1.1.md @@ -1,3 +1,15 @@ +# 1.1 Changelog + +## [1.1.1](https://github.com/PowerShell/PSResourceGet/compare/v1.1.0..v1.1.1) - 2025-03-06 + +- Bugfix to retrieve all metadata properties when finding a PSResource from a ContainerRegistry repository (#1799) +- Update README.md (#1798) +- Use authentication challenge for unauthenticated ContainerRegistry repository (#1797) +- Bugfix for Install-PSResource with varying digit version against ContainerRegistry repository (#1796) +- Bugfix for updating ContainerRegistry dependency parsing logic to account for AzPreview package (#1792) +- Add wildcard support for MAR repository for FindAll and FindByName (#1786) +- Bugfix for nuspec dependency version range calculation for RequiredModules (#1784) + ## [1.1.0](https://github.com/PowerShell/PSResourceGet/compare/v1.1.0-rc3...v1.1.0) - 2025-01-09 ### Bug Fixes diff --git a/src/Microsoft.PowerShell.PSResourceGet.psd1 b/src/Microsoft.PowerShell.PSResourceGet.psd1 index 30994ba2e..c9ca1cdd9 100644 --- a/src/Microsoft.PowerShell.PSResourceGet.psd1 +++ b/src/Microsoft.PowerShell.PSResourceGet.psd1 @@ -4,7 +4,7 @@ @{ RootModule = './Microsoft.PowerShell.PSResourceGet.dll' NestedModules = @('./Microsoft.PowerShell.PSResourceGet.psm1') - ModuleVersion = '1.1.0' + ModuleVersion = '1.1.1' CompatiblePSEditions = @('Core', 'Desktop') GUID = 'e4e0bda1-0703-44a5-b70d-8fe704cd0643' Author = 'Microsoft Corporation' @@ -56,6 +56,17 @@ ProjectUri = 'https://go.microsoft.com/fwlink/?LinkId=828955' LicenseUri = 'https://go.microsoft.com/fwlink/?LinkId=829061' ReleaseNotes = @' +## 1.1.1 + +### Bug Fix +- Bugfix to retrieve all metadata properties when finding a PSResource from a ContainerRegistry repository (#1799) +- Update README.md (#1798) +- Use authentication challenge for unauthenticated ContainerRegistry repository (#1797) +- Bugfix for Install-PSResource with varying digit version against ContainerRegistry repository (#1796) +- Bugfix for updating ContainerRegistry dependency parsing logic to account for AzPreview package (#1792) +- Add wildcard support for MAR repository for FindAll and FindByName (#1786) +- Bugfix for nuspec dependency version range calculation for RequiredModules (#1784) + ## 1.1.0 ### Bug Fix From b49b4da5d200c13e606182c42a48e6a98cd44e44 Mon Sep 17 00:00:00 2001 From: Anam Navied Date: Thu, 6 Mar 2025 13:39:24 -0500 Subject: [PATCH 10/11] fix test missing closing parenthesis --- .../FindPSResourceContainerRegistryServer.Tests.ps1 | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 index ef3d1b7d2..5b2de751b 100644 --- a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 +++ b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 @@ -285,6 +285,11 @@ Describe 'Test Find-PSResource for MAR Repository' -tags 'CI' { $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.Dependencies.Length | Should -Not -Be 0 + } + It "Should find resource with wildcard in Name" { $res = Find-PSResource -Name "Az.App*" -Repository "MAR" $res | Should -Not -BeNullOrEmpty @@ -295,9 +300,6 @@ Describe 'Test Find-PSResource for MAR Repository' -tags 'CI' { $res = Find-PSResource -Name "*" -Repository "MAR" $res | Should -Not -BeNullOrEmpty $res.Count | Should -BeGreaterThan 1 - 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.Dependencies.Length | Should -Not -Be 0 } } From 8a67ea3369da30bfe84c3f56f5e3ea4e2549d365 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Thu, 6 Mar 2025 11:50:51 -0800 Subject: [PATCH 11/11] Update .NET to 8.0.406 (#1802) --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index b832c3a01..77d62424a 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "8.0.404" + "version": "8.0.406" } }