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