Scriptbook.psm1
[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "")] param( [parameter(Mandatory = $false)][HashTable]$ImportVars ) Write-Verbose "Start loading Scriptbook module"; # check if single file module if (Test-Path (Join-Path $PSScriptRoot Public)) { $dotSourceParams = @{ Filter = '*.ps1' Recurse = $true ErrorAction = 'Stop' } $public = @(Get-ChildItem -Path (Join-Path -Path $PSScriptRoot -ChildPath 'Public') @dotSourceParams ) $private = @(Get-ChildItem -Path (Join-Path -Path $PSScriptRoot -ChildPath 'Private/*.ps1') @dotSourceParams) foreach ($import in @($private + $public)) { try { . $import.FullName } catch { throw "Unable to dot source [$($import.FullName)]" } } } $verbose = $false if ($ImportVars -and $ImportVars.ContainsKey('Verbose')) { $verbose = $ImportVars.Verbose } if ($verbose) { $VerbosePreference = 'Continue' } $strict = $true if ($ImportVars -and $ImportVars.ContainsKey('NoStrict')) { $strict = -not $ImportVars.NoStrict } if ($strict) { Set-StrictMode -Version Latest } $quiet = $false if ($ImportVars -and $ImportVars.ContainsKey('Quiet')) { $quiet = $ImportVars.Quiet } if (!($quiet)) { $module = 'Scriptbook' $scriptDir = Split-Path $MyInvocation.MyCommand.Path $manifestPath = Join-Path $scriptDir "$module.psd1" $manifest = Test-ModuleManifest -Path $manifestPath -WarningAction SilentlyContinue $version = $manifest.Version.ToString() $copyright = $manifest.Copyright $author = $manifest.Author Write-Host "$module Version $version by $author" Write-Host "Proudly created in Schiedam (NLD), $copyright" } $core = $false if ($ImportVars -and $ImportVars.ContainsKey('Core')) { $core = $ImportVars.Core } if ($core) { if ($PSVersionTable.PSVersion.Major -lt 6) { Write-Host ''.PadRight(78, '=') Throw "PowerShell version $($PSVersionTable.PSVersion) not supported by this Script" } } $Global:ConfigurePreference = $false $Global:ScriptbookSimpleHost = $false if ($ImportVars -and $ImportVars.ContainsKey('SimpleHost')) { $Global:ScriptbookSimpleHost = $ImportVars.SimpleHost } $checkModules = $true $cacheTimeFile = Join-Path $home './Scriptbook.ModuleCache.json' $importFormat = 'Scriptbook' # load dependencies from file if not override by arguments if ($null -eq $ImportVars -or ($ImportVars.ContainsKey('SettingsFile') -or ($ImportVars.ContainsKey('Quiet') -and $ImportVars.Count -eq 1)) ) { if ($ImportVars -and $ImportVars.ContainsKey('SettingsFile')) { $importFile = $ImportVars.SettingsFile } else { $importFile = '' } $scriptName = $Script:MyInvocation.ScriptName if ([string]::IsNullOrEmpty($scriptName)) { $scriptName = Join-Path $PSScriptRoot 'Scriptbook.ps1' } if (Test-Path variable:Profile) { $profileLocation = Join-Path (Split-Path $Profile) 'Scriptbook.psd1' } else { $profileLocation = '' } $importFiles = @( $importFile, [IO.Path]::ChangeExtension($scriptName, 'psd1'), [IO.Path]::ChangeExtension((Join-Path (Split-Path $scriptName) ".$(Split-Path $scriptName -Leaf)"), 'psd1'), (Join-Path (Split-Path $scriptName) 'Scriptbook.psd1'), (Join-Path (Split-Path $scriptName) '.Scriptbook.psd1'), './depends.psd1', './variables.psd1', './.depends.psd1', './.variables.psd1', './requirements.psd1', $profileLocation ) foreach ($f in $importFiles) { if ( ![string]::IsNullOrEmpty($f) -and (Test-Path -Path $f -ErrorAction Ignore) ) { $ImportVars = Import-PowerShellDataFile -Path $f if ($f.Contains('requirements.psd1')) { $importFormat = 'Requirements' } break; } } if ($null -eq $ImportVars) { $ImportVars = @{} } } if ($ImportVars -and $importFormat -eq 'Requirements') { <# Import modules from requirements format: https://docs.microsoft.com/azure/azure-functions/functions-reference-powershell sample: @{ 'Az.Accounts' = '2.*' 'Az.Compute' = '4.*' } #> $ImportVarsNew = @{} $modules = [System.Collections.ArrayList]@() foreach ($m in $ImportVars.GetEnumerator()) { $modules.Add(@{ Module = $m.Key MinimumVersion = $m.Value } ) | Out-Null } $ImportVarsNew.Add('Depends', $modules) | Out-Null $ImportVars = $ImportVarsNew } if ($ImportVars.ContainsKey('Reset') -and $ImportVars.Reset) { # default is reset } else { if (Test-Path $cacheTimeFile) { # determine if we need to load the modules from repository again # we check once a day the repository feed if new versions are available # this to speed up the start-time of our workflows function GetValidDatePart($Value) { $datePart = $null if ($Value -is [string]) { $datePart = [System.DateTime]::Parse($Value).Date } elseif ($Value -is [DateTime]) { $datePart = $Value.Date } return $datePart } $md = $null $moduleCache = Get-Content -Path $cacheTimeFile -Raw | ConvertFrom-Json if ($moduleCache.PSobject.Properties.Name -match "Time") { if ($moduleCache.Time.PSobject.Properties.Name -match "Value") { $md = GetValidDatePart $moduleCache.Time.Value } else { $md = GetValidDatePart $moduleCache.Time } if ($md -and ($md -eq ((Get-Date).Date))) { $checkModules = $false } } } } if ($checkModules -eq $true) { Set-Content -Path $cacheTimeFile -Value (@{ Time = (Get-Date) } | ConvertTo-Json) -Force Write-Verbose 'Loading modules...' } $depends = $null if ($ImportVars -and $ImportVars.ContainsKey('Depends')) { $depends = $ImportVars.Depends } if ($depends) { # TODO !!EH Add install modules from git repo # TODO !!EH Add cache module option (create copy local and import from local), used in scenarios without internet access (deployments) / isolated containers Write-Host "Loading Scriptbook dependencies..." $pref = $global:ProgressPreference $global:ProgressPreference = 'SilentlyContinue' foreach ($dependency in $depends) { if (!$dependency.ContainsKey('Module')) { throw "Module not found in dependency: $dependency" } $skip = if ($dependency.ContainsKey('Skip')) { $dependency.Skip } else { $false } if ($skip) { continue } if (!(Test-Path $dependency.Module)) { $skipPublisherCheck = if ($dependency.ContainsKey('SkipPublisherCheck')) { $dependency.SkipPublisherCheck } else { $false } $minimumVersion = if ($dependency.ContainsKey('MinimumVersion')) { $dependency.MinimumVersion } else { '' } $maximumVersion = if ($dependency.ContainsKey('MaximumVersion')) { $dependency.MaximumVersion } else { '' } $extraParams = @{} $credentialLocation = if ($dependency.ContainsKey('Credential')) { $dependency.Credential } else { $null } if ($credentialLocation) { if ($credentialLocation.StartsWith('https://')) { # dependency on TD.Util Module, load this module first if (Get-Command Get-AzureDevOpsCredential -ErrorAction Ignore) { $cred = Get-AzureDevOpsCredential -Url $credentialLocation $extraParams.Add('Credential', $cred) | Out-Null } } else { try { $cred = Get-LocalCredential -Name $credentialLocation $extraParams.Add('Credential', $cred) | Out-Null } catch { Write-Warning $_.Exception.Message } } } $repository = if ($dependency.ContainsKey('Repository')) { $dependency.Repository } else { 'PSGallery' } if ($repository -ne 'PSGallery') { $repo = Get-PSRepository -Name $repository -ErrorAction Ignore if ($null -eq $repo) { $repositoryUrl = if ($dependency.ContainsKey('RepositoryUrl')) { $dependency.RepositoryUrl } else { $null } if ($repositoryUrl) { if (Get-Command Register-AzureDevOpsPackageSource -ErrorAction Ignore) { Register-AzureDevOpsPackageSource -Name $repository -Url $repositoryUrl @extraParams } else { Register-PSRepository -Name $repository -SourceLocation $repositoryUrl -InstallationPolicy Trusted @extraParams } } } } if (Get-Module -Name $dependency.Module -ListAvailable -ErrorAction Ignore) { if ($checkModules) { if ($null -ne (Get-InstalledModule -Name $dependency.Module -ErrorAction Ignore) ) { $force = if ($dependency.ContainsKey('Force')) { $dependency.Force } else { $false } if ($minimumVersion) { $v1 = (Get-Module -Name $dependency.Module -ListAvailable | Select-Object -First 1).Version $v2 = [version]$minimumVersion if ($v2 -gt $v1) { Write-Verbose "Updating module $($dependency.Module) with MinimumVersion $minimumVersion" Update-Module -Name $dependency.Module -Force:$force -RequiredVersion $minimumVersion @extraParams } } else { Write-Verbose "Updating module $($dependency.Module) with MaximumVersion $maximumVersion" Update-Module -Name $dependency.Module -Force:$force -MaximumVersion $maximumVersion @extraParams } } else { Write-Warning "Module $($dependency.Module) not installed by Install-Module, cannot update module via Update-Module, using forced Install-Module" Write-Verbose "Installing module $($dependency.Module)" Install-Module -Name $dependency.Module -Force -Repository $repository -Scope CurrentUser -MinimumVersion $minimumVersion -MaximumVersion $maximumVersion -AllowClobber -SkipPublisherCheck:$skipPublisherCheck @extraParams } } } else { Write-Verbose "Installing module $($dependency.Module)" # TODO !!EH using -Force to install from untrusty repositories or do we need to handle this via Force attribute Install-Module -Name $dependency.Module -Force -Repository $repository -Scope CurrentUser -MinimumVersion $minimumVersion -MaximumVersion $maximumVersion -AllowClobber -SkipPublisherCheck:$skipPublisherCheck @extraParams } } if ($dependency.ContainsKey('Args')) { Import-Module -Name $dependency.Module -ArgumentList $dependency.Args -Global } else { Import-Module -Name $dependency.Module -Global } } $global:ProgressPreference = $pref } $variables = $null if ($ImportVars -and $ImportVars.ContainsKey('Variables')) { $variables = $ImportVars.Variables } if ($variables) { foreach ($kv in $variables.GetEnumerator()) { Set-Variable -Scope Global -Name $kv.Key -Value $kv.Value -Force } } # snapshot global vars, excluding parameters $Global:GlobalVarNames = @{} Get-Variable -Scope Global | ForEach-Object { if (!$_.Attributes) { $Global:GlobalVarNames.Add($_.Name, $null) | Out-Null } elseif ($_.Attributes.GetType().Name -ne 'PSVariableAttributeCollection' -or $_.Attributes.Count -eq 0) { $Global:GlobalVarNames.Add($_.Name, $null) | Out-Null } elseif ($_.Attributes[0].GetType().Name -ne 'ParameterAttribute') { $Global:GlobalVarNames.Add($_.Name, $null) | Out-Null } } # cleanup module script scope @( 'variables', 'author', 'checkModules', 'cacheTimeFile', 'copyright', 'depends', 'dotSourceParams', 'importFile', 'importFiles', 'importFormat', 'ImportVars', 'manifest', 'manifestPath', 'module', 'moduleCache', 'private', 'profileLocation', 'public', 'quiet', 'scriptDir', 'scriptName', 'version', 'verbose', 'f', 'strict', 'import' ) | ForEach-Object { Remove-Variable -Force -ErrorAction Ignore -Scope Script -Name $_ } Write-Verbose "Finished loading Scriptbook module"; # end import ; # experimental Set-Alias -Name Info -Value New-Info -Scope Global -Force -WhatIf:$false Set-Alias -Name Documentation -Value New-Info -Scope Global -Force -WhatIf:$false function New-Info { [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] param( [ScriptBlock] $Code, [string]$Comment, [Switch]$NoDisplay, [Switch]$Skip, [Switch]$AsDocumentation ) if ($null -eq $Code) { Throw "No info script block is provided. (Have you put the open curly brace on the next line?)" } if ($Skip.IsPresent) { return } if ($PSCmdlet.ShouldProcess("New-Info")) { if ($PSCmdlet.MyInvocation.InvocationName -eq 'Documentation') { $AsDocumentation = $true } $text = $null if ($Comment) { $text = $Comment + [System.Environment]::NewLine } $text += Get-CommentFromCode -ScriptBlock $Code if ($AsDocumentation.IsPresent) { $ctx = Get-RootContext [void]$ctx.Infos.Add($text) } elseif (!($NoDisplay.IsPresent) -or ($VerbosePreference -eq 'Continue') ) { Write-Info ($text | Out-String | Show-Markdown) } } } # experimental Set-Alias -Name S -Value New-Section -Scope Global -Force -WhatIf:$false Set-Alias -Name Section -Value New-Section -Scope Global -Force -WhatIf:$false Set-Alias -Name Markdown -Value New-Section -Scope Global -Force -WhatIf:$false function New-Section { [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] param( $Text, [Switch]$Skip, [ScriptBlock] $Code ) if ($Skip.IsPresent) { return } if ($PSCmdlet.ShouldProcess("New-Section")) { if ($Text -is [ScriptBlock]) { Write-ScriptBlock $Text } else { Write-StringResult "$Text" Write-StringResult '' if ($null -eq $Code) { Throw "No section script block is provided. (Have you put the open curly brace on the next line?)" } Write-ScriptBlock $Code } Write-StringResult '' } } # experimental function Add-FunctionOverWrite([alias('f', 'Func')]$aFunc, [alias('n', 'WithFunc')]$aWithFunc) { Remove-Item "Alias:\$aFunc" -Force -ErrorAction Ignore New-Alias -Name $aFunc -Value $aWithFunc -Scope Global } # experimental function Invoke-FunctionOverWritten([alias('f', 'Func')]$aFunc) { $v = Get-Alias -Name $aFunc -Scope Global -ErrorAction Ignore if ($v) { Remove-Item "Alias:\$aFunc" -Force -ErrorAction Ignore } &$aFunc if ($v) { New-Alias -Name $aFunc -Value $v.Definition -Scope Global } } # experimental function Remove-FunctionOverWrite { [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] param([alias('f', 'Func')]$aFunc) if ($PSCmdlet.ShouldProcess("Remove-FunctionOverWrite")) { Remove-Item "Alias:\$aFunc" -Force -ErrorAction Ignore } } function FormatTaskName([Parameter(Mandatory = $true)]$Format) { # not supported/needed } function Include { [CmdletBinding()] param( [ScriptBlock] $Code ) if ($null -eq $Code) { Throw "No include script block is provided. (Have you put the open curly brace on the next line?)" } & $code } function Invoke-Task([Parameter(Mandatory = $true)]$TaskName) { Invoke-PerformIfDefined -Command "Action-$TaskName" -ThrowError $true -Manual } # TODO !!EH Convert $Script:PsakeProperties to list of scriptblocks to circumvent multiple allowed properties statements in psake scripts function Properties { [CmdletBinding()] param( [ScriptBlock] $Code ) if ($null -eq $Code) { Throw "No properties script block is provided. (Have you put the open curly brace on the next line?)" } $Script:PsakeProperties = $Code } Set-Alias -Name Invoke-PSake -Value Start-PSakeWorkflow -Scope Global -Force -WhatIf:$false function Start-PSakeWorkflow { [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] param( [Parameter(Mandatory = $true, Position = 0)][string] $File, [Parameter(Position = 1)][HashTable] $Parameters, [Parameter(Position = 2)][HashTable] $Properties ) if ($PSCmdlet.ShouldProcess("Start-PSakeWorkflow")) { $Script:PsakeInvocationParameters = $Parameters $Script:PsakeInvocationProperties = $Properties . $File } } # compatibility with PSake function Task { [CmdletBinding()] param( [Parameter(Mandatory = $true, Position = 0)][string] $Name, [String[]] $Tag = @(), [String[]] $Depends = @(), $Parameters = @{}, [Switch]$AsJob, [String]$Description, [ScriptBlock]$PreCondition = { $true }, [ScriptBlock]$PostCondition, # not implemented [Switch]$ContinueOnError, [String]$FromModule, [Parameter(Position = 1)] [ScriptBlock] $Code ) $lName = $Name -replace 'Invoke-', '' if ($FromModule) { if (!($taskModule = Get-Module -Name $FromModule)) { $taskModule = Get-Module -Name $FromModule -ListAvailable -ErrorAction Ignore -Verbose:$False | Sort-Object -Property Version -Descending | Select-Object -First 1 } $psakeFilePath = Join-Path -Path $taskModule.ModuleBase -ChildPath 'psakeFile.ps1' if (Test-Path $psakeFilePath) { . $psakeFilePath } } else { if ($lName -ne 'Default' -and $lName -ne '.' ) { if ($null -eq $Code) { Write-Verbose "Task: $lName No code script block is provided. (Have you put the open curly brace on the next line?)" $Code = {} } if ($null -ne $PostCondition) { Write-Warning "Post Condition not implemented" } $eaValue = 'Stop' if ($ContinueOnError.IsPresent) { $eaValue = 'Ignore' } Register-Action -Name $Name -Tag $Tag -Depends $Depends -Parameters $Parameters -AsJob:$AsJob.IsPresent -If $PreCondition -ErrorAction $eaValue -Description $Description -Code $Code -TypeName Task } } # start build-in Action: Start Workflow if ($lName -eq 'Default' -or $lName -eq '.') { Start-Workflow -Actions $Depends -Parameters $Parameters -Location (Get-Location) } } #Set-Alias -Name ActionSetup -Value TaskSetup -Scope Global -Force -WhatIf:$false Set-Alias -Name Initialize-Action -Value TaskSetup -Scope Global -Force -WhatIf:$false function TaskSetup { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [scriptblock]$Setup ) $Script:PsakeSetupTask = $Setup } #Set-Alias -Name ActionTearDown -Value TaskTearDown -Scope Global -Force -WhatIf:$false Set-Alias -Name Complete-Action -Value TaskTearDown -Scope Global -Force -WhatIf:$false function TaskTearDown { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [scriptblock]$TearDown ) $Script:PsakeTearDownTask = $TearDown } <# .SYNOPSIS Defines an action executed by Workflow .DESCRIPTION Defines an action executed by Workflow. At least one action is required by workflow. Actions are executed in workflow sequence and/or their dependency order. By using the If condition an action can be excluded from the workflow execution. An action scriptblock can be executed multiple times by using the For expression. Default the action runs in the workflow of the current Powershell session. Optionally action scriptblock can be executed in a Docker Container or a Remote Powershell Session. Action in Docker Container - requires docker installed on current machine Action in Remote Powershell Session - requires valid PSSession with access to remote host (PS Remoting or SSH Remoting) .PARAMETER Name Set the Name of the action .PARAMETER Tag Set the tag(s) array of the action .PARAMETER Depends Set list of dependent actions. Dependent actions are executed before this action in the order of the workflow dependency graph .PARAMETER Parameters Parameters to pass to Workflow .PARAMETER AsJob Run action in Powershell Job (Separate process) .PARAMETER Description Contains description of Action .PARAMETER If Boolean expression to determine if action if executed, like { $value -eq $true } .PARAMETER Disabled Determines if action is disabled, exempt from workflow .PARAMETER NextAction Next Action to execute when action is finished .PARAMETER For Sequence expression for setting the number of executing the same action scriptblock, like { 1..10 } or { 'hello','and','goodby' } .PARAMETER Parallel Determines if the action scriptblock is executed in parallel. Used by the For expression when running in Powershell 7+ and when used with switch -AsJob .PARAMETER Container Determines if action is run in container .PARAMETER ContainerOptions Determines the container options when running action in a container. ContainerOptions.Image contains the container image used. .PARAMETER Session Contains the remote session object to run the action scriptblock on remote host via Powershell Remoting .PARAMETER Isolated Determines when if user ps-modules, scriptbook module and script/workflow are copied/used by (remote) container or remote-session. .PARAMETER Unique Always generates unique name for Action. Prevent collision with required unique Action names .PARAMETER RequiredVariables Enables checking for required variables before action starts. If variable not available in global, script or local scope action fails. .PARAMETER Comment Add Comment to Action for documentation purpose .PARAMETER SuppressOutput if present suppresses write/output of Action return value or output stream to console .PARAMETER Always Determines if Action is Always executed regardless of any error in other actions, or missing in dependency tree, or missing in Workflow Actions .PARAMETER NoSequence Determines if Action is executed when sequential/linear Workflow is executed .PARAMETER Multiple Determines if Action can be called multiple times, default Action can only be called once .PARAMETER Code Contains the action code/scriptblock to execute when action is enabled (default) .EXAMPLE .NOTES #> Set-Alias -Name Test -Value Action -Scope Global -Force -WhatIf:$false -Confirm:$False Set-Alias -Name Tests -Value Action -Scope Global -Force -WhatIf:$false -Confirm:$False Set-Alias -Name Step -Value Action -Scope Global -Force -WhatIf:$false -Confirm:$False Set-Alias -Name Activity -Value Action -Scope Global -Force -WhatIf:$false -Confirm:$False Set-Alias -Name Job -Value Action -Scope Global -Force -WhatIf:$false -Confirm:$False Set-Alias -Name Chore -Value Action -Scope Global -Force -WhatIf:$false -Confirm:$False Set-Alias -Name Stage -Value Action -Scope Global -Force -WhatIf:$false -Confirm:$False Set-Alias -Name Override -Value Action -WhatIf:$false -Confirm:$False Set-Alias -Name Setup -Value Action -Scope Global -Force -WhatIf:$false -Confirm:$False Set-Alias -Name Teardown -Value Action -Scope Global -Force -WhatIf:$false -Confirm:$False function Action { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSupportsShouldProcess", "")] param( [Parameter(Mandatory = $true, Position = 0)][string] $Name, [String[]] $Tag = @(), [String[]] $Depends = @(), [HashTable]$Parameters = @{}, [Switch]$AsJob, [ValidateNotNullOrEmpty()] [String]$Description, [ScriptBlock] $If, [alias('Skip')][switch] $Disabled, [ValidateNotNullOrEmpty()] [string][alias('Next')] $NextAction, [ScriptBlock] $For, [switch] $Parallel, [switch] $Container, [HashTable] $ContainerOptions = @{}, [AllowNull()] $Session, [switch]$Isolated, [switch]$Unique, [ValidateNotNull()] [string[]]$RequiredVariables = @(), [string]$Comment, [switch]$SuppressOutput, [switch]$Always, [switch]$NoSequence, [switch]$WhatIf, [switch]$Confirm, [switch]$Multiple, [Parameter(Position = 1)] [ScriptBlock] $Code ) $lName = $Name -replace 'Invoke-', '' if ($PSCmdlet.MyInvocation.InvocationName -eq 'Test') { $Name = "Test.$Name" $lName = $Name } if ($lName -ne 'Default' -and $lName -ne '.' ) { if ($null -eq $Code) { if ([string]::IsNullOrEmpty($Name)) { Throw "No code script block is provided and Name property is mandatory. (Have you put the open curly brace on the next line?)" } else { $n = $Name.Split("`n") if ($n.Count -gt 1) { Throw "No Name provide for Action, Name is required, found scriptblock { $Name } instead." } else { Throw "No code script block is provided for '$Name'. (Have you put the open curly brace on the next line?)" } } } $typeName = $PSCmdlet.MyInvocation.InvocationName $uniqueOption = $Unique.IsPresent $alwaysOption = $Always.IsPresent $errorOption = $ErrorActionPreference if ($typeName -eq 'Teardown') { $alwaysOption = $true } elseif ($typeName -eq 'Tests') { $errorOption = 'Continue' } $options = @{ Name = $Name Tag = $Tag Depends = $Depends Parameters = $Parameters ErrorAction = $errorOption AsJob = $AsJob.IsPresent If = $If Description = $Description Code = $Code Disabled = $Disabled.IsPresent TypeName = $typeName NextAction = $NextAction For = $For Parallel = $Parallel.IsPresent Container = $Container.IsPresent ContainerOptions = $ContainerOptions Session = $Session Isolated = $Isolated.IsPresent Unique = $uniqueOption RequiredVariables = $RequiredVariables Comment = $Comment SuppressOutput = $SuppressOutput.IsPresent Always = $alwaysOption NoSequence = $NoSequence.IsPresent WhatIf = $WhatIf.IsPresent Confirm = $Confirm.IsPresent Multiple = $Multiple.IsPresent } Register-Action @options } # Start build-in Action: Start Workflow if ($lName -eq 'Default' -or $lName -eq '.') { Start-Workflow -Actions $Depends -Parameters $Parameters -Location (Get-Location) } } <# .SYNOPSIS Add Workflow Notification .DESCRIPTION Add Notifications to Workflow. Notification are shown on console when Workflow is finished. A way to emphasis Workflow output .PARAMETER Message Notification Message .PARAMETER Decoration Determines if notification is just decorative .PARAMETER Target Determines target of Notification, just console for now. #> function Add-WorkflowNotification([ValidateNotNullOrEmpty()][string]$Message, [switch]$Decoration, $Target = 'Console') { $ctx = Get-RootContext [void]$ctx.Notifications.Add($Message) } <# .SYNOPSIS Disables action to execute .DESCRIPTION Disables action to execute, can change during run-time .PARAMETER Name Name of the Action #> function Disable-Action([ValidateNotNullOrEmpty()][string]$Name) { $ctx = Get-RootContext if ($ctx.Actions.Count -eq 0) { throw "No actions defined or workflow finished." } $action = $ctx.Actions["Action-$($Name.Replace('Action-',''))"] if ($action) { $action.Disabled = $true } else { throw "Action $Name not found in Disable-Action" } } <# .SYNOPSIS Edit the workflow parameters defined in the workflow via the host console .DESCRIPTION Edit the workflow parameters defined in the workflow via the host console. .PARAMETER Name Name of the Parameters Set .PARAMETER Path Storage location of Parameters values in json format .PARAMETER Notice Message to show in Console Host before getting values from user #> function Edit-WorkflowParameters { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] param($Name, $Path, $Notice) Read-ParameterValuesFromHost -Name $Name -Notice $Notice if ($null -ne $Path) { Save-ParameterValues -Name $Name -Path $Path } } <# .SYNOPSIS Enables action to execute .DESCRIPTION Enabled action to execute, can change during run-time .PARAMETER Name Name of the Action #> function Enable-Action([ValidateNotNullOrEmpty()][string]$Name) { $ctx = Get-RootContext if ($ctx.Actions.Count -eq 0) { throw "No actions defined or workflow finished." } $action = $ctx.Actions["Action-$($Name.Replace('Action-',''))"] if ($action) { $action.Disabled = $false } else { throw "Action $Name not found in Enable-Action" } } <# .SYNOPSIS Returns Action return-value or the output written to output streams .DESCRIPTION Returns Action return-value or the output written to output streams. Each Action can return output for use in other Actions. Normally output is ignored and does not interfere with workflow .PARAMETER Name Name of the Action #> Set-Alias -Name Get-ActionOutput -Value Get-ActionReturnValue -Scope Global -Force -WhatIf:$false Set-Alias -Name Get-ActionOutputValue -Value Get-ActionReturnValue -Scope Global -Force -WhatIf:$false function Get-ActionReturnValue { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "")] param($Name) $ctx = Get-RootContext if ($ctx.Actions.Count -eq 0) { throw "No actions defined or workflow finished." } $returnValue = $null $action = $ctx.Actions["Action-$($Name.Replace('Action-',''))"] if ($action) { $script:InvokedCommandsResult | ForEach-Object { if ($_.Command -eq $action.Name) { $returnValue = $_.ReturnValue } } } else { throw "Action $Name not found in Get-ActionReturnValue" } return $returnValue } <# .SYNOPSIS Returns Action runtime state .DESCRIPTION Returns Action runtime state. Each Action has some runtime state like return-value or exception .PARAMETER Name Name of the Action #> Set-Alias -Name Get-ActionOutput -Value Get-ActionReturnValue -Scope Global -Force -WhatIf:$false Set-Alias -Name Get-ActionOutputValue -Value Get-ActionReturnValue -Scope Global -Force -WhatIf:$false function Get-ActionState { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "")] param($Name) $ctx = Get-RootContext if ($ctx.Actions.Count -eq 0) { throw "No actions defined or workflow finished in Get-ActionState" } $returnValue = $null $action = $ctx.Actions["Action-$($Name.Replace('Action-',''))"] if ($action) { $script:InvokedCommandsResult | ForEach-Object { if ($_.Command -eq $action.Name) { $returnValue = @{ Name = $_.Name Duration = $_.Duration Exception = $_.Exception HasError = $_.HasError ReturnValue = $_.ReturnValue Command = $_.Command; Comment = $_.Comment Invoked = $true } } } if ($null -eq $returnValue) { $returnValue = @{ Name = $action.Name Duration = 0 Exception = $null HasError = $false ReturnValue = $null Command = $action.Command; Comment = $_.Comment Invoked = $false } } } else { Throw "Action $Name not found in Get-ActionState" } return $returnValue } <# .SYNOPSIS Retrieves Workflow Context .DESCRIPTION Retrieves Workflow Context .EXAMPLE $ctx = Get-WorkflowContext #> function Get-WorkflowContext { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "")] param() if (!(Get-Variable -Name Context -ErrorAction Ignore -Scope Global)) { Set-Variable -Name Context -Value @{ } -Scope Global -WhatIf:$false } return $Global:Context } <# .SYNOPSIS Retrieves one Workflow Setting .DESCRIPTION Retrieves one Workflow Setting like InTest mode .PARAMETER Name Name of Setting .EXAMPLE Get-WorkflowSetting -Name 'InTest' #> function Get-WorkflowSetting([ValidateNotNullOrEmpty()]$Name) { $ctx = Get-RootContext if ($Name -eq 'InTest') { return $ctx.InTest } else { Throw "Workflow Setting '$Name' not recognized" } } <# .SYNOPSIS Imports one or more Actions from File with parent defined parameters .DESCRIPTION Imports one or more Actions from File with parent defined parameters. Use this in your startup scripts. .PARAMETER File Name of the File .EXAMPLE Script File with One Action (myAction.ps1): param( [ValidateNotNullOrEmpty()] $MyParam1 ) Action -Name MyAction { Write-Info "Script param value: $MyParam1" Write-Info 'MyAction' } Startup Script (startup.ps1): param( $MyParam1 = 'Hello' ) Set-Location $PSScriptRoot Import-Module Scriptbook Import-Action ./myAction.ps1 #-Context $ExecutionContext Start-Workflow .EXAMPLE Alternative is in startup script: $parameters = Get-BoundParametersWithDefaultValue . ./myAction.ps1 @parameters #> Set-Alias -Name Import-Test -Value Import-Action -Scope Global -Force -WhatIf:$false Set-Alias -Name Import-Step -Value Import-Action -Scope Global -Force -WhatIf:$false #Set-Alias -Name Import-Activity -Value Import-Action -Scope Global -Force -WhatIf:$false #Set-Alias -Name Import-Job -Value Import-Action -Scope Global -Force -WhatIf:$false #Set-Alias -Name Import-Stage -Value Import-Action -Scope Global -Force -WhatIf:$false #Set-Alias -Name Import-Flow -Value Import-Action -Scope Global -Force -WhatIf:$false function Import-Action { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] [CmdletBinding()] param ( [ValidateNotNullOrEmpty()] $File, #$Context, $Invocation = $Global:MyInvocation ) $parameters = Get-BoundParametersWithDefaultValue $Invocation $localVars = @{} Get-Variable -Scope Local | ForEach-Object { [void]$localVars.Add($_.Name, $null) } Get-ChildItem $File | Sort-Object -Property FullName | ForEach-Object { $parameterList = (Get-Command -Name $_.FullName).Parameters; $parameterSelector = $parameters.Clone() foreach ($pm in $parameterSelector.Keys) { if (!$parameterList.ContainsKey($pm)) {$parameters.Remove($pm)} } . $($_.FullName) @parameters } $newLocalVars = Get-Variable -Scope Local foreach ($var in $newLocalVars.GetEnumerator()) { if (!$localVars.ContainsKey($var.Name)) { Set-Variable -Scope Global -Name $var.Name -Value $var.Value -Force -Visibility Public } } } <# Scoping Experiments. Unable to run import in Caller Scope, local vars missing --> copy local vars for now # in module scope $m = Get-Module Scriptbook & $m { param($File, $Parameters); . ./$File @Parameters } -File $File -Parameters $parameters return # in current scope . ./$File @parameters # in current scope Invoke-Command { param($File, $Parameters); . ./$File @parameters } -ArgumentList $File, $parameters #-NoNewScope # in caller context scope $module = [PSModuleInfo]::New($true) $module.SessionState = $Context.SessionState & { param($File, $Parameters); . ./$File @Parameters } -File $File -Parameters $parameters # in caller context scope . $module ./$File @parameters # in context scope $sb = { . ./configure.actions.ps1 -OrganizationPrefix 'td' -Environments @('dev', 'tst') -SubscriptionId '45f8a4be-d177-489e-8ec2-e1a53d87aadc' } $Context.InvokeCommand.InvokeScript($Context.SessionState, $sb, @()) #> <# .SYNOPSIS Imports Parameters from File .DESCRIPTION Imports Parameters from File .PARAMETER File Name of the File #> function Import-Parameters { [CmdletBinding()] param ( [ValidateNotNullOrEmpty()] $File ) . $File } <# .SYNOPSIS Invokes an Action by name. If action is not found error is generated .DESCRIPTION Invokes an Action by name. If action is not found error is generated. Allows to call an Action in another Action scriptblock. .PARAMETER Name Name of Action .EXAMPLE Invoke-Action -Name Hello #> Set-Alias -Name Invoke-Step -Value Invoke-Action -Scope Global -Force -WhatIf:$false function Invoke-Action([Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$Name) { $cnt = $script:InvokedCommands.Count Invoke-PerformIfDefined -Command "Action-$($Name.Replace('Action-',''))" -ThrowError $true -Manual if ($script:InvokedCommands.Count -eq ($cnt + 1) ) { return $script:InvokedCommands[$cnt] } else { return $null } } <# .SYNOPSIS Executes a ScriptBlock with commands .DESCRIPTION Executes a ScriptBlock with commands. If ScriptBlock contains native commands LastExitCode is checked. .PARAMETER ScriptBlock The scriptblock with commands to execute .PARAMETER Message The message to display when command fails. Use it to hide secrets or long cmd lines. .PARAMETER Location Current Location or working directory of command .PARAMETER IgnoreExitCode Ignores the LastExitCode check .PARAMETER AsJson Return result as Json Object if possible .EXAMPLE Execute { cmd.exe /c } .EXAMPLE Invoke-ScriptBlock { cmd.exe /c } -Message 'cmd' -Location c:\ -IgnoreExitCode #> Set-Alias -Name Execute -Value Invoke-ScriptBlock -Scope Global -Force -WhatIf:$false Set-Alias -Name Exec -Value Invoke-ScriptBlock -Scope Global -Force -WhatIf:$false function Invoke-ScriptBlock { [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory = $true, Position = 0)][Scriptblock] $ScriptBlock, [alias('m')] [string][ValidateNotNullOrEmpty()] $Message, [alias('wd', 'WorkingDirectory')][ValidateNotNullOrEmpty()] $Location, [switch]$IgnoreExitCode, [switch]$AsJson #,[string]$Image # run in container image, TODO !!EH Mem, Cpu, disk, returns out-files/volume ) function ConvertFrom-JsonInternal { [CmdletBinding()] param( [Parameter(ValueFromPipeline)] [string] $line ) begin { $lines = [System.Collections.ArrayList]@() } process { [void]$lines.Add($line) } end { # only valid json passes, otherwise return original stream as strings try { return $lines | ConvertFrom-Json } catch { return $lines | ConvertTo-String } } } # prevent accidental scope name collision $internalMessage = $Message Remove-Variable -Name Message -Scope Local $internalLocation = $Location Remove-Variable -Name Location -Scope Local $internalScriptBlock = $ScriptBlock Remove-Variable -Name ScriptBlock -Scope Local Write-Verbose "Start Executing $internalMessage" if (-not $PSCmdlet.ShouldProcess($internalLocation)) { return } if ($internalLocation) { Push-Location $internalLocation } try { $Global:LastExitCode = 0 # https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_scopes?view=powershell-7.1 #. $internalScriptBlock # current scope == module scope for functions in modules (scripts are in user scope) # current scope == child scope if ($AsJson.IsPresent) { Invoke-Command -ScriptBlock $internalScriptBlock | ConvertFrom-JsonInternal } else { Invoke-Command -ScriptBlock $internalScriptBlock } [Console]::ResetColor() # some programs mess this up if ( ($Global:LastExitCode -ne 0) -and !$IgnoreExitCode.IsPresent ) { # to prevent secret leaks use Message parameter in commands to hide secrets/passwords $msg = $internalMessage if (!$msg) { $msg = $internalScriptBlock.ToString() } Throw "Executing $msg failed with exit-code $($Global:LastExitCode)" } } finally { if ($internalLocation) { Pop-Location } Write-Verbose "Finish Executing $internalMessage" } } <# .SYNOPSIS Writes to the ConsoleHost .DESCRIPTION Writes to the ConsoleHost .PARAMETER InputObject Value to write to Console Host .EXAMPLE 'Hello' | Out-ScriptbookHost #> Set-Alias -Name Out-Info -Value Out-ScriptbookHost -Scope Global -Force -WhatIf:$false function Out-ScriptbookHost { [CmdletBinding()] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [object] $InputObject ) $InputObject | Out-Default } <# .SYNOPSIS Writes to the Verbose stream if -Verbose .DESCRIPTION Writes to the verbose stream if -Verbose present .PARAMETER InputObject Value to write to verbose stream .EXAMPLE 'Hello' | Out-ScriptbookVerbose or 'Hello' | Out-NullSb #> Set-Alias -Name Out-NullSb -Value Out-ScriptbookVerbose -Scope Global -Force -WhatIf:$false -ErrorAction Ignore Set-Alias -Name Out-Verbose -Value Out-ScriptbookVerbose -Scope Global -Force -WhatIf:$false -ErrorAction Ignore function Out-ScriptbookVerbose { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseProcessBlockForPipelineCommand", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingEmptyCatchBlock", "")] [CmdletBinding()] param( [Parameter(ValueFromPipeline = $true)] [object] $InputObject ) if ($InputObject) { try { if ($Global:VerbosePreference -eq 'Continue') { Write-Verbose ($InputObject | Out-String) -Verbose } elseif ($Script:RootContext) { $ctx = Get-RootContext if ($ctx.Verbose) { Write-Verbose ($InputObject | Out-String) -Verbose } } } catch { # no exception in catch 'Out-NullSb' ever } } } <# .SYNOPSIS Define Scriptbook Workflow Parameters .DESCRIPTION Define Scriptbook Workflow Parameters in PowerShell HashTable format and creates named PowerShell variable with HashTable as contents. .PARAMETER Name Name of created Parameters PowerShell variable, prevent naming conflicts by choosing unique or prefixed names .PARAMETER Path Location of Parameters values in json format .PARAMETER Override Allow the current Parameters to be over-written, default is to merge parameters from file and script .PARAMETER Code HashTable with Parameters .REMARK .EXAMPLE Parameters -Name 'DefaultParameters' { @{ Variable1 = 'one' Variable2 = 'two' } } # Access Context in Scriptbook like Write-Host $Context.DefaultParameters.Variable1 Write-Host $Context.DefaultParameters.Variable2 or $ctx = Get-WorkflowContext Write-Host $ctx.DefaultParameters.Variable1 Write-Host $ctx.DefaultParameters.Variable2 # or via variable name Write-Host $DefaultParameters.Variable1 Write-Host $DefaultParameters.Variable2 # Access Context in Scriptbook Action like Write-Host $Context.DefaultParameters.Variable1 # or Write-Host $Global:Context.DefaultParameters.Variable2 #or Write-Host $Global:DefaultParameters.Variable2 # Update parameter in Scriptbook Action with $Global:Context.DefaultParameters.Variable1 = 'newValue' or $Global:DefaultParameters.Variable1 = 'newValue' #> function Parameters { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "")] [CmdletBinding(SupportsShouldProcess = $True)] param( [Parameter(Mandatory = $true, Position = 0)][string]$Name, $Path, [switch]$Override, [Parameter(Position = 1)] [ScriptBlock]$Code ) if ($WhatIfPreference) { Write-Host "What if: Performing the operation 'Parameters' on target '$Name'" } if ($Name -eq 'Parameters') { Throw "Invalid Parameters name found, '$Name' not allowed" } if (!($Override.IsPresent) -and (Get-Variable -Name Context -ErrorAction Ignore -Scope Global) -and $Global:Context.ContainsKey($Name)) { Write-Warning "Duplicate Parameters name '$Name' found, use Override to replace current Parameter name" } if ($null -eq $Code -and !$Path) { Throw "No parameters script block is provided with HashTable. (Have you put the open curly brace on the next line?)" } try { $value = $null $valueRead = $null if ($Path -and (Test-Path $Path -ErrorAction Ignore)) { $valueRead = Read-ParameterValuesInternal -Path $Path } $value = (Invoke-Command $Code) if (-not (($value -is [HashTable]) -or ($value -is [System.Collections.Specialized.OrderedDictionary]))) { throw 'No HashTable found in ''Parameters'', minimal an empty hashtable is required in Parameters Action' } # convert parameters HashTable to internal HashTable == context, we support different formats of parameters if ($valueRead -and $Override.IsPresent) { $internalValue = $valueRead } elseif ($valueRead) { foreach ($v in $valueRead.GetEnumerator()) { $value[$v.Key] = $v.Value } $internalValue = $value } else { $internalValue = $value } # add Version variable if not found, init with 1.0.0 if (!($internalValue.Contains('Version'))) { $internalValue.Version = '1.0.0' } # create context if (!(Get-Variable -Name Context -ErrorAction Ignore -Scope Global)) { Set-Variable -Name Context -Value @{ } -Scope Global -WhatIf:$false } $Global:Context."$Name" = $internalValue Set-Variable -Name $Name -Value $internalValue -Scope Global -WhatIf:$false if ($Global:ConfigurePreference) { Read-ParameterValuesFromHost -Name $Name -Notice 'Configure Scriptbook Parameters' if ($null -ne $Path) { Save-ParameterValues -Name $Name -Path $Path } } } catch { Write-Warning "Error setting '$Name' parameters to '$Code' $($_.Exception.Message)" Write-Warning "Only HashTable @{ Name = 'default'; Name2 = 'default2'} is allowed" } } <# .SYNOPSIS Global function hooks which can be used at runtime to get more detailed information about the running workflow and actions .DESCRIPTION Global function hooks which can be used at runtime to get more detailed information about the running workflow and actions #> function Global:Invoke-BeforeWorkflow($Commands) { return $true } function Global:Invoke-AfterWorkflow($Commands, $ErrorRecord) { } function Global:Invoke-BeforePerform($Command) { return $true } function Global:Invoke-AfterPerform($Command, $ErrorRecord) { } function Global:Write-OnLog($Msg) {} function Global:Write-OnLogException($Exception) {} <# .SYNOPSIS DependsOn attribute to register function dependencies .DESCRIPTION DependsOn attribute to register function dependencies. Allows for using functions like Actions with dependency graph support. .EXAMPLE # implicit invokes function Invoke-Hello because function Invoke-Goodby is dependent on it function Invoke-Hello { Write-Info "Hello" } function Invoke-GoodBy { [DependsOn(("Hello"))]param() Write-Info "GoodBy" } Start-Workflow Goodby #> class DependsOn : System.Attribute { [string[]]$Name DependsOn([string[]]$name) { $this.Name = $name } } <# .SYNOPSIS Reads the Parameter values .DESCRIPTION Reads the Parameter values .PARAMETER Name Name of created Parameters PowerShell variable .PARAMETER Path Location of Parameters values in json format .EXAMPLE Read-ParameterValues -Name 'Params' -Path './my-parameter-values.json' #> function Read-ParameterValues { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] [CmdletBinding(SupportsShouldProcess)] param( [ValidateNotNullOrEmpty()] [string]$Name, [ValidateNotNullOrEmpty()] $Path ) if ($PSCmdlet.ShouldProcess("Read-ParameterValues")) { $internalValue = Read-ParameterValuesInternal -Path $Path if (!(Get-Variable -Name Context -ErrorAction Ignore -Scope Global)) { Set-Variable -Name Context -Value @{ } -Scope Global } $Global:Context."$Name" = $internalValue Set-Variable -Name $Name -Value $internalValue -Scope Global } } <# .SYNOPSIS Reads the Parameter values from Host / console .DESCRIPTION Reads the Parameter values Host / console .PARAMETER Name Name of created Parameters PowerShell variable .EXAMPLE Read-ParameterValuesFromHost -Name 'Params' #> function Read-ParameterValuesFromHost { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "")] [CmdletBinding()] param( [ValidateNotNullOrEmpty()] [string]$Name, [string]$Notice ) $internalValue = [ordered]@{} $ctx = Get-WorkflowContext if ($ctx.ContainsKey($Name)) { Write-Host '' if ($Notice) { Write-Host '==========================================================================' -ForegroundColor Blue Write-Host "$Notice $(if ($WhatIfPreference) { '(WhatIf)' })" -ForegroundColor Blue } Write-Host '==========================================================================' -ForegroundColor Blue Write-Host " Enter '$Name' data fields or use default value with [enter] key." Write-Host ' Use [shift][insert] keys if default Paste key is not working.' Write-Host '==========================================================================' -ForegroundColor Blue $l = 0 foreach ($parameter in $ctx[$Name].GetEnumerator()) { if ($parameter.Key.Length -gt $l) { $l = $parameter.Key.Length } } foreach ($parameter in $ctx[$Name].GetEnumerator()) { $key = $parameter.Key.PadLeft($l) $secret = $false if ($null -ne $parameter.Value) { $secret = ($parameter.Value -is [SecureString]) -or (Test-IsSecureStringStorageObject $parameter.Value) } if ($secret) { $defaultValue = '***********' } else { $defaultValue = $parameter.Value } if (!($WhatIfPreference) ) { $result = Read-Host -Prompt "$Key [$(Get-AnsiColoredString -String $defaultValue -Color 93)]" } else { $result = $null } if (![string]::IsNullOrEmpty($result)) { if ($secret) { [void]$internalValue.Add($parameter.Key, (ConvertTo-SecureString -String $result -AsPlainText)) } else { [void]$internalValue.Add($parameter.Key, $result) } } else { [void]$internalValue.Add($parameter.Key, $parameter.Value) } } } else { Throw "Invalid Parameters name found, '$Name' not does not exists" } if (!(Get-Variable -Name Context -ErrorAction Ignore -Scope Global)) { Set-Variable -Name Context -Value @{ } -Scope Global -WhatIf:$false } $Global:Context."$Name" = $internalValue Set-Variable -Name $Name -Value $internalValue -Scope Global -WhatIf:$false } <# .SYNOPSIS Registers and validates a new Action for Workflow. .DESCRIPTION Registers and validates a new Action for Workflow. Action is scanned/recorded but not executed until the workflow starts. .PARAMETER Name .PARAMETER IsGroup .PARAMETER Tag .PARAMETER Depends .PARAMETER Parameters .PARAMETER AsJob .PARAMETER If .PARAMETER Description .PARAMETER Disabled .PARAMETER TypeName .PARAMETER NextAction .PARAMETER For .PARAMETER Parallel .PARAMETER Container .PARAMETER ContainerOptions .PARAMETER Session .PARAMETER Isolated .PARAMETER Unique .PARAMETER RequiredVariables .PARAMETER Comment .PARAMETER SuppressOutput .PARAMETER Always .PARAMETER NoSequence .PARAMETER Multiple .PARAMETER Code .EXAMPLE Register-Action #> function Register-Action { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSupportsShouldProcess", "")] [OutputType([System.Void])] param( [Parameter(Mandatory = $true, Position = 0)][string][ValidateNotNullOrEmpty()]$Name, [switch]$IsGroup, [string[]] $Tag = @(), [string[]] $Depends = @(), [AllowNull()] $Parameters = @{}, [AllowNull()] [switch]$AsJob, [ScriptBlock] $If, [String]$Description, [bool]$Disabled = $false, [ValidateNotNullOrEmpty()] [string]$TypeName, [string]$NextAction, [ScriptBlock] $For, [switch]$Parallel, [switch]$Container, [HashTable]$ContainerOptions = @{}, [AllowNull()] $Session, [switch]$Isolated, [switch]$Unique, [string[]]$RequiredVariables, [string]$Comment, [switch]$SuppressOutput, [switch]$Always, [switch]$NoSequence, [switch]$WhatIf, [switch]$Confirm, [switch]$Multiple, [ScriptBlock] $Code ) $ctx = Get-RootContext $lName = $Name -replace 'Invoke-', '' if ($Unique.IsPresent) { $ctx.UniqueIdCounter += 1 $lName = "$lName-$("{0:00}" -f $ctx.UniqueIdCounter)" } # Get Comments $text = $null if ($Comment) { $text = $Comment + [System.Environment]::NewLine } $text += Get-CommentFromCode -ScriptBlock $Code $lAction = New-Object PSObject -Property @{ Code = $Code Name = "Action-$lName" DisplayName = $lName Id = (New-Guid) Tags = $Tag ErrorAction = $ErrorActionPreference Depends = $Depends Parameters = $Parameters AsJob = [bool]$AsJob.IsPresent If = $If Description = $Description IsGroup = $IsGroup.IsPresent Disabled = $Disabled TypeName = $TypeName NextAction = $NextAction For = $For Parallel = $Parallel.IsPresent Container = $Container.IsPresent ContainerOptions = $ContainerOptions Session = $Session Isolated = $Isolated RequiredVariables = $RequiredVariables Comment = ($text | Out-String) SuppressOutput = $SuppressOutput Always = $Always NoSequence = $NoSequence WhatIf = $WhatIf Confirm = $Confirm Multiple = $Multiple } if ($ctx.Actions.ContainsKey($lAction.Name)) { $displayName = $lAction.Name.Replace('Action-', '').Replace('Invoke-', '') Throw "Duplicate Name '$displayName' found, use unique name for each action/step/job/flow" } # add if ($ctx.InAction) { [void]$ctx.NestedActions.Add($lAction) } else { [void]$ctx.ActionSequence.Add($lAction) } [void]$ctx.Actions.Add($lAction.Name, $lAction) Set-Alias -Name $Name -Value Action -Scope Global -Force -ErrorAction Ignore -WhatIf:$false -Confirm:$False } <# .SYNOPSIS Resets the workflow global state/variables .DESCRIPTION Resets the workflow global state/variables and prepares for next workflow start in current session, enables support of multiple workflows in one session. Also used for unit testing purposes. .EXAMPLE Reset-Workflow #> function Reset-Workflow { [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] param( [switch]$Soft ) if ($PSCmdlet.ShouldProcess("Reset-Workflow")) { Remove-Variable -Force -ErrorAction Ignore -Scope Global -Name Context $Script:RootContext = New-RootContext -WhatIf:$false -Soft:$Soft.IsPresent # compatibility with PSake $Script:PSakeProperties = $null $Script:PSakeInvocationParameters = $null $Script:PSakeInvocationProperties = $null $Script:PSakeSetupTask = $null $Script:PSakeTearDownTask = $null } } <# .SYNOPSIS Saves the Parameter values .DESCRIPTION Saves the Parameter values .PARAMETER Name Name of created Parameters PowerShell variable .PARAMETER Path Location of Parameters values in json format .EXAMPLE Save-ParameterValues -Name 'Params' -Path './my-parameter-values.json' #> function Save-ParameterValues { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] [CmdletBinding(SupportsShouldProcess)] param( [ValidateNotNullOrEmpty()] [string]$Name, [ValidateNotNullOrEmpty()] $Path ) if ($PSCmdlet.ShouldProcess($Path)) { $ctx = Get-WorkflowContext if ($ctx.ContainsKey($Name)) { if ($ctx[$Name] -is [PSCustomObject]) { $object = $ctx[$Name] } else { $object = [PSCustomObject]$ctx[$Name] } function Set-Props { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] param($Object) foreach ($prop in $Object.PsObject.Properties) { if ($prop.Value -is [SecureString]) { $prop.Value = [SecureStringStorage]$prop.Value } } } # fix SecureString references Set-Props $object $object | ConvertTo-Json -Depth 10 | Set-Content -Path $Path } else { Throw "Invalid Parameters name found, '$Name' not does not exists" } } } <# .SYNOPSIS Set the workflow in configure Mode and enables interactive use. .DESCRIPTION Set the workflow in configure Mode and enables interactive use. Enables reading Parameters from Console. .PARAMETER Value Set Configure Mode On/Off .EXAMPLE Set-WorkflowInConfigureMode $true #> function Set-WorkflowInConfigureMode { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "")] [CmdletBinding(SupportsShouldProcess)] param( [bool]$Value ) if ($Value) { if (Get-IsPowerShellStartedInNonInteractiveMode) { Throw "Unable to set workflow in Configure Mode when running PowerShell in 'Non Interactive Mode'" } } $ctx = Get-WorkflowContext if ($PSCmdlet.ShouldProcess('Set-WorkflowInConfigureMode')) { $ctx.ConfigurePreference = $Value $Global:ConfigurePreference = $Value } } <# .SYNOPSIS Starts a Scriptbook .DESCRIPTION Starts a Scriptbook .PARAMETER File Starts the workflow actions from Workflow file specified .PARAMETER Actions Contains the Action(s) to execute in sequential order. Overrides the order found in the script file and limits the actions to execute. Depended actions are always executed except when switch NoDepends is used. Use * to select all Actions in script. Use wildcard '*" to select actions by name with wildcard. .PARAMETER Parameters Parameters to pass to Workflow .PARAMETER Container Determines if workflow is run in container .PARAMETER ContainerOptions Determines the container options when running in a container. ContainerOptions.Image contains the container image used. .PARAMETER Configure Determines if workflow starts in configure mode --> Workflow configuration parameters are fetched from Console. Workflow is not executed. .PARAMETER AsJob Run Scriptbook in Powershell Job (Separate process) .EXAMPLE Start-Scriptbook ./hallo-01.scriptbook.ps1 #> function Global:Start-Scriptbook { [CmdletBinding(SupportsShouldProcess = $True)] param( $File, $Actions, $Parameters, [switch]$Container, [HashTable]$ContainerOptions = @{}, [switch]$Configure, [switch]$AsJob ) if ($PSCmdlet.ShouldProcess($File)) { # Let WhatsIf be handled downstream } Set-WorkflowInConfigureMode $Configure.IsPresent try { if ($Configure.IsPresent) { $AsJob = $false } if ($Container.IsPresent -or ($ContainerOptions.Count -gt 0) -and !$env:InScriptbookContainer) { Start-ScriptInContainer -File $Script:MyInvocation.ScriptName -Options $ContainerOptions -Parameters $Parameters -WhatIf:$WhatIfPreference return } else { $extraParams = @{} if ($Actions) { [void]$extraParams.Add('Actions', $Actions) } if ($Parameters) { [void]$extraParams.Add('Parameters', $Parameters) } if ($WhatIfPreference) { [void]$extraParams.Add('WhatIf', $true) } if ($AsJob.IsPresent) { Start-Job { $file = $args[0] $extraParams = $args[1] . $file @extraParams } -ArgumentList $File, $extraParams | Wait-Job | Receive-Job } else { . $File @extraParams } } } finally { Set-WorkflowInConfigureMode $false } } <# .SYNOPSIS Starts a Workflow defined by Workflow Actions or PowerShell Functions .DESCRIPTION Starts a Workflow defined by Workflow Actions or PowerShell Functions. Each action scriptblock is executed once in the order given by the workflow. Use the workflow report to see the final execution order/stack. The workflow is executed by the order of actions found in the script Workflow file or by the actions parameter. When actions have dependencies they are resolved at run-time and executed according the dependency graph. To influence the execution of actions use the action 'If' property .PARAMETER Actions Contains the Action(s) to execute in sequential order. Overrides the order found in the script file and limits the actions to execute. Depended actions are always executed except when switch NoDepends is used. Use * to select all Actions in script. Use wildcard '*" to select actions by name with wildcard. .PARAMETER Parameters Parameters to pass to Workflow .PARAMETER Name Set the name of the workflow .PARAMETER Tag Set the workflow Tag(s) .PARAMETER Location Set current directory to location specified .PARAMETER File Starts the workflow actions from Workflow file specified .PARAMETER NoReport Disables the action report at the end of the workflow .PARAMETER NoLogging Disables the start/finish action logging .PARAMETER NoDepends Disables the calling of dependent actions. Allows for executing one specific Action .PARAMETER Test Starts workflow in 'TestWorkflow' Mode. No actions are executed except Test Actions .PARAMETER Transcript Creates a record of PowerShell Workflow session and saves this to a file. .PARAMETER Container Determines if workflow is run in container .PARAMETER ContainerOptions Determines the container options when running in a container. ContainerOptions.Image contains the container image used. .PARAMETER Parallel Determines if the workflow actions are executed in parallel. !Experimental .PARAMETER WhatIf / Plan Shows the Workflow execution plan .PARAMETER Documentation Shows the Workflow documentation and execution plan .EXAMPLE Start-WorkFlow .REMARKS - Workflow File not working yet --> Include ? .NOTES Workflow Workflow is modeled as a set of actions invoked in some sequence where the completion of one action flows directly into the start of the next action #> Set-Alias -Name Start-Flow -Value Start-Workflow -Scope Global -Force -WhatIf:$false Set-Alias -Name Start-Saga -Value Start-Workflow -Scope Global -Force -WhatIf:$false Set-Alias -Name Start-Pipeline -Value Start-Workflow -Scope Global -Force -WhatIf:$false function Start-Workflow { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "")] [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Position = 0)] [AllowNull()] [array][alias('Actions', 'Steps', 'Jobs', 'Activities')]$WorkflowActions, [AllowNull()] [alias('Parameters')] $WorkflowParameters, [ValidateNotNullOrEmpty()] [alias('Name')] [string]$WorkflowName = '.', [String[]] $Tag = @(), [alias('Location')] [string]$WorkflowLocation, [alias('File')] [string]$WorkflowFile, [switch]$NoReport, [switch]$NoLogging, [switch]$NoDepends, [alias('Test')] [switch]$TestWorkflow, [alias('Transcript')] [switch]$WorkflowTranscript, [alias('Container')] [switch]$WorkflowContainer, [alias('ContainerOptions')] [HashTable]$WorkflowContainerOptions = @{}, [alias('Parallel')] [switch]$WorkflowParallel, [switch]$Plan, [switch]$Documentation ) if ($Global:ConfigurePreference) { # configuring is taking place in Parameters functions # when configuring workflow we don't execute workflow Write-Verbose 'Workflow not started because we are in Configure Mode' return; } if ($ConfirmPreference -eq 'low') { $yes = New-Object System.Management.Automation.Host.ChoiceDescription '&Yes' $no = New-Object System.Management.Automation.Host.ChoiceDescription '&No' $options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no) $choiceRTN = $host.UI.PromptForChoice('Confirm', "Are you sure you want to perform this workflow '$WorkflowName'", $options, 1) if ( $choiceRTN -eq 1 ) { Write-Info "Confirm: stopping workflow '$WorkflowName'" return } $ConfirmPreference = 'High' } if ($WorkflowContainer.IsPresent -and !$env:InScriptbookContainer) { # TODO !!EH supply all the parameters of the script caller? Start-ScriptInContainer -File $Script:MyInvocation.ScriptName -Options $WorkflowContainerOptions # -Parameters ([Hashtable]$Global:MyInvocation.MyCommand.WorkflowParameters) return; } $Global:LastExitCode = 0 $script:InvokedCommands = @() $script:InvokedCommandsResult = @() $workflowErrorAction = $ErrorActionPreference if ($WorkflowLocation) { $Script:WorkflowLocation = $WorkflowLocation } else { $Script:WorkflowLocation = Get-Location } $ctx = Get-RootContext $ctx.Verbose = $VerbosePreference -eq 'Continue' $ctx.NoLogging = $NoLogging.IsPresent $ctx.InTest = $TestWorkflow.IsPresent $isWhatIf = $WhatIfPreference if ($Plan.IsPresent -or $Documentation.IsPresent) { $WhatIfPreference = $true $isWhatIf = $WhatIfPreference } if ($ctx.Verbose) { Write-Verbose "Environment Info" Write-Verbose " Computer: $([System.Environment]::MachineName)" Write-Verbose " Os: $([System.Environment]::OSVersion.VersionString)" Write-Verbose " WhoAmI: $([System.Environment]::UserName)" Write-Verbose " Powershell: $($PSVersionTable.PsVersion)" Write-Verbose "CurrentFolder: $(Get-Location)" } $scriptName = $Script:MyInvocation.ScriptName if ([string]::IsNullOrEmpty($scriptName)) { $scriptName = Join-Path $PSScriptRoot 'Scriptbook.ps1' } $hasErrors = $false $workflowStopwatch = [System.Diagnostics.Stopwatch]::StartNew(); Write-ScriptLog @{action = "Workflow-Started"; param = $WorkflowActions; } -AsWorkflow $currentLocation = Get-Location try { if ($WorkflowTranscript.IsPresent -and !$isWhatIf) { Start-Transcript -Path "$scriptName.log" -Append -Force -IncludeInvocationHeader } try { if ($WorkflowFile -and (Test-Path $WorkflowFile) ) { . $WorkflowFile } try { if (Global:Invoke-BeforeWorkflow -Commands $WorkflowActions) { try { Invoke-SetupActions -Actions $ctx.ActionSequence -ThrowError $true -Test:$TestWorkflow.IsPresent -WhatIf:$isWhatIf -Parallel:$WorkflowParallel.IsPresent if ($null -ne $WorkflowActions -and ($WorkflowActions.count -gt 0) -and ($WorkflowActions[0] -ne '*')) { $expandedActions = Expand-WorkflowActions $WorkflowActions foreach ($action in $expandedActions) { if (!($action.StartsWith('!'))) { Invoke-PerformIfDefined -Command "Invoke-$($action.Replace('Invoke-', ''))" -ThrowError $true -ActionParameters $WorkflowParameters -NoDepends:$NoDepends.IsPresent -Test:$TestWorkflow.IsPresent -WhatIf:$isWhatIf } } } else { Invoke-ActionSequence -Actions $ctx.ActionSequence -ThrowError $true -Test:$TestWorkflow.IsPresent -WhatIf:$isWhatIf -Parallel:$WorkflowParallel.IsPresent } } finally { Invoke-AlwaysActions -Actions $ctx.ActionSequence -ThrowError $true -Test:$TestWorkflow.IsPresent -WhatIf:$isWhatIf -Parallel:$WorkflowParallel.IsPresent } } } finally { Global:Invoke-AfterWorkflow -Commands $WorkflowActions } } catch { if ($workflowErrorAction -eq 'Continue') { Write-ExceptionMessage $_ -TraceLineCnt 2 -ScriptBlocksOnly } elseif ($workflowErrorAction -notin 'Ignore', 'SilentlyContinue') { $hasErrors = $true Write-ExceptionMessage $_ -TraceLineCnt 15 -ScriptBlocksOnly Global:Write-OnLogException -Exception $_.Exception Global:Invoke-AfterWorkflow -Commands $WorkflowActions -ErrorRecord $_ | Out-Null Throw } } if ($Global:LastExitCode -ne 0) { Write-Warning "Workflow: Unsuspected LastExitCode found from native commands/executable: $($Global:LastExitCode), check logging." $Global:LastExitCode = 0; } } finally { Set-Location $currentLocation $workflowStopwatch.Stop() $ctx = Get-RootContext if (!$NoReport.IsPresent) { #TODO !!EH: Fix issue ansi escape sequences and Format-Table (invalid sizing) $script:InvokedCommandsResult | ForEach-Object { $hasErrors = if ($null -ne $_.Exception) { $true } else { $hasErrors }; <#if ($_.Exception) { $_.Name = "`e[37;41m$($_.Name)`e[0m" } else { $_.Name = "`e[00;00m$($_.Name)`e[0m" }; #> } if ($workflowErrorAction -in 'Ignore', 'SilentlyContinue') { $hasErrors = $false } if ($TestWorkflow.IsPresent) { $reportTitle = 'Workflow (Test) Report' } elseif ($Documentation.IsPresent) { $reportTitle = "Workflow Documentation" } elseif ($isWhatIf) { $reportTitle = "Workflow ($(if ($Plan.IsPresent) { 'Plan' } else { 'WhatIf' })) Report" } else { $reportTitle = 'Workflow Report' } Write-ScriptLog @{action = "Workflow-Finished"; } -AsWorkflow -AsError:$hasErrors if ($Documentation.IsPresent) { Write-Experimental "Workflow Documentation" } Write-Info ''.PadRight(78, '-') if ($hasErrors) { Write-Info "$reportTitle '$WorkflowName' with errors $((Get-Date).ToString('s'))" -ForegroundColor White -BackgroundColor Red } else { Write-Info "$reportTitle '$WorkflowName' $((Get-Date).ToString('s'))" } if ($Tag) { Write-Info $Tag } Write-Info ''.PadRight(78, '-') $script:InvokedCommandsResult | ForEach-Object { if ($_.Exception) { $_.Exception = "$(Get-AnsiColoredString -String $_.Exception.Message -Color 101)" } } $script:InvokedCommandsResult | ForEach-Object { $_.Name = ''.PadLeft(($_.Indent) + 1, '-') + $_.Name } $script:InvokedCommandsResult | ForEach-Object { if ($_.Skipped -or $_.WhatIf) { $_.Duration = 'Skipped' } } if ($Documentation.IsPresent) { if (Test-Path $scriptName) { $text = Get-CommentFromCode -File $scriptName -First 1 Write-Info ($text | Out-String) Write-Info ''.PadRight(78, '-') } $ctx = Get-RootContext if ($ctx.Infos.Count -gt 0) { foreach ($info in $ctx.Infos) { Write-Info ($info | Out-String) } Write-Info ''.PadRight(78, '-') } $script:InvokedCommandsResult | ForEach-Object { $item = [PSCustomObject]$_ Write-Info "Action $(Get-AnsiColoredString -String $item.Name -Color 36)" -ForegroundColor Magenta Write-Info ''.PadRight(78, '-') if (![string]::IsNullOrEmpty($item.Comment)) { Write-Info "$($item.Comment)" } else { Write-Info '<no documentation>' } Write-Info ''.PadRight(78, '-') } $script:InvokedCommandsResult += @{ Name = ''; Duration = '================'; } Write-Info '' Write-Info "Workflow Sequence" -ForegroundColor Magenta $script:InvokedCommandsResult += @{ Name = 'Total'; Duration = $workflowStopwatch.Elapsed; } $script:InvokedCommandsResult | ForEach-Object { [PSCustomObject]$_ } | Format-Table -AutoSize -Property Name, Duration | Out-String | Write-Info } else { $script:InvokedCommandsResult += @{ Name = ''; Duration = '================'; } $script:InvokedCommandsResult += @{ Name = 'Total'; Duration = $workflowStopwatch.Elapsed; } $script:InvokedCommandsResult | ForEach-Object { [PSCustomObject]$_ } | Format-Table -AutoSize Name, Duration, Exception, @{Label = 'Output' ; Expression = { $_.ReturnValue } } | Out-String | Write-Info } Write-Info ''.PadRight(78, '-') if ($TestWorkflow.IsPresent) { $tests = 0 $testsWithError = 0 $script:InvokedCommandsResult | ForEach-Object { if ($_.ContainsKey('TypeName') -and $_.TypeName -eq 'Test') { $tests++ if ($_.Exception) { $testsWithError++ } } } $testsSkipped = 0 $ctx.Actions.Values.GetEnumerator() | ForEach-Object { if ($_.TypeName -eq 'Test' -and $_.Disabled) { $testsSkipped++ } } $testsPassed = $tests - $testsSkipped - $testsWithError Write-Host "$(Get-AnsiColoredString -Color 32 -String "Tests Passed: $testsPassed" -NotSupported:($testsWithError -gt 0)), $(Get-AnsiColoredString -Color 101 -String "Failed: $testsWithError" -NotSupported:($testsWithError -eq 0)), $(Get-AnsiColoredString -Color 93 -String "Skipped: $testsSkipped" -NotSupported:($testsSkipped -eq 0))" if ($tests -gt 0) { Write-Host "$(Get-AnsiColoredString -Color 32 -String "Tests % Passed $( [int](100 - ($testsWithError / ($tests-$testsSkipped) ) * 100))%" -NotSupported:($testsWithError -gt 0)) of tests ($($tests-$testsSkipped))" } Write-Info ''.PadRight(78, '-') } } if ($ctx.Notifications.Count -gt 0) { foreach ($notification in $ctx.Notifications) { Write-Info ($notification | Out-String) } } if ($WorkflowTranscript.IsPresent -and !$isWhatIf) { Stop-Transcript } if ($Script:RootContext) { $Script:PreviousRunContext = $Script:RootContext.PSObject.Copy() } if (!($TestWorkflow.IsPresent)) { Reset-Workflow -WhatIf:$false } } } <# .SYNOPSIS Use workflow inline .DESCRIPTION Use workflow inline or alternative use alias Flow or Pipeline .REMARK See parameters of Start-Workflow .EXAMPLE Use-Workflow -Name Workflow1 { Action Hello { Write-Info "Hello from Workflow1 1" } Action GoodBy { Write-Info "GoodBy from Workflow1 1" } } #> Set-Alias -Name Flow -Value Use-Workflow -Scope Global -Force -WhatIf:$false Set-Alias -Name Pipeline -Value Use-Workflow -Scope Global -Force -WhatIf:$false function Use-Workflow { [CmdletBinding()] param( [Parameter(Mandatory = $true, Position = 0)] [ValidateNotNullOrEmpty()] [alias('Name')] [string]$WorkflowName, [array][alias('a', 'Action', 'Actions')]$WorkflowActions, [alias('Parameters')] $WorkflowParameters, [String[]] $Tag = @(), [alias('Location')] [string]$WorkflowLocation, [alias('File')] [string]$WorkflowFile, [switch]$NoReport, [switch]$NoLogging, [switch]$NoDepends, [alias('Test')] [switch]$TestWorkflow, [Parameter(Position = 1)] [alias('Code')] [ScriptBlock] $WorkflowCode ) $invokeErrorAction = $ErrorActionPreference if ($null -eq $WorkflowCode) { Throw "No workflow code script block is provided and Name property is mandatory. (Have you put the open curly brace on the next line?)" } try { & $WorkflowCode $WorkflowParameters } catch { if ($invokeErrorAction -eq 'Continue') { Write-ScriptLog $_.Exception.Message -AsError } elseif ($invokeErrorAction -notin 'Ignore', 'SilentlyContinue') { Throw } } # force clear context on start each workflow Start-Workflow -Actions $WorkflowActions -Parameters $WorkflowParameters -Name $WorkflowName -Tag $Tag -Location $WorkflowLocation -WorkflowFile $WorkflowFile -NoReport:$NoReport.IsPresent -NoLogging:$NoLogging.IsPresent -NoDepends:$NoDepends.IsPresent -Test:$TestWorkflow.IsPresent Reset-Workflow -WhatIf:$false } <# .SYNOPSIS Define Scriptbook Workflow variables .DESCRIPTION Define Scriptbook Workflow variables in PowerShell HashTable format and creates named PowerShell variable with HasTable as contents. .PARAMETER Name Name of created Variables PowerShell HashTable variable .PARAMETER Override Allow the current Variables to be over-written .PARAMETER Code HashTable with variables .REMARK .EXAMPLE Variables -Name Samples { @{ Variable1 = 'one' Variable2 = 'two' } } # Access variables in Scriptbook with Write-Host $Context.Samples.Variable1 Write-Host $Context.Samples.Variable2 or $ctx = Get-WorkflowContext Write-Host $ctx.Samples.Variable1 Write-Host $ctx.Samples.Variable2 # Access variables in Scriptbook Action with Write-Host $Context.Samples.Variable1 # or Write-Host $Global:Context.Samples.Variable2 # Update variables in Scriptbook Action with $Global:Context.Samples.Variable1 = 'newValue' #> function Variables { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "")] [CmdletBinding()] param( [Parameter(Mandatory = $true, Position = 0)][string]$Name, [switch]$Override, [Parameter(Position = 1)] [ScriptBlock]$Code ) if ($WhatIfPreference) { Write-Host "What if: Performing the operation 'Variables' on target '$Name'" } if ($Name -eq 'Variables') { Throw "Invalid Variables name found, '$Name' not allowed" } if ( !($Override.IsPresent) -and (Get-Variable -Name Context -ErrorAction Ignore -Scope Global) -and $Global:Context.ContainsKey($Name)) { Throw "Duplicate Variables name found, use Override to replace current Variables '$Name'" } if ($null -eq $Code) { Throw "No variables script block is provided with HashTable. (Have you put the open curly brace on the next line?)" } try { $value = (Invoke-Command $Code) if (!($value -is [HashTable])) { throw 'No HashTable found in Variables' } # create context if (!(Get-Variable -Name Context -ErrorAction Ignore -Scope Global)) { Set-Variable -Name Context -Value @{ } -Scope Global -WhatIf:$false } $Global:Context."$Name" = $value Set-Variable -Name $Name -Value $value -Scope Global -WhatIf:$false } catch { Write-Warning "Error setting '$Name' variable to '$Code' $($_.Exception.Message)" Write-Warning "Only HashTable @{ Name = 'default'; Name2 = 'default2'} is allowed" Throw } } <# .SYNOPSIS Decrypts secret with Seed value .DESCRIPTION Decrypts secret with Seed value. Seed complexity is - At least one upper case letter [A-Z] - At least one lower case letter [a-z] - At least one number [0-9] - At least one special character (!,@,%,^,&,$,_) - Password length must be 7 to 25 characters. .PARAMETER Value Value to decrypt .PARAMETER Seed Seed value used to decrypt value #> function Get-DecryptedSecret { [CmdletBinding()] param( [ValidateNotNullOrEmpty()] [alias('v')] $Value, [ValidateNotNullOrEmpty()] [ValidateLength(8, 1024)] [ValidatePattern('^((?=.*[a-z])(?=.*[A-Z])(?=.*\d)|(?=.*[a-z])(?=.*[A-Z])(?=.*[^A-Za-z0-9])|(?=.*[a-z])(?=.*\d)(?=.*[^A-Za-z0-9])|(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9]))([A-Za-z\d@#$%^&£*\-_+=[\]{}|\\:();!]|\.(?!@)){8,16}$')] [alias('s', 'k', 'Key')] $Seed ) $hash = (New-Object System.Security.Cryptography.SHA256CryptoServiceProvider).ComputeHash(([system.Text.Encoding]::Unicode).GetBytes($Seed)); $iv = New-Object byte[] 16; $key = New-Object byte[] 16; [System.Buffer]::BlockCopy($hash, 0, $key, 0, $key.Length) $decryptor = ([System.Security.Cryptography.AesCryptoServiceProvider]::Create()).CreateDecryptor($key, $iv) $buffer = [System.Convert]::FromBase64String($Value); $decryptedBlob = $deCryptor.TransformFinalBlock($buffer, 0, $buffer.Length); return [System.Text.Encoding]::Unicode.GetString($decryptedBlob) } <# .SYNOPSIS Encrypts secret with Seed value .DESCRIPTION Encrypts secret with Seed value. Seed complexity is - At least one upper case letter [A-Z] - At least one lower case letter [a-z] - At least one number [0-9] - At least one special character (!,@,%,^,&,$,_) - Password length must be 7 to 25 characters. .PARAMETER Value Value to encrypts .PARAMETER Seed Seed value used to encrypts value #> function Get-EncryptedSecret { [CmdletBinding()] param( [ValidateNotNullOrEmpty()] [alias('v')] $Value, [ValidateNotNullOrEmpty()] [ValidateLength(8, 1024)] [ValidatePattern('^((?=.*[a-z])(?=.*[A-Z])(?=.*\d)|(?=.*[a-z])(?=.*[A-Z])(?=.*[^A-Za-z0-9])|(?=.*[a-z])(?=.*\d)(?=.*[^A-Za-z0-9])|(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9]))([A-Za-z\d@#$%^&£*\-_+=[\]{}|\\:();!]|\.(?!@)){8,16}$')] [alias('s', 'k', 'Key')] $Seed ) $hash = (New-Object System.Security.Cryptography.SHA256CryptoServiceProvider).ComputeHash(([system.Text.Encoding]::Unicode).GetBytes($Seed)); $iv = New-Object byte[] 16; $key = New-Object byte[] 16; [System.Buffer]::BlockCopy($hash, 0, $key, 0, $key.Length) $encryptor = ([System.Security.Cryptography.AesCryptoServiceProvider]::Create()).CreateEncryptor($key, $iv) $buffer = [System.Text.Encoding]::Unicode.GetBytes($Value); $encryptedBlob = $encryptor.TransformFinalBlock($buffer, 0, $buffer.Length); return [System.Convert]::ToBase64String($encryptedBlob) } <# .SYNOPSIS Get Credential from Local Credential cache in User profile on Windows. .DESCRIPTION Get Credential from Local Credential cache in User profile on Windows if found. Otherwise in interactive sessions Get-Credential is used to query for Credentials and store them in local cache encrypted. .PARAMETER Name Name of credential for reference only #> function Get-LocalCredential([Parameter(Mandatory = $true)][string]$Name) { #TODO !!EH Windows Only or by design? $credPath = Join-Path $home "Cred_$Name.xml" if ( Test-Path $credPath ) { $cred = Import-Clixml -Path $credPath } else { # not fail safe but better than nothing if (Get-IsPowerShellStartedInNonInteractiveMode) { Throw "Get-LocalCredential not working when running script in -NonInteractive Mode, unable to prompt for Credentials" } $parent = Split-Path $credPath -Parent if ( -not ( Test-Path $parent ) ) { New-Item -ItemType Directory -Force -Path $parent | Out-Null } $cred = Get-Credential -Title "Provide '$Name' username/password" $cred | Export-Clixml -Path $credPath } return $cred } <# .SYNOPSIS Asserts/Checks the supplied boolean condition .DESCRIPTION Asserts/Checks the supplied boolean condition. Throws an exception with message details if fails .PARAMETER Condition Boolean value of condition to check .PARAMETER Value Actual value to check .PARAMETER Operator Check comparison operator like: -eq, -ne, -gt .PARAMETER Expected Expected value for check .PARAMETER Message The message to display when assert fails .EXAMPLE Assert-Condition -Condition $false 'Error checking this condition' .EXAMPLE Assert -c (Test-Path $myFile) 'File not found' .EXAMPLE $cnt = 5 Assert-Condition ($cnt -eq 5) 'Error checking this condition' .EXAMPLE $cnt = 5 Assert-Condition ($cnt -eq 5) 'Error checking this condition' .EXAMPLE $cnt = 5 Assert-Condition -Value $cnt -Expected 5 'Error checking this condition' .EXAMPLE $cnt = 4 Assert-Condition -Value $cnt -Operator '-ne' -Expected 5 'Error checking this condition' #> Set-Alias -Name Assert -Value Assert-Condition -Scope Global -Force -WhatIf:$false function Assert-Condition { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingInvokeExpression", "")] [CmdletBinding(DefaultParameterSetName = 'Condition', SupportsShouldProcess)] param( [bool][Alias('c')][Parameter(Position = 0, Mandatory = $true, ParameterSetName = 'Condition')]$Condition, [Alias('v', 'Actual', 'Real')][Parameter(Mandatory = $true, ParameterSetName = 'Comparison')]$Value, [Alias('o')][Parameter(ParameterSetName = 'Comparison')]$Operator = '-eq', [Alias('e', 'v2', 'Value2')][Parameter(Mandatory = $true, ParameterSetName = 'Comparison')]$Expected = '-eq', [Alias('m')] [Parameter(Mandatory = $true, Position = 1, ParameterSetName = 'Condition')] [Parameter(Mandatory = $true, ParameterSetName = 'Comparison')] [string]$Message ) if ($PSCmdlet.ParameterSetName -eq 'Condition') { $expMsg = "Check: $Condition" } else { $expMsg = "Check: ($Value $Operator $Expected)" } if ($PSCmdlet.ShouldProcess("Assert-Condition", $expMsg)) { if ($PSCmdlet.ParameterSetName -eq 'Condition') { if (-not $Condition) { Write-Verbose "Assert-Condition: $Message" Throw "Assert-Condition: $Message" } } else { if ($Value -is [string]) { $check = Invoke-Expression "'$Value' $Operator '$Expected'" } else { $check = Invoke-Expression "$Value $Operator $Expected" } if (-not $check) { Write-Verbose "Assert-Condition expected '$Expected' actual '$Value' with operation '$Operator' $Message" Throw "Assert-Condition: Expected '$Expected' Actual '$Value' with Operation '$Operator' $Message" } } } } <# .SYNOPSIS Checks the minimum required version of command .DESCRIPTION Checks the minimum required version of command. Command includes Powershell commands and native commands. Select -Minimum to allow higher major versions. Default higher minimum versions are allowed. For example Version is 4.1 then 4.1-4.9 are valid versions. remarks. Version/Minimum check only works on Windows. .PARAMETER Command The command .PARAMETER Version Version of command required. Higher minor versions are allowed .PARAMETER Minimum Minimum version of command required. Higher Major version are allowed .EXAMPLE Assert-Version -Command cmd -Version 1.0 #> function Assert-Version([Parameter(Mandatory = $true)][string]$Command, [Parameter(Mandatory = $true)][string]$Version, [switch]$Minimum) { if (!$IsWindows) { $cmdNative = $Command.Replace('.exe', '') } else { $cmdNative = $Command } $cm = Get-Command $cmdNative -ErrorAction Ignore if ($cm) { if ($IsWindows) { $v = [Version]$Version if ($Minimum.IsPresent) { if ($cm.Version.Major -ge $v.Major) { return } } else { if ($cm.Version.Major -eq $v.Major) { if ($cm.Version.minor -ge $v.Minor) { # okay return } } } Throw "Invalid version of $Command found '$($cm.Version)' expected $v" } else { Write-Verbose "Assert-Version: No version/Minimum check on platforms other than Windows" } } else { Throw "$Command not installed" } } <# .SYNOPSIS Get Invocation Bound parameters with default values. .DESCRIPTION Get Invocation Bound parameters with default values. $PSBoundParameters does not contain default values .PARAMETER Invocation Contains the $MyInVocation of the script / function .EXAMPLE Script with : [CmdletBinding(SupportsShouldProcess = $True)] Param( $Param1, $Param2, ) $parameters = Get-BoundParametersWithDefaultValue $MyInvocation Now call script with @parameters . ./myScript @parameters or function with @parameters myFunction @parameters #> function Get-BoundParametersWithDefaultValue { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingEmptyCatchBlock", "")] [OutputType([HashTable])] param( $Invocation = $Global:MyInvocation ) $parameters = @{} foreach ($parameter in $Invocation.MyCommand.Parameters.GetEnumerator()) { try { $key = $parameter.Key $val = Get-Variable -Name $key -ErrorAction Stop | Select-Object -ExpandProperty Value -ErrorAction Stop [void]$parameters.Add($key, $val) } catch {} } return $parameters } <# .SYNOPSIS Gets an environment variable .DESCRIPTION Gets an environment variable, supports empty environment variable and case sensitivity .PARAMETER Name Name of the environment variable .PARAMETER Default Default value of the environment variable when not found .PARAMETER IgnoreCasing Ignores casing by checking ToLower and ToUpper variants #> function Get-EnvironmentVar { [OutputType([string])] param([alias('n')][string]$Name, [alias('d')][string]$Default = $null, [switch]$IgnoreCasing) $r = [Environment]::GetEnvironmentVariable($Name); if ($null -eq $r -and $IgnoreCasing.IsPresent) { $r = [Environment]::GetEnvironmentVariable($Name.ToLower()); if ($null -eq $r) { $r = [Environment]::GetEnvironmentVariable($Name.ToUpper()); } } if ($r -eq [char]0x2422) { $r = '' } if (($r -eq '') -or ($null -eq $r)) { $r = $Default } if ($r -eq '') { return $null } else { return $r } } <# .SYNOPSIS Returns if PowerShell console (pwsh) is started in Non Interactive mode .DESCRIPTION Returns if PowerShell console (pwsh) is started in Non Interactive mode #> function Get-IsPowerShellStartedInNonInteractiveMode { if ( ((Get-Host).Name -eq 'ConsoleHost') -and ([bool]([Environment]::GetCommandLineArgs() -like '-noni*')) ) { return $true } else { return $false } } <# .SYNOPSIS Gets property value from Object .DESCRIPTION Gets property value from Object, first checks if property exists, if not returns default value. In Set-StrictMode -Latest every property used is checked for existence --> runtime exception .PARAMETER Object Object to get property value from .PARAMETER Name Name of property .PARAMETER Default Default value if property does not exists #> function Get-PSPropertyValue { param([alias('o')][object]$Object, [alias('p')][string]$Name, [alias('d')]$Default = '') if (Test-PSProperty -o $Object -p $Name -Exact) { return $Object."$Name" } else { return $Default } } function New-SecureStringStorage { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] param([ValidateNotNullOrEmpty()]$String) return [SecureStringStorage]::New($String) } [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] class SecureStringStorage { hidden [String] $String [String] $TypeName = 'SecureStringStorage' SecureStringStorage($String) { if (($String -is [PSCustomObject]) -and ($String.TypeName -eq 'SecureStringStorage') ) { $this.String = $String.String } elseif (($String -is [SecureString])) { $this.String = $String | ConvertFrom-SecureString } else { $this.String = ConvertTo-SecureString -String $String -AsPlainText -Force | ConvertFrom-SecureString } } [string]ToString() { return $this.String } [SecureString]GetSecureString() { $secureString = ConvertTo-SecureString -String $this.String -Force return $secureString } [string]GetPlainString() { $plain = ConvertTo-SecureString -String $this.String -Force | ConvertFrom-SecureString -AsPlainText return $plain } } <# .SYNOPSIS Sets an environment variable .DESCRIPTION Sets an environment variable, supports empty environment variable .PARAMETER Name Name of the environment variable .PARAMETER Value Value of the environment variable .PARAMETER ToUpper Forces environment variable name to UpperCase #> function Set-EnvironmentVar { [CmdletBinding(SupportsShouldProcess = $True)] [OutputType([System.Void])] param( [alias('n')][string] $Name, [alias('v')][string] $Value, [alias('g')][switch]$Global, [switch] $ToUpper ) if ($Name) { $n = if ($ToUpper.IsPresent) { $Name.ToUpper() } else { $Name } if (!$Value) { $v = [char]0x2422 } else { $v = $Value } $g = if ($Global.IsPresent) { 'Machine' } else { 'Process' } if ($PSCmdlet.ShouldProcess('Set-EnvironmentVar')) { [Environment]::SetEnvironmentVariable($n, $v, $g ) } } } <# .SYNOPSIS Starts a shell command and waits for it to finish .DESCRIPTION Starts a shell command with arguments in working directory and waits for it to finish. Optionally supply credential under which execution will take place. .PARAMETER Command Name of executable to start .PARAMETER Arguments Arguments to pass to executable .PARAMETER Credential Credential to start process with .PARAMETER WorkingDirectory Working directory of executable .PARAMETER NoOutput Suppress output of executable .PARAMETER EnvVars Additionally supply environment variables to process .OUTPUTS Returns Process StdOut, StdErr and ExitCode .EXAMPLE Start-ShellCmd -Command 'cmd.exe' -Arguments '/c' .EXAMPLE $r = Start-ShellCmd -Command 'pwsh' -Arguments '-NonInteractive -NoLogo -OutputFormat Text -ExecutionPolicy Bypass -Command Get-Process' if ($r.ExitCode -ne 0) { throw "Invalid ExitCode returned from pwsh.exe : $($r.ExitCode)"} .EXAMPLE Start-ShellCmd -c 'pwsh' -a '-Command Get-Service' | Out-Null #> function Start-ShellCmd { [CmdletBinding(SupportsShouldProcess)] [OutputType([PSCustomObject])] param( [alias('c')] $Command, [alias('a')] $Arguments, [PSCredential] $Credential, [alias('w')] $WorkingDirectory = '', [alias('no')][switch] $NoOutput, $EnvVars, [switch] $Progress ) try { $pInfo = New-Object System.Diagnostics.ProcessStartInfo $pInfo.FileName = $Command $pInfo.RedirectStandardError = $true $pInfo.RedirectStandardOutput = $true $pInfo.UseShellExecute = $false $pInfo.Arguments = $Arguments $pInfo.ErrorDialog = $false if ((!$WorkingDirectory) -or ($WorkingDirectory -eq '')) { $pInfo.WorkingDirectory = Get-Location } else { $pInfo.WorkingDirectory = $WorkingDirectory } if ($env:SystemRoot) { $pInfo.LoadUserProfile = $true } if ($Credential) { $pInfo.UserName = $Credential.GetNetworkCredential().UserName if ($Credential.GetNetworkCredential().Domain) { $pInfo.Domain = $Credential.GetNetworkCredential().Domain } $pInfo.Password = $Credential.GetNetworkCredential().SecurePassword } if ($EnvVars) { foreach ($v in $EnvVars.GetEnumerator()) { if ($v.Key) { $pInfo.EnvironmentVariables[$v.Key] = $v.Value; } } } if ($PSCmdlet.ShouldProcess('Start-ShellCmd')) { $p = New-Object System.Diagnostics.Process $p.StartInfo = $pInfo if ($Progress.IsPresent) { $stdOutBuilder = New-Object -TypeName System.Text.StringBuilder $stdErrBuilder = New-Object -TypeName System.Text.StringBuilder $eventHandler = ` { if (![String]::IsNullOrEmpty($EventArgs.Data)) { $Event.MessageData.Builder.AppendLine($EventArgs.Data) if ($Event.MessageData.ShowOutput) { Write-Host $EventArgs.Data } } } $mdo = [PSCustomObject]@{ ShowOutput = !($NoOutput.IsPresent) Builder = $stdOutBuilder } $mde = [PSCustomObject]@{ ShowOutput = !($NoOutput.IsPresent) Builder = $stdErrBuilder } $stdOutEvent = Register-ObjectEvent -InputObject $p -Action $eventHandler -EventName 'OutputDataReceived' -MessageData $mdo $stdErrEvent = Register-ObjectEvent -InputObject $p -Action $eventHandler -EventName 'ErrorDataReceived' -MessageData $mde $p.Start() | Out-Null $handle = $p.Handle # cache handle to prevent $null ExitCode issue $p.BeginOutputReadLine() $p.BeginErrorReadLine() While (-not ($p.HasExited)) { $p.Refresh() } Unregister-Event -SourceIdentifier $stdOutEvent.Name; $stdOutEvent = $null; Unregister-Event -SourceIdentifier $stdErrEvent.Name; $stdErrEvent = $null; $so = $stdOutBuilder.ToString().TrimEnd("`r", "`n"); $se = $stdErrBuilder.ToString() } else { $p.Start() | Out-Null $handle = $p.Handle # cache handle to prevent $null ExitCode issue $so = $p.StandardOutput.ReadToEnd() $se = $p.StandardError.ReadToEnd() $p.WaitForExit() if (!($NoOutput.IsPresent)) { Write-Info "$so $se" } } Write-Debug "Start-ShellCmd: Process Handle: $handle" [PSCustomObject]@{ StdOut = $so StdErr = $se ExitCode = $p.ExitCode } $handle = $null } } catch { if ($_.Exception.Message.Contains('The stub received bad data')) { Throw "No domain name specified, add domain name to user '$($pInfo.UserName)' : $($_.Exception.Message)" } else { Throw } } } function Test-IsSecureStringStorageObject([ValidateNotNull()]$Object) { return ($Object -is [SecureStringStorage]) } <# .SYNOPSIS Checks if property exists on Object .DESCRIPTION Checks if property exists on Object. In Set-StrictMode -Latest every property used is checked for existence --> runtime exception .PARAMETER Object Object to test for property .PARAMETER Name Name of property .PARAMETER Exact Use exact match in property name checking #> function Test-PSProperty { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingEmptyCatchBlock", "")] [OutputType([boolean])] param([alias('o')][object]$Object, [alias('p')][string]$Name, [alias('e')][switch]$Exact) try { foreach ($prop in $Object.PSObject.Properties) { if ($Exact.IsPresent) { if ($prop.Name -eq $Name) { return $true } elseif ($prop.Name -match 'Keys') { if ($prop.Value -eq $Name) { return $true } } } else { if ($prop.Name -match $Name) { return $true } elseif ($prop.Name -match 'Keys') { if ($prop.Value -match $Name) { return $true } } } } } catch { # not found } return $false } <# .SYNOPSIS Writes Info to console host. .DESCRIPTION Writes Info to console host. .PARAMETER Object Objects to display in the host. .PARAMETER NoNewline The string representations of the input objects are concatenated to form the output. No spaces or newlines are inserted between the output strings. No newline is added after the last output string. .PARAMETER Separator Specifies a separator string to insert between objects displayed by the host. .PARAMETER ForegroundColor Specifies the text color. .PARAMETER BackgroundColor Specifies the background color. #> function Write-Info { [CmdletBinding()] [OutputType([System.Void])] param( [parameter(ValueFromPipeline = $True)] $Object, [switch]$NoNewline, $Separator, $ForegroundColor, $BackgroundColor ) Begin { $parameters = @{ Object = $null } if ($NoNewline.IsPresent) { $parameters.Add('NoNewLine', $true) } if ($Separator) { $parameters.Add('Separator', $Separator) } # TODO !!EH Remap to ansi escape codes? if ($ForegroundColor) { $parameters.Add('ForegroundColor', $ForegroundColor) } if ($BackgroundColor) { $parameters.Add('BackgroundColor', $BackgroundColor) } } Process { $parameters.Object = $Object Write-Host @parameters } End { [Console]::ResetColor() } } function Expand-WorkflowActions { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] param($Actions) $ctx = Get-RootContext $expandedActions = [System.Collections.ArrayList]@() foreach ($action in $Actions) { if ($action.Contains('*')) { foreach ($item in $ctx.Actions.GetEnumerator()) { $n = $item.Value.DisplayName if ($n -like $action) { if (!$expandedActions.Contains($n) -and !$expandedActions.Contains("!$n")) { [void]$expandedActions.Add($n) } } } } else { [void]$expandedActions.Add($action) } } return $expandedActions } function Get-AnsiColoredString { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "")] param([string]$String, [ValidateNotNull()][int]$Color, [switch]$NotSupported) # ref: https://en.wikipedia.org/wiki/ANSI_escape_code for color codes # ref: https://duffney.io/usingansiescapesequencespowershell/ if ($Global:ScriptbookSimpleHost -or $NotSupported.IsPresent) { return $String } else { return "`e[0;$($Color)m$($String)`e[0m" } } <# .SYNOPSIS Returns if defined Powershell function has no body, aka is empty #> function Get-IsPSFunctionDefinitionEmpty { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingEmptyCatchBlock", "")] param([alias('Function')]$aFunction) try { $c = Get-Command $aFunction if ($c) { $d = $c.Definition if ($d) { return $d.Trim().Length -eq 0 } else { return $true } } } catch { # ignore } return $false } function Get-RootContext { return $Script:RootContext } function Invoke-ActionSequence { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "")] [CmdletBinding(SupportsShouldProcess = $true)] param ( $Actions, $ThrowError = $false, [switch]$Test, [switch]$Parallel ) if ($null -eq $Actions -or ($Actions.Count -eq 0)) { Write-Verbose "No actions/steps found to execute" return } if ($Parallel.IsPresent -and $PSVersionTable.PSVersion.Major -ge 7) { $hasDepends = $false foreach ($action in $Actions) { if ($action.Depends.Count -gt 0) { $hasDepends = $true } } Write-Experimental "Starting workflow in parallel mode" #TODO !!EH disabled for now, not working yet $hasDepends = $true if (!$HasDepends) { $module = Get-Module Scriptbook if ($module) { $mp = $module.Path } else { throw "Module scriptbook loaded more than one time. Only Import Scriptbook Module once." } $globalScriptVariables = Get-GlobalVarsForScriptblock -AsHashTable $rc = Get-RootContext $Actions | Where-Object { $_.NoSequence -eq $false } | ForEach-Object -Parallel { $vars = $using:globalScriptVariables foreach ($v in $vars.GetEnumerator()) { Set-Variable $v.Key -Value $v.Value -Option ReadOnly -ErrorAction Ignore } Import-Module $using:mp -Args @{ Quiet = $true } $script:RootContext = $using:rc Invoke-PerformIfDefined -Command $_.Name -ThrowError $ThrowError -Test:$Test.IsPresent -WhatIf:$WhatIfPreference } return } } # sequential foreach ($action in $Actions) { if (!$action.NoSequence) { Invoke-PerformIfDefined -Command $action.Name -ThrowError $ThrowError -Test:$Test.IsPresent -WhatIf:$WhatIfPreference } } } function Invoke-AlwaysActions { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] [CmdletBinding(SupportsShouldProcess = $true)] param ( $Actions, $ThrowError = $false, [switch]$Test, [switch]$Parallel ) if ($null -eq $Actions -or ($Actions.Count -eq 0)) { Write-Verbose "No actions/steps found to execute" return } foreach ($action in $Actions) { if ($action.Always -and ($action.Name -notin $script:InvokedCommands) ) { Invoke-PerformIfDefined -Command $action.Name -ThrowError $ThrowError -Test:$Test.IsPresent -WhatIf:$WhatIfPreference } } } <# .SYNOPSIS Performs a command/action #> function Invoke-Perform { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "")] [CmdletBinding(SupportsShouldProcess = $True)] param ( [string][alias('c')]$Command, $Code = $null, [alias('d')]$Depends = $null, [alias('Parameters')]$ActionParameters, [bool]$NoReEntry, [bool][alias('AsJob')]$aAsJob, $If, [switch]$NoDepends, $NextAction, $For, [switch]$Parallel, [switch]$Container, [HashTable]$ContainerOptions, $Session, [switch]$Isolated, [switch]$Manual, [string]$TypeName, [string[]]$RequiredVariables, $Comment, [switch]$SuppressOutput, [switch]$ConfirmAction, [switch]$WhatIfAction, [switch]$Multiple ) $invokeErrorAction = $ErrorActionPreference # only way to set Error preference in Scriptblock $Global:ErrorActionPreference = $invokeErrorAction $dependencies = $Depends # simple depth first dependencies with attribute on functions if (-not $NoDepends.IsPresent) { if (!$Code) { if ($stepCommand = Get-Command -Name $Command -CommandType Function) { $d = $stepCommand.ScriptBlock.Attributes.Where{ $_.TypeId.Name -eq 'DependsOn' } if ($d) { $dependencies = $d.Name } } } # simple depth first dependencies foreach ($dependency in $dependencies) { if (!$dependency.StartsWith('Invoke-')) { $dependency = "Invoke-$dependency" } if ($dependency -NotIn $script:InvokedCommands) { if ($Command -notin $script:InvokedCommands) { Invoke-PerformIfDefined -Command $dependency -ThrowError $true -Manual:$Manual.IsPresent $Script:RootContext.IndentLevel += 1 } } } } # Check re-entry if (!($NoReEntry)) { if ($Command -in $script:InvokedCommands) { return; } } $cmdDisplayName = $Command.Replace('Action-', '').Replace('Invoke-', '') $skipped = $false # Re-apply PSake properties and Parameters in Scope try { if ((Test-Path variable:Script:PSakeProperties) -and $Script:PSakeProperties) { . $Script:PSakeProperties } if ((Test-Path variable:Script:PSakeInvocationParameters) -and $Script:PSakeInvocationParameters) { $Script:PSakeInvocationParameters.Keys | ForEach-Object { Set-Variable -Name $_ -Value $Script:PSakeInvocationParameters[$_] } $Script:PSakeInvocationParameters.Keys | ForEach-Object { Set-Variable -Name $_ -Value $Script:PSakeInvocationParameters[$_] -Scope Script } } if ((Test-Path variable:Script:PSakeInvocationProperties) -and $Script:PSakeInvocationProperties) { $Script:PSakeInvocationProperties.Keys | ForEach-Object { Set-Variable -Name $_ -Value $Script:PSakeInvocationProperties[$_] } $Script:PSakeInvocationProperties.Keys | ForEach-Object { Set-Variable -Name $_ -Value $Script:PSakeInvocationProperties[$_] -Scope Script } } } catch { Write-Warning "Issues in PSake properties and/or parameters" throw } # validate required variables if ($RequiredVariables -and $RequiredVariables.Count -gt 0) { $varsNotFound = [System.Collections.ArrayList]@() foreach ($var in $RequiredVariables) { If (!((Test-Path "variable:$var") -and ($null -ne (Get-Variable -Name $var)))) { [void]$varsNotFound.Add($var) } } if ($varsNotFound.Count -gt 0) { Throw "Required variable(s) '$($varsNotFound -join ',')' not found in $cmdDisplayName" } $varsNotFound = $null } $indentSpaces = ''.PadRight(($Script:RootContext.IndentLevel + 2) , ' ') # check start condition if ($If) { $ifResult = & $If if (!$ifResult) { $skipped = $true Write-ScriptLog @{action = "$($indentSpaces)$($TypeName): $cmdDisplayName"; time = $(Get-Date -Format s); } -AsAction Write-Info 'Skipped:' $script:InvokedCommandsResult += @{ Name = "$cmdDisplayName" Duration = 0 Indent = $Script:RootContext.IndentLevel Exception = $null HasError = $false ReturnValue = $null Command = $Command TypeName = $TypeName Comment = $Comment Confirm = $ConfirmAction WhatIf = $WhatIfPreference Skipped = $Skipped } return; } } $commandStopwatch = [System.Diagnostics.Stopwatch]::StartNew(); Write-ScriptLog @{action = "$($indentSpaces)$($TypeName): $(Get-AnsiColoredString -String $cmdDisplayName -Color 36 )"; time = $(Get-Date -Format s); } -AsAction Write-ScriptLog @{action = "$cmdDisplayName-Started"; time = $(Get-Date -Format s); } -AsAction -Verbose # check if we have something to execute if (!$Code) { if (!(Get-Item function:$Command -ErrorAction SilentlyContinue)) { Throw "Required function '$Command' not found in script" } } $ex = $null $hasError = $false # determines if exception has occurred but not how exception is handled. When ErrorPreference is 'Continue' exception is null but hasError is true $codeReturn = $null Push-Location $Script:WorkflowLocation $prevInAction = $Script:RootContext.InAction $prevNestedActions = $Script:RootContext.NestedActions $prevWhatIfPreference = $WhatIfPreference $Script:RootContext.InAction = $true $Script:RootContext.NestedActions = New-Object -TypeName 'System.Collections.ArrayList' try { if ($WhatIfAction.IsPresent) { $WhatIfPreference = $true } if (!$WhatIfPreference) { if ($ConfirmAction.IsPresent) { $yes = New-Object System.Management.Automation.Host.ChoiceDescription '&Yes' $no = New-Object System.Management.Automation.Host.ChoiceDescription '&No' $options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no) $choiceRTN = $host.UI.PromptForChoice('Confirm', "Are you sure you want to perform this action '$cmdDisplayName'", $options, 1) if ( $choiceRTN -eq 1 ) { $skipped = $true Write-Info "Confirm: Skipping action '$cmdDisplayName'" return } } } if (!$WhatIfPreference) { if ((Test-Path variable:Script:PSakeSetupTask) -and $Script:PSakeSetupTask) { & $Script:PSakeSetupTask $ActionParameters } } # check function without code if (!$Code -and (Get-IsPSFunctionDefinitionEmpty $Command)) { $skipped = $true; return; } if (!($Multiple.IsPresent)) { $script:InvokedCommands += $Command } if (!$WhatIfPreference) { $beforePerform = Global:Invoke-BeforePerform -Command $Command } else { $beforePerform = $true } if ($beforePerform) { # TODO: EH!! Refactor into multiple functions/code block # execute the code or function if ($Code) { $mp = $null if (!$Isolated.IsPresent) { $module = Get-Module Scriptbook if ($module) { $mp = $module.Path } else { throw "Module scriptbook loaded more than once. Only Import Scriptbook Module one time." } } if ($For) { $forResult = & $For if ($Parallel.IsPresent -and $PSVersionTable.PSVersion.Major -ge 7) { $codeReturn = @() $globalScriptVariables = Get-GlobalVarsForScriptblock -Isolated:$Isolated.IsPresent -AsHashTable $codeAsString = $Code.ToString() # no scriptblock allowed in parallel ForEach :) # $using:* is by ref with RunSpaces(parallel) but copy of var in Remoting # $using:* needs to be thread-safe for parallel if ($PSCmdlet.ShouldProcess($cmdDisplayName, "Invoke")) { $forResult | ForEach-Object -Parallel { $vars = $using:globalScriptVariables foreach ($v in $vars.GetEnumerator()) { Set-Variable $v.Key -Value $v.Value -WhatIf:$False -Confirm:$false } if ($using:mp) { Import-Module $using:mp -Args @{ Quiet = $true } } $Parameters = $using:ActionParameters $Parameters = $Parameters.Clone() $Parameters.ForItem = $_ $Parameters.ForParallel = $true # use local vars Set-Variable ForItem -Value $_ -WhatIf:$False -Confirm:$false -Option Constant Set-Variable ForParallel -Value $true -WhatIf:$False -Confirm:$false -Option Constant Set-Variable Tag -Value $Parameters.Tag -WhatIf:$False -Confirm:$false -Option Constant -ErrorAction Ignore Set-Variable Name -Value $Parameters.Name -WhatIf:$False -Confirm:$false Set-Variable ActionName -Value $Parameters.ActionName -WhatIf:$False -Confirm:$false -Option ReadOnly foreach ($v in $Parameters.Parameters.GetEnumerator()) { Set-Variable $v.Key -Value $v.Value -WhatIf:$False -Confirm:$false -Option Constant -ErrorAction Ignore } $code = [Scriptblock]::Create($using:codeAsString) try { & $code $Parameters } catch { $hasError = $true if ($using:invokeErrorAction -eq 'Continue') { Write-Host $_.Exception.Message -ForegroundColor White -BackgroundColor Red $ex = $_.Exception } elseif ($using:invokeErrorAction -notin 'Ignore', 'SilentlyContinue') { Throw } } } | ForEach-Object { $codeReturn += $_ } # TODO !!EH Pick-up if Exception --> hasError } } elseif ($aAsJob) { $globalScriptVariables = Get-GlobalVarsForScriptblock -Isolated:$Isolated.IsPresent -AsHashTable $codeAsString = $Code.ToString() # $using:* is by ref with RunSpaces(parallel) but copy of var in Remoting # $using:* needs to be thread-safe for parallel if ($PSCmdlet.ShouldProcess($cmdDisplayName, "Invoke")) { $forResult | ForEach-Object { $item = $_; Start-Job -ScriptBlock { $vars = $using:globalScriptVariables foreach ($v in $vars.GetEnumerator()) { Set-Variable $v.Key -Value $v.Value -WhatIf:$False -Confirm:$false } if ($using:mp) { Import-Module $using:mp -Args @{ Quiet = $true } } $Parameters = $using:ActionParameters $Parameters = $Parameters.Clone() $Parameters.ForItem = $using:item $Parameters.AsJob = $true # use local vars Set-Variable ForItem -Value $Parameters.ForItem -WhatIf:$False -Confirm:$false -Option Constant Set-Variable _ -Value $Parameters.ForItem -WhatIf:$False -Confirm:$false Set-Variable AsJob -Value $true -WhatIf:$False -Confirm:$false -Option Constant -ErrorAction Ignore Set-Variable Tag -Value $Parameters.Tag -WhatIf:$False -Confirm:$false -Option Constant -ErrorAction Ignore Set-Variable Name -Value $Parameters.Name -WhatIf:$False -Confirm:$false -Option Constant -ErrorAction Ignore Set-Variable ActionName -Value $Parameters.ActionName -WhatIf:$False -Confirm:$false -Option ReadOnly foreach ($v in $Parameters.Parameters.GetEnumerator()) { Set-Variable $v.Key -Value $v.Value -WhatIf:$False -Confirm:$false -Option Constant -ErrorAction Ignore } $code = [Scriptblock]::Create($using:codeAsString) try { & $code $Parameters } catch { $hasError = $true if ($using:invokeErrorAction -eq 'Continue') { Write-Host $_.Exception.Message -ForegroundColor White -BackgroundColor Red $ex = $_.Exception } elseif ($using:invokeErrorAction -notin 'Ignore', 'SilentlyContinue') { Throw } } } } | Out-Null Get-Job | Wait-Job | Out-Null $codeReturn = Get-Job | Receive-Job # TODO !!EH Pick-up if Exception --> hasError Get-Job | Remove-Job | Out-Null } } else { $r = @() Set-Variable AsJob -Value $true -Scope Global -WhatIf:$False -Confirm:$false Set-Variable Tag -Value $ActionParameters.Tag -Scope Global -WhatIf:$False -Confirm:$false Set-Variable Name -Value $ActionParameters.Name -Scope Global -WhatIf:$False -Confirm:$false Set-Variable ActionName -Value $ActionParameters.ActionName -Scope Global -WhatIf:$False -Confirm:$false Set-Variable Parameters -Value $ActionParameters.Parameters -Scope Global -WhatIf:$False -Confirm:$false foreach ($v in $ActionParameters.Parameters.GetEnumerator()) { Set-Variable $v.Key -Value $v.Value -Scope Global -WhatIf:$False -Confirm:$false } foreach ($forItem in $forResult) { $ActionParameters.ForItem = $forItem $ActionParameters.ForParallel = $false Set-Variable ForItem -Value $forItem -Scope Global -WhatIf:$False -Confirm:$false Set-Variable _ -Value $forItem -Scope Global -WhatIf:$False -Confirm:$false if ($PSCmdlet.ShouldProcess("$cmdDisplayName with item '$($forItem)'", "Invoke")) { try { $r += & $Code $ActionParameters } catch { $hasError = $true if ($invokeErrorAction -eq 'Continue') { Write-Host $_.Exception.Message -ForegroundColor White -BackgroundColor Red $ex = $_.Exception } elseif ($invokeErrorAction -notin 'Ignore', 'SilentlyContinue') { Throw } } } } $codeReturn = $r # TODO !!EH Pick-up if Exception --> hasError } } elseif ($aAsJob) { $globalScriptVariables = Get-GlobalVarsForScriptblock -Isolated:$Isolated.IsPresent -AsHashTable $codeAsString = $Code.ToString() $job = Start-Job -ScriptBlock { $vars = $using:globalScriptVariables foreach ($v in $vars.GetEnumerator()) { Set-Variable $v.Key -Value $v.Value -WhatIf:$False -Confirm:$false } if ($using:mp) { Import-Module $using:mp -Args @{ Quiet = $true } } $parameters = $using:ActionParameters Set-Variable Tag -Value $parameters.Tag -WhatIf:$False -Confirm:$false -Option Constant -ErrorAction Ignore Set-Variable Name -Value $parameters.Name -WhatIf:$False -Confirm:$false -Option Constant -ErrorAction Ignore Set-Variable ActionName -Value $parameters.ActionName -WhatIf:$False -Confirm:$false -Option ReadOnly foreach ($v in $parameters.Parameters.GetEnumerator()) { Set-Variable $v.Key -Value $v.Value -WhatIf:$False -Confirm:$false -Option Constant -ErrorAction Ignore } $code = [Scriptblock]::Create($using:codeAsString) try { & $code $parameters } catch { $hasError = $true if ($using:invokeErrorAction -eq 'Continue') { Write-Host $_.Exception.Message -ForegroundColor White -BackgroundColor Red $ex = $_.Exception } elseif ($using:invokeErrorAction -notin 'Ignore', 'SilentlyContinue') { Throw } } } if ($PSCmdlet.ShouldProcess($cmdDisplayName, "Invoke")) { $job | Wait-Job | Out-Null $codeReturn = $job | Receive-Job # TODO !!EH Pick-up if Exception --> hasError $job | Remove-Job | Out-Null } } elseif ($Container.IsPresent -or ($ContainerOptions.Count -gt 0)) { #TODO !!EH No nested action/code supported yet, detect nested code and wrap in workflow of it's own or add container support to Invoke-ActionSequence if ($TypeName -eq 'Stage') { Write-Unsupported "Running Stage in Container" } else { Start-ScriptInContainer -ActionName $cmdDisplayName -ActionType $TypeName -Options $ContainerOptions -Parameters $ActionParameters -Isolated:$Isolated.IsPresent -Code $Code } } elseif ($Session) { if ($Isolated.IsPresent) { $sb = $Code } else { $globalScriptVariables = Get-GlobalVarsForScriptblock -Isolated:$Isolated.IsPresent -AsHashTable $codeAsString = $Code.ToString() $sb = { $vars = $using:globalScriptVariables foreach ($v in $vars.GetEnumerator()) { Set-Variable $v.Key -Value $v.Value -WhatIf:$False -Confirm:$false } #TODO !!EH Do we need Scriptbook Module in remote, so yes install module remote (copy to remote first) # if ($using:mp) # { # Import-Module $using:mp -Args @{ Quiet = $true } # } $parameters = $using:ActionParameters Set-Variable Tag -Value $parameters.Tag -WhatIf:$False -Confirm:$false -Option Constant -ErrorAction Ignore Set-Variable Name -Value $parameters.Name -WhatIf:$False -Confirm:$false -Option Constant -ErrorAction Ignore Set-Variable ActionName -Value $parameters.ActionName -WhatIf:$False -Confirm:$false -Option ReadOnly foreach ($v in $parameters.Parameters.GetEnumerator()) { Set-Variable $v.Key -Value $v.Value -WhatIf:$False -Confirm:$false -Option Constant -ErrorAction Ignore } $code = [Scriptblock]::Create($using:codeAsString) try { & $code $parameters } catch { $hasError = $true if ($using:invokeErrorAction -eq 'Continue') { Write-Host $_.Exception.Message -ForegroundColor White -BackgroundColor Red $ex = $_.Exception } elseif ($using:invokeErrorAction -notin 'Ignore', 'SilentlyContinue') { Throw } } } } if ($PSCmdlet.ShouldProcess($cmdDisplayName, "Invoke")) { try { $codeReturn = Invoke-Command -Session $Session -ScriptBlock $sb -Args $ActionParameters } catch { $hasError = $true if ($invokeErrorAction -eq 'Continue') { Write-ScriptLog $_.Exception.Message -AsError $ex = $_.Exception } elseif ($invokeErrorAction -notin 'Ignore', 'SilentlyContinue') { Throw } } } } else { Set-Variable AsJob -Value $true -Scope Global -WhatIf:$False -Confirm:$false Set-Variable Tag -Value $ActionParameters.Tag -Scope Global -WhatIf:$False -Confirm:$false Set-Variable Name -Value $ActionParameters.Name -Scope Global -WhatIf:$False -Confirm:$false Set-Variable ActionName -Value $ActionParameters.ActionName -Scope Global -WhatIf:$False -Confirm:$false Set-Variable Parameters -Value $ActionParameters.Parameters -Scope Global -WhatIf:$False -Confirm:$false if ($PSCmdlet.ShouldProcess($cmdDisplayName, "Invoke")) { try { $codeReturn = & $Code $ActionParameters } catch { $hasError = $true if ($invokeErrorAction -eq 'Continue') { Write-ScriptLog $_.Exception.Message -AsError $ex = $_.Exception } elseif ($invokeErrorAction -notin 'Ignore', 'SilentlyContinue') { Throw } } } } } else { if ($PSCmdlet.ShouldProcess($cmdDisplayName, "Invoke")) { try { $codeReturn = &"$Command" } catch { $hasError = $true if ($invokeErrorAction -eq 'Continue') { Write-ScriptLog $_.Exception.Message -AsError $ex = $_.Exception } elseif ($invokeErrorAction -notin 'Ignore', 'SilentlyContinue') { Throw } } } } # execute nested actions if ($Script:RootContext.NestedActions.Count -gt 0) { $Script:RootContext.IndentLevel += 1 try { Invoke-ActionSequence -Actions $Script:RootContext.NestedActions -ThrowError $true -Test:$Test.IsPresent -WhatIf:$WhatIfPreference } finally { $Script:RootContext.IndentLevel -= 1 } } # show ReturnValue(s) on console if ($codeReturn -and !$SuppressOutput.IsPresent) { if ($codeReturn -is [array]) { foreach ($line in $codeReturn) { if ($line -is [string]) { Write-Info $line } else { $line } } } else { $codeReturn } } if (!$WhatIfPreference) { Global:Invoke-AfterPerform -Command $Command if ((Test-Path variable:Script:PSakeTearDownTask) -and $Script:PSakeTearDownTask) { & $Script:PSakeTearDownTask $ActionParameters } } } } catch { $hasError = $true if ($invokeErrorAction -eq 'Stop') { $ex = $_.Exception Global:Invoke-AfterPerform -Command $Command -ErrorRecord $_ Global:Write-OnLogException -Exception $ex Throw } if ($invokeErrorAction -eq 'Continue') { $ex = $_.Exception Write-ExceptionMessage $_ -TraceLineCnt 5 -ScriptBlocksOnly Global:Invoke-AfterPerform -Command $Command -ErrorRecord $_ Global:Write-OnLogException -Exception $ex } elseif ($invokeErrorAction -eq 'ContinueSilently') { Global:Invoke-AfterPerform -Command $Command } else { # ignore Global:Invoke-AfterPerform -Command $Command } } finally { Write-ScriptLog @{action = "$cmdDisplayName-Finished"; time = $(Get-Date -Format s); } -AsError:($null -ne $ex) -AsAction -Verbose $Script:RootContext.InAction = $prevInAction $Script:RootContext.NestedActions = $prevNestedActions $indent = $Script:RootContext.IndentLevel if ($Manual.IsPresent) { $indent += 1 } $script:InvokedCommandsResult += @{ Name = "$cmdDisplayName" Duration = $commandStopwatch.Elapsed Indent = $indent Exception = $ex HasError = $hasError ReturnValue = $codeReturn Command = $Command TypeName = $TypeName Comment = $Comment Confirm = $ConfirmAction WhatIf = $WhatIfPreference Skipped = $Skipped } if ($WhatIfAction.IsPresent) { $WhatIfPreference = $prevWhatIfPreference } Pop-Location } if ($NextAction) { if ($PSCmdlet.ShouldProcess($NextAction, "Invoke")) { $action = "Action-$NextAction" Invoke-PerformIfDefined -Command $action -ThrowError $ThrowError -Test:$Test.IsPresent -WhatIf:($WhatIfPreference -or $WhatIfAction.IsPresent) } } } <# .SYNOPSIS Checks if command/action is defined in script / workflow before executing .DESCRIPTION Checks if command/action is defined in script / workflow before executing, In test mode only execute test actions from workflow #> function Invoke-PerformIfDefined { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "")] [CmdletBinding(SupportsShouldProcess = $true)] param ( [string][alias('c')]$Command, $ThrowError = $false, $ActionParameters, [bool]$NoReEntry, [bool]$AsJob = $false, [switch]$NoDepends, [switch]$Test, [switch]$Manual ) $ctx = Get-RootContext $action = $ctx.Actions[$Command.Replace('Invoke-', 'Action-')] if ($action) { if ($action.Disabled) { return } # in test mode only execute test actions from workflow # not in Test mode don't execute test actions if ($Test.IsPresent) { if ($action.TypeName -notin 'Test', 'Tests', 'Setup', 'Teardown') { return } } else { if ($action.TypeName -in 'Test', 'Tests') { return } } if ($ActionParameters) { $ap = $ActionParameters } else { $ap = $action.Parameters } if ($AsJob) { $aj = $true } else { $aj = $action.AsJob } #TODO/FIXME !!EH Put action into separate method? Invoke-Perform -Command $action.Name -Code $action.Code -Depends $action.Depends -ErrorAction $action.ErrorAction -ActionParameters @{Name = $action.DisplayName; ActionName = $action.DisplayName; Tag = $action.Tags; Parameters = $ap } -NoReEntry $NoReEntry -AsJob $aj -If $action.If -NoDepends:$NoDepends.IsPresent -NextAction $Action.NextAction -For $action.For -Parallel:$action.Parallel -Container:$action.Container -ContainerOptions:$action.ContainerOptions -Session $action.Session -Isolated:$action.Isolated -Manual:$Manual.IsPresent -TypeName $action.TypeName -RequiredVariables $action.RequiredVariables -Comment $action.Comment -SuppressOutput:$action.SuppressOutput -ConfirmAction:$action.Confirm -WhatIfAction:$action.WhatIf -Multiple:$action.Multiple } else { # in test mode only execute test functions from workflow # not in Test mode don't execute test functions if ($Test.IsPresent) { if (!$Command.ToLower().Contains('test')) { return } } else { if ($Command.ToLower().Contains('test')) { return } } if (Get-Item function:$Command -ErrorAction SilentlyContinue) { Invoke-Perform -Command $Command -AsJob $AsJob -NoDepends:$NoDepends.IsPresent -WhatIf:$WhatIfPreference -TypeName 'Function' } elseif (Get-Item function:$($Command.Replace('Invoke-', '')) -ErrorAction SilentlyContinue) { Invoke-Perform -Command $Command.Replace('Invoke-', '') -AsJob $AsJob -NoDepends:$NoDepends.IsPresent -WhatIf:$WhatIfPreference -TypeName 'Function' } elseif ($ThrowError) { Throw "Action $($Command.Replace('Invoke-', '').Replace('Action-', '')) or Command $Command not found in ScriptFile(s)" } } } function Invoke-SetupActions { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] [CmdletBinding(SupportsShouldProcess = $true)] param ( $Actions, $ThrowError = $false, [switch]$Test, [switch]$Parallel ) if ($null -eq $Actions -or ($Actions.Count -eq 0)) { Write-Verbose "No actions/steps found to execute" return } foreach ($action in $Actions) { if ($action.TypeName -eq 'Setup' -and ($action.Name -notin $script:InvokedCommands) ) { Invoke-PerformIfDefined -Command $action.Name -ThrowError $ThrowError -Test:$Test.IsPresent -WhatIf:$WhatIfPreference } } } function New-RootContext { [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] param( [switch]$Soft ) if ($PSCmdlet.ShouldProcess("New-RootContext")) { $actions = New-Object -TypeName 'System.Collections.Generic.Dictionary[String,object]' -ArgumentList @([System.StringComparer]::InvariantCultureIgnoreCase) $actionSequence = New-Object -TypeName 'System.Collections.ArrayList' $infos = New-Object -TypeName 'System.Collections.ArrayList' $notifications = New-Object -TypeName 'System.Collections.ArrayList' if ($Soft.IsPresent) { if ($Script:RootContext) { $actions = $Script:RootContext.Actions $actionSequence = $Script:RootContext.ActionSequence $infos = $Script:RootContext.Infos $notifications = $Script:RootContext.Notifications } } New-Object PSObject -Property @{ Actions = $actions ActionSequence = $actionSequence IndentLevel = -1 NoLogging = $false Id = New-Guid InAction = $false NestedActions = New-Object -TypeName 'System.Collections.ArrayList' UniqueIdCounter = 1 Infos = $infos Notifications = $notifications InTest = $false ConfigurePreference = $false Verbose = $false } } } $Script:RootContext = New-RootContext -WhatIf:$false $ErrorActionPreference = 'Stop'; function InternalForceCultureEnglish { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingEmptyCatchBlock", "")] param() try { [CultureInfo]::CurrentCulture = 'en-US' } catch {} } # default InternalForceCultureEnglish function Get-TempPath() { if ( $env:TEMP ) { return ([System.IO.DirectoryInfo]$env:TEMP).FullName } else { return '/tmp' } } function Test-FileLocked([alias('p')][parameter(Mandatory = $true)]$Path) { $f = New-Object System.IO.FileInfo $Path if ((Test-Path -Path $Path) -eq $false) { return $false } try { $oStream = $f.Open([System.IO.FileMode]::Open, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None) if ($oStream) { $oStream.Close() } return $false } catch { # file is locked by a process. return $true } } function Get-GlobalVarsForScriptblock { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "")] param([switch]$Isolated, [switch]$AsUsing, [switch]$AsHashTable) if ($Isolated.IsPresent) { if ($AsHashTable.IsPresent) { return @{} } else { return '' } } if ($AsHashTable.IsPresent) { $result = @{} Get-Variable -Scope Global | ForEach-Object { if ( (!$Global:GlobalVarNames.ContainsKey($_.Name)) -and (!($_.Name.StartsWith('_'))) ) { [void]$result.Add($_.Name, $_.Value) } } } elseif ($AsUsing.IsPresent) { [string]$result = Get-Variable -Scope Global | ForEach-Object { if ( (!$Global:GlobalVarNames.ContainsKey($_.Name)) -and (!($_.Name.StartsWith('_'))) ) { "`$$($_.Name) = `$using:$($_.Name); " } } } else { # TODO !!EH Issue with $null values and PSCustomObjects, don't work with .ToString()... # reformat via ast # create scriptblock from string with Set-Variable 'Name' -Value $null # parse scriptblock with ast and set value of var [string]$result = Get-Variable -Scope Global | ForEach-Object { if ( (!$Global:GlobalVarNames.ContainsKey($_.Name)) -and (!($_.Name.StartsWith('_'))) ) { if ($null -eq $_.Value) { "Set-Variable '$($_.Name)' -Value `$null ; " } elseif ($_.Value -is [string]) { "Set-Variable '$($_.Name)' -Value '$($_.Value)' ; " } elseif ($_.Value -is [array]) { # not working yet array to string "Set-Variable '$($_.Name)' -Value '$($_.Value)' ; " } elseif ($_.Value -is [PSCustomObject] ) { # for now, figure out $v.ToString()... "Set-Variable '$($_.Name)' -Value `$null ; " } else { "Set-Variable '$($_.Name)' -Value $($_.Value) ; " } } } } return $result } function Write-ExceptionMessage([alias('e')]$ErrorRecord, [alias('f')][switch]$Full = $false, [alias('tlc')]$TraceLineCnt = 0, [switch]$ScriptBlocksOnly) { if (($VerbosePreference -eq 'Continue') -or $Full.IsPresent) { Write-Info ($ErrorRecord | Format-List * -Force | Out-String) -ForegroundColor White -BackgroundColor Red } else { Write-Info '' Write-Info 'Error:'.PadRight(78, ' ') -ForegroundColor White -BackgroundColor Red Write-Info $ErrorRecord.Exception.Message -ForegroundColor Red if ($TraceLineCnt -ne 0) { $cnt = 0; Write-Info '' Write-Info 'CallStack:'.PadRight(78, ' ') -ForegroundColor Black -BackgroundColor Yellow foreach ($line in $ErrorRecord.ScriptStackTrace.Split("`n")) { if ($ScriptBlocksOnly.IsPresent) { if ($line.StartsWith('at <ScriptBlock>')) # hardcoded { Write-Info $line -ForegroundColor Yellow } } else { Write-Info $line -ForegroundColor Yellow } $cnt++ if ($cnt -ge $TraceLineCnt) { break; } } Write-Info '' } } } function Write-Experimental($Msg) { Write-Warning "Experimental: $Msg" } function Write-Unsupported($Msg) { Write-Warning "Unsupported: $Msg" } function Get-CommentFromCode($ScriptBlock, $Script, $File, [int]$First = -1, [switch]$IncludeLineComments) { $text = $null $tokens = $errors = $null if ($ScriptBlock) { [System.Management.Automation.Language.Parser]::ParseInput($ScriptBlock.ToString(), [ref]$tokens, [ref]$errors) | Out-Null } elseif ($Script) { [System.Management.Automation.Language.Parser]::ParseInput($Script, [ref]$tokens, [ref]$errors) | Out-Null } elseif ($File) { [System.Management.Automation.Language.Parser]::ParseFile($File, [ref]$tokens, [ref]$errors) | Out-Null } else { Throw "Get-CommentFromCode: No input supplied" } $maxTokens = $First $cntTokens = 0 foreach ($token in $tokens ) { if ($token.Kind -eq 'comment') { if ($token.Text) { if ($token.Text.StartsWith('#') -and !$IncludeLineComments.IsPresent) { continue; } $txt = $token.Text -Split "`n" if ($txt.Length -gt 1) { # get indent from last line and strip $indent = $txt[$txt.Length-1].TrimEnd('#>'); if ($indent.Length -gt 0) { $txt = $txt | ForEach-Object { $_.TrimStart($indent) } } $text += $txt | Select-Object -Skip 1 -First ($txt.Count - 2) -ErrorAction Ignore } else { if ($txt.StartsWith('#')) { $text += $txt.TrimStart('# ') + "`n" } else { $text += $txt.TrimStart('<#').TrimEnd('#>') + "`n" } } } } $cntTokens++ if ( ($maxTokens -ne -1) -and ($cntTokens -ge $maxTokens) ) { break } } return $text } function Read-ParameterValuesInternal { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] [CmdletBinding()] param( [ValidateNotNullOrEmpty()] $Path ) $result = @{} $result = Get-Content -Path $Path | ConvertFrom-Json function Set-Props { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] param($Object) foreach ($prop in $Object.PsObject.Properties) { if ($prop.Value -is [HashTable]) { if ($prop.Value.ContainsKey('TypeName') -and ($prop.Value.TypeName -eq 'SecureStringStorage') ) { $prop.Value = [SecureStringStorage]$prop.Value } } elseif ($prop.Value -is [PSCustomObject]) { if ((Test-PSProperty -o $prop.Value -p 'TypeName' -Exact) -and ($prop.Value.TypeName -eq 'SecureStringStorage') ) { $prop.Value = [SecureStringStorage]$prop.Value } else { Set-Props $prop.Value } } } } # fix SecureString references Set-Props $result $ht = [ordered]@{} foreach ($prop in $result.PSObject.Properties.Name ) { [void]$ht.Add($prop, $result.$prop) } return $ht } function Start-ScriptInContainer { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingEmptyCatchBlock", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "")] [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Low')] param( $File, $Options, $Parameters, $ActionName, $ActionType = 'Action', [switch]$Isolated, [scriptblock]$Code ) if ($Options.ContainsKey('Group') -and $Options.ContainsKey('Instance') ) { # no docker used } else { if ($null -eq (Get-Command docker -ErrorAction Ignore)) { Write-Warning 'Docker not installed or found on this system' return } } # determine Scriptbook module path $m = Get-Module Scriptbook | Select-Object -Last 1 if ($null -eq $m) { Throw "Scriptbook module not found in Start-ScriptInContainer" } $scriptbookModulePath = Split-Path $m.Path -Parent #TODO !!EH make this safe with correct location of current user modules # /$home/.local/share/powershell/Modules # $home\Documents\PowerShell\Modules $userModulePath = Split-Path $m.Path -Parent $userModulePath = (Resolve-Path (Join-Path $userModulePath '../..')).Path # script path if ($File) { $f = Resolve-Path $File $scriptPath = Split-Path $f -Parent $scriptName = Split-Path $f -Leaf } else { $scriptPath = Get-Location $scriptName = '' } $root = $null if ($Options.ContainsKey('Root') -and ![string]::IsNullOrEmpty($Options.Root)) { $root = Resolve-Path $Options.Root if ($File) { if ($scriptPath.Contains($root.Path)) { $replacePath = Join-Path $root.Path '/' $scriptName = Join-Path $scriptPath.Replace($replacePath, '') $scriptName } else { throw "Script $File not found in root path '$root', orphaned paths not supported." } } $scriptPath = $root.Path } # Get container Os (Windows or Linux) $platform = 'linux' $windowsContainer = $false if ($Options.ContainsKey('Group') -and $Options.ContainsKey('Instance') ) { # no docker used } else { try { $r = docker version --format json | ConvertFrom-Json if ($r) { $windowsContainer = $r.Server.Os -eq 'windows' $platform = "$($r.Server.Os)/$($r.Server.Arch)" } } catch { # circumvent erratic behavior json output docker } } if ($Options.ContainsKey('Platform') -and ![string]::IsNullOrEmpty($Options.Platform)) { if ($Options.Platform.Contains('linux')) { $windowsContainer = $false $platform = $Options.Platform } elseif ($Options.Platform.Contains('windows')) { $windowsContainer = $true $platform = $Options.Platform } } $containerName = ([Guid]::NewGuid().ToString('n')).SubString(0, 10) $cImage = 'mcr.microsoft.com/dotnet/sdk:7.0' #TODO !!EH hardcoded for now, move to Import-Module? if ($Options.ContainsKey('Image') -and ![string]::IsNullOrEmpty($Options.Image)) { $cImage = $Options.Image } if ($Options.ContainsKey('Isolated')) { $Isolated = $Options.Isolated } $dockerCredentials = $null if ($Options.ContainsKey('Credentials') -and ($null -ne $Options.Credentials)) { $dockerCredentials = $Options.Credentials } $dockerRegistry = $null if ($Options.ContainsKey('Registry') -and ($null -ne $Options.Registry)) { $dockerRegistry = $Options.Registry } # map scriptbook module, user modules, and script if ($windowsContainer) { $workFolderName = 'Users\Public' $volumeVars = [System.Collections.ArrayList]@('-v', "`"$($scriptPath):c:\Workflow\Scripts`"", '-v', "`"$($userModulePath):c:\Workflow\ModulePath`"", '-v', "`"$($scriptbookModulePath):c:\Workflow\Scriptbook`"") } else { $workFolderName = 'home' $volumeVars = [System.Collections.ArrayList]@('-v', "`"$($scriptPath):/Workflow/Scripts`"", '-v', "`"$($userModulePath):/Workflow/ModulePath`"", '-v', "`"$($scriptbookModulePath):/Workflow/Scriptbook`"") } if ($env:RUNNER_TOOLSDIRECTORY) { [void]$volumeVars.Add('-v'); [void]$volumeVars.Add("`"$($env:RUNNER_TOOLSDIRECTORY):/opt/hostedtoolcache`"") } $envVars = [System.Collections.ArrayList]@('-e', 'InScriptbookContainer=True', '-e', "Script=$scriptName", '-e', "Action=$ActionName" ) foreach ($item in Get-ChildItem env:SCRIPTBOOK_*) { [void]$envVars.Add('-e'); [void]$envVars.Add("$($item.Name)=$($item.Value)"); } foreach ($item in Get-ChildItem env:ARM_*) { [void]$envVars.Add('-e'); [void]$envVars.Add("$($item.Name)=$($item.Value)"); } foreach ($item in Get-ChildItem env:AZURE_*) { [void]$envVars.Add('-e'); [void]$envVars.Add("$($item.Name)=$($item.Value)"); } # Add Azure DevOps & github env vars if ($env:SYSTEM_TEAMPROJECT) { foreach ($item in Get-ChildItem env:BUILD_*) { [void]$envVars.Add('-e'); [void]$envVars.Add("$($item.Name)=$($item.Value)"); } foreach ($item in Get-ChildItem env:SYSTEM_*) { [void]$envVars.Add('-e'); [void]$envVars.Add("$($item.Name)=$($item.Value)"); } foreach ($item in Get-ChildItem env:AGENT_*) { [void]$envVars.Add('-e'); [void]$envVars.Add("$($item.Name)=$($item.Value)"); } foreach ($item in Get-ChildItem env:RUNNER_*) { [void]$envVars.Add('-e'); [void]$envVars.Add("$($item.Name)=$($item.Value)"); } } if ($env:GITHUB_ACTIONS) { foreach ($item in Get-ChildItem env:GITHUB_*) { [void]$envVars.Add('-e'); [void]$envVars.Add("$($item.Name)=$($item.Value)"); } foreach ($item in Get-ChildItem env:RUNNER_*) { [void]$envVars.Add('-e'); [void]$envVars.Add("$($item.Name)=$($item.Value)"); } } if ($Options.ContainsKey('EnvVarPrefixes')) { foreach ($prefix in $Options.EnvVarPrefixes) { foreach ($item in Get-ChildItem env:"$($prefix)*") { [void]$envVars.Add('-e'); [void]$envVars.Add("$($item.Name)=$($item.Value)"); } } } if (!$File) { #TODO !!EH Issue with $null values and PSCustomObjects, don't work with .ToString(). See Get-GlobalVarsForScriptblock $variablesToAdd = Get-GlobalVarsForScriptblock } $quiet = $false if ($Options.ContainsKey('Quiet')) { $quiet = $Options.Quiet } $detailed = $false if ($Options.ContainsKey('Detailed')) { $detailed = $Options.Detailed } if ($detailed) { $quiet = $false } $importCode = @" `$inContainer = `$env:InScriptbookContainer; if (`$inContainer) { `$isolatedTag = ''; if (`$$Isolated) { `$isolatedTag = ' Isolated'; } `$typeTag = '$ActionType'; `$typeName = `$env:Action; if (`$env:Script) { `$typeTag = 'script'; `$typeName = `$env:Script; } $( if (!$quiet) { @" Write-Host ''.PadRight(78, '='); Write-Host "Running `$typeTag '`$typeName'`$isolatedTag"; Write-Host " -In Container '$cImage' On '`$([Environment]::OSVersion.VersionString)'"; Write-Host " -As `$([Environment]::UserName) With 'PowerShell `$(`$PSVersionTable.PsVersion)' At `$((Get-Date).ToString('s'))"; Write-Host ''.PadRight(78, '='); "@ } if ($detailed) { @" Write-Host 'Environment variables:' Get-ChildItem env:* | Sort-Object -Property Name | Out-String | Write-Host; "@ } ) } $( if (!$Isolated) { if ($windowsContainer) { @" `$env:PSModulePath = `$env:PSModulePath + ';c:\Workflow\ModulePath' + ';c:\Workflow\Scriptbook'; "@ } else { @" `$env:PSModulePath = `$env:PSModulePath + ':/Workflow/ModulePath' + ':/Workflow/Scriptbook'; "@ } @" Set-Location '/Workflow/Scripts'; "@ } else { if ($File) { if ($windowsContainer) { @" `$env:PSModulePath = `$env:PSModulePath + ';c:\$workFolderName\Scriptbook'; Set-Location '\$workFolderName\Scripts'; "@ } else { @" `$env:PSModulePath = `$env:PSModulePath + ':/$workFolderName/Scriptbook'; Set-Location '/$workFolderName/Scripts'; "@ } } } ) Write-Verbose "Current location: `$(Get-Location)"; $( if ($File) { # run script Write-Output "&`"./$scriptName`" -WhatIf:!$WhatIfPreference" } else { if (!$Isolated) { # import module Write-Output "Import-Module /Workflow/Scriptbook/Scriptbook.psm1 -Args @{ Quiet = `$true };" # importing vars $variablesToAdd } if ($WhatIfPreference) { Write-Output "Write-Host 'What if: Performing the operation `"Invoke`" on target `"$ActionName`"'; " Write-Output "return;" } } ) "@ $finishCode = @" `$inContainer = `$env:InScriptbookContainer; if (`$inContainer) { `$typeTag = '$ActionType'; `$typeName = `$env:Action; if (`$env:Script) { `$typeTag = 'script'; `$typeName = `$env:Script; } $( if (!$quiet) { @" Write-Host ''.PadRight(78, '='); Write-Host "Finished `$typeTag '`$typeName'"; Write-Host ''.PadRight(78, '='); "@ } ) } "@ if ($Isolated.IsPresent) { $volumeVars = @() } if ($ActionName) { $importCode = [scriptblock]::Create($importCode + "`n" + $Code.ToString() + "`n" + $finishCode) } else { $importCode = $importCode + "`n" + $finishCode } $encodedCommand = [System.Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($importCode)) $dockerContext = $null if ($Options.ContainsKey('Context') -and ![string]::IsNullOrEmpty($Options.Context)) { $dockerContext = $Options.Context } $useSeparateDockerCommands = $true if ($Options.ContainsKey('Run')) { $useSeparateDockerCommands = -not $Options.Run } function StartWithDocker { [CmdletBinding()] param( [Parameter(ValueFromRemainingArguments)]$Remaining ) $a = $null foreach ($item in $Remaining) { $a += "$($item) " } Write-Verbose "StartWithDocker $($a)" $r = Start-ShellCmd -Progress -Command docker -Arguments $a return $r } if ($Options.ContainsKey('Group') -and $Options.ContainsKey('Instance') ) { $m = Get-Module 'Az.ContainerInstance' -ErrorAction Ignore if ($null -eq $m) { # try to import module Import-Module Az.ContainerInstance } $m = Get-Module 'Az.ContainerInstance' -ErrorAction Ignore if ($null -eq $m) { throw 'Required Az.*, Az.ContainerInstance module not found in start Azure Container Instances, use Import-Module Az or Install-Module Az -Scope CurrentUser to install the Azure PowerShell Az module(s) and login with Connect-Az before running this Action' } # try to connect with default azure environment variable service principle credentials if ($env:ARM_CLIENT_ID) { Connect-AzAccount -Credential (New-Object System.Management.Automation.PSCredential ($env:ARM_CLIENT_ID, (ConvertTo-SecureString -String $env:ARM_CLIENT_SECRET -AsPlainText -Force))) -Tenant $env:ARM_TENANT_ID -ServicePrincipal -Subscription $env:ARM_SUBSCRIPTION_ID | Out-Null } if ($null -eq (Get-AzContext)) { throw 'No AzContext found in start Azure Container Instances, are you logged in to Azure with Connect-Az before running this Action' } if (!$Isolated) { throw 'Only Isolated mode supported for now in Azure Container Instances, no volume mapping yet' } # create container instance $instance = @{} if ($Options.ContainsKey('Instance')) { $instance = $Options.Instance } if (!($instance.ContainsKey('Name'))) { # max size container name is 63 characters with following allowed characters '[a-z0-9]([-a-z0-9]*[a-z0-9])?' # TODO: validate on reg if ($ActionName) { $instance.Name = "scriptbook-$($ActionName.ToLower())" } else { $instance.Name = "scriptbook" } } if (!($instance.ContainsKey('Image'))) { $instance.Image = $cImage } if (!($instance.ContainsKey('Command'))) { $instance.Command = "tail", "-f", "/dev/null" } $container = New-AzContainerInstanceObject @instance # setup container group $group = @{} if ($Options.ContainsKey('Group')) { $group = $Options.Group } if (!($group.ContainsKey('ResourceGroupName'))) { Write-Warning "Aci Container Group: Required ResourceGroupName property not found in '$($group)', provide property in Group HashTable, using default ResourceGroupName 'rg-scriptbook'" $group.ResourceGroupName = 'rg-scriptbook' } if (!($group.ContainsKey('Location'))) { Write-Warning "Aci Container Group: Required Location property not found in '$($group)', provide property in Group HashTable, use for example 'WestEurope', using default Location 'WestEurope'" $group.Location = 'WestEurope' } if (!($group.ContainsKey('Name'))) { $group.Name = "cg-scriptbook-$($containerName)" } $group.Container = $container $group.Location = $group.location try { # create container group New-AzContainerGroup @group | Out-Null # execute command Invoke-AzContainerInstanceCommand -ContainerGroupName $group.Name -ResourceGroupName $group.ResourceGroupName -ContainerName $instance.Name -Command "pwsh -NonInteractive -NoLogo -OutputFormat Text -ExecutionPolicy Bypass -EncodedCommand $encodedCommand" if ($Options.ContainsKey('Log') -and $Options.Log) { Get-AzContainerInstanceLog -ContainerGroupName $group.Name -ResourceGroupName $group.ResourceGroupName -ContainerName $instance.Name } } finally { # stop execution if not halted Stop-AzContainerGroup -Name $group.Name -ResourceGroupName $group.ResourceGroupName -ErrorAction Continue | Out-Null # remove container group $removeGroup = $Options.ContainsKey('Debug') -and $Options.Debug if (!$removeGroup) { Remove-AzContainerGroup -Name $group.Name -ResourceGroupName $group.ResourceGroupName -ErrorAction Continue | Out-Null } } } else { $containerStarted = $false try { Write-Verbose "Running container '$containerName' with image '$cImage' in $(Get-Location)" if ($dockerContext) { $r = docker context use $dockerContext if ($LASTEXITCODE -ne 0) { Throw "Error in docker context switch $dockerContext : $LastExitCode $r" } } if ($dockerCredentials -and $dockerRegistry) { $r = $dockerCredentials.Password | docker login $dockerRegistry -u $dockerCredentials.Username --password-stdin if ($LASTEXITCODE -ne 0) { Throw "Error in docker login : $LastExitCode $r" } } if ($useSeparateDockerCommands) { $r = StartWithDocker create @envVars $volumeVars --platform=$platform --tty --interactive --name "$containerName" $cImage if ($LASTEXITCODE -ne 0) { Throw "Error in docker create for container '$containerName' with image '$cImage' on platform '$platform' : $LastExitCode $r" } if ($File -and $Isolated.IsPresent) { # copy script and modules when isolated docker cp "$scriptPath" "$($containerName):/$workFolderName/Scripts" if ($m.RepositorySourceLocation) { $tmp = Join-Path (Get-TempPath) (New-Guid) New-Item $tmp -ItemType Directory | Out-Null try { $sPath = Join-Path (Join-Path $tmp Scriptbook ) $m.Version Copy-Item $scriptbookModulePath $sPath -Recurse docker cp "$tmp" "$($containerName):/$workFolderName/Scriptbook" } finally { Remove-Item $tmp -Recurse -Force -ErrorAction Ignore } } else { docker cp "$scriptbookModulePath" "$($containerName):/$workFolderName/Scriptbook" } } $r = docker start "$containerName" if ($LASTEXITCODE -ne 0) { Throw "Error in docker start for container '$containerName' with image '$cImage' on platform '$platform' : $LastExitCode $r" } $containerStarted = $true $r = Start-ShellCmd -Progress -Command 'docker' -Arguments "exec `"$containerName`" pwsh -NonInteractive -NoLogo -OutputFormat Text -ExecutionPolicy Bypass -EncodedCommand $encodedCommand" if ($r.ExitCode -ne 0) { Throw "Error in docker exec for container '$containerName' with image '$cImage' on platform '$platform' : $LastExitCode" } if ([string]::IsNullOrEmpty($r.StdOut)) { Throw "No output found in 'docker exec' command" } if (![string]::IsNullOrEmpty($r.StdErr)) { Throw "Errors found in output 'docker exec' command $($r.StdErr)" } } else { StartWithDocker run @envVars $volumeVars --platform=$platform --name "$containerName" $cImage pwsh -NonInteractive -NoLogo -OutputFormat Text -ExecutionPolicy Bypass -EncodedCommand $encodedCommand if ($LASTEXITCODE -ne 0) { Throw "Error in docker run for container '$containerName' with image '$cImage' on platform '$platform' : $LastExitCode" } } } finally { try { if ($containerStarted) { $r = docker stop "$containerName" if ($LASTEXITCODE -ne 0) { Throw "Error in docker stop for container '$containerName' : $LastExitCode $r" } } } finally { $r = docker container ls -a -f name=$containerName if ($LASTEXITCODE -ne 0) { $r = $containerName } # some context's don't support container ls --> always try to remove if ( "$r".Contains($containerName)) { $r = docker rm --force $containerName if ($LASTEXITCODE -ne 0) { Throw "Error in docker remove for container '$containerName' output: $r" } } } } } } function Write-ScriptBlock($ScriptBlock) { Write-StringResult (Invoke-Command -ScriptBlock $ScriptBlock) } function Write-ScriptLog($Msg, [switch]$AsError, [switch]$AsWarning, [switch]$AsAction, [switch]$AsWorkflow, [switch]$AsSkipped, [switch]$Verbose) { $ctx = Get-RootContext if ($ctx.NoLogging -and (!$ctx.Verbose -or !$Verbose.IsPresent)) { return } $colors = @{} if ($AsError.IsPresent) { [void]$colors.Add('ForegroundColor', 'White') [void]$colors.Add('BackgroundColor', 'Red') } elseif ($AsWarning.IsPresent) { [void]$colors.Add('ForegroundColor', 'White') [void]$colors.Add('BackgroundColor', 'Yellow') } elseif ($AsAction.IsPresent) { [void]$colors.Add('ForegroundColor', 'Blue') } elseif ($AsWorkflow.IsPresent -or $AsSkipped.IsPresent) { [void]$colors.Add('ForegroundColor', 'Magenta') } if ($Msg -and $Msg.GetType().Name -eq 'HashTable') { if ($Msg.ContainsKey('action')) { $m = $Msg.action; $Msg.Remove('action'); } elseif ($Msg.ContainsKey('command')) { $m = $Msg.command; $Msg.Remove('command'); } Write-Info $m @colors; Global:Write-OnLog -Msg $m if ($ctx.Verbose) { Write-Info ($Msg.GetEnumerator() | Sort-Object -Property Name | ForEach-Object { 'VERBOSE: @{0}:{1}' -f $_.key, $_.value }) -ForegroundColor Yellow } } else { Write-Info $Msg @colors } } function Write-StringResult($Result) { if ($Result -is [array]) { foreach ($l in $Result) { Write-Info $l } } else { Write-Info $Result } } # SIG # Begin signature block # MIIr0gYJKoZIhvcNAQcCoIIrwzCCK78CAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB # gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR # AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUXBUt8nSw3PVIdDM17BdjDYip # XkeggiUPMIIFbzCCBFegAwIBAgIQSPyTtGBVlI02p8mKidaUFjANBgkqhkiG9w0B # AQwFADB7MQswCQYDVQQGEwJHQjEbMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVy # MRAwDgYDVQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEh # MB8GA1UEAwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTIxMDUyNTAwMDAw # MFoXDTI4MTIzMTIzNTk1OVowVjELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1NlY3Rp # Z28gTGltaXRlZDEtMCsGA1UEAxMkU2VjdGlnbyBQdWJsaWMgQ29kZSBTaWduaW5n # IFJvb3QgUjQ2MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjeeUEiIE # JHQu/xYjApKKtq42haxH1CORKz7cfeIxoFFvrISR41KKteKW3tCHYySJiv/vEpM7 # fbu2ir29BX8nm2tl06UMabG8STma8W1uquSggyfamg0rUOlLW7O4ZDakfko9qXGr # YbNzszwLDO/bM1flvjQ345cbXf0fEj2CA3bm+z9m0pQxafptszSswXp43JJQ8mTH # qi0Eq8Nq6uAvp6fcbtfo/9ohq0C/ue4NnsbZnpnvxt4fqQx2sycgoda6/YDnAdLv # 64IplXCN/7sVz/7RDzaiLk8ykHRGa0c1E3cFM09jLrgt4b9lpwRrGNhx+swI8m2J # mRCxrds+LOSqGLDGBwF1Z95t6WNjHjZ/aYm+qkU+blpfj6Fby50whjDoA7NAxg0P # OM1nqFOI+rgwZfpvx+cdsYN0aT6sxGg7seZnM5q2COCABUhA7vaCZEao9XOwBpXy # bGWfv1VbHJxXGsd4RnxwqpQbghesh+m2yQ6BHEDWFhcp/FycGCvqRfXvvdVnTyhe # Be6QTHrnxvTQ/PrNPjJGEyA2igTqt6oHRpwNkzoJZplYXCmjuQymMDg80EY2NXyc # uu7D1fkKdvp+BRtAypI16dV60bV/AK6pkKrFfwGcELEW/MxuGNxvYv6mUKe4e7id # FT/+IAx1yCJaE5UZkADpGtXChvHjjuxf9OUCAwEAAaOCARIwggEOMB8GA1UdIwQY # MBaAFKARCiM+lvEH7OKvKe+CpX/QMKS0MB0GA1UdDgQWBBQy65Ka/zWWSC8oQEJw # IDaRXBeF5jAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zATBgNVHSUE # DDAKBggrBgEFBQcDAzAbBgNVHSAEFDASMAYGBFUdIAAwCAYGZ4EMAQQBMEMGA1Ud # HwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29tL0FBQUNlcnRpZmlj # YXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEFBQcwAYYYaHR0 # cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBDAUAA4IBAQASv6Hvi3Sa # mES4aUa1qyQKDKSKZ7g6gb9Fin1SB6iNH04hhTmja14tIIa/ELiueTtTzbT72ES+ # BtlcY2fUQBaHRIZyKtYyFfUSg8L54V0RQGf2QidyxSPiAjgaTCDi2wH3zUZPJqJ8 # ZsBRNraJAlTH/Fj7bADu/pimLpWhDFMpH2/YGaZPnvesCepdgsaLr4CnvYFIUoQx # 2jLsFeSmTD1sOXPUC4U5IOCFGmjhp0g4qdE2JXfBjRkWxYhMZn0vY86Y6GnfrDyo # XZ3JHFuu2PMvdM+4fvbXg50RlmKarkUT2n/cR/vfw1Kf5gZV6Z2M8jpiUbzsJA8p # 1FiAhORFe1rYMIIFjTCCBHWgAwIBAgIQDpsYjvnQLefv21DiCEAYWjANBgkqhkiG # 9w0BAQwFADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkw # FwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1 # cmVkIElEIFJvb3QgQ0EwHhcNMjIwODAxMDAwMDAwWhcNMzExMTA5MjM1OTU5WjBi # MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 # d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg # RzQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAi # MGkz7MKnJS7JIT3yithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnny # yhHS5F/WBTxSD1Ifxp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE # 5nQ7bXHiLQwb7iDVySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm # 7nfISKhmV1efVFiODCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5 # w3jHtrHEtWoYOAMQjdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsD # dV14Ztk6MUSaM0C/CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1Z # XUJ2h4mXaXpI8OCiEhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS0 # 0mFt6zPZxd9LBADMfRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hk # pjPRiQfhvbfmQ6QYuKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m8 # 00ERElvlEFDrMcXKchYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+i # sX4KJpn15GkvmB0t9dmpsh3lGwIDAQABo4IBOjCCATYwDwYDVR0TAQH/BAUwAwEB # /zAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wHwYDVR0jBBgwFoAUReui # r/SSy4IxLVGLp6chnfNtyA8wDgYDVR0PAQH/BAQDAgGGMHkGCCsGAQUFBwEBBG0w # azAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEMGCCsGAQUF # BzAChjdodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVk # SURSb290Q0EuY3J0MEUGA1UdHwQ+MDwwOqA4oDaGNGh0dHA6Ly9jcmwzLmRpZ2lj # ZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmwwEQYDVR0gBAowCDAG # BgRVHSAAMA0GCSqGSIb3DQEBDAUAA4IBAQBwoL9DXFXnOF+go3QbPbYW1/e/Vwe9 # mqyhhyzshV6pGrsi+IcaaVQi7aSId229GhT0E0p6Ly23OO/0/4C5+KH38nLeJLxS # A8hO0Cre+i1Wz/n096wwepqLsl7Uz9FDRJtDIeuWcqFItJnLnU+nBgMTdydE1Od/ # 6Fmo8L8vC6bp8jQ87PcDx4eo0kxAGTVGamlUsLihVo7spNU96LHc/RzY9HdaXFSM # b++hUD38dglohJ9vytsgjTVgHAIDyyCwrFigDkBjxZgiwbJZ9VVrzyerbHbObyMt # 9H5xaiNrIv8SuFQtJ37YOtnwtoeW/VvRXKwYw02fc7cBqZ9Xql4o4rmUMIIGGjCC # BAKgAwIBAgIQYh1tDFIBnjuQeRUgiSEcCjANBgkqhkiG9w0BAQwFADBWMQswCQYD # VQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMS0wKwYDVQQDEyRTZWN0 # aWdvIFB1YmxpYyBDb2RlIFNpZ25pbmcgUm9vdCBSNDYwHhcNMjEwMzIyMDAwMDAw # WhcNMzYwMzIxMjM1OTU5WjBUMQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGln # byBMaW1pdGVkMSswKQYDVQQDEyJTZWN0aWdvIFB1YmxpYyBDb2RlIFNpZ25pbmcg # Q0EgUjM2MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAmyudU/o1P45g # BkNqwM/1f/bIU1MYyM7TbH78WAeVF3llMwsRHgBGRmxDeEDIArCS2VCoVk4Y/8j6 # stIkmYV5Gej4NgNjVQ4BYoDjGMwdjioXan1hlaGFt4Wk9vT0k2oWJMJjL9G//N52 # 3hAm4jF4UjrW2pvv9+hdPX8tbbAfI3v0VdJiJPFy/7XwiunD7mBxNtecM6ytIdUl # h08T2z7mJEXZD9OWcJkZk5wDuf2q52PN43jc4T9OkoXZ0arWZVeffvMr/iiIROSC # zKoDmWABDRzV/UiQ5vqsaeFaqQdzFf4ed8peNWh1OaZXnYvZQgWx/SXiJDRSAolR # zZEZquE6cbcH747FHncs/Kzcn0Ccv2jrOW+LPmnOyB+tAfiWu01TPhCr9VrkxsHC # 5qFNxaThTG5j4/Kc+ODD2dX/fmBECELcvzUHf9shoFvrn35XGf2RPaNTO2uSZ6n9 # otv7jElspkfK9qEATHZcodp+R4q2OIypxR//YEb3fkDn3UayWW9bAgMBAAGjggFk # MIIBYDAfBgNVHSMEGDAWgBQy65Ka/zWWSC8oQEJwIDaRXBeF5jAdBgNVHQ4EFgQU # DyrLIIcouOxvSK4rVKYpqhekzQwwDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQI # MAYBAf8CAQAwEwYDVR0lBAwwCgYIKwYBBQUHAwMwGwYDVR0gBBQwEjAGBgRVHSAA # MAgGBmeBDAEEATBLBgNVHR8ERDBCMECgPqA8hjpodHRwOi8vY3JsLnNlY3RpZ28u # Y29tL1NlY3RpZ29QdWJsaWNDb2RlU2lnbmluZ1Jvb3RSNDYuY3JsMHsGCCsGAQUF # BwEBBG8wbTBGBggrBgEFBQcwAoY6aHR0cDovL2NydC5zZWN0aWdvLmNvbS9TZWN0 # aWdvUHVibGljQ29kZVNpZ25pbmdSb290UjQ2LnA3YzAjBggrBgEFBQcwAYYXaHR0 # cDovL29jc3Auc2VjdGlnby5jb20wDQYJKoZIhvcNAQEMBQADggIBAAb/guF3YzZu # e6EVIJsT/wT+mHVEYcNWlXHRkT+FoetAQLHI1uBy/YXKZDk8+Y1LoNqHrp22AKMG # xQtgCivnDHFyAQ9GXTmlk7MjcgQbDCx6mn7yIawsppWkvfPkKaAQsiqaT9DnMWBH # VNIabGqgQSGTrQWo43MOfsPynhbz2Hyxf5XWKZpRvr3dMapandPfYgoZ8iDL2OR3 # sYztgJrbG6VZ9DoTXFm1g0Rf97Aaen1l4c+w3DC+IkwFkvjFV3jS49ZSc4lShKK6 # BrPTJYs4NG1DGzmpToTnwoqZ8fAmi2XlZnuchC4NPSZaPATHvNIzt+z1PHo35D/f # 7j2pO1S8BCysQDHCbM5Mnomnq5aYcKCsdbh0czchOm8bkinLrYrKpii+Tk7pwL7T # jRKLXkomm5D1Umds++pip8wH2cQpf93at3VDcOK4N7EwoIJB0kak6pSzEu4I64U6 # gZs7tS/dGNSljf2OSSnRr7KWzq03zl8l75jy+hOds9TWSenLbjBQUGR96cFr6lEU # fAIEHVC1L68Y1GGxx4/eRI82ut83axHMViw1+sVpbPxg51Tbnio1lB93079WPFnY # aOvfGAA0e0zcfF/M9gXr+korwQTh2Prqooq2bYNMvUoUKD85gnJ+t0smrWrb8dee # 2CvYZXD5laGtaAxOfy/VKNmwuWuAh9kcMIIGczCCBNugAwIBAgIQeYR98Ng1Tj8U # ctnupVAebDANBgkqhkiG9w0BAQwFADBUMQswCQYDVQQGEwJHQjEYMBYGA1UEChMP # U2VjdGlnbyBMaW1pdGVkMSswKQYDVQQDEyJTZWN0aWdvIFB1YmxpYyBDb2RlIFNp # Z25pbmcgQ0EgUjM2MB4XDTIxMDcyODAwMDAwMFoXDTI0MDcyNzIzNTk1OVowXDEL # MAkGA1UEBhMCTkwxETAPBgNVBAcMCFNjaGllZGFtMRwwGgYDVQQKDBNUZWRvbiBU # ZWNobm9sb2d5IEJWMRwwGgYDVQQDDBNUZWRvbiBUZWNobm9sb2d5IEJWMIICIjAN # BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1S7lOvHhvowTe7AbeHH+DjQj3CRs # f8kurAHBIuD/JXGiDqNKg0d8p+6zjvqZYOfEviyVSm7IXpASqarrVHozM9t5HAw1 # rVI7aOfY09VqId3XHUzhusa6UN0kP56Bf2jq0dp0Ya2Q8s93PXE+8hJfHYW14Pxf # 4XAT9L+SLn8i6CCW1NfCNUTfsvb4CcTL8MKidGAk6H+EnJQlExOl+hCKfezWfmay # rWRjJbIQQsrTdPnn1VQdV2AsnQ0518lMLkBkMcaS8mf5avN1M760aTbd/j1JIf2D # 046se9LEAVZw+wjga7HwJvZhj6OsPKvxL6ZV1vai1ZW07StFH3kGd+Osa2OjbO9T # DVfKl57FWFfY4Nw7iGtVmPUBvmp1L+fUGC0PsIhbMltLqZfDNJHk0q4Jub3dPcxj # mXoH9OE3GVWH+rn6zUFm4LDg0jmfmuLpAm+sppuErvVY61SvJw6ITCm1DphRbvpT # S56qy+ck9+uo6qlm6VMohgCm/icO2RtYoxLERQrN1x0i/d9nM6jd+LpbndVvcRdv # Bpd+nOciTAUtSJkWIdbBRND9WVfpRPBUunZynkYZ+KdS8QtFES0k/RQu2wWBNqIt # kwZziuYI72FXN4m2HyzJVJUzker6OaUCfwCxLwoUdpfa0ep1QPfpl/s3kkBmMiC0 # l8Bx5Cuy59W8l8UCAwEAAaOCAbcwggGzMB8GA1UdIwQYMBaAFA8qyyCHKLjsb0iu # K1SmKaoXpM0MMB0GA1UdDgQWBBTv5cqB48RXvR408SsZJbSKP3ruojAOBgNVHQ8B # Af8EBAMCB4AwDAYDVR0TAQH/BAIwADATBgNVHSUEDDAKBggrBgEFBQcDAzARBglg # hkgBhvhCAQEEBAMCBBAwSgYDVR0gBEMwQTA1BgwrBgEEAbIxAQIBAwIwJTAjBggr # BgEFBQcCARYXaHR0cHM6Ly9zZWN0aWdvLmNvbS9DUFMwCAYGZ4EMAQQBMEkGA1Ud # HwRCMEAwPqA8oDqGOGh0dHA6Ly9jcmwuc2VjdGlnby5jb20vU2VjdGlnb1B1Ymxp # Y0NvZGVTaWduaW5nQ0FSMzYuY3JsMHkGCCsGAQUFBwEBBG0wazBEBggrBgEFBQcw # AoY4aHR0cDovL2NydC5zZWN0aWdvLmNvbS9TZWN0aWdvUHVibGljQ29kZVNpZ25p # bmdDQVIzNi5jcnQwIwYIKwYBBQUHMAGGF2h0dHA6Ly9vY3NwLnNlY3RpZ28uY29t # MBkGA1UdEQQSMBCBDmluZm9AdGVkb24uY29tMA0GCSqGSIb3DQEBDAUAA4IBgQAW # Su+2u7XZGbyh7tDXp9YskQ2vx3ZtT3NpN3JbN78FMSUKyEPIOshV2XWaP9GtCGmH # YGRlmIS0qY0zZwei8rjFg0wNon6GakCER99XBNAqE9UPpNNC+fJCT+H+rXCShjAK # 6ohXry5tOGYJ2Mjz/TCwL2LX4MN42JbS5tL0/ZivsZuk/xNBkSRPwSmTl5xAwnWZ # IN21eN11vypef5IldBTvoGAC4vTxBAgevLkn5ji0hTMZdTLbvlZdL+4n/dtl0c/B # v1FamuPc4eYaJnsD0iEv3VUxwOHJpZc2KpWS6xV1/OjrB5q75EXSmtCaEU/ssqjI # ZwUUvmwyJKjKA3Biz2Mq+INTTGaeIGdILYSwq0EZm4U3X+FXf6RTbRqyRmRn/mOu # BQIPipFLjsQgUG0VCl7G1+gOGg3YC7gmAmZQurdGofuf49YLiF9gCfP2QaVAjKdM # 06xftWsphFGIcbCmMo76gdx3TG6q8gJI5qRi8qcFxPzRQRWnq5EuiAXqa0ekuFIw # ggauMIIElqADAgECAhAHNje3JFR82Ees/ShmKl5bMA0GCSqGSIb3DQEBCwUAMGIx # CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 # dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH # NDAeFw0yMjAzMjMwMDAwMDBaFw0zNzAzMjIyMzU5NTlaMGMxCzAJBgNVBAYTAlVT # MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1 # c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0EwggIiMA0GCSqG # SIb3DQEBAQUAA4ICDwAwggIKAoICAQDGhjUGSbPBPXJJUVXHJQPE8pE3qZdRodbS # g9GeTKJtoLDMg/la9hGhRBVCX6SI82j6ffOciQt/nR+eDzMfUBMLJnOWbfhXqAJ9 # /UO0hNoR8XOxs+4rgISKIhjf69o9xBd/qxkrPkLcZ47qUT3w1lbU5ygt69OxtXXn # HwZljZQp09nsad/ZkIdGAHvbREGJ3HxqV3rwN3mfXazL6IRktFLydkf3YYMZ3V+0 # VAshaG43IbtArF+y3kp9zvU5EmfvDqVjbOSmxR3NNg1c1eYbqMFkdECnwHLFuk4f # sbVYTXn+149zk6wsOeKlSNbwsDETqVcplicu9Yemj052FVUmcJgmf6AaRyBD40Nj # gHt1biclkJg6OBGz9vae5jtb7IHeIhTZgirHkr+g3uM+onP65x9abJTyUpURK1h0 # QCirc0PO30qhHGs4xSnzyqqWc0Jon7ZGs506o9UD4L/wojzKQtwYSH8UNM/STKvv # mz3+DrhkKvp1KCRB7UK/BZxmSVJQ9FHzNklNiyDSLFc1eSuo80VgvCONWPfcYd6T # /jnA+bIwpUzX6ZhKWD7TA4j+s4/TXkt2ElGTyYwMO1uKIqjBJgj5FBASA31fI7tk # 42PgpuE+9sJ0sj8eCXbsq11GdeJgo1gJASgADoRU7s7pXcheMBK9Rp6103a50g5r # mQzSM7TNsQIDAQABo4IBXTCCAVkwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4E # FgQUuhbZbU2FL3MpdpovdYxqII+eyG8wHwYDVR0jBBgwFoAU7NfjgtJxXWRM3y5n # P+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMIMHcG # CCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQu # Y29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGln # aUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8v # Y3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNybDAgBgNV # HSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZIhvcNAQELBQADggIB # AH1ZjsCTtm+YqUQiAX5m1tghQuGwGC4QTRPPMFPOvxj7x1Bd4ksp+3CKDaopafxp # wc8dB+k+YMjYC+VcW9dth/qEICU0MWfNthKWb8RQTGIdDAiCqBa9qVbPFXONASIl # zpVpP0d3+3J0FNf/q0+KLHqrhc1DX+1gtqpPkWaeLJ7giqzl/Yy8ZCaHbJK9nXzQ # cAp876i8dU+6WvepELJd6f8oVInw1YpxdmXazPByoyP6wCeCRK6ZJxurJB4mwbfe # Kuv2nrF5mYGjVoarCkXJ38SNoOeY+/umnXKvxMfBwWpx2cYTgAnEtp/Nh4cku0+j # Sbl3ZpHxcpzpSwJSpzd+k1OsOx0ISQ+UzTl63f8lY5knLD0/a6fxZsNBzU+2QJsh # IUDQtxMkzdwdeDrknq3lNHGS1yZr5Dhzq6YBT70/O3itTK37xJV77QpfMzmHQXh6 # OOmc4d0j/R0o08f56PGYX/sr2H7yRp11LB4nLCbbbxV7HhmLNriT1ObyF5lZynDw # N7+YAN8gFk8n+2BnFqFmut1VwDophrCYoCvtlUG3OtUVmDG0YgkPCr2B2RP+v6TR # 81fZvAT6gt4y3wSJ8ADNXcL50CN/AAvkdgIm2fBldkKmKYcJRyvmfxqkhQ/8mJb2 # VVQrH4D6wPIOK+XW+6kvRBVK5xMOHds3OBqhK/bt1nz8MIIGwDCCBKigAwIBAgIQ # DE1pckuU+jwqSj0pB4A9WjANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJVUzEX # MBUGA1UEChMORGlnaUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0 # ZWQgRzQgUlNBNDA5NiBTSEEyNTYgVGltZVN0YW1waW5nIENBMB4XDTIyMDkyMTAw # MDAwMFoXDTMzMTEyMTIzNTk1OVowRjELMAkGA1UEBhMCVVMxETAPBgNVBAoTCERp # Z2lDZXJ0MSQwIgYDVQQDExtEaWdpQ2VydCBUaW1lc3RhbXAgMjAyMiAtIDIwggIi # MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDP7KUmOsap8mu7jcENmtuh6BSF # dDMaJqzQHFUeHjZtvJJVDGH0nQl3PRWWCC9rZKT9BoMW15GSOBwxApb7crGXOlWv # M+xhiummKNuQY1y9iVPgOi2Mh0KuJqTku3h4uXoW4VbGwLpkU7sqFudQSLuIaQyI # xvG+4C99O7HKU41Agx7ny3JJKB5MgB6FVueF7fJhvKo6B332q27lZt3iXPUv7Y3U # TZWEaOOAy2p50dIQkUYp6z4m8rSMzUy5Zsi7qlA4DeWMlF0ZWr/1e0BubxaompyV # R4aFeT4MXmaMGgokvpyq0py2909ueMQoP6McD1AGN7oI2TWmtR7aeFgdOej4TJEQ # ln5N4d3CraV++C0bH+wrRhijGfY59/XBT3EuiQMRoku7mL/6T+R7Nu8GRORV/zbq # 5Xwx5/PCUsTmFntafqUlc9vAapkhLWPlWfVNL5AfJ7fSqxTlOGaHUQhr+1NDOdBk # +lbP4PQK5hRtZHi7mP2Uw3Mh8y/CLiDXgazT8QfU4b3ZXUtuMZQpi+ZBpGWUwFjl # 5S4pkKa3YWT62SBsGFFguqaBDwklU/G/O+mrBw5qBzliGcnWhX8T2Y15z2LF7OF7 # ucxnEweawXjtxojIsG4yeccLWYONxu71LHx7jstkifGxxLjnU15fVdJ9GSlZA076 # XepFcxyEftfO4tQ6dwIDAQABo4IBizCCAYcwDgYDVR0PAQH/BAQDAgeAMAwGA1Ud # EwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwIAYDVR0gBBkwFzAIBgZn # gQwBBAIwCwYJYIZIAYb9bAcBMB8GA1UdIwQYMBaAFLoW2W1NhS9zKXaaL3WMaiCP # nshvMB0GA1UdDgQWBBRiit7QYfyPMRTtlwvNPSqUFN9SnDBaBgNVHR8EUzBRME+g # TaBLhklodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRS # U0E0MDk2U0hBMjU2VGltZVN0YW1waW5nQ0EuY3JsMIGQBggrBgEFBQcBAQSBgzCB # gDAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFgGCCsGAQUF # BzAChkxodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVk # RzRSU0E0MDk2U0hBMjU2VGltZVN0YW1waW5nQ0EuY3J0MA0GCSqGSIb3DQEBCwUA # A4ICAQBVqioa80bzeFc3MPx140/WhSPx/PmVOZsl5vdyipjDd9Rk/BX7NsJJUSx4 # iGNVCUY5APxp1MqbKfujP8DJAJsTHbCYidx48s18hc1Tna9i4mFmoxQqRYdKmEIr # UPwbtZ4IMAn65C3XCYl5+QnmiM59G7hqopvBU2AJ6KO4ndetHxy47JhB8PYOgPvk # /9+dEKfrALpfSo8aOlK06r8JSRU1NlmaD1TSsht/fl4JrXZUinRtytIFZyt26/+Y # siaVOBmIRBTlClmia+ciPkQh0j8cwJvtfEiy2JIMkU88ZpSvXQJT657inuTTH4YB # ZJwAwuladHUNPeF5iL8cAZfJGSOA1zZaX5YWsWMMxkZAO85dNdRZPkOaGK7DycvD # +5sTX2q1x+DzBcNZ3ydiK95ByVO5/zQQZ/YmMph7/lxClIGUgp2sCovGSxVK05iQ # RWAzgOAj3vgDpPZFR+XOuANCR+hBNnF3rf2i6Jd0Ti7aHh2MWsgemtXC8MYiqE+b # vdgcmlHEL5r2X6cnl7qWLoVXwGDneFZ/au/ClZpLEQLIgpzJGgV8unG1TnqZbPTo # ntRamMifv427GFxD9dAq6OJi7ngE273R+1sKqHB+8JeEeOMIA11HLGOoJTiXAdI/ # Otrl5fbmm9x+LMz/F0xNAKLY1gEOuIvu5uByVYksJxlh9ncBjDGCBi0wggYpAgEB # MGgwVDELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1NlY3RpZ28gTGltaXRlZDErMCkG # A1UEAxMiU2VjdGlnbyBQdWJsaWMgQ29kZSBTaWduaW5nIENBIFIzNgIQeYR98Ng1 # Tj8UctnupVAebDAJBgUrDgMCGgUAoHgwGAYKKwYBBAGCNwIBDDEKMAigAoAAoQKA # ADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3AgELMQ4wDAYK # KwYBBAGCNwIBFTAjBgkqhkiG9w0BCQQxFgQUkQwEFe3NbhFySINVChypRt8hd9kw # DQYJKoZIhvcNAQEBBQAEggIAT/ZaWOm/2/0NGCGhCAZX+TIx3Re4Sah5cKeEXqae # i69sMCXlYrMMqZRXIHz5TUDLJf1v0xhlFoFUWDSEn/D+FeCiivXm0OQw4HiZcEE1 # AOCmRIWs0vSFdU7VizyGAJl5A9v6YzdFdWMxgE01gQ3bGWw1B7rXcu7cl7twQEFn # xbKQ9wQoNDQyJMegf3kR0227x1DCNF0onj1ZR6r44Izj+4nWRiUfY09534XgWbaC # yTNjL877a12RFOyZQxt4py4Gzd/D2cvZel2Tc0t+fBqOOqycge9ibQBBPAwKw1EO # utG5z5VAY9KSQ1NQ6Qr6hE+BmaxoBWMeegPL1Vd/B8Kx8k54JRjzAP/8ml3FNe0k # FDX8ygFFpRQCuaPQZUNX5Y1FTmMWQ8zgjse+Wh5EaBoy+7AkIq+a//smb5ZDwJqQ # WHxr1Vw8XvOsqaOx9/9V76ald9J7GfTjnMp+Lk3Cvfwyy4GW6Yyf5Ua1ZvnWIwHl # FGhtkQZ2ZZAjcsdkFvsQQJWxEqCLXNTeFYvah6ozzRMs11ZocbzLpmvrCsVRwUu/ # v9F81K1OIhcbwT1M1dxQrFWHmrTQoTPA3U+65tK+RqaY5NbTZwFQAyWaugwb2gUS # J8vW9L0sfmyTKZKJMHrRLFZxPC6MMcHn7wJ6l7ZzZFDggGio9l48QiO5NtJ4BFgD # YEKhggMgMIIDHAYJKoZIhvcNAQkGMYIDDTCCAwkCAQEwdzBjMQswCQYDVQQGEwJV # UzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRy # dXN0ZWQgRzQgUlNBNDA5NiBTSEEyNTYgVGltZVN0YW1waW5nIENBAhAMTWlyS5T6 # PCpKPSkHgD1aMA0GCWCGSAFlAwQCAQUAoGkwGAYJKoZIhvcNAQkDMQsGCSqGSIb3 # DQEHATAcBgkqhkiG9w0BCQUxDxcNMjMwNzIxMTk0OTM0WjAvBgkqhkiG9w0BCQQx # IgQgqo7Nfa6LXiXym2XhpJzKcK8Cs8zsFnIu/8BY68CTVkgwDQYJKoZIhvcNAQEB # BQAEggIAsu7WdrY6UtkLPCP9W5mIXGf/dxvZyBxMRR5gWt7BtRYgzm72iqLVqR05 # EYy4qcQSxbFaioy4PP5yrGx/65VyyM7UI/gWVhWcG2nwAPNh2oIY4DWanjVVN4eR # 6RCAy8TzXNusY1eRWfLxvrDkrXEU/fla6MDlhn0AqCiCPNd6X4ASKUBPLFQSPJ41 # O9te+cy5Ozt3l3QjyGylasLQBulWtU15BGGsaQhlLjhBhecmHbx72NkH6Z6vX+Wv # BGsnhZPo+gMvcEc+9xNT3Ln3ryShctsOTLhdc00YOMX07oZjwnZSTBY+H2rIYrsg # 7Rx4N8NMDakDsPkfOkQpAMM+pKo17TJMYq0Xa/28rjw9BsztGL1s7qRObzawprBF # ePnoBdK2AIuEhYR+WfuQBrMcl9TkKAkEzOR+5625JKj8c2SpOA8L7pJ/OHNBQs2Q # sO6DTLf1H0FP0w9DeUx37Ghq6RA1dRxdwdAMSuQ2mzEAiFNMkIlhyH2ZNaawu4p/ # KvkKI1d3A+nJCM+kepUlRMdZ5QMsvmpqgOQLX3+BLVEs5koSlXbWI7oTQ4FwRfUa # vWvsKazrZCLlQjRRWwS4wd9gHRtkOs+BedGDPpOV/28KgkLb0XOan/3c0NpzWNlN # yI/2GTfwUzxETKvdpycLHGTbPC1qz9mICOOeQF0M8tJvmQEVZPU= # SIG # End signature block |