AzureAutomationTools.PackageManagement.psm1

Set-StrictMode -Version latest

$ErrorActionPreference = 'Stop'

Set-Variable -Scope Script -Name 'VariablesPropertyName' -Option 'Constant' -Value 'Variables'
Set-Variable -Scope Script -Name 'CredentialsPropertyName' -Option 'Constant' -Value 'Credentials'
Set-Variable -Scope Script -Name 'ConnectionsPropertyName' -Option 'Constant' -Value 'Connections'
Set-Variable -Scope Script -Name 'CertificatesPropertyName' -Option 'Constant' -Value 'Certificates'

$Script:Options = @{
    AssetsFolderName = 'assets'
    ModulesFolderName = 'modules'
    RunbooksFolderName = 'runbooks'
    AssetsFileName = 'assets.json'
    JsonAssetDepth = 4
    Encoding = 'UTF8'
}

$Script:WorkingFolder = ''
$Script:WorkingPackage = ''

class PackageTestResult {
    [ValidateNotNullOrEmpty()]
    [string] $Message
    [ValidateSet('Error', 'Warning')]
    [string] $Severity

    PackageTestResult (
        [string] $Message,
        [string] $Severity
    ) {
        $this.Message = $Message
        $this.Severity = $Severity
    }
}

function Get-AatWorkingFolder {
    $ret = $Script:WorkingFolder

    if (-not $Script:WorkingFolder) {
        throw "Working folder not set. Please set using Set-AatWorkingFolder."
    }

    $ret
}

function Set-AatWorkingFolder {
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$Path
    )

    if ($PSCmdlet.ShouldProcess("$Path", 'Set working folder' )) {
        $Script:WorkingFolder = (Resolve-Path -Path $Path).Path
    }
}

function Get-AatWorkingPackage {
    [CmdletBinding()]
    param()

    $ret = $Script:WorkingPackage

    if (-not $Script:WorkingPackage) {
        Write-Verbose -Message "No working package set - using first folder in $Script:WorkingFolder"
        $ret = (Get-ChildItem -Directory -Path (Get-AatWorkingFolder) | Sort-Object Name | Select-Object -First 1 -ExpandProperty Name)
    }

    $ret
}

function Set-AatWorkingPackage {
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [Alias('pn')]
        [string]$PackageName
    )

    if ($PSCmdlet.ShouldProcess("$PackageName", 'Set working package' )) {
        if (-not (Test-Path -Path (Join-path -Path (Get-AatWorkingFolder) -ChildPath $PackageName))) {
            throw "Cannot set working package to [$PackageName] - the folder does not exist in [$(Get-AatWorkingFolder)]"
        }

        $Script:WorkingPackage = $PackageName
    }
}

function Set-AatPackageOption {
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    param (
        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [Alias('afn')]
        [string]$AssetsFolderName,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [Alias('mfn')]
        [string]$ModulesFolderName,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [Alias('rfn')]
        [string]$RunbooksFolderName,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [string]$AssetsFileName,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [Alias('jad')]
        [int]$JsonAssetDepth,

        [Parameter(Mandatory = $false)]
        [ValidateSet('Unicode', 'UTF8')]
        [string]$Encoding
    )

    if ($PSCmdlet.ShouldProcess("", 'Set package option' )) {
        if ($AssetsFolderName) {
            $Script:Options.AssetsFolderName = $AssetsFolderName
        }

        if ($AssetsFileName) {
            $Script:Options.AssetsFileName = $AssetsFileName
        }

        if ($ModulesFolderName) {
            $Script:Options.ModulesFolderName = $ModulesFolderName
        }

        if ($RunbooksFolderName) {
            $Script:Options.RunbooksFolderName = $RunbooksFolderName
        }

        if ($AssetsFileName) {
            $Script:Options.AssetsFilter = $AssetsFileName
        }

        if ($JsonAssetDepth) {
            $Script:Options.JsonAssetDepth = $JsonAssetDepth
        }

        if ($Encoding) {
            $Script:Options.Encoding = $Encoding
        }
    }
}


