xDscScript.psm1

$script:localizedData = ConvertFrom-StringData @'
GetTargetResourceStartVerboseMessage = Begin executing get script.
GetScriptThrewError = The get script threw an error.
GetScriptDidNotReturnHashtable = The get script did not return a hashtable.
GetTargetResourceEndVerboseMessage = End executing get script.
SetTargetResourceStartVerboseMessage = Begin executing set script.
SetScriptThrewError = The set script threw an error.
SetTargetResourceEndVerboseMessage = End executing set script.
TestTargetResourceStartVerboseMessage = Begin executing test script.
TestScriptThrewError = The test script threw an error.
TestScriptDidNotReturnBoolean = The test script did not return a boolean.
TestTargetResourceEndVerboseMessage = End executing test script.
ExecutingScriptMessage = Executing script: {0}
'@


[DscResource()]
class xDscScript {
    [DscProperty(Key)]
    [string] $GetScript

    [DscProperty(Key)]
    [string] $TestScript

    [DscProperty(Key)]
    [string] $SetScript
    
    # Gets the resource's current state.
    [xDscScript] Get() {
        $result = GetTargetResource -GetScript $this.GetScript -SetScript $this.SetScript -TestScript $this.TestScript
        return $this
    }
    
    # Sets the desired state of the resource.
    [void] Set() {
        SetTargetResource -GetScript $this.GetScript -SetScript $this.SetScript -TestScript $this.TestScript
    }
    
    # Tests if the resource is in the desired state.
    [bool] Test() {
        $result = TestTargetResource -GetScript $this.GetScript -SetScript $this.SetScript -TestScript $this.TestScript
        return $result
    }
}

function GetTargetResource {
    [OutputType([System.Collections.Hashtable])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $GetScript,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $SetScript,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $TestScript,

        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential
    )

    Write-Verbose -Message $script:localizedData.GetTargetResourceStartVerboseMessage

    $invokeScriptParameters = @{
        ScriptBlock = [System.Management.Automation.ScriptBlock]::Create($GetScript)
    }

    if ($PSBoundParameters.ContainsKey('Credential')) {
        $invokeScriptParameters['Credential'] = $Credential
    }

    $invokeScriptResult = Invoke-Script @invokeScriptParameters

    if ($invokeScriptResult -is [System.Management.Automation.ErrorRecord]) {
        New-InvalidOperationException -Message $script:localizedData.GetScriptThrewError -ErrorRecord $invokeScriptResult
    }

    $invokeScriptResultAsHashTable = $invokeScriptResult -as [System.Collections.Hashtable]

    if ($null -eq $invokeScriptResultAsHashTable) {
        New-InvalidArgumentException -ArgumentName 'TestScript' -Message $script:localizedData.GetScriptDidNotReturnHashtable
    }

    Write-Verbose -Message $script:localizedData.GetTargetResourceEndVerboseMessage

    return $invokeScriptResultAsHashTable
}

function SetTargetResource {
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $GetScript,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $SetScript,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $TestScript,

        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential
    )

    Write-Verbose -Message $script:localizedData.SetTargetResourceStartVerboseMessage

    $invokeScriptParameters = @{
        ScriptBlock = [System.Management.Automation.ScriptBlock]::Create($SetScript)
    }

    if ($PSBoundParameters.ContainsKey('Credential')) {
        $invokeScriptParameters['Credential'] = $Credential
    }

    $invokeScriptResult = Invoke-Script @invokeScriptParameters

    if ($invokeScriptResult -is [System.Management.Automation.ErrorRecord]) {
        New-InvalidOperationException -Message $script:localizedData.SetScriptThrewError -ErrorRecord $invokeScriptResult
    }

    Write-Verbose -Message $script:localizedData.SetTargetResourceEndVerboseMessage
}

<#
    .SYNOPSIS
        Runs the given test script.
        Should return true if the resource is in the desired state and false otherwise.
 
    .PARAMETER GetScript
        Not used in Test-TargetResource.
 
    .PARAMETER SetScript
        Not used in Test-TargetResource.
 
    .PARAMETER TestScript
        The script to validate whether or not the resource is in the desired state.
 
    .PARAMETER Credential
        The Credential to run the test script under if needed.
#>

