@@ -16,7 +16,11 @@ package azure
1616
1717import (
1818 "context"
19+ "encoding/json"
1920 "fmt"
21+ "io"
22+ "net/http"
23+ "net/url"
2024 "regexp"
2125 "strconv"
2226 "strings"
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
203297func (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