lib/SubjectTasks.ps1
function Invoke-SubjectTaskActionParallel { <# .SYNOPSIS Provides the Actionrequest to the TMConsole PowerShell Session Manager for invocation. .DESCRIPTION This function will collect the ActionRequest object from TransitionManager, and will send it to SessionManager for invocation. .PARAMETER TMSession The name of the TMSession that the Broker and Subject belong to .PARAMETER Subject The TMBrokerSubject object representing a Broker's Subject Task .PARAMETER Cache The Broker's cache if available .EXAMPLE Invoke-SubjectTaskActionParallel -TMSession 'Broker' -Subject $Broker.Subjects[0][1] -Cache $Broker.Cache .OUTPUTS None #> [CmdletBinding()] param ( [Parameter(Mandatory = $false, Position = 0)] [String]$TMSession = 'Default', [Parameter(Mandatory = $true)] [TMBrokerSubject]$Subject, [Parameter(Mandatory = $false)] [Object]$Cache ) ## ## Connect to TransitionManager to collect the task to run ## # Get the session configuration Write-Verbose 'Checking for cached TMSession' $TMSessionConfig = $global:TMSessions[$TMSession] Write-Debug 'TMSessionConfig:' Write-Debug ($TMSessionConfig | ConvertTo-Json -Depth 5) if (-not $TMSessionConfig) { throw "TMSession '$TMSession' not found. Use New-TMSession command before using features." } # Tell TM that the Task's Action is starting and receive an ActionRequest with Params $Subject.ActionRequest = Start-TMTaskAction -TMSession $TMSession -TaskId $Subject.Task.Id -Force -ErrorAction 'SilentlyContinue' ## An Error invoking the task may have been handled, and no SubjectActionRequest was returned. if (-not $Subject.ActionRequest) { $Subject.Action.ExecutionStatus = 'Started' return } ## Add the TM User Session to the Subject Action Request so that the TM session and $TM variable are available during script execution if ($ActionRequest) { $Subject.ActionRequest | Add-Member -NotePropertyName 'TMUserSession' -NotePropertyValue $ActionRequest.TMUserSession -Force } ## Add the Broker ID to the ActionRequest so it's output can be interpered by TMConsole $Subject.ActionRequest | Add-Member -NotePropertyName 'BrokerId' -NotePropertyValue "TMTaskID_$($Broker.Task.Id)" -Force ## Notify invocation of the next task Write-Host "[$(Get-Date -Format "HH:mm:ss.ffff")] Queueing Parallel Task: " -NoNewline Write-Host "#$($Subject.Task.TaskNumber)" -NoNewline -ForegroundColor Cyan Write-Host ', Title: ' -NoNewline Write-Host $Subject.Task.Title -ForegroundColor Yellow ## Create a TMC message to be forwarded to SessionManager by the Write-Host handler $Subject.ActionRequest | Add-Member -NotePropertyName 'Type' -NotePropertyValue 'ActionRequest' -Force $QueueActionRequestString = "||TMC:$($Subject.ActionRequest | ConvertTo-Json -Depth 10 -Compress)" # Record the time that this Action was invoked $Subject.Action.InvokedAt = Get-Date ## The following command will execute 'Normally' in debug mode, but when run in a TMC runspace, it has a special handling, ## and will result in the ActionRequest object being sent for invocation Write-Host $QueueActionRequestString } function Invoke-SubjectTaskAction { <# .SYNOPSIS Invokes the Action associated with a Broker's Subject Task .DESCRIPTION This function will invoke the PowerShell Action associated with a Broker's Subject Task .PARAMETER TMSession The name of the TMSession that the Broker and Subject belong to .PARAMETER Subject The TMBrokerSubject object representing a Broker's Subject Task .PARAMETER Cache The Broker's cache if available .EXAMPLE Invoke-SubjectTaskAction -TMSession 'Broker' -Subject $Broker.Subjects[0][1] -Cache $Broker.Cache .OUTPUTS None #> [CmdletBinding()] param ( [Parameter(Mandatory = $false, Position = 0)] [String]$TMSession = 'Default', [Parameter(Mandatory = $true)] [TMBrokerSubject]$Subject, [Parameter(Mandatory = $false)] [Object]$Cache ) # Get the session configuration Write-Verbose 'Checking for cached TMSession' $TMSessionConfig = $global:TMSessions[$TMSession] Write-Debug 'TMSessionConfig:' Write-Debug ($TMSessionConfig | ConvertTo-Json -Depth 5) if (-not $TMSessionConfig) { throw "TMSession '$TMSession' not found. Use New-TMSession command before using features." } ## ## Connect to TransitionManager to collect the task to run ## # Get the session configuration Write-Verbose 'Checking for cached TMSession' $TMSessionConfig = $global:TMSessions[$TMSession] Write-Debug 'TMSessionConfig:' Write-Debug ($TMSessionConfig | ConvertTo-Json -Depth 5) if (-not $TMSessionConfig) { throw "TMSession '$TMSession' not found. Use New-TMSession command before using features." } # Tell TM that the Task's Action is starting and receive an ActionRequest with Params $Subject.ActionRequest = Start-TMTaskAction -TMSession $TMSession -TaskId $Subject.Task.Id -Force -ErrorAction 'SilentlyContinue' ## An Error ivoking the task may have been handled, and no SubjectActionRequest was returned. if (-not $Subject.ActionRequest) { $Subject.Action.ExecutionStatus = 'Started' return } ## Add the Broker ID to the ActionRequest so it's output can be interpered by TMConsole $Subject.ActionRequest | Add-Member -NotePropertyName 'BrokerId' -NotePropertyValue "TMTaskID_$($Broker.Task.Id)" -Force ## ## Invoke the Action in this runspace ## $InvocationRunspaceScriptBlock = [scriptblock] { param($ActionRequest, $AllowInsecureSSL) try { ## Complete the root activity to provide a consistent experience for all tasks $StartingProgressActivity = @{ Id = 0 ParentId = -1 Activity = 'Broker is Running Task: ' + $ActionRequest.task.taskNumber + ' - ' + $ActionRequest.task.title PercentComplete = 5 SecondsRemaining = -1 Completed = $False } Write-Progress @StartingProgressActivity ## Preserve the Broker's Parameters $BrokerParams = $Params $BrokerTM = $TM ## Import the ActionRequest to create necessary objects in the pipeline . Import-TMCSubjectActionRequest -ActionRequest $ActionRequest -BrokerTaskId 1 ## Invoke the Action's Script block $ActionScriptBlock = [scriptblock]::Create($ActionRequest.options.apiAction.script) Invoke-Command -ScriptBlock $ActionScriptBlock -ErrorAction 'Stop' -NoNewScope ## Create a Data Options parameter for the Complete-TMTask command $CompleteTaskParameters = @{} ## Check the Global Variable for any TMAssetUpdates to send to TransitionManager during the task completion if ($Global:TMAssetUpdates) { $CompleteTaskParameters = @{ Data = @{ assetUpdates = $Global:TMAssetUpdates } } ## Clear the Global variable now that it's assigned into the respsonse belonging ## To the subject task. Otherwise, the next subject gets the same data Remove-Variable -Name 'TMAssetUpdates' -Scope Global } ## Add SSL Exception if necessary if ($AllowInsecureSSL) { $CompleteTaskParameters | Add-Member -NotePropertyName 'AllowInsecureSSL' -NotePropertyValue $True -Force } ## Complete the TM Task, sending Updated Data values for the task Asset if ($ActionRequest.HostPID -ne 0) { Complete-TMTask -ActionRequest $ActionRequest @CompleteTaskParameters } ## Complete the root activity to provide a consistent experience for all tasks $CompleteProgressActivity = @{ Id = 0 ParentId = -1 Activity = 'Task Complete: ' + $ActionRequest.task.taskNumber + ' - ' + $ActionRequest.task.title PercentComplete = 100 SecondsRemaining = -1 Completed = $True } Write-Progress @CompleteProgressActivity $Subject.Action.ExecutionStatus = 'Successful' } catch { ## Get the Exception message $ExceptionMessage = $_.Exception.Message ## Send the Error Message (Only send if TMD started the process) if ($ActionRequest.HostPID -ne 0) { Set-TMTaskOnHold -ActionRequest $ActionRequest -Message ('Action Error: ' + $ExceptionMessage) } ## Throw the full error message Write-Host $_.Exception.Message -ForegroundColor Red $Subject.Action.ExecutionStatus = 'Failed' $Subject.Action.Errors = @($ExceptionMessage) } ## Return the Broker Params and TM scope New-Variable -Name Params -Scope Global -Value $BrokerParams -Force New-Variable -Name TM -Scope Global -Value $BrokerTM -Force } ## Notify invocation of the next task Write-Host "[$(Get-Date -Format "HH:mm:ss.ffff")] Invoking Task: " -NoNewline Write-Host "#$($Subject.Task.TaskNumber)" -NoNewline -ForegroundColor Cyan Write-Host ', Title: ' -NoNewline Write-Host $Subject.Task.Title -ForegroundColor Yellow ## Run the Script Block $RunningTime = Measure-Command { $InvokeSplat = @{ ScriptBlock = $InvocationRunspaceScriptBlock ArgumentList = @($Subject.ActionRequest, $TMSessionConfig.AllowInsecureSSL) NoNewScope = $true } $Subject.Action.InvokedAt = Get-Date Invoke-Command @InvokeSplat } $RuntimeSeconds = [Math]::Ceiling($RunningTime.TotalSeconds) ## Produce Broker output to finish the line of output if ($Subject.Action.ExecutionStatus -eq 'Failed') { Write-Host "[$(Get-Date -Format "HH:mm:ss.ffff")] Action completed with errors after running for " -NoNewline -ForegroundColor Red Write-Host $RuntimeSeconds -NoNewline -ForegroundColor Red Write-Host " seconds. Error: $($Subject.Action.Errors -join ', ')" -ForegroundColor Red } else { Write-Host "[$(Get-Date -Format "HH:mm:ss.ffff")] Action completed successfully in: " -NoNewline Write-Host $RuntimeSeconds -NoNewline -ForegroundColor Cyan Write-Host ' seconds' } } function Import-TMCSubjectActionRequest { param( [Parameter(Mandatory = $true)][PSObject]$ActionRequest, [Parameter(Mandatory = $true)][Int64]$BrokerTaskId ) ## Remove any leftover 'get_' parameter names $ActionRequest.params.PSObject.Properties.Name | Where-Object { $_ -like 'get_*' } | ForEach-Object { $ActionRequest.params.PSObject.Properties.Remove($_) } ## Allow Required Modules to be imported automatically $PSModuleAutoloadingPreference = 'All' $Global:PSModuleAutoloadingPreference = 'All' ## Import the remaining Variables New-Variable -Name 'ActionRequest' -Value $ActionRequest -Force -Scope Global New-Variable -Name 'Params' -Value $ActionRequest.params -Force -Scope Global ## Compute a Project root folder from the Server, Project and Event $TMServerUrl = ([uri]$ActionRequest.options.callback.siteUrl).Host $TMProjectName = $ActionRequest.task.project.name $TMEventName = $ActionRequest.task.event.name $ProjectRootFolder = Join-Path $Global:userPaths.root ($TMServerUrl -replace '.transitionmanager.net', '') $TMProjectName $TMEventName ## Create a Convenient TM Object with useful details $TM = [pscustomobject]@{ Server = @{ Url = $TMServerUrl # Version = [Version]::parse($ActionRequest.tmUserSession.tmVersion) } Project = @{ Id = $ActionRequest.task.project.id Name = $TMProjectName } Event = [pscustomobject]@{ Id = $ActionRequest.task.event.id Name = $TMEventName Bundles = $ActionRequest.task.event.bundles } Provider = @{ Id = $ActionRequest.options.apiAction.provider.id Name = $ActionRequest.options.apiAction.provider.name } Action = @{ Id = $ActionRequest.options.apiAction.id Name = $ActionRequest.options.apiAction.name } Task = [pscustomobject]@{ Id = $ActionRequest.task.id Title = $ActionRequest.task.title TaskNumber = $ActionRequest.task.taskNumber Invoker = $ActionRequest.task.assignedTo.name Team = $ActionRequest.task.team } Asset = $ActionRequest.task.asset # The tmUserSession object is not present, exclude for now # User = @{ # Id = $ActionRequest.tmUserSession.userContext.user.id # Username = $ActionRequest.tmUserSession.userContext.user.username # Name = $ActionRequest.tmUserSession.userContext.person.fullName # } Paths = @{ debug = Join-Path $ProjectRootFolder 'Debug' logs = Join-Path $ProjectRootFolder 'Logs' queue = Join-Path $ProjectRootFolder 'Queue' config = Join-Path $ProjectRootFolder 'Config' input = Join-Path $ProjectRootFolder 'Input' output = Join-Path $ProjectRootFolder 'Output' credentials = Join-Path $ProjectRootFolder 'Credentials' git = Join-Path $ProjectRootFolder 'Git' referencedesigns = Join-Path $ProjectRootFolder 'Reference Designs' } } ## Scope the variable as global so the user will have access to it New-Variable -Name 'TM' -Value $TM -Scope Global -Force } |