Symptom
Every Azure reservation approve fails at calculatePrice with HTTP 400:
Failed to approve: execution d73b0ffe-8ada-42e0-902b-27a63ce54088 could not be approved:
some purchases failed: [Standard_D2a_v4: purchase failed: calculatePrice (attempt 1/3):
calculatePrice failed with status 400:
{"error":{"code":"DisplayNameInvalid","message":"Value of DisplayName property has invalid characters.
Allowed characters are alpha numeric, underscore and hyphen up to 64 characters."}}]
User-reported on the deployed feat/multicloud-web-frontend branch.
Root cause
5 Azure service clients construct displayName with spaces in the literal — Azure rejects anything outside [A-Za-z0-9_-]:
| File |
Line |
Current format |
providers/azure/services/cache/client.go |
296 |
"Redis Cache Reservation - %s" |
providers/azure/services/compute/client.go |
381 |
"VM Reservation - %s" |
providers/azure/services/cosmosdb/client.go |
292 |
"Cosmos DB Reservation - %s" |
providers/azure/services/database/client.go |
300 |
"SQL DB Reservation - %s" |
providers/azure/services/search/client.go |
267 |
"Search Service Reservation - %s" |
Spaces and the surrounding-spaces hyphen separator are both outside the allowlist. The trailing %s substitutes rec.ResourceType which is already alphanumeric-with-underscores (e.g. Standard_D2a_v4) so the SKU portion is fine — only the literal prefix is broken.
This appears to be a long-standing bug; the recent 2-step calculatePrice→purchase refactor (#680, PR) didn't introduce it but is the first time the field is actually round-tripped to Azure's API (the old path may have skipped displayName validation by going through a different SDK call).
Fix
Replace each literal with an underscore-separated, allowlist-conformant variant:
// before
"displayName": fmt.Sprintf("Redis Cache Reservation - %s", rec.ResourceType),
// after
"displayName": fmt.Sprintf("Redis_Cache_Reservation_%s", rec.ResourceType),
Concrete per-service replacements (preserve service identity for operators reading the Azure portal):
| Service |
New displayName format |
| cache |
Redis_Cache_Reservation_%s |
| compute |
VM_Reservation_%s |
| cosmosdb |
Cosmos_DB_Reservation_%s |
| database |
SQL_DB_Reservation_%s |
| search |
Search_Service_Reservation_%s |
All produce strings well under the 64-char limit (max from Search_Service_Reservation_<SKU> is ~40-50 chars for realistic SKUs).
Also add a small helper to enforce the rule centrally and prevent regression — likely in providers/azure/services/internal/reservations/ (where the shared 2-step purchase helper already lives):
// SanitizeDisplayName returns s with any character outside [A-Za-z0-9_-]
// replaced by '_', truncated to 64 chars. Azure rejects DisplayName fields
// that don't match this allowlist with DisplayNameInvalid 400.
func SanitizeDisplayName(s string) string { ... }
Each service then calls reservations.SanitizeDisplayName(fmt.Sprintf(...)) so future format changes are auto-sanitized.
Tests
Per service:
- Existing two-step / purchase tests pass the constructed
displayName through the mock capture — extend them to assert the regex ^[A-Za-z0-9_-]{1,64}$ matches the captured value.
- One new test for
SanitizeDisplayName covering: spaces → _, special chars → _, length truncation at 64, already-conformant input passes unchanged.
Why this is P1
Every Azure approve on the deployed branch currently fails immediately. There's no workaround for the user — they can't approve any Azure purchase. The fix is mechanical and tight (5 files + 1 small helper). Worth shipping today.
Related
Symptom
Every Azure reservation approve fails at
calculatePricewith HTTP 400:User-reported on the deployed
feat/multicloud-web-frontendbranch.Root cause
5 Azure service clients construct
displayNamewith spaces in the literal — Azure rejects anything outside[A-Za-z0-9_-]:providers/azure/services/cache/client.go"Redis Cache Reservation - %s"providers/azure/services/compute/client.go"VM Reservation - %s"providers/azure/services/cosmosdb/client.go"Cosmos DB Reservation - %s"providers/azure/services/database/client.go"SQL DB Reservation - %s"providers/azure/services/search/client.go"Search Service Reservation - %s"Spaces and the surrounding-spaces hyphen separator are both outside the allowlist. The trailing
%ssubstitutesrec.ResourceTypewhich is already alphanumeric-with-underscores (e.g.Standard_D2a_v4) so the SKU portion is fine — only the literal prefix is broken.This appears to be a long-standing bug; the recent 2-step calculatePrice→purchase refactor (#680, PR) didn't introduce it but is the first time the field is actually round-tripped to Azure's API (the old path may have skipped
displayNamevalidation by going through a different SDK call).Fix
Replace each literal with an underscore-separated, allowlist-conformant variant:
Concrete per-service replacements (preserve service identity for operators reading the Azure portal):
Redis_Cache_Reservation_%sVM_Reservation_%sCosmos_DB_Reservation_%sSQL_DB_Reservation_%sSearch_Service_Reservation_%sAll produce strings well under the 64-char limit (max from
Search_Service_Reservation_<SKU>is ~40-50 chars for realistic SKUs).Also add a small helper to enforce the rule centrally and prevent regression — likely in
providers/azure/services/internal/reservations/(where the shared 2-step purchase helper already lives):Each service then calls
reservations.SanitizeDisplayName(fmt.Sprintf(...))so future format changes are auto-sanitized.Tests
Per service:
displayNamethrough the mock capture — extend them to assert the regex^[A-Za-z0-9_-]{1,64}$matches the captured value.SanitizeDisplayNamecovering: spaces →_, special chars →_, length truncation at 64, already-conformant input passes unchanged.Why this is P1
Every Azure approve on the deployed branch currently fails immediately. There's no workaround for the user — they can't approve any Azure purchase. The fix is mechanical and tight (5 files + 1 small helper). Worth shipping today.
Related
fix/issue-677-azure-calculate-price) — landed the 2-step flow that surfaced this.