1 #!/usr/bin/env pwsh
2 
3 # Copyright (c) Microsoft Corporation. All rights reserved.
4 # Licensed under the MIT License.
5 
6 #Requires -Version 6.0
7 #Requires -PSEdition Core
8 #Requires -Modules @{ModuleName='Az.Accounts'; ModuleVersion='1.6.4'}
9 #Requires -Modules @{ModuleName='Az.Resources'; ModuleVersion='1.8.0'}
10 
11 [CmdletBinding(DefaultParameterSetName = 'Default', SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
12 param (
13     # Limit $BaseName to enough characters to be under limit plus prefixes, and https://docs.microsoft.com/azure/architecture/best-practices/resource-naming
14     [Parameter()]
15     [ValidatePattern('^[-a-zA-Z0-9\.\(\)_]{0,80}(?<=[a-zA-Z0-9\(\)])$')]
16     [string] $BaseName,
17 
18     [ValidatePattern('^[-\w\._\(\)]+$')]
19     [string] $ResourceGroupName,
20 
21     [Parameter(Mandatory = $true, Position = 0)]
22     [string] $ServiceDirectory,
23 
24     [Parameter()]
25     [ValidatePattern('^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$')]
26     [string] $TestApplicationId,
27 
28     [Parameter()]
29     [string] $TestApplicationSecret,
30 
31     [Parameter()]
32     [ValidatePattern('^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$')]
33     [string] $TestApplicationOid,
34 
35     [Parameter(ParameterSetName = 'Provisioner', Mandatory = $true)]
36     [ValidateNotNullOrEmpty()]
37     [string] $TenantId,
38 
39     # Azure SDK Developer Playground subscription
40     [Parameter()]
41     [ValidatePattern('^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$')]
42     [string] $SubscriptionId = 'faa080af-c1d8-40ad-9cce-e1a450ca5b57',
43 
44     [Parameter(ParameterSetName = 'Provisioner', Mandatory = $true)]
45     [ValidatePattern('^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$')]
46     [string] $ProvisionerApplicationId,
47 
48     [Parameter(ParameterSetName = 'Provisioner', Mandatory = $true)]
49     [string] $ProvisionerApplicationSecret,
50 
51     [Parameter()]
52     [ValidateRange(0, [int]::MaxValue)]
53     [int] $DeleteAfterHours,
54 
55     [Parameter()]
56     [string] $Location = '',
57 
58     [Parameter()]
59     [ValidateSet('AzureCloud', 'AzureUSGovernment', 'AzureChinaCloud', 'Dogfood')]
60     [string] $Environment = 'AzureCloud',
61 
62     [Parameter()]
63     [hashtable] $ArmTemplateParameters,
64 
65     [Parameter()]
66     [hashtable] $AdditionalParameters,
67 
68     [Parameter()]
69     [ValidateNotNull()]
70     [hashtable] $EnvironmentVariables = @{},
71 
72     [Parameter()]
73     [switch] $CI = ($null -ne $env:SYSTEM_TEAMPROJECTID),
74 
75     [Parameter()]
76     [switch] $Force,
77 
78     [Parameter()]
79     [switch] $OutFile
80 )
81 
82 # By default stop for any error.
83 if (!$PSBoundParameters.ContainsKey('ErrorAction')) {
84     $ErrorActionPreference = 'Stop'
85 }
86 
Log($Message)87 function Log($Message) {
88     Write-Host ('{0} - {1}' -f [DateTime]::Now.ToLongTimeString(), $Message)
89 }
90 
Retry([scriptblock] $Action, [int] $Attempts = 5)91 function Retry([scriptblock] $Action, [int] $Attempts = 5) {
92     $attempt = 0
93     $sleep = 5
94 
95     while ($attempt -lt $Attempts) {
96         try {
97             $attempt++
98             return $Action.Invoke()
99         } catch {
100             if ($attempt -lt $Attempts) {
101                 $sleep *= 2
102 
103                 Write-Warning "Attempt $attempt failed: $_. Trying again in $sleep seconds..."
104                 Start-Sleep -Seconds $sleep
105             } else {
106                 Write-Error -ErrorRecord $_
107             }
108         }
109     }
110 }
111 
MergeHashes([hashtable] $source, [psvariable] $dest)112 function MergeHashes([hashtable] $source, [psvariable] $dest) {
113     foreach ($key in $source.Keys) {
114         if ($dest.Value.ContainsKey($key) -and $dest.Value[$key] -ne $source[$key]) {
115             Write-Warning ("Overwriting '$($dest.Name).$($key)' with value '$($dest.Value[$key])' " +
116                           "to new value '$($source[$key])'")
117         }
118         $dest.Value[$key] = $source[$key]
119     }
120 }
121 
122 # Support actions to invoke on exit.
123 $exitActions = @({
124     if ($exitActions.Count -gt 1) {
125         Write-Verbose 'Running registered exit actions'
126     }
127 })
128 
129 New-Variable -Name 'initialContext' -Value (Get-AzContext) -Option Constant
130 if ($initialContext) {
131     $exitActions += {
132         Write-Verbose "Restoring initial context: $($initialContext.Account)"
133         $null = $initialContext | Select-AzContext
134     }
135 }
136 
137 # try..finally will also trap Ctrl+C.
138 try {
139 
140     # Enumerate test resources to deploy. Fail if none found.
141     $repositoryRoot = "$PSScriptRoot/../../.." | Resolve-Path
142     $root = [System.IO.Path]::Combine($repositoryRoot, "sdk", $ServiceDirectory) | Resolve-Path
143     $templateFileName = 'test-resources.json'
144     $templateFiles = @()
145 
146     Write-Verbose "Checking for '$templateFileName' files under '$root'"
147     Get-ChildItem -Path $root -Filter $templateFileName -Recurse | ForEach-Object {
148         $templateFile = $_.FullName
149 
150         Write-Verbose "Found template '$templateFile'"
151         $templateFiles += $templateFile
152     }
153 
154     if (!$templateFiles) {
155         Write-Warning -Message "No template files found under '$root'"
156         exit
157     }
158 
159     $UserName =  if ($env:USER) { $env:USER } else { "${env:USERNAME}" }
160     # Remove spaces, etc. that may be in $UserName
161     $UserName = $UserName -replace '\W'
162 
163     # Make sure $BaseName is set.
164     if ($CI) {
165         $BaseName = 't' + (New-Guid).ToString('n').Substring(0, 16)
166         Log "Generated base name '$BaseName' for CI build"
167     } elseif (!$BaseName) {
168         $BaseName = "$UserName$ServiceDirectory"
169         Log "BaseName was not set. Using default base name: '$BaseName'"
170     }
171 
172     # Make sure pre- and post-scripts are passed formerly required arguments.
173     $PSBoundParameters['BaseName'] = $BaseName
174 
175     # Try detecting repos that support OutFile and defaulting to it
176     if (!$CI -and !$PSBoundParameters.ContainsKey('OutFile') -and $IsWindows) {
177         # TODO: find a better way to detect the language
178         if (Test-Path "$repositoryRoot/eng/service.proj") {
179             $OutFile = $true
180             Log "Detected .NET repository. Defaulting OutFile to true. Test environment settings would be stored into the file so you don't need to set environment variables manually."
181         }
182     }
183 
184     # If no location is specified use safe default locations for the given
185     # environment. If no matching environment is found $Location remains an empty
186     # string.
187     if (!$Location) {
188         $Location = @{
189             'AzureCloud' = 'westus2';
190             'AzureUSGovernment' = 'usgovvirginia';
191             'AzureChinaCloud' = 'chinaeast2';
192             'Dogfood' = 'westus'
193         }[$Environment]
194 
195         Write-Verbose "Location was not set. Using default location for environment: '$Location'"
196     }
197 
198     if (!$CI) {
199 
200         # Make sure the user is logged in to create a service principal.
201         $context = Get-AzContext;
202         if (!$context) {
203             $subscriptionName = $SubscriptionId
204 
205             # Use cache of well-known team subs without having to be authenticated.
206             $wellKnownSubscriptions = @{
207                 'faa080af-c1d8-40ad-9cce-e1a450ca5b57' = 'Azure SDK Developer Playground'
208                 'a18897a6-7e44-457d-9260-f2854c0aca42' = 'Azure SDK Engineering System'
209                 '2cd617ea-1866-46b1-90e3-fffb087ebf9b' = 'Azure SDK Test Resources'
210             }
211 
212             if ($wellKnownSubscriptions.ContainsKey($SubscriptionId)) {
213                 $subscriptionName = '{0} ({1})' -f $wellKnownSubscriptions[$SubscriptionId], $SubscriptionId
214             }
215 
216             Log "You are not logged in; connecting to $subscriptionName"
217             $context = (Connect-AzAccount -Subscription $SubscriptionId).Context
218         }
219 
220         # If no test application ID is specified during an interactive session, create a new service principal.
221         if (!$TestApplicationId) {
222 
223             # Cache the created service principal in this session for frequent reuse.
224             $servicePrincipal = if ($AzureTestPrincipal -and (Get-AzADServicePrincipal -ApplicationId $AzureTestPrincipal.ApplicationId)) {
225                 Log "TestApplicationId was not specified; loading cached service principal '$($AzureTestPrincipal.ApplicationId)'"
226                 $AzureTestPrincipal
227             } else {
228                 Log 'TestApplicationId was not specified; creating a new service principal'
229                 $global:AzureTestPrincipal = New-AzADServicePrincipal -Role Owner
230 
231                 Log "Created service principal '$AzureTestPrincipal'"
232                 $AzureTestPrincipal
233             }
234 
235             $TestApplicationId = $servicePrincipal.ApplicationId
236             $TestApplicationSecret = (ConvertFrom-SecureString $servicePrincipal.Secret -AsPlainText);
237 
238             # Make sure pre- and post-scripts are passed formerly required arguments.
239             $PSBoundParameters['TestApplicationId'] = $TestApplicationId
240             $PSBoundParameters['TestApplicationSecret'] = $TestApplicationSecret
241         }
242 
243         if (!$ProvisionerApplicationId) {
244             $ProvisionerApplicationId = $TestApplicationId
245             $ProvisionerApplicationSecret = $TestApplicationSecret
246             $TenantId = $context.Tenant.Id
247         }
248     }
249 
250     # Log in as and run pre- and post-scripts as the provisioner service principal.
251     if ($ProvisionerApplicationId) {
252         $null = Disable-AzContextAutosave -Scope Process
253 
254         Log "Logging into service principal '$ProvisionerApplicationId'"
255         $provisionerSecret = ConvertTo-SecureString -String $ProvisionerApplicationSecret -AsPlainText -Force
256         $provisionerCredential = [System.Management.Automation.PSCredential]::new($ProvisionerApplicationId, $provisionerSecret)
257 
258         # Use the given subscription ID if provided.
259         $subscriptionArgs = if ($SubscriptionId) {
260             @{SubscriptionId = $SubscriptionId}
261         } else {
262             @{}
263         }
264 
265         $provisionerAccount = Retry {
266             Connect-AzAccount -Force:$Force -Tenant $TenantId -Credential $provisionerCredential -ServicePrincipal -Environment $Environment @subscriptionArgs
267         }
268 
269         $exitActions += {
270             Write-Verbose "Logging out of service principal '$($provisionerAccount.Context.Account)'"
271 
272             # Only attempt to disconnect if the -WhatIf flag was not set. Otherwise, this call is not necessary and will fail.
273             if ($PSCmdlet.ShouldProcess($ProvisionerApplicationId)) {
274                 $null = Disconnect-AzAccount -AzureContext $provisionerAccount.Context
275             }
276         }
277     }
278 
279     # Get test application OID from ID if not already provided.
280     if ($TestApplicationId -and !$TestApplicationOid) {
281         $testServicePrincipal = Retry {
282             Get-AzADServicePrincipal -ApplicationId $TestApplicationId
283         }
284 
285         if ($testServicePrincipal -and $testServicePrincipal.Id) {
286             $script:TestApplicationOid = $testServicePrincipal.Id
287         }
288     }
289 
290     # Determine the Azure context that the script is running in.
291     $context = Get-AzContext;
292 
293     # If the ServiceDirectory is an absolute path use the last directory name
294     # (e.g. D:\foo\bar\ -> bar)
295     $serviceName = if (Split-Path -IsAbsolute  $ServiceDirectory) {
296         Split-Path -Leaf $ServiceDirectory
297     } else {
298         $ServiceDirectory
299     }
300 
301     $ResourceGroupName = if ($ResourceGroupName) {
302         $ResourceGroupName
303     } elseif ($CI) {
304         # Format the resource group name based on resource group naming recommendations and limitations.
305         "rg-{0}-$BaseName" -f ($serviceName -replace '[\\\/:]', '-').Substring(0, [Math]::Min($serviceName.Length, 90 - $BaseName.Length - 4)).Trim('-')
306     } else {
307         "rg-$BaseName"
308     }
309 
310     # Tag the resource group to be deleted after a certain number of hours if specified.
311     $tags = @{
312         Creator = $UserName
313         ServiceDirectory = $ServiceDirectory
314     }
315 
316     if ($PSBoundParameters.ContainsKey('DeleteAfterHours')) {
317         $deleteAfter = [DateTime]::UtcNow.AddHours($DeleteAfterHours)
318         $tags.Add('DeleteAfter', $deleteAfter.ToString('o'))
319     }
320 
321     if ($CI) {
322         # Add tags for the current CI job.
323         $tags += @{
324             BuildId = "${env:BUILD_BUILDID}"
325             BuildJob = "${env:AGENT_JOBNAME}"
326             BuildNumber = "${env:BUILD_BUILDNUMBER}"
327             BuildReason = "${env:BUILD_REASON}"
328         }
329 
330         # Set the resource group name variable.
331         Write-Host "Setting variable 'AZURE_RESOURCEGROUP_NAME': $ResourceGroupName"
332         Write-Host "##vso[task.setvariable variable=AZURE_RESOURCEGROUP_NAME;]$ResourceGroupName"
333         if ($EnvironmentVariables.ContainsKey('AZURE_RESOURCEGROUP_NAME') -and `
334             $EnvironmentVariables['AZURE_RESOURCEGROUP_NAME'] -ne $ResourceGroupName)
335         {
336             Write-Warning ("Overwriting 'EnvironmentVariables.AZURE_RESOURCEGROUP_NAME' with value " +
337                 "'$($EnvironmentVariables['AZURE_RESOURCEGROUP_NAME'])' " + "to new value '$($ResourceGroupName)'")
338         }
339         $EnvironmentVariables['AZURE_RESOURCEGROUP_NAME'] = $ResourceGroupName
340     }
341 
342     Log "Creating resource group '$ResourceGroupName' in location '$Location'"
343     $resourceGroup = Retry {
344         New-AzResourceGroup -Name "$ResourceGroupName" -Location $Location -Tag $tags -Force:$Force
345     }
346 
347     if ($resourceGroup.ProvisioningState -eq 'Succeeded') {
348         # New-AzResourceGroup would've written an error and stopped the pipeline by default anyway.
349         Write-Verbose "Successfully created resource group '$($resourceGroup.ResourceGroupName)'"
350     }
351     elseif (!$resourceGroup -and !$PSCmdlet.ShouldProcess($resourceGroupName)) {
352         # If the -WhatIf flag was passed, there will be no resource group created. Fake it.
353         $resourceGroup = [PSCustomObject]@{
354             ResourceGroupName = $resourceGroupName
355             Location = $Location
356         }
357     }
358 
359     # Populate the template parameters and merge any additional specified.
360     $templateParameters = @{
361         baseName = $BaseName
362         testApplicationId = $TestApplicationId
363         testApplicationOid = "$TestApplicationOid"
364     }
365 
366     if ($TenantId) {
367         $templateParameters.Add('tenantId', $TenantId)
368     }
369     if ($TestApplicationSecret) {
370         $templateParameters.Add('testApplicationSecret', $TestApplicationSecret)
371     }
372 
373     MergeHashes $ArmTemplateParameters $(Get-Variable templateParameters)
374     MergeHashes $AdditionalParameters $(Get-Variable templateParameters)
375 
376     # Include environment-specific parameters only if not already provided as part of the "ArmTemplateParameters"
377     if (($context.Environment.StorageEndpointSuffix) -and (-not ($templateParameters.ContainsKey('storageEndpointSuffix')))) {
378         $templateParameters.Add('storageEndpointSuffix', $context.Environment.StorageEndpointSuffix)
379     }
380 
381     # Try to detect the shell based on the parent process name (e.g. launch via shebang).
382     $shell, $shellExportFormat = if (($parentProcessName = (Get-Process -Id $PID).Parent.ProcessName) -and $parentProcessName -eq 'cmd') {
383         'cmd', 'set {0}={1}'
384     } elseif (@('bash', 'csh', 'tcsh', 'zsh') -contains $parentProcessName) {
385         'shell', 'export {0}={1}'
386     } else {
387         'PowerShell', '${{env:{0}}} = ''{1}'''
388     }
389 
390     # Deploy the templates
391     foreach ($templateFile in $templateFiles) {
392         # Deployment fails if we pass in more parameters than are defined.
393         Write-Verbose "Removing unnecessary parameters from template '$templateFile'"
394         $templateJson = Get-Content -LiteralPath $templateFile | ConvertFrom-Json
395         $templateParameterNames = $templateJson.parameters.PSObject.Properties.Name
396 
397         $templateFileParameters = $templateParameters.Clone()
398         foreach ($key in $templateParameters.Keys) {
399             if ($templateParameterNames -notcontains $key) {
400                 Write-Verbose "Removing unnecessary parameter '$key'"
401                 $templateFileParameters.Remove($key)
402             }
403         }
404 
405         $preDeploymentScript = $templateFile | Split-Path | Join-Path -ChildPath 'test-resources-pre.ps1'
406         if (Test-Path $preDeploymentScript) {
407             Log "Invoking pre-deployment script '$preDeploymentScript'"
408             &$preDeploymentScript -ResourceGroupName $ResourceGroupName @PSBoundParameters
409         }
410 
411         Log "Deploying template '$templateFile' to resource group '$($resourceGroup.ResourceGroupName)'"
412         $deployment = Retry {
413             $lastDebugPreference = $DebugPreference
414             try {
415                 if ($CI) {
416                     $DebugPreference = "Continue"
417                 }
418                 New-AzResourceGroupDeployment -Name $BaseName -ResourceGroupName $resourceGroup.ResourceGroupName -TemplateFile $templateFile -TemplateParameterObject $templateFileParameters
419             } catch {
420                 Write-Output @"
421 #####################################################
422 # For help debugging live test provisioning issues, #
423 # see http://aka.ms/azsdk/engsys/live-test-help,    #
424 #####################################################
425 "@
426                 throw
427             } finally {
428                 $DebugPreference = $lastDebugPreference
429             }
430         }
431 
432         if ($deployment.ProvisioningState -eq 'Succeeded') {
433             # New-AzResourceGroupDeployment would've written an error and stopped the pipeline by default anyway.
434             Write-Verbose "Successfully deployed template '$templateFile' to resource group '$($resourceGroup.ResourceGroupName)'"
435         }
436 
437         $serviceDirectoryPrefix = $serviceName.ToUpperInvariant() + "_"
438 
439         # Add default values
440         $deploymentOutputs = @{
441             "$($serviceDirectoryPrefix)CLIENT_ID" = $TestApplicationId;
442             "$($serviceDirectoryPrefix)CLIENT_SECRET" = $TestApplicationSecret;
443             "$($serviceDirectoryPrefix)TENANT_ID" = $context.Tenant.Id;
444             "$($serviceDirectoryPrefix)SUBSCRIPTION_ID" =  $context.Subscription.Id;
445             "$($serviceDirectoryPrefix)RESOURCE_GROUP" = $resourceGroup.ResourceGroupName;
446             "$($serviceDirectoryPrefix)LOCATION" = $resourceGroup.Location;
447             "$($serviceDirectoryPrefix)ENVIRONMENT" = $context.Environment.Name;
448             "$($serviceDirectoryPrefix)AZURE_AUTHORITY_HOST" = $context.Environment.ActiveDirectoryAuthority;
449             "$($serviceDirectoryPrefix)RESOURCE_MANAGER_URL" = $context.Environment.ResourceManagerUrl;
450             "$($serviceDirectoryPrefix)SERVICE_MANAGEMENT_URL" = $context.Environment.ServiceManagementUrl;
451             "$($serviceDirectoryPrefix)STORAGE_ENDPOINT_SUFFIX" = $context.Environment.StorageEndpointSuffix;
452         }
453 
454         MergeHashes $EnvironmentVariables $(Get-Variable deploymentOutputs)
455 
456         foreach ($key in $deployment.Outputs.Keys) {
457             $variable = $deployment.Outputs[$key]
458 
459             # Work around bug that makes the first few characters of environment variables be lowercase.
460             $key = $key.ToUpperInvariant()
461 
462             if ($variable.Type -eq 'String' -or $variable.Type -eq 'SecureString') {
463                 $deploymentOutputs[$key] = $variable.Value
464             }
465         }
466 
467         if ($OutFile) {
468             if (!$IsWindows) {
469                 Write-Host "File option is supported only on Windows"
470             }
471 
472             $outputFile = "$templateFile.env"
473 
474             $environmentText = $deploymentOutputs | ConvertTo-Json;
475             $bytes = ([System.Text.Encoding]::UTF8).GetBytes($environmentText)
476             $protectedBytes = [Security.Cryptography.ProtectedData]::Protect($bytes, $null, [Security.Cryptography.DataProtectionScope]::CurrentUser)
477 
478             Set-Content $outputFile -Value $protectedBytes -AsByteStream -Force
479 
480             Write-Host "Test environment settings`n $environmentText`nstored into encrypted $outputFile"
481         } else {
482 
483             if (!$CI) {
484                 # Write an extra new line to isolate the environment variables for easy reading.
485                 Log "Persist the following environment variables based on your detected shell ($shell):`n"
486             }
487 
488             foreach ($key in $deploymentOutputs.Keys) {
489                 $value = $deploymentOutputs[$key]
490                 $EnvironmentVariables[$key] = $value
491 
492                 if ($CI) {
493                     # Treat all ARM template output variables as secrets since "SecureString" variables do not set values.
494                     # In order to mask secrets but set environment variables for any given ARM template, we set variables twice as shown below.
495                     Write-Host "Setting variable '$key': ***"
496                     Write-Host "##vso[task.setvariable variable=_$key;issecret=true;]$($value)"
497                     Write-Host "##vso[task.setvariable variable=$key;]$($value)"
498                 } else {
499                     Write-Host ($shellExportFormat -f $key, $value)
500                 }
501             }
502 
503             if ($key) {
504                 # Isolate the environment variables for easy reading.
505                 Write-Host "`n"
506                 $key = $null
507             }
508         }
509 
510         $postDeploymentScript = $templateFile | Split-Path | Join-Path -ChildPath 'test-resources-post.ps1'
511         if (Test-Path $postDeploymentScript) {
512             Log "Invoking post-deployment script '$postDeploymentScript'"
513             &$postDeploymentScript -ResourceGroupName $ResourceGroupName -DeploymentOutputs $deploymentOutputs @PSBoundParameters
514         }
515     }
516 
517 } finally {
518     $exitActions.Invoke()
519 }
520 
521 # Suppress output locally
522 if ($CI) {
523     return $EnvironmentVariables
524 }
525 
526 <#
527 .SYNOPSIS
528 Deploys live test resources defined for a service directory to Azure.
529 
530 .DESCRIPTION
531 Deploys live test resouces specified in test-resources.json files to a resource
532 group.
533 
534 This script searches the directory specified in $ServiceDirectory recursively
535 for files named test-resources.json. All found test-resources.json files will be
536 deployed to the test resource group.
537 
538 If no test-resources.json files are located the script exits without making
539 changes to the Azure environment.
540 
541 A service principal must first be created before this script is run and passed
542 to $TestApplicationId and $TestApplicationSecret. Test resources will grant this
543 service principal access.
544 
545 This script uses credentials already specified in Connect-AzAccount or those
546 specified in $ProvisionerApplicationId and $ProvisionerApplicationSecret.
547 
548 .PARAMETER BaseName
549 A name to use in the resource group and passed to the ARM template as 'baseName'.
550 Limit $BaseName to enough characters to be under limit plus prefixes specified in
551 the ARM template. See also https://docs.microsoft.com/azure/architecture/best-practices/resource-naming
552 
553 Note: The value specified for this parameter will be overriden and generated
554 by New-TestResources.ps1 if $CI is specified.
555 
556 .PARAMETER ResourceGroupName
557 Set this value to deploy directly to a Resource Group that has already been
558 created.
559 
560 .PARAMETER ServiceDirectory
561 A directory under 'sdk' in the repository root - optionally with subdirectories
562 specified - in which to discover ARM templates named 'test-resources.json'.
563 This can also be an absolute path or specify parent directories.
564 
565 .PARAMETER TestApplicationId
566 The AAD Application ID to authenticate the test runner against deployed
567 resources. Passed to the ARM template as 'testApplicationId'.
568 
569 This application is used by the test runner to execute tests against the
570 live test resources.
571 
572 .PARAMETER TestApplicationSecret
573 Optional service principal secret (password) to authenticate the test runner
574 against deployed resources. Passed to the ARM template as
575 'testApplicationSecret'.
576 
577 This application is used by the test runner to execute tests against the
578 live test resources.
579 
580 .PARAMETER TestApplicationOid
581 Service Principal Object ID of the AAD Test application. This is used to assign
582 permissions to the AAD application so it can access tested features on the live
583 test resources (e.g. Role Assignments on resources). It is passed as to the ARM
584 template as 'testApplicationOid'
585 
586 For more information on the relationship between AAD Applications and Service
587 Principals see: https://docs.microsoft.com/azure/active-directory/develop/app-objects-and-service-principals
588 
589 .PARAMETER TenantId
590 The tenant ID of a service principal when a provisioner is specified. The same
591 Tenant ID is used for Test Application and Provisioner Application. This value
592 is passed to the ARM template as 'tenantId'.
593 
594 .PARAMETER SubscriptionId
595 Optional subscription ID to use for new resources when logging in as a
596 provisioner. You can also use Set-AzContext if not provisioning.
597 
598 The default is the Azure SDK Developer Playground subscription ID.
599 
600 .PARAMETER ProvisionerApplicationId
601 The AAD Application ID used to provision test resources when a provisioner is
602 specified.
603 
604 If none is specified New-TestResources.ps1 uses the TestApplicationId.
605 
606 This value is not passed to the ARM template.
607 
608 .PARAMETER ProvisionerApplicationSecret
609 A service principal secret (password) used to provision test resources when a
610 provisioner is specified.
611 
612 If none is specified New-TestResources.ps1 uses the TestApplicationSecret.
613 
614 This value is not passed to the ARM template.
615 
616 .PARAMETER DeleteAfterHours
617 Optional. Positive integer number of hours from the current time to set the
618 'DeleteAfter' tag on the created resource group. The computed value is a
619 timestamp of the form "2020-03-04T09:07:04.3083910Z".
620 
621 If this value is not specified no 'DeleteAfter' tag will be assigned to the
622 created resource group.
623 
624 An optional cleanup process can delete resource groups whose "DeleteAfter"
625 timestamp is less than the current time.
626 
627 This isused for CI automation.
628 
629 .PARAMETER Location
630 Optional location where resources should be created. If left empty, the default
631 is based on the cloud to which the template is being deployed:
632 
633 * AzureCloud -> 'westus2'
634 * AzureUSGovernment -> 'usgovvirginia'
635 * AzureChinaCloud -> 'chinaeast2'
636 * Dogfood -> 'westus'
637 
638 .PARAMETER Environment
639 Name of the cloud environment. The default is the Azure Public Cloud
640 ('AzureCloud')
641 
642 .PARAMETER AdditionalParameters
643 Optional key-value pairs of parameters to pass to the ARM template(s) and pre-post scripts.
644 
645 .PARAMETER ArmTemplateParameters
646 Optional key-value pairs of parameters to pass to the ARM template(s).
647 
648 .PARAMETER EnvironmentVariables
649 Optional key-value pairs of parameters to set as environment variables to the shell.
650 
651 .PARAMETER CI
652 Indicates the script is run as part of a Continuous Integration / Continuous
653 Deployment (CI/CD) build (only Azure Pipelines is currently supported).
654 
655 .PARAMETER Force
656 Force creation of resources instead of being prompted.
657 
658 .PARAMETER OutFile
659 Save test environment settings into a test-resources.json.env file next to test-resources.json. File is protected via DPAPI. Supported only on windows.
660 The environment file would be scoped to the current repository directory.
661 
662 .EXAMPLE
663 Connect-AzAccount -Subscription "REPLACE_WITH_SUBSCRIPTION_ID"
664 New-TestResources.ps1 -ServiceDirectory 'keyvault'
665 
666 Run this in a desktop environment to create new AAD apps and Service Principals
667 that can be used to provision resources and run live tests.
668 
669 Requires PowerShell 7 to use ConvertFrom-SecureString -AsPlainText or convert
670 the SecureString to plaintext by another means.
671 
672 .EXAMPLE
673 New-TestResources.ps1 `
674     -BaseName 'Generated' `
675     -ServiceDirectory '$(ServiceDirectory)' `
676     -TenantId '$(TenantId)' `
677     -ProvisionerApplicationId '$(ProvisionerId)' `
678     -ProvisionerApplicationSecret '$(ProvisionerSecret)' `
679     -TestApplicationId '$(TestAppId)' `
680     -TestApplicationSecret '$(TestAppSecret)' `
681     -DeleteAfterHours 24 `
682     -CI `
683     -Force `
684     -Verbose
685 
686 Run this in an Azure DevOps CI (with approrpiate variables configured) before
687 executing live tests. The script will output variables as secrets (to enable
688 log redaction).
689 
690 #>
691