Skip to content

Commit e8dd148

Browse files
iamanmolmHarness
authored andcommitted
feat:[CCM-24734]: Improve on public prices for Azure services (#18)
* 897602 feat:[CCM-24734]: Improve on public prices for Azure services
1 parent a211f7a commit e8dd148

File tree

1 file changed

+108
-6
lines changed

1 file changed

+108
-6
lines changed

internal/cloudinfo/providers/azure/cloudinfo.go

Lines changed: 108 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ package azure
1616

1717
import (
1818
"context"
19+
"encoding/json"
1920
"fmt"
21+
"io"
22+
"net/http"
23+
"net/url"
2024
"regexp"
2125
"strconv"
2226
"strings"
@@ -54,6 +58,11 @@ var (
5458
"uk": "uk",
5559
"us": "us",
5660
"za": "southafrica",
61+
"pl": "poland",
62+
"es": "spain",
63+
"il": "israel",
64+
"ch": "switzerland",
65+
"mx": "mexico",
5766
}
5867

5968
// mtBasic, _ = regexp.Compile("^BASIC.A\\d+[_Promo]*$")
@@ -199,10 +208,99 @@ func (a *AzureInfoer) checkRegionID(regionID string, regions map[string]string)
199208
return false
200209
}
201210

211+
type RetailPriceItem struct {
212+
ArmSkuName string `json:"armSkuName"`
213+
Price float64 `json:"retailPrice"`
214+
ProductName string `json:"productName"`
215+
Region string `json:"armRegionName"`
216+
StartDate time.Time `json:"effectiveStartDate"`
217+
MeterName string `json:"meterName"`
218+
}
219+
220+
type RetailPriceResponse struct {
221+
Items []RetailPriceItem `json:"Items"`
222+
NextPageURL string `json:"NextPageLink"`
223+
}
224+
225+
func (a *AzureInfoer) getPricingWithRetailPricesAPI() (map[string]map[string]types.Price, error) {
226+
allPrices := make(map[string]map[string]types.Price)
227+
228+
filter := "serviceName eq 'Virtual Machines' and priceType eq 'Consumption'"
229+
baseURL := "https://prices.azure.com/api/retail/prices?$filter=" + url.QueryEscape(filter)
230+
// ex: https://prices.azure.com/api/retail/prices?$filter=serviceName%20eq%20%27Virtual%20Machines%27%20and%20priceType%20eq%20%27Consumption%27%20and%20armSkuName%20eq%20%27Standard_D8as_v5%27%20and%20armRegionName%20eq%20%27australiaeast%27
231+
232+
url := baseURL
233+
for url != "" {
234+
client := &http.Client{}
235+
req, _ := http.NewRequest("GET", url, nil)
236+
req.Header.Set("User-Agent", "go-client")
237+
238+
resp, err := client.Do(req)
239+
if err != nil {
240+
return allPrices, fmt.Errorf("failed to call retail API: %w", err)
241+
}
242+
defer resp.Body.Close()
243+
244+
if resp.StatusCode != http.StatusOK {
245+
body, _ := io.ReadAll(resp.Body)
246+
return allPrices, fmt.Errorf("unexpected status code %d: %s", resp.StatusCode, string(body))
247+
}
248+
249+
body, err := io.ReadAll(resp.Body)
250+
if err != nil {
251+
return allPrices, fmt.Errorf("error while getting body %w", err)
252+
}
253+
254+
var data RetailPriceResponse
255+
if err := json.Unmarshal(body, &data); err != nil {
256+
return nil, fmt.Errorf("failed to unmarshal retail API response: %w", err)
257+
}
258+
259+
for _, item := range data.Items {
260+
// Ignoring the windows pricing similar to below
261+
if strings.Contains(strings.ToLower(item.ProductName), "windows") {
262+
continue
263+
}
264+
265+
// Ignoring the product if it do not contain virtual machines
266+
if !strings.Contains(strings.ToLower(item.ProductName), "virtual machines") {
267+
continue
268+
}
269+
270+
region := strings.ToLower(item.Region)
271+
272+
if allPrices[region] == nil {
273+
allPrices[region] = make(map[string]types.Price)
274+
}
275+
276+
price := allPrices[region][item.ArmSkuName]
277+
if strings.Contains(strings.ToLower(item.MeterName), "spot") {
278+
spotPrice := make(types.SpotPriceInfo)
279+
spotPrice[region] = item.Price
280+
price.SpotPrice = spotPrice
281+
} else if strings.Contains(strings.ToLower(item.MeterName), "low priority") {
282+
// ignore this as this is old spot type pricing
283+
} else {
284+
price.OnDemandPrice = item.Price
285+
}
286+
287+
allPrices[region][item.ArmSkuName] = price
288+
}
289+
290+
url = data.NextPageURL
291+
}
292+
293+
return allPrices, nil
294+
}
295+
202296
// Initialize downloads and parses the Rate Card API's meter list on Azure
203297
func (a *AzureInfoer) Initialize() (map[string]map[string]types.Price, error) {
204298
a.log.Debug("initializing price info")
205-
allPrices := make(map[string]map[string]types.Price)
299+
allPrices, err := a.getPricingWithRetailPricesAPI()
300+
if err != nil {
301+
a.log.Error(fmt.Sprintf("error while fetching azure prices with retail prices API %v", err))
302+
allPrices = make(map[string]map[string]types.Price)
303+
}
206304

207305
regions, err := a.GetRegions("compute")
208306
if err != nil {
@@ -246,12 +344,16 @@ func (a *AzureInfoer) Initialize() (map[string]map[string]types.Price, error) {
246344
for _, instanceType := range instanceTypes {
247345
price := allPrices[region][instanceType]
248346
if !strings.Contains(*v.MeterName, "Low Priority") {
249-
price.OnDemandPrice = priceInUsd
347+
if price.OnDemandPrice == 0 {
348+
price.OnDemandPrice = priceInUsd
349+
}
250350
} else {
251-
spotPrice := make(types.SpotPriceInfo)
252-
spotPrice[region] = priceInUsd
253-
price.SpotPrice = spotPrice
254-
metrics.ReportAzureSpotPrice(region, instanceType, priceInUsd)
351+
if price.SpotPrice == nil {
352+
spotPrice := make(types.SpotPriceInfo)
353+
spotPrice[region] = priceInUsd
354+
price.SpotPrice = spotPrice
355+
metrics.ReportAzureSpotPrice(region, instanceType, priceInUsd)
356+
}
255357
}
256358

257359
allPrices[region][instanceType] = price

0 commit comments

Comments
 (0)