Private/Convert-AzLocalFleetSettingsSchemaVersion.ps1

function Convert-AzLocalFleetSettingsSchemaVersion {
    <#
    .SYNOPSIS
        Upgrades fleet-settings.yml schema v1 or v2 text to schema v3.
    .DESCRIPTION
        Schema v1 receives the grouped tag-filter example. Schema v2 flat tag
        pairs are converted to named one-tag groups, preserving their OR intent.
        Existing comments, ordering, and line endings are preserved. Schema v3
        text is returned unchanged.
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory = $true)]
        [AllowEmptyString()]
        [string]$Text,

        [Parameter(Mandatory = $false)]
        [string]$SourcePath = '<inline>'
    )

    $matches = [regex]::Matches($Text, '(?m)^(\s*schemaVersion\s*:\s*)(\d+)(\s*(?:#.*)?)$')
    if ($matches.Count -eq 0) {
        $matches = [regex]::Matches($Text, '(?m)^(\s*#\s*schemaVersion\s*:\s*)(\d+)(\s*)$')
        if ($matches.Count -eq 0) {
            return [pscustomobject]@{
                Migrated    = $false
                FromVersion = $null
                ToVersion   = 3
                NewText     = $Text
                Reason      = 'NoSchemaDeclaration'
            }
        }
    }
    if ($matches.Count -ne 1) {
        throw "Convert-AzLocalFleetSettingsSchemaVersion: '$SourcePath' contains multiple schemaVersion declarations."
    }

    $currentVersion = [int]$matches[0].Groups[2].Value
    if ($currentVersion -gt 3) {
        throw "Convert-AzLocalFleetSettingsSchemaVersion: '$SourcePath' uses schemaVersion $currentVersion, which is newer than this module supports."
    }
    if ($currentVersion -eq 3) {
        return [pscustomobject]@{
            Migrated    = $false
            FromVersion = 3
            ToVersion   = 3
            NewText     = $Text
            Reason      = 'Current'
        }
    }
    if ($currentVersion -notin @(1, 2)) {
        throw "Convert-AzLocalFleetSettingsSchemaVersion: '$SourcePath' uses unsupported schemaVersion $currentVersion."
    }

    $match = $matches[0]
    $replacement = $match.Groups[1].Value + '3' + $match.Groups[3].Value
    $newText = $Text.Substring(0, $match.Index) + $replacement + $Text.Substring($match.Index + $match.Length)
    $newText = [regex]::Replace($newText, '(?im)(fleet settings \(schema version )\d+(\))', '${1}3${2}')
    $newText = [regex]::Replace($newText, '(?m)^(\s*#\s*AZLOCAL-FLEET-SETTINGS-SCHEMA-)V[12](\s*)$', '${1}V3${2}')

    $newline = if ($Text.Contains("`r`n")) { "`r`n" } else { "`n" }
    $settingsMarker = '# AZLOCAL-FLEET-SETTINGS-SCHEMA-V3'
    if ($currentVersion -eq 2 -and $newText -match '(?im)^\s*(?:#\s*)?clusterTagFilters\s*:') {
        $lines = [regex]::Split($newText, '\r?\n')
        $convertedLines = [System.Collections.Generic.List[string]]::new()
        $inTagFilters = $false
        $commentPrefix = ''
        foreach ($line in $lines) {
            $logicalLine = $line
            $lineCommentPrefix = ''
            if ($line -match '^(?<prefix>\s*#\s?)(?<content>.*)$') {
                $lineCommentPrefix = $Matches.prefix
                $logicalLine = $Matches.content
            }
            if ($logicalLine -match '^\s{2}clusterTagFilters\s*:') {
                $inTagFilters = $true
                $commentPrefix = $lineCommentPrefix
                [void]$convertedLines.Add($line)
                continue
            }
            if ($inTagFilters -and $logicalLine -match '^\S|^\s{2}\S') {
                $inTagFilters = $false
            }
            if ($inTagFilters -and $logicalLine -match '^\s{4}-\s*name\s*:\s*(.*?)\s*$') {
                $rawName = $Matches[1]
                [void]$convertedLines.Add($line)
                [void]$convertedLines.Add("${commentPrefix} tags:")
                [void]$convertedLines.Add("${commentPrefix} - name: $rawName")
                continue
            }
            if ($inTagFilters -and $logicalLine -match '^\s{6}value\s*:\s*(.*?)\s*$') {
                [void]$convertedLines.Add("${commentPrefix} value: $($Matches[1])")
                continue
            }
            [void]$convertedLines.Add($line)
        }
        $newText = $convertedLines -join $newline
    }
    elseif ($newText -notmatch '(?im)^\s*#?\s*clusterTagFilters\s*:') {
        $commentedSettings = @(
            $settingsMarker
            '# Optional global cluster admission policy. Tags in a group use AND; groups use OR.'
            '# Matching is exact and case-insensitive; a missing tag does not match its group.'
            '# Add these lines beneath your active scope block when enabling the policy:'
            '# clusterTagFilters:'
            '# - name: Production'
            '# tags:'
            '# - name: Environment'
            '# value: Production'
            '# - name: ManagedBy'
            '# value: CentralIT'
            '# - name: Test'
            '# tags:'
            '# - name: Environment'
            '# value: Test'
        ) -join $newline
        if ($newText.Length -gt 0 -and -not $newText.EndsWith($newline)) {
            $newText += $newline
        }
        $newText += $newline + $commentedSettings + $newline
    }

    return [pscustomobject]@{
        Migrated    = $true
        FromVersion = $currentVersion
        ToVersion   = 3
        NewText     = $newText
        Reason      = 'Upgraded'
    }
}