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-CafResourceName { <# .SYNOPSIS Generates an Azure resource name that follows the Cloud Adoption Framework naming convention. .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. 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 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 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 Seed 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, -Separator, -InstanceNumberPadding and -RandomSuffixLength, created with New-CafResourceNamingConvention. 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 -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.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 -Seed 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/-Separator/etc. are saved in contoso.json (see New-CafResourceNamingConvention) 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] $Separator = '-', [Parameter(ValueFromPipelineByPropertyName)] [ValidateRange(0, [int]::MaxValue)] [int] $InstanceNumber, [Parameter(ValueFromPipelineByPropertyName)] [ValidateRange(1, 10)] [int] $InstanceNumberPadding = 3, [Parameter(ValueFromPipelineByPropertyName)] [ValidateRange(0, 24)] [int] $RandomSuffixLength = 0, [Parameter(ValueFromPipelineByPropertyName)] [int] $Seed, [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 } $EffectiveSeparator = if ($PSBoundParameters.ContainsKey('Separator')) { $Separator } elseif ($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 } else { $CleanPrefix = Get-CleanString -InputObject $EffectivePrefix -Pattern $definition.InvalidCharsRegex $CleanSuffix = Get-CleanString -InputObject $EffectiveSuffix -Pattern $definition.InvalidCharsRegex } $ActualSeparator = if ($definition.Dashes) { $EffectiveSeparator } else { '' } $HasInstanceNumber = $PSBoundParameters.ContainsKey('InstanceNumber') $InstanceSegment = if ($HasInstanceNumber) { $InstanceNumber.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/suffix." } else { Write-Verbose "No reserved segments; the full $mainBudget character limit is available for the prefix/slug/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) | 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'." } if ($InstanceSegment) { $resourceName = $resourceName + $ActualSeparator + $InstanceSegment Write-Verbose "Appended instance segment '$InstanceSegment' -> '$resourceName'." } 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. $CharPool = 'abcdefghijklmnopqrstuvwxyz0123456789' $Random = if ($PSBoundParameters.ContainsKey('Seed')) { [System.Random]::new($Seed) } else { [System.Random]::new() } $RandomSuffix = -join (1..$EffectiveRandomSuffixLength | ForEach-Object { $CharPool[$Random.Next($CharPool.Length)] }) $resourceName = $resourceName + $ActualSeparator + $RandomSuffix Write-Verbose "Appended random suffix '$RandomSuffix' -> '$resourceName'." } if ($definition.Lowercase -and $resourceName -cmatch '[A-Z]') { $resourceName = $resourceName.ToLower() Write-Warning 'Resource name has been converted to lower case due to naming requirement.' Write-Verbose "Converted to lowercase -> '$resourceName'." } if ($resourceName -notmatch $definition.ValidationRegex) { Write-Error "The generated name '$resourceName' is not valid for $($definition.ResourceType) (pattern: $($definition.ValidationRegex))." } else { Write-Verbose "Final result: '$resourceName'." return $resourceName } } } 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-CafResourceNamingConvention { <# .SYNOPSIS Saves a reusable set of Get-CafResourceName defaults (prefix, suffix, 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, -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. .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, environment, or region - 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 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-CafResourceNamingConvention -Path ./contoso.json -Prefix contoso, prod -Separator '-' -InstanceNumberPadding 2 Saves a convention so that Get-CafResourceName -ConventionPath ./contoso.json applies that prefix, 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] $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('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, -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 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 } } } 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-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 }) } |