Private/Get-AvmInterfaceDiff.ps1
|
function Get-AvmInterfaceDiff { <# .SYNOPSIS Compares the module interface (params/outputs) between two versions of an AVM module. #> [CmdletBinding()] param( [PSCustomObject]$Reference, [string]$TargetVersion ) $diff = [PSCustomObject]@{ available = $false addedRequired = @() # NEW required inputs (no default) — BREAKING removedInputs = @() # Removed/renamed params — BREAKING typeChanged = @() # Type changes — BREAKING addedOptional = @() # New optional inputs — non-breaking removedOutputs = @() # Removed/renamed outputs — BREAKING addedOutputs = @() # New outputs — non-breaking rawError = $null } try { if ($Reference.ecosystem -eq 'bicep') { $diff = Get-BicepInterfaceDiff -Reference $Reference -TargetVersion $TargetVersion } elseif ($Reference.ecosystem -eq 'terraform') { $diff = Get-TerraformInterfaceDiff -Reference $Reference -TargetVersion $TargetVersion } } catch { $diff.rawError = $_.ToString() Write-Verbose "Interface diff unavailable for $($Reference.registryPath): $_" } return $diff } function Get-BicepInterfaceDiff { [CmdletBinding()] param([PSCustomObject]$Reference, [string]$TargetVersion) $diff = [PSCustomObject]@{ available = $false; addedRequired = @(); removedInputs = @() typeChanged = @(); addedOptional = @(); removedOutputs = @() addedOutputs = @(); rawError = $null } # Fetch main.bicep from GitHub bicep-registry-modules for both versions $baseUrl = "https://raw.githubusercontent.com/Azure/bicep-registry-modules" $modPath = "avm/$($Reference.category)/$($Reference.group)/$($Reference.module)/main.bicep" # Build git tags: typically 'avm/<category>/<group>/<module>/<version>' $tagPrefix = "avm/$($Reference.category)/$($Reference.group)/$($Reference.module)" $curTag = "$tagPrefix/$($Reference.currentVersion)" $tgtTag = "$tagPrefix/$TargetVersion" $curContent = Invoke-AvmGitHubRequest -Uri "$baseUrl/$curTag/$modPath" $tgtContent = Invoke-AvmGitHubRequest -Uri "$baseUrl/$tgtTag/$modPath" if (-not $curContent -or -not $tgtContent) { return $diff } $diff.available = $true $curParams = ParseBicepParams -Content $curContent $tgtParams = ParseBicepParams -Content $tgtContent $curOutputs = ParseBicepOutputs -Content $curContent $tgtOutputs = ParseBicepOutputs -Content $tgtContent # Required param added (no default in target, absent in current) foreach ($p in $tgtParams.Keys) { if (-not $curParams.ContainsKey($p) -and -not $tgtParams[$p].hasDefault) { $diff.addedRequired += $p } elseif (-not $curParams.ContainsKey($p) -and $tgtParams[$p].hasDefault) { $diff.addedOptional += $p } elseif ($curParams.ContainsKey($p) -and $tgtParams[$p].type -ne $curParams[$p].type) { $diff.typeChanged += "$p (${$curParams[$p].type} -> ${$tgtParams[$p].type})" } } # Removed params foreach ($p in $curParams.Keys) { if (-not $tgtParams.ContainsKey($p)) { $diff.removedInputs += $p } } # Outputs foreach ($o in $curOutputs) { if ($o -notin $tgtOutputs) { $diff.removedOutputs += $o } } foreach ($o in $tgtOutputs) { if ($o -notin $curOutputs) { $diff.addedOutputs += $o } } return $diff } function ParseBicepParams { param([string]$Content) $params = @{} $simplePattern = [regex]"^param\s+(\w+)\s+(\S+)(\s*=\s*.+)?$" foreach ($m in $simplePattern.Matches($Content)) { $name = $m.Groups[1].Value $type = $m.Groups[2].Value $hasDefault = $m.Groups[3].Success $params[$name] = @{ type = $type; hasDefault = $hasDefault } } return $params } function ParseBicepOutputs { param([string]$Content) $outputs = @() $pattern = [regex]"^output\s+(\w+)\s+" foreach ($m in $pattern.Matches($Content)) { $outputs += $m.Groups[1].Value } return $outputs } function Get-TerraformInterfaceDiff { [CmdletBinding()] param([PSCustomObject]$Reference, [string]$TargetVersion) $diff = [PSCustomObject]@{ available = $false; addedRequired = @(); removedInputs = @() typeChanged = @(); addedOptional = @(); removedOutputs = @() addedOutputs = @(); rawError = $null } # Repo: Azure/terraform-azurerm-<avmModuleName> or Azure/<avmModuleName> $repoName = $Reference.module -replace '^avm-', 'terraform-azurerm-avm-' $baseUrl = "https://raw.githubusercontent.com/Azure/$repoName" $curVars = Invoke-AvmGitHubRequest -Uri "$baseUrl/v$($Reference.currentVersion)/variables.tf" $tgtVars = Invoke-AvmGitHubRequest -Uri "$baseUrl/v$TargetVersion/variables.tf" $curOuts = Invoke-AvmGitHubRequest -Uri "$baseUrl/v$($Reference.currentVersion)/outputs.tf" $tgtOuts = Invoke-AvmGitHubRequest -Uri "$baseUrl/v$TargetVersion/outputs.tf" if (-not $curVars -or -not $tgtVars -or -not $curOuts -or -not $tgtOuts) { return $diff } $diff.available = $true $curVarMap = ParseTfVariables -Content $curVars $tgtVarMap = ParseTfVariables -Content $tgtVars $curOutMap = ParseTfOutputs -Content $curOuts $tgtOutMap = ParseTfOutputs -Content $tgtOuts foreach ($v in $tgtVarMap.Keys) { if (-not $curVarMap.ContainsKey($v)) { if (-not $tgtVarMap[$v].hasDefault) { $diff.addedRequired += $v } else { $diff.addedOptional += $v } } elseif ($curVarMap[$v].type -ne $tgtVarMap[$v].type) { $diff.typeChanged += "$v ($($curVarMap[$v].type) -> $($tgtVarMap[$v].type))" } } foreach ($v in $curVarMap.Keys) { if (-not $tgtVarMap.ContainsKey($v)) { $diff.removedInputs += $v } } foreach ($o in $curOutMap) { if ($o -notin $tgtOutMap) { $diff.removedOutputs += $o } } foreach ($o in $tgtOutMap) { if ($o -notin $curOutMap) { $diff.addedOutputs += $o } } return $diff } function ParseTfVariables { param([string]$Content) $vars = @{} $blockPattern = [regex]'(?s)variable\s+"(\w+)"\s*\{([^}]+)\}' foreach ($m in $blockPattern.Matches($Content)) { $name = $m.Groups[1].Value $body = $m.Groups[2].Value $typeMatch = [regex]::Match($body, 'type\s*=\s*(\S+)') $type = if ($typeMatch.Success) { $typeMatch.Groups[1].Value } else { 'any' } $hasDefault = $body -match 'default\s*=' $vars[$name] = @{ type = $type; hasDefault = $hasDefault } } return $vars } function ParseTfOutputs { param([string]$Content) $outputs = @() $pattern = [regex]'output\s+"(\w+)"\s*\{' foreach ($m in $pattern.Matches($Content)) { $outputs += $m.Groups[1].Value } return $outputs } |