8000 Merge branch 'master' of https://github.com/PowerShell/PSResourceGet … · PowerShell/PSResourceGet@8e0dd47 · GitHub
[go: up one dir, main page]

Skip to content

Commit 8e0dd47

Browse files
committed
Merge branch 'master' of https://github.com/PowerShell/PSResourceGet into bugfix-containerregistry-2digitinstall
2 parents f51a8b8 + 9c741e4 commit 8e0dd47

File tree

5 files changed

+227
-23
lines changed

5 files changed

+227
-23
lines changed

CHANGELOG/1.1.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
## [1.1.0](https://github.com/PowerShell/PSResourceGet/compare/v1.1.0-rc3...v1.1.0) - 2025-01-09
2+
3+
### Bug Fixes
4+
5+
- Bugfix for publishing .nupkg file to ContainerRegistry repository (#1763)
6+
- Bugfix for PMPs like Artifactory needing modified filter query parameter to proxy upstream (#1761)
7+
- Bugfix for ContainerRegistry repository to parse out dependencies from metadata (#1766)
8+
- Bugfix for Install-PSResource Null pointer occurring when package is present only in upstream feed in ADO (#1760)
9+
- Bugfix for local repository casing issue on Linux (#1750)
10+
- Update README.md (#1759)
11+
- Bug fix for case sensitive License.txt when RequireLicense is specified (#1757)
12+
- Bug fix for broken -Quiet parameter for Save-PSResource (#1745)

global.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"sdk": {
3-
"version": "8.0.403"
3+
"version": "8.0.404"
44
}
55
}

src/Microsoft.PowerShell.PSResourceGet.psd1

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
'udres')
4747
PrivateData = @{
4848
PSData = @{
49-
Prerelease = 'rc3'
49+
# Prerelease = ''
5050
Tags = @('PackageManagement',
5151
'PSEdition_Desktop',
5252
'PSEdition_Core',
@@ -56,6 +56,18 @@
5656
ProjectUri = 'https://go.microsoft.com/fwlink/?LinkId=828955'
5757
LicenseUri = 'https://go.microsoft.com/fwlink/?LinkId=829061'
5858
ReleaseNotes = @'
59+
## 1.1.0
60+
61+
### Bug Fix
62+
- Bugfix for publishing .nupkg file to ContainerRegistry repository (#1763)
63+
- Bugfix for PMPs like Artifactory needing modified filter query parameter to proxy upstream (#1761)
64+
- Bugfix for ContainerRegistry repository to parse out dependencies from metadata (#1766)
65+
- Bugfix for Install-PSResource Null pointer occurring when package is present only in upstream feed in ADO (#1760)
66+
- Bugfix for local repository casing issue on Linux (#1750)
67+
- Update README.md (#1759)
68+
- Bug fix for case sensitive License.txt when RequireLicense is specified (#1757)
69+
- Bug fix for broken -Quiet parameter for Save-PSResource (#1745)
70+
5971
## 1.1.0-rc3
6072
6173
### Bug Fix

src/code/ContainerRegistryServerAPICalls.cs

Lines changed: 156 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,16 @@ internal class ContainerRegistryServerAPICalls : ServerApiCall
3838
private static readonly FindResults emptyResponseResults = new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: containerRegistryFindResponseType);
3939

4040
const string containerRegistryRefreshTokenTemplate = "grant_type=access_token&service={0}&tenant={1}&access_token={2}"; // 0 - registry, 1 - tenant, 2 - access token
41-
const string containerRegistryAccessTokenTemplate = "grant_type=refresh_token&service={0}&scope=repository:*:*&refresh_token={1}"; // 0 - registry, 1 - refresh token
41+
const string containerRegistryAccessTokenTemplate = "grant_type=refresh_token&service={0}&scope=repository:*:*&scope=registry:catalog:*&refresh_token={1}"; // 0 - registry, 1 - refresh token
4242
const string containerRegistryOAuthExchangeUrlTemplate = "https://{0}/oauth2/exchange"; // 0 - registry
4343
const string containerRegistryOAuthTokenUrlTemplate = "https://{0}/oauth2/token"; // 0 - registry
4444
const string containerRegistryManifestUrlTemplate = "https://{0}/v2/{1}/manifests/{2}"; // 0 - registry, 1 - repo(modulename), 2 - tag(version)
4545
const string containerRegistryBlobDownloadUrlTemplate = "https://{0}/v2/{1}/blobs/{2}"; // 0 - registry, 1 - repo(modulename), 2 - layer digest
4646
const string containerRegistryFindImageVersionUrlTemplate = "https://{0}/v2/{1}/tags/list"; // 0 - registry, 1 - repo(modulename)
4747
const string containerRegistryStartUploadTemplate = "https://{0}/v2/{1}/blobs/uploads/"; // 0 - registry, 1 - packagename
4848
const string containerRegistryEndUploadTemplate = "https://{0}{1}&digest=sha256:{2}"; // 0 - registry, 1 - location, 2 - digest
49+
const string defaultScope = "&scope=repository:*:*&scope=registry:catalog:*";
50+
const string containerRegistryRepositoryListTemplate = "https://{0}/v2/_catalog"; // 0 - registry
4951

5052
#endregion
5153

@@ -76,13 +78,13 @@ public ContainerRegistryServerAPICalls(PSRepositoryInfo repository, PSCmdlet cmd
7678
public override FindResults FindAll(bool includePrerelease, ResourceType type, out ErrorRecord errRecord)
7779
{
7880
_cmdletPassedIn.WriteDebug("In ContainerRegistryServerAPICalls::FindAll()");
79-
errRecord = new ErrorRecord(
80-
new InvalidOperationException($"Find all is not supported for the ContainerRegistry server protocol repository '{Repository.Name}'"),
81-
"FindAllFailure",
82-
ErrorCategory.InvalidOperation,
83-
this);
81+
var findResult = FindPackages("*", includePrerelease, out errRecord);
82+
if (errRecord != null)
83+
{
84+
return emptyResponseResults;
85+
}
8486

85-
return emptyResponseResults;
87+
return findResult;
8688
}
8789

8890
/// <summary>
@@ -161,13 +163,13 @@ public override FindResults FindNameWithTag(string packageName, string[] tags, b
161163
public override FindResults FindNameGlobbing(string packageName, bool includePrerelease, ResourceType type, out ErrorRecord errRecord)
162164
{
163165
_cmdletPassedIn.WriteDebug("In ContainerRegistryServerAPICalls::FindNameGlobbing()");
164-
errRecord = new ErrorRecord(
165-
new InvalidOperationException($"FindNameGlobbing all is not supported for the ContainerRegistry server protocol repository '{Repository.Name}'"),
166-
"FindNameGlobbingFailure",
167-
ErrorCategory.InvalidOperation,
168-
this);
166+
var findResult = FindPackages(packageName, includePrerelease, out errRecord);
167+
if (errRecord != null)
168+
{
169+
return emptyResponseResults;
170+
}
169171

170-
return emptyResponseResults;
172+
return findResult;
171173
}
172174

173175
/// <summary>
@@ -391,12 +393,18 @@ internal string GetContainerRegistryAccessToken(out ErrorRecord errRecord)
391393
}
392394
else
393395
{
394-
bool isRepositoryUnauthenticated = IsContainerRegistryUnauthenticated(Repository.Uri.ToString(), out errRecord);
396+
bool isRepositoryUnauthenticated = IsContainerRegistryUnauthenticated(Repository.Uri.ToString(), out errRecord, out accessToken);
395397
if (errRecord != null)
396398
{
397399
return null;
398400
}
399401

402+
if (!string.IsNullOrEmpty(accessToken))
403+
{
404+
_cmdletPassedIn.WriteVerbose("Anonymous access token retrieved.");
405+
return accessToken;
406+
}
407+
400408
if (!isRepositoryUnauthenticated)
401409
{
402410
accessToken = Utils.GetAzAccessToken();
@@ -436,15 +444,82 @@ internal string GetContainerRegistryAccessToken(out ErrorRecord errRecord)
436444
/// <summary>
437445
/// Checks if container registry repository is unauthenticated.
438446
/// </summary>
439-
internal bool IsContainerRegistryUnauthenticated(string containerRegistyUrl, out ErrorRecord errRecord)
447+
internal bool IsContainerRegistryUnauthenticated(string containerRegistyUrl, out ErrorRecord errRecord, out string anonymousAccessToken)
440448
{
441449
_cmdletPassedIn.WriteDebug("In ContainerRegistryServerAPICalls::IsContainerRegistryUnauthenticated()");
442450
errRecord = null;
451+
anonymousAccessToken = string.Empty;
443452
string endpoint = $"{containerRegistyUrl}/v2/";
444453
HttpResponseMessage response;
445454
try
446455
{
447456
response = _sessionClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, endpoint)).Result;
457+
458+
if (response.StatusCode == HttpStatusCode.Unauthorized)
459+
{
460+
// check if there is a auth challenge header
461+
if (response.Headers.WwwAuthenticate.Count() > 0)
462+
{
463+
var authHeader = response.Headers.WwwAuthenticate.First();
464+
if (authHeader.Scheme == "Bearer")
465+
{
466+
// check if there is a realm
467+
if (authHeader.Parameter.Contains("realm"))
468+
{
469+
// get the realm
470+
var realm = authHeader.Parameter.Split(',')?.Where(x => x.Contains("realm"))?.FirstOrDefault()?.Split('=')[1]?.Trim('"');
471+
// get the service
472+
var service = authHeader.Parameter.Split(',')?.Where(x => x.Contains("service"))?.FirstOrDefault()?.Split('=')[1]?.Trim('"');
473+
474+
if (string.IsNullOrEmpty(realm) || string.IsNullOrEmpty(service))
475+
{
476+
errRecord = new ErrorRecord(
477+
new InvalidOperationException("Failed to get realm or service from the auth challenge header."),
478+
"RegistryUnauthenticationCheckError",
479+
ErrorCategory.InvalidResult,
480+
this);
481+
482+
return false;
483+
}
484+
485+
string content = "grant_type=access_token&service=" + service + defaultScope;
486+
var contentHeaders = new Collection<KeyValuePair<string, string>> { new KeyValuePair<string, string>("Content-Type", "application/x-www-form-urlencoded") };
487+
488+
// get the anonymous access token
489+
var url = $"{realm}?service={service}{defaultScope}";
490+
491+
// we dont check the errorrecord here because we want to return false if we get a 401 and not throw an error
492+
var results = GetHttpResponseJObjectUsingContentHeaders(url, HttpMethod.Get, content, contentHeaders, out _);
493+
494+
if (results == null)
495+
{
496+
_cmdletPassedIn.WriteDebug("Failed to get access token from the realm. results is null.");
497+
return false;
498+
}
499+
500+
if (results["access_token"] == null)
501+
{
502+
_cmdletPassedIn.WriteDebug($"Failed to get access token from the realm. access_token is null. results: {results}");
503+
return false;
504+
}
505+
506+
anonymousAccessToken = results["access_token"].ToString();
507+
_cmdletPassedIn.WriteDebug("Anonymous access token retrieved");
508+
return true;
509+
}
510+
}
511+
}
512+
}
513+
}
514+
catch (HttpRequestException hre)
515+
{
516+
errRecord = new ErrorRecord(
517+
hre,
518+
"RegistryAnonymousAcquireError",
519+
ErrorCategory.ConnectionError,
520+
this);
521+
522+
return false;
448523
}
449524
catch (Exception e)
450525
{
@@ -591,6 +666,20 @@ internal JObject FindContainerRegistryImageTags(string packageName, string versi
591666
return GetHttpResponseJObjectUsingDefaultHeaders(findImageUrl, HttpMethod.Get, defaultHeaders, out errRecord);
592667
}
593668

669+
/// <summary>
670+
/// Helper method to find all packages on container registry
671+
/// </summary>
672+
/// <param name="containerRegistryAccessToken"></param>
673+
/// <param name="errRecord"></param>
674+
/// <returns></returns>
675+
internal JObject FindAllRepositories(string containerRegistryAccessToken, out ErrorRecord errRecord)
676+
{
677+
_cmdletPassedIn.WriteDebug("In ContainerRegistryServerAPICalls::FindAllRepositories()");
678+
string repositoryListUrl = string.Format(containerRegistryRepositoryListTemplate, Registry);
679+
var defaultHeaders = GetDefaultHeaders(containerRegistryAccessToken);
680+
return GetHttpResponseJObjectUsingDefaultHeaders(repositoryListUrl, HttpMethod.Get, defaultHeaders, out errRecord);
681+
}
682+
594683
/// <summary>
595684
/// Get metadata for a package version.
596685
/// </summary>
@@ -1705,12 +1794,63 @@ private string PrependMARPrefix(string packageName)
17051794

17061795
// If the repostitory is MAR and its not a wildcard search, we need to prefix the package name with MAR prefix.
17071796
string updatedPackageName = Repository.IsMARRepository() && packageName.Trim() != "*"
1708-
? string.Concat(prefix, packageName)
1797+
? packageName.StartsWith(prefix) ? packageName : string.Concat(prefix, packageName)
17091798
: packageName;
17101799

17111800
return updatedPackageName;
17121801
}
17131802

1803+
private FindResults FindPackages(string packageName, bool includePrerelease, out ErrorRecord errRecord)
1804+
{
1805+
_cmdletPassedIn.WriteDebug("In ContainerRegistryServerAPICalls::FindPackages()");
1806+
errRecord = null;
1807+
string containerRegistryAccessToken = GetContainerRegistryAccessToken(out errRecord);
1808+
if (errRecord != null)
1809+
{
1810+
return emptyResponseResults;
1811+
}
1812+
1813+
var pkgResult = FindAllRepositories(containerRegistryAccessToken, out errRecord);
1814+
if (errRecord != null)
1815+
{
1816+
return emptyResponseResults;
1817+
}
1818+
1819+
List<Hashtable> repositoriesList = new List<Hashtable>();
1820+
var isMAR = Repository.IsMARRepository();
1821+
1822+
// Convert the list of repositories to a list of hashtables
1823+
foreach (var repository in pkgResult["repositories"].ToList())
1824+
{
1825+
string repositoryName = repository.ToString();
1826+
1827+
if (isMAR && !repositoryName.StartsWith(PSRepositoryInfo.MARPrefix))
1828+
{
1829+
continue;
1830+
}
1831+
1832+
// This remove the 'psresource/' prefix from the repository name for comparison with wildcard.
1833+
string moduleName = repositoryName.StartsWith("psresource/") ? repositoryName.Substring(11) : repositoryName;
1834+
1835+
WildcardPattern wildcardPattern = new WildcardPattern(packageName, WildcardOptions.IgnoreCase);
1836+
1837+
if (!wildcardPattern.IsMatch(moduleName))
1838+
{
1839+
continue;
1840+
}
1841+
1842+
_cmdletPassedIn.WriteDebug($"Found repository: {repositoryName}");
1843+
1844+
repositoriesList.AddRange(FindPackagesWithVersionHelper(repositoryName, VersionType.VersionRange, versionRange: VersionRange.All, requiredVersion: null, includePrerelease, getOnlyLatest: true, out errRecord));
1845+
if (errRecord != null)
1846+
{
1847+
return emptyResponseResults;
1848+
}
1849+
}
1850+
1851+
return new FindResults(stringResponse: new string[] { }, hashtableResponse: repositoriesList.ToArray(), responseType: containerRegistryFindResponseType);
1852+
}
1853+
17141854
#endregion
17151855
}
17161856
}

test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -171,12 +171,11 @@ Describe 'Test HTTP Find-PSResource for ACR Server Protocol' -tags 'CI' {
171171
$err[0].FullyQualifiedErrorId | Should -BeExactly "FindCommandOrDscResourceFailure,Microsoft.PowerShell.PSResourceGet.Cmdlets.FindPSResource"
172172
}
173173

174-
It "Should not find all resources given Name '*'" {
174+
It "Should find all resources given Name '*'" {
175175
# FindAll()
176176
$res = Find-PSResource -Name "*" -Repository $ACRRepoName -ErrorVariable err -ErrorAction SilentlyContinue
177-
$res | Should -BeNullOrEmpty
178-
$err.Count | Should -BeGreaterThan 0
179-
$err[0].FullyQualifiedErrorId | Should -BeExactly "FindAllFailure,Microsoft.PowerShell.PSResourceGet.Cmdlets.FindPSResource"
177+
$res | Should -Not -BeNullOrEmpty
178+
$res.Count | Should -BeGreaterThan 0
180179
}
181180

182181
It "Should find script given Name" {
@@ -267,12 +266,53 @@ Describe 'Test Find-PSResource for MAR Repository' -tags 'CI' {
267266
It "Should find resource given specific Name, Version null" {
268267
$res = Find-PSResource -Name "Az.Accounts" -Repository "MAR"
269268
$res.Name | Should -Be "Az.Accounts"
270-
$res.Version | Should -Be "4.0.0"
269+
$res.Version | Should -BeGreaterThan ([Version]"4.0.0")
271270
}
272271

273272
It "Should find resource and its dependency given specific Name and Version" {
274273
$res = Find-PSResource -Name "Az.Storage" -Version "8.0.0" -Repository "MAR"
275274
$res.Dependencies.Length | Should -Be 1
276275
$res.Dependencies[0].Name | Should -Be "Az.Accounts"
277276
}
277+
278+
It "Should find resource with wildcard in Name" {
279+
$res = Find-PSResource -Name "Az.App*" -Repository "MAR"
280+
$res | Should -Not -BeNullOrEmpty
281+
$res.Count | Should -BeGreaterThan 1
282+
}
283+
284+
It "Should find all resource with wildcard in Name" {
285+
$res = Find-PSResource -Name "*" -Repository "MAR"
286+
$res | Should -Not -BeNullOrEmpty
287+
$res.Count | Should -BeGreaterThan 1
288+
}
289+
}
290+
291+
# Skip this test fo
292+
Describe 'Test Find-PSResource for unauthenticated ACR repository' -tags 'CI' {
293+
BeforeAll {
294+
$skipOnWinPS = $PSVersionTable.PSVersion.Major -eq 5
295+
296+
if (-not $skipOnWinPS) {
297+
Register-PSResourceRepository -Name "Unauthenticated" -Uri "https://psresourcegetnoauth.azurecr.io/" -ApiVersion "ContainerRegistry"
298+
}
299+
}
300+
301+
AfterAll {
302+
if (-not $skipOnWinPS) {
303+
Unregister-PSResourceRepository -Name "Unauthenticated"
304+
}
305+
}
306+
307+
It "Should find resource given specific Name, Version null" {
308+
309+
if ($skipOnWinPS) {
310+
Set-ItResult -Pending -Because "Skipping test on Windows PowerShell"
311+
return
312+
}
313+
314+
$res = Find-PSResource -Name "hello-world" -Repository "Unauthenticated"
315+
$res.Name | Should -Be "hello-world"
316+
$res.Version | Should -Be "5.0.0"
317+
}
278318
}

0 commit comments

Comments
 (0)
0