Public/Invoke-AzLocalClusterInventory.ps1
|
function Invoke-AzLocalClusterInventory { <# .SYNOPSIS Runs the Step.1 cluster-inventory pipeline workload: queries the fleet, writes timestamped + canonical CSV / JSON artifacts plus the operator README, and emits the JUnit-style step summary + step outputs for the v0.8.5 thin-YAML Step.1 pipeline. .DESCRIPTION Phase 1 (v0.8.5) of the thin-YAML refactor. Condenses the inline `run: |` body of the v0.8.4 `Step.1_inventory-clusters.yml` (GitHub Actions + Azure DevOps) into a single cmdlet call so the per-platform yml shrinks to a few lines and the workload becomes unit-testable against a synthetic Get-AzLocalClusterInventory result. The cmdlet: 1. Resolves the output directory (defaults to `./artifacts` on GitHub Actions / local, or `$env:BUILD_ARTIFACTSTAGINGDIRECTORY` on Azure DevOps - matching the v0.8.4 yml). 2. Calls `Get-AzLocalClusterInventory -PassThru -ExportPath <csv>` ONCE (the v0.8.4 yml called it twice; the cmdlet path collapses that into one query + a local JSON serialisation). 3. Serialises the same in-memory inventory to JSON. 4. Copies the timestamped CSV to a canonical `ClusterUpdateRings.csv` (the filename the Step.2 'Manage UpdateRing Tags' pipeline consumes by default). 5. Writes a `README_Instructions.txt` next to the CSV that explains the three-step operator flow (edit CSV in Excel, commit, run Step.2). 6. Emits the markdown step summary (Total / WithTag / WithoutTag metric table + UpdateRing distribution table) via `Add-AzLocalPipelineStepSummary`. 7. Emits four step outputs via `Set-AzLocalPipelineOutput`: `cluster_count`, `with_tag_count`, `without_tag_count`, `csv_path`. Internal reuse (per the v0.8.5 thin-YAML consistency contract): * `Get-AzLocalClusterInventory` for the actual inventory query. * `Add-AzLocalPipelineStepSummary` for the rendered markdown. * `Set-AzLocalPipelineOutput` for the step outputs. * `Get-AzLocalPipelineHost` is implicit (the above branch on it). .PARAMETER OutputDirectory Directory to write the CSV / JSON / canonical CSV / README into. Created if it does not exist. Defaults to `./artifacts` (which is what the v0.8.4 GH yml uses) or, on AzureDevOps, to `$env:BUILD_ARTIFACTSTAGINGDIRECTORY` if that env var is set (matching the v0.8.4 ADO yml). .PARAMETER SubscriptionFilter Optional. When set and non-empty, passed through as `-SubscriptionId` to `Get-AzLocalClusterInventory`. When empty / null, the inventory runs across every subscription the pipeline identity can see. .PARAMETER Timestamp DateTime used to build the timestamped CSV / JSON filenames. Defaults to `Get-Date`. Tests pass a fixed value so filenames are deterministic. .PARAMETER CanonicalCsvFileName Filename for the canonical (no-timestamp) CSV copy. Default `ClusterUpdateRings.csv` - matches the Step.2 default `csv_file_path` input of `config/ClusterUpdateRings.csv`. .PARAMETER ReadmeFileName Filename for the operator README. Default `README_Instructions.txt`. .PARAMETER InstalledModuleVersion Optional [string] used in the README footer ('Generated by AzLocal.UpdateManagement v<x>'). .PARAMETER SummaryFileName Per-task summary filename used by `Add-AzLocalPipelineStepSummary` on Azure DevOps and Local hosts. Default `cluster-inventory-summary.md`. .PARAMETER PassThru When set, returns a single PSCustomObject summarising the run (ClusterCount, WithTagCount, WithoutTagCount, CsvPath, JsonPath, CanonicalCsvPath, ReadmePath, Clusters, UpdateRingDistribution). Without -PassThru the cmdlet emits nothing to the pipeline; the artifacts and step outputs are still produced. .OUTPUTS Nothing by default. When -PassThru is set, a single PSCustomObject: ClusterCount [int] WithTagCount [int] WithoutTagCount [int] CsvPath [string] JsonPath [string] CanonicalCsvPath [string] ReadmePath [string] Clusters [PSCustomObject[]] UpdateRingDistribution [PSCustomObject[]] (Name + Count) .EXAMPLE Invoke-AzLocalClusterInventory -PassThru Runs the inventory against every subscription the identity can see, writes the four artifacts to `./artifacts`, emits the markdown summary + step outputs to the active pipeline host, and returns the summary object. .NOTES Module: AzLocal.UpdateManagement (v0.8.5+) Roadmap: Step.1 - Inventory Azure Local Clusters. #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string]$OutputDirectory, [Parameter(Mandatory = $false)] [AllowEmptyString()] [AllowNull()] [string]$SubscriptionFilter, [Parameter(Mandatory = $false)] [datetime]$Timestamp = (Get-Date), [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string]$CanonicalCsvFileName = 'ClusterUpdateRings.csv', [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string]$ReadmeFileName = 'README_Instructions.txt', [Parameter(Mandatory = $false)] [AllowEmptyString()] [AllowNull()] [string]$InstalledModuleVersion, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string]$SummaryFileName = 'cluster-inventory-summary.md', [Parameter(Mandatory = $false)] [switch]$PassThru ) $pipelineHost = Get-AzLocalPipelineHost if (-not $OutputDirectory) { if ($pipelineHost -eq 'AzureDevOps' -and $env:BUILD_ARTIFACTSTAGINGDIRECTORY) { $OutputDirectory = $env:BUILD_ARTIFACTSTAGINGDIRECTORY } else { $OutputDirectory = './artifacts' } } if (-not (Test-Path -LiteralPath $OutputDirectory)) { New-Item -ItemType Directory -Path $OutputDirectory -Force | Out-Null } $timestampStr = $Timestamp.ToString('yyyyMMdd_HHmmss') $csvPath = Join-Path -Path $OutputDirectory -ChildPath ("ClusterInventory_{0}.csv" -f $timestampStr) $jsonPath = Join-Path -Path $OutputDirectory -ChildPath ("ClusterInventory_{0}.json" -f $timestampStr) $canonicalCsvPath = Join-Path -Path $OutputDirectory -ChildPath $CanonicalCsvFileName $readmePath = Join-Path -Path $OutputDirectory -ChildPath $ReadmeFileName $invParams = @{} if ($SubscriptionFilter) { $invParams['SubscriptionId'] = $SubscriptionFilter Write-Host "Filtering to subscription: $SubscriptionFilter" } else { Write-Host 'Querying all accessible subscriptions' } Write-Host '--- Running cluster inventory ---' $inventory = Get-AzLocalClusterInventory @invParams -ExportPath $csvPath -PassThru if ($null -eq $inventory) { $inventory = @() } # Force array shape so .Count is reliable even when a single row comes back as a bare PSCustomObject. $inventory = @($inventory) # Serialise the same in-memory inventory to JSON next to the CSV. $inventory | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $jsonPath -Encoding UTF8 if (Test-Path -LiteralPath $csvPath) { Copy-Item -LiteralPath $csvPath -Destination $canonicalCsvPath -Force Write-Host "Canonical copy created: $canonicalCsvPath (edit this one)" } else { # Empty fleet: Get-AzLocalClusterInventory does not write a CSV when there # are zero rows. Synthesise an empty canonical CSV so Step.2 has a file # to read (a header-only CSV is treated as 'no work to do'). $emptyHeader = 'ClusterName,ResourceGroup,SubscriptionId,SubscriptionName,UpdateRing,HasUpdateRingTag,UpdateStartWindow,UpdateExclusions,UpdateSideloaded,UpdateVersionInProgress,ResourceId' Set-Content -LiteralPath $canonicalCsvPath -Value $emptyHeader -Encoding UTF8 Set-Content -LiteralPath $csvPath -Value $emptyHeader -Encoding UTF8 Write-Host "No cluster rows returned - wrote header-only CSV to $canonicalCsvPath." } # README operator instructions. $generatedUtc = $Timestamp.ToUniversalTime().ToString("yyyy-MM-dd HH:mm:ss 'UTC'") $moduleVersionForReadme = if ($InstalledModuleVersion) { $InstalledModuleVersion } else { '(unknown)' } $readme = @' ======================================================================== Cluster Inventory - Next Steps AzLocal.UpdateManagement (UpdateRing tagging workflow) ======================================================================== This artifact contains the cluster inventory generated by the 'Inventory Azure Local Clusters' pipeline. Use it to assign each cluster to an update wave (UpdateRing tag), then commit the result back into your repository so the 'Manage UpdateRing Tags' pipeline can apply the tags to Azure. ------------------------------------------------------------------------ FILES IN THIS ARTIFACT ------------------------------------------------------------------------ ClusterUpdateRings.csv <-- EDIT THIS FILE. Canonical name expected by the 'Manage UpdateRing Tags' pipeline's default csv_file_path / csvFilePath input (config/ClusterUpdateRings.csv). ClusterInventory_<timestamp>.csv Audit / historical copy of this run's inventory. Do NOT edit - keep it so you can diff future inventory runs against this snapshot. ClusterInventory_<timestamp>.json Machine-readable copy for dashboards / custom integrations. README_Instructions.txt This file. ------------------------------------------------------------------------ STEP 1 - Open ClusterUpdateRings.csv in Excel ------------------------------------------------------------------------ - For each cluster row, fill in the 'UpdateRing' column with the wave name you want that cluster to belong to. Examples: Canary, Pilot, Production (or your own scheme, e.g. Wave1, Wave2, Wave3 / Dev, Test, Prod) - Leave the 'UpdateRing' column EMPTY for any cluster you do not want this run to tag - those rows will be skipped. - Save as CSV (UTF-8). Do not change other columns or column order. - Do NOT rename the file. Keep it as 'ClusterUpdateRings.csv' so the 'Manage UpdateRing Tags' pipeline's default path picks it up. ------------------------------------------------------------------------ STEP 2 - Commit ClusterUpdateRings.csv into your ops repository ------------------------------------------------------------------------ - Recommended path: config/ClusterUpdateRings.csv - Commit and push (PR review optional but recommended). ------------------------------------------------------------------------ STEP 3 - Run the 'Manage UpdateRing Tags' pipeline ------------------------------------------------------------------------ - Trigger 'Manage UpdateRing Tags' (Step.2) and point its csv_file_path / csvFilePath input at the path you committed in STEP 2 (default is 'config/ClusterUpdateRings.csv'). - RECOMMENDED FIRST RUN: leave 'dry_run' / 'dryRun' = true. The pipeline will preview which tags would be set/changed without modifying any Azure resources. - Once the preview looks correct, re-run with dry-run = false to apply the tags. Use force = true only if you need to overwrite existing UpdateRing values already set on a cluster. ------------------------------------------------------------------------ DOCUMENTATION ------------------------------------------------------------------------ Pipeline examples README (online): https://github.com/NeilBird/Azure-Local/blob/main/AzLocal.UpdateManagement/Automation-Pipeline-Examples/README.md Module README (online): https://github.com/NeilBird/Azure-Local/blob/main/AzLocal.UpdateManagement/README.md Module on the PowerShell Gallery: https://www.powershellgallery.com/packages/AzLocal.UpdateManagement Generated by AzLocal.UpdateManagement v__MODULE_VERSION__ Run timestamp (UTC): __TIMESTAMP__ ======================================================================== '@ $readme = $readme.Replace('__MODULE_VERSION__', $moduleVersionForReadme).Replace('__TIMESTAMP__', $generatedUtc) Set-Content -LiteralPath $readmePath -Value $readme -Encoding ASCII Write-Host "Instructions written to: $readmePath" # Count tag coverage. $clusterCount = [int]$inventory.Count $withTag = [int]@($inventory | Where-Object { $_.HasUpdateRingTag -eq 'Yes' }).Count $withoutTag = $clusterCount - $withTag # UpdateRing distribution (rows with a non-empty UpdateRing value). $ringGroups = @($inventory | Where-Object { $_.UpdateRing } | Group-Object -Property UpdateRing | Sort-Object -Property Name) $ringDistribution = @($ringGroups | ForEach-Object { [pscustomobject]@{ Name = $_.Name; Count = $_.Count } }) Write-Host '' Write-Host '========================================' Write-Host 'Inventory Complete' Write-Host '========================================' Write-Host ("Total Clusters : {0}" -f $clusterCount) Write-Host ("With UpdateRing Tag : {0}" -f $withTag) Write-Host ("Without UpdateRing Tag : {0}" -f $withoutTag) Write-Host ("CSV : {0}" -f $csvPath) Write-Host ("JSON : {0}" -f $jsonPath) # Step outputs. Set-AzLocalPipelineOutput -Name 'cluster_count' -Value ([string]$clusterCount) Set-AzLocalPipelineOutput -Name 'with_tag_count' -Value ([string]$withTag) Set-AzLocalPipelineOutput -Name 'without_tag_count' -Value ([string]$withoutTag) Set-AzLocalPipelineOutput -Name 'csv_path' -Value ([string]$csvPath) # Markdown step summary. $sb = New-Object System.Text.StringBuilder [void]$sb.AppendLine('## Step.1 - Cluster Inventory') [void]$sb.AppendLine('') [void]$sb.AppendLine('| Metric | Value |') [void]$sb.AppendLine('|--------|-------|') [void]$sb.AppendLine(("| Total Clusters | {0} |" -f $clusterCount)) [void]$sb.AppendLine(("| With UpdateRing Tag | {0} |" -f $withTag)) [void]$sb.AppendLine(("| Without UpdateRing Tag | {0} |" -f $withoutTag)) [void]$sb.AppendLine('') [void]$sb.AppendLine('### UpdateRing Distribution') [void]$sb.AppendLine('') if ($ringDistribution.Count -gt 0) { [void]$sb.AppendLine('| UpdateRing | Count |') [void]$sb.AppendLine('|------------|-------|') foreach ($row in $ringDistribution) { [void]$sb.AppendLine(("| {0} | {1} |" -f $row.Name, $row.Count)) } } else { [void]$sb.AppendLine('No UpdateRing tags found.') } [void]$sb.AppendLine('') [void]$sb.AppendLine('### Next Steps') [void]$sb.AppendLine('') [void]$sb.AppendLine('1. Download the artifact attached to this run.') [void]$sb.AppendLine('2. Open `ClusterUpdateRings.csv` in Excel and fill in the `UpdateRing` column.') [void]$sb.AppendLine('3. Commit the edited CSV (recommended path `config/ClusterUpdateRings.csv`).') [void]$sb.AppendLine('4. Run the "Manage UpdateRing Tags" pipeline (Step.2).') Add-AzLocalPipelineStepSummary -Markdown $sb.ToString() -SummaryFileName $SummaryFileName | Out-Null if ($PassThru) { return [pscustomobject]@{ ClusterCount = $clusterCount WithTagCount = $withTag WithoutTagCount = $withoutTag CsvPath = $csvPath JsonPath = $jsonPath CanonicalCsvPath = $canonicalCsvPath ReadmePath = $readmePath Clusters = $inventory UpdateRingDistribution = $ringDistribution } } } |