AzureCAF.psm1
|
function Export-CafResourceType { <# .SYNOPSIS Exports the Cloud Adoption Framework naming rules for one or more resource types to a CSV or Markdown file. .DESCRIPTION Writes the ResourceType/Resource/Slug/Scope/length/casing fields returned by Get-CafResourceType to a file, as a reference table a team can review without running PowerShell - e.g. to print, link from a wiki page, or attach to a design doc. The format is chosen from -Path's extension: .csv or .md (.markdown also works). Accepts the same objects Get-CafResourceType returns on the pipeline, so you can export a filtered subset; with nothing piped in, it exports every resource type the module knows about. .PARAMETER Path Where to write the export. Must end in .csv, .md or .markdown. .PARAMETER InputObject Resource type definitions to export, as returned by Get-CafResourceType. Defaults to every resource type the module knows about (equivalent to piping in `Get-CafResourceType`) if nothing is supplied. .PARAMETER Force Overwrite -Path if it already exists. .EXAMPLE Export-CafResourceType -Path ./resource-types.md Writes a Markdown table of every resource type the module knows about. .EXAMPLE Get-CafResourceType -ResourceType *storage* | Export-CafResourceType -Path ./storage-types.csv Exports just the storage-related resource types to CSV. .LINK https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/azure-best-practices/resource-abbreviations #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory, Position = 0)] [ValidateNotNullOrEmpty()] [string] $Path, [Parameter(ValueFromPipeline)] [object[]] $InputObject, [switch] $Force ) begin { $Items = [System.Collections.Generic.List[object]]::new() } process { foreach ($Item in $InputObject) { $Items.Add($Item) } } end { $Extension = [System.IO.Path]::GetExtension($Path).ToLowerInvariant() if ($Extension -notin '.csv', '.md', '.markdown') { Write-Error "'$Path' must end in .csv, .md or .markdown." return } if ($Items.Count -eq 0) { $Items.AddRange(@(Get-CafResourceType)) } if ((Test-Path -Path $Path) -and -not $Force) { Write-Error "'$Path' already exists. Specify -Force to overwrite it." return } if (-not $PSCmdlet.ShouldProcess($Path, "Export $($Items.Count) resource type(s)")) { return } $Properties = 'ResourceType', 'Resource', 'Slug', 'Scope', 'MinLength', 'MaxLength', 'Lowercase', 'Dashes' if ($Extension -eq '.csv') { $Items | Select-Object -Property $Properties | Export-Csv -Path $Path -NoTypeInformation } else { $Lines = [System.Collections.Generic.List[string]]::new() $Lines.Add("| $($Properties -join ' | ') |") $Lines.Add("| $(($Properties | ForEach-Object { '---' }) -join ' | ') |") foreach ($Item in $Items) { $Cells = foreach ($Property in $Properties) { "$($Item.$Property)".Replace('|', '\|') } $Lines.Add("| $($Cells -join ' | ') |") } Set-Content -Path $Path -Value $Lines } Get-Item -Path $Path } } function Get-CafRegion { <# .SYNOPSIS Looks up the abbreviation for one or more Azure regions. .DESCRIPTION Searches the module's bundled regionDefinition.json for Azure regions and returns the short abbreviation Get-CafResourceName's -Region parameter resolves it to (e.g. 'eastus' -> 'eus'), along with its Azure-paired region (e.g. 'eastus' -> 'westus'), if it has one. This is a local, offline lookup against a static data set bundled with the module - it's not a replacement for Get-AzLocation (from the Az.Resources module), which calls the live Azure Resource Manager API to report which regions are available to a subscription right now. Region abbreviations aren't officially published by Microsoft for every region - the bundled list is sourced from https://www.jlaundry.nz/2022/azure_region_abbreviations/, a community reference, and may not cover newly-announced regions. PairedRegion is sourced from Microsoft's official https://learn.microsoft.com/en-us/azure/reliability/regions-list and is $null for a region that doesn't have one - newer regions increasingly don't, having moved to availability zones instead of a paired region for resiliency. PairedRegion is informational only; Get-CafResourceName's -Region doesn't resolve against it. .PARAMETER Region The ARM region name to search for, e.g. 'eastus', its display name, e.g. 'East US', or its CAF slug, e.g. 'eus'. The ARM name and display name match as a substring by default, same as -ResourceType on Get-CafResourceType, so 'east' alone matches multiple regions - the slug only matches exactly, since it's short enough that partial matching on it would mostly just be ambiguous. If omitted, every known region is returned. .EXAMPLE Get-CafRegion -Region eastus Returns the 'eus' abbreviation for East US, along with its paired region, 'westus'. .EXAMPLE Get-CafRegion -Region 'East US' Looks up the same region by its display name instead of its ARM name. .EXAMPLE Get-CafRegion -Region eus Looks up the same region by its slug instead. .EXAMPLE Get-CafRegion Lists every region known to the module. .LINK https://www.jlaundry.nz/2022/azure_region_abbreviations/ #> [CmdletBinding()] param( [Parameter(Position = 0)] [string] $Region ) $AllDefinitions = @(Get-CafRegionDefinition) $definitions = $AllDefinitions if ($Region) { $definitions = @($AllDefinitions | Where-Object Slug -EQ $Region) if ($definitions.Count -eq 0) { $definitions = Select-CafMatch -InputObject $AllDefinitions -Property Region -Value $Region } if ($definitions.Count -eq 0) { $definitions = Select-CafMatch -InputObject $AllDefinitions -Property DisplayName -Value $Region } } $definitions | Select-Object Region, DisplayName, Slug, PairedRegion } function Get-CafResourceName { <# .SYNOPSIS Generates an Azure resource name that follows the Cloud Adoption Framework naming convention, returning just the name as a string. .DESCRIPTION A thin wrapper around New-CafResourceName that returns just the generated Name string, for callers who don't need the ResourceType/Resource/Slug/InstanceNumber/RandomSuffix that produced it. Every parameter here behaves exactly as it does on New-CafResourceName - see its help for the full description of each one, the length-budget/trimming/cleaning behaviour, and -ConventionPath support. Use New-CafResourceName directly instead if you need to tell generated names apart when batch-generating across different resource types and/or -Count, or to export the result (e.g. to CSV) alongside the values that produced it. .PARAMETER ResourceType The ARM resource type to generate a name for, e.g. 'Microsoft.Compute/virtualMachines'. Matches as a substring by default, so 'virtualMachines' also works - as long as it narrows down to a single resource type, or you also specify -Resource. Tab completion lists the resource types known to the module. Normally used on its own (or -Alias, or -Resource, used instead); takes precedence if more than one happens to be specified, e.g. by piping in an object that has ResourceType, Alias and Resource properties all describing the same entry. .PARAMETER Alias The Cloud Adoption Framework abbreviation to generate a name for, e.g. 'vmss', if you already know it, used as a short alias for the ARM resource type. Matches as a substring by default, same as -ResourceType, and also accepts -Slug as an alias (so piping Get-CafResourceType's output straight in binds to this by its Slug property). Normally used on its own, instead of -ResourceType. .PARAMETER Resource The friendly name of the resource to generate, e.g. 'Key vault' - every such name in the module's data is unique, so on its own this works as a third alternative to -ResourceType/ -Alias (also matching as a substring, so 'vault' alone works too). It also doubles as a disambiguator when combined with -ResourceType/-Alias, for the handful of ARM types used by more than one kind of resource (for example 'Microsoft.Web/sites' covers both a "Web app" and a "Function app", with different recommended abbreviations) - required in that case, since -ResourceType/-Alias alone would otherwise be ambiguous. .PARAMETER Prefix One or more strings to prepend to the resource name, in order, e.g. a company name, environment, or region. Falls back to -ConventionPath's value if not specified. .PARAMETER Suffix One or more strings to append to the resource name, in order. Falls back to -ConventionPath's value if not specified. .PARAMETER Region An Azure region to append to the resource name, as its ARM name (e.g. 'eastus'), display name (e.g. 'East US') or its CAF slug (e.g. 'eus') - resolved to the CAF-recommended abbreviation the same way -ResourceType resolves to -Slug, and inserted after any -Suffix but before an instance number, so a -Suffix you specify always comes right after the resource type's slug. Already passing the slug in is a no-op that just confirms it's a recognized one. Falls back to -ConventionPath's value if not specified. Use Get-CafRegion to look up the abbreviation a given region resolves to, or to see every region the module knows about. Region abbreviations aren't officially published by Microsoft for every region, so an unrecognized value errors rather than guessing - pass the code yourself via -Suffix instead if that happens. .PARAMETER Separator The character(s) used to join the prefix, abbreviation and suffix segments together. Ignored for resource types that don't permit a separator character, in which case the segments are concatenated directly. Defaults to '-', or to -ConventionPath's value if it specifies one. .PARAMETER InstanceNumber An instance number to append to the resource name, for differentiating otherwise-identical resources (e.g. 'vm-sql-test-001', 'vm-sql-test-002' in the Cloud Adoption Framework's own naming examples). Zero-padded to -InstanceNumberPadding digits and reserved out of the resource type's maximum length before the rest of the name is trimmed, so it's never cut short. .PARAMETER InstanceNumberPadding The minimum number of digits to zero-pad -InstanceNumber to, e.g. the default of 3 formats '1' as '001'. A number that's already at least this many digits long is left as-is. Ignored unless -InstanceNumber is specified. Falls back to -ConventionPath's value if not specified. .PARAMETER Count Generates this many names in one call, with sequential instance numbers, instead of just one. Starts from -InstanceNumber if specified (e.g. -InstanceNumber 5 -Count 3 produces 005, 006, 007), or from 1 otherwise. Implies an instance number even if -InstanceNumber isn't specified, since otherwise every generated name would be identical. Each generated name gets its own random suffix if -RandomSuffixLength is also specified. .PARAMETER RandomSuffixLength Appends this many random lowercase alphanumeric characters to the end of the name, for scenarios that require a globally unique name (such as a storage account or key vault). Always lowercase, even for a resource type that otherwise allows uppercase - the suffix exists for uniqueness, not communication, and doesn't need the extra entropy. The characters are reserved out of the resource type's maximum length before the rest of the name is trimmed, so the random suffix is never cut short. Falls back to -ConventionPath's value if not specified. .PARAMETER RandomSuffixSeed Seeds the random number generator used by -RandomSuffixLength, so the same seed (with the same other parameters) always produces the same random characters - useful if you need to generate the same name idempotently, e.g. across repeated deployments. Ignored unless -RandomSuffixLength is specified. .PARAMETER ConventionPath Path to a JSON file of organizational naming-convention defaults for -Prefix, -Suffix, -Region, -Separator, -InstanceNumberPadding and -RandomSuffixLength, created with New-CafNamingConvention. Any of those you also specify explicitly (including via the pipeline) take precedence over the file - this just saves retyping the same org-standard values on every call. .PARAMETER NoClean By default, characters that aren't valid for the given resource type are stripped from -Prefix and -Suffix before the name is built. Specify -NoClean to pass them through unmodified instead (the generated name will then fail validation if it contains invalid characters). .EXAMPLE Get-CafResourceName -ResourceType Microsoft.Compute/virtualMachines -Prefix contoso, prod -Suffix 01 Generates a virtual machine name such as 'contoso-prod-vm-01'. .EXAMPLE Get-CafResourceName -ResourceType KeyVault -Prefix contoso -ResourceType matches as a substring, so this is equivalent to specifying the full 'Microsoft.KeyVault/vaults' ARM type. If a substring matches more than one resource type, narrow it down with a longer -ResourceType value and/or -Resource. .EXAMPLE Get-CafResourceName -Alias vmss -Prefix contoso If you already know the Cloud Adoption Framework abbreviation, -Alias (or its -Slug alias) is a shorter alternative to -ResourceType. .EXAMPLE Get-CafResourceName -Resource 'Key vault' -Prefix contoso -Resource alone works too, since every friendly name is unique - a third alternative to -ResourceType/-Alias for when you only remember the resource's plain-English name. .EXAMPLE Get-CafResourceName -ResourceType Microsoft.Compute/virtualMachines -Prefix contoso, prod -Region eastus Generates 'contoso-prod-vm-eus' - -Region resolves 'eastus' to its CAF-recommended abbreviation the same way -ResourceType resolves to its slug, and inserts it after any -Suffix, so -Suffix always comes right after the resource type's slug. .EXAMPLE Get-CafResourceName -ResourceType Microsoft.Compute/virtualMachines -Prefix contoso, prod -InstanceNumber 1 Generates 'contoso-prod-vm-001' - -InstanceNumber is zero-padded to 3 digits by default, matching the Cloud Adoption Framework's own example names such as 'vm-sql-test-001'. .EXAMPLE Get-CafResourceName -ResourceType Microsoft.Compute/virtualMachines -Prefix contoso, prod -Count 3 Generates 'contoso-prod-vm-001', 'contoso-prod-vm-002' and 'contoso-prod-vm-003' in one call. .EXAMPLE Get-CafResourceName -ResourceType Microsoft.Storage/storageAccounts -Prefix contoso -RandomSuffixLength 6 Generates a storage account name such as 'contosostabcdef', with 6 random characters appended to help ensure the globally-unique name doesn't collide with one that already exists. .EXAMPLE Get-CafResourceName -ResourceType Microsoft.Storage/storageAccounts -Prefix contoso -RandomSuffixLength 6 -RandomSuffixSeed 42 Generates the same storage account name every time it's run, since the random suffix is seeded. .EXAMPLE Get-CafResourceName -ResourceType Microsoft.Web/sites -Resource 'Function app' -Prefix contoso, prod Generates a function app name. -Resource is required here because 'Microsoft.Web/sites' is also used for web apps, which use a different abbreviation. .EXAMPLE Get-CafResourceType -Slug kv | Get-CafResourceName -Prefix contoso -RandomSuffixLength 4 Pipes the resource type definition for the 'kv' (Key Vault) abbreviation straight into Get-CafResourceName. .EXAMPLE Get-CafResourceName -ResourceType Microsoft.Compute/virtualMachines -Prefix contoso -ConventionPath ./contoso.json Uses -Prefix explicitly, but falls back to whatever -Suffix/-Region/-Separator/etc. are saved in contoso.json (see New-CafNamingConvention) for anything not specified here. .EXAMPLE Import-Csv ./resources.csv | Get-CafResourceName Batch-generates a name for every row of resources.csv, which needs at least a ResourceType or Alias column, and may also have Resource/Prefix/Suffix/Separator/InstanceNumber columns. .LINK https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/azure-best-practices/resource-abbreviations #> [CmdletBinding()] param( [Parameter(Position = 0, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [string] $ResourceType, [Parameter(ValueFromPipelineByPropertyName)] [Alias('Slug')] [ValidateNotNullOrEmpty()] [string] $Alias, [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [string] $Resource, [Parameter(ValueFromPipelineByPropertyName)] [string[]] $Prefix, [Parameter(ValueFromPipelineByPropertyName)] [string[]] $Suffix, [Parameter(ValueFromPipelineByPropertyName)] [string] $Region, [Parameter(ValueFromPipelineByPropertyName)] [string] $Separator = '-', [Parameter(ValueFromPipelineByPropertyName)] [ValidateRange(0, [int]::MaxValue)] [int] $InstanceNumber, [Parameter(ValueFromPipelineByPropertyName)] [ValidateRange(1, 10)] [int] $InstanceNumberPadding = 3, [Parameter(ValueFromPipelineByPropertyName)] [ValidateRange(1, 1000)] [int] $Count = 1, [Parameter(ValueFromPipelineByPropertyName)] [ValidateRange(0, 24)] [int] $RandomSuffixLength = 0, [Parameter(ValueFromPipelineByPropertyName)] [int] $RandomSuffixSeed, [string] $ConventionPath, [Parameter(ValueFromPipelineByPropertyName)] [switch] $NoClean ) begin { # Every parameter above exists only to be bound and forwarded to New-CafResourceName via the # proxy-command plumbing below (for CmdletBinding/tab-completion/help purposes), never read # directly here - this satisfies PSScriptAnalyzer's PSReviewUnusedParameter rule. $null = $ResourceType, $Alias, $Resource, $Prefix, $Suffix, $Region, $Separator, $InstanceNumber, $InstanceNumberPadding, $Count, $RandomSuffixLength, $RandomSuffixSeed, $ConventionPath, $NoClean # A hand-rolled `New-CafResourceName @PSBoundParameters` here would be unsafe: $PSBoundParameters # keeps stale values from a previous pipeline object once a later object lacks that property (a # known PowerShell quirk), which would leak e.g. one row's -Suffix into the next row's call. The # proxy-command pattern below instead feeds each pipeline object into New-CafResourceName's own # pipeline binding, which rebinds correctly per object. $WrappedCommand = $ExecutionContext.InvokeCommand.GetCommand('New-CafResourceName', [System.Management.Automation.CommandTypes]::Function) $ScriptCmd = { & $WrappedCommand @PSBoundParameters | ForEach-Object Name } $SteppablePipeline = $ScriptCmd.GetSteppablePipeline($MyInvocation.CommandOrigin) $SteppablePipeline.Begin($PSCmdlet) } process { $SteppablePipeline.Process($_) } end { $SteppablePipeline.End() } } function Get-CafResourceType { <# .SYNOPSIS Looks up the Cloud Adoption Framework naming rules for one or more Azure resource types. .DESCRIPTION Searches the module's bundled resourceDefinition.json for Azure resource types and returns their recommended CAF abbreviation along with the naming scope, length limits and casing rules that Get-CafResourceName uses to build a compliant name. This is a local, offline lookup against a static data set bundled with the module. It's not a replacement for Get-AzResourceProvider (from the Az.Resources module): that cmdlet calls the live Azure Resource Manager API to report which resource providers/types are registered and available in a subscription, and requires being signed in to Azure. Get-CafResourceType has no dependency on Az.Resources or an Azure session - it only answers "what's the recommended naming convention for this resource type", not "is this resource type available to me right now". .PARAMETER ResourceType The ARM resource type to search for, e.g. 'Microsoft.Compute/virtualMachines'. Matches as a substring by default, e.g. 'virtualMachines' or 'compute' both match it too - pass an explicit wildcard pattern (e.g. 'Microsoft.Compute/*') for tighter control, such as anchoring to the start. If omitted, all resource types are returned (optionally filtered by -Resource/-Alias). .PARAMETER Resource The friendly resource name to search for, e.g. 'Virtual machine'. Matches as a substring by default, same as -ResourceType. Useful to disambiguate ARM resource types that cover more than one kind of resource (for example 'Microsoft.Compute/disks' covers both a "Managed disk (OS)" and a "Managed disk (data)"). .PARAMETER Alias The Cloud Adoption Framework abbreviation to search for, e.g. 'vm', used as a short alias for the ARM resource type. Matches as a substring by default, same as -ResourceType/-Resource, and also accepts -Slug as an alias. .EXAMPLE Get-CafResourceType -ResourceType Microsoft.Compute/virtualMachines Returns the naming rule for an Azure virtual machine. .EXAMPLE Get-CafResourceType -ResourceType storageAccount Finds resource types whose ARM type contains 'storageAccount'. .EXAMPLE Get-CafResourceType -Alias vm Finds the resource type(s) that use the 'vm' abbreviation. .EXAMPLE Get-CafResourceType Lists every resource type known to the module. .LINK https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/azure-best-practices/resource-abbreviations #> [CmdletBinding()] param( [Parameter(Position = 0)] [string] $ResourceType, [string] $Resource, [Alias('Slug')] [string] $Alias ) $definitions = @(Get-CafResourceDefinition) if ($ResourceType) { $definitions = Select-CafMatch -InputObject $definitions -Property ResourceType -Value $ResourceType } if ($Resource) { $definitions = Select-CafMatch -InputObject $definitions -Property Resource -Value $Resource } if ($Alias) { $definitions = Select-CafMatch -InputObject $definitions -Property Slug -Value $Alias } $definitions | Select-Object ResourceType, Resource, Slug, Scope, MinLength, MaxLength, Lowercase, Dashes } function New-CafNamingConvention { <# .SYNOPSIS Saves a reusable set of Get-CafResourceName defaults (prefix, suffix, region, separator, etc.) to a JSON file, so your organization's naming convention doesn't need to be retyped on every call. .DESCRIPTION Writes a small JSON file that Get-CafResourceName's -ConventionPath reads defaults from for -Prefix, -Suffix, -Region, -Separator, -InstanceNumberPadding and -RandomSuffixLength. Anything you pass explicitly to Get-CafResourceName still takes precedence over the file - this is just a way to avoid repeating the same organization-standard values (company name, environment, region, your preferred separator/padding) on every call. -RandomSuffixSeed deliberately isn't one of these defaults, even though Get-CafResourceName has it - a convention file is meant to be the same shared values reused verbatim by everyone who points -ConventionPath at it, but a seed baked in that way would make the "random" suffix identical for every one of those calls, defeating the uniqueness -RandomSuffixLength exists for. It only makes sense as a per-call value (e.g. for one idempotent redeploy), so it stays something you pass to Get-CafResourceName directly. .PARAMETER Path Where to save the convention file, e.g. './contoso.json'. .PARAMETER Prefix One or more strings to use as the default -Prefix, e.g. a company name or environment - see the naming components guide this links to for what's commonly included. .PARAMETER Suffix One or more strings to use as the default -Suffix. .PARAMETER Region An Azure region (ARM name, display name or slug) to use as the default -Region - resolved and stored as its ARM region name, the same way Get-CafResourceName resolves it, so an unrecognized value is caught here rather than on every later call that uses the file. .PARAMETER Separator The default -Separator. Omit to leave this to Get-CafResourceName's own default ('-'). .PARAMETER InstanceNumberPadding The default -InstanceNumberPadding. Omit to leave this to Get-CafResourceName's own default (3). .PARAMETER RandomSuffixLength The default -RandomSuffixLength. Omit to leave this to Get-CafResourceName's own default (0, i.e. no random suffix unless a call asks for one explicitly). .PARAMETER Force Overwrite -Path if it already exists. .EXAMPLE New-CafNamingConvention -Path ./contoso.json -Prefix contoso, prod -Region eastus -Separator '-' -InstanceNumberPadding 2 Saves a convention so that Get-CafResourceName -ConventionPath ./contoso.json applies that prefix, region, separator and padding by default. .EXAMPLE Get-CafResourceName -ResourceType Microsoft.Compute/virtualMachines -ConventionPath ./contoso.json -InstanceNumber 1 Uses the saved convention, overriding only -InstanceNumber for this particular call. .LINK https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/azure-best-practices/resource-naming#choose-naming-components #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory, Position = 0)] [string] $Path, [string[]] $Prefix, [string[]] $Suffix, [string] $Region, [string] $Separator, [ValidateRange(1, 10)] [int] $InstanceNumberPadding, [ValidateRange(0, 24)] [int] $RandomSuffixLength, [switch] $Force ) if ((Test-Path -Path $Path) -and -not $Force) { Write-Error "'$Path' already exists. Specify -Force to overwrite it." return } $Convention = [ordered]@{} if ($PSBoundParameters.ContainsKey('Prefix')) { $Convention.Prefix = $Prefix } if ($PSBoundParameters.ContainsKey('Suffix')) { $Convention.Suffix = $Suffix } if ($PSBoundParameters.ContainsKey('Region')) { $RegionDefinition = Resolve-CafRegion -Region $Region if (-not $RegionDefinition) { return } $Convention.Region = $RegionDefinition.Region } if ($PSBoundParameters.ContainsKey('Separator')) { $Convention.Separator = $Separator } if ($PSBoundParameters.ContainsKey('InstanceNumberPadding')) { $Convention.InstanceNumberPadding = $InstanceNumberPadding } if ($PSBoundParameters.ContainsKey('RandomSuffixLength')) { $Convention.RandomSuffixLength = $RandomSuffixLength } if ($Convention.Count -eq 0) { Write-Error 'Specify at least one of -Prefix, -Suffix, -Region, -Separator, -InstanceNumberPadding or -RandomSuffixLength.' return } if ($PSCmdlet.ShouldProcess($Path, 'Create naming convention file')) { $Convention | ConvertTo-Json | Set-Content -Path $Path Get-Item -Path $Path } } function New-CafResourceName { <# .SYNOPSIS Generates an Azure resource name that follows the Cloud Adoption Framework naming convention, returning the full result as an object. .DESCRIPTION Builds a resource name from an optional prefix, the Cloud Adoption Framework recommended abbreviation for the given Azure resource type, and an optional suffix. The result is checked against the resource type's length limit and allowed characters, and is trimmed and/or converted to lowercase automatically if the resource type requires it. Returns an object with Name, ResourceType, Resource, Slug, InstanceNumber and RandomSuffix properties - use Get-CafResourceName instead if you just want the name as a plain string. Accepts pipeline input by property name, so a list of resource specs (e.g. imported from a CSV or a PSD1 describing a whole deployment) can be piped straight through to generate every name for a project in one batch - see the pipeline example below. Use Get-CafResourceType to discover the ResourceType/Resource values this cmdlet accepts. Use -Verbose to see the length-budget reasoning (reserved segments, trimming decisions) behind a given result. .PARAMETER ResourceType The ARM resource type to generate a name for, e.g. 'Microsoft.Compute/virtualMachines'. Matches as a substring by default, so 'virtualMachines' also works - as long as it narrows down to a single resource type, or you also specify -Resource. Tab completion lists the resource types known to the module. Normally used on its own (or -Alias, or -Resource, used instead); takes precedence if more than one happens to be specified, e.g. by piping in an object that has ResourceType, Alias and Resource properties all describing the same entry. .PARAMETER Alias The Cloud Adoption Framework abbreviation to generate a name for, e.g. 'vmss', if you already know it, used as a short alias for the ARM resource type. Matches as a substring by default, same as -ResourceType, and also accepts -Slug as an alias (so piping Get-CafResourceType's output straight in binds to this by its Slug property). Normally used on its own, instead of -ResourceType. .PARAMETER Resource The friendly name of the resource to generate, e.g. 'Key vault' - every such name in the module's data is unique, so on its own this works as a third alternative to -ResourceType/ -Alias (also matching as a substring, so 'vault' alone works too). It also doubles as a disambiguator when combined with -ResourceType/-Alias, for the handful of ARM types used by more than one kind of resource (for example 'Microsoft.Web/sites' covers both a "Web app" and a "Function app", with different recommended abbreviations) - required in that case, since -ResourceType/-Alias alone would otherwise be ambiguous. .PARAMETER Prefix One or more strings to prepend to the resource name, in order, e.g. a company name, environment, or region. Falls back to -ConventionPath's value if not specified. .PARAMETER Suffix One or more strings to append to the resource name, in order. Falls back to -ConventionPath's value if not specified. .PARAMETER Region An Azure region to append to the resource name, as its ARM name (e.g. 'eastus'), display name (e.g. 'East US') or its CAF slug (e.g. 'eus') - resolved to the CAF-recommended abbreviation the same way -ResourceType resolves to -Slug, and inserted after any -Suffix but before an instance number, so a -Suffix you specify always comes right after the resource type's slug. Already passing the slug in is a no-op that just confirms it's a recognized one. Falls back to -ConventionPath's value if not specified. Use Get-CafRegion to look up the abbreviation a given region resolves to, or to see every region the module knows about. Region abbreviations aren't officially published by Microsoft for every region, so an unrecognized value errors rather than guessing - pass the code yourself via -Suffix instead if that happens. .PARAMETER Separator The character(s) used to join the prefix, abbreviation and suffix segments together. Ignored for resource types that don't permit a separator character, in which case the segments are concatenated directly. Defaults to '-', or to -ConventionPath's value if it specifies one. .PARAMETER InstanceNumber An instance number to append to the resource name, for differentiating otherwise-identical resources (e.g. 'vm-sql-test-001', 'vm-sql-test-002' in the Cloud Adoption Framework's own naming examples). Zero-padded to -InstanceNumberPadding digits and reserved out of the resource type's maximum length before the rest of the name is trimmed, so it's never cut short. .PARAMETER InstanceNumberPadding The minimum number of digits to zero-pad -InstanceNumber to, e.g. the default of 3 formats '1' as '001'. A number that's already at least this many digits long is left as-is. Ignored unless -InstanceNumber is specified. Falls back to -ConventionPath's value if not specified. .PARAMETER Count Generates this many names in one call, with sequential instance numbers, instead of just one. Starts from -InstanceNumber if specified (e.g. -InstanceNumber 5 -Count 3 produces 005, 006, 007), or from 1 otherwise. Implies an instance number even if -InstanceNumber isn't specified, since otherwise every generated name would be identical. Each generated name gets its own random suffix if -RandomSuffixLength is also specified. .PARAMETER RandomSuffixLength Appends this many random lowercase alphanumeric characters to the end of the name, for scenarios that require a globally unique name (such as a storage account or key vault). Always lowercase, even for a resource type that otherwise allows uppercase - the suffix exists for uniqueness, not communication, and doesn't need the extra entropy. The characters are reserved out of the resource type's maximum length before the rest of the name is trimmed, so the random suffix is never cut short. Falls back to -ConventionPath's value if not specified. .PARAMETER RandomSuffixSeed Seeds the random number generator used by -RandomSuffixLength, so the same seed (with the same other parameters) always produces the same random characters - useful if you need to generate the same name idempotently, e.g. across repeated deployments. Ignored unless -RandomSuffixLength is specified. .PARAMETER ConventionPath Path to a JSON file of organizational naming-convention defaults for -Prefix, -Suffix, -Region, -Separator, -InstanceNumberPadding and -RandomSuffixLength, created with New-CafNamingConvention. Any of those you also specify explicitly (including via the pipeline) take precedence over the file - this just saves retyping the same org-standard values on every call. .PARAMETER NoClean By default, characters that aren't valid for the given resource type are stripped from -Prefix and -Suffix before the name is built. Specify -NoClean to pass them through unmodified instead (the generated name will then fail validation if it contains invalid characters). .EXAMPLE New-CafResourceName -ResourceType Microsoft.Compute/virtualMachines -Prefix contoso, prod -Suffix 01 Generates a virtual machine name such as 'contoso-prod-vm-01', returned as an object with Name, ResourceType, Resource, Slug, InstanceNumber and RandomSuffix properties. .EXAMPLE New-CafResourceName -ResourceType KeyVault -Prefix contoso -ResourceType matches as a substring, so this is equivalent to specifying the full 'Microsoft.KeyVault/vaults' ARM type. If a substring matches more than one resource type, narrow it down with a longer -ResourceType value and/or -Resource. .EXAMPLE New-CafResourceName -Alias vmss -Prefix contoso If you already know the Cloud Adoption Framework abbreviation, -Alias (or its -Slug alias) is a shorter alternative to -ResourceType. .EXAMPLE New-CafResourceName -Resource 'Key vault' -Prefix contoso -Resource alone works too, since every friendly name is unique - a third alternative to -ResourceType/-Alias for when you only remember the resource's plain-English name. .EXAMPLE New-CafResourceName -ResourceType Microsoft.Compute/virtualMachines -Prefix contoso, prod -Region eastus Generates 'contoso-prod-vm-eus' - -Region resolves 'eastus' to its CAF-recommended abbreviation the same way -ResourceType resolves to its slug, and inserts it after any -Suffix, so -Suffix always comes right after the resource type's slug. .EXAMPLE New-CafResourceName -ResourceType Microsoft.Compute/virtualMachines -Prefix contoso, prod -InstanceNumber 1 Generates 'contoso-prod-vm-001' - -InstanceNumber is zero-padded to 3 digits by default, matching the Cloud Adoption Framework's own example names such as 'vm-sql-test-001'. .EXAMPLE New-CafResourceName -ResourceType Microsoft.Compute/virtualMachines -Prefix contoso, prod -Count 3 Generates 'contoso-prod-vm-001', 'contoso-prod-vm-002' and 'contoso-prod-vm-003' in one call. .EXAMPLE New-CafResourceName -ResourceType Microsoft.Storage/storageAccounts -Prefix contoso -RandomSuffixLength 6 Generates a storage account name such as 'contosostabcdef', with 6 random characters appended to help ensure the globally-unique name doesn't collide with one that already exists. .EXAMPLE New-CafResourceName -ResourceType Microsoft.Storage/storageAccounts -Prefix contoso -RandomSuffixLength 6 -RandomSuffixSeed 42 Generates the same storage account name every time it's run, since the random suffix is seeded. .EXAMPLE New-CafResourceName -ResourceType Microsoft.Web/sites -Resource 'Function app' -Prefix contoso, prod Generates a function app name. -Resource is required here because 'Microsoft.Web/sites' is also used for web apps, which use a different abbreviation. .EXAMPLE Get-CafResourceType -Slug kv | New-CafResourceName -Prefix contoso -RandomSuffixLength 4 Pipes the resource type definition for the 'kv' (Key Vault) abbreviation straight into New-CafResourceName. .EXAMPLE New-CafResourceName -ResourceType Microsoft.Compute/virtualMachines -Prefix contoso -ConventionPath ./contoso.json Uses -Prefix explicitly, but falls back to whatever -Suffix/-Region/-Separator/etc. are saved in contoso.json (see New-CafNamingConvention) for anything not specified here. .EXAMPLE Import-Csv ./resources.csv | New-CafResourceName | Export-Csv ./generated-names.csv Batch-generates a name for every row of resources.csv, which needs at least a ResourceType or Alias column, and may also have Resource/Prefix/Suffix/Separator/InstanceNumber columns. The ResourceType/Resource/Slug/InstanceNumber/RandomSuffix that produced each name is kept alongside it, rather than returning bare strings that lose track of which input row (or which -Count iteration) each one came from. .LINK https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/azure-best-practices/resource-abbreviations #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Pure in-memory name generator - the New verb reflects that it returns a new result object (as opposed to Get-CafResourceName''s plain string), not that it changes any external state.')] [CmdletBinding()] param( [Parameter(Position = 0, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [string] $ResourceType, [Parameter(ValueFromPipelineByPropertyName)] [Alias('Slug')] [ValidateNotNullOrEmpty()] [string] $Alias, [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [string] $Resource, [Parameter(ValueFromPipelineByPropertyName)] [string[]] $Prefix, [Parameter(ValueFromPipelineByPropertyName)] [string[]] $Suffix, [Parameter(ValueFromPipelineByPropertyName)] [string] $Region, [Parameter(ValueFromPipelineByPropertyName)] [string] $Separator = '-', [Parameter(ValueFromPipelineByPropertyName)] [ValidateRange(0, [int]::MaxValue)] [int] $InstanceNumber, [Parameter(ValueFromPipelineByPropertyName)] [ValidateRange(1, 10)] [int] $InstanceNumberPadding = 3, [Parameter(ValueFromPipelineByPropertyName)] [ValidateRange(1, 1000)] [int] $Count = 1, [Parameter(ValueFromPipelineByPropertyName)] [ValidateRange(0, 24)] [int] $RandomSuffixLength = 0, [Parameter(ValueFromPipelineByPropertyName)] [int] $RandomSuffixSeed, [string] $ConventionPath, [Parameter(ValueFromPipelineByPropertyName)] [switch] $NoClean ) begin { function Get-CleanString { param ( [string[]]$InputObject, [string]$Pattern ) foreach ($Str in $InputObject) { $Str -replace $Pattern, '' } } function Get-TrimString { param ( [string]$InputObject, [int]$MaxLength ) if ($InputObject.Length -gt $MaxLength) { $InputObject.Substring(0, $MaxLength) } else { $InputObject } } $Convention = $null if ($ConventionPath) { if (-not (Test-Path -Path $ConventionPath -PathType Leaf)) { $PSCmdlet.ThrowTerminatingError( [System.Management.Automation.ErrorRecord]::new( [System.IO.FileNotFoundException]::new("Naming convention file not found: '$ConventionPath'."), 'ConventionFileNotFound', [System.Management.Automation.ErrorCategory]::ObjectNotFound, $ConventionPath ) ) } try { $Convention = Get-Content -Path $ConventionPath -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop Write-Verbose "Loaded naming convention from '$ConventionPath'." } catch { $PSCmdlet.ThrowTerminatingError($_) } } } process { if (-not $ResourceType -and -not $Alias -and -not $Resource) { Write-Error 'Specify -ResourceType, -Alias or -Resource to identify the resource type.' return } $definition = Resolve-CafResourceDefinition -ResourceType $ResourceType -Alias $Alias -Resource $Resource if (-not $definition) { return } Write-Verbose "Resolved to $($definition.ResourceType) [$($definition.Resource)] (slug '$($definition.Slug)', max length $($definition.MaxLength), dashes $($definition.Dashes), lowercase $($definition.Lowercase))." $EffectivePrefix = if ($PSBoundParameters.ContainsKey('Prefix')) { $Prefix } else { $Convention.Prefix } $EffectiveSuffix = if ($PSBoundParameters.ContainsKey('Suffix')) { $Suffix } else { $Convention.Suffix } $EffectiveRegion = if ($PSBoundParameters.ContainsKey('Region')) { $Region } else { $Convention.Region } if ($EffectiveRegion) { $RegionDefinition = Resolve-CafRegion -Region $EffectiveRegion if (-not $RegionDefinition) { return } Write-Verbose "Resolved region '$EffectiveRegion' to '$($RegionDefinition.Region)' (slug '$($RegionDefinition.Slug)')." $EffectiveRegionSlug = $RegionDefinition.Slug } $EffectiveSeparator = if ($PSBoundParameters.ContainsKey('Separator')) { $Separator } elseif ($null -ne $Convention.Separator) { $Convention.Separator } else { $Separator } $EffectiveInstanceNumberPadding = if ($PSBoundParameters.ContainsKey('InstanceNumberPadding')) { $InstanceNumberPadding } elseif ($Convention.InstanceNumberPadding) { $Convention.InstanceNumberPadding } else { $InstanceNumberPadding } $EffectiveRandomSuffixLength = if ($PSBoundParameters.ContainsKey('RandomSuffixLength')) { $RandomSuffixLength } elseif ($null -ne $Convention.RandomSuffixLength) { $Convention.RandomSuffixLength } else { $RandomSuffixLength } if ($EffectiveInstanceNumberPadding -lt 1 -or $EffectiveInstanceNumberPadding -gt 10) { Write-Error "InstanceNumberPadding from the naming convention must be between 1 and 10, but was $EffectiveInstanceNumberPadding." return } if ($EffectiveRandomSuffixLength -lt 0 -or $EffectiveRandomSuffixLength -gt 24) { Write-Error "RandomSuffixLength from the naming convention must be between 0 and 24, but was $EffectiveRandomSuffixLength." return } if ($NoClean) { $CleanPrefix = $EffectivePrefix $CleanSuffix = $EffectiveSuffix $CleanRegionSlug = $EffectiveRegionSlug } else { $CleanPrefix = Get-CleanString -InputObject $EffectivePrefix -Pattern $definition.InvalidCharsRegex $CleanSuffix = Get-CleanString -InputObject $EffectiveSuffix -Pattern $definition.InvalidCharsRegex $CleanRegionSlug = Get-CleanString -InputObject $EffectiveRegionSlug -Pattern $definition.InvalidCharsRegex } $ActualSeparator = if ($definition.Dashes) { $EffectiveSeparator } else { '' } $HasInstanceNumber = $PSBoundParameters.ContainsKey('InstanceNumber') -or $Count -gt 1 $StartInstanceNumber = if ($PSBoundParameters.ContainsKey('InstanceNumber')) { $InstanceNumber } else { 1 } $EndInstanceNumber = $StartInstanceNumber + $Count - 1 # Reserve space based on the longest instance number in the sequence, so the trimmed base name # (prefix/slug/region/suffix) is the same length for every generated name, not just the first one. $InstanceSegment = if ($HasInstanceNumber) { $EndInstanceNumber.ToString().PadLeft($EffectiveInstanceNumberPadding, '0') } else { $null } $reservedLength = 0 $reservedDescriptions = @() if ($InstanceSegment) { $reservedLength += $InstanceSegment.Length + $ActualSeparator.Length $reservedDescriptions += 'the instance number' } if ($EffectiveRandomSuffixLength -gt 0) { $reservedLength += $EffectiveRandomSuffixLength + $ActualSeparator.Length $reservedDescriptions += 'the random suffix' } $mainBudget = $definition.MaxLength - $reservedLength if ($reservedDescriptions.Count -gt 0) { Write-Verbose "Reserving $reservedLength character(s) for $($reservedDescriptions -join ' and '); $mainBudget of $($definition.MaxLength) remain for the prefix/slug/region/suffix." } else { Write-Verbose "No reserved segments; the full $mainBudget character limit is available for the prefix/slug/region/suffix." } if ($mainBudget -lt $definition.Slug.Length) { Write-Error "Not enough room within the $($definition.MaxLength) character limit for $($definition.ResourceType): '$($definition.Slug)' needs $($definition.Slug.Length) characters, but only $mainBudget are available after reserving space for $($reservedDescriptions -join ' and ')." return } $resourceArr = @($CleanPrefix) + @($definition.Slug) + @($CleanSuffix) + @($CleanRegionSlug) | Where-Object { $_ } $resourceName = $resourceArr -join $ActualSeparator Write-Verbose "Joined '$($resourceArr -join "', '")' with separator '$ActualSeparator' -> '$resourceName' ($($resourceName.Length) characters)." if ($resourceName.Length -gt $mainBudget) { $beforeTrimLength = $resourceName.Length $resourceName = Get-TrimString -InputObject $resourceName -MaxLength $mainBudget $reservedNote = if ($reservedDescriptions.Count -gt 0) { " (after reserving space for $($reservedDescriptions -join ' and '))" } else { '' } Write-Warning "Resource name has been trimmed to $mainBudget characters due to the $($definition.MaxLength) character max length restriction$reservedNote." Write-Verbose "Trimmed from $beforeTrimLength to $mainBudget characters -> '$resourceName'." } # A single shared Random, created once per pipeline item rather than per generated name, so a # -RandomSuffixSeed produces a reproducible sequence of distinct suffixes across the whole -Count batch. $Random = if ($EffectiveRandomSuffixLength -gt 0) { if ($PSBoundParameters.ContainsKey('RandomSuffixSeed')) { [System.Random]::new($RandomSuffixSeed) } else { [System.Random]::new() } } else { $null } $CharPool = 'abcdefghijklmnopqrstuvwxyz0123456789' for ($InstanceCounter = $StartInstanceNumber; $InstanceCounter -le $EndInstanceNumber; $InstanceCounter++) { $FinalName = $resourceName if ($HasInstanceNumber) { $CurrentInstanceSegment = $InstanceCounter.ToString().PadLeft($EffectiveInstanceNumberPadding, '0') $FinalName = $FinalName + $ActualSeparator + $CurrentInstanceSegment Write-Verbose "Appended instance segment '$CurrentInstanceSegment' -> '$FinalName'." } if ($EffectiveRandomSuffixLength -gt 0) { # Always lowercase alphanumeric, even for resource types that allow uppercase - the # random suffix exists for uniqueness, not communication, and 36^RandomSuffixLength is # already far more entropy than a uniqueness suffix needs. $RandomSuffix = -join (1..$EffectiveRandomSuffixLength | ForEach-Object { $CharPool[$Random.Next($CharPool.Length)] }) $FinalName = $FinalName + $ActualSeparator + $RandomSuffix Write-Verbose "Appended random suffix '$RandomSuffix' -> '$FinalName'." } if ($definition.Lowercase -and $FinalName -cmatch '[A-Z]') { $FinalName = $FinalName.ToLower() Write-Warning 'Resource name has been converted to lower case due to naming requirement.' Write-Verbose "Converted to lowercase -> '$FinalName'." } if ($FinalName -notmatch $definition.ValidationRegex) { Write-Error "The generated name '$FinalName' is not valid for $($definition.ResourceType) (pattern: $($definition.ValidationRegex))." } else { Write-Verbose "Final result: '$FinalName'." [pscustomobject]@{ Name = $FinalName ResourceType = $definition.ResourceType Resource = $definition.Resource Slug = $definition.Slug InstanceNumber = if ($HasInstanceNumber) { $InstanceCounter } else { $null } RandomSuffix = if ($EffectiveRandomSuffixLength -gt 0) { $RandomSuffix } else { $null } } } } } } function Test-CafResourceName { <# .SYNOPSIS Tests whether a name complies with the Cloud Adoption Framework naming rules for an Azure resource type. .DESCRIPTION Checks a resource name against the same rules Get-CafResourceName enforces when building one: minimum/maximum length, required casing, whether the resource type allows a dash, and the overall set of valid characters. Returns an object showing the pass/fail result of each rule, a plain-English Reason when it's invalid, and an overall IsValid result - or just the overall true/false with -Quiet. IsValid is the authoritative result (the same validation pattern Get-CafResourceName itself checks names against); the other fields are a diagnostic breakdown to help pinpoint why a name failed. For a small number of resource types the breakdown can't perfectly add up to IsValid - for example a character that's only disallowed as the *first* character of a name is flagged as invalid by -ValidCharacters wherever it appears, even though IsValid may still be true if it appears elsewhere. Treat IsValid as the source of truth. .PARAMETER Name The resource name to test, e.g. 'contoso-prod-vm-001'. .PARAMETER ResourceType The ARM resource type to test the name against, e.g. 'Microsoft.Compute/virtualMachines'. Matches as a substring by default. Normally used on its own (or -Alias, or -Resource, used instead); takes precedence if more than one happens to be specified, e.g. by piping in an object that has ResourceType, Alias and Resource properties all describing the same entry. .PARAMETER Alias The Cloud Adoption Framework abbreviation to test the name against, e.g. 'vmss', used as a short alias for the ARM resource type. Matches as a substring by default, and also accepts -Slug as an alias. Normally used on its own, instead of -ResourceType. .PARAMETER Resource The friendly name of the resource to test against, e.g. 'Key vault' - every such name in the module's data is unique, so on its own this works as a third alternative to -ResourceType/ -Alias (also matching as a substring). It also doubles as a disambiguator when combined with -ResourceType/-Alias, for the handful of ARM types used by more than one kind of resource (for example 'Microsoft.Web/sites' covers both a "Web app" and a "Function app") - required in that case, since -ResourceType/-Alias alone would otherwise be ambiguous. .PARAMETER Quiet Return only an overall $true/$false instead of the full breakdown object. .EXAMPLE Test-CafResourceName -Name 'contoso-prod-vm-001' -ResourceType Microsoft.Compute/virtualMachines Returns an object showing which naming rules the name passes or fails, and an overall IsValid. .EXAMPLE (Test-CafResourceName -Name 'CONTOSO' -ResourceType Microsoft.Storage/storageAccounts).Reason Returns 'Name must be all lowercase for this resource type.' - Reason explains an invalid result in plain English; it's $null when IsValid is $true. .EXAMPLE Test-CafResourceName -Name 'Contoso_VM!' -Alias vm -Quiet Returns $false - the name is uppercase, contains an underscore and an exclamation mark, none of which are valid for a virtual machine name. .EXAMPLE Test-CafResourceName -Name 'contoso-kv' -Resource 'Key vault' -Resource alone works too, since every friendly name is unique - a third alternative to -ResourceType/-Alias for when you only remember the resource's plain-English name. .EXAMPLE Get-CafResourceName -ResourceType Microsoft.Storage/storageAccounts -Prefix contoso -RandomSuffixLength 6 | Test-CafResourceName -ResourceType Microsoft.Storage/storageAccounts -Quiet Round-trips a name generated by Get-CafResourceName back through Test-CafResourceName. .LINK https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/azure-best-practices/resource-abbreviations #> [CmdletBinding()] param( [Parameter(Mandatory, Position = 0, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [string] $Name, [Parameter(Position = 1, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [string] $ResourceType, [Parameter(ValueFromPipelineByPropertyName)] [Alias('Slug')] [ValidateNotNullOrEmpty()] [string] $Alias, [Parameter(ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [string] $Resource, [switch] $Quiet ) process { if (-not $ResourceType -and -not $Alias -and -not $Resource) { Write-Error 'Specify -ResourceType, -Alias or -Resource to identify the resource type.' return } $definition = Resolve-CafResourceDefinition -ResourceType $ResourceType -Alias $Alias -Resource $Resource if (-not $definition) { return } $MinLength = $Name.Length -ge $definition.MinLength $MaxLength = $Name.Length -le $definition.MaxLength $Casing = -not ($definition.Lowercase -and $Name -cmatch '[A-Z]') $Dashes = $definition.Dashes -or $Name -cnotmatch '-' $ValidCharacters = $Name -cnotmatch $definition.InvalidCharsRegex $IsValid = $Name -cmatch $definition.ValidationRegex if ($Quiet) { return $IsValid } $Reasons = [System.Collections.Generic.List[string]]::new() if ($IsValid) { # Even if every breakdown field is individually true, IsValid is the authoritative check - # there's nothing to explain when it passed, regardless of what the breakdown says. } else { if (-not $MinLength) { $Reasons.Add("Name is $($Name.Length) character$(if ($Name.Length -ne 1) { 's' }), which is shorter than the $($definition.MinLength) character minimum.") } if (-not $MaxLength) { $Reasons.Add("Name is $($Name.Length) characters, which exceeds the $($definition.MaxLength) character maximum by $($Name.Length - $definition.MaxLength).") } if (-not $Casing) { $Reasons.Add('Name must be all lowercase for this resource type.') } if (-not $Dashes) { $Reasons.Add("Name contains a dash ('-'), which this resource type does not allow.") } if (-not $ValidCharacters) { $Reasons.Add('Name contains characters that are not allowed for this resource type.') } if ($Reasons.Count -eq 0) { # None of the simple per-rule checks explain it - some resource types have additional # positional rules (e.g. must start/end with a letter) that aren't broken out above. $Reasons.Add("Name does not match the required pattern for this resource type (pattern: $($definition.ValidationRegex)).") } } [pscustomobject]@{ Name = $Name ResourceType = $definition.ResourceType Resource = $definition.Resource IsValid = $IsValid Reason = if ($Reasons.Count -gt 0) { $Reasons -join ' ' } else { $null } MinLength = $MinLength MaxLength = $MaxLength Casing = $Casing Dashes = $Dashes ValidCharacters = $ValidCharacters } } } # The JSON data files live alongside this one (Private/) so $PSScriptRoot finds them correctly in both # layouts this file runs in: dot-sourced here in dev mode ($PSScriptRoot = this Private folder), and # inlined into the combined/staged .psm1 (see Build/Tasks/CombineFunctionsAndStage.ps1), where the # build copies these same two files to the staged module's root - $PSScriptRoot there resolves to that # root too, since the combined file *is* the module's root script at that point. $Script:ResourceDefinitionPath = Join-Path $PSScriptRoot 'resourceDefinition.json' $Script:RegionDefinitionPath = Join-Path $PSScriptRoot 'regionDefinition.json' # Registered here (rather than via the [ArgumentCompleter(...)] attribute on each parameter) because an # attribute scriptblock isn't bound to this module's session state, so it can't see private functions # like Get-CafResourceDefinition - it would silently fail and PowerShell would fall back to file # completion. A scriptblock defined directly in a module file (dot-sourced here, inlined in the # combined/staged build) doesn't have that problem. $ResourceTypeCompleter = { param($CommandName, $ParameterName, $WordToComplete, $CommandAst, $FakeBoundParameters) $null = $CommandName, $ParameterName, $CommandAst, $FakeBoundParameters # Substring match, same as the cmdlets' own -ResourceType/-Alias partial matching, so what # tab-completes is consistent with what actually resolves. $Pattern = Get-CafWildcardPattern -Value $WordToComplete (Get-CafResourceDefinition).ResourceType | Select-Object -Unique | Where-Object { $_ -like $Pattern } | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } $SlugCompleter = { param($CommandName, $ParameterName, $WordToComplete, $CommandAst, $FakeBoundParameters) $null = $CommandName, $ParameterName, $CommandAst, $FakeBoundParameters $Pattern = Get-CafWildcardPattern -Value $WordToComplete (Get-CafResourceDefinition).Slug | Select-Object -Unique | Where-Object { $_ -like $Pattern } | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } } $ResourceCompleter = { param($CommandName, $ParameterName, $WordToComplete, $CommandAst, $FakeBoundParameters) $null = $CommandName, $ParameterName, $CommandAst $Definitions = @(Get-CafResourceDefinition) # Narrow to whichever resource type the caller has already typed, if any, so -Resource only # offers the friendly names that actually apply to it (e.g. just 'Web app'/'Function app' once # -ResourceType Microsoft.Web/sites is on the line), rather than every friendly name there is. # $FakeBoundParameters is always keyed by the canonical parameter name, even if the caller typed # the -Slug alias instead of -Alias, so there's no separate case to handle for that. if ($FakeBoundParameters.ResourceType) { $Definitions = Select-CafMatch -InputObject $Definitions -Property ResourceType -Value $FakeBoundParameters.ResourceType } elseif ($FakeBoundParameters.Alias) { $Definitions = Select-CafMatch -InputObject $Definitions -Property Slug -Value $FakeBoundParameters.Alias } $Pattern = Get-CafWildcardPattern -Value $WordToComplete $Definitions.Resource | Select-Object -Unique | Where-Object { $_ -like $Pattern } | Sort-Object | ForEach-Object { # Friendly names routinely contain spaces/parentheses (e.g. 'Managed disk (OS)'), so the # completion text needs quoting to stay a single valid token once it's inserted. $Quoted = "'" + $_.Replace("'", "''") + "'" [System.Management.Automation.CompletionResult]::new($Quoted, $_, 'ParameterValue', $_) } } $RegionCompleter = { param($CommandName, $ParameterName, $WordToComplete, $CommandAst, $FakeBoundParameters) $null = $CommandName, $ParameterName, $CommandAst, $FakeBoundParameters # Matches against the ARM region name, its display name or its slug, same fields Resolve-CafRegion # accepts, so typing 'east', 'East US' or 'eus' all offer a completion - but always completes to the # ARM name, since that's the unambiguous value every other Azure PowerShell -Location parameter # expects. $Pattern = Get-CafWildcardPattern -Value $WordToComplete (Get-CafRegionDefinition) | Where-Object { $_.Region -like $Pattern -or $_.DisplayName -like $Pattern -or $_.Slug -like $Pattern } | Sort-Object -Property Region -Unique | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_.Region, "$($_.Region) ($($_.DisplayName))", 'ParameterValue', $_.DisplayName) } } Register-ArgumentCompleter -CommandName 'Get-CafResourceName', 'New-CafResourceName', 'Test-CafResourceName', 'Get-CafResourceType' -ParameterName 'ResourceType' -ScriptBlock $ResourceTypeCompleter Register-ArgumentCompleter -CommandName 'Get-CafResourceName', 'New-CafResourceName', 'Test-CafResourceName', 'Get-CafResourceType' -ParameterName 'Alias' -ScriptBlock $SlugCompleter Register-ArgumentCompleter -CommandName 'Get-CafResourceName', 'New-CafResourceName', 'Test-CafResourceName', 'Get-CafResourceType' -ParameterName 'Resource' -ScriptBlock $ResourceCompleter Register-ArgumentCompleter -CommandName 'Get-CafResourceName', 'New-CafResourceName', 'Get-CafRegion' -ParameterName 'Region' -ScriptBlock $RegionCompleter function Get-CafRegionDefinition { <# .SYNOPSIS Returns the cached set of Azure region abbreviation definitions. .DESCRIPTION Loads regionDefinition.json from the module root the first time it's needed and caches it for the lifetime of the module session, so repeated calls to Get-CafResourceName/Get-CafRegion don't re-read and re-parse the file from disk every time. .EXAMPLE Get-CafRegionDefinition #> [CmdletBinding()] param() if (-not $Script:CafRegionDefinitions) { $Script:CafRegionDefinitions = Get-Content -Path $Script:RegionDefinitionPath -Raw | ConvertFrom-Json } $Script:CafRegionDefinitions } function Get-CafResourceDefinition { <# .SYNOPSIS Returns the cached set of CAF resource naming definitions. .DESCRIPTION Loads resourceDefinition.json from the module root the first time it's needed and caches it for the lifetime of the module session, so repeated calls to Get-CafResourceName/Get-CafResourceType don't re-read and re-parse the file from disk every time. .EXAMPLE Get-CafResourceDefinition #> [CmdletBinding()] param() if (-not $Script:CafResourceDefinitions) { $Script:CafResourceDefinitions = Get-Content -Path $Script:ResourceDefinitionPath -Raw | ConvertFrom-Json } $Script:CafResourceDefinitions } function Get-CafWildcardPattern { <# .SYNOPSIS Turns a plain string into a partial ('contains') wildcard pattern. .DESCRIPTION Used by Get-CafResourceName/Get-CafResourceType so that -ResourceType and -Resource match as a substring by default (e.g. 'virtualMachines' matches 'Microsoft.Compute/virtualMachines'), while still respecting an explicit wildcard pattern (e.g. '*storageAccount*' or 'disk?') if the caller already supplied one, rather than wrapping it a second time. .EXAMPLE Get-CafWildcardPattern -Value 'virtualMachines' #> [CmdletBinding()] param( [Parameter(Mandatory)] [AllowEmptyString()] [string] $Value ) if ($Value -match '[*?\[\]]') { $Value } else { "*$Value*" } } function Resolve-CafRegion { <# .SYNOPSIS Resolves a -Region value to exactly one regionDefinition.json entry. .DESCRIPTION Shared resolution logic for Get-CafResourceName and Get-CafRegion, so both cmdlets identify a region the same way and report identical errors for an unknown/ambiguous value. Checks for an exact (case-insensitive) match against the ARM region name (e.g. 'eastus'), its display name (e.g. 'East US') or its CAF slug (e.g. 'eus') first - checking all three up front, rather than one at a time, matters for a short value like a slug: 'ae' (Australia East's slug) is also a substring of several other regions' ARM names (e.g. 'canadaeast', 'israelcentral'), so falling through to a substring search would make it ambiguous instead of resolving it. Only once no exact match exists does it fall back to a substring ('contains') search, same as -ResourceType/-Resource elsewhere in the module - against the ARM region name first, then the display name. The slug isn't included in that substring fallback: it's short enough that partial matching on it would mostly just produce ambiguous results, and a caller who doesn't already know the exact slug should be matching on the region name/display name instead. Writes a non-terminating error and returns nothing if the value doesn't resolve to exactly one entry - callers should check for a $null/empty result and return without continuing. .EXAMPLE Resolve-CafRegion -Region 'eastus' .EXAMPLE Resolve-CafRegion -Region 'East US' .EXAMPLE Resolve-CafRegion -Region 'eus' #> [CmdletBinding()] param( [Parameter(Mandatory)] [string] $Region ) $definitions = @(Get-CafRegionDefinition) $candidates = @($definitions | Where-Object { $_.Region -eq $Region -or $_.DisplayName -eq $Region -or $_.Slug -eq $Region }) if ($candidates.Count -eq 0) { $candidates = Select-CafMatch -InputObject $definitions -Property Region -Value $Region if ($candidates.Count -eq 0) { $candidates = Select-CafMatch -InputObject $definitions -Property DisplayName -Value $Region } } if ($candidates.Count -eq 0) { Write-Error "No Azure region matches '$Region'. Use Get-CafRegion to search for a supported region." return } if ($candidates.Count -gt 1) { $options = ($candidates | ForEach-Object { "$($_.Region) [$($_.DisplayName)]" }) -join "', '" Write-Error "'$Region' matches more than one Azure region. Narrow it down with the ARM region name: '$options'." return } $candidates[0] } function Resolve-CafResourceDefinition { <# .SYNOPSIS Resolves a -ResourceType, -Alias or -Resource value to exactly one resourceDefinition.json entry. .DESCRIPTION Shared resolution logic for Get-CafResourceName and Test-CafResourceName, so all three cmdlets identify a resource type the same way and report identical errors for an unknown/ambiguous value. Writes a non-terminating error and returns nothing if the value doesn't resolve to exactly one entry - callers should check for a $null/empty result and return without continuing. -ResourceType takes priority as the primary identifier if given, then -Alias, with -Resource narrowing *within* whichever of those matched (for the handful of types used by more than one kind of resource). When neither -ResourceType nor -Alias is given, -Resource is used as the primary identifier instead - every "Resource" friendly name in resourceDefinition.json happens to be globally unique today, so this only fails if that ever stops being true upstream. Callers don't need to guard against more than one of -ResourceType/-Alias/-Resource being supplied - e.g. piping Get-CafResourceType's output through by property name binds ResourceType, Resource and (via its Slug alias) Alias all at once, since they're all present on that object and all describe the same entry. Precedence just needs to be deterministic for that case, not an error - it's not a conflict unless the caller explicitly typed contradictory values, which isn't something this function can distinguish from the pipeline case anyway. .EXAMPLE Resolve-CafResourceDefinition -ResourceType 'Microsoft.Compute/virtualMachines' .EXAMPLE Resolve-CafResourceDefinition -Alias 'afd' -Resource 'classic' .EXAMPLE Resolve-CafResourceDefinition -Resource 'Key vault' #> [CmdletBinding()] param( [string] $ResourceType, [string] $Alias, [string] $Resource ) $ResourceIsPrimary = -not $ResourceType -and -not $Alias if ($ResourceType) { $identifier = "'$ResourceType'" $candidates = Select-CafMatch -InputObject @(Get-CafResourceDefinition) -Property ResourceType -Value $ResourceType } elseif ($Alias) { $identifier = "alias '$Alias'" $candidates = Select-CafMatch -InputObject @(Get-CafResourceDefinition) -Property Slug -Value $Alias } else { $identifier = "resource '$Resource'" $candidates = Select-CafMatch -InputObject @(Get-CafResourceDefinition) -Property Resource -Value $Resource } if ($candidates.Count -eq 0) { Write-Error "No resource type matches $identifier. Use Get-CafResourceType to search for a supported resource type." return } if ($Resource -and -not $ResourceIsPrimary) { $allCandidates = $candidates $candidates = Select-CafMatch -InputObject $candidates -Property Resource -Value $Resource if ($candidates.Count -eq 0) { $options = ($allCandidates | ForEach-Object { "$($_.ResourceType) [$($_.Resource)]" }) -join "', '" Write-Error "'$Resource' doesn't match any resource for $identifier. Available: '$options'." return } } if ($candidates.Count -gt 1) { $options = ($candidates | ForEach-Object { "$($_.ResourceType) [$($_.Resource)]" }) -join "', '" $suggestion = if ($ResourceIsPrimary) { '-ResourceType' } else { '-Resource' } Write-Error "$identifier matches more than one resource type. Narrow it down with $suggestion`: '$options'." return } $candidates[0] } function Select-CafMatch { <# .SYNOPSIS Filters a collection to the items whose property exactly matches a value, falling back to a partial ('contains') match only when nothing matches exactly. .DESCRIPTION Used by Get-CafResourceName/Get-CafResourceType to make -ResourceType/-Resource match as a substring for convenience, without that breaking exact lookups: partial matching alone is ambiguous for values like 'Microsoft.Compute/virtualMachines', since (case-insensitively) it's also a substring of 'Microsoft.Compute/virtualMachineScaleSets'. Checking for an exact match first means a fully-qualified value always resolves to itself, and the substring fallback only kicks in when there was no exact match to begin with. .EXAMPLE Select-CafMatch -InputObject $definitions -Property ResourceType -Value 'Microsoft.Compute/virtualMachines' #> [CmdletBinding()] param( [Parameter(Mandatory)] [object[]] $InputObject, [Parameter(Mandatory)] [string] $Property, [Parameter(Mandatory)] [string] $Value ) $exact = @($InputObject | Where-Object { $_.$Property -eq $Value }) if ($exact.Count -gt 0) { return $exact } $pattern = Get-CafWildcardPattern -Value $Value @($InputObject | Where-Object { $_.$Property -like $pattern }) } |