Error Handling in ForEach-Object -Parallel CMDLET.

Ramakrishna Abhijeet P 70 Reputation points
2024-05-30T09:39:05.6366667+00:00

I Use a PowerShell 7 Script to Validate Resource Group and its JSON contents DisplayName, ObjectID and Role Definition Name. My Script will use Parallel block to loop through the subscriptions and validate RG First and then foreach Role Definition it has to valid it first and then checks the given ObjectID is a valid SPN or Group or User.
Currently I'm using normal forEach-Object but it's taking too long to validate. I need a simple solution.

Note: Repo has Different Folders with Subscription Names in each folder consists of RG names as JSON files.

Windows Server PowerShell
Windows Server PowerShell
Windows Server: A family of Microsoft server operating systems that support enterprise-level management, data storage, applications, and communications.PowerShell: A family of Microsoft task automation and configuration management frameworks consisting of a command-line shell and associated scripting language.
5,537 questions
PowerShell
PowerShell
A family of Microsoft task automation and configuration management frameworks consisting of a command-line shell and associated scripting language.
2,552 questions
0 comments No comments
{count} votes

3 answers

Sort by: Most helpful
  1. Rich Matheisen 46,796 Reputation points
    2024-05-30T15:35:11.37+00:00

    Are you asking about error handling or collecting errors?

    Using the example below, you'll create a variable you can reference as $BadStuff that will either be scalar (if there's only one error reported) or an array (if multiple errors are reported). Note that in this example, "errors" are only "exceptions". If your scriptblock "handles" the error and doesn't die, but merely sends the results of any errors in the success stream, there won't be any "errors" to handle.

    $results = <collection> |
        ForEach-Object -Parallel {<code-that-does-the-work>} -ErrorVariable BadStuff
    

    P.S. The missing "$" in the -ErrorVariable value isn't a mistake.


  2. Neuvi Jiang 1,300 Reputation points Microsoft Vendor
    2024-05-31T07:15:31.9833333+00:00

    Hi Ramakrishna Abhijeet P,

    Thank you for posting in the Q&A Forums.

    The following is a sample script showing how to use ForEach-Object -Parallel to parallelize your validation tasks. This script assumes that you have a directory structure that contains JSON files, each containing a DisplayName, ObjectID, and role definition name.

    # Ensure you have the necessary modules
    Import-Module Az
    
    # Define the base directory containing subscription folders
    $baseDir = "C:\Path\To\Your\Repo"
    
    # Get all subscription folders
    $subscriptionFolders = Get-ChildItem -Path $baseDir -Directory
    
    # Function to validate a resource group
    function Validate-ResourceGroup {
        param (
            [string]$resourceGroupPath
        )
    
        # Read the JSON content from the file
        $jsonContent = Get-Content -Path $resourceGroupPath -Raw | ConvertFrom-Json
    
        $displayName = $jsonContent.DisplayName
        $objectId = $jsonContent.ObjectID
        $roleDefinitionName = $jsonContent.RoleDefinitionName
    
        # Validate DisplayName, ObjectID, and RoleDefinitionName
        $isValidDisplayName = $true # Replace with actual validation logic
        $isValidObjectId = $true # Replace with actual validation logic
        $isValidRoleDefinition = $true # Replace with actual validation logic
    
        # Output the validation result
        [PSCustomObject]@{
            DisplayName = $displayName
            ObjectID = $objectId
            RoleDefinitionName = $roleDefinitionName
            IsValidDisplayName = $isValidDisplayName
            IsValidObjectId = $isValidObjectId
            IsValidRoleDefinition = $isValidRoleDefinition
        }
    }
    
    # Process each subscription folder in parallel
    $subscriptionFolders | ForEach-Object -Parallel {
        param ($baseDir)
    
        $subscriptionName = $_.Name
        $resourceGroupFiles = Get-ChildItem -Path "$baseDir\$subscriptionName" -Filter *.json
    
        # Process each resource group file in the subscription folder
        $resourceGroupFiles | ForEach-Object {
            $result = Validate-ResourceGroup -resourceGroupPath $_.FullName
            Write-Output $result
        }
    } -ArgumentList $baseDir -ThrottleLimit 4 | Export-Csv -Path "ValidationResults.csv" -NoTypeInformation
    
    
    

    The above is the content of the code, you can according to the comments to make changes for your environment.

    Best regards

    NeuviJ

    ============================================

    If the Answer is helpful, please click "Accept Answer" and upvote it.


  3. Rich Matheisen 46,796 Reputation points
    2024-06-04T21:29:53.88+00:00

    Using the last file you posted, I went through the code and made some changes.

    Also, because you're collecting the output from the Foreach-Object -Parallel in $allErrors there's no need for anything to be thread-safe -- a simple array is all that's necessary.

    param(
        [CmdletBinding()]
        $dirName = 'C:\Infrastructure\Azure-RBAC-Exceptions',
        $environment = 'DEV'
    )
     
    Set-StrictMode -Version Latest
    $ErrorActionPreference = "Stop"
    $InformationPreference = "Continue"
     
    # Get all the subscription folders for the given environment
    $Subscriptionfolders = Get-ChildItem -Path $dirName -Directory
    if ($environment -eq 'DEV') {
        $subscriptions = $Subscriptionfolders | Where-Object { $_.Name -like "AGDEV_*" -and $_.Name -notlike "*IAM*" }
    }
    else {
        $subscriptions = $Subscriptionfolders | Where-Object { $_.Name -like "AG_*" -and $_.Name -notlike "*IAM*" }
    }
     
    # Validate each subscription in parallel
    [array]$allErrors = $subscriptions | 
        ForEach-Object -ThrottleLimit 4 -Parallel {
            [array]$localErrors = @()
            $SubscriptionName = $_.Name
            $subscriptionFolder = Join-Path -Path $using:dirName -ChildPath $SubscriptionName
    
            # what should happen if this fails?
            if (-NOT (Select-AzSubscription -Subscription $SubscriptionName -Verbose) ){  # should this use the -Current switch?
                $localErrors += "Subscription '$SubscriptionNam' does not exist"
            }
            else{
                foreach ($resourceGroupFile in (Get-ChildItem -Path $subscriptionFolder -File) ) {
                    $resourceGroup = $resourceGroupFile.BaseName
                    try {
                        Get-AzResourceGroup -Name $resourceGroup -ErrorAction Stop | Out-Null
                    }
                    catch {
                        $localErrors += "Resource group '$resourceGroup' does not exist."
                    }
            
                    $roleAssignments = Get-Content -Path $resourceGroupFile.FullName | ConvertFrom-Json
                    foreach ($roleAssignment in $roleAssignments) {
                        try {
                            Get-AzRoleDefinition -Name $roleAssignment.RoleDefinitionName | Out-Null`
                        }
                        catch {
                            $localErrors += "Role definition '$($roleAssignment.RoleDefinitionName)' does not exist."
            #                continue
                        }
            
                        $ADObject = $null
                        try {
                            $ADObject = Get-AzADServicePrincipal -ObjectId $roleAssignment.ObjectId -ErrorAction Stop
                        }
                        catch {
                            try {
                                $ADObject = Get-AzADGroup -ObjectId $roleAssignment.ObjectId -ErrorAction Stop
                            }
                            catch {
                                try {
                                    $ADObject = Get-AzADUser -ObjectId $roleAssignment.ObjectId -ErrorAction Stop
                                }
                                catch {
                                    $localErrors += "Object ID '$($roleAssignment.ObjectId)' for '$($roleAssignment.DisplayName)' does not exist in Active Directory."
                                    continue
                                }
                            }
                        }
            
                        if ($ADObject -and $ADObject.DisplayName -ne $roleAssignment.DisplayName) {
                            $localErrors += "Display name mismatch for Object ID '$($roleAssignment.ObjectId)': expected '$($roleAssignment.DisplayName)', found '$($ADObject.DisplayName)'."
                        }
                    }
                }
            }
        }
        return $localErrors # what do you want returned? An array or the array's contents?
    
    # why is any of the code below necessary?
    # the $allErrors variable should contain all the errors (or all the arrays of errors)
    
    # Add all errors to the concurrent bag
    $allErrors | ForEach-Object { $errorFindings.Add($_) }
     
    if ($errorFindings.Count -gt 0) {
        $errorFindings | ForEach-Object { Write-Error $_ }
    }
    

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.