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
2 changes: 1 addition & 1 deletion providers/azure/internal/pricing/retail_prices.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ type HTTPClient interface {
// type in their own package so the JSON decode produces values the caller
// already knows how to read.
type Page[T any] struct {
Items []T `json:"Items"`
NextPageLink string `json:"NextPageLink"`
Items []T `json:"Items"`
Count int `json:"Count"`
}

Expand Down
26 changes: 26 additions & 0 deletions providers/azure/internal/pricing/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package pricing

// RetailPriceItem is the canonical item shape for the Azure Retail Prices API
// (https://prices.azure.com/api/retail/prices). It is the union of all
// fields used by the service clients in this repository; services that only
// need a subset can safely ignore the extra fields; they will decode to their
// zero value.
//
// Keeping a single named type here means every service uses the same
// pricing.FetchAll[pricing.RetailPriceItem] call and the same item accessors,
// so a field rename or addition only requires a single edit.
type RetailPriceItem struct {
CurrencyCode string `json:"currencyCode"`
ArmRegionName string `json:"armRegionName"`
Location string `json:"location"`
ProductName string `json:"productName"`
ServiceName string `json:"serviceName"`
ArmSKUName string `json:"armSkuName"`
SKUName string `json:"skuName"`
MeterName string `json:"meterName"`
UnitOfMeasure string `json:"unitOfMeasure"`
ReservationTerm string `json:"reservationTerm"`
Type string `json:"type"`
RetailPrice float64 `json:"retailPrice"`
UnitPrice float64 `json:"unitPrice"`
}
26 changes: 4 additions & 22 deletions providers/azure/services/cache/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,26 +134,8 @@ func (c *CacheClient) GetRegion() string {
return c.region
}

// CacheRetailPriceItem is the Azure Retail Prices API item shape for
// Redis Cache. Lifted from the previous inline anonymous struct so it
// can serve as the type parameter to pricing.FetchAll.
type CacheRetailPriceItem struct {
CurrencyCode string `json:"currencyCode"`
RetailPrice float64 `json:"retailPrice"`
UnitPrice float64 `json:"unitPrice"`
ArmRegionName string `json:"armRegionName"`
ProductName string `json:"productName"`
ServiceName string `json:"serviceName"`
ArmSKUName string `json:"armSkuName"`
MeterName string `json:"meterName"`
ReservationTerm string `json:"reservationTerm"`
Type string `json:"type"`
}

// AzureRetailPrice is the service-local envelope consumers still reference.
type AzureRetailPrice struct {
Items []CacheRetailPriceItem `json:"Items"`
}
// AzureRetailPrice is the response envelope for the Azure Retail Prices API.
type AzureRetailPrice = pricing.Page[pricing.RetailPriceItem]

// GetRecommendations gets Redis Cache reservation recommendations from Azure Consumption API
func (c *CacheClient) GetRecommendations(ctx context.Context, params common.RecommendationParams) ([]common.Recommendation, error) {
Expand Down Expand Up @@ -590,7 +572,7 @@ func (c *CacheClient) fetchAzurePricing(ctx context.Context, serviceName, sku, r
params.Add("api-version", "2023-01-01-preview")

initialURL := "https://prices.azure.com/api/retail/prices?" + params.Encode()
items, err := pricing.FetchAll[CacheRetailPriceItem](ctx, c.httpClient, initialURL, pricing.DefaultPageTimeout, pricing.DefaultMaxPages)
items, err := pricing.FetchAll[pricing.RetailPriceItem](ctx, c.httpClient, initialURL, pricing.DefaultPageTimeout, pricing.DefaultMaxPages)
if err != nil {
return nil, err
}
Expand All @@ -608,7 +590,7 @@ func azureTermString(termYears int) string {
}

// extractRedisPricing extracts on-demand and reservation pricing from price items
func extractRedisPricing(items []CacheRetailPriceItem, termYears int) (onDemand, reservation float64, currency string) {
func extractRedisPricing(items []pricing.RetailPriceItem, termYears int) (onDemand, reservation float64, currency string) {
currency = "USD"
termStr := azureTermString(termYears)

Expand Down
7 changes: 4 additions & 3 deletions providers/azure/services/cache/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/stretchr/testify/require"

"github.com/LeanerCloud/CUDly/pkg/common"
azpricing "github.com/LeanerCloud/CUDly/providers/azure/internal/pricing"
"github.com/LeanerCloud/CUDly/providers/azure/mocks"
)

Expand Down Expand Up @@ -223,7 +224,7 @@ func TestCacheClient_ValidateOffering_InvalidSKU(t *testing.T) {

func TestAzureRetailPriceStructure(t *testing.T) {
price := AzureRetailPrice{
Items: []CacheRetailPriceItem{
Items: []azpricing.RetailPriceItem{
{
CurrencyCode: "USD",
RetailPrice: 100.0,
Expand Down Expand Up @@ -1293,7 +1294,7 @@ func TestCacheClient_GetValidResourceTypes_PageCapFires(t *testing.T) {
// skipped and reservationPrice remained 0, causing a false "no reservation
// pricing found" error even when the pricing row was present.
func TestExtractRedisPricing_SingularOneYear(t *testing.T) {
items := []CacheRetailPriceItem{
items := []azpricing.RetailPriceItem{
{
CurrencyCode: "USD",
RetailPrice: 0.68,
Expand All @@ -1318,7 +1319,7 @@ func TestExtractRedisPricing_SingularOneYear(t *testing.T) {
// TestExtractRedisPricing_PluralThreeYears verifies that the plural form
// "3 Years" continues to work for multi-year terms.
func TestExtractRedisPricing_PluralThreeYears(t *testing.T) {
items := []CacheRetailPriceItem{
items := []azpricing.RetailPriceItem{
{CurrencyCode: "USD", RetailPrice: 0.68, UnitPrice: 0.68, Type: "Consumption"},
{CurrencyCode: "USD", RetailPrice: 11000.0, ReservationTerm: "3 Years", Type: "Reservation"},
}
Expand Down
26 changes: 9 additions & 17 deletions providers/azure/services/compute/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,23 +157,15 @@ func (c *ComputeClient) GetRegion() string {
return c.region
}

// AzureRetailPrice represents pricing from Azure Retail Prices API
type AzureRetailPriceItem struct {
CurrencyCode string `json:"currencyCode"`
RetailPrice float64 `json:"retailPrice"`
UnitPrice float64 `json:"unitPrice"`
ArmRegionName string `json:"armRegionName"`
ProductName string `json:"productName"`
ServiceName string `json:"serviceName"`
ArmSKUName string `json:"armSkuName"`
ReservationTerm string `json:"reservationTerm"`
Type string `json:"type"`
}

type AzureRetailPrice struct {
Items []AzureRetailPriceItem `json:"Items"`
NextPageLink string `json:"NextPageLink"`
}
// AzureRetailPriceItem is a type alias for pricing.RetailPriceItem kept for
// backward compatibility within this package. New code should use
// pricing.RetailPriceItem directly.
type AzureRetailPriceItem = pricing.RetailPriceItem

// AzureRetailPrice is the response envelope for the Azure Retail Prices API.
// It wraps pricing.Page[pricing.RetailPriceItem] under a package-local name
// so existing call sites do not need to be updated.
type AzureRetailPrice = pricing.Page[pricing.RetailPriceItem]

// GetRecommendations gets VM RI recommendations from Azure Consumption API
func (c *ComputeClient) GetRecommendations(ctx context.Context, params common.RecommendationParams) ([]common.Recommendation, error) {
Expand Down
29 changes: 6 additions & 23 deletions providers/azure/services/cosmosdb/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,29 +125,12 @@ func (c *CosmosDBClient) GetRegion() string {
return c.region
}

// CosmosRetailPriceItem is the Azure Retail Prices API item shape for
// Cosmos DB. Lifted from the previous inline anonymous struct so it can
// serve as the type parameter to pricing.FetchAll.
type CosmosRetailPriceItem struct {
CurrencyCode string `json:"currencyCode"`
RetailPrice float64 `json:"retailPrice"`
UnitPrice float64 `json:"unitPrice"`
ArmRegionName string `json:"armRegionName"`
Location string `json:"location"`
MeterName string `json:"meterName"`
SKUName string `json:"skuName"`
ProductName string `json:"productName"`
ServiceName string `json:"serviceName"`
UnitOfMeasure string `json:"unitOfMeasure"`
Type string `json:"type"`
ArmSKUName string `json:"armSkuName"`
ReservationTerm string `json:"reservationTerm"`
}

// AzureRetailPrice is the service-local envelope consumers still reference.
type AzureRetailPrice struct {
Items []CosmosRetailPriceItem `json:"Items"`
}
// CosmosRetailPriceItem is a type alias for pricing.RetailPriceItem kept for
// backward compatibility within this package.
type CosmosRetailPriceItem = pricing.RetailPriceItem

// AzureRetailPrice is the response envelope for the Azure Retail Prices API.
type AzureRetailPrice = pricing.Page[pricing.RetailPriceItem]

// GetRecommendations gets Cosmos DB reservation recommendations from Azure Consumption API
func (c *CosmosDBClient) GetRecommendations(ctx context.Context, params common.RecommendationParams) ([]common.Recommendation, error) {
Expand Down
32 changes: 4 additions & 28 deletions providers/azure/services/database/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,32 +127,8 @@ func (c *DatabaseClient) GetRegion() string {
return c.region
}

// DatabaseRetailPriceItem is the Azure Retail Prices API item shape for
// SQL Database products. Lifted from the previous inline anonymous
// struct so it can serve as the type parameter to pricing.FetchAll.
type DatabaseRetailPriceItem struct {
CurrencyCode string `json:"currencyCode"`
RetailPrice float64 `json:"retailPrice"`
UnitPrice float64 `json:"unitPrice"`
ArmRegionName string `json:"armRegionName"`
Location string `json:"location"`
MeterName string `json:"meterName"`
SKUName string `json:"skuName"`
ProductName string `json:"productName"`
ServiceName string `json:"serviceName"`
UnitOfMeasure string `json:"unitOfMeasure"`
Type string `json:"type"`
ArmSKUName string `json:"armSkuName"`
ReservationTerm string `json:"reservationTerm"`
}

// AzureRetailPrice is the service-local envelope consumers still reference.
// The shared pricing walker produces the items slice; this wrapper lets
// the rest of the package keep its `*AzureRetailPrice` idiom without a
// wider rename.
type AzureRetailPrice struct {
Items []DatabaseRetailPriceItem `json:"Items"`
}
// AzureRetailPrice is the response envelope for the Azure Retail Prices API.
type AzureRetailPrice = pricing.Page[pricing.RetailPriceItem]

// GetRecommendations gets SQL Database reservation recommendations from Azure Consumption API
func (c *DatabaseClient) GetRecommendations(ctx context.Context, params common.RecommendationParams) ([]common.Recommendation, error) {
Expand Down Expand Up @@ -575,7 +551,7 @@ func (c *DatabaseClient) fetchAzurePricing(ctx context.Context, filter string) (
params.Add("api-version", "2023-01-01-preview")

initialURL := "https://prices.azure.com/api/retail/prices?" + params.Encode()
items, err := pricing.FetchAll[DatabaseRetailPriceItem](ctx, c.httpClient, initialURL, pricing.DefaultPageTimeout, pricing.DefaultMaxPages)
items, err := pricing.FetchAll[pricing.RetailPriceItem](ctx, c.httpClient, initialURL, pricing.DefaultPageTimeout, pricing.DefaultMaxPages)
if err != nil {
return nil, err
}
Expand All @@ -593,7 +569,7 @@ func azureTermString(termYears int) string {
}

// extractSQLPricing extracts on-demand and reservation pricing from price items
func extractSQLPricing(items []DatabaseRetailPriceItem, termYears int) (onDemand, reservation float64, currency string) {
func extractSQLPricing(items []pricing.RetailPriceItem, termYears int) (onDemand, reservation float64, currency string) {
currency = "USD"
termStr := azureTermString(termYears)

Expand Down
3 changes: 2 additions & 1 deletion providers/azure/services/database/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/stretchr/testify/require"

"github.com/LeanerCloud/CUDly/pkg/common"
azpricing "github.com/LeanerCloud/CUDly/providers/azure/internal/pricing"
"github.com/LeanerCloud/CUDly/providers/azure/mocks"
)

Expand Down Expand Up @@ -213,7 +214,7 @@ func TestSQLPricingStructure(t *testing.T) {

func TestAzureRetailPriceStructure(t *testing.T) {
price := AzureRetailPrice{
Items: []DatabaseRetailPriceItem{
Items: []azpricing.RetailPriceItem{
{
CurrencyCode: "USD",
RetailPrice: 500.0,
Expand Down
21 changes: 4 additions & 17 deletions providers/azure/services/managedredis/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,21 +111,8 @@ func (c *ManagedRedisClient) GetRegion() string {
return c.region
}

// redisPriceItem is a single record from the Azure Retail Prices API used by
// pricing.FetchAll as the type parameter T. Named so it can satisfy the
// constraint; the anonymous struct in AzureRetailPrice was the blocker.
type redisPriceItem struct {
CurrencyCode string `json:"currencyCode"`
RetailPrice float64 `json:"retailPrice"`
UnitPrice float64 `json:"unitPrice"`
ArmRegionName string `json:"armRegionName"`
ProductName string `json:"productName"`
ServiceName string `json:"serviceName"`
ArmSKUName string `json:"armSkuName"`
MeterName string `json:"meterName"`
ReservationTerm string `json:"reservationTerm"`
Type string `json:"type"`
}
// AzureRetailPrice is the response envelope for the Azure Retail Prices API.
type AzureRetailPrice = pricing.Page[pricing.RetailPriceItem]

// GetRecommendations gets Redis Cache reservation recommendations from the Azure Consumption API.
func (c *ManagedRedisClient) GetRecommendations(ctx context.Context, params common.RecommendationParams) ([]common.Recommendation, error) {
Expand Down Expand Up @@ -458,7 +445,7 @@ func (c *ManagedRedisClient) getRedisPricing(ctx context.Context, sku, region st

initialURL := "https://prices.azure.com/api/retail/prices?" + params.Encode()

items, err := pricing.FetchAll[redisPriceItem](ctx, c.httpClient, initialURL, pricing.DefaultPageTimeout, pricing.DefaultMaxPages)
items, err := pricing.FetchAll[pricing.RetailPriceItem](ctx, c.httpClient, initialURL, pricing.DefaultPageTimeout, pricing.DefaultMaxPages)
if err != nil {
return nil, fmt.Errorf("failed to fetch Redis Cache pricing: %w", err)
}
Expand Down Expand Up @@ -501,7 +488,7 @@ func azureTermString(termYears int) string {

// parsePriceItems extracts on-demand price, reservation price, and currency
// from the flat list returned by the Azure Retail Prices API.
func parsePriceItems(items []redisPriceItem, termYears int) (onDemand, reservation float64, currency string) {
func parsePriceItems(items []pricing.RetailPriceItem, termYears int) (onDemand, reservation float64, currency string) {
currency = "USD"
termStr := azureTermString(termYears)
for _, item := range items {
Expand Down
19 changes: 3 additions & 16 deletions providers/azure/services/savingsplans/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -388,21 +388,8 @@ func checkValidateResponse(resp armbillingbenefits.RPClientValidatePurchaseRespo
return nil
}

// savingsPlanPriceItem is a single record from the Azure Retail Prices API used
// by pricing.FetchAll as the type parameter T (requires a named type, not an
// anonymous struct). NOTE: this struct mirrors the one in services/compute and
// services/search; consolidating into internal/pricing is tracked as a follow-up.
type savingsPlanPriceItem struct {
CurrencyCode string `json:"currencyCode"`
RetailPrice float64 `json:"retailPrice"`
UnitPrice float64 `json:"unitPrice"`
ArmRegionName string `json:"armRegionName"`
ProductName string `json:"productName"`
ServiceName string `json:"serviceName"`
ArmSKUName string `json:"armSkuName"`
ReservationTerm string `json:"reservationTerm"`
Type string `json:"type"`
}
// AzureRetailPrice is the response envelope for the Azure Retail Prices API.
type AzureRetailPrice = pricing.Page[pricing.RetailPriceItem]

// GetOfferingDetails retrieves pricing details for an Azure Savings Plan offering.
func (c *Client) GetOfferingDetails(ctx context.Context, rec common.Recommendation) (*common.OfferingDetails, error) {
Expand Down Expand Up @@ -475,7 +462,7 @@ func (c *Client) fetchOnDemandRate(ctx context.Context, planType string) (float6
params.Add("api-version", "2023-01-01-preview")
initialURL := "https://prices.azure.com/api/retail/prices?" + params.Encode()

items, err := pricing.FetchAll[savingsPlanPriceItem](ctx, c.httpClient, initialURL, pricing.DefaultPageTimeout, pricing.DefaultMaxPages)
items, err := pricing.FetchAll[pricing.RetailPriceItem](ctx, c.httpClient, initialURL, pricing.DefaultPageTimeout, pricing.DefaultMaxPages)
if err != nil {
return 0, fmt.Errorf("failed to fetch on-demand rate for plan type %s: %w", planType, err)
}
Expand Down
Loading
Loading