Modules/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1
|
function Invoke-IdleStepTriggerDirectorySync { <# .SYNOPSIS Triggers a directory sync cycle and optionally waits for completion. .DESCRIPTION This is a provider-agnostic step. The host must supply a provider instance via Context.Providers[<ProviderAlias>] that implements: - StartSyncCycle(PolicyType, AuthSession) - GetSyncCycleState(AuthSession) The step is designed for remote execution and requires an elevated auth session provided by the host's AuthSessionBroker. Authentication: - With.AuthSessionName (required): routing key for AuthSessionBroker - With.AuthSessionOptions (optional, hashtable): forwarded to broker for session selection - ScriptBlocks in AuthSessionOptions are rejected (security boundary) .PARAMETER Context Execution context created by IdLE.Core. .PARAMETER Step Normalized step object from the plan. Must contain a 'With' hashtable with keys: - AuthSessionName (required, string): auth session name for broker - PolicyType (required, string): 'Delta' or 'Initial' (case-insensitive) - Provider (optional, string): provider alias, defaults to 'DirectorySync' - Wait (optional, bool): wait for cycle completion, defaults to $false - TimeoutSeconds (optional, int): wait timeout, defaults to 600 - PollIntervalSeconds (optional, int): poll interval, defaults to 10 - AuthSessionOptions (optional, hashtable): forwarded to broker .OUTPUTS PSCustomObject (PSTypeName: IdLE.StepResult) .EXAMPLE $step = @{ Name = 'Trigger directory sync' Type = 'IdLE.Step.TriggerDirectorySync' With = @{ AuthSessionName = 'DirectorySync' PolicyType = 'Delta' Wait = $true } } #> [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateNotNull()] [object] $Context, [Parameter(Mandatory)] [ValidateNotNull()] [object] $Step ) $with = $Step.With if ($null -eq $with -or -not ($with -is [hashtable])) { throw "TriggerDirectorySync requires 'With' to be a hashtable." } # Validate required inputs if (-not $with.ContainsKey('AuthSessionName')) { throw "TriggerDirectorySync requires With.AuthSessionName." } if (-not $with.ContainsKey('PolicyType')) { throw "TriggerDirectorySync requires With.PolicyType." } $policyType = [string]$with.PolicyType if ($policyType -notin @('Delta', 'Initial')) { throw "TriggerDirectorySync: With.PolicyType must be 'Delta' or 'Initial' (case-insensitive). Got: $policyType" } # Optional inputs with defaults $providerAlias = if ($with.ContainsKey('Provider')) { [string]$with.Provider } else { 'DirectorySync' } $wait = if ($with.ContainsKey('Wait')) { [bool]$with.Wait } else { $false } $timeoutSeconds = if ($with.ContainsKey('TimeoutSeconds')) { [int]$with.TimeoutSeconds } else { 600 } $pollIntervalSeconds = if ($with.ContainsKey('PollIntervalSeconds')) { [int]$with.PollIntervalSeconds } else { 10 } # Validate timeout and poll interval if ($timeoutSeconds -le 0) { throw "TriggerDirectorySync: With.TimeoutSeconds must be greater than 0. Got: $timeoutSeconds" } if ($pollIntervalSeconds -le 0) { throw "TriggerDirectorySync: With.PollIntervalSeconds must be greater than 0. Got: $pollIntervalSeconds" } # Validate provider exists if (-not ($Context.PSObject.Properties.Name -contains 'Providers')) { throw "Context does not contain a Providers hashtable." } if ($null -eq $Context.Providers -or -not ($Context.Providers -is [hashtable])) { throw "Context.Providers must be a hashtable." } if (-not $Context.Providers.ContainsKey($providerAlias)) { throw "Provider '$providerAlias' was not supplied by the host." } $stepName = if ($Step.PSObject.Properties.Name -contains 'Name') { [string]$Step.Name } else { 'TriggerDirectorySync' } try { # Trigger sync cycle $Context.EventSink.WriteEvent('DirectorySyncTriggered', "Triggering $policyType sync cycle", $stepName, @{ PolicyType = $policyType }) $startResult = Invoke-IdleProviderMethod ` -Context $Context ` -With $with ` -ProviderAlias $providerAlias ` -MethodName 'StartSyncCycle' ` -MethodArguments @($policyType) $changed = $false if ($null -ne $startResult -and ($startResult.PSObject.Properties.Name -contains 'Started')) { $changed = [bool]$startResult.Started } # If wait is requested, poll until complete or timeout if ($wait) { $Context.EventSink.WriteEvent('DirectorySyncWaiting', "Waiting for sync cycle to complete (timeout: ${timeoutSeconds}s)", $stepName, @{ TimeoutSeconds = $timeoutSeconds PollIntervalSeconds = $pollIntervalSeconds }) $startTime = [datetime]::UtcNow $attempt = 0 while ($true) { $attempt++ $elapsed = ([datetime]::UtcNow - $startTime).TotalSeconds if ($elapsed -ge $timeoutSeconds) { # Timeout reached - fail $stateResult = Invoke-IdleProviderMethod ` -Context $Context ` -With $with ` -ProviderAlias $providerAlias ` -MethodName 'GetSyncCycleState' ` -MethodArguments @() $lastState = if ($null -ne $stateResult) { $stateResult.State } else { 'Unknown' } $Context.EventSink.WriteEvent('DirectorySyncFailed', "Sync cycle wait timeout after ${timeoutSeconds}s", $stepName, @{ TimeoutSeconds = $timeoutSeconds ElapsedSeconds = [int]$elapsed LastKnownState = $lastState }) throw "TriggerDirectorySync: Timeout waiting for sync cycle to complete after ${timeoutSeconds}s. Last known state: $lastState" } # Poll state $stateResult = Invoke-IdleProviderMethod ` -Context $Context ` -With $with ` -ProviderAlias $providerAlias ` -MethodName 'GetSyncCycleState' ` -MethodArguments @() $inProgress = $true if ($null -ne $stateResult -and ($stateResult.PSObject.Properties.Name -contains 'InProgress')) { $inProgress = [bool]$stateResult.InProgress } $currentState = if ($null -ne $stateResult) { $stateResult.State } else { 'Unknown' } $Context.EventSink.WriteEvent('DirectorySyncPoll', "Poll attempt $attempt - State: $currentState", $stepName, @{ Attempt = $attempt State = $currentState InProgress = $inProgress ElapsedSeconds = [int]$elapsed }) if (-not $inProgress) { # Sync cycle completed $Context.EventSink.WriteEvent('DirectorySyncCompleted', "Sync cycle completed", $stepName, @{ Attempts = $attempt ElapsedSeconds = [int]$elapsed }) break } # Wait before next poll Start-Sleep -Seconds $pollIntervalSeconds } } else { # Not waiting - sync triggered successfully $Context.EventSink.WriteEvent('DirectorySyncCompleted', "Sync cycle triggered (not waiting)", $stepName, @{ PolicyType = $policyType }) } return [pscustomobject]@{ PSTypeName = 'IdLE.StepResult' Name = $stepName Type = [string]$Step.Type Status = 'Completed' Changed = $changed Error = $null } } catch { $Context.EventSink.WriteEvent('DirectorySyncFailed', "Failed to trigger or wait for sync cycle: $_", $stepName, @{ Error = $_.Exception.Message }) throw } } |