function Get-AatPackageOption {
    [OutputType('System.Collections.HashTable')]
    [CmdletBinding()]
    param()

    $Script:Options
}

function Get-AatPackagePath {
    [CmdletBinding()]
    param(
        [Alias('pn')]
        [string]$PackageName
    )

    $WorkingFolderPath = Get-AatWorkingFolder

    if (-not $PackageName) {
        $PackageName = Get-AatWorkingPackage
    }

    $PackagePath = (Join-Path -Path $WorkingFolderPath -ChildPath $PackageName)
    $PackagePath
}

function Get-AatPackageFolderPath {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ParameterSetName = 'Assets')]
        [Alias('a')]
        [switch]$Assets,

        [Parameter(Mandatory = $true, ParameterSetName = 'Modules')]
        [Alias('m')]
        [switch]$Modules,

        [Parameter(Mandatory = $true, ParameterSetName = 'Runbooks')]
        [Alias('rb')]
        [switch]$Runbooks,

        [ValidateNotNullOrEmpty()]
        [Alias('pn')]
        [string]$PackageName
    )

    $ret = $null

    if (-not $PackageName) {
        $PackagePath = Get-AatPackagePath
    }
    else {
        $PackagePath = Get-AatPackagePath -PackageName $PackageName
    }


    $FolderName = $null

    switch ($true) {
        $Assets.IsPresent { $FolderName = $Script:Options.AssetsFolderName }
        $Modules.IsPresent { $FolderName = $Script:Options.ModulesFolderName }
        $Runbooks.IsPresent { $FolderName = $Script:Options.RunbooksFolderName }
        Default {throw 'Unknown package folder.'}
    }

    if ($FolderName -eq '.') {
        $ret = $PackagePath
    }
    else {
        $ret = (Join-Path -Path $PackagePath -ChildPath $FolderName)
    }

    $ret
}

function New-AatAutomationPackage {
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$Name,

        [Alias('dcaf')]
        [Switch]$DontCreateAssetsFile,

        [Alias('is')]
        [Switch]$IncludeSamples
    )

    if ($PSCmdlet.ShouldProcess("$Name", 'Create automation package' )) {
        $Path = Get-AatWorkingFolder

        Write-Verbose "Creating package folder [$Name] in '$Path' ..."

        $RootPath = (Join-Path -Path $Path -ChildPath $Name)

        if (Test-Path -Path $RootPath) {
            if ((Get-ChildItem -Path $RootPath)) {
                throw "Cannot create package folder - path [$RootPath] aleady exists and is not empty."
            }
        }
        else {
            New-Item -Path $RootPath -ItemType 'Directory' | Out-Null
        }

        Set-AatWorkingPackage -PackageName $Name

        $AssetsPath = Get-AatPackageFolderPath -Assets
        $ModulesPath = Get-AatPackageFolderPath -Modules
        $RunbooksPath = Get-AatPackageFolderPath -Runbooks

        $AssetsPath, $ModulesPath, $RunbooksPath | ForEach-Object {
            if ($_ -ne $RootPath) {
                Write-Verbose -Message "Creating folder '$_' ..."

                New-Item $_ -ItemType 'Directory' | Out-Null

                Write-Verbose -Message "Creating folder '$_' - done."
            }
        }

        if (-not $DontCreateAssetsFile.IsPresent) {
            New-AatAssetsFile -Name $Script:Options.AssetsFileName -IncludeVariables -IncludeCredentials
        }

        if ($IncludeSamples.IsPresent) {
            Write-Verbose "Creating samples ..."

            $SampleAssetsFileName = 'sample-assets.json'
            New-AatAssetsFile -Name $SampleAssetsFileName -All
            Add-AatPackageVariable -AssetsFileName $SampleAssetsFileName -Name 'simple-plaintext-variable'  -Value 'lorem'
            Add-AatPackageVariable -AssetsFileName $SampleAssetsFileName -Name 'simple-encrypted-variable'  -IsEncrypted
            $Object = @{
                SimpleProperty = 'Lorem'
                ComplexProperty = @{
                    Ipsum = 1
                    Dolor = 'monkey'
                }
            }
            Add-AatPackageVariable -AssetsFileName $SampleAssetsFileName -Name 'object-variable' $Object
            Write-Verbose "Creating samples - done."
        }

        Write-Verbose "Creating package folder [$Name] in '$Path' - done"
    }
}

