Skip to content

bug(providers/azure): displayName contains spaces — every Azure approve fails with DisplayNameInvalid #685

Description

@cristim

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions