Public/Invoke-RunbookRun.ps1
|
function Invoke-RunbookRun { <# .SYNOPSIS Runs a runbook on one or more specified tenants or untenanted. Supports both traditional snapshots and Configuration as Code (CaC) runbooks. .DESCRIPTION Runs a runbook on one or more specified tenants or untenanted. Scheduling is optional. The function automatically detects whether the project uses Configuration as Code (CaC) or traditional snapshots: - For CaC projects: Runs the runbook directly from a Git branch (defaults to the project's default branch) - For traditional projects: Runs a published runbook snapshot When using CaC runbooks, if no BranchName is specified, the function will automatically use the project's default branch. .PARAMETER Runbook The runbook to run. Can be a RunbookResource object or a string that will be transformed to a RunbookResource. This parameter is used for Configuration as Code (CaC) projects. When specified, the runbook will be run directly from a Git branch. If no BranchName is provided, the project's default branch will be used automatically. This parameter is mandatory when using the 'Runbook' parameter set. .PARAMETER RunbookSnapshot The runbook snapshot to run for traditional (non-CaC) projects. This parameter is used to run a published snapshot of a runbook from projects that do not use Configuration as Code. This parameter is mandatory when using the 'Snapshot' parameter set. .PARAMETER BranchName Optional. The Git branch name to use for Configuration as Code projects. Can be either: - Short branch name (e.g., 'main', 'develop') - Canonical branch name (e.g., 'refs/heads/main') If not specified for CaC projects, the project's default branch will be used automatically. This parameter is ignored for traditional (non-CaC) projects. .PARAMETER Tenant Optional. One or more tenants for which to run the runbook. Only valid if the project's TenantedDeploymentMode is set to 'Tenanted' or 'TenantedOrUntenanted'. Each tenant will be validated to ensure it is connected to the specified project and environment before execution. .PARAMETER Environment Required. The environment in which to run the runbook. Must be a valid environment in the current Octopus space. .PARAMETER QueueTime Optional. Schedule the runbook run to start at a specific date and time. .PARAMETER ExpiryInMin Optional. Number of minutes until the scheduled run expires. Default is 60 minutes. Only applicable when QueueTime is specified. .PARAMETER FormValue Optional. A hashtable of form values (prompted variables) to pass to the runbook. The key should be the variable name or ID, and the value is the variable value to use during the runbook run. .PARAMETER Force Optional. Bypass confirmation prompts. .EXAMPLE PS C:\> Invoke-RunbookRun -Runbook "test git runbook" -Environment Test Runs a CaC runbook using the project's default branch (auto-detected), untenanted. .EXAMPLE PS C:\> Invoke-RunbookRun -Runbook "testgitrunbook" -Environment Test -BranchName "test" Runs a CaC runbook from the 'test' branch, untenanted. .EXAMPLE PS C:\> Invoke-RunbookRun -Runbook "test git runbook" -Environment Production -BranchName "refs/heads/main" -Tenant XXROM001 Runs a CaC runbook from the 'main' branch (using canonical name) for a specific tenant. .EXAMPLE PS C:\> Invoke-RunbookRun -RunbookSnapshot "RunbookSnapshots-1541" -Tenant XXROM001 -Environment Production Runs a traditional runbook snapshot for a specific tenant in the Production environment. .EXAMPLE PS C:\> Invoke-RunbookRun -RunbookSnapshot (Get-RunbookSnapshot -Runbook "Maintenance" -Latest) -Environment Test Runs the latest published runbook snapshot (for traditional projects) in the Test environment, untenanted. .EXAMPLE PS C:\> Invoke-RunbookRun -Runbook "MaintenanceRunbook" -Tenant XXROM001, XXROM002 -Environment Production -FormValue @{'VariableName' = 'value'} Runs a CaC runbook for multiple tenants and sets a prompted variable value. .EXAMPLE PS C:\> Invoke-RunbookRun -Runbook "ScheduledTask" -Environment Production -QueueTime (Get-Date).AddHours(2) -ExpiryInMin 120 Schedules a CaC runbook to run in 2 hours with an expiry time of 120 minutes. .NOTES - Requires an active connection to Octopus Deploy (use Connect-Octopus first) - The function uses parameter sets to distinguish between CaC runbooks (Runbook parameter) and traditional snapshots (RunbookSnapshot parameter) - For CaC projects, if no BranchName is specified, the project's default branch is used automatically - The BranchName parameter is only applicable to CaC projects and will be ignored for traditional projects - Tenant support depends on the project's TenantedDeploymentMode setting (Tenanted, Untenanted, or TenantedOrUntenanted) - When using the Runbook parameter, the project must be a Configuration as Code project - When using the RunbookSnapshot parameter, the project must be a traditional (non-CaC) project - The Force parameter bypasses the confirmation prompt (ConfirmImpact is set to 'High') #> [CmdletBinding(DefaultParameterSetName = 'Runbook', SupportsShouldProcess = $true, PositionalBinding = $false, HelpUri = 'http://www.google.com/', ConfirmImpact = 'High')] param ( # If only runbook is provided then the published snapshot should be used # Parameter help description [Parameter(Mandatory = $true, ParameterSetName = 'Runbook')] [RunbookSingleTransformation()] [Octopus.Client.Model.RunbookResource] $Runbook, [Parameter(Mandatory = $true, ParameterSetName = 'Snapshot')] [RunbookSnapshotSingleTransformation()] [Octopus.Client.Model.RunbookSnapshotResource] $RunbookSnapshot, # Git branch name for Configuration as Code runbooks (optional - defaults to project default branch) [Parameter(Mandatory = $false, ParameterSetName = 'Runbook')] [String] $BranchName, [Parameter(Mandatory = $false)] [TenantTransformation()] [Octopus.Client.Model.TenantResource[]] $Tenant, [Parameter(Mandatory = $true)] [EnvironmentSingleTransformation()] [Octopus.Client.Model.EnvironmentResource] $Environment, # property help [Parameter(Mandatory = $false)] [Datetime] $QueueTime, # property help [Parameter(Mandatory = $false)] [Int16] $ExpiryInMin = 60, # Formvalue. Accepts a dictionary with the variable id as key and value as value [Parameter(Mandatory = $false)] [Hashtable]$FormValue, [Parameter(Mandatory = $false)] [switch] $Force ) begin { try { ValidateConnection } catch { $PSCmdlet.ThrowTerminatingError($_) } } process { if ($PSCmdlet.ParameterSetName -eq 'snapshot') { $Runbook = Get-Runbook -ID $RunbookSnapshot.RunbookId -ErrorAction Stop $project = Get-Project -ID $runbookSnapshot.ProjectId -ErrorAction Stop } if ($PSCmdlet.ParameterSetName -eq 'Runbook') { $project = Get-Project -ID $Runbook.ProjectId -ErrorAction Stop # There is an edge case where the project id CaC but the runbook is not # this is the case when the runbook has not Link if ($project.isVersionControlled -and $Runbook.Links.count -gt 0) { Write-Verbose "Runbook '$($Runbook.Name)' is a Configuration as Code runbook." } elseif (-not $project.isVersionControlled) { Write-Verbose "Project '$($project.name)' is not a Configuration as Code project. Use the RunbookSnapshot parameter to run a traditional runbook snapshot." $message = "'{0}' is not a Configuration as Code project. Use the RunbookSnapshot parameter to run a traditional runbook snapshot." -f $project.name $myError = Get-CustomError -Message $message -Category InvalidData -Exception System.ArgumentException $PSCmdlet.WriteError($myError) return } else { Write-Verbose "Runbook '$($Runbook.Name)' is not a Configuration as Code runbook. Use the RunbookSnapshot parameter to run a traditional runbook snapshot." $message = "'{0}' is not a Configuration as Code runbook. Use the RunbookSnapshot parameter to run a traditional runbook snapshot." -f $Runbook.Name $myError = Get-CustomError -Message $message -Category InvalidData -Exception System.ArgumentException $PSCmdlet.WriteError($myError) return } } # Validate tenant mode if ($Tenant) { # check if project supports tenanted deployments if ($project.TenantedDeploymentMode -eq 'Untenanted') { $message = "'{0}' does not support tenanted deployments" -f $project.name $myError = Get-CustomError -Message $message -Category InvalidData -Exception System.ArgumentException $PSCmdlet.WriteError($myError) return } } else { # check if project supports untenanted deployments if ($project.TenantedDeploymentMode -eq 'Tenanted') { $message = "'{0}' does not support untenanted deployments" -f $project.name $myError = Get-CustomError -Message $message -Category InvalidData -Exception System.ArgumentException $PSCmdlet.WriteError($myError) return } } if ($PSCmdlet.ParameterSetName -eq 'snapshot') { # Traditional Project Path - use snapshot-based execution Write-Verbose "Project '$($project.name)' uses traditional runbook snapshots" # Prepare ShouldProcess messages $shouldMessage1 = "Run runbook snapshot '{0}' in environment '{1}'" -f $runbookSnapshot.Name, $environment.Name $shouldMessage2 = "Run {0}/{1}" -f $project.Name, $runbook.Name if ($force -or $PSCmdlet.ShouldProcess($shouldMessage1, $shouldMessage2)) { # Create a new runbook run object $runbookRun = [Octopus.Client.Model.RunbookRunResource]::new() $runbookRun.EnvironmentId = $environment.Id $runbookRun.ProjectId = $RunbookSnapshot.ProjectId $runbookRun.RunbookSnapshotId = $RunbookSnapshot.ID $runbookRun.RunbookId = $RunbookSnapshot.RunbookId if ($QueueTime) { $runbookRun.QueueTime = $QueueTime $runbookRun.QueueTimeExpiry = $QueueTime.AddMinutes($ExpiryInMin) } # Add variables to runbook run if passed in if ($FormValue) { foreach ($key in $FormValue.keys) { $runbookRun.FormValues.Add($key, $FormValue[$key]) } } if ($Tenant) { # Run tenanted runbook for each tenant foreach ($_tenant in $Tenant) { # Validate tenant is connected to project environment if (! ($_tenant.ProjectEnvironments[$Project.id] -contains $Environment.Id)) { $message = "'{0}' is not connected to '{1}' in '{2}'" -f $_tenant.name, $Project.name, $Environment.name try { throw $message } catch { $PSCmdlet.WriteError($_) continue } } $runbookRun.TenantId = $_tenant.id try { Write-Verbose "Running traditional runbook snapshot for tenant '$($_tenant.Name)'" return $repo._repository.RunbookRuns.Create($runbookRun) } catch { $PSCmdlet.WriteError($_) } } } else { # Execute runbook without tenant try { Write-Verbose "Running traditional runbook snapshot (untenanted)" return $repo._repository.RunbookRuns.Create($runbookRun) } catch { $PSCmdlet.WriteError($_) } } } } # Branch logic based on project type if ($PSCmdlet.ParameterSetName -eq 'Runbook') { # Resolve Git branch $branches = Get-GitBranch -Project $Project if ($BranchName) { # Filter to find the matching branch by name or canonical name $selectedBranch = $branches | Where-Object { $_.Name -eq $BranchName -or $_.CanonicalName -eq $BranchName } if (-not $selectedBranch) { $availableBranches = ($branches | ForEach-Object { $_.Name }) -join ', ' $message = "Project '$($Project.name)' has no branch called '$BranchName'. Available branches: $availableBranches" $myError = Get-CustomError -Message $message -Category InvalidData -Exception System.ArgumentException $PSCmdlet.ThrowTerminatingError($myError) } Write-Verbose "Using branch '$($selectedBranch.CanonicalName)' to run runbook" } else { # Use default branch $selectedBranch = $branches | Where-Object { $_.IsDefault -eq $true } if (-not $selectedBranch) { $message = "Project '$($Project.name)' has no default branch configured" $myError = Get-CustomError -Message $message -Category InvalidData -Exception System.InvalidOperationException $PSCmdlet.ThrowTerminatingError($myError) } Write-Verbose "Using default branch '$($selectedBranch.CanonicalName)' to run runbook" } # Get the runbook slug $runbookSlug = $runbook.Slug # Prepare ShouldProcess messages $shouldMessage1 = "Run runbook '{0}' from branch '{1}' in environment '{2}'" -f $runbook.Name, $selectedBranch.Name, $environment.Name $shouldMessage2 = "Run {0}/{1}" -f $project.Name, $runbook.Name if ($force -or $PSCmdlet.ShouldProcess($shouldMessage1, $shouldMessage2)) { if ($Tenant) { # Run tenanted runbook for each tenant foreach ($_tenant in $Tenant) { # Validate tenant is connected to project environment if (! ($_tenant.ProjectEnvironments[$Project.id] -contains $Environment.Id)) { $message = "'{0}' is not connected to '{1}' in '{2}'" -f $_tenant.name, $Project.name, $Environment.name try { throw $message } catch { $PSCmdlet.WriteError($_) continue } } # Create GitRunbookRunParameters for this tenant $gitRunParams = [Octopus.Client.Model.GitRunbookRunParameters]::new($environment.Id) $gitRunParams.TenantId = $_tenant.Id if ($QueueTime) { $gitRunParams.QueueTime = $QueueTime $gitRunParams.QueueTimeExpiry = $QueueTime.AddMinutes($ExpiryInMin) } # Add form values if provided if ($FormValue) { foreach ($key in $FormValue.Keys) { $gitRunParams.FormValues.Add($key, $FormValue[$key]) } } # Create wrapper RunGitRunbookParameters $runbookRunParams = [Octopus.Client.Model.RunGitRunbookParameters]::new($environment.Id) $runbookRunParams.Runs = @($gitRunParams) try { Write-Verbose "Running CaC runbook '$runbookSlug' for tenant '$($_tenant.Name)'" $repo._repository.Runbooks.Run($project, $selectedBranch.CanonicalName, $runbookSlug, $runbookRunParams) } catch { $PSCmdlet.WriteError($_) } } } else { # Execute untenanted runbook # Create GitRunbookRunParameters (no tenant) $gitRunParams = [Octopus.Client.Model.GitRunbookRunParameters]::new($environment.Id) if ($QueueTime) { $gitRunParams.QueueTime = $QueueTime $gitRunParams.QueueTimeExpiry = $QueueTime.AddMinutes($ExpiryInMin) } # Add form values if provided if ($FormValue) { foreach ($key in $FormValue.Keys) { $gitRunParams.FormValues.Add($key, $FormValue[$key]) } } # Create wrapper RunGitRunbookParameters $runbookRunParams = [Octopus.Client.Model.RunGitRunbookParameters]::new($environment.Id) $runbookRunParams.Runs = @($gitRunParams) try { Write-Verbose "Running CaC runbook '$runbookSlug' (untenanted)" $repo._repository.Runbooks.Run($project, $selectedBranch.CanonicalName, $runbookSlug, $runbookRunParams) } catch { $PSCmdlet.WriteError($_) } } } } } end {} } |