Skip to content

Commit ce46bc3

Browse files
feat: Add DNS-safe tag sanitization for Azure DNS resources
- Created ConvertTo-DnsSafeTags function to remove spaces and parentheses from tag keys - Created Set-DnsSafeTagsForVirtualHubs function to implement tag fallback logic (dns_zone_tags -> connectivity_tags -> overall_tags) - Modified Write-TfvarsJsonFile to automatically sanitize DNS zone tags when processing virtual_hubs configuration - Added comprehensive unit tests for both new functions (20 tests total, all passing) - All existing tests pass (55 tests total) Co-authored-by: jaredfholgate <1612200+jaredfholgate@users.noreply.github.com>
1 parent ee7e5e9 commit ce46bc3

File tree

5 files changed

+520
-0
lines changed

5 files changed

+520
-0
lines changed
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
function ConvertTo-DnsSafeTags {
2+
<#
3+
.SYNOPSIS
4+
Converts tags to DNS-safe format by removing spaces and parentheses from tag keys.
5+
6+
.DESCRIPTION
7+
Azure DNS zones don't support the use of spaces or parentheses in tag keys, or tag keys that start with a number.
8+
This function sanitizes tag keys to make them DNS-safe.
9+
Reference: https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/tag-resources
10+
11+
.PARAMETER tags
12+
The hashtable or PSCustomObject containing tags to sanitize.
13+
14+
.EXAMPLE
15+
ConvertTo-DnsSafeTags -tags @{"Business Application" = "ALZ"; "Owner" = "Platform"}
16+
Returns: @{"BusinessApplication" = "ALZ"; "Owner" = "Platform"}
17+
18+
.EXAMPLE
19+
ConvertTo-DnsSafeTags -tags @{"Business Unit (Primary)" = "IT"; "1stTag" = "value"}
20+
Returns: @{"BusinessUnitPrimary" = "IT"; "_1stTag" = "value"}
21+
#>
22+
[CmdletBinding()]
23+
param (
24+
[Parameter(Mandatory = $false)]
25+
[object] $tags
26+
)
27+
28+
if ($null -eq $tags) {
29+
return $null
30+
}
31+
32+
$dnsSafeTags = @{}
33+
34+
# Handle both hashtables and PSCustomObjects
35+
if ($tags -is [hashtable]) {
36+
foreach ($key in $tags.Keys) {
37+
$safeKey = $key -replace '\s+', '' # Remove all whitespace
38+
$safeKey = $safeKey -replace '[()]', '' # Remove parentheses
39+
# Ensure key doesn't start with a number by prepending underscore if needed
40+
if ($safeKey -match '^\d') {
41+
$safeKey = "_$safeKey"
42+
}
43+
if ($safeKey -ne "") {
44+
$dnsSafeTags[$safeKey] = $tags[$key]
45+
} else {
46+
Write-Warning "Tag key '$key' resulted in empty string after sanitization and was skipped"
47+
}
48+
}
49+
} elseif ($tags -is [PSCustomObject]) {
50+
foreach ($property in $tags.PSObject.Properties) {
51+
$safeKey = $property.Name -replace '\s+', '' # Remove all whitespace
52+
$safeKey = $safeKey -replace '[()]', '' # Remove parentheses
53+
# Ensure key doesn't start with a number by prepending underscore if needed
54+
if ($safeKey -match '^\d') {
55+
$safeKey = "_$safeKey"
56+
}
57+
if ($safeKey -ne "") {
58+
$dnsSafeTags[$safeKey] = $property.Value
59+
} else {
60+
Write-Warning "Tag key '$($property.Name)' resulted in empty string after sanitization and was skipped"
61+
}
62+
}
63+
} else {
64+
Write-Verbose "Tag format is neither hashtable nor PSCustomObject, returning as-is"
65+
return $tags
66+
}
67+
68+
return $dnsSafeTags
69+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
function Set-DnsSafeTagsForVirtualHubs {
2+
<#
3+
.SYNOPSIS
4+
Processes virtual_hubs configuration to ensure DNS zone tags are DNS-safe.
5+
6+
.DESCRIPTION
7+
This function processes the virtual_hubs configuration object and sanitizes tags for private_dns_zones
8+
to ensure they don't contain spaces or other characters not supported by Azure DNS.
9+
Implements fallback logic: private_dns_zones.tags -> connectivity_tags -> overall tags (all sanitized)
10+
11+
.PARAMETER virtualHubs
12+
The virtual_hubs configuration object to process.
13+
14+
.PARAMETER connectivityTags
15+
Optional connectivity-level tags to use as fallback.
16+
17+
.PARAMETER overallTags
18+
Optional overall/global tags to use as final fallback.
19+
20+
.EXAMPLE
21+
Set-DnsSafeTagsForVirtualHubs -virtualHubs $config -connectivityTags @{"Business Unit" = "IT"}
22+
#>
23+
[CmdletBinding()]
24+
param (
25+
[Parameter(Mandatory = $false)]
26+
[object] $virtualHubs,
27+
28+
[Parameter(Mandatory = $false)]
29+
[object] $connectivityTags = $null,
30+
31+
[Parameter(Mandatory = $false)]
32+
[object] $overallTags = $null
33+
)
34+
35+
if ($null -eq $virtualHubs) {
36+
return $virtualHubs
37+
}
38+
39+
# Process each virtual hub
40+
foreach ($hubProperty in $virtualHubs.PSObject.Properties) {
41+
$hub = $hubProperty.Value
42+
43+
if ($null -eq $hub) {
44+
continue
45+
}
46+
47+
# Check if this hub has private_dns_zones configuration
48+
$privateDnsZonesProperty = $hub.PSObject.Properties | Where-Object { $_.Name -eq "private_dns_zones" }
49+
50+
if ($null -ne $privateDnsZonesProperty) {
51+
$privateDnsZones = $privateDnsZonesProperty.Value
52+
53+
if ($null -ne $privateDnsZones) {
54+
# Check if DNS zone has its own tags
55+
$dnsTagsProperty = $privateDnsZones.PSObject.Properties | Where-Object { $_.Name -eq "tags" }
56+
57+
if ($null -ne $dnsTagsProperty -and $null -ne $dnsTagsProperty.Value) {
58+
# DNS zone has its own tags - sanitize them
59+
Write-Verbose "Sanitizing DNS zone tags for hub: $($hubProperty.Name)"
60+
$sanitizedTags = ConvertTo-DnsSafeTags -tags $dnsTagsProperty.Value
61+
$privateDnsZones.tags = $sanitizedTags
62+
} else {
63+
# No DNS-specific tags, implement fallback logic
64+
Write-Verbose "No DNS-specific tags found for hub: $($hubProperty.Name), applying fallback logic"
65+
66+
$tagsToUse = $null
67+
68+
# Try connectivity tags first
69+
if ($null -ne $connectivityTags) {
70+
Write-Verbose "Using connectivity tags as fallback"
71+
$tagsToUse = $connectivityTags
72+
}
73+
# Fall back to overall tags if connectivity tags not available
74+
elseif ($null -ne $overallTags) {
75+
Write-Verbose "Using overall tags as fallback"
76+
$tagsToUse = $overallTags
77+
}
78+
79+
# Sanitize and apply the fallback tags
80+
if ($null -ne $tagsToUse) {
81+
$sanitizedTags = ConvertTo-DnsSafeTags -tags $tagsToUse
82+
83+
# Add tags property if it doesn't exist
84+
if ($null -eq $dnsTagsProperty) {
85+
$privateDnsZones | Add-Member -NotePropertyName "tags" -NotePropertyValue $sanitizedTags -Force
86+
} else {
87+
$privateDnsZones.tags = $sanitizedTags
88+
}
89+
}
90+
}
91+
}
92+
}
93+
}
94+
95+
return $virtualHubs
96+
}

src/ALZ/Private/Config-Helpers/Write-TfvarsJsonFile.ps1

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,20 @@ function Write-TfvarsJsonFile {
1919

2020
$jsonObject = [ordered]@{}
2121

22+
# Extract connectivity and overall tags for DNS fallback logic
23+
$connectivityTags = $null
24+
$overallTags = $null
25+
26+
$connectivityTagsProperty = $configuration.PSObject.Properties | Where-Object { $_.Name -eq "connectivity_tags" }
27+
if ($null -ne $connectivityTagsProperty) {
28+
$connectivityTags = $connectivityTagsProperty.Value.Value
29+
}
30+
31+
$tagsProperty = $configuration.PSObject.Properties | Where-Object { $_.Name -eq "tags" }
32+
if ($null -ne $tagsProperty) {
33+
$overallTags = $tagsProperty.Value.Value
34+
}
35+
2236
foreach ($configurationProperty in $configuration.PSObject.Properties | Sort-Object Name) {
2337
if ($skipItems -contains $configurationProperty.Name) {
2438
Write-Verbose "Skipping configuration property: $($configurationProperty.Name)"
@@ -36,6 +50,12 @@ function Write-TfvarsJsonFile {
3650
$configurationValue = [System.IO.Path]::GetFileName($configurationValue)
3751
}
3852

53+
# Process virtual_hubs to sanitize DNS zone tags
54+
if ($configurationProperty.Name -eq "virtual_hubs" -and $null -ne $configurationValue) {
55+
Write-Verbose "Processing virtual_hubs configuration to apply DNS-safe tags"
56+
$configurationValue = Set-DnsSafeTagsForVirtualHubs -virtualHubs $configurationValue -connectivityTags $connectivityTags -overallTags $overallTags
57+
}
58+
3959
Write-Verbose "Writing to tfvars.json - Configuration Property: $($configurationProperty.Name) - Configuration Value: $configurationValue"
4060
$jsonObject.Add("$($configurationProperty.Name)", $configurationValue)
4161
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
Describe "ConvertTo-DnsSafeTags" {
2+
BeforeAll {
3+
# Directly source the function file
4+
. "$PSScriptRoot/../../../ALZ/Private/Config-Helpers/ConvertTo-DnsSafeTags.ps1"
5+
}
6+
7+
Context "When converting hashtable tags" {
8+
It "Should remove spaces from tag keys" {
9+
$tags = @{
10+
"Business Application" = "ALZ"
11+
"Owner" = "Platform"
12+
}
13+
14+
$result = ConvertTo-DnsSafeTags -tags $tags
15+
16+
$result.Keys | Should -Contain "BusinessApplication"
17+
$result.Keys | Should -Contain "Owner"
18+
$result.Keys | Should -Not -Contain "Business Application"
19+
$result["BusinessApplication"] | Should -Be "ALZ"
20+
$result["Owner"] | Should -Be "Platform"
21+
}
22+
23+
It "Should remove parentheses from tag keys" {
24+
$tags = @{
25+
"Business Unit (Primary)" = "IT"
26+
"Cost Center (Backup)" = "12345"
27+
}
28+
29+
$result = ConvertTo-DnsSafeTags -tags $tags
30+
31+
$result.Keys | Should -Contain "BusinessUnitPrimary"
32+
$result.Keys | Should -Contain "CostCenterBackup"
33+
$result["BusinessUnitPrimary"] | Should -Be "IT"
34+
$result["CostCenterBackup"] | Should -Be "12345"
35+
}
36+
37+
It "Should prefix tag keys that start with a number" {
38+
$tags = @{
39+
"1stTag" = "value1"
40+
"2ndTag" = "value2"
41+
}
42+
43+
$result = ConvertTo-DnsSafeTags -tags $tags
44+
45+
$result.Keys | Should -Contain "_1stTag"
46+
$result.Keys | Should -Contain "_2ndTag"
47+
$result["_1stTag"] | Should -Be "value1"
48+
$result["_2ndTag"] | Should -Be "value2"
49+
}
50+
51+
It "Should handle tags with multiple spaces and special characters" {
52+
$tags = @{
53+
"Business Application (Main)" = "ALZ"
54+
" Owner " = "Platform"
55+
}
56+
57+
$result = ConvertTo-DnsSafeTags -tags $tags
58+
59+
$result.Keys | Should -Contain "BusinessApplicationMain"
60+
$result.Keys | Should -Contain "Owner"
61+
$result["BusinessApplicationMain"] | Should -Be "ALZ"
62+
$result["Owner"] | Should -Be "Platform"
63+
}
64+
65+
It "Should return null for null input" {
66+
$result = ConvertTo-DnsSafeTags -tags $null
67+
$result | Should -Be $null
68+
}
69+
70+
It "Should handle empty hashtable" {
71+
$tags = @{}
72+
$result = ConvertTo-DnsSafeTags -tags $tags
73+
$result.Count | Should -Be 0
74+
}
75+
}
76+
77+
Context "When converting PSCustomObject tags" {
78+
It "Should remove spaces from tag keys in PSCustomObject" {
79+
$tags = [PSCustomObject]@{
80+
"Business Application" = "ALZ"
81+
"Owner" = "Platform"
82+
}
83+
84+
$result = ConvertTo-DnsSafeTags -tags $tags
85+
86+
$result.Keys | Should -Contain "BusinessApplication"
87+
$result.Keys | Should -Contain "Owner"
88+
$result["BusinessApplication"] | Should -Be "ALZ"
89+
$result["Owner"] | Should -Be "Platform"
90+
}
91+
92+
It "Should handle PSCustomObject with special characters" {
93+
$tags = [PSCustomObject]@{
94+
"Business Unit (Test)" = "IT"
95+
"1stTag" = "value"
96+
}
97+
98+
$result = ConvertTo-DnsSafeTags -tags $tags
99+
100+
$result.Keys | Should -Contain "BusinessUnitTest"
101+
$result.Keys | Should -Contain "_1stTag"
102+
$result["BusinessUnitTest"] | Should -Be "IT"
103+
$result["_1stTag"] | Should -Be "value"
104+
}
105+
}
106+
107+
Context "When handling edge cases" {
108+
It "Should skip tags that result in empty keys" {
109+
$tags = @{
110+
" " = "value"
111+
"Owner" = "Platform"
112+
}
113+
114+
# Should issue a warning but not fail
115+
$result = ConvertTo-DnsSafeTags -tags $tags -WarningAction SilentlyContinue
116+
117+
$result.Keys | Should -Contain "Owner"
118+
$result.Keys | Should -Not -Contain " "
119+
$result.Keys | Should -Not -Contain ""
120+
$result.Count | Should -Be 1
121+
}
122+
123+
It "Should handle tags with only spaces and parentheses" {
124+
$tags = @{
125+
"( )" = "value"
126+
"Owner" = "Platform"
127+
}
128+
129+
$result = ConvertTo-DnsSafeTags -tags $tags -WarningAction SilentlyContinue
130+
131+
$result.Keys | Should -Contain "Owner"
132+
$result.Count | Should -Be 1
133+
}
134+
}
135+
}

0 commit comments

Comments
 (0)