Public/Update-AzLocalApplyUpdatesScheduleConfig.ps1
|
function Update-AzLocalApplyUpdatesScheduleConfig { <# .SYNOPSIS Schema-version-aware updater for an existing apply-updates-schedule.yml. Backs the old file up as <name>.v<oldVersion>.old.yml and writes the migrated content. Customer schedule rows + operator comments are preserved verbatim by every recipe. .DESCRIPTION Partner to New-AzLocalApplyUpdatesScheduleConfig (which writes a FRESH file). This cmdlet is the dedicated maintenance entry point for an EXISTING schedule file: * -SchemaMigrate (default): walks the per-hop recipes registered by the module's migration framework (Private/Convert-AzLocalScheduleSchemaVersion.ps1) from the file's current schemaVersion up to the module's current $script:ScheduleSchemaCurrentVersion. The original file is renamed to <name>.v<oldVersion>.old.yml in the same directory (keeping the .yml extension so editors / git diff continue to treat it as YAML), then the new content is written. If the file is ALREADY on the current schema version, the cmdlet logs that fact and exits without writing anything. * (Future) -MergeNewRings: re-discover the fleet via Resource Graph, append a new schedule row for every UpdateRing tag value present in the fleet that is not yet referenced by ANY existing schedule row. Reserved for v0.7.70 - not implemented in v0.7.69; the parameter is rejected with a clear message. Both modes are intentionally non-destructive: - The customer's cycleWeeks, cycleAnchor*, and ALL schedule rows are preserved by every migration recipe (recipes operate on raw text and only touch the top-level schemaVersion field plus any newly-introduced top-level fields). - The original file is never silently overwritten - either renamed to <name>.v<oldVersion>.old.yml (SchemaMigrate hop) or left untouched (no-op). - The backup file is left in your working tree. Source control (git diff <name>.v<oldVersion>.old.yml <name>) gives you a full review of what changed. Delete the backup once you have committed the migration. Supports -WhatIf / -Confirm. .PARAMETER Path Path to the existing schedule file. Must exist. .PARAMETER SchemaMigrate Run schema-version migration to the module's current schema version. This is the default action. .PARAMETER MergeNewRings Reserved for v0.7.70. Currently throws with a remediation message. .PARAMETER PassThru Emit a [PSCustomObject] describing what happened (Action, FromVersion, ToVersion, BackupPath, Hops[]). Default: no pipeline output, only Write-Log messages. .OUTPUTS Nothing by default. With -PassThru: [PSCustomObject]@{ Action = 'Migrated' | 'Unchanged-SchemaCurrent' Path = <full path> FromVersion = <int> ToVersion = <int> BackupPath = <string or $null> Hops = <object[]> } .EXAMPLE Update-AzLocalApplyUpdatesScheduleConfig -Path .\apply-updates-schedule.yml After 'Update-Module AzLocal.UpdateManagement', run this once to bring the schedule file up to the module's current schema. If no migration is needed it is a no-op. .EXAMPLE Update-AzLocalApplyUpdatesScheduleConfig -Path .\apply-updates-schedule.yml -PassThru | Format-List Same migration, but emit the structured result so it can be piped into change-control logs. .EXAMPLE Update-AzLocalApplyUpdatesScheduleConfig -Path .\apply-updates-schedule.yml -WhatIf Preview the migration without renaming the original or writing the new file. #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium', DefaultParameterSetName = 'SchemaMigrate')] [OutputType([PSCustomObject])] param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$Path, [Parameter(ParameterSetName = 'SchemaMigrate')] [switch]$SchemaMigrate, [Parameter(ParameterSetName = 'MergeNewRings')] [switch]$MergeNewRings, [Parameter()] [switch]$PassThru ) # ---- 0. Reject the v0.7.70-reserved switch ---------------------- if ($MergeNewRings) { throw "Update-AzLocalApplyUpdatesScheduleConfig: -MergeNewRings is reserved for v0.7.70 (fleet-drift detection). Use -SchemaMigrate (default) for schema version migration today, or call New-AzLocalApplyUpdatesScheduleConfig to regenerate the file from scratch." } # ---- 1. Path validation ---------------------------------------- if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { throw "Update-AzLocalApplyUpdatesScheduleConfig: file not found: '$Path'. To create a starter file, use 'New-AzLocalApplyUpdatesScheduleConfig -OutputPath <path>'." } $full = (Resolve-Path -LiteralPath $Path).Path $text = Get-Content -LiteralPath $full -Raw -ErrorAction Stop # ---- 2. Run the migrator --------------------------------------- Write-Log -Message "Reading $full and computing migration to schema version $script:ScheduleSchemaCurrentVersion..." -Level Info $result = Convert-AzLocalScheduleSchemaVersion -Text $text -TargetSchemaVersion $script:ScheduleSchemaCurrentVersion -SourcePath $full # ---- 3. No-op path: schema already current ---------------------- if (-not $result.Migrated) { Write-Log -Message "Schedule file is already on schemaVersion=$($result.ToVersion). No changes required." -Level Info if ($PassThru) { return [pscustomobject]@{ Action = 'Unchanged-SchemaCurrent' Path = $full FromVersion = $result.FromVersion ToVersion = $result.ToVersion BackupPath = $null Hops = @() } } return } # ---- 4. Migration path: backup + write ------------------------- # Backup naming: <basename>.v<oldVersion>.old.yml # - keeps the .yml extension so editors and 'git diff' still treat it as YAML # - .v<N>.old makes the relationship to the migrated file obvious # - placed in the same directory so a single 'git diff' shows both files $dir = [System.IO.Path]::GetDirectoryName($full) $base = [System.IO.Path]::GetFileNameWithoutExtension($full) $backupName = "$base.v$($result.FromVersion).old.yml" $backupPath = if ($dir) { Join-Path $dir $backupName } else { $backupName } if ((Test-Path -LiteralPath $backupPath) -and -not $WhatIfPreference) { # An earlier migration left a backup with this exact version label. # Refuse rather than overwrite the prior backup - this is most # commonly a sign that a previous migration didn't get committed. throw "Update-AzLocalApplyUpdatesScheduleConfig: backup target '$backupPath' already exists. A previous migration from version $($result.FromVersion) was not cleaned up. Review/commit/delete it, then re-run." } $changeSummary = ($result.Hops | ForEach-Object { "v$($_.FromVersion)->v$($_.ToVersion): $(($_.Changes -join '; '))" }) -join ' | ' $shouldMsg = "Migrate schemaVersion $($result.FromVersion) -> $($result.ToVersion). Backup '$([IO.Path]::GetFileName($full))' as '$backupName'. Changes: $changeSummary" if (-not $PSCmdlet.ShouldProcess($full, $shouldMsg)) { Write-Log -Message "WhatIf/Confirm declined: schedule file NOT modified. Computed migration was: $shouldMsg" -Level Info if ($PassThru) { return [pscustomobject]@{ Action = 'WhatIf' Path = $full FromVersion = $result.FromVersion ToVersion = $result.ToVersion BackupPath = $backupPath Hops = $result.Hops } } return } # Atomic-ish: rename original first so we never have two valid copies # at the canonical path. If the rename succeeds but the write fails, # the operator still has a working schedule file at <backupName>. Rename-Item -LiteralPath $full -NewName $backupName -ErrorAction Stop Write-Log -Message "Renamed original to: $backupPath" -Level Info try { [System.IO.File]::WriteAllText($full, $result.NewText, [System.Text.UTF8Encoding]::new($false)) } catch { # Roll the rename back so the operator is not left without a # schedule file in the canonical location. Write-Log -Message "Write of migrated content FAILED: $($_.Exception.Message). Rolling back the rename so the original is restored." -Level Error try { Rename-Item -LiteralPath $backupPath -NewName ([System.IO.Path]::GetFileName($full)) -ErrorAction Stop } catch { Write-Log -Message "ROLLBACK ALSO FAILED. Manual recovery needed: rename '$backupPath' back to '$full' by hand." -Level Error } throw } Write-Log -Message "Migrated $full to schemaVersion=$($result.ToVersion):" -Level Success foreach ($hop in $result.Hops) { Write-Log -Message " v$($hop.FromVersion) -> v$($hop.ToVersion):" -Level Info foreach ($c in $hop.Changes) { Write-Log -Message " + $c" -Level Info } } Write-Log -Message "Review the migration with: git diff -- ""$([IO.Path]::GetFileName($full))""" -Level Info Write-Log -Message "Once you have committed the new file, the backup '$backupName' can be removed." -Level Info if ($PassThru) { return [pscustomobject]@{ Action = 'Migrated' Path = $full FromVersion = $result.FromVersion ToVersion = $result.ToVersion BackupPath = $backupPath Hops = $result.Hops } } } |