Public/Get-HDTMachineOverride.ps1

function Get-HDTMachineOverride {
    <#
        .SYNOPSIS
            Reads the per-machine variable override for a UUID, if one exists.
 
        .DESCRIPTION
            Source 2 of five: Control\machines\<UUID>.yaml, "the MDT-database
            equivalent, but file-based; a SQL or REST provider can be plugged in
            later behind the same interface". This is that provider's file
            implementation.
 
            NO FILE IS THE NORMAL CASE. Most machines have no override, so an
            absent file returns $null rather than throwing, and an empty -Uuid
            returns $null without touching the filesystem at all - the engine
            asks before it necessarily knows the UUID.
 
            A file that EXISTS but is wrong is a different matter and fails fast,
            naming the file. An override that silently does nothing
            is precisely the MDT-database debugging problem HDT exists to end, so
            an unknown key, a variable outside the HDT namespace or an attempt to
            assign an engine-owned _HDT* variable are all refused rather than
            ignored.
 
            The file is read through the injected IFileSystem, never Get-Content,
            so the whole path is provable with no share and no disk (PROJECT
            constraint 4).
 
            The returned variables are re-materialised into an ordered,
            case-insensitive dictionary for the same reason Import-HDTRuleDocument
            does it: resolution applies them in document order and looks them up
            without caring how the author spelled the case.
 
        .PARAMETER WorkspaceRoot
            The workspace root. The override is at
            <WorkspaceRoot>\Control\machines\<Uuid>.yaml.
 
        .PARAMETER Uuid
            The machine UUID, as HDTUUID reports it. Empty or $null returns $null.
            Matching is case-insensitive because Windows paths are.
 
        .PARAMETER FileSystem
            An IFileSystem - the real adapter in production, New-HDTFakeFileSystem
            in a test.
 
        .OUTPUTS
            System.Management.Automation.PSCustomObject with Path and Variable, or
            $null when there is no override. Path comes back with the variables
            because provenance names the file that supplied each value.
 
        .EXAMPLE
            $override = Get-HDTMachineOverride -WorkspaceRoot 'X:\Deploy' `
                -Uuid $fact['HDTUUID'] -FileSystem (New-HDTFileSystem)
 
        .EXAMPLE
            Resolve-HDTVariable -MachineOverride $override.Variable `
                -MachineOverridePath $override.Path -Fact $fact
 
            How the result is handed to the resolution engine.
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [string] $WorkspaceRoot,

        [Parameter(Mandatory = $true, Position = 1)]
        [AllowNull()]
        [AllowEmptyString()]
        [string] $Uuid,

        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [object] $FileSystem
    )

    Set-StrictMode -Version Latest
    $ErrorActionPreference = 'Stop'

    $supportedSchemaVersion = 1

    # A machine whose UUID is unknown has no override to find, and asking the
    # filesystem about '<root>\Control\machines\.yaml' would be a lie.
    if ([string]::IsNullOrWhiteSpace($Uuid)) {
        return $null
    }

    $control = Join-Path -Path $WorkspaceRoot -ChildPath 'Control'
    $machine = Join-Path -Path $control -ChildPath 'machines'
    $path = Join-Path -Path $machine -ChildPath ('{0}.yaml' -f $Uuid)

    if (-not $FileSystem.TestPath($path)) {
        return $null
    }

    $document = ConvertFrom-HDTYaml -Yaml $FileSystem.ReadAllText($path) -Path $path

    # -- the document ---------------------------------------------------------

    if ($null -eq $document) {
        $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $path `
                    -Message 'the file is empty. A machine override must declare schemaVersion and at least one variable.'))
    }

    if (-not ($document -is [System.Collections.IDictionary])) {
        $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $path `
                    -Message ("the document must be a mapping with schemaVersion and variables keys, but it is a {0}." -f $document.GetType().Name)))
    }

    foreach ($key in @($document.Keys)) {
        if (@('schemaVersion', 'variables') -notcontains [string] $key) {
            $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $path `
                        -Message ("'{0}' is not a key a machine override may declare. The allowed keys are schemaVersion and variables." -f $key)))
        }
    }

    if (-not $document.Contains('schemaVersion')) {
        $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $path `
                    -Message 'schemaVersion is missing. Every HDT document declares one; this engine understands schemaVersion 1.'))
    }

    $schemaVersion = $document['schemaVersion']
    if (-not (($schemaVersion -is [int]) -or ($schemaVersion -is [long]))) {
        $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $path `
                    -Message ("schemaVersion must be an integer, but it is '{0}'." -f $schemaVersion)))
    }

    $supported = $false
    try {
        $supported = Test-HDTSchemaVersion -SchemaVersion ([int] $schemaVersion) -Supported $supportedSchemaVersion
    } catch {
        $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $path `
                    -Message ("schemaVersion {0} is not a valid schema version. It must be 1 or greater." -f $schemaVersion)))
    }

    if (-not $supported) {
        $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $path `
                    -Message ("schemaVersion {0} is newer than this engine understands (schemaVersion {1}). Upgrade the engine rather than the workspace." -f $schemaVersion, $supportedSchemaVersion)))
    }

    # -- the variables --------------------------------------------------------

    if (-not $document.Contains('variables')) {
        $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $path `
                    -Message 'the variables key is missing. A machine override exists to set variables for this machine.'))
    }

    $declared = $document['variables']
    if (-not ($declared -is [System.Collections.IDictionary])) {
        $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $path `
                    -Message 'the variables key must be a mapping of variable name to value.'))
    }

    if (@($declared.Keys).Count -eq 0) {
        $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $path `
                    -Message 'the variables mapping is empty. Delete the file rather than shipping an override that sets nothing.'))
    }

    $variable = [System.Collections.Specialized.OrderedDictionary]::new([System.StringComparer]::OrdinalIgnoreCase)

    foreach ($key in @($declared.Keys)) {
        $name = [string] $key

        if ($name.StartsWith('_')) {
            $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $path `
                        -Message ("'{0}' is engine-owned and cannot be assigned. A variable named _HDT* is set by the engine and is read-only." -f $name)))
        }

        if ($name -cnotmatch '^HDT[A-Za-z0-9_]*$') {
            $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $path `
                        -Message ("'{0}' is not an HDT variable name. Every deployment variable is prefixed HDT; run Get-HDTVariableMap for the MDT translation." -f $name)))
        }

        $variable[$name] = $declared[$key]
    }

    return [pscustomobject] ([ordered] @{
            Path     = $path
            Variable = $variable
        })
}