Skip to content

Commit 7ec6a19

Browse files
Merge pull request #4782 from Francisco-Gamino/managed-dependencies-update
Handle lack of internet connection or if the www.powershellgallery.com is down when creating a PowerShell function app.
2 parents 1822839 + 1f26172 commit 7ec6a19

12 files changed

+392
-18
lines changed

src/WebJobs.Script/FileProvisioning/FuncAppFileProvisionerFactory.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,19 @@
33

44
using System;
55
using Microsoft.Azure.WebJobs.Script.FileProvisioning.PowerShell;
6+
using Microsoft.Extensions.Logging;
67

78
namespace Microsoft.Azure.WebJobs.Script.FileProvisioning
89
{
910
internal class FuncAppFileProvisionerFactory : IFuncAppFileProvisionerFactory
1011
{
12+
private readonly ILoggerFactory _loggerFactory;
13+
14+
public FuncAppFileProvisionerFactory(ILoggerFactory loggerFactory)
15+
{
16+
_loggerFactory = loggerFactory;
17+
}
18+
1119
public IFuncAppFileProvisioner CreatFileProvisioner(string runtime)
1220
{
1321
if (string.IsNullOrWhiteSpace(runtime))
@@ -18,7 +26,7 @@ public IFuncAppFileProvisioner CreatFileProvisioner(string runtime)
1826
switch (runtime.ToLowerInvariant())
1927
{
2028
case "powershell":
21-
return new PowerShellFileProvisioner();
29+
return new PowerShellFileProvisioner(_loggerFactory);
2230
default:
2331
return null;
2432
}

src/WebJobs.Script/FileProvisioning/PowerShell/PowerShellFileProvisioner.cs

Lines changed: 159 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,37 @@
33

44
using System;
55
using System.IO;
6+
using System.Net.Http;
7+
using System.Text.RegularExpressions;
68
using System.Threading.Tasks;
7-
using Newtonsoft.Json;
8-
using Newtonsoft.Json.Linq;
9+
using System.Xml;
10+
using Microsoft.Extensions.Logging;
911

1012
namespace Microsoft.Azure.WebJobs.Script.FileProvisioning.PowerShell
1113
{
1214
internal class PowerShellFileProvisioner : IFuncAppFileProvisioner
1315
{
16+
private const string AzModuleName = "Az";
17+
private const string PowerShellGalleryFindPackagesByIdUri = "https://www.powershellgallery.com/api/v2/FindPackagesById()?id=";
18+
19+
private const string ProfilePs1FileName = "profile.ps1";
20+
private const string RequirementsPsd1FileName = "requirements.psd1";
21+
22+
private const string RequirementsPsd1ResourceFileName = "Microsoft.Azure.WebJobs.Script.FileProvisioning.PowerShell.requirements.psd1";
23+
private const string ProfilePs1ResourceFileName = "Microsoft.Azure.WebJobs.Script.FileProvisioning.PowerShell.profile.ps1";
24+
25+
private readonly ILogger _logger;
26+
27+
public PowerShellFileProvisioner(ILoggerFactory loggerFactory)
28+
{
29+
if (loggerFactory == null)
30+
{
31+
throw new ArgumentNullException(nameof(loggerFactory));
32+
}
33+
34+
_logger = loggerFactory.CreateLogger<FuncAppFileProvisionerFactory>();
35+
}
36+
1437
/// <summary>
1538
/// Adds the required files to the function app
1639
/// </summary>
@@ -30,22 +53,151 @@ public Task ProvisionFiles(string scriptRootPath)
3053

3154
private void AddRequirementsFile(string scriptRootPath)
3255
{
33-
string requirementsFilePath = Path.Combine(scriptRootPath, "requirements.psd1");
56+
string requirementsFilePath = Path.Combine(scriptRootPath, RequirementsPsd1FileName);
57+
3458
if (!File.Exists(requirementsFilePath))
3559
{
36-
string content = FileUtility.ReadResourceString($"Microsoft.Azure.WebJobs.Script.FileProvisioning.PowerShell.requirements.psd1");
37-
File.WriteAllText(requirementsFilePath, content);
60+
_logger.LogDebug($"Creating {RequirementsPsd1FileName} at {scriptRootPath}");
61+
62+
string requirementsContent = FileUtility.ReadResourceString(RequirementsPsd1ResourceFileName);
63+
string guidance = null;
64+
65+
try
66+
{
67+
string majorVersion = GetLatestAzModuleMajorVersion();
68+
69+
requirementsContent = Regex.Replace(requirementsContent, @"#(\s?)'Az'", "'Az'");
70+
requirementsContent = Regex.Replace(requirementsContent, "MAJOR_VERSION", majorVersion);
71+
}
72+
catch
73+
{
74+
guidance = "Uncomment the next line and replace the MAJOR_VERSION, e.g., 'Az' = '2.*'";
75+
_logger.LogDebug($"Failed to get Az module version. Edit the {RequirementsPsd1FileName} file when the powershellgallery.com is accessible.");
76+
}
77+
78+
requirementsContent = Regex.Replace(requirementsContent, "GUIDANCE", guidance ?? string.Empty);
79+
File.WriteAllText(requirementsFilePath, requirementsContent);
80+
81+
_logger.LogDebug($"{RequirementsPsd1FileName} created sucessfully.");
3882
}
3983
}
4084

4185
private void AddProfileFile(string scriptRootPath)
4286
{
43-
string profileFilePath = Path.Combine(scriptRootPath, "profile.ps1");
87+
string profileFilePath = Path.Combine(scriptRootPath, ProfilePs1FileName);
88+
4489
if (!File.Exists(profileFilePath))
4590
{
46-
string content = FileUtility.ReadResourceString($"Microsoft.Azure.WebJobs.Script.FileProvisioning.PowerShell.profile.ps1");
91+
_logger.LogDebug($"Creating {ProfilePs1FileName} at {scriptRootPath}");
92+
93+
string content = FileUtility.ReadResourceString(ProfilePs1ResourceFileName);
4794
File.WriteAllText(profileFilePath, content);
95+
96+
_logger.LogDebug($"{ProfilePs1FileName} created sucessfully.");
4897
}
4998
}
99+
100+
protected virtual string GetLatestAzModuleMajorVersion()
101+
{
102+
Uri address = new Uri($"{PowerShellGalleryFindPackagesByIdUri}'{AzModuleName}'");
103+
104+
Stream stream = null;
105+
bool throwException = false;
106+
string latestMajorVersion = null;
107+
108+
var retryCount = 3;
109+
while (true)
110+
{
111+
using (var client = new HttpClient())
112+
{
113+
try
114+
{
115+
var response = client.GetAsync(address).Result;
116+
117+
// Throw if not a successful request
118+
response.EnsureSuccessStatusCode();
119+
120+
stream = response.Content.ReadAsStreamAsync().Result;
121+
break;
122+
}
123+
catch (Exception)
124+
{
125+
if (retryCount <= 0)
126+
{
127+
throw;
128+
}
129+
130+
retryCount--;
131+
}
132+
}
133+
}
134+
135+
if (stream == null)
136+
{
137+
throwException = true;
138+
}
139+
else
140+
{
141+
latestMajorVersion = GetModuleMajorVersion(stream);
142+
}
143+
144+
// If we could not find the latest module version, error out.
145+
if (throwException || string.IsNullOrEmpty(latestMajorVersion))
146+
{
147+
throw new Exception($@"Failed to get module version for {AzModuleName}.");
148+
}
149+
150+
return latestMajorVersion;
151+
}
152+
153+
protected internal string GetModuleMajorVersion(Stream stream)
154+
{
155+
if (stream == null)
156+
{
157+
throw new ArgumentNullException(nameof(stream));
158+
}
159+
160+
// Load up the XML response
161+
XmlDocument doc = new XmlDocument();
162+
using (XmlReader reader = XmlReader.Create(stream))
163+
{
164+
doc.Load(reader);
165+
}
166+
167+
const string AtomPrefix = "ps";
168+
const string AtomUri = "http://www.w3.org/2005/Atom";
169+
const string DataServicePrefix = "d";
170+
const string DataServiceUri = "http://schemas.microsoft.com/ado/2007/08/dataservices";
171+
const string MetadaPrefix = "m";
172+
const string MetadataUri = "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata";
173+
const string XPathExpression = "//m:properties[d:IsPrerelease = \"false\"]/d:Version";
174+
175+
// Add the namespaces for the gallery xml content
176+
XmlNamespaceManager nsmgr = new XmlNamespaceManager(doc.NameTable);
177+
nsmgr.AddNamespace(AtomPrefix, AtomUri);
178+
nsmgr.AddNamespace(DataServicePrefix, DataServiceUri);
179+
nsmgr.AddNamespace(MetadaPrefix, MetadataUri);
180+
181+
// Find the version information
182+
XmlNode root = doc.DocumentElement;
183+
var props = root.SelectNodes(XPathExpression, nsmgr);
184+
185+
Version latestVersion = null;
186+
187+
if (props != null && props.Count > 0)
188+
{
189+
foreach (XmlNode prop in props)
190+
{
191+
Version.TryParse(prop.FirstChild.Value, out var currentVersion);
192+
193+
if (latestVersion == null || currentVersion > latestVersion)
194+
{
195+
latestVersion = currentVersion;
196+
}
197+
}
198+
}
199+
200+
return latestVersion?.ToString().Split('.')[0];
201+
}
50202
}
51203
}
Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1-
@{
2-
Az = '2.*'
1+
# This file enables modules to be automatically managed by the Functions service.
2+
# See https://aka.ms/functionsmanageddependency for additional information.
3+
#
4+
@{
5+
# For latest supported version, go to 'https://www.powershellgallery.com/packages/Az'. GUIDANCE
6+
# 'Az' = 'MAJOR_VERSION.*'
37
}

test/WebJobs.Script.Tests/FileProvisioning/FuncAppFileProvisionerFactoryTests.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,22 @@
44
using System;
55
using Microsoft.Azure.WebJobs.Script.FileProvisioning;
66
using Microsoft.Azure.WebJobs.Script.FileProvisioning.PowerShell;
7+
using Microsoft.Extensions.Logging;
8+
using Microsoft.Extensions.Logging.Abstractions;
9+
using Moq;
710
using Xunit;
811

912
namespace Microsoft.Azure.WebJobs.Script.Tests.FileAugmentation
1013
{
1114
public class FuncAppFileProvisionerFactoryTests
1215
{
1316
private readonly IFuncAppFileProvisionerFactory _funcAppFileProvisionerFactory;
17+
private readonly ILoggerFactory _loggerFactory;
1418

1519
public FuncAppFileProvisionerFactoryTests()
1620
{
17-
_funcAppFileProvisionerFactory = new FuncAppFileProvisionerFactory();
21+
_loggerFactory = new LoggerFactory();
22+
_funcAppFileProvisionerFactory = new FuncAppFileProvisionerFactory(_loggerFactory);
1823
}
1924

2025
[Theory]

test/WebJobs.Script.Tests/FileProvisioning/FuncAppFileProvisioningServiceTests.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
using System.Threading.Tasks;
88
using Microsoft.Azure.WebJobs.Script.FileProvisioning;
99
using Microsoft.Azure.WebJobs.Script.Rpc;
10+
using Microsoft.Extensions.Logging;
11+
using Microsoft.Extensions.Logging.Abstractions;
1012
using Microsoft.Extensions.Options;
1113
using Xunit;
1214

@@ -16,6 +18,7 @@ public class FuncAppFileProvisioningServiceTests
1618
{
1719
private readonly IOptionsMonitor<ScriptApplicationHostOptions> _optionsMonitor;
1820
private readonly IFuncAppFileProvisionerFactory _funcAppFileProvisionerFactory;
21+
private readonly ILoggerFactory _loggerFactory;
1922
private readonly IEnvironment _environment;
2023
private readonly string _scriptRootPath;
2124
private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
@@ -30,7 +33,8 @@ public FuncAppFileProvisioningServiceTests()
3033

3134
_optionsMonitor = TestHelpers.CreateOptionsMonitor(applicationHostOptions);
3235
_environment = new TestEnvironment();
33-
_funcAppFileProvisionerFactory = new FuncAppFileProvisionerFactory();
36+
_loggerFactory = new LoggerFactory();
37+
_funcAppFileProvisionerFactory = new FuncAppFileProvisionerFactory(_loggerFactory);
3438
}
3539

3640
[Fact]

0 commit comments

Comments
 (0)