function Test-AatAutomationPackage {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [Alias('pn')]
        [string]$PackageName,

        [Alias('iw')]
        [switch]$IgnoreWarnings
    )

    [PackageTestResult[]]$TestResults = @()
    $WorkingFolder = Get-AatWorkingFolder

    Write-Verbose -Message "Working folder: '$WorkingFolder)'"

    if (-not $PackageName) {
        $PackageName = Get-AatWorkingPackage
    }

    Write-Verbose -Message "Testing package: [$PackageName]"

    Write-Verbose -Message 'Testing package folders ...'

    $AssetsPath = Get-AatPackageFolderPath -Assets -PackageName $PackageName

    if (-not (Test-Path -Path $AssetsPath)) {
        $TestResults += New-Object -TypeName PackageTestResult -ArgumentList "Package folder [Assets] '$AssetsPath' not found", 'Error'
    }

    $ModulesPath = Get-AatPackageFolderPath -Modules -PackageName $PackageName

    if (-not (Test-Path -Path $ModulesPath)) {
        $TestResults += New-Object -TypeName PackageTestResult -ArgumentList "Package folder [Modules] '$ModulesPath' not found", 'Error'
    }

    $RunbooksPath = Get-AatPackageFolderPath -Runbooks -PackageName $PackageName

    if (-not (Test-Path -Path $RunbooksPath)) {
        $TestResults += New-Object -TypeName PackageTestResult -ArgumentList "Package folder [Runbooks] '$RunbooksPath' not found", 'Error'
    }

    Write-Verbose -Message "Testing package folders - done."

    Write-Verbose -Message "Testing assets ..."

    $AssetsFilePath = (Join-Path -Path $AssetsPath -ChildPath $Script:Options.AssetsFileName)

    if (Test-Path -Path $AssetsPath) {
        if (-not ((Test-Path $AssetsFilePath) -or $IgnoreWarnings.IsPresent)) {
            $TestResults += New-Object -TypeName PackageTestResult -ArgumentList "Assets file '$AssetsFilePath' not found", 'Warning'
        }

        Get-ChildItem -Path $AssetsPath -File | ForEach-Object {
            $AssetsFilePath = $_.FullName

            Write-Verbose "Testing assets file '$AssetsFilePath' ..."

            $Assets = Get-Content -Raw -Path $AssetsFilePath | ConvertFrom-Json -ErrorAction Ignore

            if (-not $Assets) {
                $TestResults += New-Object -TypeName PackageTestResult -ArgumentList "Assets file '$AssetsFilePath' does not contain valid json", 'Error'
            }
            else {
                [string[]]$ExpectedProperties = 'Certificates', 'Credentials', 'Variables', 'Connections'
                [string[]]$Properties = @()
                $Properties += $Assets.psobject.Properties | Select-Object -ExpandProperty Name
                $Intersection = (Compare-Object -ReferenceObject $ExpectedProperties -DifferenceObject $Properties -IncludeEqual -ExcludeDifferent)

                if (-not $Intersection) {
                    $TestResults += New-Object -TypeName PackageTestResult -ArgumentList "Assets file '$AssetsFilePath' does not contain any of the supported properties ('Certificates', 'Credentials', 'Variables', 'Connections')", 'Error'
                }

                $UnsupportedProperties = (Compare-Object -ReferenceObject $ExpectedProperties -DifferenceObject $Properties | Where-Object SideIndicator -EQ '=>')

                $UnsupportedProperties | ForEach-Object {
                    $TestResults += New-Object -TypeName PackageTestResult -ArgumentList "Assets file '$AssetsFilePath' contains unsupported property [$($_.InputObject)]", 'Error'
                }

                if ($Assets.psobject.properties | Where-Object Name -eq 'Variables') {
                    $i = 0

                    $Assets.Variables | ForEach-Object {
                        $i++
                        if (-not ($_.psobject.properties | Where-Object Name -eq 'Name')) {
                            $TestResults += New-Object -TypeName PackageTestResult -ArgumentList "Variable definition [$i] in '$AssetsFilePath' missing required property [Name]", 'Error'
                        }

                        if (-not ($_.psobject.properties | Where-Object Name -eq 'Value')) {
                            $TestResults += New-Object -TypeName PackageTestResult -ArgumentList "Variable definition [$i] in '$AssetsFilePath' missing required property [Value]", 'Error'
                        }

                        if (-not ($_.psobject.properties | Where-Object Name -eq 'IsEncrypted')) {
                            $TestResults += New-Object -TypeName PackageTestResult -ArgumentList "Variable definition [$i] in '$AssetsFilePath' missing required property [IsEncrypted]", 'Error'
                        }

                        ($_.psobject.properties | Where-Object Name -notin 'Name', 'Value', 'IsEncrypted' | Select-Object -ExpandProperty 'Name') | ForEach-Object {
                            $TestResults += New-Object -TypeName PackageTestResult -ArgumentList "Variable definition [$i] in '$AssetsFilePath' contains unsupported property [$_]", 'Error'
                        }
                    }

                    $Assets.Variables | Where-Object {$_.psobject.Properties.name -eq 'Name'} |  Select-Object -ExpandProperty 'Name' | Group-Object  | Where-Object Count -GT 1 | ForEach-Object {
                        $TestResults += New-Object -TypeName PackageTestResult -ArgumentList "Assets file '$AssetsFilePath' contains duplicate defintion of variable [$_]", 'Error'
                    }
                }
            }

            Write-Verbose "Testing assets file '$AssetsFilePath' - done."
        }
    }

    Write-Verbose -Message "Testing modules ..."

    if (Test-Path -Path $ModulesPath) {
        $ModuleFiles = (Get-ChildItem -Path $ModulesPath -File)

        if (-not $ModuleFiles) {
            if (-not $IgnoreWarnings.IsPresent) {
                $TestResults += New-Object -TypeName PackageTestResult -ArgumentList "No module defintions found in '$ModulesPath'", 'Warning'
            }
        }
        else {
            $ModuleFiles | ForEach-Object {
                $ModulesFilePath = $_.FullName
                $Modules = Get-Content -Raw -Path $ModulesFilePath | ConvertFrom-Json -ErrorAction Ignore

                if (-not $Modules) {
                    $TestResults += New-Object -TypeName PackageTestResult -ArgumentList "Modules file '$ModulesFilePath' does not contain valid json.", 'Error'
                }
            }
        }
    }

    Write-Verbose -Message "Testing modules - done."

    Write-Verbose -Message "Testing runbooks ..."

    if (Test-Path -Path $RunbooksPath) {
        $RunbookFiles = (Get-ChildItem -Path $RunbooksPath -File)

        if (-not $RunbookFiles) {
            if (-not $IgnoreWarnings.IsPresent) {
                $TestResults += New-Object -TypeName PackageTestResult -ArgumentList "No runbooks found in '$RunbooksPath'", 'Warning'
            }
        }
    }

    Write-Verbose -Message "Testing modules - done."

    $TestResults
}

function New-AatAssetsFile {
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    param(
        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [string]$Name,

        [Parameter(Mandatory = $false, ParameterSetName = 'ChooseAssets')]
        [Alias('vars')]
        [switch]$IncludeVariables,

        [Parameter(Mandatory = $false, ParameterSetName = 'ChooseAssets')]
        [Alias('creds')]
        [switch]$IncludeCredentials,

        [Parameter(Mandatory = $false, ParameterSetName = 'ChooseAssets')]
        [Alias('certs')]
        [switch]$IncludeCertificates,

        [Parameter(Mandatory = $false, ParameterSetName = 'ChooseAssets')]
        [Alias('conns')]
        [switch]$IncludeConnections,

        [Parameter(Mandatory = $true, ParameterSetName = 'AllAssets')]
        [switch]$All
    )
    if (-not $Name) {
        $Name = $Script:Options.AssetsFileName
    }

    if ($PSCmdlet.ShouldProcess("$Name", 'Create assets file' )) {
        $Assets = @{}

        if ($All.IsPresent -or $IncludeVariables.IsPresent) {
            $Assets.Add($Script:VariablesPropertyName, @())
        }

        if ($All.IsPresent -or $IncludeCredentials.IsPresent) {
            $Assets.Add($Script:CredentialsPropertyName, @())
        }

        if ($All.IsPresent -or $IncludeCertificates.IsPresent) {
            $Assets.Add($Script:CertificatesPropertyName, @())
        }

        if ($All.IsPresent -or $IncludeConnections.IsPresent) {
            $Assets.Add($Script:ConnectionsPropertyName, @())
        }

        $AssetsFilePath = Join-Path -Path (Get-AatPackageFolderPath -Assets) -ChildPath $Name

        Write-Verbose -Message "Creating assets file '$AssetsFilePath' ..."

        if (Test-Path $AssetsFilePath) {
            throw "Cannot create assets file - file '$AssetsFilePath' already exists."
        }
        else {
            $Assets | ConvertTo-Json -Depth ($Script:Options.JsonAssetDepth + 2) | Out-File -Encoding $Script:Options.Encoding  -FilePath $AssetsFilePath
        }

        Write-Verbose -Message "Creating assets file '$AssetsFilePath' - done."
    }
}

