@@ -117,6 +117,8 @@ param (
117117 $NewTestResourcesRemainingArguments
118118)
119119
120+ . (Join-Path $PSScriptRoot .. scripts Helpers Resource- Helpers.ps1)
121+ . $PSScriptRoot / TestResources- Helpers.ps1
120122. $PSScriptRoot / SubConfig- Helpers.ps1
121123
122124if (! $ServicePrincipalAuth ) {
@@ -131,272 +133,6 @@ if (!$PSBoundParameters.ContainsKey('ErrorAction')) {
131133 $ErrorActionPreference = ' Stop'
132134}
133135
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- }
400136
401137# Support actions to invoke on exit.
402138$exitActions = @ ({
@@ -843,31 +579,7 @@ try {
843579 - templateFile $templateFile `
844580 - environmentVariables $EnvironmentVariables
845581
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
871583
872584 $postDeploymentScript = $templateFile.originalFilePath | Split-Path | Join-Path - ChildPath " $ResourceType -resources-post.ps1"
873585 if (Test-Path $postDeploymentScript ) {
0 commit comments