Private/Convert-AzLocalSideloadCatalogSchemaVersion.ps1
|
function Convert-AzLocalSideloadCatalogSchemaVersion { <# .SYNOPSIS Migrates a sideload-catalog.yml file from an older schema version to the module's current schema version (or any chosen target). Text-surgery based - operator comments, SBE (OEM) entries, and row order are preserved verbatim. .DESCRIPTION Sibling of Convert-AzLocalScheduleSchemaVersion.ps1 (the schedule-file migrator) and follows the IDENTICAL design so the two schema frameworks behave the same way. The function walks the per-hop recipe table registered in this file (see $script:SideloadCatalogSchemaRecipes below). Each recipe is a ScriptBlock with signature: param([string]$Text) -> @{ Text = <new>; Changes = @(<strings>) } Recipes operate on RAW TEXT, not on the parsed structure. This is deliberate so operator-authored YAML comments, manually-staged SBE (OEM) package entries, and download/TODO annotations survive every migration hop. Each recipe is expected to be IDEMPOTENT: running it twice on the same input must produce the same output, and MUST update the top-level 'schemaVersion:' line itself. Walker behaviour (identical to the schedule migrator): * If $current == $target -> return Migrated=$false (no-op). * If $current > $target -> throw (downgrade requested; bad). * If $current < $target -> walk recipes $current -> $current+1 -> ... -> $target. If any hop is missing from the table, throw. The recipe table ships EMPTY at schema v1: there is no hop to run yet, so a v1 catalog is always a no-op. The framework exists so the FIRST non-additive catalog format change is a small, well-tested recipe addition rather than a from-scratch build. Backup-on-write is performed by the caller (Update-AzLocalSideloadCatalog -SchemaMigrate) - this function only computes the new text and reports what changed. .PARAMETER Text Raw YAML text of the customer's catalog file. .PARAMETER TargetSchemaVersion Schema version to migrate TO. Default: this module's current ($script:SideloadCatalogSchemaCurrentVersion). Tests can override to exercise specific hops. .PARAMETER SourcePath Optional path used in error messages. .OUTPUTS [PSCustomObject] with: Migrated [bool] FromVersion [int] ToVersion [int] NewText [string] - migrated YAML text (or original if no-op) Hops [object[]] - one row per executed recipe: { FromVersion, ToVersion, Changes[] } #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter(Mandatory = $true)] [AllowEmptyString()] [string]$Text, [Parameter(Mandatory = $false)] [int]$TargetSchemaVersion = $script:SideloadCatalogSchemaCurrentVersion, [Parameter(Mandatory = $false)] [string]$SourcePath = '<inline>' ) # Read the current schemaVersion straight from the text. The catalog has # no struct parser that surfaces schemaVersion, so a narrow regex is used. # A catalog with NO schemaVersion line is treated as v1 (back-compat: the # v1 reader ignored the field, so early files may omit it). $current = 1 $svMatch = [regex]::Match($Text, '(?m)^\s*schemaVersion\s*:\s*(\d+)\b') if ($svMatch.Success) { $current = [int]$svMatch.Groups[1].Value } if ($current -gt $TargetSchemaVersion) { throw "Convert-AzLocalSideloadCatalogSchemaVersion: '$SourcePath' is on schemaVersion=$current but this module only supports up to $TargetSchemaVersion. Upgrade the AzLocal.UpdateManagement module, then re-run Update-AzLocalSideloadCatalog -SchemaMigrate." } if ($current -eq $TargetSchemaVersion) { return [pscustomobject]@{ Migrated = $false FromVersion = $current ToVersion = $TargetSchemaVersion NewText = $Text Hops = @() } } $hops = New-Object System.Collections.Generic.List[object] $workingText = $Text for ($v = $current; $v -lt $TargetSchemaVersion; $v++) { $key = "$v->$($v + 1)" # $script:SideloadCatalogSchemaRecipes is an [ordered] dictionary, # which exposes .Contains() but NOT .ContainsKey() - the latter throws # MethodNotFound on System.Collections.Specialized.OrderedDictionary. if (-not $script:SideloadCatalogSchemaRecipes.Contains($key)) { throw "Convert-AzLocalSideloadCatalogSchemaVersion: no migration recipe registered for '$key'. The module is missing a hop - this is a bug; file at https://github.com/NeilBird/Azure-Local/issues." } $recipe = $script:SideloadCatalogSchemaRecipes[$key] $hopResult = & $recipe $workingText if (-not $hopResult.ContainsKey('Text') -or -not $hopResult.ContainsKey('Changes')) { throw "Convert-AzLocalSideloadCatalogSchemaVersion: recipe '$key' did not return the expected @{ Text=...; Changes=... } shape." } $workingText = [string]$hopResult.Text $hops.Add([pscustomobject]@{ FromVersion = $v ToVersion = $v + 1 Changes = @($hopResult.Changes) }) | Out-Null } return [pscustomobject]@{ Migrated = $true FromVersion = $current ToVersion = $TargetSchemaVersion NewText = $workingText Hops = $hops.ToArray() } } # ===================================================================== # Sideload-catalog schema migration recipes - dispatch table # ===================================================================== # Each value is a ScriptBlock with signature: # param([string]$Text) -> @{ Text = <new YAML>; Changes = @(<strings>) } # # Recipes MUST be idempotent (running twice = same output as running once) # and MUST update the top-level 'schemaVersion:' line themselves. # # The table ships EMPTY at schema v1 - there is no migration hop to run yet. # This is intentional: the framework is wired up (engine + version var + # reader guard + -SchemaMigrate entry point) so that the FIRST non-additive # catalog format change is a small, well-tested recipe addition. # # To add the first hop in a future module version: # 1. Bump $script:SideloadCatalogSchemaCurrentVersion in the .psm1. # 2. Append a recipe here with the new 'N->N+1' key (text-surgery, # idempotent, marker-keyed - see the schedule recipe table in # Private/Convert-AzLocalScheduleSchemaVersion.ps1 for a worked # example that preserves operator comments and rows verbatim). # 3. Add tests in Tests/AzLocal.UpdateManagement.Tests.ps1 for the new # hop in isolation AND chained from version 1. # ===================================================================== $script:SideloadCatalogSchemaRecipes = [ordered]@{} |