Skip to content

Commit 2d7012f

Browse files
azure-sdkbenbp
andauthored
Sync eng/common directory with azure-sdk-tools for PR 8558 (#36350)
* Support storage network access and worm removal in remove test resources script * Move storage network access script to common resource helpers file * Improve storage container deletion resilience * Plumb through pool variable to live test cleanup template * Add sleep for network rule application --------- Co-authored-by: Ben Broderick Phillips <bebroder@microsoft.com>
1 parent dc40798 commit 2d7012f

File tree

5 files changed

+501
-321
lines changed

5 files changed

+501
-321
lines changed

eng/common/TestResources/New-TestResources.ps1

Lines changed: 3 additions & 291 deletions
Original file line numberDiff line numberDiff line change
@@ -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

122124
if (!$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-
"`nTry 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-
"`nNOTE: 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`nstored 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) {

eng/common/TestResources/Remove-TestResources.ps1

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,19 @@ param (
6161
[Parameter()]
6262
[switch] $ServicePrincipalAuth,
6363

64+
# List of CIDR ranges to add to specific resource firewalls, e.g. @(10.100.0.0/16, 10.200.0.0/16)
65+
[Parameter()]
66+
[ValidateCount(0,399)]
67+
[Validatescript({
68+
foreach ($range in $PSItem) {
69+
if ($range -like '*/31' -or $range -like '*/32') {
70+
throw "Firewall IP Ranges cannot contain a /31 or /32 CIDR"
71+
}
72+
}
73+
return $true
74+
})]
75+
[array] $AllowIpRanges = @(),
76+
6477
[Parameter()]
6578
[switch] $Force,
6679

@@ -69,6 +82,9 @@ param (
6982
$RemoveTestResourcesRemainingArguments
7083
)
7184

85+
. (Join-Path $PSScriptRoot .. scripts Helpers Resource-Helpers.ps1)
86+
. (Join-Path $PSScriptRoot TestResources-Helpers.ps1)
87+
7288
# By default stop for any error.
7389
if (!$PSBoundParameters.ContainsKey('ErrorAction')) {
7490
$ErrorActionPreference = 'Stop'
@@ -241,6 +257,9 @@ $verifyDeleteScript = {
241257
# Get any resources that can be purged after the resource group is deleted coerced into a collection even if empty.
242258
$purgeableResources = Get-PurgeableGroupResources $ResourceGroupName
243259

260+
SetResourceNetworkAccessRules -ResourceGroupName $ResourceGroupName -AllowIpRanges $AllowIpRanges -Override -CI:$CI
261+
Remove-WormStorageAccounts -GroupPrefix $ResourceGroupName -CI:$CI
262+
244263
Log "Deleting resource group '$ResourceGroupName'"
245264
if ($Force -and !$purgeableResources) {
246265
Remove-AzResourceGroup -Name "$ResourceGroupName" -Force:$Force -AsJob

0 commit comments

Comments
 (0)