public/Get-AzureAPISpecsVersionList.ps1

<#
.SYNOPSIS
Get an overview of all API Versions for any existing ProviderNamespace/ResourceType combination
 
.DESCRIPTION
Get an overview of all API Versions for any existing ProviderNamespace/ResourceType combination
 
.PARAMETER IncludePreview
Optional. Provide if preview versions should be included
 
.PARAMETER KeepArtifacts
Optional. Provide if any downloaded data should not be removed after the function ran. This is useful to speed up subsequent runs.
 
.EXAMPLE
Get-AzureAPISpecsVersionList
 
Returns an object like:
{
    "Microsoft.AAD": {
        "domainServices": [
            "2017-01-01",
            "2017-06-01",
            "2020-01-01",
            "2021-03-01",
            "2021-05-01",
            "2022-09-01"
        ],
        "domainServices/ouContainer": [
            "2017-06-01",
            "2020-01-01",
            "2021-03-01",
            "2021-05-01",
            "2022-09-01"
        ]
    },
    (...)
}
 
#>

function Get-AzureAPISpecsVersionList {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [switch] $IncludePreview,

        [Parameter(Mandatory = $false)]
        [switch] $KeepArtifacts
    )

    begin {
        Write-Debug ('{0} entered' -f $MyInvocation.MyCommand)
    }

    process {

        #########################################
        ## Temp Clone API Specs Repository ##
        #########################################
        $repoUrl = $script:CONFIG.url_CloneRESTAPISpecRepository
        $repoName = Split-Path $repoUrl -LeafBase
        $specificationPath = Join-Path (Join-Path $script:temp $repoName) 'specification'

        Copy-CustomRepository -RepoUrl $repoUrl -RepoName $repoName

        try {
            ##############################
            ## Collect API Versions ##
            ##############################
            $relevantFolderList = Get-FolderList -RootFolder $specificationPath

            $filePaths = $relevantFolderList | Foreach-Object { (Get-ChildItem -Path $_ -Recurse -Filter '*.json').FullName } | Where-Object { 
                (Split-Path $_ -Leaf) -notin @('common.json', 'privatelinks.json') -and
                ($_ -replace '\\', '/') -notlike "*/examples/*"
            }

            if (-not $IncludePreview) {
                $beforeCount = $filePaths.Count
                $filePaths = $filePaths | Where-Object { ($_ -replace '\\', '/') -notlike "*/preview/*" }
                Write-Verbose ("Filtered [{0}] files for preview versions out" -f ($beforeCount - $filePaths.Count))
            }

            Write-Verbose ("Found [{0}] files to analyze" -f $filePaths.Count)

            $resultSet = @{}
            for ($fileIndex = 0; $fileIndex -lt $filePaths.Count; $fileIndex++) {
                $filePath = $filePaths[$fileIndex]

                $apiVersion = Split-Path (Split-Path $filePath -Parent) -Leaf

                $specificationData = Get-Content $filePath -Raw | ConvertFrom-Json -AsHashtable
                $pathData = $specificationData.paths

                if (-not $pathData) {
                    continue
                }

                # Collect all URL paths that
                # - contain a 'PUT' - which means you can create the resource
                # - end with a variable name {...} or a string (i.e. given value like "default")
                # - contain the 'Microsoft.' provider, or equals any of the exceptions like ResourceGroup & ResourceTags
                $relevantPaths = $pathData.Keys | Where-Object { 
                    $pathData[$_].Keys -contains 'put' -and 
                    $_ -match "\w+}$|\w+$" -and 
                    ($_ -match ".*\/providers\/Microsoft\..+" -or # Ignoring anything without 'providers' in the name as it does not seem to be relevant to IaC
                    $_ -in @(
                        '/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}', # Special case: Resource Group
                        '/{scope}/providers/Microsoft.Resources/tags/default' # Special case: Tags
                    )) 
                }

                if (-not $relevantPaths) {
                    continue
                }

                foreach ($relevantPath in $relevantPaths) {

                    if ($relevantPath -eq '/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}') {
                        # Special case: Resource Group
                        $providerNamespace = 'Microsoft.Resources'
                        $resourceType = 'resourceGroups'
                    }
                    else {
                        $identifierElem = ($relevantPath -split '\/providers\/')[-1] -split '\/'
                        $providerNamespace = $identifierElem[0] # E.g. Microsoft.Storage
                    
                        # Add the remaining elements (every 2nd as everything in between represents a 'name')
                        $remainingRelevantElem = $identifierElem[1..($identifierElem.Count)]
                        $resourceType = ''
                        for ($index = 0; $index -lt $remainingRelevantElem.Count; $index++) {
                            if ($index % 2 -eq 0) {
                                $resourceType += ('/{0}' -f $remainingRelevantElem[$index])
                            }
                        }
                        $resourceType = $resourceType.TrimStart('/')
                    }

                    if ($resultSet.Keys -notcontains $providerNamespace) {
                        $resultSet[$providerNamespace] = @{}
                    }

                    if ($relevantPath -match ".+}\/{(\w+)}\/{\w+}$") {
                        # Special resource types like PrivateDNSZones
                        # '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Network/dnszones/{zoneName}/{recordType}/{relativeRecordSetName}'
                        
                        # $pathElemName = ($resourceType -split '/')[-2] -replace '\{|}'
                        $pathElemName = $matches[1]
                        # Needs special handling as the 'recordType' in this example must be resolved with a parameter that specifies to be part of the path like ["in": "path"]
                        # 1. check: Direct URL Parameter
                        $pathParameters = $pathData[$relevantPath].put.parameters | Where-Object { 
                            $_.in -eq 'path' -and $_.name -eq $pathElemName
                        }

                        if (-not $pathParameters) {
                            continue
                        }

                        # 2. check: Parameters-specification element - Only relevant for non-deployment types?
                        # if (-not $pathParameters) {
                        # $pathParameters = $specificationData.parameters.Keys | Where-Object {
                        # $specificationData.parameters[$_].in -eq 'path'
                        # } | ForEach-Object {
                        # $specificationData.parameters[$_]
                        # }
                        # }

                        if($pathParameters.Keys -contains 'enum') {
                            foreach($elem in $pathParameters.enum) {
                                $formattedResourceType = $resourceType -replace "\{$pathElemName}", $elem

                                if ($resultSet[$providerNamespace].Keys -notcontains $formattedResourceType) {
                                    $resultSet[$providerNamespace][$formattedResourceType] = @()
                                }

                                $apiVersionList = (@() + $resultSet[$providerNamespace][$formattedResourceType] + $apiVersion) | Sort-Object -Unique
                                $resultSet[$providerNamespace][$formattedResourceType] = $apiVersionList -is [array] ? $apiVersionList : @($apiVersion)
                            }
                        }
                        else {
                            Write-Verbose "Ignoring [$relevantPath] in file [$filePath] as we failed to resolve the path's properties." 
                        }
                    }
                    else {        
                        if ($resultSet[$providerNamespace].Keys -notcontains $resourceType) {
                            $resultSet[$providerNamespace][$resourceType] = @()
                        }

                        $apiVersionList = (@() + $resultSet[$providerNamespace][$resourceType] + $apiVersion) | Sort-Object -Unique
                        $resultSet[$providerNamespace][$resourceType] = $apiVersionList -is [array] ? $apiVersionList : @($apiVersion)
                    }
                }
                $percentageComplete = [System.Math]::Floor(($fileIndex / $filePaths.Count) * 100)
                Write-Progress -Activity "Analyzing in progress" -Status ("[{0}/{1}] or {2}% files processed" -f $fileIndex, $filePaths.count, $percentageComplete) -PercentComplete $percentageComplete
            }

            # Order result
            $orderedResultSet = [ordered]@{}
            foreach ($providerNamespace in ($resultSet.Keys | Sort-Object)) {
                
                if ($orderedResultSet.Keys -notcontains $providerNamespace) {
                    $orderedResultSet[$providerNamespace] = [ordered]@{}
                }
                
                foreach ($resourceType in ($resultSet[$providerNamespace].Keys | Sort-Object)) {
                    $orderedResultSet[$providerNamespace][$resourceType] = $resultSet[$providerNamespace][$resourceType]
                }
            }

            $orderedResultSet
        }
        catch {
            throw ($_, $_.ScriptStackTrace)
        }
        finally {
            ##########################
            ## Remove Artifacts ##
            ##########################
            if (-not $KeepArtifacts) {
                Write-Verbose ('Deleting temp folder [{0}]' -f $script:temp)
                $null = Remove-Item $script:temp -Recurse -Force
            }
        }
    }

    end {
        Write-Debug ('{0} exited' -f $MyInvocation.MyCommand)
    }
}