function New-AatPackageVariable {
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low')]
    param (
        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [string]$Name,

        [Parameter(Mandatory = $true, ParameterSetName = 'Plain')]
        [ValidateNotNullOrEmpty()]

        [Object]$Value,

        [Parameter(Mandatory = $true, ParameterSetName = 'Encrypted')]

        [switch]$IsEncrypted,

        [switch]$AsJson
    )

    if ($PSCmdlet.ShouldProcess("$Name", 'Create variable definition' )) {
        $Variable = [pscustomobject]@{
            Name = $Name
            IsEncrypted = $IsEncrypted.IsPresent
            Value = $Value
        }

        $ret = $Variable

        if ($AsJson.IsPresent) {
            $ret = $ret | ConvertTo-Json -Depth $Script:Options.JsonAssetDepth
        }

        $ret
    }
}

function Add-AatPackageVariable {
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low')]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$Name,

        [Parameter(Mandatory = $true, ParameterSetName = 'Plain')]
        [ValidateNotNullOrEmpty()]
        [object]$Value,

        [Parameter(Mandatory = $true, ParameterSetName = 'Encrypted')]
        [switch]$IsEncrypted,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [string]$AssetsFileName
    )

    if ($PSCmdlet.ShouldProcess("$Name", 'Add variable definition' )) {
        $NewVariableParams = @{
            Name = $Name
        }

        if ($PsCmdlet.ParameterSetName -eq 'Plain') {
            $NewVariableParams.Add('Value', $Value)
        }

        if ($PsCmdlet.ParameterSetName -eq 'Encrypted') {
            $NewVariableParams.Add('IsEncrypted', $IsEncrypted.IsPresent)
        }

        $Variable = New-AatPackageVariable @NewVariableParams

        Write-verbose -Message "Working folder: '$(Get-AatWorkingFolder)'"
        Write-verbose -Message "Working package: [$(Get-AatWorkingPackage)]"

        if (-not $AssetsFileName) {
            $AssetsFileName = $Script:Options.AssetsFileName
        }

        Write-verbose -Message "Assets file: [$AssetsFileName]"

        $AssetsFolderPath = (Get-AatPackageFolderPath -Assets)
        $AssetsFilePath = Join-Path -Path $AssetsFolderPath -ChildPath $AssetsFileName

        Write-verbose -Message "Assets file path: '$AssetsFilePath'"

        $Assets = Get-Content -Raw -Path $AssetsFilePath | ConvertFrom-Json

        if (($Assets.Variables | Where-Object Name -eq $Name)) {
            throw "Cannot add variable [$Name], '$AssetsFilePath' already contains a definition for [$Name]."
        }

        $Assets.Variables += $Variable
        $Json = $Assets | ConvertTo-Json -Depth ($Script:Options.JsonAssetDepth + 2)
        $Json | Out-File -FilePath $AssetsFilePath -Encoding $Script:Options.Encoding
    }
}

