Private/State.ps1
|
function Read-State { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [VSphereConnectorConfiguration]$Config ) $statePath = Get-StatePath -Config $Config if (-not (Test-Path -Path $statePath)) { $default = New-State Save-State -State $default -Config $Config return $default } try { $raw = Get-Content -Path $statePath -Raw $parsed = $raw | ConvertFrom-Json Assert-StateObject -ParsedState $parsed $lastReceivedUtc = ConvertTo-StateUtcTimestamp -Value $parsed.watermarks.last_received_utc -FieldName 'watermarks.last_received_utc' $rawLastSentUtc = $parsed.watermarks.last_sent_utc $lastSentUtc = if ($null -eq $rawLastSentUtc -or ($rawLastSentUtc -is [string] -and [string]::IsNullOrWhiteSpace($rawLastSentUtc))) { '' } else { ConvertTo-StateUtcTimestamp -Value $rawLastSentUtc -FieldName 'watermarks.last_sent_utc' } $state = [VSphereConnectorState]::new() $state.schema_version = '1.0' $state.last_update_utc = [DateTime]::UtcNow.ToString('o') $watermarks = [VSphereConnectorStateWatermarks]::new() $watermarks.last_received_utc = $lastReceivedUtc $watermarks.last_sent_utc = $lastSentUtc $state.watermarks = $watermarks Write-CustomLog -Message "Loaded state from: $statePath" -Severity 'INFO' return $state } catch { Write-CustomLog -Message "Failed to load state. Path=$statePath Error=$($_.Exception.Message)" -Severity 'ERROR' throw } } function Assert-StateObject { [CmdletBinding()] param( [object]$ParsedState ) if ($null -eq $ParsedState) { $errorMsg = "Invalid state: state file parsed to null" Write-CustomLog -Message $errorMsg -Severity 'ERROR' throw $errorMsg } if ([string]::IsNullOrWhiteSpace([string]$ParsedState.schema_version)) { $errorMsg = "Invalid state: 'schema_version' is required." Write-CustomLog -Message $errorMsg -Severity 'ERROR' throw $errorMsg } if ([string]$ParsedState.schema_version -ne '1.0') { $errorMsg = "Invalid state: unsupported schema_version '$($ParsedState.schema_version)'." Write-CustomLog -Message $errorMsg -Severity 'ERROR' throw $errorMsg } if ($null -eq $ParsedState.watermarks) { $errorMsg = "Invalid state: 'watermarks' section is required." Write-CustomLog -Message $errorMsg -Severity 'ERROR' throw $errorMsg } } function Get-StatePath { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [VSphereConnectorConfiguration]$Config ) $stateDir = Join-Path -Path $Config.ScriptRootPath -ChildPath (Join-Path -Path 'State' -ChildPath $Config.EnvironmentConfig.Name) return (Join-Path -Path $stateDir -ChildPath 'state.json') } function New-State { [CmdletBinding()] param() $now = [DateTime]::UtcNow $state = [VSphereConnectorState]::new() $state.schema_version = '1.0' $state.last_update_utc = $now.ToString('o') $watermarks = [VSphereConnectorStateWatermarks]::new() $watermarks.last_received_utc = $now.AddMinutes(-5).ToString('o') $watermarks.last_sent_utc = '' $state.watermarks = $watermarks return $state } function Save-State { [CmdletBinding()] param( [VSphereConnectorState]$State, [Parameter(Mandatory = $true)] [VSphereConnectorConfiguration]$Config ) try { $path = Get-StatePath -Config $Config $stateDir = Split-Path -Path $path -Parent if (-not (Test-Path -Path $stateDir)) { New-Item -Path $stateDir -ItemType Directory -Force | Out-Null Write-CustomLog -Message "Created state directory: $stateDir" -Severity 'INFO' } if ($null -eq $State -or $null -eq $State.watermarks) { throw "State object is missing required 'watermarks' section." } $now = [DateTime]::UtcNow $stateToSave = [VSphereConnectorState]::new() $stateToSave.schema_version = '1.0' $stateToSave.last_update_utc = $now.ToString('o') $watermarks = [VSphereConnectorStateWatermarks]::new() $watermarks.last_received_utc = [string]$State.watermarks.last_received_utc $watermarks.last_sent_utc = [string]$State.watermarks.last_sent_utc $stateToSave.watermarks = $watermarks $json = $stateToSave | ConvertTo-Json -Depth 10 $tempPath = "$path.tmp-$([Guid]::NewGuid().ToString('N'))" Set-Content -Path $tempPath -Value $json -Encoding utf8 Move-Item -Path $tempPath -Destination $path -Force Write-CustomLog -Message "Saved state to: $path" -Severity 'INFO' } catch { Write-CustomLog -Message "Failed to save state. Path=$path Error=$($_.Exception.Message)" -Severity 'ERROR' throw } } function ConvertTo-StateUtcTimestamp { param( [object]$Value, [string]$FieldName ) $normalized = if ($Value -is [DateTime]) { ([DateTime]$Value).ToUniversalTime().ToString('o') } elseif ($Value -is [DateTimeOffset]) { ([DateTimeOffset]$Value).ToUniversalTime().ToString('o') } else { [string]$Value } if ([string]::IsNullOrWhiteSpace($normalized)) { $errorMsg = "Invalid state: '$FieldName' is required." Write-CustomLog -Message $errorMsg -Severity 'ERROR' throw $errorMsg } if (-not $normalized.EndsWith('Z')) { $errorMsg = "Invalid state: '$FieldName' must be in UTC (RFC3339 with 'Z'). Value: '$normalized'" Write-CustomLog -Message $errorMsg -Severity 'ERROR' throw $errorMsg } try { [void](ConvertFrom-RfcUtcTimestamp -Value $normalized) } catch { $errorMsg = "Invalid state: '$FieldName' must be a valid RFC3339 datetime. Value: '$normalized'" Write-CustomLog -Message $errorMsg -Severity 'ERROR' throw $errorMsg } return $normalized } function Test-StateHasPendingData { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [VSphereConnectorState]$State ) if ([string]::IsNullOrWhiteSpace($State.watermarks.last_sent_utc)) { return $true } $lastReceivedUtc = ConvertFrom-RfcUtcTimestamp -Value $State.watermarks.last_received_utc $lastSentUtc = ConvertFrom-RfcUtcTimestamp -Value $State.watermarks.last_sent_utc return ($lastReceivedUtc -gt $lastSentUtc) } |