Skip to content

Commit 56a70bb

Browse files
committed
Add ConsentToRecurringPAYG support and role assignment retry logic
Sync changes from claestom/sql-arc-policy-license-config into the sql-server-samples fork. azurepolicy.json: - Add ConsentToRecurringPAYG compliance checks in existenceCondition: when target is PAYG, resources must also have the consent property to be considered compliant (handles both new transitions and backward compatibility for pre-consent PAYG extensions) - Add ConsentToRecurringPAYG to the deployment template: when target is PAYG, the remediation sets Consented=true with a UTC timestamp; non-PAYG targets use the base LicenseType-only settings - Add consentTimestamp template parameter (auto-generated via utcNow) deployment.ps1: - Add retry loop (5 attempts, 10s delay) for managed identity role assignments to handle replication delays after policy assignment creation; also handles Conflict responses gracefully README.md: - Add Recurring Billing Consent (PAYG) section documenting the consent behavior, compliance evaluation, and immutability note
1 parent bdc1ebf commit 56a70bb

3 files changed

Lines changed: 72 additions & 9 deletions

File tree

samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,14 @@ $TargetLicenseType = "PAYG" # Must match
151151

152152
> **Note:** Use `-GrantMissingPermissions` to automatically check and assign any missing required roles before remediation starts.
153153
154+
## Recurring Billing Consent (PAYG)
155+
156+
When `TargetLicenseType` is set to `PAYG`, the policy automatically includes `ConsentToRecurringPAYG` in the extension settings with `Consented: true` and a UTC timestamp. This is required for recurring pay-as-you-go billing as described in the [Microsoft documentation](https://learn.microsoft.com/en-us/sql/sql-server/azure-arc/manage-pay-as-you-go-transition?view=sql-server-ver17#recurring-billing-consent).
157+
158+
The policy also checks for `ConsentToRecurringPAYG` in its compliance evaluation — resources with `LicenseType: PAYG` but missing the consent property are flagged as non-compliant and remediated. This applies both when transitioning to PAYG and for existing PAYG extensions that predate the consent requirement (backward compatibility).
159+
160+
> **Note:** Once `ConsentToRecurringPAYG` is set on an extension, it cannot be removed — this is enforced by the Azure resource provider. When transitioning away from PAYG, the policy changes `LicenseType` but leaves the consent property in place.
161+
154162
## Managed Identity And Roles
155163

156164
The policy assignment is created with `-IdentityType SystemAssigned`. Azure creates a managed identity on the assignment and uses it to apply DeployIfNotExists changes during enforcement and remediation.

samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/policy/azurepolicy.json

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,26 @@
112112
{
113113
"value": "[length(intersection(field('Microsoft.HybridCompute/machines/extensions/settings'), createObject('LicenseType', parameters('targetLicenseType'))))]",
114114
"equals": 1
115+
},
116+
{
117+
"anyOf": [
118+
{
119+
"value": "[parameters('targetLicenseType')]",
120+
"notEquals": "PAYG"
121+
},
122+
{
123+
"allOf": [
124+
{
125+
"value": "[parameters('targetLicenseType')]",
126+
"equals": "PAYG"
127+
},
128+
{
129+
"field": "Microsoft.HybridCompute/machines/extensions/settings",
130+
"ContainsKey": "ConsentToRecurringPAYG"
131+
}
132+
]
133+
}
134+
]
115135
}
116136
]
117137
},
@@ -156,6 +176,10 @@
156176
{
157177
"value": "[contains(parameters('licenseTypesToOverwrite'), 'PAYG')]",
158178
"equals": false
179+
},
180+
{
181+
"field": "Microsoft.HybridCompute/machines/extensions/settings",
182+
"ContainsKey": "ConsentToRecurringPAYG"
159183
}
160184
]
161185
},
@@ -214,14 +238,29 @@
214238
"metadata": {
215239
"description": "LicenseType value to set on the extension."
216240
}
241+
},
242+
"consentTimestamp": {
243+
"type": "string",
244+
"defaultValue": "[utcNow('yyyy-MM-ddTHH:mm:ssZ')]",
245+
"metadata": {
246+
"description": "UTC timestamp for recurring PAYG consent. Auto-generated at deployment time."
247+
}
217248
}
218249
},
219250
"functions": [],
220251
"variables": {
221252
"vmExtensionPublisher": "Microsoft.AzureData",
222-
"licenseSettings": {
253+
"baseSettings": {
223254
"LicenseType": "[parameters('targetLicenseType')]"
224-
}
255+
},
256+
"paygConsentSettings": {
257+
"LicenseType": "[parameters('targetLicenseType')]",
258+
"ConsentToRecurringPAYG": {
259+
"Consented": true,
260+
"ConsentTimestamp": "[parameters('consentTimestamp')]"
261+
}
262+
},
263+
"licenseSettings": "[if(equals(parameters('targetLicenseType'), 'PAYG'), variables('paygConsentSettings'), variables('baseSettings'))]"
225264
},
226265
"resources": [
227266
{

samples/manage/azure-arc-enabled-sql-server/compliance/arc-sql-license-type-compliance/scripts/deployment.ps1

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -109,13 +109,29 @@ if (-not $SkipManagedIdentityRoleAssignment) {
109109
-ErrorAction SilentlyContinue
110110

111111
if (-not $existingRole) {
112-
New-AzRoleAssignment `
113-
-ObjectId $principalId `
114-
-RoleDefinitionName $requiredRoleName `
115-
-Scope $AssignmentScope `
116-
-ErrorAction Stop | Out-Null
117-
118-
Write-Output "Assigned '$requiredRoleName' to policy assignment identity ($principalId) at scope $AssignmentScope."
112+
$maxRetries = 5
113+
$retryDelay = 10
114+
for ($i = 1; $i -le $maxRetries; $i++) {
115+
try {
116+
New-AzRoleAssignment `
117+
-ObjectId $principalId `
118+
-RoleDefinitionName $requiredRoleName `
119+
-Scope $AssignmentScope `
120+
-ErrorAction Stop | Out-Null
121+
122+
Write-Output "Assigned '$requiredRoleName' to policy assignment identity ($principalId) at scope $AssignmentScope."
123+
break
124+
}
125+
catch {
126+
if ($_.Exception.Message -match 'Conflict') {
127+
Write-Output "Assigned '$requiredRoleName' to policy assignment identity ($principalId) at scope $AssignmentScope (confirmed after retry)."
128+
break
129+
}
130+
if ($i -eq $maxRetries) { throw }
131+
Write-Output "Waiting ${retryDelay}s for identity replication before assigning '$requiredRoleName' ($i/$maxRetries)..."
132+
Start-Sleep -Seconds $retryDelay
133+
}
134+
}
119135
}
120136
else {
121137
Write-Output "Policy assignment identity already has '$requiredRoleName' at scope $AssignmentScope."

0 commit comments

Comments
 (0)