@@ -117,6 +117,8 @@ param (
117
117
$NewTestResourcesRemainingArguments
118
118
)
119
119
120
+ . (Join-Path $PSScriptRoot .. scripts Helpers Resource- Helpers.ps1)
121
+ . $PSScriptRoot / TestResources- Helpers.ps1
120
122
. $PSScriptRoot / SubConfig- Helpers.ps1
121
123
122
124
if (! $ServicePrincipalAuth ) {
@@ -131,272 +133,6 @@ if (!$PSBoundParameters.ContainsKey('ErrorAction')) {
131
133
$ErrorActionPreference = ' Stop'
132
134
}
133
135
134
- function Log ($Message )
135
- {
136
- Write-Host (' {0} - {1}' -f [DateTime ]::Now.ToLongTimeString(), $Message )
137
- }
138
-
139
- # vso commands are specially formatted log lines that are parsed by Azure Pipelines
140
- # to perform additional actions, most commonly marking values as secrets.
141
- # https://docs.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands
142
- function LogVsoCommand ([string ]$message )
143
- {
144
- if (! $CI -or $SuppressVsoCommands ) {
145
- return
146
- }
147
- Write-Host $message
148
- }
149
-
150
- function Retry ([scriptblock ] $Action , [int ] $Attempts = 5 )
151
- {
152
- $attempt = 0
153
- $sleep = 5
154
-
155
- while ($attempt -lt $Attempts ) {
156
- try {
157
- $attempt ++
158
- return $Action.Invoke ()
159
- } catch {
160
- if ($attempt -lt $Attempts ) {
161
- $sleep *= 2
162
-
163
- Write-Warning " Attempt $attempt failed: $_ . Trying again in $sleep seconds..."
164
- Start-Sleep - Seconds $sleep
165
- } else {
166
- throw
167
- }
168
- }
169
- }
170
- }
171
-
172
- # NewServicePrincipalWrapper creates an object from an AAD graph or Microsoft Graph service principal object type.
173
- # This is necessary to work around breaking changes introduced in Az version 7.0.0:
174
- # https://azure.microsoft.com/en-us/updates/update-your-apps-to-use-microsoft-graph-before-30-june-2022/
175
- function NewServicePrincipalWrapper ([string ]$subscription , [string ]$resourceGroup , [string ]$displayName )
176
- {
177
- if ((Get-Module Az.Resources).Version -eq " 5.3.0" ) {
178
- # https://github.com/Azure/azure-powershell/issues/17040
179
- # New-AzAdServicePrincipal calls will fail with:
180
- # "You cannot call a method on a null-valued expression."
181
- Write-Warning " Az.Resources version 5.3.0 is not supported. Please update to >= 5.3.1"
182
- Write-Warning " Update-Module Az.Resources -RequiredVersion 5.3.1"
183
- exit 1
184
- }
185
-
186
- try {
187
- $servicePrincipal = Retry {
188
- New-AzADServicePrincipal - Role " Owner" - Scope " /subscriptions/$SubscriptionId /resourceGroups/$ResourceGroupName " - DisplayName $displayName
189
- }
190
- } catch {
191
- # The underlying error "The directory object quota limit for the Principal has been exceeded" gets overwritten by the module trying
192
- # to call New-AzADApplication with a null object instead of stopping execution, which makes this case hard to diagnose because it prints the following:
193
- # "Cannot bind argument to parameter 'ObjectId' because it is an empty string."
194
- # Provide a more helpful diagnostic prompt to the user if appropriate:
195
- $totalApps = (Get-AzADApplication - OwnedApplication).Length
196
- $msg = " App Registrations owned by you total $totalApps and may exceed the max quota (likely around 135)." + `
197
- " `n Try removing some at https://ms.portal.azure.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/RegisteredApps" + `
198
- " or by running the following command to remove apps created by this script:" + `
199
- " `n Get-AzADApplication -DisplayNameStartsWith '$baseName ' | Remove-AzADApplication" + `
200
- " `n NOTE: You may need to wait for the quota number to be updated after removing unused applications."
201
- Write-Warning $msg
202
- throw
203
- }
204
-
205
- $spPassword = " "
206
- $appId = " "
207
- if (Get-Member - Name " Secret" - InputObject $servicePrincipal - MemberType property) {
208
- Write-Verbose " Using legacy PSADServicePrincipal object type from AAD graph API"
209
- # Secret property exists on PSADServicePrincipal type from AAD graph in Az # module versions < 7.0.0
210
- $spPassword = $servicePrincipal.Secret
211
- $appId = $servicePrincipal.ApplicationId
212
- } else {
213
- if ((Get-Module Az.Resources).Version -eq " 5.1.0" ) {
214
- Write-Verbose " Creating password and credential for service principal via MS Graph API"
215
- Write-Warning " Please update Az.Resources to >= 5.2.0 by running 'Update-Module Az'"
216
- # Microsoft graph objects (Az.Resources version == 5.1.0) do not provision a secret on creation so it must be added separately.
217
- # Submitting a password credential object without specifying a password will result in one being generated on the server side.
218
- $password = New-Object - TypeName " Microsoft.Azure.PowerShell.Cmdlets.Resources.MSGraph.Models.ApiV10.MicrosoftGraphPasswordCredential"
219
- $password.DisplayName = " Password for $displayName "
220
- $credential = Retry { New-AzADSpCredential - PasswordCredentials $password - ServicePrincipalObject $servicePrincipal - ErrorAction ' Stop' }
221
- $spPassword = ConvertTo-SecureString $credential.SecretText - AsPlainText - Force
222
- $appId = $servicePrincipal.AppId
223
- } else {
224
- Write-Verbose " Creating service principal credential via MS Graph API"
225
- # In 5.2.0 the password credential issue was fixed (see https://github.com/Azure/azure-powershell/pull/16690) but the
226
- # parameter set was changed making the above call fail due to a missing ServicePrincipalId parameter.
227
- $credential = Retry { $servicePrincipal | New-AzADSpCredential - ErrorAction ' Stop' }
228
- $spPassword = ConvertTo-SecureString $credential.SecretText - AsPlainText - Force
229
- $appId = $servicePrincipal.AppId
230
- }
231
- }
232
-
233
- return @ {
234
- AppId = $appId
235
- ApplicationId = $appId
236
- # This is the ObjectId/OID but most return objects use .Id so keep it consistent to prevent confusion
237
- Id = $servicePrincipal.Id
238
- DisplayName = $servicePrincipal.DisplayName
239
- Secret = $spPassword
240
- }
241
- }
242
-
243
- function LoadCloudConfig ([string ] $env )
244
- {
245
- $configPath = " $PSScriptRoot /clouds/$env .json"
246
- if (! (Test-Path $configPath )) {
247
- Write-Warning " Could not find cloud configuration for environment '$env '"
248
- return @ {}
249
- }
250
-
251
- $config = Get-Content $configPath | ConvertFrom-Json - AsHashtable
252
- return $config
253
- }
254
-
255
- function MergeHashes ([hashtable ] $source , [psvariable ] $dest )
256
- {
257
- foreach ($key in $source.Keys ) {
258
- if ($dest.Value.Contains ($key ) -and $dest.Value [$key ] -ne $source [$key ]) {
259
- Write-Warning (" Overwriting '$ ( $dest.Name ) .$ ( $key ) ' with value '$ ( $dest.Value [$key ]) ' " +
260
- " to new value '$ ( $source [$key ]) '" )
261
- }
262
- $dest.Value [$key ] = $source [$key ]
263
- }
264
- }
265
-
266
- function BuildBicepFile ([System.IO.FileSystemInfo ] $file )
267
- {
268
- if (! (Get-Command bicep - ErrorAction Ignore)) {
269
- Write-Error " A bicep file was found at '$ ( $file.FullName ) ' but the Azure Bicep CLI is not installed. See aka.ms/bicep-install"
270
- throw
271
- }
272
-
273
- $tmp = $env: TEMP ? $env: TEMP : [System.IO.Path ]::GetTempPath()
274
- $templateFilePath = Join-Path $tmp " $ResourceType -resources.$ ( New-Guid ) .compiled.json"
275
-
276
- # Az can deploy bicep files natively, but by compiling here it becomes easier to parse the
277
- # outputted json for mismatched parameter declarations.
278
- bicep build $file.FullName -- outfile $templateFilePath
279
- if ($LASTEXITCODE ) {
280
- Write-Error " Failure building bicep file '$ ( $file.FullName ) '"
281
- throw
282
- }
283
-
284
- return $templateFilePath
285
- }
286
-
287
- function BuildDeploymentOutputs ([string ]$serviceName , [object ]$azContext , [object ]$deployment , [hashtable ]$environmentVariables ) {
288
- $serviceDirectoryPrefix = BuildServiceDirectoryPrefix $serviceName
289
- # Add default values
290
- $deploymentOutputs = [Ordered ]@ {
291
- " ${serviceDirectoryPrefix} SUBSCRIPTION_ID" = $azContext.Subscription.Id ;
292
- " ${serviceDirectoryPrefix} RESOURCE_GROUP" = $resourceGroup.ResourceGroupName ;
293
- " ${serviceDirectoryPrefix} LOCATION" = $resourceGroup.Location ;
294
- " ${serviceDirectoryPrefix} ENVIRONMENT" = $azContext.Environment.Name ;
295
- " ${serviceDirectoryPrefix} AZURE_AUTHORITY_HOST" = $azContext.Environment.ActiveDirectoryAuthority ;
296
- " ${serviceDirectoryPrefix} RESOURCE_MANAGER_URL" = $azContext.Environment.ResourceManagerUrl ;
297
- " ${serviceDirectoryPrefix} SERVICE_MANAGEMENT_URL" = $azContext.Environment.ServiceManagementUrl ;
298
- " AZURE_SERVICE_DIRECTORY" = $serviceName.ToUpperInvariant ();
299
- }
300
-
301
- if ($ServicePrincipalAuth ) {
302
- $deploymentOutputs [" ${serviceDirectoryPrefix} CLIENT_ID" ] = $TestApplicationId ;
303
- $deploymentOutputs [" ${serviceDirectoryPrefix} CLIENT_SECRET" ] = $TestApplicationSecret ;
304
- $deploymentOutputs [" ${serviceDirectoryPrefix} TENANT_ID" ] = $azContext.Tenant.Id ;
305
- }
306
-
307
- MergeHashes $environmentVariables $ (Get-Variable deploymentOutputs)
308
-
309
- foreach ($key in $deployment.Outputs.Keys ) {
310
- $variable = $deployment.Outputs [$key ]
311
-
312
- # Work around bug that makes the first few characters of environment variables be lowercase.
313
- $key = $key.ToUpperInvariant ()
314
-
315
- if ($variable.Type -eq ' String' -or $variable.Type -eq ' SecureString' ) {
316
- $deploymentOutputs [$key ] = $variable.Value
317
- }
318
- }
319
-
320
- # Force capitalization of all keys to avoid Azure Pipelines confusion with
321
- # variable auto-capitalization and OS env var capitalization differences
322
- $capitalized = @ {}
323
- foreach ($item in $deploymentOutputs.GetEnumerator ()) {
324
- $capitalized [$item.Name.ToUpperInvariant ()] = $item.Value
325
- }
326
-
327
- return $capitalized
328
- }
329
-
330
- function SetDeploymentOutputs (
331
- [string ]$serviceName ,
332
- [object ]$azContext ,
333
- [object ]$deployment ,
334
- [object ]$templateFile ,
335
- [hashtable ]$environmentVariables = @ {}
336
- ) {
337
- $deploymentEnvironmentVariables = $environmentVariables.Clone ()
338
- $deploymentOutputs = BuildDeploymentOutputs $serviceName $azContext $deployment $deploymentEnvironmentVariables
339
-
340
- if ($OutFile ) {
341
- if (! $IsWindows ) {
342
- Write-Host ' File option is supported only on Windows'
343
- }
344
-
345
- $outputFile = " $ ( $templateFile.originalFilePath ) .env"
346
-
347
- $environmentText = $deploymentOutputs | ConvertTo-Json ;
348
- $bytes = [System.Text.Encoding ]::UTF8.GetBytes($environmentText )
349
- $protectedBytes = [Security.Cryptography.ProtectedData ]::Protect($bytes , $null , [Security.Cryptography.DataProtectionScope ]::CurrentUser)
350
-
351
- Set-Content $outputFile - Value $protectedBytes - AsByteStream - Force
352
-
353
- Write-Host " Test environment settings`n $environmentText `n stored into encrypted $outputFile "
354
- } else {
355
- if (! $CI ) {
356
- # Write an extra new line to isolate the environment variables for easy reading.
357
- Log " Persist the following environment variables based on your detected shell ($shell ):`n "
358
- }
359
-
360
- # Write overwrite warnings first, since local execution prints a runnable command to export variables
361
- foreach ($key in $deploymentOutputs.Keys ) {
362
- if ([Environment ]::GetEnvironmentVariable($key )) {
363
- Write-Warning " Deployment outputs will overwrite pre-existing environment variable '$key '"
364
- }
365
- }
366
-
367
- # Marking values as secret by allowed keys below is not sufficient, as there may be outputs set in the ARM/bicep
368
- # file that re-mark those values as secret (since all user-provided deployment outputs are treated as secret by default).
369
- # This variable supports a second check on not marking previously allowed keys/values as secret.
370
- $notSecretValues = @ ()
371
- foreach ($key in $deploymentOutputs.Keys ) {
372
- $value = $deploymentOutputs [$key ]
373
- $deploymentEnvironmentVariables [$key ] = $value
374
-
375
- if ($CI ) {
376
- if (ShouldMarkValueAsSecret $serviceName $key $value $notSecretValues ) {
377
- # Treat all ARM template output variables as secrets since "SecureString" variables do not set values.
378
- # In order to mask secrets but set environment variables for any given ARM template, we set variables twice as shown below.
379
- LogVsoCommand " ##vso[task.setvariable variable=_$key ;issecret=true;]$value "
380
- Write-Host " Setting variable as secret '$key '"
381
- } else {
382
- Write-Host " Setting variable '$key ': $value "
383
- $notSecretValues += $value
384
- }
385
- LogVsoCommand " ##vso[task.setvariable variable=$key ;]$value "
386
- } else {
387
- Write-Host ($shellExportFormat -f $key , $value )
388
- }
389
- }
390
-
391
- if ($key ) {
392
- # Isolate the environment variables for easy reading.
393
- Write-Host " `n "
394
- $key = $null
395
- }
396
- }
397
-
398
- return $deploymentEnvironmentVariables , $deploymentOutputs
399
- }
400
136
401
137
# Support actions to invoke on exit.
402
138
$exitActions = @ ({
@@ -843,31 +579,7 @@ try {
843
579
- templateFile $templateFile `
844
580
- environmentVariables $EnvironmentVariables
845
581
846
- $storageAccounts = Retry { Get-AzResource - ResourceGroupName $ResourceGroupName - ResourceType " Microsoft.Storage/storageAccounts" }
847
- # Add client IP to storage account when running as local user. Pipeline's have their own vnet with access
848
- if ($storageAccounts ) {
849
- foreach ($account in $storageAccounts ) {
850
- $rules = Get-AzStorageAccountNetworkRuleSet - ResourceGroupName $ResourceGroupName - AccountName $account.Name
851
- if ($rules -and $rules.DefaultAction -eq " Allow" ) {
852
- Write-Host " Restricting network rules in storage account '$ ( $account.Name ) ' to deny access by default"
853
- Retry { Update-AzStorageAccountNetworkRuleSet - ResourceGroupName $ResourceGroupName - Name $account.Name - DefaultAction Deny }
854
- if ($CI -and $env: PoolSubnet ) {
855
- Write-Host " Enabling access to '$ ( $account.Name ) ' from pipeline subnet $ ( $env: PoolSubnet ) "
856
- Retry { Add-AzStorageAccountNetworkRule - ResourceGroupName $ResourceGroupName - Name $account.Name - VirtualNetworkResourceId $env: PoolSubnet }
857
- } elseif ($AllowIpRanges ) {
858
- Write-Host " Enabling access to '$ ( $account.Name ) ' to $ ( $AllowIpRanges.Length ) IP ranges"
859
- $ipRanges = $AllowIpRanges | ForEach-Object {
860
- @ { Action = ' allow' ; IPAddressOrRange = $_ }
861
- }
862
- Retry { Update-AzStorageAccountNetworkRuleSet - ResourceGroupName $ResourceGroupName - Name $account.Name - IPRule $ipRanges | Out-Null }
863
- } elseif (! $CI ) {
864
- Write-Host " Enabling access to '$ ( $account.Name ) ' from client IP"
865
- $clientIp ?? = Retry { Invoke-RestMethod - Uri ' https://icanhazip.com/' } # cloudflare owned ip site
866
- Retry { Add-AzStorageAccountNetworkRule - ResourceGroupName $ResourceGroupName - Name $account.Name - IPAddressOrRange $clientIp | Out-Null }
867
- }
868
- }
869
- }
870
- }
582
+ SetResourceNetworkAccessRules - ResourceGroupName $ResourceGroupName - AllowIpRanges $AllowIpRanges - CI:$CI
871
583
872
584
$postDeploymentScript = $templateFile.originalFilePath | Split-Path | Join-Path - ChildPath " $ResourceType -resources-post.ps1"
873
585
if (Test-Path $postDeploymentScript ) {
0 commit comments