function DeployRunbooks {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$Path,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [ValidatePattern('^.*\.ps1$')]
        [string]$Filter
    )

    $RunbooksRoot = Join-Path -Path $Path -ChildPath 'runbooks'

    Write-Verbose -Message "Runbooks Root: '$RunbooksRoot'"

    if (-not (Test-Path $RunbooksRoot)) {
        throw "Path not found: '$RunbooksRoot'"
    }

    $CommonParameters = @{ResourceGroupName = $ResourceGroupName; AutomationAccountName = $AutomationAccountName}

    $Runbooks = Get-ChildItem -Path $RunbooksRoot -File -Filter $Filter

    if (-not $Runbooks) {
        Write-Warning -Message "No runbooks found in $RunbooksRoot matching $Filter"
    }

    $Runbooks | ForEach-Object {
        $CommonParameters

        $ExistingRunbooks = @()
        $ExistingRunbooks += (Get-AzureRmAutomationRunbook @CommonParameters| Select-Object Name) | ForEach-Object {$_.Name}

        if ($ExistingRunbooks.Contains($_.BaseName)) {
            Write-Output "Removing existing runbook $($_.BaseName) ..."

            Remove-AzureRmAutomationRunbook @CommonParameters -Name $_.BaseName -Force

            Write-Output "Removing existing runbook $($_.BaseName) - done."
        }

        Write-Output "Importing runbook $($_.BaseName) ..."

        Import-AzureRmAutomationRunbook -Name $_.BaseName -Type 'PowerShell' -Path $_.FullName -Published @CommonParameters

        Write-Output "Importing runbook $($_.BaseName) - done."
    }
}

function DeployAssets {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$Path,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [ValidatePattern('^.*\.json$')]
        [string]$Filter
    )

    $AssetsRoot = Join-Path -Path $Path -ChildPath 'assets'

    Write-Output "Assets Root: '$AssetsRoot'"
    Write-Output "Filter: [$Filter]"

    if (-not (Test-Path $AssetsRoot)) {
        throw "Path not found: '$AssetsRoot'"
    }

    $CommonParameters = @{ResourceGroupName = $ResourceGroupName; AutomationAccountName = $AutomationAccountName}

    Get-ChildItem -Path $AssetsRoot -File -Filter $Filter | ForEach-Object {
        Write-Output "Assets File: '$($_.FullName)'"
        #Write-Output (Get-Content $_.FullName -Raw)
        $Assets = Get-Content $_.FullName -Raw | ConvertFrom-Json

        $ExistingVariables = @()
        $ExistingVariables += (Get-AzureRmAutomationVariable @CommonParameters| Select-Object Name) | ForEach-Object {$_.Name}

        if ($DeployVariables) {
            $Assets.Variables | ForEach-Object {
                if ($_.IsEncrypted) {
                    $Value = 'totally not the value!'
                }
                elseif (('Int32', 'Boolean', 'String', 'Double').Contains($_.Value.GetType().Name)) {
                    $Value = $_.Value
                }
                else {
                    $Value = $_.Value | ConvertTo-Json -Depth $Script:Options.JsonAssetDepth
                }

                $Params = @{Name = $_.Name; Encrypted = $_.IsEncrypted; Value = $Value}
                $Params += $CommonParameters

                if ($ExistingVariables.Contains($_.Name)) {
                    Write-Output "Updating existing variable [$($_.Name)] ..."

                    Set-AzureRmAutomationVariable @Params

                    Write-Output "Updating existing variable [$($_.Name)] - done."

                }
                else {
                    Write-Output "Creating new variable [$($_.Name)] .."

                    New-AzureRmAutomationVariable @Params

                    Write-Output "Creating new variable [$($_.Name)] - done."
                }

                if ($_.IsEncrypted) {
                    Write-Warning "Password must be manually set for variable [$($_.Name)]"
                }
            }
        }

        if ($DeployCredentials) {
            if ($Assets.psobject.properties.Name -notcontains 'Credentials') {
                Write-Warning "No credentials found in [$($_.FullName)]."
            }
            else {
                $ExistingCredentials = @()
                $ExistingCredentials += (Get-AzureRmAutomationCredential @CommonParameters| Select-Object Name) | ForEach-Object {$_.Name}

                $Assets.Credentials | ForEach-Object {
                    # See https://github.com/PowerShell/PSScriptAnalyzer/issues/574
                    #$Password = [guid]::NewGuid() | ConvertTo-SecureString -asPlainText -Force
                    $Password = [SecureString]::new()
                    (New-Guid).Guid.ToCharArray() | ForEach-Object {$Password.AppendChar($_)} # i.e. definitely not the password
                    $Username = $_.Username
                    $Credential = New-Object System.Management.Automation.PSCredential($Username, $Password)
                    $Params = @{Name = $_.Name; Value = $Credential; }
                    $Params += $CommonParameters

                    if ($ExistingCredentials.Contains($_.Name)) {
                        if ($NewCredentialsOnly) {
                            Write-Output "Skipping existing credential [$($_.Name)]."
                        }
                        else {
                            Write-Output "Updating existing credential [$($_.Name)] ..."

                            Set-AzureRmAutomationCredential @Params

                            Write-Output "Updating existing credential [$($_.Name)] - done."
                            Write-Warning "Password must be manually set for credential $($_.Name)"
                        }
                    }
                    else {
                        Write-Output "Creating new credential [$($_.Name)] ..."

                        New-AzureRmAutomationCredential @Params

                        Write-Output "Creating new credential [$($_.Name)] - done."
                        Write-Warning "Password must be manually set for credential [$($_.Name)]"
                    }
                }
            }
        }
    }
}

