Skip to content

Commit 5d39f79

Browse files
authored
Managed Dependencies: handle paginated PSGallery response (#313)
Handle paginated PSGallery response
1 parent e37e853 commit 5d39f79

File tree

4 files changed

+431
-43
lines changed

4 files changed

+431
-43
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//
2+
// Copyright (c) Microsoft. All rights reserved.
3+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
4+
//
5+
6+
namespace Microsoft.Azure.Functions.PowerShellWorker.DependencyManagement
7+
{
8+
using System;
9+
using System.IO;
10+
11+
internal interface IPowerShellGallerySearchInvoker
12+
{
13+
Stream Invoke(Uri uri);
14+
}
15+
}

src/DependencyManagement/PowerShellGalleryModuleProvider.cs

Lines changed: 49 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,21 @@
66
namespace Microsoft.Azure.Functions.PowerShellWorker.DependencyManagement
77
{
88
using System;
9-
using System.IO;
109
using System.Management.Automation;
11-
using System.Net.Http;
1210
using System.Xml;
1311

1412
using Microsoft.Azure.Functions.PowerShellWorker.PowerShell;
1513
using Microsoft.Azure.Functions.PowerShellWorker.Utility;
1614

1715
internal class PowerShellGalleryModuleProvider : IModuleProvider
1816
{
17+
private readonly IPowerShellGallerySearchInvoker _searchInvoker;
18+
19+
public PowerShellGalleryModuleProvider(IPowerShellGallerySearchInvoker searchInvoker = null)
20+
{
21+
_searchInvoker = searchInvoker ?? new PowerShellGallerySearchInvoker();
22+
}
23+
1924
/// <summary>
2025
/// Returns the latest module version from the PSGallery for the given module name and major version.
2126
/// </summary>
@@ -27,38 +32,18 @@ public string GetLatestPublishedModuleVersion(string moduleName, string majorVer
2732

2833
Uri address = new Uri($"{PowerShellGalleryFindPackagesByIdUri}'{moduleName}'");
2934

30-
string latestMajorVersion = null;
31-
Stream stream = null;
35+
var expectedVersionStart = majorVersion + ".";
36+
37+
Version latestVersion = null;
3238

33-
var retryCount = 3;
34-
while (true)
39+
do
3540
{
36-
using (var client = new HttpClient())
41+
var stream = _searchInvoker.Invoke(address);
42+
if (stream == null)
3743
{
38-
try
39-
{
40-
var response = client.GetAsync(address).Result;
41-
42-
// Throw is not a successful request
43-
response.EnsureSuccessStatusCode();
44-
45-
stream = response.Content.ReadAsStreamAsync().Result;
46-
break;
47-
}
48-
catch (Exception)
49-
{
50-
if (retryCount <= 0)
51-
{
52-
throw;
53-
}
54-
55-
retryCount--;
56-
}
44+
break;
5745
}
58-
}
5946

60-
if (stream != null)
61-
{
6247
// Load up the XML response
6348
XmlDocument doc = new XmlDocument();
6449
using (XmlReader reader = XmlReader.Create(stream))
@@ -68,27 +53,20 @@ public string GetLatestPublishedModuleVersion(string moduleName, string majorVer
6853

6954
// Add the namespaces for the gallery xml content
7055
XmlNamespaceManager nsmgr = new XmlNamespaceManager(doc.NameTable);
71-
nsmgr.AddNamespace("ps", "http://www.w3.org/2005/Atom");
56+
nsmgr.AddNamespace("a", "http://www.w3.org/2005/Atom");
7257
nsmgr.AddNamespace("d", "http://schemas.microsoft.com/ado/2007/08/dataservices");
7358
nsmgr.AddNamespace("m", "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata");
7459

75-
// Find the version information
7660
XmlNode root = doc.DocumentElement;
77-
var props = root.SelectNodes("//m:properties[d:IsPrerelease = \"false\"]/d:Version", nsmgr);
61+
latestVersion = GetLatestVersion(root, nsmgr, expectedVersionStart, latestVersion);
7862

79-
if (props != null && props.Count > 0)
80-
{
81-
foreach (XmlNode prop in props)
82-
{
83-
if (prop.FirstChild.Value.StartsWith(majorVersion))
84-
{
85-
latestMajorVersion = prop.FirstChild.Value;
86-
}
87-
}
88-
}
63+
// The response may be paginated. In this case, the current page
64+
// contains a link to the next page.
65+
address = GetNextLink(root, nsmgr);
8966
}
67+
while (address != null);
9068

91-
return latestMajorVersion;
69+
return latestVersion?.ToString();
9270
}
9371

9472
/// <summary>
@@ -125,5 +103,33 @@ public void Cleanup(PowerShell pwsh)
125103
.AddParameter("ErrorAction", "SilentlyContinue")
126104
.InvokeAndClearCommands();
127105
}
106+
107+
private static Version GetLatestVersion(
108+
XmlNode root, XmlNamespaceManager namespaceManager, string expectedVersionStart, Version latestVersion)
109+
{
110+
var versions = root.SelectNodes("/a:feed/a:entry/m:properties[d:IsPrerelease = \"false\"]/d:Version", namespaceManager);
111+
if (versions != null)
112+
{
113+
foreach (XmlNode prop in versions)
114+
{
115+
if (prop.FirstChild.Value.StartsWith(expectedVersionStart)
116+
&& Version.TryParse(prop.FirstChild.Value, out var thisVersion))
117+
{
118+
if (latestVersion == null || thisVersion > latestVersion)
119+
{
120+
latestVersion = thisVersion;
121+
}
122+
}
123+
}
124+
}
125+
126+
return latestVersion;
127+
}
128+
129+
private static Uri GetNextLink(XmlNode root, XmlNamespaceManager namespaceManager)
130+
{
131+
var nextLink = root.SelectNodes("/a:feed/a:link[@rel=\"next\"]/@href", namespaceManager);
132+
return nextLink.Count == 1 ? new Uri(nextLink[0].Value) : null;
133+
}
128134
}
129135
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
//
2+
// Copyright (c) Microsoft. All rights reserved.
3+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
4+
//
5+
6+
namespace Microsoft.Azure.Functions.PowerShellWorker.DependencyManagement
7+
{
8+
using System;
9+
using System.IO;
10+
using System.Net.Http;
11+
12+
internal class PowerShellGallerySearchInvoker : IPowerShellGallerySearchInvoker
13+
{
14+
public Stream Invoke(Uri uri)
15+
{
16+
var retryCount = 3;
17+
while (true)
18+
{
19+
using (var client = new HttpClient())
20+
{
21+
try
22+
{
23+
var response = client.GetAsync(uri).Result;
24+
25+
// Throw is not a successful request
26+
response.EnsureSuccessStatusCode();
27+
28+
return response.Content.ReadAsStreamAsync().Result;
29+
}
30+
catch (Exception)
31+
{
32+
if (retryCount <= 0)
33+
{
34+
throw;
35+
}
36+
37+
retryCount--;
38+
}
39+
}
40+
}
41+
}
42+
}
43+
}

0 commit comments

Comments
 (0)