Private/Context/Get-AvmModuleContextInternal.ps1
|
function Get-AvmModuleContextInternal { <# .SYNOPSIS Walk up the filesystem from $Path looking for the nearest Bicep or Terraform module/monorepo signature defined in the consolidation plan section 5. Throws AvmContextException when nothing matches. .DESCRIPTION Private helper used by the public Get-AvmModuleContext verb. Resolution order (highest precedence first): 1. A committed .avm/context.psd1 override file anywhere up the tree from $Path (see Read-AvmContextOverride for the schema). 2. Heuristic detection. Rules at each directory: a. Bicep monorepo root: bicepconfig.json + avm/{res,ptn,utl}/ b. Terraform module repo: terraform.tf + examples/ + tests/ c. Bicep module path: main.bicep + version.json d. Terraform module path: any *.tf + tests/ The 'module path' rules can match anywhere inside a monorepo or repo; the 'repo' rules only match at the repo root. We walk upward from the starting directory once for each tier and prefer the more specific module-path match. When a bicep module sits inside a monorepo, the Scope field gets populated from 'avm/<scope>/<name>/'. $Ecosystem filters the heuristic phase: when set to 'bicep' or 'terraform' we only consider rules in that ecosystem. The override file phase always runs regardless because the override is intended to be the final word on classification. #> [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory)] [string] $Path, [ValidateSet('auto', 'bicep', 'terraform')] [string] $Ecosystem = 'auto' ) Set-StrictMode -Version 3.0 $ErrorActionPreference = 'Stop' if (-not (Test-Path -LiteralPath $Path)) { throw [AvmContextException]::new("Path does not exist: $Path") } $start = (Resolve-Path -LiteralPath $Path).ProviderPath if ((Get-Item -LiteralPath $start).PSIsContainer -eq $false) { $start = Split-Path -Parent $start } # 1. Committed override file. Highest precedence. $override = Read-AvmContextOverride -Path $start if ($override) { if ($Ecosystem -ne 'auto' -and $override.Ecosystem -ne $Ecosystem) { throw [AvmContextException]::new( "Ecosystem '$Ecosystem' conflicts with .avm/context.psd1 at $($override.Root) which declares '$($override.Ecosystem)'.") } return $override } $tryBicep = $Ecosystem -in @('auto', 'bicep') $tryTerraform = $Ecosystem -in @('auto', 'terraform') # 2a. Try repo-root rules walking up from start. $dir = $start $rootHit = $null while ($dir) { if ($tryBicep) { $bicepCfg = Join-Path $dir 'bicepconfig.json' if (Test-Path -LiteralPath $bicepCfg) { foreach ($scope in @('res', 'ptn', 'utl')) { $sub = Join-Path (Join-Path $dir 'avm') $scope if (Test-Path -LiteralPath $sub -PathType Container) { $rootHit = [pscustomobject][ordered]@{ Kind = 'bicep-monorepo' Root = $dir Ecosystem = 'bicep' Scope = $null Owner = $null } break } } } } if (-not $rootHit -and $tryTerraform) { $tfRoot = Join-Path $dir 'terraform.tf' $exDir = Join-Path $dir 'examples' $teDir = Join-Path $dir 'tests' if ((Test-Path -LiteralPath $tfRoot) -and (Test-Path -LiteralPath $exDir -PathType Container) -and (Test-Path -LiteralPath $teDir -PathType Container)) { $rootHit = [pscustomobject][ordered]@{ Kind = 'terraform-module-repo' Root = $dir Ecosystem = 'terraform' Scope = $null Owner = $null } } } if ($rootHit) { break } $parent = Split-Path -Parent $dir if (-not $parent -or $parent -eq $dir) { break } $dir = $parent } # 2b. Try module-path rules walking up from start. $dir = $start $pathHit = $null while ($dir) { if ($tryBicep) { $mainBicep = Join-Path $dir 'main.bicep' $verJson = Join-Path $dir 'version.json' if ((Test-Path -LiteralPath $mainBicep) -and (Test-Path -LiteralPath $verJson)) { $pathHit = [pscustomobject][ordered]@{ Kind = 'bicep-module' Root = $dir Ecosystem = 'bicep' Scope = $null Owner = $null } break } } if ($tryTerraform) { $testsDir = Join-Path $dir 'tests' if (Test-Path -LiteralPath $testsDir -PathType Container) { $tfs = Get-ChildItem -LiteralPath $dir -Filter '*.tf' -File -ErrorAction SilentlyContinue if ($tfs) { $pathHit = [pscustomobject][ordered]@{ Kind = 'terraform-module-path' Root = $dir Ecosystem = 'terraform' Scope = $null Owner = $null } break } } } $parent = Split-Path -Parent $dir if (-not $parent -or $parent -eq $dir) { break } $dir = $parent } # Scope detection: bicep module inside a monorepo. if ($pathHit -and $pathHit.Kind -eq 'bicep-module' -and $rootHit -and $rootHit.Kind -eq 'bicep-monorepo') { $rel = $pathHit.Root.Substring($rootHit.Root.Length).TrimStart([char]'/', [char]'\') $parts = $rel -split '[\\/]' if ($parts.Count -ge 3 -and $parts[0] -ceq 'avm' -and $parts[1] -in @('res', 'ptn', 'utl')) { $pathHit.Scope = $parts[1] } } # Resolution priority: module-path (more specific) over repo-root, EXCEPT # when both tiers match at the same directory. In that case the repo-root # classification is more specific (e.g. a Terraform module repo is also # technically a terraform-module-path, but the repo signature dominates). if ($pathHit -and $rootHit -and $pathHit.Root -eq $rootHit.Root) { return $rootHit } if ($pathHit) { return $pathHit } if ($rootHit) { return $rootHit } $hint = if ($Ecosystem -ne 'auto') { " (Ecosystem='$Ecosystem' filter applied)" } else { '' } throw [AvmContextException]::new( "No Bicep or Terraform module context found starting from '$start' upward$hint. " + "Expected one of: bicepconfig.json+avm/, terraform.tf+examples+tests, main.bicep+version.json, or *.tf+tests. " + "To override, place a .avm/context.psd1 file at the repo root.") } |