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
3 changes: 2 additions & 1 deletion providers/azure/services/cache/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/consumption/armconsumption"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/redis/armredis/v3"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/reservations/armreservations"

"github.com/LeanerCloud/CUDly/pkg/common"
"github.com/LeanerCloud/CUDly/pkg/logging"
Expand Down Expand Up @@ -320,7 +321,7 @@ func (c *CacheClient) PurchaseCommitment(ctx context.Context, rec common.Recomme
},
"location": c.region,
"properties": map[string]interface{}{
"reservedResourceType": "RedisCache",
"reservedResourceType": string(armreservations.ReservedResourceTypeRedisCache),
"billingScopeId": fmt.Sprintf("/subscriptions/%s", c.subscriptionID),
"term": fmt.Sprintf("P%dY", termYears),
"quantity": rec.Count,
Expand Down
3 changes: 2 additions & 1 deletion providers/azure/services/compute/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/consumption/armconsumption"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/reservations/armreservations"

"github.com/LeanerCloud/CUDly/pkg/common"
"github.com/LeanerCloud/CUDly/pkg/logging"
Expand Down Expand Up @@ -427,7 +428,7 @@ func (c *ComputeClient) buildReservationBody(rec common.Recommendation, source,
"sku": map[string]string{"name": rec.ResourceType},
"location": c.region,
"properties": map[string]interface{}{
"reservedResourceType": "VirtualMachines",
"reservedResourceType": string(armreservations.ReservedResourceTypeVirtualMachines),
"billingScopeId": fmt.Sprintf("/subscriptions/%s", c.subscriptionID),
"term": fmt.Sprintf("P%dY", termYears),
"quantity": rec.Count,
Expand Down
3 changes: 2 additions & 1 deletion providers/azure/services/cosmosdb/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/consumption/armconsumption"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos/v2"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/reservations/armreservations"

"github.com/LeanerCloud/CUDly/pkg/common"
"github.com/LeanerCloud/CUDly/pkg/logging"
Expand Down Expand Up @@ -313,7 +314,7 @@ func (c *CosmosDBClient) PurchaseCommitment(ctx context.Context, rec common.Reco
},
"location": c.region,
"properties": map[string]interface{}{
"reservedResourceType": "CosmosDb",
"reservedResourceType": string(armreservations.ReservedResourceTypeCosmosDb),
"billingScopeId": fmt.Sprintf("/subscriptions/%s", c.subscriptionID),
"term": fmt.Sprintf("P%dY", termYears),
"quantity": rec.Count,
Expand Down
3 changes: 2 additions & 1 deletion providers/azure/services/database/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/consumption/armconsumption"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/reservations/armreservations"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql"

"github.com/LeanerCloud/CUDly/pkg/common"
Expand Down Expand Up @@ -318,7 +319,7 @@ func (c *DatabaseClient) PurchaseCommitment(ctx context.Context, rec common.Reco
},
"location": c.region,
"properties": map[string]interface{}{
"reservedResourceType": "SqlDatabase",
"reservedResourceType": string(armreservations.ReservedResourceTypeSQLDatabases),
"billingScopeId": fmt.Sprintf("/subscriptions/%s", c.subscriptionID),
"term": fmt.Sprintf("P%dY", termYears),
"quantity": rec.Count,
Expand Down
52 changes: 52 additions & 0 deletions providers/azure/services/database/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/consumption/armconsumption"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/reservations/armreservations"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
Expand Down Expand Up @@ -1246,3 +1247,54 @@ func TestDatabaseClient_PurchaseCommitment_DisplayNameConformsToAzureAllowlist(t
assert.Regexp(t, `^sql-`, capturedDisplayName)
assert.Contains(t, capturedDisplayName, "GP_Gen5_2")
}

// TestDatabaseClient_PurchaseCommitment_CanonicalReservedResourceType guards
// against regression of issue #1189: the calculatePrice/purchase body must
// send the canonical SDK enum value "SqlDatabases", not the hand-written
// "SqlDatabase" Azure rejects as an invalid reservedResourceType.
func TestDatabaseClient_PurchaseCommitment_CanonicalReservedResourceType(t *testing.T) {
ctx := context.Background()
mockHTTP := &MockHTTPClient{}
mockCred := &MockTokenCredential{token: "test-token"}
client := NewClientWithHTTP(mockCred, "test-subscription", "eastus", mockHTTP)

const orderID = "azure-db-rrt"
var capturedRRT string
mockHTTP.On("Do", mock.MatchedBy(func(r *http.Request) bool {
if r.Method != http.MethodPost || r.URL.Path != "/providers/Microsoft.Capacity/calculatePrice" {
return false
}
if r.Body == nil {
return true
}
bodyBytes, _ := io.ReadAll(r.Body)
r.Body = io.NopCloser(bytes.NewReader(bodyBytes))
var body map[string]interface{}
if err := json.Unmarshal(bodyBytes, &body); err == nil {
if props, ok := body["properties"].(map[string]interface{}); ok {
if rrt, ok := props["reservedResourceType"].(string); ok {
capturedRRT = rrt
}
}
}
return true
})).Return(createMockHTTPResponse(http.StatusOK, calcPriceRespJSON(orderID)), nil).Once()
mockHTTP.On("Do", mock.MatchedBy(func(r *http.Request) bool {
return r.Method == http.MethodPost &&
r.URL.Path == "/providers/Microsoft.Capacity/reservationOrders/"+orderID+"/purchase"
})).Return(createMockHTTPResponse(http.StatusOK, `{}`), nil).Once()

rec := common.Recommendation{
ResourceType: "GP_Gen5_2",
Term: "1yr",
Count: 1,
CommitmentCost: 1500.0,
}
_, err := client.PurchaseCommitment(ctx, rec, common.PurchaseOptions{Source: common.PurchaseSourceCLI})
require.NoError(t, err)
assert.Equal(t, string(armreservations.ReservedResourceTypeSQLDatabases), capturedRRT)
assert.Contains(t, armreservations.PossibleReservedResourceTypeValues(),
armreservations.ReservedResourceType(capturedRRT),
"reservedResourceType %q must be a member of armreservations.PossibleReservedResourceTypeValues()", capturedRRT)
mockHTTP.AssertExpectations(t)
}
3 changes: 2 additions & 1 deletion providers/azure/services/managedredis/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/consumption/armconsumption"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/redis/armredis/v3"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/reservations/armreservations"

"github.com/LeanerCloud/CUDly/pkg/common"
"github.com/LeanerCloud/CUDly/providers/azure/internal/httpclient"
Expand Down Expand Up @@ -260,7 +261,7 @@ func (c *ManagedRedisClient) PurchaseCommitment(ctx context.Context, rec common.
},
"location": c.region,
"properties": map[string]interface{}{
"reservedResourceType": "RedisCache",
"reservedResourceType": string(armreservations.ReservedResourceTypeRedisCache),
"billingScopeId": fmt.Sprintf("/subscriptions/%s", c.subscriptionID),
"term": fmt.Sprintf("P%dY", termYears),
"quantity": rec.Count,
Expand Down
5 changes: 5 additions & 0 deletions providers/azure/services/search/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,11 @@ func (c *SearchClient) PurchaseCommitment(ctx context.Context, rec common.Recomm
},
"location": c.region,
"properties": map[string]interface{}{
// "SearchService" has no counterpart in the armreservations
// ReservedResourceType enum (checked v1.1.0 and v2.0.0), so it
// cannot be expressed as an SDK constant like the other service
// clients do; the literal is kept until verified against the
// live reservation catalog (see issue #1189).
"reservedResourceType": "SearchService",
"billingScopeId": fmt.Sprintf("/subscriptions/%s", c.subscriptionID),
"term": fmt.Sprintf("P%dY", termYears),
Expand Down
3 changes: 2 additions & 1 deletion providers/azure/services/synapse/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/consumption/armconsumption"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/reservations/armreservations"

"github.com/LeanerCloud/CUDly/pkg/common"
"github.com/LeanerCloud/CUDly/providers/azure/internal/httpclient"
Expand Down Expand Up @@ -268,7 +269,7 @@ func (c *SynapseClient) PurchaseCommitment(ctx context.Context, rec common.Recom
},
"location": c.region,
"properties": map[string]interface{}{
"reservedResourceType": "SqlDW",
"reservedResourceType": string(armreservations.ReservedResourceTypeSQLDataWarehouse),
"billingScopeId": fmt.Sprintf("/subscriptions/%s", c.subscriptionID),
"term": fmt.Sprintf("P%dY", termYears),
"quantity": rec.Count,
Expand Down
53 changes: 53 additions & 0 deletions providers/azure/services/synapse/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package synapse
import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
"net/http"
Expand All @@ -12,6 +13,7 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/consumption/armconsumption"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/reservations/armreservations"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -852,3 +854,54 @@ func TestGetOfferingDetails_noReservationPrice(t *testing.T) {
// helper's contract is now exercised in providers/azure/services/internal/reservations
// package tests; the synapse-side coverage is via the executor-level
// PurchaseCommitment tests above.

// TestPurchaseCommitment_canonicalReservedResourceType guards against
// regression of issue #1189: the calculatePrice/purchase body must send the
// canonical SDK enum value "SqlDataWarehouse", not the hand-written "SqlDW"
// Azure rejects as an invalid reservedResourceType.
func TestPurchaseCommitment_canonicalReservedResourceType(t *testing.T) {
mHTTP := &mockHTTPClient{}
t.Cleanup(func() { mHTTP.AssertExpectations(t) })

const orderID = "syn-order-rrt"
var capturedRRT string
mHTTP.On("Do", mock.MatchedBy(func(r *http.Request) bool {
if r.Method != http.MethodPost || r.URL.Path != "/providers/Microsoft.Capacity/calculatePrice" {
return false
}
if r.Body == nil {
return true
}
bodyBytes, _ := io.ReadAll(r.Body)
r.Body = io.NopCloser(bytes.NewReader(bodyBytes))
var body map[string]interface{}
if err := json.Unmarshal(bodyBytes, &body); err == nil {
if props, ok := body["properties"].(map[string]interface{}); ok {
if rrt, ok := props["reservedResourceType"].(string); ok {
capturedRRT = rrt
}
}
}
return true
})).Return(newHTTPResponse(http.StatusOK, calcPriceRespJSON(orderID)), nil).Once()
mHTTP.On("Do", mock.MatchedBy(func(r *http.Request) bool {
return r.Method == http.MethodPost &&
r.URL.Path == "/providers/Microsoft.Capacity/reservationOrders/"+orderID+"/purchase"
})).Return(newHTTPResponse(http.StatusOK, `{}`), nil).Once()

cred := &mockTokenCredential{token: "test-token"}
c := NewClientWithHTTP(cred, "sub-123", "eastus", mHTTP)

rec := common.Recommendation{
ResourceType: "DW1000c",
Term: "1yr",
Count: 1,
CommitmentCost: 5000.0,
}
_, err := c.PurchaseCommitment(context.Background(), rec, common.PurchaseOptions{Source: common.PurchaseSourceCLI})
require.NoError(t, err)
assert.Equal(t, string(armreservations.ReservedResourceTypeSQLDataWarehouse), capturedRRT)
assert.Contains(t, armreservations.PossibleReservedResourceTypeValues(),
armreservations.ReservedResourceType(capturedRRT),
"reservedResourceType %q must be a member of armreservations.PossibleReservedResourceTypeValues()", capturedRRT)
}
Loading