Public/Assert-AzLocalAzureSubscriptionAccess.ps1
|
function Assert-AzLocalAzureSubscriptionAccess { <# .SYNOPSIS Fails the pipeline step with a meaningful run-summary message when the authenticated identity can see no Azure subscriptions. .DESCRIPTION Every AzLocal.UpdateManagement monitoring / update pipeline runs fleet-wide Azure Resource Graph queries across EVERY subscription the federated identity can read. When `azure/login` (GitHub) or `AzureCLI@2` (Azure DevOps) authenticates successfully but the identity has zero subscription role assignments, `az login` may still fail hard with the cryptic "No subscriptions found for ***", OR the login "succeeds" and every downstream ARG query silently returns nothing - producing empty reports and a confusing cascade (e.g. the JUnit publish step then reports "No test report files were found"). This cmdlet turns that failure mode into ONE clear, actionable message: 1. Enumerates the subscriptions visible to the current `az` context (`az account list --output json`). 2. Counts the ENABLED subscriptions. 3. If the enabled count is below -MinimumCount: - emits a red `::error` / `##vso[task.logissue type=error]` annotation via Write-AzLocalPipelineError, - writes a rich remediation block to the run summary via Add-AzLocalPipelineStepSummary (so the operator does NOT have to expand the step logs), - sets the `subscription_count` step output to 0, - throws a concise terminating error so the step exits non-zero. 4. Otherwise it writes a one-line confirmation to the summary, sets the `subscription_count` step output, and (with -PassThru) returns the enabled count. Host detection (GitHub / AzureDevOps / Local) flows through the existing Private helpers - no per-host branching here. .PARAMETER MinimumCount Minimum number of ENABLED subscriptions that must be visible for the step to pass. Defaults to 1. .PARAMETER SubscriptionListJson Optional. The raw JSON array normally returned by `az account list --output json`. When supplied, the cmdlet does NOT shell out to `az` (used by unit tests and by callers that already have the account list in hand). When omitted, the cmdlet runs `az account list --output json` itself. .PARAMETER SummaryFileName Forwarded to Add-AzLocalPipelineStepSummary. Defaults to 'azlocal-subscription-access.md'. Ignored on GitHub Actions (which uses a single $env:GITHUB_STEP_SUMMARY file managed by the runner). .PARAMETER PassThru When set, returns the count of enabled subscriptions on success. .OUTPUTS Nothing by default. With -PassThru, returns an [int] enabled count. Throws a terminating error when the enabled count is below -MinimumCount. .EXAMPLE Assert-AzLocalAzureSubscriptionAccess Default gate placed immediately after the Azure login step(s). Fails the job with a remediation block on the run summary when no subscriptions are visible. .EXAMPLE $count = Assert-AzLocalAzureSubscriptionAccess -MinimumCount 2 -PassThru #> [CmdletBinding()] [OutputType([int])] param( [Parameter()] [ValidateRange(1, [int]::MaxValue)] [int]$MinimumCount = 1, [Parameter()] [AllowEmptyString()] [AllowNull()] [string]$SubscriptionListJson, [Parameter()] [ValidateNotNullOrEmpty()] [string]$SummaryFileName = 'azlocal-subscription-access.md', [Parameter()] [switch]$PassThru ) # 1. Obtain the account list (inject for tests, else shell out to az). $rawJson = $SubscriptionListJson if (-not $PSBoundParameters.ContainsKey('SubscriptionListJson')) { try { $rawJson = (& az account list --output json 2>$null | Out-String) } catch { $rawJson = '' Write-Verbose "Assert-AzLocalAzureSubscriptionAccess: 'az account list' threw - $($_.Exception.Message)" } } # 2. Parse. A missing / empty / non-array payload is treated as zero subscriptions. $accounts = @() if (-not [string]::IsNullOrWhiteSpace($rawJson)) { try { $parsed = $rawJson | ConvertFrom-Json -ErrorAction Stop if ($null -ne $parsed) { $accounts = @($parsed) } } catch { Write-Verbose "Assert-AzLocalAzureSubscriptionAccess: could not parse 'az account list' JSON - $($_.Exception.Message)" $accounts = @() } } $totalCount = $accounts.Count $enabled = @($accounts | Where-Object { $_.PSObject.Properties['state'] -and $_.state -and ($_.state -ieq 'Enabled') }) # If the payload does not carry a 'state' field at all, fall back to counting # every returned account as accessible (older az versions / injected fixtures). $enabledCount = if ($accounts | Where-Object { $_.PSObject.Properties['state'] }) { $enabled.Count } else { $totalCount } # 3. Failure path: below the minimum -> meaningful error + run-summary block + throw. if ($enabledCount -lt $MinimumCount) { $stateNote = if ($totalCount -gt $enabledCount) { "The identity CAN see $totalCount subscription(s) but only $enabledCount is/are in the 'Enabled' state." } else { "The identity can see $enabledCount subscription(s) (need at least $MinimumCount)." } $summary = @" ## :x: Azure login returned no accessible subscriptions $stateNote This pipeline runs **fleet-wide** Azure Resource Graph queries across every subscription the federated identity can read. With no accessible subscription, every query returns nothing, reports are empty, and downstream steps (e.g. the JUnit "Publish Results" step) fail with misleading errors such as ``No test report files were found``. The real cause is subscription access, not the reporting step. ### How to fix 1. **Assign an RBAC role** to the pipeline's service principal / federated identity. It needs at least **Reader** on each target subscription (or on a management group that contains them); update operations need the appropriate write role. Zero role assignments = zero visible subscriptions. 2. **Confirm the tenant**: check that ``AZURE_TENANT_ID`` points to the tenant that owns the Azure Local clusters' subscriptions. 3. **Confirm the identity**: check that ``AZURE_CLIENT_ID`` is the intended app registration / managed identity (not a stale or wrong-tenant one). 4. If subscriptions exist but are not ``Enabled``, re-enable them (or clear any billing / policy hold) before re-running. _Verified via ``az account list`` after the Azure login step._ "@ Write-AzLocalPipelineError ` -Title 'Azure login returned no accessible subscriptions' ` -Message ("This pipeline needs at least $MinimumCount accessible (Enabled) Azure subscription but found $enabledCount. " + 'Assign the federated identity at least Reader on the target subscription(s) or management group, and confirm AZURE_TENANT_ID / AZURE_CLIENT_ID. See the run summary for full remediation steps.') [void](Add-AzLocalPipelineStepSummary -Markdown $summary -SummaryFileName $SummaryFileName) Set-AzLocalPipelineOutput -Name 'subscription_count' -Value $enabledCount.ToString() throw "Assert-AzLocalAzureSubscriptionAccess: no accessible Azure subscriptions (found $enabledCount, need $MinimumCount). See the run summary for remediation." } # 4. Success path. Write-Host "Azure subscription access verified: $enabledCount enabled subscription(s) visible to the pipeline identity." $okSummary = "_Azure subscription access verified: **$enabledCount** enabled subscription(s) visible to the pipeline identity._`n" [void](Add-AzLocalPipelineStepSummary -Markdown $okSummary -SummaryFileName $SummaryFileName) Set-AzLocalPipelineOutput -Name 'subscription_count' -Value $enabledCount.ToString() if ($PassThru) { return $enabledCount } } |