mirror of
https://github.com/Micke-K/IntuneManagement.git
synced 2026-09-28 19:05:38 +02:00
IntuneManagement 4.0.0-beta1
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
#region Policy sub-resource orchestrator
|
||||
#
|
||||
# Drives the per-class GetSubResourceBatchRequests / ApplySubResourceBatchResult
|
||||
# contract defined on IntunePolicyBase. The contract lets a policy class declare
|
||||
# the extra Graph GETs it needs to be fully hydrated beyond the main object body
|
||||
# (relationships, script content, etc.) so the hydrator pipeline can fan them
|
||||
# out via $batch in parallel instead of paying one round-trip per policy.
|
||||
#
|
||||
# Phase loop:
|
||||
# 1. Phase 1 requests come from policy.GetSubResourceBatchRequests(1).
|
||||
# 2. Identical URLs across policies are coalesced into ONE batch sub-request
|
||||
# and the single response is fanned out to every (policy, key) that asked
|
||||
# for it (e.g. two AppConfig policies referencing the same mobileApp fetch
|
||||
# it once). Method + Headers must also match for coalescing.
|
||||
# 3. Each phase batch is dispatched via Invoke-GraphBatchRequest.
|
||||
# 4. Each response is routed back to its owning policy(ies) via
|
||||
# ApplySubResourceBatchResult($phase, $key, $body), which returns any
|
||||
# follow-up requests for phase+1.
|
||||
# 5. Loop continues until no policy returns more requests, or until $maxPhases.
|
||||
# 6. After all phases, FinalizeSubResources() runs once per opted-in policy so
|
||||
# classes can assemble derived properties from the applied responses.
|
||||
#
|
||||
# Opted-in policies are flagged via $_HasSubResourceBatch on the class. Default
|
||||
# is $false, so unchanged classes stay on the inline path until they migrate.
|
||||
|
||||
function Invoke-PolicySubresourceFetch
|
||||
{
|
||||
param(
|
||||
[Parameter(Mandatory)] $Policies,
|
||||
[int] $TokenId = 0
|
||||
)
|
||||
|
||||
if(-not $Policies) { return }
|
||||
$list = @($Policies)
|
||||
if($list.Count -eq 0) { return }
|
||||
|
||||
$optIn = [System.Collections.Generic.List[PSCustomObject]]::new()
|
||||
foreach($p in $list) {
|
||||
if($null -eq $p) { continue }
|
||||
if($p._HasSubResourceBatch -ne $true) { continue }
|
||||
if(-not $p.Id) { continue }
|
||||
# Clear any per-phase state from a previous hydrate of the same wrapper
|
||||
# (e.g. UI list-row re-open). Without this, partial state from a failed
|
||||
# phase-2 in a prior run would silently leak into the next hydrate's
|
||||
# ApplySubResourceBatchResult lookups.
|
||||
if($p.PSObject.Properties['_SubResourceState']) {
|
||||
$p._SubResourceState = $null
|
||||
}
|
||||
[void]$optIn.Add($p)
|
||||
}
|
||||
if($optIn.Count -eq 0) { return }
|
||||
|
||||
# Phase 1: ask every opted-in policy what it needs.
|
||||
$pending = [System.Collections.Generic.List[PSCustomObject]]::new()
|
||||
foreach($policy in $optIn) {
|
||||
try {
|
||||
$reqs = $policy.GetSubResourceBatchRequests(1)
|
||||
}
|
||||
catch {
|
||||
Write-LogError "Policy sub-resource: GetSubResourceBatchRequests on $($policy.GetType().Name) ($($policy.Name)) threw" $_.Exception
|
||||
continue
|
||||
}
|
||||
foreach($r in @($reqs)) {
|
||||
if($r -and $r.Url -and $r.Key) {
|
||||
[void]$pending.Add([PSCustomObject]@{ Policy = $policy; Request = $r })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if($pending.Count -eq 0) {
|
||||
foreach($p in $optIn) { $p._IsFullObject = $true }
|
||||
return
|
||||
}
|
||||
|
||||
Write-Log "Policy sub-resource: $($pending.Count) phase-1 request(s) across $($optIn.Count) policy(ies)"
|
||||
|
||||
$maxPhases = 5
|
||||
$globalIdx = 0
|
||||
|
||||
for($phase = 1; $phase -le $maxPhases -and $pending.Count -gt 0; $phase++) {
|
||||
|
||||
$batchObjects = [System.Collections.Generic.List[PSCustomObject]]::new()
|
||||
# reqId -> list of { Policy; Key } so one coalesced response fans out to
|
||||
# every requester. urlKey -> reqId so identical requests reuse one slot.
|
||||
$routing = @{}
|
||||
$reqIdByUrl = @{}
|
||||
|
||||
foreach($entry in $pending) {
|
||||
$req = $entry.Request
|
||||
$headers = if($req.Headers) { $req.Headers } else { @{ Accept = 'application/json;odata.metadata=minimal' } }
|
||||
$method = if($req.Method) { $req.Method } else { 'GET' }
|
||||
$url = ([string]$req.Url).TrimStart('/')
|
||||
|
||||
# Coalesce identical (method + url + headers) requests so a shared
|
||||
# sub-resource is fetched once. Headers are serialized into the key
|
||||
# so requests differing only by Accept aren't wrongly merged.
|
||||
$hdrKey = ($headers.GetEnumerator() | Sort-Object Name | ForEach-Object { "$($_.Name)=$($_.Value)" }) -join ';'
|
||||
$urlKey = "$method`n$url`n$hdrKey"
|
||||
|
||||
if($reqIdByUrl.ContainsKey($urlKey)) {
|
||||
$reqId = $reqIdByUrl[$urlKey]
|
||||
}
|
||||
else {
|
||||
$globalIdx++
|
||||
$reqId = "psrf_${phase}_${globalIdx}"
|
||||
$reqIdByUrl[$urlKey] = $reqId
|
||||
[void]$batchObjects.Add([PSCustomObject]@{
|
||||
id = $reqId
|
||||
method = $method
|
||||
url = $url
|
||||
headers = $headers
|
||||
})
|
||||
$routing[$reqId] = [System.Collections.Generic.List[PSCustomObject]]::new()
|
||||
}
|
||||
[void]$routing[$reqId].Add([PSCustomObject]@{ Policy = $entry.Policy; Key = [string]$req.Key })
|
||||
}
|
||||
|
||||
$batchType = "Hydrate:Subresource:Phase$phase"
|
||||
$results = @(Invoke-GraphBatchRequest -BatchObjects $batchObjects -BatchType $batchType -TokenId $TokenId -SkipWarnings -IncludedFailed)
|
||||
|
||||
$next = [System.Collections.Generic.List[PSCustomObject]]::new()
|
||||
|
||||
foreach($result in $results) {
|
||||
$routes = $routing["$($result.Id)"]
|
||||
if(-not $routes) { continue }
|
||||
|
||||
$body = $null
|
||||
if($result.Status -ge 200 -and $result.Status -lt 300) {
|
||||
$body = $result.body
|
||||
}
|
||||
|
||||
foreach($route in $routes) {
|
||||
if($null -eq $body) {
|
||||
Write-Log "Policy sub-resource: $($route.Policy.GetType().Name) ($($route.Policy.Name)) phase $phase key '$($route.Key)' returned HTTP $($result.Status)" 2
|
||||
}
|
||||
try {
|
||||
$followups = $route.Policy.ApplySubResourceBatchResult($phase, $route.Key, $body)
|
||||
}
|
||||
catch {
|
||||
Write-LogError "Policy sub-resource: ApplySubResourceBatchResult on $($route.Policy.GetType().Name) (phase=$phase, key=$($route.Key)) threw" $_.Exception
|
||||
continue
|
||||
}
|
||||
|
||||
foreach($f in @($followups)) {
|
||||
if($f -and $f.Url -and $f.Key) {
|
||||
[void]$next.Add([PSCustomObject]@{ Policy = $route.Policy; Request = $f })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$pending = $next
|
||||
}
|
||||
|
||||
if($pending.Count -gt 0) {
|
||||
# Hitting the phase cap means a class's ApplySubResourceBatchResult keeps
|
||||
# returning follow-ups indefinitely. Always a bug in the class — emit at
|
||||
# error level so it surfaces in the log filter, not just verbose noise.
|
||||
Write-Log "Policy sub-resource: phase cap ($maxPhases) reached with $($pending.Count) request(s) still pending - runaway in a class implementation; data is incomplete" 3
|
||||
}
|
||||
|
||||
# Finalize pass: let each opted-in policy assemble derived properties from
|
||||
# the responses already applied (e.g. AppConfiguration's #CustomRefTargetedApps).
|
||||
# Every IntunePolicyBase inherits FinalizeSubResources; the method-existence
|
||||
# guard keeps the orchestrator tolerant of stand-in objects that only
|
||||
# implement the request/apply half of the contract.
|
||||
foreach($p in $optIn) {
|
||||
if(-not $p.PSObject.Methods['FinalizeSubResources']) { continue }
|
||||
try {
|
||||
$p.FinalizeSubResources()
|
||||
}
|
||||
catch {
|
||||
Write-LogError "Policy sub-resource: FinalizeSubResources on $($p.GetType().Name) ($($p.Name)) threw" $_.Exception
|
||||
}
|
||||
}
|
||||
|
||||
foreach($p in $optIn) {
|
||||
$p._IsFullObject = $true
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
Reference in New Issue
Block a user