Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 61 additions & 3 deletions arm/CUDly-CrossSubscription/template.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"contentVersion": "1.0.0.0",

"metadata": {
"description": "CUDly Cross-Subscription Role Assignments — deploy this in every target Azure subscription that CUDly should manage. It grants the CUDly service principal the permissions needed to query reservation recommendations and purchase Azure Reservations."
"description": "CUDly Cross-Subscription Role Assignments — deploy this in every target Azure subscription that CUDly should manage. It grants the CUDly service principal the permissions needed to query reservation recommendations and purchase Azure Reservations and Savings Plans."
},

"parameters": {
Expand All @@ -27,10 +27,58 @@
"reader": "/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7",
"costManagementReader": "/providers/Microsoft.Authorization/roleDefinitions/72fafb9e-0641-4937-9268-a91bfd8191a3",
"reservationPurchaser": "/providers/Microsoft.Authorization/roleDefinitions/f7b75c60-3036-4b75-91c3-6b41c27c1689"
}
},
"customRoleName": "[guid(subscription().subscriptionId, 'cudly-reservation-purchaser')]",
"customRoleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', guid(subscription().subscriptionId, 'cudly-reservation-purchaser'))]"
},

"resources": [
{
"type": "Microsoft.Authorization/roleDefinitions",
"apiVersion": "2022-04-01",
"name": "[variables('customRoleName')]",
"properties": {
"roleName": "CUDly Reservation and Savings Plan Purchaser",
"description": "Custom role granting CUDly exactly the Microsoft.Capacity and Microsoft.BillingBenefits actions it calls at runtime. Complements the built-in Reservation Purchaser assignment (which covers catalog reads and recommendations) by adding the purchase and calculatePrice actions that the built-in role omits.",
"type": "CustomRole",
"permissions": [
{
"actions": [
"Microsoft.Capacity/calculateprice/action",
"Microsoft.Capacity/reservationorders/write",
"Microsoft.Capacity/reservationorders/read",
"Microsoft.Capacity/reservationorders/reservations/read",
"Microsoft.Capacity/register/action",
"Microsoft.Capacity/catalogs/read",
"Microsoft.BillingBenefits/savingsPlanOrderAliases/write",
"Microsoft.BillingBenefits/savingsPlanOrders/read",
"Microsoft.BillingBenefits/savingsPlanOrders/savingsPlans/read",
"Microsoft.BillingBenefits/savingsPlanOrders/action"
],
"notActions": []
}
],
"assignableScopes": [
"[subscription().id]"
]
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
},

{
"type": "Microsoft.Authorization/roleAssignments",
"apiVersion": "2022-04-01",
"name": "[guid(parameters('servicePrincipalObjectId'), 'cudlyCustomRole', subscription().subscriptionId)]",
"dependsOn": [
"[variables('customRoleDefinitionId')]"
],
"properties": {
"roleDefinitionId": "[variables('customRoleDefinitionId')]",
"principalId": "[parameters('servicePrincipalObjectId')]",
"principalType": "ServicePrincipal",
"description": "CUDly — purchase reservations and savings plans via custom role that enumerates every required action explicitly"
}
},

{
"type": "Microsoft.Authorization/roleAssignments",
"apiVersion": "2022-04-01",
Expand All @@ -42,6 +90,7 @@
"description": "CUDly — enumerate subscription resources and locations"
}
},

{
"type": "Microsoft.Authorization/roleAssignments",
"apiVersion": "2022-04-01",
Expand All @@ -53,6 +102,7 @@
"description": "CUDly — read cost and reservation utilisation data"
}
},

{
"type": "Microsoft.Authorization/roleAssignments",
"apiVersion": "2022-04-01",
Expand All @@ -61,9 +111,10 @@
"roleDefinitionId": "[variables('roles').reservationPurchaser]",
"principalId": "[parameters('servicePrincipalObjectId')]",
"principalType": "ServicePrincipal",
"description": "CUDly — purchase Azure Reservations in this subscription"
"description": "CUDly — reservation catalog reads and recommendations via built-in Reservation Purchaser role (kept in addition to the custom role)"
}
},

{
"type": "Microsoft.Authorization/roleAssignments",
"apiVersion": "2022-04-01",
Expand Down Expand Up @@ -92,6 +143,13 @@
"metadata": {
"description": "Azure AD tenant ID to provide when registering this account in CUDly (azure_tenant_id field)."
}
},
"customRoleDefinitionId": {
"type": "string",
"value": "[variables('customRoleDefinitionId')]",
"metadata": {
"description": "Resource ID of the CUDly custom role definition created in this subscription."
}
}
}
}
42 changes: 35 additions & 7 deletions known-issues.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,34 @@
# Known Issues

The seven limitations previously listed here have been resolved or
scoped-down with explicit follow-ups. This file tracks what's still
outstanding so future work has a clear starting point.
This file tracks outstanding limitations and things that require operator
action. Resolved items are moved to the Resolved section at the bottom.

## Outstanding

### Azure ARM template re-deployment required for purchase support (issue #731)

The built-in "Reservation Purchaser" role (f7b75c60-3036-4b75-91c3-6b41c27c1689)
does not include `Microsoft.Capacity/calculateprice/action`,
`Microsoft.Capacity/reservationorders/write`, or
`Microsoft.BillingBenefits/savingsPlanOrderAliases/write`. Without these,
the live purchase API returns 403.

`arm/CUDly-CrossSubscription/template.json` has been updated (fix/731-arm-roles)
to add a custom role "CUDly Reservation and Savings Plan Purchaser" that enumerates
all required actions explicitly. Existing tenants who applied the ARM template before
this fix MUST re-deploy it:

```bash
az deployment sub create \
--location eastus \
--template-file arm/CUDly-CrossSubscription/template.json \
--parameters servicePrincipalObjectId=<SP-object-id> \
--name CUDly-CrossSubscription \
--no-prompt
```

Until re-deployed, `PurchaseCommitment` and `ValidateOffering` for savings plans
will continue to return 403.

## Resolved

Expand Down Expand Up @@ -184,10 +210,12 @@ outstanding so future work has a clear starting point.
- **GCP account `serene-bazaar-666` deploy SA missing `compute.regions.list`**:
Visible in production Lambda logs (`2026-04-21T16:28:22Z` and onward):

[ERROR] GCP account GCP serene-bazaar-666 (serene-bazaar-666):
get recommendations: failed to get regions: failed to list regions:
googleapi: Error 403: Required 'compute.regions.list' permission
for 'projects/serene-bazaar-666'
```text
[ERROR] GCP account GCP serene-bazaar-666 (serene-bazaar-666):
get recommendations: failed to get regions: failed to list regions:
googleapi: Error 403: Required 'compute.regions.list' permission
for 'projects/serene-bazaar-666'
```

The deploy service account that CUDly impersonates for that project
doesn't have `roles/compute.viewer` (or a custom role that includes
Expand Down
Loading