3
3
4
4
using System ;
5
5
using System . IO ;
6
+ using System . Net . Http ;
7
+ using System . Text . RegularExpressions ;
6
8
using System . Threading . Tasks ;
7
- using Newtonsoft . Json ;
8
- using Newtonsoft . Json . Linq ;
9
+ using System . Xml ;
10
+ using Microsoft . Extensions . Logging ;
9
11
10
12
namespace Microsoft . Azure . WebJobs . Script . FileProvisioning . PowerShell
11
13
{
12
14
internal class PowerShellFileProvisioner : IFuncAppFileProvisioner
13
15
{
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
+
14
37
/// <summary>
15
38
/// Adds the required files to the function app
16
39
/// </summary>
@@ -30,22 +53,151 @@ public Task ProvisionFiles(string scriptRootPath)
30
53
31
54
private void AddRequirementsFile ( string scriptRootPath )
32
55
{
33
- string requirementsFilePath = Path . Combine ( scriptRootPath , "requirements.psd1" ) ;
56
+ string requirementsFilePath = Path . Combine ( scriptRootPath , RequirementsPsd1FileName ) ;
57
+
34
58
if ( ! File . Exists ( requirementsFilePath ) )
35
59
{
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.") ;
38
82
}
39
83
}
40
84
41
85
private void AddProfileFile ( string scriptRootPath )
42
86
{
43
- string profileFilePath = Path . Combine ( scriptRootPath , "profile.ps1" ) ;
87
+ string profileFilePath = Path . Combine ( scriptRootPath , ProfilePs1FileName ) ;
88
+
44
89
if ( ! File . Exists ( profileFilePath ) )
45
90
{
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 ) ;
47
94
File . WriteAllText ( profileFilePath , content ) ;
95
+
96
+ _logger . LogDebug ( $ "{ ProfilePs1FileName } created sucessfully.") ;
48
97
}
49
98
}
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
+ }
50
202
}
51
203
}
0 commit comments