function TestTargetResource {
    [OutputType([System.Boolean])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $GetScript,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $SetScript,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $TestScript,

        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential
    )

    Write-Verbose -Message $script:localizedData.TestTargetResourceStartVerboseMessage

    $invokeScriptParameters = @{
        ScriptBlock = [System.Management.Automation.ScriptBlock]::Create($TestScript)
    }

    if ($PSBoundParameters.ContainsKey('Credential')) {
        $invokeScriptParameters['Credential'] = $Credential
    }

    $invokeScriptResult = Invoke-Script @invokeScriptParameters

    # If the script is returing multiple objects, then we consider the last object to be the result of the script execution.
    if ($invokeScriptResult -is [System.Object[]] -and $invokeScriptResult.Count -gt 0) {
        $invokeScriptResult = $invokeScriptResult[$invokeScriptResult.Count - 1]
    }

    if ($invokeScriptResult -is [System.Management.Automation.ErrorRecord]) {
        New-InvalidOperationException -Message $script:localizedData.TestScriptThrewError -ErrorRecord $invokeScriptResult
    }

    if ($null -eq $invokeScriptResult -or -not ($invokeScriptResult -is [System.Boolean])) {
        New-InvalidArgumentException -ArgumentName 'TestScript' -Message $script:localizedData.TestScriptDidNotReturnBoolean
    }

    Write-Verbose -Message $script:localizedData.TestTargetResourceEndVerboseMessage

    return $invokeScriptResult
}

<#
    .SYNOPSIS
        Invokes the given script block.
 
        The output of the script will be returned unless the script throws an error.
        If the script throws an error, the ErrorRecord will be returned rather than thrown.
 
    .PARAMETER ScriptBlock
        The script block to invoke.
 
    .PARAMETER Credential
        The credential to run the script under if needed.
#>

function Invoke-Script {
    [OutputType([System.Object])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.Management.Automation.ScriptBlock]
        $ScriptBlock,

        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential
    )

    $scriptResult = $null

    try {
        Write-Verbose -Message ($script:localizedData.ExecutingScriptMessage -f $ScriptBlock)

        if ($null -ne $Credential) {
            $scriptResult = Invoke-Command -ScriptBlock $ScriptBlock -Credential $Credential -ComputerName .
        }
        else {
            $scriptResult = & $ScriptBlock
        }
    }
    catch {
        # Surfacing the error thrown by the execution of the script
        $scriptResult = $_
    }

    return $scriptResult
}

<#
    .SYNOPSIS
        Creates and throws an invalid argument exception.
 
    .PARAMETER Message
        The message explaining why this error is being thrown.
 
    .PARAMETER ArgumentName
        The name of the invalid argument that is causing this error to be thrown.
#>

function New-InvalidArgumentException {
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $Message,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $ArgumentName
    )

    $argumentException = New-Object -TypeName 'ArgumentException' `
        -ArgumentList @($Message, $ArgumentName)
    $newObjectParams = @{
        TypeName     = 'System.Management.Automation.ErrorRecord'
        ArgumentList = @($argumentException, $ArgumentName, 'InvalidArgument', $null)
    }
    $errorRecord = New-Object @newObjectParams

    throw $errorRecord
}

<#
    .SYNOPSIS
        Creates and throws an invalid operation exception.
 
    .PARAMETER Message
        The message explaining why this error is being thrown.
 
    .PARAMETER ErrorRecord
        The error record containing the exception that is causing this terminating
        error.
#>

function New-InvalidOperationException {
    [CmdletBinding()]
    param
    (
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $Message,

        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.ErrorRecord]
        $ErrorRecord
    )

    if ($null -eq $Message) {
        $invalidOperationException = New-Object -TypeName 'InvalidOperationException'
    }
    elseif ($null -eq $ErrorRecord) {
        $invalidOperationException = New-Object -TypeName 'InvalidOperationException' `
            -ArgumentList @( $Message )
    }
    else {
        $invalidOperationException = New-Object -TypeName 'InvalidOperationException' `
            -ArgumentList @( $Message, $ErrorRecord.Exception )
    }

    $newObjectParams = @{
        TypeName     = 'System.Management.Automation.ErrorRecord'
        ArgumentList = @( $invalidOperationException.ToString(), 'MachineStateIncorrect', 'InvalidOperation', $null )
    }

    $errorRecordToThrow = New-Object @newObjectParams
    throw $errorRecordToThrow
}