Skip to content

Commit

Permalink
Support for inline script in azurepowershell task
Browse files Browse the repository at this point in the history
* Support for inline script in azurepowershell task

* Resolving CR comments
  • Loading branch information
pragupta-ms authored Dec 5, 2016
1 parent 8cc3b3c commit d923fb1
Show file tree
Hide file tree
Showing 11 changed files with 162 additions and 58 deletions.
106 changes: 62 additions & 44 deletions Tasks/AzurePowerShell/AzurePowerShell.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -2,64 +2,82 @@ Trace-VstsEnteringInvocation $MyInvocation
Import-VstsLocStrings "$PSScriptRoot\Task.json"

# Get inputs.
$scriptPath = Get-VstsInput -Name ScriptPath -Require
$scriptType = Get-VstsInput -Name ScriptType -Require
$scriptPath = Get-VstsInput -Name ScriptPath
$scriptInline = Get-VstsInput -Name Inline
$scriptArguments = Get-VstsInput -Name ScriptArguments

# Validate the script path and args do not contains new-lines. Otherwise, it will
# break invoking the script via Invoke-Expression.
if ($scriptPath -match '[\r\n]') {
throw (Get-VstsLocString -Key InvalidScriptPath0 -ArgumentList $scriptPath)
if ($scriptType -eq "FilePath") {
if ($scriptPath -match '[\r\n]' -or [string]::IsNullOrWhitespace($scriptPath)) {
throw (Get-VstsLocString -Key InvalidScriptPath0 -ArgumentList $scriptPath)
}
}

if ($scriptArguments -match '[\r\n]') {
throw (Get-VstsLocString -Key InvalidScriptArguments0 -ArgumentList $scriptArguments)
}

# Initialize Azure.
Import-Module $PSScriptRoot\ps_modules\VstsAzureHelpers_
Initialize-Azure
try {
# Initialize Azure.
Import-Module $PSScriptRoot\ps_modules\VstsAzureHelpers_
Initialize-Azure

# Trace the expression as it will be invoked.
If ($scriptType -eq "InlineScript") {
$tempFileName = [guid]::NewGuid().ToString() + ".ps1";
$scriptPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), $tempFileName);
($scriptInline | Out-File $scriptPath)
}

# Trace the expression as it will be invoked.
$scriptCommand = "& '$($scriptPath.Replace("'", "''"))' $scriptArguments"
Remove-Variable -Name scriptPath
Remove-Variable -Name scriptArguments
$scriptCommand = "& '$($scriptPath.Replace("'", "''"))' $scriptArguments"
Remove-Variable -Name scriptArguments

# Remove all commands imported from VstsTaskSdk, other than Out-Default.
# Remove all commands imported from VstsAzureHelpers_.
Get-ChildItem -LiteralPath function: |
Where-Object {
($_.ModuleName -eq 'VstsTaskSdk' -and $_.Name -ne 'Out-Default') -or
($_.Name -eq 'Invoke-VstsTaskScript') -or
($_.ModuleName -eq 'VstsAzureHelpers_' )
} |
Remove-Item
# Remove all commands imported from VstsTaskSdk, other than Out-Default.
# Remove all commands imported from VstsAzureHelpers_.
Get-ChildItem -LiteralPath function: |
Where-Object {
($_.ModuleName -eq 'VstsTaskSdk' -and $_.Name -ne 'Out-Default') -or
($_.Name -eq 'Invoke-VstsTaskScript') -or
($_.ModuleName -eq 'VstsAzureHelpers_' )
} |
Remove-Item

# For compatibility with the legacy handler implementation, set the error action
# preference to continue. An implication of changing the preference to Continue,
# is that Invoke-VstsTaskScript will no longer handle setting the result to failed.
$global:ErrorActionPreference = 'Continue'
# For compatibility with the legacy handler implementation, set the error action
# preference to continue. An implication of changing the preference to Continue,
# is that Invoke-VstsTaskScript will no longer handle setting the result to failed.
$global:ErrorActionPreference = 'Continue'

