diff --git a/eng/common/TestResources/New-TestResources.ps1 b/eng/common/TestResources/New-TestResources.ps1 index fed1bd7b38e8..07917f60aa1f 100644 --- a/eng/common/TestResources/New-TestResources.ps1 +++ b/eng/common/TestResources/New-TestResources.ps1 @@ -79,6 +79,8 @@ param ( [switch] $OutFile ) +. $PSScriptRoot/SubConfig-Helpers.ps1 + # By default stop for any error. if (!$PSBoundParameters.ContainsKey('ErrorAction')) { $ErrorActionPreference = 'Stop' @@ -126,7 +128,7 @@ function LoadCloudConfig([string] $env) function MergeHashes([hashtable] $source, [psvariable] $dest) { foreach ($key in $source.Keys) { - if ($dest.Value.ContainsKey($key) -and $dest.Value[$key] -ne $source[$key]) { + if ($dest.Value.Contains($key) -and $dest.Value[$key] -ne $source[$key]) { Write-Warning ("Overwriting '$($dest.Name).$($key)' with value '$($dest.Value[$key])' " + "to new value '$($source[$key])'") } @@ -155,6 +157,93 @@ function BuildBicepFile([System.IO.FileSystemInfo] $file) return $templateFilePath } +function BuildDeploymentOutputs([string]$serviceDirectoryPrefix, [object]$azContext, [object]$deployment) { + # Add default values + $deploymentOutputs = [Ordered]@{ + "${serviceDirectoryPrefix}CLIENT_ID" = $TestApplicationId; + "${serviceDirectoryPrefix}CLIENT_SECRET" = $TestApplicationSecret; + "${serviceDirectoryPrefix}TENANT_ID" = $azContext.Tenant.Id; + "${serviceDirectoryPrefix}SUBSCRIPTION_ID" = $azContext.Subscription.Id; + "${serviceDirectoryPrefix}RESOURCE_GROUP" = $resourceGroup.ResourceGroupName; + "${serviceDirectoryPrefix}LOCATION" = $resourceGroup.Location; + "${serviceDirectoryPrefix}ENVIRONMENT" = $azContext.Environment.Name; + "${serviceDirectoryPrefix}AZURE_AUTHORITY_HOST" = $azContext.Environment.ActiveDirectoryAuthority; + "${serviceDirectoryPrefix}RESOURCE_MANAGER_URL" = $azContext.Environment.ResourceManagerUrl; + "${serviceDirectoryPrefix}SERVICE_MANAGEMENT_URL" = $azContext.Environment.ServiceManagementUrl; + } + + MergeHashes $EnvironmentVariables $(Get-Variable deploymentOutputs) + + foreach ($key in $deployment.Outputs.Keys) { + $variable = $deployment.Outputs[$key] + + # Work around bug that makes the first few characters of environment variables be lowercase. + $key = $key.ToUpperInvariant() + + if ($variable.Type -eq 'String' -or $variable.Type -eq 'SecureString') { + $deploymentOutputs[$key] = $variable.Value + } + } + + return $deploymentOutputs +} + +function SetDeploymentOutputs([string]$serviceName, [object]$azContext, [object]$deployment, [object]$templateFile) { + $serviceDirectoryPrefix = $serviceName.ToUpperInvariant() + "_" + $deploymentOutputs = BuildDeploymentOutputs $serviceDirectoryPrefix $azContext $deployment + + if ($OutFile) { + if (!$IsWindows) { + Write-Host 'File option is supported only on Windows' + } + + $outputFile = "$($templateFile.originalFilePath).env" + + $environmentText = $deploymentOutputs | ConvertTo-Json; + $bytes = [System.Text.Encoding]::UTF8.GetBytes($environmentText) + $protectedBytes = [Security.Cryptography.ProtectedData]::Protect($bytes, $null, [Security.Cryptography.DataProtectionScope]::CurrentUser) + + Set-Content $outputFile -Value $protectedBytes -AsByteStream -Force + + Write-Host "Test environment settings`n $environmentText`nstored into encrypted $outputFile" + } else { + if (!$CI) { + # Write an extra new line to isolate the environment variables for easy reading. + Log "Persist the following environment variables based on your detected shell ($shell):`n" + } + + # Marking values as secret by allowed keys below is not sufficient, as there may be outputs set in the ARM/bicep + # file that re-mark those values as secret (since all user-provided deployment outputs are treated as secret by default). + # This variable supports a second check on not marking previously allowed keys/values as secret. + $notSecretValues = @() + foreach ($key in $deploymentOutputs.Keys) { + $value = $deploymentOutputs[$key] + $EnvironmentVariables[$key] = $value + + if ($CI) { + if (ShouldMarkValueAsSecret $serviceDirectoryPrefix $key $value $notSecretValues) { + # Treat all ARM template output variables as secrets since "SecureString" variables do not set values. + # In order to mask secrets but set environment variables for any given ARM template, we set variables twice as shown below. + Write-Host "##vso[task.setvariable variable=_$key;issecret=true;]$value" + Write-Host "Setting variable as secret '$key': $value" + } else { + Write-Host "Setting variable '$key': $value" + $notSecretValues += $value + } + Write-Host "##vso[task.setvariable variable=$key;]$value" + } else { + Write-Host ($shellExportFormat -f $key, $value) + } + } + + if ($key) { + # Isolate the environment variables for easy reading. + Write-Host "`n" + $key = $null + } + } +} + # Support actions to invoke on exit. $exitActions = @({ if ($exitActions.Count -gt 1) { @@ -580,78 +669,7 @@ try { Write-Verbose "Successfully deployed template '$($templateFile.jsonFilePath)' to resource group '$($resourceGroup.ResourceGroupName)'" } - $serviceDirectoryPrefix = $serviceName.ToUpperInvariant() + "_" - - # Add default values - $deploymentOutputs = @{ - "$($serviceDirectoryPrefix)CLIENT_ID" = $TestApplicationId; - "$($serviceDirectoryPrefix)CLIENT_SECRET" = $TestApplicationSecret; - "$($serviceDirectoryPrefix)TENANT_ID" = $context.Tenant.Id; - "$($serviceDirectoryPrefix)SUBSCRIPTION_ID" = $context.Subscription.Id; - "$($serviceDirectoryPrefix)RESOURCE_GROUP" = $resourceGroup.ResourceGroupName; - "$($serviceDirectoryPrefix)LOCATION" = $resourceGroup.Location; - "$($serviceDirectoryPrefix)ENVIRONMENT" = $context.Environment.Name; - "$($serviceDirectoryPrefix)AZURE_AUTHORITY_HOST" = $context.Environment.ActiveDirectoryAuthority; - "$($serviceDirectoryPrefix)RESOURCE_MANAGER_URL" = $context.Environment.ResourceManagerUrl; - "$($serviceDirectoryPrefix)SERVICE_MANAGEMENT_URL" = $context.Environment.ServiceManagementUrl; - "$($serviceDirectoryPrefix)STORAGE_ENDPOINT_SUFFIX" = $context.Environment.StorageEndpointSuffix; - } - - MergeHashes $EnvironmentVariables $(Get-Variable deploymentOutputs) - - foreach ($key in $deployment.Outputs.Keys) { - $variable = $deployment.Outputs[$key] - - # Work around bug that makes the first few characters of environment variables be lowercase. - $key = $key.ToUpperInvariant() - - if ($variable.Type -eq 'String' -or $variable.Type -eq 'SecureString') { - $deploymentOutputs[$key] = $variable.Value - } - } - - if ($OutFile) { - if (!$IsWindows) { - Write-Host 'File option is supported only on Windows' - } - - $outputFile = "$($templateFile.originalFilePath).env" - - $environmentText = $deploymentOutputs | ConvertTo-Json; - $bytes = [System.Text.Encoding]::UTF8.GetBytes($environmentText) - $protectedBytes = [Security.Cryptography.ProtectedData]::Protect($bytes, $null, [Security.Cryptography.DataProtectionScope]::CurrentUser) - - Set-Content $outputFile -Value $protectedBytes -AsByteStream -Force - - Write-Host "Test environment settings`n $environmentText`nstored into encrypted $outputFile" - } else { - - if (!$CI) { - # Write an extra new line to isolate the environment variables for easy reading. - Log "Persist the following environment variables based on your detected shell ($shell):`n" - } - - foreach ($key in $deploymentOutputs.Keys) { - $value = $deploymentOutputs[$key] - $EnvironmentVariables[$key] = $value - - if ($CI) { - # Treat all ARM template output variables as secrets since "SecureString" variables do not set values. - # In order to mask secrets but set environment variables for any given ARM template, we set variables twice as shown below. - Write-Host "Setting variable '$key': ***" - Write-Host "##vso[task.setvariable variable=_$key;issecret=true;]$($value)" - Write-Host "##vso[task.setvariable variable=$key;]$($value)" - } else { - Write-Host ($shellExportFormat -f $key, $value) - } - } - - if ($key) { - # Isolate the environment variables for easy reading. - Write-Host "`n" - $key = $null - } - } + SetDeploymentOutputs $serviceName $context $deployment $templateFile $postDeploymentScript = $templateFile.originalFilePath | Split-Path | Join-Path -ChildPath 'test-resources-post.ps1' if (Test-Path $postDeploymentScript) { diff --git a/eng/common/TestResources/SubConfig-Helpers.ps1 b/eng/common/TestResources/SubConfig-Helpers.ps1 new file mode 100644 index 000000000000..df4dc9cd0576 --- /dev/null +++ b/eng/common/TestResources/SubConfig-Helpers.ps1 @@ -0,0 +1,93 @@ +function ShouldMarkValueAsSecret([string]$serviceDirectoryPrefix, [string]$key, [string]$value, [array]$allowedValues = @()) +{ + $logOutputNonSecret = @( + # Environment Variables + "RESOURCEGROUP_NAME", + # Deployment Outputs + "CLIENT_ID", + "TENANT_ID", + "SUBSCRIPTION_ID", + "RESOURCE_GROUP", + "LOCATION", + "ENVIRONMENT", + "AUTHORITY_HOST", + "RESOURCE_MANAGER_URL", + "SERVICE_MANAGEMENT_URL", + "ENDPOINT_SUFFIX", + # This is used in many places and is harder to extract from the base subscription config, so hardcode it for now. + "STORAGE_ENDPOINT_SUFFIX", + # Parameters + "Environment", + "SubscriptionId", + "TenantId", + "TestApplicationId", + "TestApplicationOid", + "ProvisionerApplicationId" + ) + + $suffix1 = $key -replace $serviceDirectoryPrefix, "" + $suffix2 = $key -replace "AZURE_", "" + $variants = @($key, $suffix1, $suffix2) + if ($variants | Where-Object { $logOutputNonSecret -contains $_ }) { + return $false + } + + if ($allowedValues -contains $value) { + return $false + } + + return $true +} + +function SetSubscriptionConfiguration([object]$subscriptionConfiguration) +{ + foreach($pair in $subscriptionConfiguration.GetEnumerator()) { + if ($pair.Value -is [Hashtable]) { + foreach($nestedPair in $pair.Value.GetEnumerator()) { + # Mark values as secret so we don't print json blobs containing secrets in the logs. + # Prepend underscore to the variable name, so we can still access the variable names via environment + # variables if they get set subsequently. + if (ShouldMarkValueAsSecret "AZURE_" $nestedPair.Name $nestedPair.Value) { + Write-Host "##vso[task.setvariable variable=_$($nestedPair.Name);issecret=true;]$($nestedPair.Value)" + } + } + } else { + if (ShouldMarkValueAsSecret "AZURE_" $pair.Name $pair.Value) { + Write-Host "##vso[task.setvariable variable=_$($pair.Name);issecret=true;]$($pair.Value)" + } + } + } + + Write-Host ($subscriptionConfiguration | ConvertTo-Json) + $serialized = $subscriptionConfiguration | ConvertTo-Json -Compress + Write-Host "##vso[task.setvariable variable=SubscriptionConfiguration;]$serialized" +} + +function UpdateSubscriptionConfiguration([object]$subscriptionConfigurationBase, [object]$subscriptionConfiguration) +{ + foreach ($pair in $subscriptionConfiguration.GetEnumerator()) { + if ($pair.Value -is [Hashtable]) { + if (!$subscriptionConfigurationBase.ContainsKey($pair.Name)) { + $subscriptionConfigurationBase[$pair.Name] = @{} + } + foreach($nestedPair in $pair.Value.GetEnumerator()) { + # Mark values as secret so we don't print json blobs containing secrets in the logs. + # Prepend underscore to the variable name, so we can still access the variable names via environment + # variables if they get set subsequently. + if (ShouldMarkValueAsSecret "AZURE_" $nestedPair.Name $nestedPair.Value) { + Write-Host "##vso[task.setvariable variable=_$($nestedPair.Name);issecret=true;]$($nestedPair.Value)" + } + $subscriptionConfigurationBase[$pair.Name][$nestedPair.Name] = $nestedPair.Value + } + } else { + if (ShouldMarkValueAsSecret "AZURE_" $pair.Name $pair.Value) { + Write-Host "##vso[task.setvariable variable=_$($pair.Name);issecret=true;]$($pair.Value)" + } + $subscriptionConfigurationBase[$pair.Name] = $pair.Value + } + } + + $serialized = $subscriptionConfigurationBase | ConvertTo-Json -Compress + Write-Host ($subscriptionConfigurationBase | ConvertTo-Json) + Write-Host "##vso[task.setvariable variable=SubscriptionConfiguration;]$serialized" +} diff --git a/eng/common/TestResources/build-test-resource-config.yml b/eng/common/TestResources/build-test-resource-config.yml index 9aeaca918df6..2335f94f48db 100644 --- a/eng/common/TestResources/build-test-resource-config.yml +++ b/eng/common/TestResources/build-test-resource-config.yml @@ -13,22 +13,8 @@ steps: ${{ parameters.SubscriptionConfiguration }} '@ | ConvertFrom-Json -AsHashtable - foreach($pair in $config.GetEnumerator()) { - if ($pair.Value -is [Hashtable]) { - foreach($nestedPair in $pair.Value.GetEnumerator()) { - # Mark values as secret so we don't print json blobs containing secrets in the logs. - # Prepend underscore to the variable name, so we can still access the variable names via environment - # variables if they get set subsequently. - Write-Host "##vso[task.setvariable variable=_$($nestedPair.Name);issecret=true;]$($nestedPair.Value)" - } - } else { - Write-Host "##vso[task.setvariable variable=_$($pair.Name);issecret=true;]$($pair.Value)" - } - } - - Write-Host ($config | ConvertTo-Json) - $serialized = $config | ConvertTo-Json -Compress - Write-Host "##vso[task.setvariable variable=SubscriptionConfiguration;]$serialized" + . ./eng/common/TestResources/SubConfig-Helpers.ps1 + SetSubscriptionConfiguration $config displayName: Initialize SubscriptionConfiguration variable - ${{ if parameters.SubscriptionConfigurations }}: @@ -39,33 +25,14 @@ steps: - ${{ each config in parameters.SubscriptionConfigurations }}: - pwsh: | - $config = @' + $configBase = @' $(SubscriptionConfiguration) '@ | ConvertFrom-Json -AsHashtable - $addToConfig = @' + $config = @' ${{ config }} '@ | ConvertFrom-Json -AsHashtable - foreach ($pair in $addToConfig.GetEnumerator()) { - if ($pair.Value -is [Hashtable]) { - if (!$config.ContainsKey($pair.Name)) { - $config[$pair.Name] = @{} - } - foreach($nestedPair in $pair.Value.GetEnumerator()) { - # Mark values as secret so we don't print json blobs containing secrets in the logs. - # Prepend underscore to the variable name, so we can still access the variable names via environment - # variables if they get set subsequently. - Write-Host "##vso[task.setvariable variable=_$($nestedPair.Name);issecret=true;]$($nestedPair.Value)" - $config[$pair.Name][$nestedPair.Name] = $nestedPair.Value - } - } else { - Write-Host "##vso[task.setvariable variable=_$($pair.Name);issecret=true;]$($pair.Value)" - $config[$pair.Name] = $pair.Value - } - } - - $serialized = $config | ConvertTo-Json -Compress - Write-Host ($config | ConvertTo-Json) - Write-Host "##vso[task.setvariable variable=SubscriptionConfiguration;]$serialized" + . ./eng/common/TestResources/SubConfig-Helpers.ps1 + UpdateSubscriptionConfiguration $configBase $config displayName: Merge Test Resource Configurations