Explorar el Código

Merge branch 'develop' into alex/opencost-1753-cluster-id

Alex Meijer hace 3 años
padre
commit
60d7d2cb80
Se han modificado 2 ficheros con 462 adiciones y 75 borrados
  1. 60 74
      pkg/cloud/awsprovider.go
  2. 402 1
      pkg/cloud/awsprovider_test.go

+ 60 - 74
pkg/cloud/awsprovider.go

@@ -64,7 +64,7 @@ var (
 	// It's of the form aws:///us-east-2a/i-0fea4fd46592d050b and we want i-0fea4fd46592d050b, if it exists
 	provIdRx      = regexp.MustCompile("aws:///([^/]+)/([^/]+)")
 	usageTypeRegx = regexp.MustCompile(".*(-|^)(EBS.+)")
-	versionRx     = regexp.MustCompile("^#Version: (\\d+)\\.\\d+$")
+	versionRx     = regexp.MustCompile(`^#Version: (\\d+)\\.\\d+$`)
 	regionRx      = regexp.MustCompile("([a-z]+-[a-z]+-[0-9])")
 )
 
@@ -257,6 +257,7 @@ type AWSPricingTerms struct {
 // AWSOfferTerm is a sku extension used to pay for the node.
 type AWSOfferTerm struct {
 	Sku             string                  `json:"sku"`
+	OfferTermCode   string                  `json:"offerTermCode"`
 	PriceDimensions map[string]*AWSRateCode `json:"priceDimensions"`
 }
 
@@ -299,16 +300,20 @@ type AWSProductTerms struct {
 // ClusterIdEnvVar is the environment variable in which one can manually set the ClusterId
 const ClusterIdEnvVar = "AWS_CLUSTER_ID"
 
-// OnDemandRateCode is appended to an node sku
-const OnDemandRateCode = ".JRTCKXETXF"
-const OnDemandRateCodeCn = ".99YE2YK9UR"
+// OnDemandRateCodes is are sets of identifiers for offerTermCodes matching 'On Demand' rates
+var OnDemandRateCodes = map[string]struct{}{
+	"JRTCKXETXF": {},
+}
 
-// ReservedRateCode is appended to a node sku
-const ReservedRateCode = ".38NPMPTW36"
+var OnDemandRateCodesCn = map[string]struct{}{
+	"99YE2YK9UR": {},
+	"5Y9WH78GDR": {},
+	"KW44MY7SZN": {},
+}
 
 // HourlyRateCode is appended to a node sku
-const HourlyRateCode = ".6YS6EN2CT7"
-const HourlyRateCodeCn = ".Q7UJUT2CE6"
+const HourlyRateCode = "6YS6EN2CT7"
+const HourlyRateCodeCn = "Q7UJUT2CE6"
 
 // volTypes are used to map between AWS UsageTypes and
 // EBS volume types, as they would appear in K8s storage class
@@ -359,34 +364,6 @@ var locationToRegion = map[string]string{
 	"AWS GovCloud (US-West)":    "us-gov-west-1",
 }
 
-var regionToBillingRegionCode = map[string]string{
-	"us-east-2":      "USE2",
-	"us-east-1":      "",
-	"us-west-1":      "USW1",
-	"us-west-2":      "USW2",
-	"ap-east-1":      "APE1",
-	"ap-south-1":     "APS3",
-	"ap-northeast-3": "APN3",
-	"ap-northeast-2": "APN2",
-	"ap-southeast-1": "APS1",
-	"ap-southeast-2": "APS2",
-	"ap-northeast-1": "APN1",
-	"ap-southeast-3": "APS4",
-	"ca-central-1":   "CAN1",
-	"cn-north-1":     "",
-	"cn-northwest-1": "",
-	"eu-central-1":   "EUC1",
-	"eu-west-1":      "EU",
-	"eu-west-2":      "EUW2",
-	"eu-west-3":      "EUW3",
-	"eu-north-1":     "EUN1",
-	"eu-south-1":     "EUS1",
-	"sa-east-1":      "SAE1",
-	"af-south-1":     "AFS1",
-	"us-gov-east-1":  "UGE1",
-	"us-gov-west-1":  "UGW1",
-}
-
 var loadedAWSSecret bool = false
 var awsSecret *AWSAccessKey = nil
 
@@ -923,19 +900,53 @@ func (aws *AWS) DownloadPricingData() error {
 		}
 	}
 
-	aws.Pricing = make(map[string]*AWSProductTerms)
 	aws.ValidPricingKeys = make(map[string]bool)
-	skusToKeys := make(map[string]string)
 
 	resp, pricingURL, err := aws.getRegionPricing(nodeList)
 	if err != nil {
 		return err
 	}
+	err = aws.populatePricing(resp, inputkeys)
+	if err != nil {
+		return err
+	}
+	log.Infof("Finished downloading \"%s\"", pricingURL)
+
+	if !aws.SpotRefreshEnabled() {
+		return nil
+	}
+
+	// Always run spot pricing refresh when performing download
+	aws.refreshSpotPricing(true)
+
+	// Only start a single refresh goroutine
+	if !aws.SpotRefreshRunning {
+		aws.SpotRefreshRunning = true
+
+		go func() {
+			defer errs.HandlePanic()
+
+			for {
+				log.Infof("Spot Pricing Refresh scheduled in %.2f minutes.", SpotRefreshDuration.Minutes())
+				time.Sleep(SpotRefreshDuration)
+
+				// Reoccurring refresh checks update times
+				aws.refreshSpotPricing(false)
+			}
+		}()
+	}
+
+	return nil
+}
+
+func (aws *AWS) populatePricing(resp *http.Response, inputkeys map[string]bool) error {
+	aws.Pricing = make(map[string]*AWSProductTerms)
+	skusToKeys := make(map[string]string)
 	dec := json.NewDecoder(resp.Body)
 	for {
 		t, err := dec.Token()
 		if err == io.EOF {
-			log.Infof("done loading \"%s\"\n", pricingURL)
+			log.Infof("done loading \"%s\"\n", resp.Request.URL.String())
 			break
 		} else if err != nil {
 			log.Errorf("error parsing response json %v", resp.Body)
@@ -955,7 +966,7 @@ func (aws *AWS) DownloadPricingData() error {
 
 				err = dec.Decode(&product)
 				if err != nil {
-					log.Errorf("Error parsing response from \"%s\": %v", pricingURL, err.Error())
+					log.Errorf("Error parsing response from \"%s\": %v", resp.Request.URL.String(), err.Error())
 					break
 				}
 
@@ -1024,7 +1035,8 @@ func (aws *AWS) DownloadPricingData() error {
 					if err != nil {
 						return err
 					}
-					skuOnDemand, err := dec.Token()
+					// SKUOndemand
+					_, err = dec.Token()
 					if err != nil {
 						return err
 					}
@@ -1040,10 +1052,10 @@ func (aws *AWS) DownloadPricingData() error {
 						aws.Pricing[key].OnDemand = offerTerm
 						aws.Pricing[spotKey].OnDemand = offerTerm
 						var cost string
-						if sku.(string)+OnDemandRateCode == skuOnDemand {
-							cost = offerTerm.PriceDimensions[sku.(string)+OnDemandRateCode+HourlyRateCode].PricePerUnit.USD
-						} else if sku.(string)+OnDemandRateCodeCn == skuOnDemand {
-							cost = offerTerm.PriceDimensions[sku.(string)+OnDemandRateCodeCn+HourlyRateCodeCn].PricePerUnit.CNY
+						if _, isMatch := OnDemandRateCodes[offerTerm.OfferTermCode]; isMatch {
+							cost = offerTerm.PriceDimensions[strings.Join([]string{sku.(string), offerTerm.OfferTermCode, HourlyRateCode}, ".")].PricePerUnit.USD
+						} else if _, isMatch := OnDemandRateCodesCn[offerTerm.OfferTermCode]; isMatch {
+							cost = offerTerm.PriceDimensions[strings.Join([]string{sku.(string), offerTerm.OfferTermCode, HourlyRateCodeCn}, ".")].PricePerUnit.CNY
 						}
 						if strings.Contains(key, "EBS:VolumeP-IOPS.piops") {
 							// If the specific UsageType is the per IO cost used on io1 volumes
@@ -1072,32 +1084,6 @@ func (aws *AWS) DownloadPricingData() error {
 			}
 		}
 	}
-	log.Infof("Finished downloading \"%s\"", pricingURL)
-
-	if !aws.SpotRefreshEnabled() {
-		return nil
-	}
-
-	// Always run spot pricing refresh when performing download
-	aws.refreshSpotPricing(true)
-
-	// Only start a single refresh goroutine
-	if !aws.SpotRefreshRunning {
-		aws.SpotRefreshRunning = true
-
-		go func() {
-			defer errs.HandlePanic()
-
-			for {
-				log.Infof("Spot Pricing Refresh scheduled in %.2f minutes.", SpotRefreshDuration.Minutes())
-				time.Sleep(SpotRefreshDuration)
-
-				// Reoccurring refresh checks update times
-				aws.refreshSpotPricing(false)
-			}
-		}()
-	}
-
 	return nil
 }
 
@@ -1268,12 +1254,12 @@ func (aws *AWS) createNode(terms *AWSProductTerms, usageType string, k Key) (*No
 
 	}
 	var cost string
-	c, ok := terms.OnDemand.PriceDimensions[terms.Sku+OnDemandRateCode+HourlyRateCode]
+	c, ok := terms.OnDemand.PriceDimensions[strings.Join([]string{terms.Sku, terms.OnDemand.OfferTermCode, HourlyRateCode}, ".")]
 	if ok {
 		cost = c.PricePerUnit.USD
 	} else {
 		// Check for Chinese pricing before throwing error
-		c, ok = terms.OnDemand.PriceDimensions[terms.Sku+OnDemandRateCodeCn+HourlyRateCodeCn]
+		c, ok = terms.OnDemand.PriceDimensions[strings.Join([]string{terms.Sku, terms.OnDemand.OfferTermCode, HourlyRateCodeCn}, ".")]
 		if ok {
 			cost = c.PricePerUnit.CNY
 		} else {
@@ -1433,7 +1419,7 @@ func (aws *AWS) getAWSAuth(forceReload bool, cp *CustomPricing) (string, string)
 	}
 
 	// 3. Fall back to env vars
-	if env.GetAWSAccessKeyID() == "" || env.GetAWSAccessKeyID() == "" {
+	if env.GetAWSAccessKeyID() == "" || env.GetAWSAccessKeySecret() == "" {
 		aws.serviceAccountChecks.set("hasKey", &ServiceAccountCheck{
 			Message: "AWS ServiceKey exists",
 			Status:  false,

+ 402 - 1
pkg/cloud/awsprovider_test.go

@@ -1,6 +1,13 @@
 package cloud
 
-import "testing"
+import (
+	"bytes"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"reflect"
+	"testing"
+)
 
 func Test_awsKey_getUsageType(t *testing.T) {
 	type fields struct {
@@ -91,3 +98,397 @@ func Test_awsKey_getUsageType(t *testing.T) {
 		})
 	}
 }
+
+// Test_populate_pricing
+//
+// Objective: To test core pricing population logic for AWS
+//
+//	Case 0: US endpoints
+//	 Take a portion of json returned from ondemand terms in us endpoints
+//	 load the request into the http response and give it to the function
+//	 inspect the resulting aws object after the function returns and validate fields
+//	Case 1: Chinese endpoints
+//	 Same as above US test case, except using CN PV offer codes
+//	 Validate populated fields in AWS object
+func Test_populate_pricing(t *testing.T) {
+	awsTest := AWS{
+		ValidPricingKeys: map[string]bool{},
+	}
+	inputkeys := map[string]bool{
+		"us-east-2,m5.large,linux": true,
+	}
+	// Case 0
+	awsUSEastString := `
+	{
+		"formatVersion" : "v1.0",
+		"disclaimer" : "This pricing list is for informational purposes only. All prices are subject to the additional terms included in the pricing pages on http://aws.amazon.com. All Free Tier prices are also subject to the terms included at https://aws.amazon.com/free/",
+		"offerCode" : "AmazonEC2",
+		"version" : "20230322145651",
+		"publicationDate" : "2023-03-22T14:56:51Z",
+		"products" : {
+			"8D49XP354UEYTHGM" : {
+				"sku" : "8D49XP354UEYTHGM",
+				"productFamily" : "Compute Instance",
+				"attributes" : {
+				  "servicecode" : "AmazonEC2",
+				  "location" : "US East (Ohio)",
+				  "locationType" : "AWS Region",
+				  "instanceType" : "m5.large",
+				  "currentGeneration" : "Yes",
+				  "instanceFamily" : "General purpose",
+				  "vcpu" : "2",
+				  "physicalProcessor" : "Intel Xeon Platinum 8175",
+				  "clockSpeed" : "3.1 GHz",
+				  "memory" : "8 GiB",
+				  "storage" : "EBS only",
+				  "networkPerformance" : "Up to 10 Gigabit",
+				  "processorArchitecture" : "64-bit",
+				  "tenancy" : "Shared",
+				  "operatingSystem" : "Linux",
+				  "licenseModel" : "No License required",
+				  "usagetype" : "USE2-BoxUsage:m5.large",
+				  "operation" : "RunInstances",
+				  "availabilityzone" : "NA",
+				  "capacitystatus" : "Used",
+				  "classicnetworkingsupport" : "false",
+				  "dedicatedEbsThroughput" : "Up to 2120 Mbps",
+				  "ecu" : "10",
+				  "enhancedNetworkingSupported" : "Yes",
+				  "gpuMemory" : "NA",
+				  "intelAvxAvailable" : "Yes",
+				  "intelAvx2Available" : "Yes",
+				  "intelTurboAvailable" : "Yes",
+				  "marketoption" : "OnDemand",
+				  "normalizationSizeFactor" : "4",
+				  "preInstalledSw" : "NA",
+				  "processorFeatures" : "Intel AVX; Intel AVX2; Intel AVX512; Intel Turbo",
+				  "regionCode" : "us-east-2",
+				  "servicename" : "Amazon Elastic Compute Cloud",
+				  "vpcnetworkingsupport" : "true"
+				}
+			},
+			"9ZEEN7WWWQKAG292" : {
+				"sku" : "9ZEEN7WWWQKAG292",
+				"productFamily" : "Compute Instance",
+				"attributes" : {
+				  "servicecode" : "AmazonEC2",
+				  "location" : "US East (Ohio)",
+				  "locationType" : "AWS Region",
+				  "instanceType" : "p3.8xlarge",
+				  "currentGeneration" : "Yes",
+				  "instanceFamily" : "GPU instance",
+				  "vcpu" : "32",
+				  "physicalProcessor" : "Intel Xeon E5-2686 v4 (Broadwell)",
+				  "clockSpeed" : "2.3 GHz",
+				  "memory" : "244 GiB",
+				  "storage" : "EBS only",
+				  "networkPerformance" : "10 Gigabit",
+				  "processorArchitecture" : "64-bit",
+				  "tenancy" : "Shared",
+				  "operatingSystem" : "Windows",
+				  "licenseModel" : "Bring your own license",
+				  "usagetype" : "USE2-BoxUsage:p3.8xlarge",
+				  "operation" : "RunInstances:0800",
+				  "availabilityzone" : "NA",
+				  "capacitystatus" : "Used",
+				  "classicnetworkingsupport" : "false",
+				  "dedicatedEbsThroughput" : "7000 Mbps",
+				  "ecu" : "97",
+				  "enhancedNetworkingSupported" : "Yes",
+				  "gpu" : "4",
+				  "gpuMemory" : "NA",
+				  "intelAvxAvailable" : "Yes",
+				  "intelAvx2Available" : "Yes",
+				  "intelTurboAvailable" : "Yes",
+				  "marketoption" : "OnDemand",
+				  "normalizationSizeFactor" : "64",
+				  "preInstalledSw" : "NA",
+				  "processorFeatures" : "Intel AVX; Intel AVX2; Intel Turbo",
+				  "regionCode" : "us-east-2",
+				  "servicename" : "Amazon Elastic Compute Cloud",
+				  "vpcnetworkingsupport" : "true"
+				}
+			},
+			"M6UGCCQ3CDJQAA37" : {
+				"sku" : "M6UGCCQ3CDJQAA37",
+				"productFamily" : "Storage",
+				"attributes" : {
+				  "servicecode" : "AmazonEC2",
+				  "location" : "US East (Ohio)",
+				  "locationType" : "AWS Region",
+				  "storageMedia" : "SSD-backed",
+				  "volumeType" : "General Purpose",
+				  "maxVolumeSize" : "16 TiB",
+				  "maxIopsvolume" : "16000",
+				  "maxThroughputvolume" : "1000 MiB/s",
+				  "usagetype" : "USE2-EBS:VolumeUsage.gp3",
+				  "operation" : "",
+				  "regionCode" : "us-east-2",
+				  "servicename" : "Amazon Elastic Compute Cloud",
+				  "volumeApiName" : "gp3"
+				}
+			  }
+		},
+		"terms" : {
+			"OnDemand" : {
+				"M6UGCCQ3CDJQAA37" : {
+					"M6UGCCQ3CDJQAA37.JRTCKXETXF" : {
+					  "offerTermCode" : "JRTCKXETXF",
+					  "sku" : "M6UGCCQ3CDJQAA37",
+					  "effectiveDate" : "2023-03-01T00:00:00Z",
+					  "priceDimensions" : {
+						"M6UGCCQ3CDJQAA37.JRTCKXETXF.6YS6EN2CT7" : {
+						  "rateCode" : "M6UGCCQ3CDJQAA37.JRTCKXETXF.6YS6EN2CT7",
+						  "description" : "$0.08 per GB-month of General Purpose (gp3) provisioned storage - US East (Ohio)",
+						  "beginRange" : "0",
+						  "endRange" : "Inf",
+						  "unit" : "GB-Mo",
+						  "pricePerUnit" : {
+							"USD" : "0.0800000000"
+						  },
+						  "appliesTo" : [ ]
+						}
+					  },
+					  "termAttributes" : { }
+					}
+				},
+				"9ZEEN7WWWQKAG292" : {
+					"9ZEEN7WWWQKAG292.JRTCKXETXF" : {
+					  "offerTermCode" : "JRTCKXETXF",
+					  "sku" : "9ZEEN7WWWQKAG292",
+					  "effectiveDate" : "2023-03-01T00:00:00Z",
+					  "priceDimensions" : {
+						"9ZEEN7WWWQKAG292.JRTCKXETXF.6YS6EN2CT7" : {
+						  "rateCode" : "9ZEEN7WWWQKAG292.JRTCKXETXF.6YS6EN2CT7",
+						  "description" : "$12.24 per On Demand Windows BYOL p3.8xlarge Instance Hour",
+						  "beginRange" : "0",
+						  "endRange" : "Inf",
+						  "unit" : "Hrs",
+						  "pricePerUnit" : {
+							"USD" : "12.2400000000"
+						  },
+						  "appliesTo" : [ ]
+						}
+					  },
+					  "termAttributes" : { }
+					}
+				},
+				"8D49XP354UEYTHGM" : {
+					"8D49XP354UEYTHGM.MZU6U2429S" : {
+					  "offerTermCode" : "MZU6U2429S",
+					  "sku" : "8D49XP354UEYTHGM",
+					  "effectiveDate" : "2019-01-01T00:00:00Z",
+					  "priceDimensions" : {
+						"8D49XP354UEYTHGM.MZU6U2429S.2TG2D8R56U" : {
+						  "rateCode" : "8D49XP354UEYTHGM.MZU6U2429S.2TG2D8R56U",
+						  "description" : "Upfront Fee",
+						  "unit" : "Quantity",
+						  "pricePerUnit" : {
+							"USD" : "1161"
+						  },
+						  "appliesTo" : [ ]
+						},
+					  },
+					  "termAttributes" : {
+						"LeaseContractLength" : "3yr",
+						"OfferingClass" : "convertible",
+						"PurchaseOption" : "All Upfront"
+					  }
+					}
+				}
+			}
+		},
+		"attributesList" : { }
+	}
+	`
+
+	testResponse := http.Response{
+		Body: ioutil.NopCloser(bytes.NewBufferString(awsUSEastString)),
+		Request: &http.Request{
+			URL: &url.URL{
+				Scheme: "https",
+				Host:   "test-aws-http-endpoint:443",
+			},
+		},
+	}
+
+	awsTest.populatePricing(&testResponse, inputkeys)
+
+	expectedProdTermsDisk := &AWSProductTerms{
+		Sku:     "M6UGCCQ3CDJQAA37",
+		Memory:  "",
+		Storage: "",
+		VCpu:    "",
+		GPU:     "",
+		OnDemand: &AWSOfferTerm{
+			Sku:           "M6UGCCQ3CDJQAA37",
+			OfferTermCode: "JRTCKXETXF",
+			PriceDimensions: map[string]*AWSRateCode{
+				"M6UGCCQ3CDJQAA37.JRTCKXETXF.6YS6EN2CT7": &AWSRateCode{
+					Unit: "GB-Mo",
+					PricePerUnit: AWSCurrencyCode{
+						USD: "0.0800000000",
+						CNY: "",
+					},
+				},
+			},
+		},
+		PV: &PV{
+			Cost:       "0.00010958904109589041",
+			CostPerIO:  "",
+			Class:      "gp3",
+			Size:       "",
+			Region:     "us-east-2",
+			ProviderID: "",
+		},
+	}
+
+	expectedProdTermsInstanceOndemand := &AWSProductTerms{
+		Sku:     "8D49XP354UEYTHGM",
+		Memory:  "8 GiB",
+		Storage: "EBS only",
+		VCpu:    "2",
+		GPU:     "",
+		OnDemand: &AWSOfferTerm{
+			Sku:             "",
+			OfferTermCode:   "",
+			PriceDimensions: nil,
+		},
+	}
+
+	expectedProdTermsInstanceSpot := &AWSProductTerms{
+		Sku:     "8D49XP354UEYTHGM",
+		Memory:  "8 GiB",
+		Storage: "EBS only",
+		VCpu:    "2",
+		GPU:     "",
+		OnDemand: &AWSOfferTerm{
+			Sku:             "",
+			OfferTermCode:   "",
+			PriceDimensions: nil,
+		},
+	}
+
+	expectedPricing := map[string]*AWSProductTerms{
+		"us-east-2,EBS:VolumeUsage.gp3":             expectedProdTermsDisk,
+		"us-east-2,EBS:VolumeUsage.gp3,preemptible": expectedProdTermsDisk,
+		"us-east-2,m5.large,linux":                  expectedProdTermsInstanceOndemand,
+		"us-east-2,m5.large,linux,preemptible":      expectedProdTermsInstanceSpot,
+	}
+
+	if !reflect.DeepEqual(expectedPricing, awsTest.Pricing) {
+		t.Fatalf("expected parsed pricing did not match actual parsed result (us-east-1)")
+	}
+
+	// Case 1
+	awsCnString := `
+	{
+		"formatVersion" : "v1.0",
+		"disclaimer" : "This pricing list is for informational purposes only. All prices are subject to the additional terms included in the pricing pages on http://www.amazonaws.cn.",
+		"offerCode" : "AmazonEC2",
+		"version" : "20230314154740",
+		"publicationDate" : "2023-03-14T15:47:40Z",
+		"products" : {
+			"R83VXG9NAPDASEGN" : {
+				"sku" : "R83VXG9NAPDASEGN",
+				"productFamily" : "Storage",
+				"attributes" : {
+				  "servicecode" : "AmazonEC2",
+				  "location" : "China (Ningxia)",
+				  "locationType" : "AWS Region",
+				  "storageMedia" : "SSD-backed",
+				  "volumeType" : "General Purpose",
+				  "maxVolumeSize" : "16 TiB",
+				  "maxIopsvolume" : "16000",
+				  "maxThroughputvolume" : "1000 MiB/s",
+				  "usagetype" : "CNW1-EBS:VolumeUsage.gp3",
+				  "operation" : "",
+				  "regionCode" : "cn-northwest-1",
+				  "servicename" : "Amazon Elastic Compute Cloud",
+				  "volumeApiName" : "gp3"
+				}
+			}
+		},
+		"terms" : {
+			"OnDemand" : {
+			  "R83VXG9NAPDASEGN" : {
+				"R83VXG9NAPDASEGN.5Y9WH78GDR" : {
+				  "offerTermCode" : "5Y9WH78GDR",
+				  "sku" : "R83VXG9NAPDASEGN",
+				  "effectiveDate" : "2023-03-01T00:00:00Z",
+				  "priceDimensions" : {
+					"R83VXG9NAPDASEGN.5Y9WH78GDR.Q7UJUT2CE6" : {
+					  "rateCode" : "R83VXG9NAPDASEGN.5Y9WH78GDR.Q7UJUT2CE6",
+					  "description" : "0.5312 CNY per GB-month of General Purpose (gp3) provisioned storage - China (Ningxia)",
+					  "beginRange" : "0",
+					  "endRange" : "Inf",
+					  "unit" : "GB-Mo",
+					  "pricePerUnit" : {
+						"CNY" : "0.5312000000"
+					  },
+					  "appliesTo" : [ ]
+					}
+				  },
+				  "termAttributes" : { }
+				}
+			  }
+			}
+	    },
+	  "attributesList" : { }
+	}
+	`
+	awsTest = AWS{
+		ValidPricingKeys: map[string]bool{},
+	}
+
+	testResponse = http.Response{
+		Body: ioutil.NopCloser(bytes.NewBufferString(awsCnString)),
+		Request: &http.Request{
+			URL: &url.URL{
+				Scheme: "https",
+				Host:   "test-aws-http-endpoint:443",
+			},
+		},
+	}
+
+	awsTest.populatePricing(&testResponse, inputkeys)
+
+	expectedProdTermsDisk = &AWSProductTerms{
+		Sku:     "R83VXG9NAPDASEGN",
+		Memory:  "",
+		Storage: "",
+		VCpu:    "",
+		GPU:     "",
+		OnDemand: &AWSOfferTerm{
+			Sku:           "R83VXG9NAPDASEGN",
+			OfferTermCode: "5Y9WH78GDR",
+			PriceDimensions: map[string]*AWSRateCode{
+				"R83VXG9NAPDASEGN.5Y9WH78GDR.Q7UJUT2CE6": &AWSRateCode{
+					Unit: "GB-Mo",
+					PricePerUnit: AWSCurrencyCode{
+						USD: "",
+						CNY: "0.5312000000",
+					},
+				},
+			},
+		},
+		PV: &PV{
+			Cost:       "0.0007276712328767123",
+			CostPerIO:  "",
+			Class:      "gp3",
+			Size:       "",
+			Region:     "cn-northwest-1",
+			ProviderID: "",
+		},
+	}
+
+	expectedPricing = map[string]*AWSProductTerms{
+		"cn-northwest-1,EBS:VolumeUsage.gp3":             expectedProdTermsDisk,
+		"cn-northwest-1,EBS:VolumeUsage.gp3,preemptible": expectedProdTermsDisk,
+	}
+
+	if !reflect.DeepEqual(expectedPricing, awsTest.Pricing) {
+		t.Fatalf("expected parsed pricing did not match actual parsed result (cn)")
+	}
+
+}