# Run the user's script. Redirect the error pipeline to the output pipeline to enable
# a couple goals due to compatibility with the legacy handler implementation:
# 1) STDERR from external commands needs to be converted into error records. Piping
# the redirected error output to an intermediate command before it is piped to
# Out-Default will implicitly perform the conversion.
# 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)) |
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.
,$_
# Run the user's script. Redirect the error pipeline to the output pipeline to enable
# a couple goals due to compatibility with the legacy handler implementation:
# 1) STDERR from external commands needs to be converted into error records. Piping
# the redirected error output to an intermediate command before it is piped to
# Out-Default will implicitly perform the conversion.
# 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)) |
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]"
# Set the task result to failed if the object is an error record.
if ($_ -is [System.Management.Automation.ErrorRecord]) {
"##vso[task.complete result=Failed]"
}
}
}
Finally {
If ($scriptType -eq "InlineScript" -and (Test-Path $scriptPath) -eq $true ) {
Remove-Item $scriptPath -ErrorAction 'SilentlyContinue'
}

Remove-Variable -Name scriptPath
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@
"loc.friendlyName": "Azure PowerShell",
"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: $(ScriptPath)",
"loc.instanceNameFormat": "Azure PowerShell script: $(ScriptType)",
"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",
"loc.input.label.ConnectedServiceNameARM": "Azure RM Subscription",
"loc.input.help.ConnectedServiceNameARM": "Azure Resource Manager subscription to configure before running PowerShell",
"loc.input.label.ScriptType": "Script Type",
"loc.input.help.ScriptType": "Type of the script: File Path or Inline Script",
"loc.input.label.ScriptPath": "Script Path",
"loc.input.help.ScriptPath": "Path of the script. Should be fully qualified path or relative to the default working directory.",
"loc.input.label.Inline": "Inline Script",
"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.messages.InvalidScriptArguments0": "Invalid script arguments '{0}'. Line breaks are not allowed.",
Expand Down
3 changes: 2 additions & 1 deletion Tasks/AzurePowerShell/Tests/DoesNotUnravelOutput.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ param()

# Arrange.
. $PSScriptRoot\..\..\..\Tests\lib\Initialize-Test.ps1
Register-Mock Get-VstsInput { "$PSScriptRoot/DoesNotUnravelOutput_TargetScript.ps1" } -- -Name ScriptPath -Require
Register-Mock Get-VstsInput { "FilePath" } -- -Name ScriptType -Require
Register-Mock Get-VstsInput { "$PSScriptRoot/DoesNotUnravelOutput_TargetScript.ps1" } -- -Name ScriptPath
Register-Mock Initialize-Azure

# Act.
Expand Down
3 changes: 3 additions & 0 deletions Tasks/AzurePowerShell/Tests/L0.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ describe('AzurePowerShell Suite', function () {
it('performs basic flow', (done) => {
psr.run(path.join(__dirname, 'PerformsBasicFlow.ps1'), done);
})
it('validates inline script flow', (done) => {
psr.run(path.join(__dirname, 'ValidateInlineScriptFlow.ps1'), done);
})
it('redirects errors', (done) => {
psr.run(path.join(__dirname, 'RedirectsErrors.ps1'), done);
})
Expand Down
3 changes: 2 additions & 1 deletion Tasks/AzurePowerShell/Tests/PerformsBasicFlow.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ param()

# Arrange.
. $PSScriptRoot\..\..\..\Tests\lib\Initialize-Test.ps1
Register-Mock Get-VstsInput { "$PSScriptRoot/PerformsBasicFlow_TargetScript.ps1" } -- -Name ScriptPath -Require
Register-Mock Get-VstsInput { "FilePath" } -- -Name ScriptType -Require
Register-Mock Get-VstsInput { "$PSScriptRoot/PerformsBasicFlow_TargetScript.ps1" } -- -Name ScriptPath
Register-Mock Get-VstsInput { 'arg1 arg2' } -- -Name ScriptArguments
Register-Mock Initialize-Azure

Expand Down
3 changes: 2 additions & 1 deletion Tasks/AzurePowerShell/Tests/RedirectsErrors.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ param()

# Arrange.
. $PSScriptRoot\..\..\..\Tests\lib\Initialize-Test.ps1
Register-Mock Get-VstsInput { "$PSScriptRoot/RedirectsErrors_TargetScript.ps1" } -- -Name ScriptPath -Require
Register-Mock Get-VstsInput { "FilePath" } -- -Name ScriptType -Require
Register-Mock Get-VstsInput { "$PSScriptRoot/RedirectsErrors_TargetScript.ps1" } -- -Name ScriptPath
Register-Mock Initialize-Azure

# Act.
Expand Down
4 changes: 2 additions & 2 deletions Tasks/AzurePowerShell/Tests/RemovesFunctionsAndVariables.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ param()
. $PSScriptRoot\..\..\..\Tests\lib\Initialize-Test.ps1

# Arrange the task inputs.
Register-Mock Get-VstsInput { "$PSScriptRoot/RemovesFunctionsAndVariables_TargetScript.ps1" } -- -Name ScriptPath -Require
Register-Mock Get-VstsInput { "FilePath" } -- -Name ScriptType -Require
Register-Mock Get-VstsInput { "$PSScriptRoot/RemovesFunctionsAndVariables_TargetScript.ps1" } -- -Name ScriptPath

# Arrange the mock task SDK module.
New-Module -Name VstsTaskSdk -ScriptBlock {
Expand Down Expand Up @@ -39,6 +40,5 @@ Assert-AreEqual $false $actual.FunctionNames.ContainsKey('SomeAzureHelpersFuncti
Assert-AreEqual $false $actual.FunctionNames.ContainsKey('SomeAzureHelpersFunction2')

# Assert the local variables from the task script were removed.
Assert-AreEqual $false $actual.VariableNames.ContainsKey('scriptPath')
Assert-AreEqual $false $actual.VariableNames.ContainsKey('scriptArguments')
Assert-AreEqual $false $actual.VariableNames.ContainsKey('scriptCommand')
3 changes: 2 additions & 1 deletion Tasks/AzurePowerShell/Tests/ThrowsWhenInvalidScriptPath.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ param()
. $PSScriptRoot\..\..\..\Tests\lib\Initialize-Test.ps1
foreach ($path in @( "script`rpath", "script`npath" )) {
Unregister-Mock Get-VstsInput
Register-Mock Get-VstsInput { $path } -- -Name ScriptPath -Require
Register-Mock Get-VstsInput { "FilePath" } -- -Name ScriptType -Require
Register-Mock Get-VstsInput { $path } -- -Name ScriptPath

# Act/Assert.
Assert-Throws {
Expand Down
21 changes: 21 additions & 0 deletions Tasks/AzurePowerShell/Tests/ValidateInlineScriptFlow.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[CmdletBinding()]
param()

# Arrange.
. $PSScriptRoot\..\..\..\Tests\lib\Initialize-Test.ps1
Unregister-Mock Get-VstsInput
Register-Mock Get-VstsInput { "InlineScript" } -- -Name ScriptType -Require
Register-Mock Get-VstsInput { ",@( 'item 1', 'item 2')" } -- -Name Inline
Register-Mock Initialize-Azure

# Act.
$actual = @( & $PSScriptRoot\..\AzurePowerShell.ps1 )
$global:ErrorActionPreference = 'Stop' # Reset to stop.

# Assert the correct number of elements is returned.
Assert-AreEqual 1 $actual.Length

# Assert item 1 and 2 are in an array together.
Assert-AreEqual 2 @($actual[0]).Length
Assert-AreEqual 'item 1' $actual[0][0]
Assert-AreEqual 'item 2' $actual[0][1]
35 changes: 31 additions & 4 deletions Tasks/AzurePowerShell/task.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"version": {
"Major": 1,
"Minor": 1,
"Patch": 3
"Patch": 4
},
"demands": [
"azureps"
Expand Down Expand Up @@ -54,13 +54,40 @@
"helpMarkDown": "Azure Resource Manager subscription to configure before running PowerShell",
"visibleRule": "ConnectedServiceNameSelector = ConnectedServiceNameARM"
},
{
"name": "ScriptType",
"type": "pickList",
"label": "Script Type",
"required": true,
"helpMarkDown": "Type of the script: File Path or Inline Script",
"defaultValue": "FilePath",
"options": {
"FilePath": "Script File Path",
"InlineScript": "Inline Script"
}
},
{
"name": "ScriptPath",
"type": "filePath",
"label": "Script Path",
"defaultValue": "",
"required": true,
"helpMarkDown": "Path of the script. Should be fully qualified path or relative to the default working directory."
"required": false,
"helpMarkDown": "Path of the script. Should be fully qualified path or relative to the default working directory.",
"visibleRule": "ScriptType = FilePath"
},
{
"name": "Inline",
"type": "multiLine",
"label": "Inline Script",
"required": false,
"defaultValue": "# You can write your azure powershell scripts inline here. \n# You can also pass predefined and custom variables to this script using arguments",
"helpMarkDown": "Enter the script to execute.",
"visibleRule": "ScriptType = InlineScript",
"properties": {
"resizable": "true",
"rows": "10",
"maxLength": "500"
}
},
{
"name": "ScriptArguments",
Expand All @@ -74,7 +101,7 @@
"helpMarkDown": "Additional parameters to pass to PowerShell. Can be either ordinal or named parameters."
}
],
"instanceNameFormat": "Azure PowerShell script: $(ScriptPath)",
"instanceNameFormat": "Azure PowerShell script: $(ScriptType)",
"execution": {
"PowerShell3": {
"target": "AzurePowerShell.ps1"
Expand Down
33 changes: 30 additions & 3 deletions Tasks/AzurePowerShell/task.loc.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"version": {
"Major": 1,
"Minor": 1,
"Patch": 3
"Patch": 4
},
"demands": [
"azureps"
Expand Down Expand Up @@ -54,13 +54,40 @@
"helpMarkDown": "ms-resource:loc.input.help.ConnectedServiceNameARM",
"visibleRule": "ConnectedServiceNameSelector = ConnectedServiceNameARM"
},
{
"name": "ScriptType",
"type": "pickList",
"label": "ms-resource:loc.input.label.ScriptType",
"required": true,
"helpMarkDown": "ms-resource:loc.input.help.ScriptType",
"defaultValue": "FilePath",
"options": {
"FilePath": "Script File Path",
"InlineScript": "Inline Script"
}
},
{
"name": "ScriptPath",
"type": "filePath",
"label": "ms-resource:loc.input.label.ScriptPath",
"defaultValue": "",
"required": true,
"helpMarkDown": "ms-resource:loc.input.help.ScriptPath"
"required": false,
"helpMarkDown": "ms-resource:loc.input.help.ScriptPath",
"visibleRule": "ScriptType = FilePath"
},
{
"name": "Inline",
"type": "multiLine",
"label": "ms-resource:loc.input.label.Inline",
"required": false,
"defaultValue": "# You can write your azure powershell scripts inline here. \n# You can also pass predefined and custom variables to this script using arguments",
"helpMarkDown": "ms-resource:loc.input.help.Inline",
"visibleRule": "ScriptType = InlineScript",
"properties": {
"resizable": "true",
"rows": "10",
"maxLength": "500"
}
},
{
"name": "ScriptArguments",
Expand Down

0 comments on commit d923fb1

Please sign in to comment.