function DeployModules {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$Path
    )

    $ModulesRoot = Join-Path -Path $Path -ChildPath 'modules'

    if (-not (Test-Path $ModulesRoot)) {
        throw "Path not found: '$ModulesRoot'"
    }

    Write-Output "Modules Root: '$ModulesRoot'"

    $ModulesFiles = Get-ChildItem -Path $ModulesRoot -File -Filter '*.json'

    if (-not $ModulesFiles) {
        Write-Warning -Message "No modules files found in $ModulesRoot matching '*.json'"
    }

    $CommonParameters = @{ResourceGroupName = $ResourceGroupName; AutomationAccountName = $AutomationAccountName};

    $ModulesFiles | ForEach-Object {
        Write-Output "Modules file: '$($_.FullName)'";
        $Modules = Get-Content $_.FullName -Raw | ConvertFrom-Json;
        $ExistingModules = (Get-AzureRmAutomationModule @CommonParameters| Select-Object 'Name', 'Version');

        foreach ($Module in $Modules) {
            $ContentLink = $Module.Package
            $Params = @{Name = $Module.Name; }
            $Params += $CommonParameters

            #FIX failing - version property is coming back blank
            if ($ExistingModules.Where( {$_.Name -eq $Module.Name -and $_.Version -eq $Module.Version})) {
                Write-Output "Existing module up to date - skipping."
            }
            elseif ($ExistingModules.Where( {$_.Name -eq $Module.Name })) {
                Write-Output "Updating existing module ..."

                # Need to use New- rather than Set- as workaround for Set- not working correctly - raised
                # as an issue: https://github.com/Azure/azure-powershell/issues/5064
                New-AzureRmAutomationModule -ContentLink $ContentLink @Params

                Write-Output "Updating existing module - done."
            }
            else {
                Write-Output "Adding new module ..."

                New-AzureRmAutomationModule -ContentLink $ContentLink @Params

                Write-Output "Adding new module - done."
            }
        }
    }
}

