Skip to content

Instantly share code, notes, and snippets.

@indented-automation
Last active October 4, 2017 13:46
Show Gist options
  • Save indented-automation/3c0fc156cd62e45e1632b9302700815a to your computer and use it in GitHub Desktop.
Save indented-automation/3c0fc156cd62e45e1632b9302700815a to your computer and use it in GitHub Desktop.
Octopus Deploy - Stream redirection
{
"Id": "ActionTemplates-62",
"Name": "OctoPS",
"Description": "A PowerShell host implementation with flexible stream redirection.\n\nThe step parameters may be used to set values for the preference variables. The ErrorActionPreference variable is set to Continue by default for all scripts executing within the host.\n\nPreference variables may be set within scripts, overriding any settings defined by the step. Additional output may be requested using the individual parameters for each stream (for example, the Verbose parameter).\n\n**Limitations and known issues**\n\n- Write-Host is not supported except in PowerShell 5 as a wrapper for Write-Information.\n- Several of the IIS commands (Stop-WebSite, Stop-WebAppPool) appear to break the output streams used by the host.",
"ActionType": "Octopus.Script",
"Version": 13,
"CommunityActionTemplateId": null,
"Properties": {
"Octopus.Action.Script.Syntax": "PowerShell",
"Octopus.Action.Script.ScriptSource": "Inline",
"Octopus.Action.RunOnServer": "false",
"Octopus.Action.Script.ScriptBody": " #\r\n # Utility\r\n #\r\n\r\n function Get-OctoPSVariable {\r\n <#\r\n .SYNOPSIS\r\n Get the value of a variable used to implement this wrapper.\r\n .DESCRIPTION\r\n Variables used by this host are prefixed with string in an attempt to avoid polluting a users workspace.\r\n #>\r\n\r\n param (\r\n # The name of the variable\r\n [Parameter(Mandatory = $true)]\r\n [String]$Name,\r\n\r\n [Switch]$NameOnly\r\n )\r\n\r\n $prefix = 'f63498009f613e78416cae8a71e98d0d'\r\n\r\n $value = Get-Variable ('{0}{1}' -f $prefix, $name) -ValueOnly:(-not $NameOnly) -ErrorAction SilentlyContinue\r\n if ($NameOnly) {\r\n $value.Name\r\n } else {\r\n $parsedValue = $true\r\n if ([Boolean]::TryParse($value, [ref]$parsedValue)) {\r\n $parsedValue\r\n } else {\r\n $value\r\n }\r\n }\r\n }\r\n\r\n #\r\n # Host implementation\r\n #\r\n # Each of the functions below shares variables with global scope.\r\n\r\n function Copy-OctoPSVariable {\r\n <#\r\n .SYNOPSIS\r\n Copy variables into the initial session state.\r\n .DESCRIPTION\r\n Copy variables into the initial session state.\r\n\r\n An attempt is made to exclude default and reserved variables. \r\n #>\r\n\r\n [System.Collections.Generic.HashSet[String]]$variables =\r\n [PowerShell]::Create().Runspace.SessionStateProxy.InvokeProvider.Item.Get('variable:\\*').Name\r\n\r\n foreach ($variable in Get-OctoPSVariable -Name * -NameOnly) {\r\n $null = $variables.Add($variable)\r\n }\r\n Get-Variable -Scope Global |\r\n Where-Object {\r\n -not $variables.Contains($_.Name) -and\r\n $_.Name -notin 'LASTEXITCODE', 'PROFILE', 'PSCommandPath', 'PSScriptRoot', 'octoPS', 'octoPSSettings' -and\r\n $_.Value -ne $null -and\r\n ($_.Value.GetType().IsPrimitive -or $_.Value.GetType() -in [String], [Hashtable])\r\n } |\r\n ForEach-Object {\r\n ' Adding {0} to session state' -f $_.Name |\r\n Write-OctoPSMessage -Type 'OctoPS'\r\n\r\n $variableEntry = New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry(\r\n $_.Name,\r\n $_.Value,\r\n ''\r\n )\r\n $octoPS.InitialSessionState.Variables.Add($variableEntry)\r\n }\r\n }\r\n\r\n function Set-OctoPSPreferenceVariable {\r\n [CmdletBinding()]\r\n param (\r\n # The name of the preference variable.\r\n [String]$Name,\r\n\r\n # Set the preference variable to the following value.\r\n [String]$Value = 'Continue'\r\n )\r\n\r\n $octoPSSettings.PSObject.Properties | Where-Object { $_.Name -like '*Preference' } | ForEach-Object {\r\n ' Setting preference variable {0} to {1} in session state' -f $_.Name, $_.Value |\r\n Write-OctoPSMessage -Type 'OctoPS'\r\n\r\n $octoPS.initialSessionState.Variables.Remove($_.Name, $null)\r\n $variableEntry = New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry(\r\n $_.Name,\r\n [System.Management.Automation.ActionPreference]$_.Value,\r\n ''\r\n )\r\n $octoPS.initialSessionState.Variables.Add($variableEntry)\r\n }\r\n }\r\n\r\n function Register-OctoPSEvent {\r\n foreach ($stream in $octoPS.PSHost.Streams.PSObject.Properties) {\r\n Register-ObjectEvent $octoPS.PSHost.Streams.($stream.Name) -EventName DataAdded \r\n }\r\n Register-ObjectEvent $octoPS.OutputObject -EventName DataAdded\r\n }\r\n\r\n filter Write-OctoPSMessage {\r\n <#\r\n .SYNOPSIS\r\n Write a message to standard out.\r\n .DESCRIPTION\r\n Write a message to standard out.\r\n \r\n Verbose, Information, and Debug messages are prefixed with the name of the source stream.\r\n #>\r\n\r\n [CmdletBinding()]\r\n param (\r\n # A message to write.\r\n [Parameter(ValueFromPipeline = $true)]\r\n [Object]$Message,\r\n\r\n # The name of the original stream.\r\n [ValidateSet('Default', 'Verbose', 'Information', 'Warning', 'Error', 'Debug', 'Progress', 'OctoPS')]\r\n [String]$Type = 'Default'\r\n )\r\n\r\n if (-not $psboundparameters.ContainsKey('Type')) {\r\n if ($Message -and $Message.GetType().Name -match 'Record$') {\r\n $Type = $Message.GetType().Name -replace 'Record$'\r\n } else {\r\n $Type = 'Default'\r\n }\r\n }\r\n\r\n if ($Type -in 'Verbose', 'Information', 'Debug') {\r\n if (-not $octoPSSettings.Redirect.$Type) {\r\n '##octopus[stdout-verbose]'\r\n }\r\n\r\n '{0}: {1}' -f $Type.ToUpper(), $Message\r\n \r\n if (-not $octoPSSettings.Redirect.$Type) {\r\n '##octopus[stdout-default]'\r\n }\r\n } elseif ($Type -eq 'Progress') {\r\n # Not implemented: Ignore progress output.\r\n } elseif ($Type -ne 'Default') {\r\n switch ($Type) {\r\n 'Warning' {\r\n '##octopus[stdout-warning]'\r\n 'WARNING: {0}' -f $Message\r\n }\r\n 'Error' {\r\n '##octopus[stdout-error]'\r\n $Message.Exception.Message.Trim()\r\n $Message.InvocationInfo.PositionMessage.Trim()\r\n ' + CategoryInfo : {0}' -f $Message.CategoryInfo.ToString()\r\n ' + + FullyQualifiedErrorId : {0}' -f $Message.FullyQualifiedErrorId\r\n }\r\n 'OctoPS' {\r\n '##octopus[stdout-verbose]'\r\n 'OCTOPS: {0}' -f $Message\r\n }\r\n }\r\n '##octopus[stdout-default]'\r\n } else {\r\n $Message\r\n }\r\n }\r\n\r\n function Invoke-OctoPSScript {\r\n param (\r\n [String]$Script\r\n )\r\n\r\n $inputObject = New-Object System.Management.Automation.PSDataCollection[PSObject]\r\n $asyncResult = $octoPS.PSHost.AddScript($Script).BeginInvoke($inputObject, $octoPS.OutputObject)\r\n\r\n do {\r\n Start-Sleep -Milliseconds 100\r\n foreach ($event in (Get-Event)) {\r\n $event.Sender[$event.SourceEventArgs.Index] | Write-OctoPSMessage\r\n $event | Remove-Event\r\n }\r\n } until ($asyncResult.IsCompleted)\r\n\r\n if ($octoPS.PSHost.HadErrors -and $octoPS.PSHost.InvocationStateInfo.State -eq 'Failed') {\r\n throw $octoPS.PSHost.InvocationStateInfo.Reason\r\n }\r\n }\r\n\r\n #\r\n # Main\r\n #\r\n\r\n # Settings derived from the step implementation\r\n $octoPSSettings = [PSCustomObject]@{\r\n Script = Get-OctoPSVariable PowerShellScript\r\n ErrorActionPreference = Get-OctoPSVariable ErrorAction\r\n DebugPreference = @('SilentlyContinue', 'Continue')[(Get-OctoPSVariable Debug)]\r\n InformationPreference = @('SilentlyContinue', 'Continue')[(Get-OctoPSVariable Information)]\r\n VerbosePreference = @('SilentlyContinue', 'Continue')[(Get-OctoPSVariable Verbose)]\r\n Redirect = [PSCustomObject]@{\r\n Debug = Get-OctoPSVariable RedirectDebug\r\n Information = Get-OctoPSVariable RedirectInformation\r\n Verbose = Get-OctoPSVariable RedirectVerbose\r\n }\r\n }\r\n\r\n # Holds all variables used to define the host\r\n $octoPS = [PSCustomObject]@{\r\n InitialSessionState = [InitialSessionState]::CreateDefault()\r\n OutputObject = New-Object System.Management.Automation.PSDataCollection[PSObject]\r\n PSHost = $null\r\n EventRegistration = $null\r\n }\r\n\r\n try {\r\n 'Starting octoPS' | Write-OctoPSMessage -Type 'OctoPS'\r\n\r\n 'Copying variables to session state' | Write-OctoPSMessage -Type 'OctoPS'\r\n Copy-OctoPSVariable\r\n\r\n 'Setting preference variables' | Write-OctoPSMessage -Type 'OctoPS'\r\n Set-OctoPSPreferenceVariable\r\n\r\n 'Creating PowerShell host' | Write-OctoPSMessage -Type 'OctoPS'\r\n $octoPS.PSHost = [PowerShell]::Create($octoPS.InitialSessionState)\r\n \r\n 'Registering event handlers' | Write-OctoPSMessage -Type 'OctoPS'\r\n Register-OctoPSEvent\r\n\r\n 'Invoking script' | Write-OctoPSMessage -Type 'OctoPS'\r\n Invoke-OctoPSScript $octoPSSettings.Script\r\n\r\n 'Script complete' | Write-OctoPSMessage -Type 'OctoPS'\r\n } catch {\r\n 'Script failed' | Write-OctoPSMessage -Type 'OctoPS'\r\n\r\n throw\r\n } finally {\r\n Get-EventSubscriber | Unregister-Event\r\n }",
"Octopus.Action.Script.ScriptFileName": null,
"Octopus.Action.Package.FeedId": null,
"Octopus.Action.Package.PackageId": null
},
"Parameters": [
{
"Id": "974a74f2-c647-47b2-a2e2-5d0170984488",
"Name": "f63498009f613e78416cae8a71e98d0dPowerShellScript",
"Label": "PowerShell source code",
"HelpText": "A PowerShell script to execute.",
"DefaultValue": "",
"DisplaySettings": {
"Octopus.ControlType": "MultiLineText"
},
"Links": {}
},
{
"Id": "3c631719-5c2f-487e-995c-1a09db07c360",
"Name": "f63498009f613e78416cae8a71e98d0dErrorAction",
"Label": "Error action preference",
"HelpText": "The value of the error action preference variable.",
"DefaultValue": "Continue",
"DisplaySettings": {
"Octopus.ControlType": "Select",
"Octopus.SelectOptions": "SilentlyContinue\nStop\nContinue\nIgnore"
},
"Links": {}
},
{
"Id": "ad3b8603-f546-49de-9795-cd2cd767cbd7",
"Name": "f63498009f613e78416cae8a71e98d0dVerbose",
"Label": "Verbose preference",
"HelpText": "Sets the value of VerbosePreference to continue. By default verbose output is only shown if the VerbosePreference or Verbose parameter is used within the script.",
"DefaultValue": "False",
"DisplaySettings": {
"Octopus.ControlType": "Checkbox"
},
"Links": {}
},
{
"Id": "d29604ec-b84f-4b62-932e-8e14684d69f1",
"Name": "f63498009f613e78416cae8a71e98d0dRedirectVerbose",
"Label": "Redirect Verbose",
"HelpText": "Redirect verbose to standard out.",
"DefaultValue": "False",
"DisplaySettings": {
"Octopus.ControlType": "Checkbox"
},
"Links": {}
},
{
"Id": "f28ca0fc-a64b-494f-934b-618d1d548f22",
"Name": "f63498009f613e78416cae8a71e98d0dInformation",
"Label": "Information preference",
"HelpText": "Sets the value of the InformationPreference to continue.",
"DefaultValue": "False",
"DisplaySettings": {
"Octopus.ControlType": "Checkbox"
},
"Links": {}
},
{
"Id": "7f75df1f-1995-48ec-946b-ec611f00e0bb",
"Name": "f63498009f613e78416cae8a71e98d0dRedirectInformation",
"Label": "Redirect Information",
"HelpText": "Redirect information to standard out.",
"DefaultValue": "False",
"DisplaySettings": {
"Octopus.ControlType": "Checkbox"
},
"Links": {}
},
{
"Id": "9ea3dcbb-fe9a-44bd-86dd-362288594306",
"Name": "f63498009f613e78416cae8a71e98d0dDebug",
"Label": "Debug preference",
"HelpText": "Sets the value of the DebugPreference to continue.",
"DefaultValue": "False",
"DisplaySettings": {
"Octopus.ControlType": "Checkbox"
},
"Links": {}
},
{
"Id": "1cb0d0d7-5d33-4035-a546-104edf41652c",
"Name": "f63498009f613e78416cae8a71e98d0dRedirectDebug",
"Label": "Redirect Debug",
"HelpText": "Redirect debug to standard out.",
"DefaultValue": "False",
"DisplaySettings": {
"Octopus.ControlType": "Checkbox"
},
"Links": {}
}
],
"$Meta": {
"ExportedAt": "2017-10-04T13:46:09.035Z",
"OctopusVersion": "3.17.2",
"Type": "ActionTemplate"
}
}
#
# Utility
#
function Get-OctoPSVariable {
<#
.SYNOPSIS
Get the value of a variable used to implement this wrapper.
.DESCRIPTION
Variables used by this host are prefixed with string in an attempt to avoid polluting a users workspace.
#>
param (
# The name of the variable
[Parameter(Mandatory = $true)]
[String]$Name,
[Switch]$NameOnly
)
$prefix = 'f63498009f613e78416cae8a71e98d0d'
$value = Get-Variable ('{0}{1}' -f $prefix, $name) -ValueOnly:(-not $NameOnly) -ErrorAction SilentlyContinue
if ($NameOnly) {
$value.Name
} else {
$parsedValue = $true
if ([Boolean]::TryParse($value, [ref]$parsedValue)) {
$parsedValue
} else {
$value
}
}
}
#
# Host implementation
#
# Each of the functions below shares variables with global scope.
function Copy-OctoPSVariable {
<#
.SYNOPSIS
Copy variables into the initial session state.
.DESCRIPTION
Copy variables into the initial session state.
An attempt is made to exclude default and reserved variables.
#>
[System.Collections.Generic.HashSet[String]]$variables =
[PowerShell]::Create().Runspace.SessionStateProxy.InvokeProvider.Item.Get('variable:\*').Name
foreach ($variable in Get-OctoPSVariable -Name * -NameOnly) {
$null = $variables.Add($variable)
}
Get-Variable -Scope Global |
Where-Object {
-not $variables.Contains($_.Name) -and
$_.Name -notin 'LASTEXITCODE', 'PROFILE', 'PSCommandPath', 'PSScriptRoot', 'octoPS', 'octoPSSettings' -and
$_.Value -ne $null -and
($_.Value.GetType().IsPrimitive -or $_.Value.GetType() -in [String], [Hashtable])
} |
ForEach-Object {
' Adding {0} to session state' -f $_.Name |
Write-OctoPSMessage -Type 'OctoPS'
$variableEntry = New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry(
$_.Name,
$_.Value,
''
)
$octoPS.InitialSessionState.Variables.Add($variableEntry)
}
}
function Set-OctoPSPreferenceVariable {
[CmdletBinding()]
param (
# The name of the preference variable.
[String]$Name,
# Set the preference variable to the following value.
[String]$Value = 'Continue'
)
$octoPSSettings.PSObject.Properties | Where-Object { $_.Name -like '*Preference' } | ForEach-Object {
' Setting preference variable {0} to {1} in session state' -f $_.Name, $_.Value |
Write-OctoPSMessage -Type 'OctoPS'
$octoPS.initialSessionState.Variables.Remove($_.Name, $null)
$variableEntry = New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry(
$_.Name,
[System.Management.Automation.ActionPreference]$_.Value,
''
)
$octoPS.initialSessionState.Variables.Add($variableEntry)
}
}
function Register-OctoPSEvent {
foreach ($stream in $octoPS.PSHost.Streams.PSObject.Properties) {
Register-ObjectEvent $octoPS.PSHost.Streams.($stream.Name) -EventName DataAdded
}
Register-ObjectEvent $octoPS.OutputObject -EventName DataAdded
}
filter Write-OctoPSMessage {
<#
.SYNOPSIS
Write a message to standard out.
.DESCRIPTION
Write a message to standard out.
Verbose, Information, and Debug messages are prefixed with the name of the source stream.
#>
[CmdletBinding()]
param (
# A message to write.
[Parameter(ValueFromPipeline = $true)]
[Object]$Message,
# The name of the original stream.
[ValidateSet('Default', 'Verbose', 'Information', 'Warning', 'Error', 'Debug', 'Progress', 'OctoPS')]
[String]$Type = 'Default'
)
if (-not $psboundparameters.ContainsKey('Type')) {
if ($Message -and $Message.GetType().Name -match 'Record$') {
$Type = $Message.GetType().Name -replace 'Record$'
} else {
$Type = 'Default'
}
}
if ($Type -in 'Verbose', 'Information', 'Debug') {
if (-not $octoPSSettings.Redirect.$Type) {
'##octopus[stdout-verbose]'
}
'{0}: {1}' -f $Type.ToUpper(), $Message
if (-not $octoPSSettings.Redirect.$Type) {
'##octopus[stdout-default]'
}
} elseif ($Type -eq 'Progress') {
# Not implemented: Ignore progress output.
} elseif ($Type -ne 'Default') {
switch ($Type) {
'Warning' {
'##octopus[stdout-warning]'
'WARNING: {0}' -f $Message
}
'Error' {
'##octopus[stdout-error]'
$Message.Exception.Message.Trim()
$Message.InvocationInfo.PositionMessage.Trim()
' + CategoryInfo : {0}' -f $Message.CategoryInfo.ToString()
' + + FullyQualifiedErrorId : {0}' -f $Message.FullyQualifiedErrorId
}
'OctoPS' {
'##octopus[stdout-verbose]'
'OCTOPS: {0}' -f $Message
}
}
'##octopus[stdout-default]'
} else {
$Message
}
}
function Invoke-OctoPSScript {
param (
[String]$Script
)
$inputObject = New-Object System.Management.Automation.PSDataCollection[PSObject]
$asyncResult = $octoPS.PSHost.AddScript($Script).BeginInvoke($inputObject, $octoPS.OutputObject)
do {
Start-Sleep -Milliseconds 100
foreach ($event in (Get-Event)) {
$event.Sender[$event.SourceEventArgs.Index] | Write-OctoPSMessage
$event | Remove-Event
}
} until ($asyncResult.IsCompleted)
if ($octoPS.PSHost.HadErrors -and $octoPS.PSHost.InvocationStateInfo.State -eq 'Failed') {
throw $octoPS.PSHost.InvocationStateInfo.Reason
}
}
#
# Main
#
# Settings derived from the step implementation
$octoPSSettings = [PSCustomObject]@{
Script = Get-OctoPSVariable PowerShellScript
ErrorActionPreference = Get-OctoPSVariable ErrorAction
DebugPreference = @('SilentlyContinue', 'Continue')[(Get-OctoPSVariable Debug)]
InformationPreference = @('SilentlyContinue', 'Continue')[(Get-OctoPSVariable Information)]
VerbosePreference = @('SilentlyContinue', 'Continue')[(Get-OctoPSVariable Verbose)]
Redirect = [PSCustomObject]@{
Debug = Get-OctoPSVariable RedirectDebug
Information = Get-OctoPSVariable RedirectInformation
Verbose = Get-OctoPSVariable RedirectVerbose
}
}
# Holds all variables used to define the host
$octoPS = [PSCustomObject]@{
InitialSessionState = [InitialSessionState]::CreateDefault()
OutputObject = New-Object System.Management.Automation.PSDataCollection[PSObject]
PSHost = $null
EventRegistration = $null
}
try {
'Starting octoPS' | Write-OctoPSMessage -Type 'OctoPS'
'Copying variables to session state' | Write-OctoPSMessage -Type 'OctoPS'
Copy-OctoPSVariable
'Setting preference variables' | Write-OctoPSMessage -Type 'OctoPS'
Set-OctoPSPreferenceVariable
'Creating PowerShell host' | Write-OctoPSMessage -Type 'OctoPS'
$octoPS.PSHost = [PowerShell]::Create($octoPS.InitialSessionState)
'Registering event handlers' | Write-OctoPSMessage -Type 'OctoPS'
Register-OctoPSEvent
'Invoking script' | Write-OctoPSMessage -Type 'OctoPS'
Invoke-OctoPSScript $octoPSSettings.Script
'Script complete' | Write-OctoPSMessage -Type 'OctoPS'
} catch {
'Script failed' | Write-OctoPSMessage -Type 'OctoPS'
throw
} finally {
Get-EventSubscriber | Unregister-Event
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment