...

Text file src/github.com/Azure/azure-sdk-for-go/eng/common/TestResources/New-TestResources.ps1

Documentation: github.com/Azure/azure-sdk-for-go/eng/common/TestResources

     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')]
    12param (
    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 is assumed if not set
    40    [Parameter()]
    41    [ValidatePattern('^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$')]
    42    [string] $SubscriptionId,
    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 = $false)]
    49    [ValidatePattern('^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$')]
    50    [string] $ProvisionerApplicationOid,
    51
    52    [Parameter(ParameterSetName = 'Provisioner', Mandatory = $true)]
    53    [string] $ProvisionerApplicationSecret,
    54
    55    [Parameter()]
    56    [ValidateRange(1, 7*24)]
    57    [int] $DeleteAfterHours = 120,
    58
    59    [Parameter()]
    60    [string] $Location = '',
    61
    62    [Parameter()]
    63    [ValidateSet('AzureCloud', 'AzureUSGovernment', 'AzureChinaCloud', 'Dogfood')]
    64    [string] $Environment = 'AzureCloud',
    65
    66    [Parameter()]
    67    [hashtable] $ArmTemplateParameters,
    68
    69    [Parameter()]
    70    [hashtable] $AdditionalParameters,
    71
    72    [Parameter()]
    73    [ValidateNotNull()]
    74    [hashtable] $EnvironmentVariables = @{},
    75
    76    [Parameter()]
    77    [switch] $CI = ($null -ne $env:SYSTEM_TEAMPROJECTID),
    78
    79    [Parameter()]
    80    [switch] $Force,
    81
    82    [Parameter()]
    83    [switch] $OutFile,
    84
    85    [Parameter()]
    86    [switch] $SuppressVsoCommands = ($null -eq $env:SYSTEM_TEAMPROJECTID),
    87
    88    # Captures any arguments not declared here (no parameter errors)
    89    # This enables backwards compatibility with old script versions in
    90    # hotfix branches if and when the dynamic subscription configuration
    91    # secrets get updated to add new parameters.
    92    [Parameter(ValueFromRemainingArguments = $true)]
    93    $NewTestResourcesRemainingArguments
    94)
    95
    96. $PSScriptRoot/SubConfig-Helpers.ps1
    97
    98# By default stop for any error.
    99if (!$PSBoundParameters.ContainsKey('ErrorAction')) {
   100    $ErrorActionPreference = 'Stop'
   101}
   102
   103function Log($Message)
   104{
   105    Write-Host ('{0} - {1}' -f [DateTime]::Now.ToLongTimeString(), $Message)
   106}
   107
   108# vso commands are specially formatted log lines that are parsed by Azure Pipelines
   109# to perform additional actions, most commonly marking values as secrets.
   110# https://docs.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands
   111function LogVsoCommand([string]$message)
   112{
   113    if (!$CI -or $SuppressVsoCommands) {
   114        return
   115    }
   116    Write-Host $message
   117}
   118
   119function Retry([scriptblock] $Action, [int] $Attempts = 5)
   120{
   121    $attempt = 0
   122    $sleep = 5
   123
   124    while ($attempt -lt $Attempts) {
   125        try {
   126            $attempt++
   127            return $Action.Invoke()
   128        } catch {
   129            if ($attempt -lt $Attempts) {
   130                $sleep *= 2
   131
   132                Write-Warning "Attempt $attempt failed: $_. Trying again in $sleep seconds..."
   133                Start-Sleep -Seconds $sleep
   134            } else {
   135                Write-Error -ErrorRecord $_
   136            }
   137        }
   138    }
   139}
   140
   141# NewServicePrincipalWrapper creates an object from an AAD graph or Microsoft Graph service principal object type.
   142# This is necessary to work around breaking changes introduced in Az version 7.0.0:
   143# https://azure.microsoft.com/en-us/updates/update-your-apps-to-use-microsoft-graph-before-30-june-2022/
   144function NewServicePrincipalWrapper([string]$subscription, [string]$resourceGroup, [string]$displayName)
   145{
   146    if ((Get-Module Az.Resources).Version -eq "5.3.0") {
   147        # https://github.com/Azure/azure-powershell/issues/17040
   148        # New-AzAdServicePrincipal calls will fail with:
   149        # "You cannot call a method on a null-valued expression."
   150        Write-Warning "Az.Resources version 5.3.0 is not supported. Please update to >= 5.3.1"
   151        Write-Warning "Update-Module Az.Resources -RequiredVersion 5.3.1"
   152        exit 1
   153    }
   154    $servicePrincipal = Retry {
   155        New-AzADServicePrincipal -Role "Owner" -Scope "/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroupName" -DisplayName $displayName
   156    }
   157    $spPassword = ""
   158    $appId = ""
   159    if (Get-Member -Name "Secret" -InputObject $servicePrincipal -MemberType property) {
   160        Write-Verbose "Using legacy PSADServicePrincipal object type from AAD graph API"
   161        # Secret property exists on PSADServicePrincipal type from AAD graph in Az # module versions < 7.0.0
   162        $spPassword = $servicePrincipal.Secret
   163        $appId = $servicePrincipal.ApplicationId
   164    } else {
   165        if ((Get-Module Az.Resources).Version -eq "5.1.0") {
   166            Write-Verbose "Creating password and credential for service principal via MS Graph API"
   167            Write-Warning "Please update Az.Resources to >= 5.2.0 by running 'Update-Module Az'"
   168            # Microsoft graph objects (Az.Resources version == 5.1.0) do not provision a secret on creation so it must be added separately.
   169            # Submitting a password credential object without specifying a password will result in one being generated on the server side.
   170            $password = New-Object -TypeName "Microsoft.Azure.PowerShell.Cmdlets.Resources.MSGraph.Models.ApiV10.MicrosoftGraphPasswordCredential"
   171            $password.DisplayName = "Password for $displayName"
   172            $credential = Retry { New-AzADSpCredential -PasswordCredentials $password -ServicePrincipalObject $servicePrincipal }
   173            $spPassword = ConvertTo-SecureString $credential.SecretText -AsPlainText -Force
   174            $appId = $servicePrincipal.AppId
   175        } else {
   176            Write-Verbose "Creating service principal credential via MS Graph API"
   177            # In 5.2.0 the password credential issue was fixed (see https://github.com/Azure/azure-powershell/pull/16690) but the
   178            # parameter set was changed making the above call fail due to a missing ServicePrincipalId parameter.
   179            $credential = Retry { $servicePrincipal | New-AzADSpCredential }
   180            $spPassword = ConvertTo-SecureString $credential.SecretText -AsPlainText -Force
   181            $appId = $servicePrincipal.AppId
   182        }
   183    }
   184
   185    return @{
   186        AppId = $appId
   187        ApplicationId = $appId
   188        # This is the ObjectId/OID but most return objects use .Id so keep it consistent to prevent confusion
   189        Id = $servicePrincipal.Id
   190        DisplayName = $servicePrincipal.DisplayName
   191        Secret = $spPassword
   192    }
   193}
   194
   195function LoadCloudConfig([string] $env)
   196{
   197    $configPath = "$PSScriptRoot/clouds/$env.json"
   198    if (!(Test-Path $configPath)) {
   199        Write-Warning "Could not find cloud configuration for environment '$env'"
   200        return @{}
   201    }
   202
   203    $config = Get-Content $configPath | ConvertFrom-Json -AsHashtable
   204    return $config
   205}
   206
   207function MergeHashes([hashtable] $source, [psvariable] $dest)
   208{
   209    foreach ($key in $source.Keys) {
   210        if ($dest.Value.Contains($key) -and $dest.Value[$key] -ne $source[$key]) {
   211            Write-Warning ("Overwriting '$($dest.Name).$($key)' with value '$($dest.Value[$key])' " +
   212                          "to new value '$($source[$key])'")
   213        }
   214        $dest.Value[$key] = $source[$key]
   215    }
   216}
   217
   218function BuildBicepFile([System.IO.FileSystemInfo] $file)
   219{
   220    if (!(Get-Command bicep -ErrorAction Ignore)) {
   221        Write-Error "A bicep file was found at '$($file.FullName)' but the Azure Bicep CLI is not installed. See https://aka.ms/install-bicep-pwsh"
   222        throw
   223    }
   224
   225    $tmp = $env:TEMP ? $env:TEMP : [System.IO.Path]::GetTempPath()
   226    $templateFilePath = Join-Path $tmp "test-resources.$(New-Guid).compiled.json"
   227
   228    # Az can deploy bicep files natively, but by compiling here it becomes easier to parse the
   229    # outputted json for mismatched parameter declarations.
   230    bicep build $file.FullName --outfile $templateFilePath
   231    if ($LASTEXITCODE) {
   232        Write-Error "Failure building bicep file '$($file.FullName)'"
   233        throw
   234    }
   235
   236    return $templateFilePath
   237}
   238
   239function BuildDeploymentOutputs([string]$serviceName, [object]$azContext, [object]$deployment) {
   240    $serviceDirectoryPrefix = BuildServiceDirectoryPrefix $serviceName
   241    # Add default values
   242    $deploymentOutputs = [Ordered]@{
   243        "${serviceDirectoryPrefix}CLIENT_ID" = $TestApplicationId;
   244        "${serviceDirectoryPrefix}CLIENT_SECRET" = $TestApplicationSecret;
   245        "${serviceDirectoryPrefix}TENANT_ID" = $azContext.Tenant.Id;
   246        "${serviceDirectoryPrefix}SUBSCRIPTION_ID" =  $azContext.Subscription.Id;
   247        "${serviceDirectoryPrefix}RESOURCE_GROUP" = $resourceGroup.ResourceGroupName;
   248        "${serviceDirectoryPrefix}LOCATION" = $resourceGroup.Location;
   249        "${serviceDirectoryPrefix}ENVIRONMENT" = $azContext.Environment.Name;
   250        "${serviceDirectoryPrefix}AZURE_AUTHORITY_HOST" = $azContext.Environment.ActiveDirectoryAuthority;
   251        "${serviceDirectoryPrefix}RESOURCE_MANAGER_URL" = $azContext.Environment.ResourceManagerUrl;
   252        "${serviceDirectoryPrefix}SERVICE_MANAGEMENT_URL" = $azContext.Environment.ServiceManagementUrl;
   253        "AZURE_SERVICE_DIRECTORY" = $serviceName.ToUpperInvariant();
   254    }
   255
   256    MergeHashes $EnvironmentVariables $(Get-Variable deploymentOutputs)
   257
   258    foreach ($key in $deployment.Outputs.Keys) {
   259        $variable = $deployment.Outputs[$key]
   260
   261        # Work around bug that makes the first few characters of environment variables be lowercase.
   262        $key = $key.ToUpperInvariant()
   263
   264        if ($variable.Type -eq 'String' -or $variable.Type -eq 'SecureString') {
   265            $deploymentOutputs[$key] = $variable.Value
   266        }
   267    }
   268
   269    return $deploymentOutputs
   270}
   271
   272function SetDeploymentOutputs([string]$serviceName, [object]$azContext, [object]$deployment, [object]$templateFile) {
   273    $deploymentOutputs = BuildDeploymentOutputs $serviceName $azContext $deployment
   274
   275    if ($OutFile) {
   276        if (!$IsWindows) {
   277            Write-Host 'File option is supported only on Windows'
   278        }
   279
   280        $outputFile = "$($templateFile.originalFilePath).env"
   281
   282        $environmentText = $deploymentOutputs | ConvertTo-Json;
   283        $bytes = [System.Text.Encoding]::UTF8.GetBytes($environmentText)
   284        $protectedBytes = [Security.Cryptography.ProtectedData]::Protect($bytes, $null, [Security.Cryptography.DataProtectionScope]::CurrentUser)
   285
   286        Set-Content $outputFile -Value $protectedBytes -AsByteStream -Force
   287
   288        Write-Host "Test environment settings`n $environmentText`nstored into encrypted $outputFile"
   289    } else {
   290        if (!$CI) {
   291            # Write an extra new line to isolate the environment variables for easy reading.
   292            Log "Persist the following environment variables based on your detected shell ($shell):`n"
   293        }
   294
   295        # Marking values as secret by allowed keys below is not sufficient, as there may be outputs set in the ARM/bicep
   296        # file that re-mark those values as secret (since all user-provided deployment outputs are treated as secret by default).
   297        # This variable supports a second check on not marking previously allowed keys/values as secret.
   298        $notSecretValues = @()
   299        foreach ($key in $deploymentOutputs.Keys) {
   300            $value = $deploymentOutputs[$key]
   301            $EnvironmentVariables[$key] = $value
   302
   303            if ($CI) {
   304                if (ShouldMarkValueAsSecret $serviceName $key $value $notSecretValues) {
   305                    # Treat all ARM template output variables as secrets since "SecureString" variables do not set values.
   306                    # In order to mask secrets but set environment variables for any given ARM template, we set variables twice as shown below.
   307                    LogVsoCommand "##vso[task.setvariable variable=_$key;issecret=true;]$value"
   308                    Write-Host "Setting variable as secret '$key'"
   309                } else {
   310                    Write-Host "Setting variable '$key': $value"
   311                    $notSecretValues += $value
   312                }
   313                LogVsoCommand "##vso[task.setvariable variable=$key;]$value"
   314            } else {
   315                Write-Host ($shellExportFormat -f $key, $value)
   316            }
   317        }
   318
   319        if ($key) {
   320            # Isolate the environment variables for easy reading.
   321            Write-Host "`n"
   322            $key = $null
   323        }
   324    }
   325
   326    return $deploymentOutputs
   327}
   328
   329# Support actions to invoke on exit.
   330$exitActions = @({
   331    if ($exitActions.Count -gt 1) {
   332        Write-Verbose 'Running registered exit actions'
   333    }
   334})
   335
   336New-Variable -Name 'initialContext' -Value (Get-AzContext) -Option Constant
   337if ($initialContext) {
   338    $exitActions += {
   339        Write-Verbose "Restoring initial context: $($initialContext.Account)"
   340        $null = $initialContext | Select-AzContext
   341    }
   342}
   343
   344# try..finally will also trap Ctrl+C.
   345try {
   346
   347    # Enumerate test resources to deploy. Fail if none found.
   348    $repositoryRoot = "$PSScriptRoot/../../.." | Resolve-Path
   349    $root = [System.IO.Path]::Combine($repositoryRoot, "sdk", $ServiceDirectory) | Resolve-Path
   350    $templateFiles = @()
   351
   352    'test-resources.json', 'test-resources.bicep' | ForEach-Object {
   353        Write-Verbose "Checking for '$_' files under '$root'"
   354        Get-ChildItem -Path $root -Filter "$_" -Recurse | ForEach-Object {
   355            Write-Verbose "Found template '$($_.FullName)'"
   356            if ($_.Extension -eq '.bicep') {
   357                $templateFile = @{originalFilePath = $_.FullName; jsonFilePath = (BuildBicepFile $_)}
   358                $templateFiles += $templateFile
   359            } else {
   360                $templateFile = @{originalFilePath = $_.FullName; jsonFilePath = $_.FullName}
   361                $templateFiles += $templateFile
   362            }
   363        }
   364    }
   365
   366    if (!$templateFiles) {
   367        Write-Warning -Message "No template files found under '$root'"
   368        exit
   369    }
   370
   371    $UserName = GetUserName
   372
   373    if (!$BaseName) {
   374        if ($CI) {
   375            $BaseName = 't' + (New-Guid).ToString('n').Substring(0, 16)
   376            Log "Generated base name '$BaseName' for CI build"
   377        } else {
   378            $BaseName = GetBaseName $UserName (GetServiceLeafDirectoryName $ServiceDirectory)
   379            Log "BaseName was not set. Using default base name '$BaseName'"
   380        }
   381    }
   382
   383    # Make sure pre- and post-scripts are passed formerly required arguments.
   384    $PSBoundParameters['BaseName'] = $BaseName
   385
   386    # Try detecting repos that support OutFile and defaulting to it
   387    if (!$CI -and !$PSBoundParameters.ContainsKey('OutFile') -and $IsWindows) {
   388        # TODO: find a better way to detect the language
   389        if (Test-Path "$repositoryRoot/eng/service.proj") {
   390            $OutFile = $true
   391            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."
   392        }
   393    }
   394
   395    # If no location is specified use safe default locations for the given
   396    # environment. If no matching environment is found $Location remains an empty
   397    # string.
   398    if (!$Location) {
   399        $Location = @{
   400            'AzureCloud' = 'westus2';
   401            'AzureUSGovernment' = 'usgovvirginia';
   402            'AzureChinaCloud' = 'chinaeast2';
   403            'Dogfood' = 'westus'
   404        }[$Environment]
   405
   406        Write-Verbose "Location was not set. Using default location for environment: '$Location'"
   407    }
   408
   409    if (!$CI -and $PSCmdlet.ParameterSetName -ne "Provisioner") {
   410        # Make sure the user is logged in to create a service principal.
   411        $context = Get-AzContext;
   412        if (!$context) {
   413            Log 'User not logged in. Logging in now...'
   414            $context = (Connect-AzAccount).Context
   415        }
   416
   417        $currentSubcriptionId = $context.Subscription.Id
   418
   419        # If no subscription was specified, try to select the Azure SDK Developer Playground subscription.
   420        # Ignore errors to leave the automatically selected subscription.
   421        if ($SubscriptionId) {
   422            if ($currentSubcriptionId -ne $SubscriptionId) {
   423                Log "Selecting subscription '$SubscriptionId'"
   424                $null = Select-AzSubscription -Subscription $SubscriptionId
   425
   426                $exitActions += {
   427                    Log "Selecting previous subscription '$currentSubcriptionId'"
   428                    $null = Select-AzSubscription -Subscription $currentSubcriptionId
   429                }
   430
   431                # Update the context.
   432                $context = Get-AzContext
   433            }
   434        } else {
   435            if ($currentSubcriptionId -ne 'faa080af-c1d8-40ad-9cce-e1a450ca5b57') {
   436                Log "Attempting to select subscription 'Azure SDK Developer Playground (faa080af-c1d8-40ad-9cce-e1a450ca5b57)'"
   437                $null = Select-AzSubscription -Subscription 'faa080af-c1d8-40ad-9cce-e1a450ca5b57' -ErrorAction Ignore
   438
   439                # Update the context.
   440                $context = Get-AzContext
   441            }
   442
   443            $SubscriptionId = $context.Subscription.Id
   444            $PSBoundParameters['SubscriptionId'] = $SubscriptionId
   445        }
   446
   447        # Use cache of well-known team subs without having to be authenticated.
   448        $wellKnownSubscriptions = @{
   449            'faa080af-c1d8-40ad-9cce-e1a450ca5b57' = 'Azure SDK Developer Playground'
   450            'a18897a6-7e44-457d-9260-f2854c0aca42' = 'Azure SDK Engineering System'
   451            '2cd617ea-1866-46b1-90e3-fffb087ebf9b' = 'Azure SDK Test Resources'
   452        }
   453
   454        # Print which subscription is currently selected.
   455        $subscriptionName = $context.Subscription.Id
   456        if ($wellKnownSubscriptions.ContainsKey($subscriptionName)) {
   457            $subscriptionName = '{0} ({1})' -f $wellKnownSubscriptions[$subscriptionName], $subscriptionName
   458        }
   459
   460        Log "Using subscription '$subscriptionName'"
   461
   462        # Make sure the TenantId is also updated from the current context.
   463        # PSBoundParameters is not updated to avoid confusing parameter sets.
   464        if (!$TenantId) {
   465            $TenantId = $context.Subscription.TenantId
   466        }
   467    }
   468
   469    # If a provisioner service principal was provided, log into it to perform the pre- and post-scripts and deployments.
   470    if ($ProvisionerApplicationId) {
   471        $null = Disable-AzContextAutosave -Scope Process
   472
   473        Log "Logging into service principal '$ProvisionerApplicationId'."
   474        $provisionerSecret = ConvertTo-SecureString -String $ProvisionerApplicationSecret -AsPlainText -Force
   475        $provisionerCredential = [System.Management.Automation.PSCredential]::new($ProvisionerApplicationId, $provisionerSecret)
   476
   477        # Use the given subscription ID if provided.
   478        $subscriptionArgs = if ($SubscriptionId) {
   479            @{Subscription = $SubscriptionId}
   480        } else {
   481            @{}
   482        }
   483
   484        $provisionerAccount = Retry {
   485            Connect-AzAccount -Force:$Force -Tenant $TenantId -Credential $provisionerCredential -ServicePrincipal -Environment $Environment @subscriptionArgs
   486        }
   487
   488        $exitActions += {
   489            Write-Verbose "Logging out of service principal '$($provisionerAccount.Context.Account)'"
   490
   491            # Only attempt to disconnect if the -WhatIf flag was not set. Otherwise, this call is not necessary and will fail.
   492            if ($PSCmdlet.ShouldProcess($ProvisionerApplicationId)) {
   493                $null = Disconnect-AzAccount -AzureContext $provisionerAccount.Context
   494            }
   495        }
   496    }
   497
   498    # Determine the Azure context that the script is running in.
   499    $context = Get-AzContext;
   500
   501    # Make sure the provisioner OID is set so we can pass it through to the deployment.
   502    if (!$ProvisionerApplicationId -and !$ProvisionerApplicationOid) {
   503        if ($context.Account.Type -eq 'User') {
   504            $user = Get-AzADUser -UserPrincipalName $context.Account.Id
   505            $ProvisionerApplicationOid = $user.Id
   506        } elseif ($context.Account.Type -eq 'ServicePrincipal') {
   507            $sp = Get-AzADServicePrincipal -ApplicationId $context.Account.Id
   508            $ProvisionerApplicationOid = $sp.Id
   509        } else {
   510            Write-Warning "Getting the OID for provisioner type '$($context.Account.Type)' is not supported and will not be passed to deployments (seldom required)."
   511        }
   512    } elseif (!$ProvisionerApplicationOid) {
   513        $sp = Get-AzADServicePrincipal -ApplicationId $ProvisionerApplicationId
   514        $ProvisionerApplicationOid = $sp.Id
   515    }
   516
   517    $serviceName = GetServiceLeafDirectoryName $ServiceDirectory
   518
   519    $ResourceGroupName = if ($ResourceGroupName) {
   520        $ResourceGroupName
   521    } elseif ($CI) {
   522        # Format the resource group name based on resource group naming recommendations and limitations.
   523        "rg-{0}-$BaseName" -f ($serviceName -replace '[\.\\\/:]', '-').ToLowerInvariant().Substring(0, [Math]::Min($serviceName.Length, 90 - $BaseName.Length - 4)).Trim('-')
   524    } else {
   525        "rg-$BaseName"
   526    }
   527
   528    $tags = @{
   529        Owners = $UserName
   530        ServiceDirectory = $ServiceDirectory
   531    }
   532
   533    # Tag the resource group to be deleted after a certain number of hours.
   534    Write-Warning "Any clean-up scripts running against subscription '$SubscriptionId' may delete resource group '$ResourceGroupName' after $DeleteAfterHours hours."
   535    $deleteAfter = [DateTime]::UtcNow.AddHours($DeleteAfterHours).ToString('o')
   536    $tags['DeleteAfter'] = $deleteAfter
   537
   538    if ($CI) {
   539        # Add tags for the current CI job.
   540        $tags += @{
   541            BuildId = "${env:BUILD_BUILDID}"
   542            BuildJob = "${env:AGENT_JOBNAME}"
   543            BuildNumber = "${env:BUILD_BUILDNUMBER}"
   544            BuildReason = "${env:BUILD_REASON}"
   545        }
   546
   547        # Set an environment variable marking that resources have been deployed
   548        # This variable can be consumed as a yaml condition in later stages of the pipeline
   549        # to determine whether resources should be removed.
   550        Write-Host "Setting variable 'CI_HAS_DEPLOYED_RESOURCES': 'true'"
   551        LogVsoCommand "##vso[task.setvariable variable=CI_HAS_DEPLOYED_RESOURCES;]true"
   552        $EnvironmentVariables['CI_HAS_DEPLOYED_RESOURCES'] = $true
   553    }
   554
   555    Log "Creating resource group '$ResourceGroupName' in location '$Location'"
   556    $resourceGroup = Retry {
   557        New-AzResourceGroup -Name "$ResourceGroupName" -Location $Location -Tag $tags -Force:$Force
   558    }
   559
   560    if ($resourceGroup.ProvisioningState -eq 'Succeeded') {
   561        # New-AzResourceGroup would've written an error and stopped the pipeline by default anyway.
   562        Write-Verbose "Successfully created resource group '$($resourceGroup.ResourceGroupName)'"
   563    }
   564    elseif (!$resourceGroup) {
   565        if (!$PSCmdlet.ShouldProcess($resourceGroupName)) {
   566            # If the -WhatIf flag was passed, there will be no resource group created. Fake it.
   567            $resourceGroup = [PSCustomObject]@{
   568                ResourceGroupName = $resourceGroupName
   569                Location = $Location
   570            }
   571        } else {
   572            Write-Error "Resource group '$ResourceGroupName' already exists." -Category ResourceExists -RecommendedAction "Delete resource group '$ResourceGroupName', or overwrite it when redeploying."
   573        }
   574    }
   575
   576    # If no test application ID was specified during an interactive session, create a new service principal.
   577    if (!$CI -and !$TestApplicationId) {
   578        # Cache the created service principal in this session for frequent reuse.
   579        $servicePrincipal = if ($AzureTestPrincipal -and (Get-AzADServicePrincipal -ApplicationId $AzureTestPrincipal.AppId) -and $AzureTestSubscription -eq $SubscriptionId) {
   580            Log "TestApplicationId was not specified; loading cached service principal '$($AzureTestPrincipal.AppId)'"
   581            $AzureTestPrincipal
   582        } else {
   583            Log "TestApplicationId was not specified; creating a new service principal in subscription '$SubscriptionId'"
   584            $suffix = (New-Guid).ToString('n').Substring(0, 4)
   585
   586            # Service principals in the Microsoft AAD tenant must end with an @microsoft.com domain; those in other tenants
   587            # are not permitted to do so. Format the non-MS name so there's an assocation with the Azure SDK.
   588            if ($TenantId -eq '72f988bf-86f1-41af-91ab-2d7cd011db47') {
   589                $displayName = "test-resources-$($baseName)$suffix.microsoft.com"
   590            } else {
   591                $displayName = "$($baseName)$suffix.test-resources.azure.sdk"
   592            }
   593
   594            $servicePrincipalWrapper = NewServicePrincipalWrapper -subscription $SubscriptionId -resourceGroup $ResourceGroupName -displayName $DisplayName
   595
   596            $global:AzureTestPrincipal = $servicePrincipalWrapper
   597            $global:AzureTestSubscription = $SubscriptionId
   598
   599            Log "Created service principal. AppId: '$($AzureTestPrincipal.AppId)' ObjectId: '$($AzureTestPrincipal.Id)'"
   600            $servicePrincipalWrapper
   601            $resourceGroupRoleAssigned = $true
   602        }
   603
   604        $TestApplicationId = $servicePrincipal.AppId
   605        $TestApplicationOid = $servicePrincipal.Id
   606        $TestApplicationSecret = (ConvertFrom-SecureString $servicePrincipal.Secret -AsPlainText)
   607    }
   608
   609    # Get test application OID from ID if not already provided. This may fail if the
   610    # provisioner is a service principal without permissions to query AAD. This is a
   611    # critical failure, but we should prompt with possible remediation.
   612    if ($TestApplicationId -and !$TestApplicationOid) {
   613        Log "Attempting to query the Object ID for the test service principal"
   614
   615        try {
   616            $testServicePrincipal = Retry {
   617                Get-AzADServicePrincipal -ApplicationId $TestApplicationId
   618            }
   619        }
   620        catch {
   621            Write-Warning "The Object ID of the test application was unable to be queried. You may want to consider passing it explicitly with the 'TestApplicationOid` parameter."
   622            throw $_.Exception
   623        }
   624
   625        if ($testServicePrincipal -and $testServicePrincipal.Id) {
   626            $script:TestApplicationOid = $testServicePrincipal.Id
   627        }
   628    }
   629
   630    # Make sure pre- and post-scripts are passed formerly required arguments.
   631    $PSBoundParameters['TestApplicationId'] = $TestApplicationId
   632    $PSBoundParameters['TestApplicationOid'] = $TestApplicationOid
   633    $PSBoundParameters['TestApplicationSecret'] = $TestApplicationSecret
   634
   635    # If the role hasn't been explicitly assigned to the resource group and a cached service principal is in use,
   636    # query to see if the grant is needed.
   637    if (!$resourceGroupRoleAssigned -and $AzureTestPrincipal) {
   638        $roleAssignment = Get-AzRoleAssignment -ObjectId $AzureTestPrincipal.Id -RoleDefinitionName 'Owner' -ResourceGroupName "$ResourceGroupName" -ErrorAction SilentlyContinue
   639        $resourceGroupRoleAssigned = ($roleAssignment.RoleDefinitionName -eq 'Owner')
   640    }
   641
   642   # If needed, grant the test service principal ownership over the resource group. This may fail if the provisioner
   643   # is a service principal without permissions to grant RBAC roles to other service principals. That should not be
   644   # considered a critical failure, as the test application may have subscription-level permissions and not require
   645   # the explicit grant.
   646   if (!$resourceGroupRoleAssigned) {
   647        Log "Attempting to assigning the 'Owner' role for '$ResourceGroupName' to the Test Application '$TestApplicationId'"
   648        $principalOwnerAssignment = New-AzRoleAssignment -RoleDefinitionName "Owner" -ApplicationId "$TestApplicationId" -ResourceGroupName "$ResourceGroupName" -ErrorAction SilentlyContinue
   649
   650        if ($principalOwnerAssignment.RoleDefinitionName -eq 'Owner') {
   651            Write-Verbose "Successfully assigned ownership of '$ResourceGroupName' to the Test Application '$TestApplicationId'"
   652        } else {
   653            Write-Warning "The 'Owner' role for '$ResourceGroupName' could not be assigned. You may need to manually grant 'Owner' for the resource group to the Test Application '$TestApplicationId' if it does not have subscription-level permissions."
   654        }
   655    }
   656
   657    # Populate the template parameters and merge any additional specified.
   658    $templateParameters = @{
   659        baseName = $BaseName
   660        testApplicationId = $TestApplicationId
   661        testApplicationOid = "$TestApplicationOid"
   662    }
   663    if ($ProvisionerApplicationOid) {
   664        $templateParameters["provisionerApplicationOid"] = "$ProvisionerApplicationOid"
   665    }
   666
   667    if ($TenantId) {
   668        $templateParameters.Add('tenantId', $TenantId)
   669    }
   670    if ($TestApplicationSecret) {
   671        $templateParameters.Add('testApplicationSecret', $TestApplicationSecret)
   672    }
   673
   674    $defaultCloudParameters = LoadCloudConfig $Environment
   675    MergeHashes $defaultCloudParameters $(Get-Variable templateParameters)
   676    MergeHashes $ArmTemplateParameters $(Get-Variable templateParameters)
   677    MergeHashes $AdditionalParameters $(Get-Variable templateParameters)
   678
   679    # Include environment-specific parameters only if not already provided as part of the "ArmTemplateParameters"
   680    if (($context.Environment.StorageEndpointSuffix) -and (-not ($templateParameters.ContainsKey('storageEndpointSuffix')))) {
   681        $templateParameters.Add('storageEndpointSuffix', $context.Environment.StorageEndpointSuffix)
   682    }
   683
   684    # Try to detect the shell based on the parent process name (e.g. launch via shebang).
   685    $shell, $shellExportFormat = if (($parentProcessName = (Get-Process -Id $PID).Parent.ProcessName) -and $parentProcessName -eq 'cmd') {
   686        'cmd', 'set {0}={1}'
   687    } elseif (@('bash', 'csh', 'tcsh', 'zsh') -contains $parentProcessName) {
   688        'shell', 'export {0}={1}'
   689    } else {
   690        'PowerShell', '${{env:{0}}} = ''{1}'''
   691    }
   692
   693    # Deploy the templates
   694    foreach ($templateFile in $templateFiles) {
   695        # Deployment fails if we pass in more parameters than are defined.
   696        Write-Verbose "Removing unnecessary parameters from template '$($templateFile.jsonFilePath)'"
   697        $templateJson = Get-Content -LiteralPath $templateFile.jsonFilePath | ConvertFrom-Json
   698        $templateParameterNames = $templateJson.parameters.PSObject.Properties.Name
   699
   700        $templateFileParameters = $templateParameters.Clone()
   701        foreach ($key in $templateParameters.Keys) {
   702            if ($templateParameterNames -notcontains $key) {
   703                Write-Verbose "Removing unnecessary parameter '$key'"
   704                $templateFileParameters.Remove($key)
   705            }
   706        }
   707
   708        $preDeploymentScript = $templateFile.originalFilePath | Split-Path | Join-Path -ChildPath 'test-resources-pre.ps1'
   709        if (Test-Path $preDeploymentScript) {
   710            Log "Invoking pre-deployment script '$preDeploymentScript'"
   711            &$preDeploymentScript -ResourceGroupName $ResourceGroupName @PSBoundParameters
   712        }
   713
   714        $msg = if ($templateFile.jsonFilePath -ne $templateFile.originalFilePath) {
   715            "Deployment template $($templateFile.jsonFilePath) from $($templateFile.originalFilePath) to resource group $($resourceGroup.ResourceGroupName)"
   716        } else {
   717            "Deployment template $($templateFile.jsonFilePath) to resource group $($resourceGroup.ResourceGroupName)"
   718        }
   719        Log $msg
   720
   721        $deployment = Retry {
   722            $lastDebugPreference = $DebugPreference
   723            try {
   724                if ($CI) {
   725                    $DebugPreference = 'Continue'
   726                }
   727                New-AzResourceGroupDeployment -Name $BaseName -ResourceGroupName $resourceGroup.ResourceGroupName -TemplateFile $templateFile.jsonFilePath -TemplateParameterObject $templateFileParameters -Force:$Force
   728            } catch {
   729                Write-Output @'
   730#####################################################
   731# For help debugging live test provisioning issues, #
   732# see http://aka.ms/azsdk/engsys/live-test-help,    #
   733#####################################################
   734'@
   735                throw
   736            } finally {
   737                $DebugPreference = $lastDebugPreference
   738            }
   739        }
   740
   741        if ($deployment.ProvisioningState -eq 'Succeeded') {
   742            # New-AzResourceGroupDeployment would've written an error and stopped the pipeline by default anyway.
   743            Write-Verbose "Successfully deployed template '$($templateFile.jsonFilePath)' to resource group '$($resourceGroup.ResourceGroupName)'"
   744        }
   745
   746        $deploymentOutputs = SetDeploymentOutputs $serviceName $context $deployment $templateFile
   747
   748        $postDeploymentScript = $templateFile.originalFilePath | Split-Path | Join-Path -ChildPath 'test-resources-post.ps1'
   749        if (Test-Path $postDeploymentScript) {
   750            Log "Invoking post-deployment script '$postDeploymentScript'"
   751            &$postDeploymentScript -ResourceGroupName $ResourceGroupName -DeploymentOutputs $deploymentOutputs @PSBoundParameters
   752        }
   753
   754        if ($templateFile.jsonFilePath.EndsWith('.compiled.json')) {
   755            Write-Verbose "Removing compiled bicep file $($templateFile.jsonFilePath)"
   756            Remove-Item $templateFile.jsonFilePath
   757        }
   758    }
   759
   760} finally {
   761    $exitActions.Invoke()
   762}
   763
   764# Suppress output locally
   765if ($CI) {
   766    return $EnvironmentVariables
   767}
   768
   769<#
   770.SYNOPSIS
   771Deploys live test resources defined for a service directory to Azure.
   772
   773.DESCRIPTION
   774Deploys live test resouces specified in test-resources.json or test-resources.bicep
   775files to a new resource group.
   776
   777This script searches the directory specified in $ServiceDirectory recursively
   778for files named test-resources.json or test-resources.bicep. All found test-resources.json
   779and test-resources.bicep files will be deployed to the test resource group.
   780
   781If no test-resources.json or test-resources.bicep files are located the script
   782exits without making changes to the Azure environment.
   783
   784A service principal may optionally be passed to $TestApplicationId and $TestApplicationSecret.
   785Test resources will grant this service principal access to the created resources.
   786If no service principal is specified, a new one will be created and assigned the
   787'Owner' role for the resource group associated with the test resources.
   788
   789This script runs in the context of credentials already specified in Connect-AzAccount
   790or those specified in $ProvisionerApplicationId and $ProvisionerApplicationSecret.
   791
   792.PARAMETER BaseName
   793A name to use in the resource group and passed to the ARM template as 'baseName'.
   794Limit $BaseName to enough characters to be under limit plus prefixes specified in
   795the ARM template. See also https://docs.microsoft.com/azure/architecture/best-practices/resource-naming
   796
   797Note: The value specified for this parameter will be overriden and generated
   798by New-TestResources.ps1 if $CI is specified.
   799
   800.PARAMETER ResourceGroupName
   801Set this value to deploy directly to a Resource Group that has already been
   802created or to create a new resource group with this name.
   803
   804If not specified, the $BaseName will be used to generate name for the resource
   805group that will be created.
   806
   807.PARAMETER ServiceDirectory
   808A directory under 'sdk' in the repository root - optionally with subdirectories
   809specified - in which to discover ARM templates named 'test-resources.json' and
   810Bicep templates named 'test-resources.bicep'. This can also be an absolute path
   811or specify parent directories.
   812
   813.PARAMETER TestApplicationId
   814Optional Azure Active Directory Application ID to authenticate the test runner
   815against deployed resources. Passed to the ARM template as 'testApplicationId'.
   816
   817If not specified, a new AAD Application will be created and assigned the 'Owner'
   818role for the resource group associated with the test resources. No permissions
   819will be granted to the subscription or other resources.
   820
   821For those specifying a Provisioner Application principal as 'ProvisionerApplicationId',
   822it will need the permission 'Application.ReadWrite.OwnedBy' for the Microsoft Graph API
   823in order to create the Test Application principal.
   824
   825This application is used by the test runner to execute tests against the
   826live test resources.
   827
   828.PARAMETER TestApplicationSecret
   829Optional service principal secret (password) to authenticate the test runner
   830against deployed resources. Passed to the ARM template as
   831'testApplicationSecret'.
   832
   833This application is used by the test runner to execute tests against the
   834live test resources.
   835
   836.PARAMETER TestApplicationOid
   837Service Principal Object ID of the AAD Test Application. This is used to assign
   838permissions to the AAD application so it can access tested features on the live
   839test resources (e.g. Role Assignments on resources). It is passed as to the ARM
   840template as 'testApplicationOid'
   841
   842If not specified, an attempt will be made to query it from the Azure Active Directory
   843tenant. For those specifying a service principal as 'ProvisionerApplicationId',
   844it will need the permission 'Application.Read.All' for the Microsoft Graph API
   845in order to query AAD.
   846
   847For more information on the relationship between AAD Applications and Service
   848Principals see: https://docs.microsoft.com/azure/active-directory/develop/app-objects-and-service-principals
   849
   850.PARAMETER TenantId
   851The tenant ID of a service principal when a provisioner is specified. The same
   852Tenant ID is used for Test Application and Provisioner Application.
   853
   854This value is passed to the ARM template as 'tenantId'.
   855
   856.PARAMETER SubscriptionId
   857Optional subscription ID to use for new resources when logging in as a
   858provisioner. You can also use Set-AzContext if not provisioning.
   859
   860If you do not specify a SubscriptionId and are not logged in, one will be
   861automatically selected for you by the Connect-AzAccount cmdlet.
   862
   863Once you are logged in (or were previously), the selected SubscriptionId
   864will be used for subsequent operations that are specific to a subscription.
   865
   866.PARAMETER ProvisionerApplicationId
   867Optional Application ID of the Azure Active Directory service principal to use for
   868provisioning the test resources. If not, specified New-TestResources.ps1 uses the
   869context of the caller to provision.
   870
   871If specified, the Provisioner Application principal would benefit from the following
   872permissions to the Microsoft Graph API:
   873
   874  - 'Application.Read.All' in order to query AAD to obtain the 'TestApplicaitonOid'
   875
   876  - 'Application.ReadWrite.OwnedBy' in order to create the Test Application principal
   877     or grant an existing principal ownership of the resource group associated with
   878     the test resources.
   879
   880If the provisioner does not have these permissions, it can still be used with
   881New-TestResources.ps1 by specifying an existing Test Application principal, including
   882its Object ID, and managing permissions to the resource group manually.
   883
   884This value is not passed to the ARM template.
   885
   886.PARAMETER ProvisionerApplicationSecret
   887A service principal secret (password) used to provision test resources when a
   888provisioner is specified.
   889
   890This value is not passed to the ARM template.
   891
   892.PARAMETER DeleteAfterHours
   893Positive integer number of hours from the current time to set the
   894'DeleteAfter' tag on the created resource group. The computed value is a
   895timestamp of the form "2020-03-04T09:07:04.3083910Z".
   896
   897An optional cleanup process can delete resource groups whose "DeleteAfter"
   898timestamp is less than the current time.
   899
   900This is used for CI automation.
   901
   902.PARAMETER Location
   903Optional location where resources should be created. If left empty, the default
   904is based on the cloud to which the template is being deployed:
   905
   906* AzureCloud -> 'westus2'
   907* AzureUSGovernment -> 'usgovvirginia'
   908* AzureChinaCloud -> 'chinaeast2'
   909* Dogfood -> 'westus'
   910
   911.PARAMETER Environment
   912Optional name of the cloud environment. The default is the Azure Public Cloud
   913('AzureCloud')
   914
   915.PARAMETER AdditionalParameters
   916Optional key-value pairs of parameters to pass to the ARM template(s) and pre-post scripts.
   917
   918.PARAMETER ArmTemplateParameters
   919Optional key-value pairs of parameters to pass to the ARM template(s).
   920
   921.PARAMETER EnvironmentVariables
   922Optional key-value pairs of parameters to set as environment variables to the shell.
   923
   924.PARAMETER CI
   925Indicates the script is run as part of a Continuous Integration / Continuous
   926Deployment (CI/CD) build (only Azure Pipelines is currently supported).
   927
   928.PARAMETER Force
   929Force creation of resources instead of being prompted.
   930
   931.PARAMETER OutFile
   932Save test environment settings into a .env file next to test resources template.
   933The contents of the file are protected via the .NET Data Protection API (DPAPI).
   934This is supported only on Windows. The environment file is scoped to the current
   935service directory.
   936
   937The environment file will be named for the test resources template that it was
   938generated for. For ARM templates, it will be test-resources.json.env. For
   939Bicep templates, test-resources.bicep.env.
   940
   941.PARAMETER SuppressVsoCommands
   942By default, the -CI parameter will print out secrets to logs with Azure Pipelines log
   943commands that cause them to be redacted. For CI environments that don't support this (like
   944stress test clusters), this flag can be set to $false to avoid printing out these secrets to the logs.
   945
   946.EXAMPLE
   947Connect-AzAccount -Subscription 'REPLACE_WITH_SUBSCRIPTION_ID'
   948New-TestResources.ps1 keyvault
   949
   950Run this in a desktop environment to create a new AAD application and Service Principal
   951for running live tests against the test resources created. The principal will have ownership
   952rights to the resource group and the resources that it contains, but no other resources in
   953the subscription.
   954
   955Requires PowerShell 7 to use ConvertFrom-SecureString -AsPlainText or convert
   956the SecureString to plaintext by another means.
   957
   958.EXAMPLE
   959Connect-AzAccount -Subscription 'REPLACE_WITH_SUBSCRIPTION_ID'
   960New-TestResources.ps1 `
   961    -BaseName 'azsdk' `
   962    -ServiceDirectory 'keyvault' `
   963    -SubscriptionId 'REPLACE_WITH_SUBSCRIPTION_ID' `
   964    -ResourceGroupName 'REPLACE_WITH_NAME_FOR_RESOURCE_GROUP' `
   965    -Location 'eastus'
   966
   967Run this in a desktop environment to specify the name and location of the resource
   968group that test resources are being deployed to. This will also create a new AAD
   969application and Service Principal for running live tests against the rest resources
   970created. The principal will have ownership rights to the resource group and the
   971resources that it contains, but no other resources in the subscription.
   972
   973Requires PowerShell 7 to use ConvertFrom-SecureString -AsPlainText or convert
   974the SecureString to plaintext by another means.
   975
   976.EXAMPLE
   977Connect-AzAccount -Subscription 'REPLACE_WITH_SUBSCRIPTION_ID'
   978New-TestResources.ps1 `
   979    -BaseName 'azsdk' `
   980    -ServiceDirectory 'keyvault' `
   981    -SubscriptionId 'REPLACE_WITH_SUBSCRIPTION_ID' `
   982    -ResourceGroupName 'REPLACE_WITH_NAME_FOR_RESOURCE_GROUP' `
   983    -Location 'eastus' `
   984    -TestApplicationId 'REPLACE_WITH_TEST_APPLICATION_ID' `
   985    -TestApplicationSecret 'REPLACE_WITH_TEST_APPLICATION_SECRET'
   986
   987Run this in a desktop environment to specify the name and location of the resource
   988group that test resources are being deployed to. This will grant ownership rights
   989to the 'TestApplicationId' for the resource group and the resources that it contains,
   990without altering its existing permissions.
   991
   992.EXAMPLE
   993New-TestResources.ps1 `
   994    -BaseName 'azsdk' `
   995    -ServiceDirectory 'keyvault' `
   996    -SubscriptionId 'REPLACE_WITH_SUBSCRIPTION_ID' `
   997    -ResourceGroupName 'REPLACE_WITH_NAME_FOR_RESOURCE_GROUP' `
   998    -Location 'eastus' `
   999    -ProvisionerApplicationId 'REPLACE_WITH_PROVISIONER_APPLICATION_ID' `
  1000    -ProvisionerApplicationSecret 'REPLACE_WITH_PROVISIONER_APPLICATION_ID' `
  1001    -TestApplicationId 'REPLACE_WITH_TEST_APPLICATION_ID' `
  1002    -TestApplicationOid 'REPLACE_WITH_TEST_APPLICATION_OBJECT_ID' `
  1003    -TestApplicationSecret 'REPLACE_WITH_TEST_APPLICATION_SECRET'
  1004
  1005Run this in a desktop environment to specify the name and location of the resource
  1006group that test resources are being deployed to. The script will be executed in the
  1007context of the 'ProvisionerApplicationId' rather than the caller.
  1008
  1009Depending on the permissions of the Provisioner Application principal, the script may
  1010grant ownership rights 'TestApplicationId' for the resource group and the resources
  1011that it contains, or may emit a message indicating that it was unable to perform the grant.
  1012
  1013For the Provisioner Application principal to perform the grant, it will need the
  1014permission 'Application.ReadWrite.OwnedBy' for the Microsoft Graph API.
  1015
  1016Requires PowerShell 7 to use ConvertFrom-SecureString -AsPlainText or convert
  1017the SecureString to plaintext by another means.
  1018
  1019.EXAMPLE
  1020New-TestResources.ps1 `
  1021    -ServiceDirectory '$(ServiceDirectory)' `
  1022    -TenantId '$(TenantId)' `
  1023    -ProvisionerApplicationId '$(ProvisionerId)' `
  1024    -ProvisionerApplicationSecret '$(ProvisionerSecret)' `
  1025    -TestApplicationId '$(TestAppId)' `
  1026    -TestApplicationSecret '$(TestAppSecret)' `
  1027    -DeleteAfterHours 24 `
  1028    -CI `
  1029    -Force `
  1030    -Verbose
  1031
  1032Run this in an Azure DevOps CI (with approrpiate variables configured) before
  1033executing live tests. The script will output variables as secrets (to enable
  1034log redaction).
  1035
  1036#>

View as plain text