Actions/Engine/DscV3.ps1

<#
.SYNOPSIS
Engine action: evaluate a resource through dsc.exe (DSC v3 — cross-platform).
 
.DESCRIPTION
Drives Microsoft's DSC v3 command-line engine (`dsc resource <verb>`), which runs on
Windows, Linux and macOS. This is what makes hosted Linux/macOS agents useful, since
Invoke-DscResource is Windows / PowerShell-DSC-v2 first.
 
Behavior:
  - maps Test/Set/Get to the dsc resource sub-command,
  - passes the desired-state Property hashtable as JSON on --input,
  - surfaces a non-zero dsc.exe exit as a thrown error, so the runner records the
    resource as failed (feeding the exit-code work in a later phase),
  - parses the JSON result into the normalized engine contract shape.
 
.PARAMETER Context
A hashtable with keys: Method ('Test'|'Set'|'Get'), ModuleName, Name, Property.
 
.OUTPUTS
An object exposing InDesiredState, RebootRequired, Message and Raw (normalized by the
runner into a [DscMethodResult]).
#>

param(
    [hashtable]$Context = @{}
)

# DSC v3 resource types are namespaced 'Owner/Resource', matching the runner's task type.
$resourceType = '{0}/{1}' -f $Context.ModuleName, $Context.Name

# Fail fast on a type that is not even shaped like a DSC v3 identifier. Without this the
# only signal is an opaque non-zero exit from dsc.exe ('resource not found'); this points the
# operator straight at the compiled configuration. The check is structural (namespace/name);
# whether the type truly exists is still confirmed by dsc.exe itself below. Kept inline so the
# engine action stays self-contained (it is also driven directly by the CI smoke test).
$v3TypePattern = '^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)*/[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)*$'
if ($resourceType -notmatch $v3TypePattern) {
    throw "[Actions/Engine/DscV3] Resource type '$resourceType' is not a DSC v3 identifier. DSC v3 types are 'namespace/name' (for example 'Microsoft.Windows/Registry'); a compiled configuration targeting the DscV3 engine must use DSC v3 resource types."
}

$verb = switch ($Context.Method) {
    'Test' { 'test' }
    'Set'  { 'set' }
    'Get'  { 'get' }
    default { throw "[Actions/Engine/DscV3] Unsupported method '$($Context.Method)'." }
}

$property = $Context.Property
if ($null -eq $property) { $property = @{} }
$inputJson = $property | ConvertTo-Json -Depth 32 -Compress

$arguments = @('resource', $verb, '--resource', $resourceType, '--input', $inputJson)

Write-Verbose "[Actions/Engine/DscV3] dsc $($arguments -join ' ')"
$run = Invoke-DscExecutable -Arguments $arguments

if ($run.ExitCode -ne 0) {
    throw "[Actions/Engine/DscV3] 'dsc resource $verb' failed for [$resourceType] (exit $($run.ExitCode)): $($run.Output)"
}

$parsed = $null
if (-not [string]::IsNullOrWhiteSpace($run.Output)) {
    try {
        $parsed = $run.Output | ConvertFrom-Json
    }
    catch {
        throw "[Actions/Engine/DscV3] Could not parse dsc.exe output as JSON for [$resourceType]: $($_.Exception.Message)"
    }
}

# DSC v3 result shapes:
# test -> { desiredState, actualState, inDesiredState, differingProperties }
# set -> { beforeState, afterState, changedProperties }
# get -> { actualState } (or the state object directly)
$inDesiredState = $true
$message = $null

if ($Context.Method -eq 'Test') {
    if ($null -ne $parsed -and $null -ne $parsed.PSObject.Properties['inDesiredState']) {
        $inDesiredState = [bool]$parsed.inDesiredState
    }
    if ($null -ne $parsed -and $parsed.PSObject.Properties['differingProperties'] -and $parsed.differingProperties) {
        $message = "Differing properties: {0}" -f ($parsed.differingProperties -join ', ')
    }
}
elseif ($Context.Method -eq 'Set') {
    if ($null -ne $parsed -and $parsed.PSObject.Properties['changedProperties'] -and $parsed.changedProperties) {
        $message = "Changed properties: {0}" -f ($parsed.changedProperties -join ', ')
    }
}

return [pscustomobject]@{
    InDesiredState = $inDesiredState
    RebootRequired = $false
    Message        = $message
    Raw            = $parsed
}