diff --git a/Tasks/AzurePowerShell/AzurePowerShell.ps1 b/Tasks/AzurePowerShell/AzurePowerShell.ps1 index 6e3c167613f6..5cb5d9a8248d 100644 --- a/Tasks/AzurePowerShell/AzurePowerShell.ps1 +++ b/Tasks/AzurePowerShell/AzurePowerShell.ps1 @@ -6,6 +6,8 @@ $scriptType = Get-VstsInput -Name ScriptType -Require $scriptPath = Get-VstsInput -Name ScriptPath $scriptInline = Get-VstsInput -Name Inline $scriptArguments = Get-VstsInput -Name ScriptArguments +$__vsts_input_errorActionPreference = Get-VstsInput -Name errorActionPreference +$__vsts_input_failOnStandardError = Get-VstsInput -Name FailOnStandardError $targetAzurePs = Get-VstsInput -Name TargetAzurePs $customTargetAzurePs = Get-VstsInput -Name CustomTargetAzurePs @@ -75,11 +77,12 @@ Update-PSModulePathForHostedAgent -targetAzurePs $targetAzurePs -authScheme $aut try { # Initialize Azure. Import-Module $PSScriptRoot\ps_modules\VstsAzureHelpers_ - Initialize-Azure -azurePsVersion $targetAzurePs + Initialize-Azure -azurePsVersion $targetAzurePs -strict # Trace the expression as it will be invoked. $__vstsAzPSInlineScriptPath = $null If ($scriptType -eq "InlineScript") { - $__vstsAzPSInlineScriptPath = [System.IO.Path]::Combine(([System.IO.Path]::GetTempPath()), ([guid]::NewGuid().ToString() + ".ps1")); + $scriptArguments = $null + $__vstsAzPSInlineScriptPath = [System.IO.Path]::Combine($env:Agent_TempDirectory, ([guid]::NewGuid().ToString() + ".ps1")); ($scriptInline | Out-File $__vstsAzPSInlineScriptPath) $scriptPath = $__vstsAzPSInlineScriptPath } @@ -118,20 +121,33 @@ try { # 2) The task result needs to be set to failed if an error record is encountered. # As mentioned above, the requirement to handle this is an implication of changing # the error action preference. - ([scriptblock]::Create($scriptCommand)) | + ([scriptblock]::Create($scriptCommand)) | ForEach-Object { Remove-Variable -Name scriptCommand Write-Host "##[command]$_" . $_ 2>&1 - } | + } | ForEach-Object { - # Put the object back into the pipeline. When doing this, the object needs - # to be wrapped in an array to prevent unraveling. - ,$_ - - # Set the task result to failed if the object is an error record. - if ($_ -is [System.Management.Automation.ErrorRecord]) { - "##vso[task.complete result=Failed]" + if($_ -is [System.Management.Automation.ErrorRecord]) { + if($_.FullyQualifiedErrorId -eq "NativeCommandError" -or $_.FullyQualifiedErrorId -eq "NativeCommandErrorMessage") { + ,$_ + if($__vsts_input_failOnStandardError -eq $true) { + "##vso[task.complete result=Failed]" + } + } + else { + if($__vsts_input_errorActionPreference -eq "continue") { + ,$_ + if($__vsts_input_failOnStandardError -eq $true) { + "##vso[task.complete result=Failed]" + } + } + elseif($__vsts_input_errorActionPreference -eq "stop") { + throw $_ + } + } + } else { + ,$_ } } } diff --git a/Tasks/AzurePowerShell/Strings/resources.resjson/en-US/resources.resjson b/Tasks/AzurePowerShell/Strings/resources.resjson/en-US/resources.resjson index 22aaf4e82e52..d3ac96623405 100644 --- a/Tasks/AzurePowerShell/Strings/resources.resjson/en-US/resources.resjson +++ b/Tasks/AzurePowerShell/Strings/resources.resjson/en-US/resources.resjson @@ -3,6 +3,8 @@ "loc.helpMarkDown": "[More Information](https://go.microsoft.com/fwlink/?LinkID=613749)", "loc.description": "Run a PowerShell script within an Azure environment", "loc.instanceNameFormat": "Azure PowerShell script: $(ScriptType)", + "loc.releaseNotes": "This is an early preview. Added support for Fail on standard error and ErrorActionPreference", + "loc.group.displayName.AzurePowerShellVersionOptions": "Azure PowerShell version options", "loc.input.label.ConnectedServiceNameSelector": "Azure Connection Type", "loc.input.label.ConnectedServiceName": "Azure Classic Subscription", "loc.input.help.ConnectedServiceName": "Azure Classic subscription to configure before running PowerShell", @@ -16,6 +18,10 @@ "loc.input.help.Inline": "Enter the script to execute.", "loc.input.label.ScriptArguments": "Script Arguments", "loc.input.help.ScriptArguments": "Additional parameters to pass to PowerShell. Can be either ordinal or named parameters.", + "loc.input.label.errorActionPreference": "ErrorActionPreference", + "loc.input.help.errorActionPreference": "Select the value of the ErrorActionPreference variable for executing the script.", + "loc.input.label.FailOnStandardError": "Fail on Standard Error", + "loc.input.help.FailOnStandardError": "If this is true, this task will fail if any errors are written to the error pipeline, or if any data is written to the Standard Error stream.", "loc.input.label.TargetAzurePs": "Azure PowerShell Version", "loc.input.help.TargetAzurePs": "In case of hosted agents, the supported Azure PowerShell Versions are: 2.1.0, 3.8.0, 4.2.1 and 5.1.1(Hosted VS2017 Queue), 3.6.0(Hosted Queue).\nTo pick the latest version available on the agent, select \"Latest installed version\".\n\nFor private agents you can specify preferred version of Azure PowerShell using \"Specify version\"", "loc.input.label.CustomTargetAzurePs": "Preferred Azure PowerShell Version", diff --git a/Tasks/AzurePowerShell/Tests/DoesNotFailOnStandardError.ps1 b/Tasks/AzurePowerShell/Tests/DoesNotFailOnStandardError.ps1 new file mode 100644 index 000000000000..421a00083a78 --- /dev/null +++ b/Tasks/AzurePowerShell/Tests/DoesNotFailOnStandardError.ps1 @@ -0,0 +1,24 @@ +[CmdletBinding()] +param() + +# Arrange. +. $PSScriptRoot\..\..\..\Tests\lib\Initialize-Test.ps1 +$targetAzurePs = "4.1.0" +Register-Mock Get-VstsInput { "FilePath" } -- -Name ScriptType -Require +Register-Mock Get-VstsInput { "$PSScriptRoot/RedirectsErrors_TargetScript.ps1" } -- -Name ScriptPath +Register-Mock Get-VstsInput { $targetAzurePs } -- -Name TargetAzurePs +Register-Mock Get-VstsInput { "continue" } -- -Name errorActionPreference +Register-Mock Get-VstsInput { $false } -- -Name FailOnStandardError +Register-Mock Update-PSModulePathForHostedAgent +Register-Mock Initialize-Azure + +# Act. +$actual = @( & $PSScriptRoot\..\AzurePowerShell.ps1 ) +$global:ErrorActionPreference = 'Stop' # Reset to stop. + +# Assert. +Assert-AreEqual 4 $actual.Length +Assert-AreEqual 'Some output 1' $actual[0] +Assert-AreEqual 'Some error 1' $actual[1].Exception.Message +Assert-AreEqual 'Some output 2' $actual[2] +Assert-AreEqual 'Some error 2' $actual[3].Exception.Message \ No newline at end of file diff --git a/Tasks/AzurePowerShell/Tests/DoesNotThrowForNativeCommandError.ps1 b/Tasks/AzurePowerShell/Tests/DoesNotThrowForNativeCommandError.ps1 new file mode 100644 index 000000000000..c5d4226df35d --- /dev/null +++ b/Tasks/AzurePowerShell/Tests/DoesNotThrowForNativeCommandError.ps1 @@ -0,0 +1,24 @@ +[CmdletBinding()] +param() + +# Arrange. +. $PSScriptRoot\..\..\..\Tests\lib\Initialize-Test.ps1 +$targetAzurePs = "4.1.0" +Register-Mock Get-VstsInput { "FilePath" } -- -Name ScriptType -Require +Register-Mock Get-VstsInput { "$PSScriptRoot/NativeCommandError_TargetScript.ps1" } -- -Name ScriptPath +Register-Mock Get-VstsInput { $targetAzurePs } -- -Name TargetAzurePs +Register-Mock Get-VstsInput { "stop" } -- -Name errorActionPreference +Register-Mock Get-VstsInput { $false } -- -Name FailOnStandardError +Register-Mock Update-PSModulePathForHostedAgent +Register-Mock Initialize-Azure + +# Act. +$actual = @( & $PSScriptRoot\..\AzurePowerShell.ps1 ) +$global:ErrorActionPreference = 'Stop' # Reset to stop. + +# Assert. +Assert-AreEqual 4 $actual.Length +Assert-AreEqual 'output 1' $actual[0] +Assert-AreEqual 'NativeCommandError' $actual[1].FullyQualifiedErrorId +Assert-AreEqual 'NativeCommandErrorMessage' $actual[2].FullyQualifiedErrorId +Assert-AreEqual 'output 2' $actual[3] \ No newline at end of file diff --git a/Tasks/AzurePowerShell/Tests/DoesNotUnravelOutput.ps1 b/Tasks/AzurePowerShell/Tests/DoesNotUnravelOutput.ps1 index 1c826b284c7a..b02b8d2ee4df 100644 --- a/Tasks/AzurePowerShell/Tests/DoesNotUnravelOutput.ps1 +++ b/Tasks/AzurePowerShell/Tests/DoesNotUnravelOutput.ps1 @@ -7,6 +7,8 @@ $targetAzurePs = "4.1.0" Register-Mock Get-VstsInput { "FilePath" } -- -Name ScriptType -Require Register-Mock Get-VstsInput { "$PSScriptRoot/DoesNotUnravelOutput_TargetScript.ps1" } -- -Name ScriptPath Register-Mock Get-VstsInput { $targetAzurePs } -- -Name TargetAzurePs +Register-Mock Get-VstsInput { "continue" } -- -Name errorActionPreference +Register-Mock Get-VstsInput { $true } -- -Name FailOnStandardError Register-Mock Update-PSModulePathForHostedAgent Register-Mock Initialize-Azure diff --git a/Tasks/AzurePowerShell/Tests/FailsForNativeCommandError.ps1 b/Tasks/AzurePowerShell/Tests/FailsForNativeCommandError.ps1 new file mode 100644 index 000000000000..e29f9a48996c --- /dev/null +++ b/Tasks/AzurePowerShell/Tests/FailsForNativeCommandError.ps1 @@ -0,0 +1,26 @@ +[CmdletBinding()] +param() + +# Arrange. +. $PSScriptRoot\..\..\..\Tests\lib\Initialize-Test.ps1 +$targetAzurePs = "4.1.0" +Register-Mock Get-VstsInput { "FilePath" } -- -Name ScriptType -Require +Register-Mock Get-VstsInput { "$PSScriptRoot/NativeCommandError_TargetScript.ps1" } -- -Name ScriptPath +Register-Mock Get-VstsInput { $targetAzurePs } -- -Name TargetAzurePs +Register-Mock Get-VstsInput { "silentlyContinue" } -- -Name errorActionPreference +Register-Mock Get-VstsInput { $true } -- -Name FailOnStandardError +Register-Mock Update-PSModulePathForHostedAgent +Register-Mock Initialize-Azure + +# Act. +$actual = @( & $PSScriptRoot\..\AzurePowerShell.ps1 ) +$global:ErrorActionPreference = 'Stop' # Reset to stop. + +# Assert. +Assert-AreEqual 6 $actual.Length +Assert-AreEqual 'output 1' $actual[0] +Assert-AreEqual 'NativeCommandError' $actual[1].FullyQualifiedErrorId +Assert-AreEqual '##vso[task.complete result=Failed]' $actual[2] +Assert-AreEqual 'NativeCommandErrorMessage' $actual[3].FullyQualifiedErrorId +Assert-AreEqual '##vso[task.complete result=Failed]' $actual[4] +Assert-AreEqual 'output 2' $actual[5] \ No newline at end of file diff --git a/Tasks/AzurePowerShell/Tests/L0.ts b/Tasks/AzurePowerShell/Tests/L0.ts index 6d6f47852428..cd89d48d4bdb 100644 --- a/Tasks/AzurePowerShell/Tests/L0.ts +++ b/Tasks/AzurePowerShell/Tests/L0.ts @@ -39,6 +39,9 @@ describe('AzurePowerShell Suite', function () { it('redirects errors', (done) => { psr.run(path.join(__dirname, 'RedirectsErrors.ps1'), done); }) + it('does not fail if failonstandarderror is set to false', (done) => { + psr.run(path.join(__dirname, 'DoesNotFailOnStandardError.ps1'), done); + }) it('removes functions and variables', (done) => { psr.run(path.join(__dirname, 'RemovesFunctionsAndVariables.ps1'), done); }) @@ -51,6 +54,12 @@ describe('AzurePowerShell Suite', function () { it('throws when invalid script path', (done) => { psr.run(path.join(__dirname, 'ThrowsWhenInvalidScriptPath.ps1'), done); }) + it('does not fail if native command writes to stderr and failonstderr is false', (done) => { + psr.run(path.join(__dirname, 'DoesNotThrowForNativeCommandError.ps1'), done); + }) + it('fails for native command error if fail on standard error is true', (done) => { + psr.run(path.join(__dirname, 'FailsForNativeCommandError.ps1'), done); + }) it('Get-LatestModule returns the latest available module', (done) => { psr.run(path.join(__dirname, 'Utility.Get-LatestModule.ps1'), done); }) diff --git a/Tasks/AzurePowerShell/Tests/NativeCommandError_TargetScript.ps1 b/Tasks/AzurePowerShell/Tests/NativeCommandError_TargetScript.ps1 new file mode 100644 index 000000000000..1a08ec6c770a --- /dev/null +++ b/Tasks/AzurePowerShell/Tests/NativeCommandError_TargetScript.ps1 @@ -0,0 +1,3 @@ +Write-Output "output 1" +net user foobar +Write-Output "output 2" \ No newline at end of file diff --git a/Tasks/AzurePowerShell/Tests/PerformsBasicFlow.ps1 b/Tasks/AzurePowerShell/Tests/PerformsBasicFlow.ps1 index a81c9d342aaf..f2eb1dc3caff 100644 --- a/Tasks/AzurePowerShell/Tests/PerformsBasicFlow.ps1 +++ b/Tasks/AzurePowerShell/Tests/PerformsBasicFlow.ps1 @@ -8,6 +8,8 @@ Register-Mock Get-VstsInput { "FilePath" } -- -Name ScriptType -Require Register-Mock Get-VstsInput { "$PSScriptRoot/PerformsBasicFlow_TargetScript.ps1" } -- -Name ScriptPath Register-Mock Get-VstsInput { $targetAzurePs } -- -Name TargetAzurePs Register-Mock Get-VstsInput { 'arg1 arg2' } -- -Name ScriptArguments +Register-Mock Get-VstsInput { "continue" } -- -Name errorActionPreference +Register-Mock Get-VstsInput { $true } -- -Name FailOnStandardError Register-Mock Update-PSModulePathForHostedAgent Register-Mock Initialize-Azure Register-Mock Get-VstsEndpoint { @{auth = @{ scheme = "ServicePrincipal" }} } diff --git a/Tasks/AzurePowerShell/Tests/RedirectsErrors.ps1 b/Tasks/AzurePowerShell/Tests/RedirectsErrors.ps1 index 8d6ed76dcdc2..ba43a74c14b2 100644 --- a/Tasks/AzurePowerShell/Tests/RedirectsErrors.ps1 +++ b/Tasks/AzurePowerShell/Tests/RedirectsErrors.ps1 @@ -7,6 +7,8 @@ $targetAzurePs = "4.1.0" Register-Mock Get-VstsInput { "FilePath" } -- -Name ScriptType -Require Register-Mock Get-VstsInput { "$PSScriptRoot/RedirectsErrors_TargetScript.ps1" } -- -Name ScriptPath Register-Mock Get-VstsInput { $targetAzurePs } -- -Name TargetAzurePs +Register-Mock Get-VstsInput { "continue" } -- -Name errorActionPreference +Register-Mock Get-VstsInput { $true } -- -Name FailOnStandardError Register-Mock Update-PSModulePathForHostedAgent Register-Mock Initialize-Azure diff --git a/Tasks/AzurePowerShell/Tests/RemovesFunctionsAndVariables.ps1 b/Tasks/AzurePowerShell/Tests/RemovesFunctionsAndVariables.ps1 index 413195b3ccd5..0b15a8f29d6c 100644 --- a/Tasks/AzurePowerShell/Tests/RemovesFunctionsAndVariables.ps1 +++ b/Tasks/AzurePowerShell/Tests/RemovesFunctionsAndVariables.ps1 @@ -9,6 +9,8 @@ $targetAzurePs = "4.1.0" Register-Mock Get-VstsInput { "FilePath" } -- -Name ScriptType -Require Register-Mock Get-VstsInput { "$PSScriptRoot/RemovesFunctionsAndVariables_TargetScript.ps1" } -- -Name ScriptPath Register-Mock Get-VstsInput { $targetAzurePs } -- -Name TargetAzurePs +Register-Mock Get-VstsInput { "continue" } -- -Name errorActionPreference +Register-Mock Get-VstsInput { $true } -- -Name FailOnStandardError Register-Mock Update-PSModulePathForHostedAgent # Arrange the mock task SDK module. diff --git a/Tasks/AzurePowerShell/Tests/ValidateInlineScriptFlow.ps1 b/Tasks/AzurePowerShell/Tests/ValidateInlineScriptFlow.ps1 index 4ca11acc4397..afe41ceb7533 100644 --- a/Tasks/AzurePowerShell/Tests/ValidateInlineScriptFlow.ps1 +++ b/Tasks/AzurePowerShell/Tests/ValidateInlineScriptFlow.ps1 @@ -5,9 +5,14 @@ param() . $PSScriptRoot\..\..\..\Tests\lib\Initialize-Test.ps1 Unregister-Mock Get-VstsInput $targetAzurePs = "4.1.0" +if([string]::IsNullOrEmpty($env:Agent_TempDirectory)) { + $env:Agent_TempDirectory = $env:TEMP +} Register-Mock Get-VstsInput { "InlineScript" } -- -Name ScriptType -Require Register-Mock Get-VstsInput { ",@( 'item 1', 'item 2')" } -- -Name Inline Register-Mock Get-VstsInput { $targetAzurePs } -- -Name TargetAzurePs +Register-Mock Get-VstsInput { "continue" } -- -Name errorActionPreference +Register-Mock Get-VstsInput { $true } -- -Name FailOnStandardError Register-Mock Update-PSModulePathForHostedAgent Register-Mock Initialize-Azure diff --git a/Tasks/AzurePowerShell/task.json b/Tasks/AzurePowerShell/task.json index 8dd17bc6cd60..66aff5753e94 100644 --- a/Tasks/AzurePowerShell/task.json +++ b/Tasks/AzurePowerShell/task.json @@ -15,14 +15,23 @@ ], "author": "Microsoft Corporation", "version": { - "Major": 2, + "Major": 3, "Minor": 0, - "Patch": 7 + "Patch": 0 }, + "releaseNotes": "This is an early preview. Added support for Fail on standard error and ErrorActionPreference", "demands": [ "azureps" ], - "minimumAgentVersion": "1.95.0", + "preview": true, + "groups": [ + { + "name": "AzurePowerShellVersionOptions", + "displayName": "Azure PowerShell version options", + "isExpanded": true + } + ], + "minimumAgentVersion": "2.0.0", "inputs": [ { "name": "ConnectedServiceNameSelector", @@ -59,9 +68,9 @@ }, { "name": "ScriptType", - "type": "pickList", + "type": "radio", "label": "Script Type", - "required": true, + "required": false, "helpMarkDown": "Type of the script: File Path or Inline Script", "defaultValue": "FilePath", "options": { @@ -97,16 +106,38 @@ "type": "string", "label": "Script Arguments", "defaultValue": "", + "visibleRule": "ScriptType = FilePath", "required": false, "properties": { "editorExtension": "ms.vss-services-azure.parameters-grid" }, "helpMarkDown": "Additional parameters to pass to PowerShell. Can be either ordinal or named parameters." }, + { + "name": "errorActionPreference", + "type": "pickList", + "label": "ErrorActionPreference", + "required": false, + "defaultValue": "stop", + "options": { + "stop": "Stop", + "continue": "Continue", + "silentlyContinue": "SilentlyContinue" + }, + "helpMarkDown": "Select the value of the ErrorActionPreference variable for executing the script." + }, + { + "name": "FailOnStandardError", + "type": "boolean", + "label": "Fail on Standard Error", + "required": false, + "defaultValue": "false", + "helpMarkDown": "If this is true, this task will fail if any errors are written to the error pipeline, or if any data is written to the Standard Error stream." + }, { "name": "TargetAzurePs", "aliases": ["azurePowerShellVersion"], - "type": "pickList", + "type": "radio", "label": "Azure PowerShell Version", "defaultValue": "OtherVersion", "required": false, @@ -114,6 +145,7 @@ "LatestVersion": "Latest installed version", "OtherVersion": "Specify other version" }, + "groupName": "AzurePowerShellVersionOptions", "helpMarkDown": "In case of hosted agents, the supported Azure PowerShell Versions are: 2.1.0, 3.8.0, 4.2.1 and 5.1.1(Hosted VS2017 Queue), 3.6.0(Hosted Queue).\nTo pick the latest version available on the agent, select \"Latest installed version\".\n\nFor private agents you can specify preferred version of Azure PowerShell using \"Specify version\"" }, { @@ -124,6 +156,7 @@ "defaultValue": "", "required": true, "visibleRule": "TargetAzurePs = OtherVersion", + "groupName": "AzurePowerShellVersionOptions", "helpMarkDown": "Preferred Azure PowerShell Version needs to be a proper semantic version eg. 1.2.3. Regex like 2.\\*,2.3.\\* is not supported. The Hosted VS2017 Pool currently supports versions: 2.1.0, 3.8.0, 4.2.1, 5.1.1" } ], diff --git a/Tasks/AzurePowerShell/task.loc.json b/Tasks/AzurePowerShell/task.loc.json index aafdec72d311..12db0be5558d 100644 --- a/Tasks/AzurePowerShell/task.loc.json +++ b/Tasks/AzurePowerShell/task.loc.json @@ -15,14 +15,23 @@ ], "author": "Microsoft Corporation", "version": { - "Major": 2, + "Major": 3, "Minor": 0, - "Patch": 7 + "Patch": 0 }, + "releaseNotes": "ms-resource:loc.releaseNotes", "demands": [ "azureps" ], - "minimumAgentVersion": "1.95.0", + "preview": true, + "groups": [ + { + "name": "AzurePowerShellVersionOptions", + "displayName": "ms-resource:loc.group.displayName.AzurePowerShellVersionOptions", + "isExpanded": true + } + ], + "minimumAgentVersion": "2.0.0", "inputs": [ { "name": "ConnectedServiceNameSelector", @@ -65,9 +74,9 @@ }, { "name": "ScriptType", - "type": "pickList", + "type": "radio", "label": "ms-resource:loc.input.label.ScriptType", - "required": true, + "required": false, "helpMarkDown": "ms-resource:loc.input.help.ScriptType", "defaultValue": "FilePath", "options": { @@ -103,18 +112,40 @@ "type": "string", "label": "ms-resource:loc.input.label.ScriptArguments", "defaultValue": "", + "visibleRule": "ScriptType = FilePath", "required": false, "properties": { "editorExtension": "ms.vss-services-azure.parameters-grid" }, "helpMarkDown": "ms-resource:loc.input.help.ScriptArguments" }, + { + "name": "errorActionPreference", + "type": "pickList", + "label": "ms-resource:loc.input.label.errorActionPreference", + "required": false, + "defaultValue": "stop", + "options": { + "stop": "Stop", + "continue": "Continue", + "silentlyContinue": "SilentlyContinue" + }, + "helpMarkDown": "ms-resource:loc.input.help.errorActionPreference" + }, + { + "name": "FailOnStandardError", + "type": "boolean", + "label": "ms-resource:loc.input.label.FailOnStandardError", + "required": false, + "defaultValue": "false", + "helpMarkDown": "ms-resource:loc.input.help.FailOnStandardError" + }, { "name": "TargetAzurePs", "aliases": [ "azurePowerShellVersion" ], - "type": "pickList", + "type": "radio", "label": "ms-resource:loc.input.label.TargetAzurePs", "defaultValue": "OtherVersion", "required": false, @@ -122,6 +153,7 @@ "LatestVersion": "Latest installed version", "OtherVersion": "Specify other version" }, + "groupName": "AzurePowerShellVersionOptions", "helpMarkDown": "ms-resource:loc.input.help.TargetAzurePs" }, { @@ -134,6 +166,7 @@ "defaultValue": "", "required": true, "visibleRule": "TargetAzurePs = OtherVersion", + "groupName": "AzurePowerShellVersionOptions", "helpMarkDown": "ms-resource:loc.input.help.CustomTargetAzurePs" } ], diff --git a/Tasks/Common/VstsAzureHelpers_/ImportFunctions.ps1 b/Tasks/Common/VstsAzureHelpers_/ImportFunctions.ps1 index a5ae786c6f9f..fef1ae8f297a 100644 --- a/Tasks/Common/VstsAzureHelpers_/ImportFunctions.ps1 +++ b/Tasks/Common/VstsAzureHelpers_/ImportFunctions.ps1 @@ -4,7 +4,8 @@ [Parameter(Mandatory = $true)] [ValidateSet('Azure', 'AzureRM')] [string[]] $PreferredModule, - [string] $azurePsVersion) + [string] $azurePsVersion, + [switch] $strict) Trace-VstsEnteringInvocation $MyInvocation try { @@ -14,39 +15,42 @@ $azure = (Import-FromModulePath -Classic:$true -azurePsVersion $azurePsVersion) -or (Import-FromSdkPath -Classic:$true -azurePsVersion $azurePsVersion) $azureRM = (Import-FromModulePath -Classic:$false -azurePsVersion $azurePsVersion) -or (Import-FromSdkPath -Classic:$false -azurePsVersion $azurePsVersion) if (!$azure -and !$azureRM) { - Discover-AvailableAzureModules - if ($azurePsVersion) { - throw (Get-VstsLocString -Key AZ_ModuleNotFound -ArgumentList $azurePsVersion) - } else { - throw (Get-VstsLocString -Key AZ_ModuleNotFound -ArgumentList "Any version") - } + ThrowAzureModuleNotFoundException -azurePsVersion $azurePsVersion -modules "Azure, AzureRM" } } elseif ($PreferredModule -contains 'Azure') { - # Attempt to import Azure but fallback to AzureRM. + # Attempt to import Azure but fallback to AzureRM unless strict is specified. if (!(Import-FromModulePath -Classic:$true -azurePsVersion $azurePsVersion) -and - !(Import-FromSdkPath -Classic:$true -azurePsVersion $azurePsVersion) -and - !(Import-FromModulePath -Classic:$false -azurePsVersion $azurePsVersion) -and - !(Import-FromSdkPath -Classic:$false -azurePsVersion $azurePsVersion)) + !(Import-FromSdkPath -Classic:$true -azurePsVersion $azurePsVersion)) { - Discover-AvailableAzureModules - if ($azurePsVersion) { - throw (Get-VstsLocString -Key AZ_ModuleNotFound -ArgumentList $azurePsVersion) - } else { - throw (Get-VstsLocString -Key AZ_ModuleNotFound -ArgumentList "Any version") + if ($strict -eq $true) + { + ThrowAzureModuleNotFoundException -azurePsVersion $azurePsVersion -modules "Azure" + } + else + { + if(!(Import-FromModulePath -Classic:$false -azurePsVersion $azurePsVersion) -and + !(Import-FromSdkPath -Classic:$false -azurePsVersion $azurePsVersion)) + { + ThrowAzureModuleNotFoundException -azurePsVersion $azurePsVersion -modules "Azure, AzureRM" + } } } } else { - # Attempt to import AzureRM but fallback to Azure. + # Attempt to import AzureRM but fallback to Azure unless strict is specified if (!(Import-FromModulePath -Classic:$false -azurePsVersion $azurePsVersion) -and - !(Import-FromSdkPath -Classic:$false -azurePsVersion $azurePsVersion) -and - !(Import-FromModulePath -Classic:$true -azurePsVersion $azurePsVersion) -and - !(Import-FromSdkPath -Classic:$true -azurePsVersion $azurePsVersion)) + !(Import-FromSdkPath -Classic:$false -azurePsVersion $azurePsVersion)) { - Discover-AvailableAzureModules - if ($azurePsVersion) { - throw (Get-VstsLocString -Key AZ_ModuleNotFound -ArgumentList $azurePsVersion) - } else { - throw (Get-VstsLocString -Key AZ_ModuleNotFound -ArgumentList "Any version") + if ($strict -eq $true) + { + ThrowAzureModuleNotFoundException -azurePsVersion $azurePsVersion -modules "AzureRM" + } + else + { + if(!(Import-FromModulePath -Classic:$true -azurePsVersion $azurePsVersion) -and + !(Import-FromSdkPath -Classic:$true -azurePsVersion $azurePsVersion)) + { + ThrowAzureModuleNotFoundException -azurePsVersion $azurePsVersion -modules "Azure, AzureRM" + } } } } @@ -214,6 +218,17 @@ function Import-AzureRmSubmodulesFromSdkPath { } } +function ThrowAzureModuleNotFoundException { + param([string] $azurePsVersion, + [string] $modules) + Discover-AvailableAzureModules + if ($azurePsVersion) { + throw (Get-VstsLocString -Key AZ_ModuleNotFound -ArgumentList $azurePsVersion, $modules) + } else { + throw (Get-VstsLocString -Key AZ_ModuleNotFound -ArgumentList "Any version", $modules) + } +} + function Discover-AvailableAzureModules { $env:PSModulePath = $env:SystemDrive + "\Modules;" + $env:PSModulePath Write-Host $(Get-VstsLocString -Key AZ_AvailableModules -ArgumentList "Azure") diff --git a/Tasks/Common/VstsAzureHelpers_/InitializeFunctions.ps1 b/Tasks/Common/VstsAzureHelpers_/InitializeFunctions.ps1 index fa7bc8a74941..b4fd58267d23 100644 --- a/Tasks/Common/VstsAzureHelpers_/InitializeFunctions.ps1 +++ b/Tasks/Common/VstsAzureHelpers_/InitializeFunctions.ps1 @@ -90,8 +90,13 @@ function Initialize-AzureSubscription { # Add account (AzureRM). if ($script:azureRMProfileModule) { try { - Write-Host "##[command]Add-AzureRMAccount -Credential $psCredential" - $null = Add-AzureRMAccount -Credential $psCredential + if (Get-Command -Name "Add-AzureRmAccount" -ErrorAction "SilentlyContinue") { + Write-Host "##[command] Add-AzureRMAccount -Credential $psCredential" + $null = Add-AzureRMAccount -Credential $psCredential + } else { + Write-Host "##[command] Connect-AzureRMAccount -Credential $psCredential" + $null = Connect-AzureRMAccount -Credential $psCredential + } } catch { # Provide an additional, custom, credentials-related error message. Write-VstsTaskError -Message $_.Exception.Message @@ -130,15 +135,19 @@ function Initialize-AzureSubscription { } else { # Else, this is AzureRM. try { - if(CmdletHasMember -cmdlet "Add-AzureRMAccount" -memberName "EnvironmentName") - { - Write-Host "##[command]Add-AzureRMAccount -ServicePrincipal -Tenant $($Endpoint.Auth.Parameters.TenantId) -Credential $psCredential -EnvironmentName $environmentName" - $null = Add-AzureRMAccount -ServicePrincipal -Tenant $Endpoint.Auth.Parameters.TenantId -Credential $psCredential -EnvironmentName $environmentName + if (Get-Command -Name "Add-AzureRmAccount" -ErrorAction "SilentlyContinue") { + if (CmdletHasMember -cmdlet "Add-AzureRMAccount" -memberName "EnvironmentName") { + Write-Host "##[command]Add-AzureRMAccount -ServicePrincipal -Tenant $($Endpoint.Auth.Parameters.TenantId) -Credential $psCredential -EnvironmentName $environmentName" + $null = Add-AzureRMAccount -ServicePrincipal -Tenant $Endpoint.Auth.Parameters.TenantId -Credential $psCredential -EnvironmentName $environmentName + } + else { + Write-Host "##[command]Add-AzureRMAccount -ServicePrincipal -Tenant $($Endpoint.Auth.Parameters.TenantId) -Credential $psCredential -Environment $environmentName" + $null = Add-AzureRMAccount -ServicePrincipal -Tenant $Endpoint.Auth.Parameters.TenantId -Credential $psCredential -Environment $environmentName + } } - else - { - Write-Host "##[command]Add-AzureRMAccount -ServicePrincipal -Tenant $($Endpoint.Auth.Parameters.TenantId) -Credential $psCredential -Environment $environmentName" - $null = Add-AzureRMAccount -ServicePrincipal -Tenant $Endpoint.Auth.Parameters.TenantId -Credential $psCredential -Environment $environmentName + else { + Write-Host "##[command]Connect-AzureRMAccount -ServicePrincipal -Tenant $($Endpoint.Auth.Parameters.TenantId) -Credential $psCredential -Environment $environmentName" + $null = Connect-AzureRMAccount -ServicePrincipal -Tenant $Endpoint.Auth.Parameters.TenantId -Credential $psCredential -Environment $environmentName } } catch { # Provide an additional, custom, credentials-related error message. @@ -182,8 +191,15 @@ function Set-CurrentAzureRMSubscription { $additional = @{ } if ($TenantId) { $additional['TenantId'] = $TenantId } - Write-Host "##[command]Select-AzureRMSubscription -SubscriptionId $SubscriptionId $(Format-Splat $additional)" - $null = Select-AzureRMSubscription -SubscriptionId $SubscriptionId @additional + + if (Get-Command -Name "Select-AzureRmSubscription" -ErrorAction "SilentlyContinue") { + Write-Host "##[command] Select-AzureRMSubscription -SubscriptionId $SubscriptionId $(Format-Splat $additional)" + $null = Select-AzureRMSubscription -SubscriptionId $SubscriptionId @additional + } + else { + Write-Host "##[command] Set-AzureRmContext -SubscriptionId $SubscriptionId $(Format-Splat $additional)" + $null = Set-AzureRmContext -SubscriptionId $SubscriptionId @additional + } } function Set-UserAgent { @@ -225,7 +241,7 @@ function CmdletHasMember { } catch { - return false; + return $false; } } diff --git a/Tasks/Common/VstsAzureHelpers_/Strings/resources.resjson/en-US/resources.resjson b/Tasks/Common/VstsAzureHelpers_/Strings/resources.resjson/en-US/resources.resjson index abbd1d8ea127..7704f009e32b 100644 --- a/Tasks/Common/VstsAzureHelpers_/Strings/resources.resjson/en-US/resources.resjson +++ b/Tasks/Common/VstsAzureHelpers_/Strings/resources.resjson/en-US/resources.resjson @@ -2,7 +2,7 @@ "loc.messages.AZ_AzureRMProfileModuleNotFound": "Module 'AzureRM.Profile' not found. The 'AzureRM' module may not be fully installed. Running the following PowerShell commands from an elevated session may resolve the issue: Import-Module AzureRM ; Install-AzureRM", "loc.messages.AZ_CertificateAuthNotSupported": "Certificate based authentication is not supported. Azure PowerShell module is not found.", "loc.messages.AZ_CredentialsError": "There was an error with the Azure credentials used for the deployment.", - "loc.messages.AZ_ModuleNotFound": "Neither the Azure module ('{0}') nor the AzureRM module ('{0}') was found. If the module was recently installed, retry after restarting the VSTS task agent.", + "loc.messages.AZ_ModuleNotFound": "Could not find the modules: '{1}' with Version: '{0}'. If the module was recently installed, retry after restarting the VSTS task agent.", "loc.messages.AZ_RequiresMinVersion0": "The required minimum version ({0}) of the Azure PowerShell module is not installed.", "loc.messages.AZ_ServicePrincipalError": "There was an error with the service principal used for the deployment.", "loc.messages.AZ_ServicePrincipalAuthNotSupportedAzureVersion0": "Service principal authentication is not supported in version '{0}' of the Azure module.", diff --git a/Tasks/Common/VstsAzureHelpers_/Tests/Import-AzureModule.ThrowsWhenNotFound.ps1 b/Tasks/Common/VstsAzureHelpers_/Tests/Import-AzureModule.ThrowsWhenNotFound.ps1 index 8b782fda6a92..3f9c917345a3 100644 --- a/Tasks/Common/VstsAzureHelpers_/Tests/Import-AzureModule.ThrowsWhenNotFound.ps1 +++ b/Tasks/Common/VstsAzureHelpers_/Tests/Import-AzureModule.ThrowsWhenNotFound.ps1 @@ -16,5 +16,5 @@ foreach ($variableSet in $variableSets) { Write-Verbose ('-' * 80) # Act/Assert. - Assert-Throws { & $module Import-AzureModule -PreferredModule $variableSet.PreferredModule -azurePsVersion "4.1.0" } -MessagePattern "AZ_ModuleNotFound 4.1.0" + Assert-Throws { & $module Import-AzureModule -PreferredModule $variableSet.PreferredModule -azurePsVersion "4.1.0" } -MessagePattern "AZ_ModuleNotFound 4.1.0 Azure, AzureRM" } diff --git a/Tasks/Common/VstsAzureHelpers_/Tests/Initialize-Azure.PassesInputs.ps1 b/Tasks/Common/VstsAzureHelpers_/Tests/Initialize-Azure.PassesInputs.ps1 index a0bc26868f5c..fc50c5a00945 100644 --- a/Tasks/Common/VstsAzureHelpers_/Tests/Initialize-Azure.PassesInputs.ps1 +++ b/Tasks/Common/VstsAzureHelpers_/Tests/Initialize-Azure.PassesInputs.ps1 @@ -93,6 +93,6 @@ foreach ($variableSet in $variableSets) { Initialize-Azure # Assert. - Assert-WasCalled Import-AzureModule -- -PreferredModule $variableSet.ExpectedPreferredModule -azurePsVersion "" + Assert-WasCalled Import-AzureModule -- -PreferredModule $variableSet.ExpectedPreferredModule -azurePsVersion "" -strict:$false Assert-WasCalled Initialize-AzureSubscription -- -Endpoint $variableSet.Endpoint -StorageAccount $variableSet.StorageAccount } diff --git a/Tasks/Common/VstsAzureHelpers_/VstsAzureHelpers_.psm1 b/Tasks/Common/VstsAzureHelpers_/VstsAzureHelpers_.psm1 index 398213f98d85..5c2e5c97edd8 100644 --- a/Tasks/Common/VstsAzureHelpers_/VstsAzureHelpers_.psm1 +++ b/Tasks/Common/VstsAzureHelpers_/VstsAzureHelpers_.psm1 @@ -18,7 +18,8 @@ Import-VstsLocStrings -LiteralPath $PSScriptRoot/module.json # This is the only public function. function Initialize-Azure { [CmdletBinding()] - param( [string] $azurePsVersion ) + param( [string] $azurePsVersion, + [switch] $strict ) Trace-VstsEnteringInvocation $MyInvocation try { # Get the inputs. @@ -36,7 +37,7 @@ function Initialize-Azure { $preferredModules = @( ) if ($endpoint.Auth.Scheme -eq 'ServicePrincipal') { $preferredModules += 'AzureRM' - } elseif ($endpoint.Auth.Scheme -eq 'UserNamePassword') { + } elseif ($endpoint.Auth.Scheme -eq 'UserNamePassword' -and $strict -eq $false) { $preferredModules += 'Azure' $preferredModules += 'AzureRM' } else { @@ -44,7 +45,7 @@ function Initialize-Azure { } # Import/initialize the Azure module. - Import-AzureModule -PreferredModule $preferredModules -azurePsVersion $azurePsVersion + Import-AzureModule -PreferredModule $preferredModules -azurePsVersion $azurePsVersion -strict:$strict Initialize-AzureSubscription -Endpoint $endpoint -StorageAccount $storageAccount } finally { Trace-VstsLeavingInvocation $MyInvocation diff --git a/Tasks/Common/VstsAzureHelpers_/module.json b/Tasks/Common/VstsAzureHelpers_/module.json index e770dec7b110..4f6793a1b6a6 100644 --- a/Tasks/Common/VstsAzureHelpers_/module.json +++ b/Tasks/Common/VstsAzureHelpers_/module.json @@ -3,7 +3,7 @@ "AZ_AzureRMProfileModuleNotFound": "Module 'AzureRM.Profile' not found. The 'AzureRM' module may not be fully installed. Running the following PowerShell commands from an elevated session may resolve the issue: Import-Module AzureRM ; Install-AzureRM", "AZ_CertificateAuthNotSupported": "Certificate based authentication is not supported. Azure PowerShell module is not found.", "AZ_CredentialsError": "There was an error with the Azure credentials used for the deployment.", - "AZ_ModuleNotFound": "Neither the Azure module ('{0}') nor the AzureRM module ('{0}') was found. If the module was recently installed, retry after restarting the VSTS task agent.", + "AZ_ModuleNotFound": "Could not find the modules: '{1}' with Version: '{0}'. If the module was recently installed, retry after restarting the VSTS task agent.", "AZ_RequiresMinVersion0": "The required minimum version ({0}) of the Azure PowerShell module is not installed.", "AZ_ServicePrincipalError": "There was an error with the service principal used for the deployment.", "AZ_ServicePrincipalAuthNotSupportedAzureVersion0": "Service principal authentication is not supported in version '{0}' of the Azure module.",