content/src/scripts/Resolve-ResourceName.ps1
|
# --------------------------------------------------------------------------- # Resolve-ResourceName.ps1 # Axeon Naming Convention Engine # # A token-based, pattern-driven naming resolver that produces Azure resource # names from a declarative configuration in platform-spec.json. # # TOKEN SYSTEM # ──────────── # Tokens are placeholders in patterns, enclosed in braces: {org}, {env}, etc. # Each token has three length variants: # s = short (e.g. "ax", "p", "vnet") # m = medium (e.g. "axeon", "prod", "vnetwork") # l = long (e.g. "axeon-global", "production", "virtual-network") # # IN-PATTERN LENGTH OVERRIDE # ────────────────────────── # A pattern can force a specific length for any token: # {org} → uses the default length (naming.defaultLength) # {org:s} → always uses short # {org:m} → always uses medium # {org:l} → always uses long # # RESOLUTION ORDER # ──────────────── # 1. Look for a resource-specific override in naming.overrides.{resourceType} # 2. Look for a resource-specific pattern in naming.patterns.{resourceType} # 3. Fall back to naming.patterns.default # 4. Resolve each {token[:length]} using naming.tokens + naming.resources # 5. Join segments with the configured separator # 6. Apply casing, charset filtering, and maxLength constraints # # CHARSET CONSTRAINTS # ─────────────────── # alphanumeric → [a-z0-9] only (e.g. storage accounts) # alphanumericHyphen → [a-z0-9-] (e.g. key vaults) # any → no filtering (default) # --------------------------------------------------------------------------- <# .SYNOPSIS Resolves an Azure resource name using the Axeon naming convention engine. .DESCRIPTION Takes the naming configuration from platform-spec.json and a resource type key, resolves all tokens, applies constraints, and returns the final name. .PARAMETER NamingConfig The naming hashtable from platform-spec (i.e. $spec.naming). .PARAMETER ResourceType The logical resource type key (e.g. "resourceGroup", "storageAccount"). .PARAMETER TokenOverrides Optional hashtable of token values to override for this specific call. For example: @{ idx = @{ s='1'; m='02'; l='002' } } Or shorthand: @{ idx = '1' } (uses the value for all lengths) .PARAMETER Index Shorthand for overriding the idx token with a specific number. Automatically generates s/m/l variants: 0 → "0"/"01"/"001". .PARAMETER FailoverKind Shorthand for overriding the fok token. Accepts "primary" or "secondary". .EXAMPLE Resolve-ResourceName -NamingConfig $spec.naming -ResourceType 'resourceGroup' # → "rg-ax-lza-p-uks-shr-0" .EXAMPLE Resolve-ResourceName -NamingConfig $spec.naming -ResourceType 'storageAccount' -Index 1 # → "axlzapuksst1" .EXAMPLE Resolve-ResourceName -NamingConfig $spec.naming -ResourceType 'virtualNetwork' -TokenOverrides @{ typ = @{ s='wl'; m='workload'; l='workload-instance' } } # → "vnet-ax-lza-p-uks-wl-0" #> function Resolve-ResourceName { [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory)] [hashtable]$NamingConfig, [Parameter(Mandatory)] [string]$ResourceType, [hashtable]$TokenOverrides = @{}, [int]$Index = -1, [ValidateSet('primary', 'secondary', 'tertiary')] [string]$FailoverKind ) # ── Merge defaults with resource-specific overrides ────────────────── $defaults = @{ separator = $NamingConfig.separator ?? '-' casing = $NamingConfig.casing ?? 'lower' defaultLength = $NamingConfig.defaultLength ?? 's' charset = 'any' maxLength = 0 # 0 = unlimited } $override = if ($NamingConfig.ContainsKey('overrides') -and $NamingConfig.overrides.ContainsKey($ResourceType)) { $NamingConfig.overrides[$ResourceType] } else { @{} } $separator = $override.separator ?? $defaults.separator $casing = $override.casing ?? $defaults.casing $defaultLength = $override.defaultLength ?? $defaults.defaultLength $charset = $override.charset ?? $defaults.charset $maxLength = [int]($override.maxLength ?? $defaults.maxLength) # ── Select pattern ─────────────────────────────────────────────────── $patterns = $NamingConfig.patterns ?? @{} $pattern = if ($override.ContainsKey('pattern')) { $override.pattern } elseif ($patterns.ContainsKey($ResourceType)) { $patterns[$ResourceType] } elseif ($patterns.ContainsKey('default')) { $patterns['default'] } else { throw "No naming pattern found for resource type '$ResourceType' and no default pattern defined." } # ── Build the token value map ──────────────────────────────────────── # Start from configured tokens $tokenMap = @{} if ($NamingConfig.ContainsKey('tokens')) { foreach ($key in $NamingConfig.tokens.Keys) { $tokenMap[$key] = $NamingConfig.tokens[$key] } } # Add the special {rsc} token from the resources catalog if ($NamingConfig.ContainsKey('resources') -and $NamingConfig.resources.ContainsKey($ResourceType)) { $tokenMap['rsc'] = $NamingConfig.resources[$ResourceType] } else { # If resource type isn't cataloged, use the key itself $tokenMap['rsc'] = @{ s = $ResourceType; m = $ResourceType; l = $ResourceType } } # Apply shorthand Index override → generates s/m/l if ($Index -ge 0) { $TokenOverrides['idx'] = @{ s = "$Index" m = $Index.ToString('D2') l = $Index.ToString('D3') } } # Apply shorthand FailoverKind override if ($FailoverKind) { $fokMap = @{ primary = @{ s = 'pri'; m = 'primary'; l = 'active-region' } secondary = @{ s = 'sec'; m = 'secondary'; l = 'standby-region' } tertiary = @{ s = 'ter'; m = 'tertiary'; l = 'tertiary-region' } } $TokenOverrides['fok'] = $fokMap[$FailoverKind] } # Merge explicit overrides (they win) foreach ($key in $TokenOverrides.Keys) { $val = $TokenOverrides[$key] if ($val -is [hashtable]) { $tokenMap[$key] = $val } else { # Scalar shorthand → same value for all lengths $tokenMap[$key] = @{ s = "$val"; m = "$val"; l = "$val" } } } # ── Resolve tokens in the pattern ──────────────────────────────────── # Regex matches {token} or {token:length} $resolved = [regex]::Replace($pattern, '\{(\w+)(?::([sml]))?\}', { param($match) $tokenName = $match.Groups[1].Value $lengthHint = $match.Groups[2].Value if (-not $lengthHint) { $lengthHint = $defaultLength } if ($tokenMap.ContainsKey($tokenName)) { $entry = $tokenMap[$tokenName] if ($entry -is [hashtable] -and $entry.ContainsKey($lengthHint)) { return $entry[$lengthHint] } elseif ($entry -is [hashtable]) { # Fall back: try s → m → l return ($entry['s'] ?? $entry['m'] ?? $entry['l'] ?? $tokenName) } else { return "$entry" } } # If token not found, leave placeholder for debugging Write-Warning "Naming token '{$tokenName}' not defined — left as placeholder." return "{$tokenName}" }) # ── Post-processing ────────────────────────────────────────────────── # Replace any remaining separators from token values (e.g. "axeon-global") # with the configured separator, then clean up double separators if ($separator -eq '') { $resolved = $resolved -replace '-', '' } else { # Collapse multiple consecutive separators into one $resolved = $resolved -replace "[${separator}]{2,}", $separator # Trim leading/trailing separators $resolved = $resolved.Trim($separator) } # Casing $resolved = switch ($casing) { 'lower' { $resolved.ToLower() } 'upper' { $resolved.ToUpper() } default { $resolved } } # Charset filtering $resolved = switch ($charset) { 'alphanumeric' { ($resolved -replace '[^a-zA-Z0-9]', '') } 'alphanumericHyphen' { ($resolved -replace '[^a-zA-Z0-9-]', '') } default { $resolved } } # Max length (truncate from the right) if ($maxLength -gt 0 -and $resolved.Length -gt $maxLength) { $resolved = $resolved.Substring(0, $maxLength) # Don't end with a separator $resolved = $resolved.TrimEnd($separator) } return $resolved } <# .SYNOPSIS Resolves all resource names in a single call and returns a hashtable. .DESCRIPTION Iterates over all resource types defined in naming.resources and resolves each one, returning a lookup table. Useful for passing to Bicep parameters. .PARAMETER NamingConfig The naming hashtable from platform-spec. .PARAMETER TokenOverrides Optional overrides applied to all resource types. .PARAMETER Index Instance index to apply globally. .EXAMPLE $names = Resolve-AllResourceNames -NamingConfig $spec.naming -Index 0 $names.resourceGroup # → "rg-ax-lza-p-uks-shr-0" $names.storageAccount # → "axlzapuksst0" $names.virtualNetwork # → "vnet-ax-lza-p-uks-shr-0" #> function Resolve-AllResourceNames { [CmdletBinding()] [OutputType([hashtable])] param( [Parameter(Mandatory)] [hashtable]$NamingConfig, [hashtable]$TokenOverrides = @{}, [int]$Index = -1, [string]$FailoverKind ) $result = @{} $resources = $NamingConfig.resources ?? @{} foreach ($resourceType in $resources.Keys) { $params = @{ NamingConfig = $NamingConfig ResourceType = $resourceType TokenOverrides = $TokenOverrides } if ($Index -ge 0) { $params['Index'] = $Index } if ($FailoverKind) { $params['FailoverKind'] = $FailoverKind } $result[$resourceType] = Resolve-ResourceName @params } return $result } <# .SYNOPSIS Validates the naming configuration for completeness and correctness. .DESCRIPTION Checks that all tokens referenced in patterns are defined, that required sections exist, and that length variants are consistent. .PARAMETER NamingConfig The naming hashtable from platform-spec. #> function Test-NamingConfig { [CmdletBinding()] param( [Parameter(Mandatory)] [hashtable]$NamingConfig ) $errors = @() # Must have tokens if (-not $NamingConfig.ContainsKey('tokens') -or $NamingConfig.tokens.Count -eq 0) { $errors += "naming.tokens is missing or empty" } # Must have at least a default pattern if (-not $NamingConfig.ContainsKey('patterns') -or -not $NamingConfig.patterns.ContainsKey('default')) { $errors += "naming.patterns.default is required" } # Validate each token has s/m/l if ($NamingConfig.ContainsKey('tokens')) { foreach ($key in $NamingConfig.tokens.Keys) { $token = $NamingConfig.tokens[$key] foreach ($len in @('s', 'm', 'l')) { if (-not $token.ContainsKey($len)) { $errors += "Token '$key' is missing length variant '$len'" } } } } # Validate resource types have s/m/l if ($NamingConfig.ContainsKey('resources')) { foreach ($key in $NamingConfig.resources.Keys) { $rsc = $NamingConfig.resources[$key] foreach ($len in @('s', 'm', 'l')) { if (-not $rsc.ContainsKey($len)) { $errors += "Resource type '$key' is missing length variant '$len'" } } } } # Validate that all tokens in patterns are resolvable $allTokenNames = @() if ($NamingConfig.ContainsKey('tokens')) { $allTokenNames += $NamingConfig.tokens.Keys } $allTokenNames += 'rsc' # always available $allPatterns = @() if ($NamingConfig.ContainsKey('patterns')) { $allPatterns += $NamingConfig.patterns.Values } if ($NamingConfig.ContainsKey('overrides')) { foreach ($ov in $NamingConfig.overrides.Values) { if ($ov.ContainsKey('pattern')) { $allPatterns += $ov.pattern } } } foreach ($pat in $allPatterns) { $matches = [regex]::Matches($pat, '\{(\w+)(?::[sml])?\}') foreach ($m in $matches) { $tName = $m.Groups[1].Value if ($tName -notin $allTokenNames) { $errors += "Pattern '$pat' references undefined token '{$tName}'" } } } # Report if ($errors.Count -gt 0) { foreach ($e in $errors) { Write-Warning "Naming config: $e" } return $false } return $true } <# .SYNOPSIS Prints a formatted preview table of all resolved resource names. .DESCRIPTION Useful for debugging/reviewing naming conventions before deployment. .PARAMETER NamingConfig The naming hashtable from platform-spec. .PARAMETER Index Instance index for preview. #> function Show-NamingPreview { [CmdletBinding()] param( [Parameter(Mandatory)] [hashtable]$NamingConfig, [int]$Index = 0 ) Write-Host "" Write-Host " Axeon Naming Convention Preview" -ForegroundColor Cyan Write-Host " ═══════════════════════════════════════════════════════" -ForegroundColor DarkCyan Write-Host "" $resources = $NamingConfig.resources ?? @{} # Column widths $maxKey = ($resources.Keys | ForEach-Object { $_.Length } | Measure-Object -Maximum).Maximum $maxKey = [Math]::Max($maxKey, 20) Write-Host (" {0,-$maxKey} {1,-35} {2}" -f "Resource Type", "Name (short)", "Name (long)") -ForegroundColor DarkGray Write-Host (" {0,-$maxKey} {1,-35} {2}" -f ("-" * $maxKey), ("-" * 35), ("-" * 35)) -ForegroundColor DarkGray foreach ($resourceType in ($resources.Keys | Sort-Object)) { # Resolve with short default $shortConfig = Copy-NamingConfig $NamingConfig $shortConfig.defaultLength = 's' $shortName = Resolve-ResourceName -NamingConfig $shortConfig -ResourceType $resourceType -Index $Index # Resolve with long default $longConfig = Copy-NamingConfig $NamingConfig $longConfig.defaultLength = 'l' $longName = Resolve-ResourceName -NamingConfig $longConfig -ResourceType $resourceType -Index $Index Write-Host (" {0,-$maxKey} {1,-35} {2}" -f $resourceType, $shortName, $longName) -ForegroundColor White } Write-Host "" } # ── Internal helper: deep-copy a naming config hashtable ───────────────── function Copy-NamingConfig { param([hashtable]$Source) $json = $Source | ConvertTo-Json -Depth 10 return ($json | ConvertFrom-Json -AsHashtable) } |