function Publish-AatAutomationPackage {
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High', DefaultParameterSetName = 'NamedPackages')]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [Alias('rg')]
        [string]$ResourceGroupName,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [Alias('aa')]
        [string]$AutomationAccountName,

        [Parameter(Mandatory = $false)]
        [Alias('dr')]
        [Switch]$DeployRunbooks,

        [Parameter(Mandatory = $false)]
        [Alias('dm')]
        [Switch]$DeployModules,

        [Parameter(Mandatory = $false)]
        [Alias('dv')]
        [Switch]$DeployVariables,

        [Parameter(Mandatory = $false)]
        [Alias('dc')]
        [Switch]$DeployCredentials,

        [Parameter(Mandatory = $false)]
        [Alias('nco')]
        [Switch]$NewCredentialsOnly,

        [Parameter(Mandatory = $false)]
        [ValidatePattern('^.+\.ps1$')]
        [Alias('rf')]
        [string]$RunbookFilter = '*.ps1',

        [Parameter(Mandatory = $false)]
        [ValidatePattern('^.+\.json$')]
        [Alias('aff')]
        [string]$AssetsFileFilter = '*.json',

        [Parameter(Mandatory = $false, ParameterSetName = 'NamedPackages')]
        [ValidateNotNullOrEmpty()]
        [Alias('pn')]
        [string[]]$PackageName,

        [Parameter(ParameterSetName = 'AllPackages')]
        [Alias('All')]
        [switch]$AllPackages
    )

    Write-Verbose -Message "Parameter set: $($PSCmdlet.ParameterSetName)"

    if ($PSCmdlet.ParameterSetName -eq 'NamedPackages') {
        if (-not $PackageName) {
            $PackageName = Get-AatWorkingPackage
        }
    }
    elseif ($PSCmdlet.ParameterSetName -eq 'AllPackages') {
        $PackageName = (Get-ChildItem -Path (Get-AatWorkingFolder) -Directory).Name
    }
    else {
        throw "Unexpected parameter set: $($PSCmdlet.ParameterSetName)"
    }

    Write-Verbose -Message "Parameters:"

    $PSBoundParameters.GetEnumerator() | ForEach-Object {
        Write-Verbose -Message "$($_.Key): $($_.Value)"
    }

    $DeployAssets = $DeployVariables -or $DeployCredentials

    if (-not ($DeployRunbooks.IsPresent -or $DeployModules.IsPresent -or $DeployAssets)) {
        Write-Warning -Message "Nothing to deploy, please specify -DeployRunbooks, -DeployModules, -DeployVariables, or -DeployCredentials"
    }
    elseif ($PSCmdlet.ShouldProcess("$ResourceGroupName/$AutomationAccountName", 'Publish automation packages')) {
        $Paths = $PackageName | ForEach-Object {
            $Path = Join-Path -Path (Get-AatWorkingFolder) -ChildPath $_ -Resolve
            Write-Verbose -Message "Will publish package: $_ ($Path)"
            $Path
        }

        $Paths | ForEach-Object {
            Write-Verbose -Message "Searching '$_'"

            if ($DeployRunbooks) {
                Write-Output "INFO: Deploying runbooks from '$_'."
                DeployRunbooks -Path $_ -Filter $RunbookFilter
                Write-Output "INFO: Deploying runbooks from '$_' - done."
            }

            if ($DeployModules) {
                Write-Output "INFO: Deploying modules from '$_'."
                DeployModules -Path $_
                Write-Output "INFO: Deploying modules from '$_'- done."
            }

            if ($DeployAssets) {
                Write-Output "INFO: Deploying assets from '$_'."
                DeployAssets -Path $_ -Filter $AssetsFileFilter
                Write-Output "INFO: Deploying assets from '$_' - done."
            }

            $CommonParameters = @{ResourceGroupName = $ResourceGroupName; AutomationAccountName = $AutomationAccountName}
            $VariableName = '~LastDeploymentTime'
            $Value = (Get-Date).ToUniversalTime().ToString('s') + 'Z'
            $Params = @{Name = $VariableName}
            $Params += $CommonParameters
            $Variable = (Get-AzureRmAutomationVariable @Params -ErrorAction 'Ignore')
            $Params += @{Encrypted = $false; Value = $Value}

            if (!$Variable) {
                New-AzureRmAutomationVariable @Params
            }
            else {
                Set-AzureRmAutomationVariable @Params
            }

            Write-Verbose -Message "Searching '$_' - done."
        }
    }
}