Worker friendly custom step templates

Shawn Sesna

Introduction

In September of 2018, Michael Richardson wrote a blog post introducing the feature of referencing packages within script steps. The second scenario described in "Why?" explains that prior to this feature, you needed to push the package to a target first, then the script task could execute against what was extracted. Michael goes on to explain that this method was both more complicated and wouldn't run on an Octopus server or a worker.

Custom Step Templates

Using Michael's idea, just like the run a script step, we have the ability to reference a package from within a Custom Step Template

We also have the ability to make the referenced package dynamic by assigning the package ID to a parameter of our step template.

This allows us to reference the extracted package files and do something with them:

$OctopusParameters["Octopus.Action.Package[$myPackageId].ExtractedPath"]

What About Specialized Software?

There are times when simply having the files available on a worker isn't enough to make them worker compatible such as deploying SSIS packages to a SQL server. The worker will have the .ispac file available to it, but it doesn't have the software installed to know what to do with it. One way to solve this is to install the software on all of your workers. This adds complexity in that all of your workers need to be maintained to make sure the right version of the software is installed and/or updated. Another method is to make use of the PowerShell Gallery to install the necessary PowerShell modules at deploy-time. For the SSIS example, the SqlServer module in the PowerShell Gallery contains the necessary .dlls needed to allow a worker to deploy the .ispac to SQL server.

The following scripts are provided for demonstration purposes.

Using the following code, we can check to see if the worker has the necessary modules installed. If the module is not available, download the specified version (latest if not specified) to a temporary folder and include it so that the cmdlets are available.

First, create our temporary folder within the current work folder:

# Define PowerShell Modules path
$LocalModules = (New-Item "$PSScriptRoot\Modules" -ItemType Directory -Force).FullName

Next, we'll add this folder to the PowerShell Module Path for this session:

# Add folder to the PowerShell Modules Path
$env:PSModulePath = "$LocalModules;$env:PSModulePath"

Now, let's define a function that will check to see if the module is installed:

function Get-ModuleInstalled
{
    # Define parameters
    param(
        $PowerShellModuleName
    )

    # Check to see if the module is installed
    if ($null -ne (Get-Module -ListAvailable -Name $PowerShellModuleName))
    {
        # It is installed
        return $true
    }
    else
    {
        # Module not installed
        return $false
    }
}

And then a function that will install it if it's missing:

function Install-PowerShellModule
{
    # Define parameters
    param(
        $PowerShellModuleName,
        $LocalModulesPath
    )

    # Save the module in the temporary location
    Save-Module -Name $PowerShellModuleName -Path $LocalModulesPath -Force

    # Display
    Write-Output "Importing module $PowerShellModuleName ..."

    # Import the module
    Import-Module -Name $PowerShellModuleName
}

Lastly, we'll define a function that will load the .dll(s) so their namespaces can be used:

If using this for the SqlServer module, you'll next to add an Exclude to the Get-ChildItem:

Get-ChildItem -Path $ModulePath -Exclude msv*.dll

Function Load-Assemblies
{
    # Declare parameters
    param(
        $PowerShellModuleName
    )

    # Get the folder where the module ended up in
    $ModulePath = [System.IO.Path]::GetDirectoryName((Get-Module $PowerShellModuleName).Path)

    # Loop through the assemblies
    foreach($assemblyFile in (Get-ChildItem -Path $ModulePath | Where-Object {$_.Extension -eq ".dll"}))
    {
        # Load the assembly
        [Reflection.Assembly]::LoadFile($assemblyFile.FullName) | Out-Null
    }    
}

Once those are defined, call our functions and install if necessary:

# Check to see if SqlServer module is installed
if ((Get-ModuleInstalled -PowerShellModuleName "SqlServer") -ne $true)
{
    # Display message
    Write-Output "PowerShell module SqlServer not present, downloading temporary copy ..."

    # Download and install temporary copy
    Install-PowerShellModule -PowerShellModuleName "SqlServer" -LocalModulesPath $LocalModules

    # Dependent assemblies
    Load-Assemblies -PowerShellModuleName "SqlServer"
}
else
{
    # Load the IntegrationServices Assembly
    [Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.Management.IntegrationServices") | Out-Null # Out-Null supresses a message that would normally be displayed saying it loaded out of GAC
}

For our SSIS example, our worker now has the necessary components available to it to deploy the .ispac file!

Summary

In this post, we learned how to reference a package from a custom step template, make the package ID reference dynamic by making it a step template parameter, and dynamically download and install PowerShell modules to make worker friendly templates.

Loading...