Public/Set-AzureUtilsTagFromResourceGroup.ps1

function Set-AzureUtilsTagFromResourceGroup {
    <#
        .SYNOPSIS
            Inherits resource-group tags down onto the resources inside each group.

        .DESCRIPTION
            For every resource in scope, compares its tags with its parent resource
            group's tags (read together in a single Azure Resource Graph query) and
            applies the group's tags onto the resource with Update-AzTag
            -Operation Merge. By default only tag keys the resource is MISSING are
            added (existing values are never touched); use -Overwrite to also replace
            values that differ from the group's. Restrict which keys are inherited
            with -Tag.

            Rows are grouped by subscription (the context is switched per group). This
            cmdlet changes Azure resources, so it supports -WhatIf and -Confirm.
            Requires Az.Resources (Update-AzTag). Messages are en-US.

        .PARAMETER SubscriptionId
            One or more subscription IDs. Default: every enabled subscription.

        .PARAMETER ManagementGroupId
            One or more management groups to scan (requires Az.ResourceGraph).

        .PARAMETER Tag
            Only inherit these tag keys. Default: every tag on the resource group.

        .PARAMETER Overwrite
            Also overwrite resource tag values that differ from the group's value.
            Without this switch, only missing tag keys are added.

        .PARAMETER Quiet
            Suppress the per-resource '[INFO] ...' lines (errors are still shown).

        .EXAMPLE
            Set-AzureUtilsTagFromResourceGroup -WhatIf

            Previews which resources would inherit which group tags.

        .EXAMPLE
            Set-AzureUtilsTagFromResourceGroup -Tag costCenter, environment -SubscriptionId $sub

            Inherits only those two keys from each resource group onto its resources.

        .LINK
            https://github.com/hendersonandrade/powershell-module-azureUtils

        .OUTPUTS
            None. Writes a console report.
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium', DefaultParameterSetName = 'Subscriptions')]
    param(
        [Parameter(ParameterSetName = 'Subscriptions')]
        [string[]] $SubscriptionId,

        [Parameter(ParameterSetName = 'ManagementGroup', Mandatory)]
        [string[]] $ManagementGroupId,

        [string[]] $Tag,

        [switch] $Overwrite,

        [switch] $Quiet
    )

    $null = Assert-AzureUtilsContext

    if (-not (Get-Command -Name 'Update-AzTag' -ErrorAction SilentlyContinue)) {
        throw [System.Management.Automation.RuntimeException]::new(
            "Set-AzureUtilsTagFromResourceGroup requires the 'Az.Resources' module. Install it with: Install-Module Az.Resources -Scope CurrentUser"
        )
    }

    $resolved = Resolve-AzureUtilsScope -SubscriptionId $SubscriptionId -ManagementGroupId $ManagementGroupId
    if ($resolved.Type -eq 'Subscription' -and -not $resolved.HasSubscriptions) {
        Write-Warning 'No accessible subscriptions in scope.'
        return
    }

    $query = New-AzureUtilsInheritedTagQuery
    $scope = $resolved.Scope
    Write-Verbose "Inherited-tag query:`n$query"

    # Build the { ResourceId, Tags } work list: for each resource, the group tags it
    # should inherit (missing keys, or differing keys when -Overwrite).
    $apply = [System.Collections.Generic.List[object]]::new()
    Invoke-AzureUtilsGraphQuery -Query $query @scope | ForEach-Object {
        $resTags = ConvertTo-AzureUtilsHashtable -InputObject $_.tags
        $rgTags  = ConvertTo-AzureUtilsHashtable -InputObject $_.rgTags

        $inherit = @{}
        foreach ($key in $rgTags.Keys) {
            if ($Tag -and ($Tag -notcontains $key)) { continue }
            $value = $rgTags[$key]
            if ($resTags.ContainsKey($key)) {
                if (-not $Overwrite) { continue }
                if ([string]$resTags[$key] -eq [string]$value) { continue }
            }
            $inherit[$key] = $value
        }

        if ($inherit.Count -gt 0) {
            $apply.Add([pscustomobject]@{ ResourceId = [string]$_.id; Tags = $inherit })
        }
    }

    $groups   = $apply | Group-Object { if ($_.ResourceId -match '/subscriptions/([0-9a-fA-F-]{36})') { $Matches[1] } else { '' } }
    $subCount = @($groups | Where-Object { $_.Name }).Count

    Write-Host ''
    Write-Host 'Azure Resource-Group Tag Inheritance' -ForegroundColor Cyan
    Write-Host ('-' * 51) -ForegroundColor DarkGray
    Write-Host (" Scope: {0} [{1}]" -f $resolved.Label, $resolved.Type)
    Write-Host (" Resources to update: {0}" -f $apply.Count)
    Write-Host (" Subscriptions: {0}" -f $subCount)
    Write-Host (" Mode: {0}" -f $(if ($Overwrite) { 'add missing + overwrite differing' } else { 'add missing keys only' }))
    Write-Host ''
    Write-Host 'Starting apply...'

    if ($apply.Count -eq 0) {
        Write-AzureUtilsStatus -Level WARN -Message 'No resources need inherited tags.'
        return
    }

    $applied    = 0
    $errorCount = 0
    $n          = 0
    $original   = Get-AzContext

    try {
        foreach ($group in $groups) {
            $subId = $group.Name
            if ($subId) {
                try {
                    $null = Set-AzContext -SubscriptionId $subId -ErrorAction Stop
                }
                catch {
                    $errorCount += $group.Count
                    Write-AzureUtilsStatus -Level ERROR -Message ("Cannot switch to subscription {0}; skipping {1} resource(s): {2}" -f $subId, $group.Count, $_.Exception.Message)
                    $n += $group.Count
                    continue
                }
            }

            foreach ($rec in $group.Group) {
                $n++
                $summary = (($rec.Tags.Keys | Sort-Object | ForEach-Object { '{0}={1}' -f $_, $rec.Tags[$_] }) -join '; ')

                if ($PSCmdlet.ShouldProcess($rec.ResourceId, "Inherit tags: $summary")) {
                    try {
                        $null = Update-AzTag -ResourceId $rec.ResourceId -Tag $rec.Tags -Operation Merge -ErrorAction Stop
                        $applied++
                        if (-not $Quiet) {
                            Write-AzureUtilsStatus -Level INFO -Message ("{0} of {1} inherited {2} tag(s) onto {3}: {4}" -f $n, $apply.Count, $rec.Tags.Count, $rec.ResourceId, $summary)
                        }
                    }
                    catch {
                        $errorCount++
                        Write-AzureUtilsStatus -Level ERROR -Message ("{0}: {1}" -f $rec.ResourceId, $_.Exception.Message)
                    }
                }
            }
        }
    }
    finally {
        if ($original) { $null = Set-AzContext -Context $original -ErrorAction SilentlyContinue }
    }

    Write-AzureUtilsStatus -Level INFO -Message 'Apply Finish'
    Write-Host ("Updated {0} of {1} resource(s){2}." -f $applied, $apply.Count, $(if ($errorCount) { ", $errorCount error(s)" } else { '' }))
}