src/collect/Get-ScoutCostInventory.ps1
|
#Requires -Version 7.0 Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' <# .SYNOPSIS Per-subscription actual-cost data, grouped by type/resource group/location/service -- ported into src/collect from Modules/Private/Extraction/Get-AZTICostInventory.ps1 (AB#5639/5647). .DESCRIPTION Cost Management is an opt-in Az module (`Az.CostManagement`) that is deliberately NOT in this repo's auto-installed dependency list (AzureScout.psm1's own `.NOTES` explain why: installing it drags in a newer `Az.Accounts`, and two `Az.Accounts` versions side by side crash every subsequent module import with a stack overflow). Cost data is therefore OPTIONAL ENRICHMENT that must never cost the caller their run. .PARAMETER Subscriptions Subscription objects with `.id` and `.name`. .PARAMETER Days When >= 365, queries the full previous calendar year (Jan 1 -- Dec 31). Otherwise queries from the start of two months ago through end-of-today, matching the legacy function. .PARAMETER Granularity Passed straight through to `Invoke-AzCostManagementQuery -DatasetGranularity` (e.g. 'Monthly', 'Daily'). .OUTPUTS One `[pscustomobject]` per subscription: `SubscriptionId`, `SubscriptionName`, `CostData` (the `Invoke-AzCostManagementQuery` result rows, or `@()` if cost data could not be retrieved for that subscription -- NEVER `$null`, so a caller's `@($x.CostData).Count` is always safe under StrictMode). .NOTES Tracks ADO AB#5639 (Task AB#5647, Epic AB#5638). THE NON-FATAL GUARANTEE IS THE POINT (mirrors the fix already shipped in the legacy function for AB#5636): a missing `Az.CostManagement` module, or any other Cost Management failure, degrades that ONE subscription's `CostData` to `@()` with a `Write-Warning` naming the cause -- it never throws uncaught, and it never aborts collection for any OTHER subscription. The historical defect this guards against was a bare `throw $_.Exception.Message` followed by an unreachable `$Costs = @()` fallback that could never run; every `catch` block here ends in an assignment, not a `throw`. #> function Get-ScoutCostInventory { [CmdletBinding()] param( [Parameter(Mandatory)] [object[]] $Subscriptions, [int] $Days = 60, [string] $Granularity = 'Monthly' ) $today = Get-Date $endDate = Get-Date -Year $today.Year -Month $today.Month -Day $today.Day -Hour 23 -Minute 59 -Second 59 -Millisecond 0 if ($Days -ge 365) { $startDate = Get-Date -Year $endDate.AddYears(-1).Year -Month 1 -Day 1 -Hour 0 -Minute 0 -Second 0 -Millisecond 1 $endDate = Get-Date -Year $startDate.Year -Month 12 -Day 31 -Hour 23 -Minute 59 -Second 59 -Millisecond 0 } else { $startDate = (Get-Date -Day 1).AddMonths(-2) } $grouping = @( @{ Name = 'ResourceType'; Type = 'Dimension' } @{ Name = 'ResourceGroup'; Type = 'Dimension' } @{ Name = 'ResourceLocation'; Type = 'Dimension' } @{ Name = 'ServiceName'; Type = 'Dimension' } ) $aggregation = @{ totalCost = @{ name = 'PreTaxCost'; function = 'Sum' } } $costManagementAvailable = [bool](Get-Command Invoke-AzCostManagementQuery -ErrorAction SilentlyContinue) if (-not $costManagementAvailable) { Write-Warning "Get-ScoutCostInventory: the Az.CostManagement module is not installed -- every subscription's CostData will be empty. Install it with 'Install-Module Az.CostManagement -Scope CurrentUser' to include cost data." } $result = foreach ($subscription in $Subscriptions) { $subId = $subscription.id $subName = if ($subscription.PSObject.Properties['name']) { $subscription.name } else { $subId } $scope = "/subscriptions/$subId/" $costs = @() if ($costManagementAvailable) { try { $costs = @(Invoke-AzCostManagementQuery -Type ActualCost -Scope $scope -Timeframe Custom ` -DatasetGranularity $Granularity -DatasetGrouping $grouping -DatasetAggregation $aggregation ` -TimePeriodFrom $startDate -TimePeriodTo $endDate -ErrorAction Stop) } catch { # Cost data is optional enrichment -- it must never cost the caller their # inventory (AB#5636's defect class: a bare `throw` here previously aborted # the whole run and made the `$costs = @()` fallback unreachable). $costs = @() Write-Warning "Get-ScoutCostInventory: cost data skipped for subscription '$subName': $($_.Exception.Message)" } } [pscustomobject]@{ SubscriptionId = $subId SubscriptionName = $subName CostData = $costs } } return @($result) } |