Просмотр исходного кода

Merge pull request #1507 from avrodrigues5/avr/wip-aliyun-integ

PV pricing for cloud disk attached to the Alibaba K8s Cluster
Michael Dresser 3 лет назад
Родитель
Сommit
7876ed6d44
2 измененных файлов с 608 добавлено и 103 удалено
  1. 286 80
      pkg/cloud/aliyunprovider.go
  2. 322 23
      pkg/cloud/aliyunprovider_test.go

+ 286 - 80
pkg/cloud/aliyunprovider.go

@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"io"
 	"io/ioutil"
+	"regexp"
 	"strings"
 	"sync"
 	"time"
@@ -28,6 +29,7 @@ const (
 	ALIBABA_ECS_VERSION                        = "2014-05-26"
 	ALIBABA_ECS_DOMAIN                         = "ecs.aliyuncs.com"
 	ALIBABA_DESCRIBE_PRICE_API_ACTION          = "DescribePrice"
+	ALIBABA_DESCRIBE_DISK_API_ACTION           = "DescribeDisks"
 	ALIBABA_INSTANCE_RESOURCE_TYPE             = "instance"
 	ALIBABA_DISK_RESOURCE_TYPE                 = "disk"
 	ALIBABA_PAY_AS_YOU_GO_BILLING              = "Pay-As-You-Go"
@@ -41,7 +43,22 @@ const (
 	ALIBABA_UNKNOWN_INSTANCE_FAMILY_TYPE       = "unknown"
 	ALIBABA_NOT_SUPPORTED_INSTANCE_FAMILY_TYPE = "unsupported"
 	ALIBABA_ENHANCED_GENERAL_PURPOSE_TYPE      = "g6e"
-	ALIBABA_SYSTEMDISK_CLOUD_ESSD_CATEGORY     = "cloud_essd"
+	ALIBABA_DISK_CLOUD_ESSD_CATEGORY           = "cloud_essd"
+	ALIBABA_DISK_CLOUD_CATEGORY                = "cloud"
+	ALIBABA_DATA_DISK_CATEGORY                 = "data"
+	ALIBABA_SYSTEM_DISK_CATEGORY               = "system"
+	ALIBABA_DATA_DISK_PREFIX                   = "DataDisk"
+	ALIBABA_PV_CLOUD_DISK_TYPE                 = "CloudDisk"
+	ALIBABA_PV_NAS_TYPE                        = "NAS"
+	ALIBABA_PV_OSS_TYPE                        = "OSS"
+	ALIBABA_DEFAULT_DATADISK_SIZE              = "2000"
+	ALIBABA_DISK_TOPOLOGY_REGION_LABEL         = "topology.diskplugin.csi.alibabacloud.com/region"
+	ALIBABA_DISK_TOPOLOGY_ZONE_LABEL           = "topology.diskplugin.csi.alibabacloud.com/zone"
+)
+
+var (
+	// Regular expression to get the numerical value of PV suffix with GiB from *v1.PersistentVolume.
+	sizeRegEx = regexp.MustCompile("(.*?)Gi")
 )
 
 // Why predefined and dependency on code? Can be converted to API call - https://www.alibabacloud.com/help/en/elastic-compute-service/latest/regions-describeregions
@@ -90,15 +107,29 @@ type AlibabaAccessKey struct {
 	SecretAccessKey string `json:"alibaba_secret_access_key"`
 }
 
-// TO-DO: Slim Version of k8s disk assigned to a node, To be used if price adjustment need to happen with local disk information passed to describePrice.
+// Slim Version of k8s disk assigned to a node or PV, To be used if price adjustment need to happen with local disk information passed to describePrice.
 type SlimK8sDisk struct {
 	DiskType         string
 	RegionID         string
+	PriceUnit        string
+	SizeInGiB        string
 	DiskCategory     string
 	PerformanceLevel string
-	PriceUnit        string
-	SizeInGiB        int32
 	ProviderID       string
+	StorageClass     string
+}
+
+func NewSlimK8sDisk(diskType, regionID, priceUnit, diskCategory, performanceLevel, providerID, storageClass, sizeInGiB string) *SlimK8sDisk {
+	return &SlimK8sDisk{
+		DiskType:         diskType,
+		RegionID:         regionID,
+		PriceUnit:        priceUnit,
+		SizeInGiB:        sizeInGiB,
+		DiskCategory:     diskCategory,
+		PerformanceLevel: performanceLevel,
+		ProviderID:       providerID,
+		StorageClass:     storageClass,
+	}
 }
 
 // Slim version of a k8s v1.node just to pass along the object of this struct instead of constant getting the labels from within v1.Node & unit testing.
@@ -126,14 +157,18 @@ func NewSlimK8sNode(instanceType, regionID, priceUnit, memorySizeInKiB, osType,
 	}
 }
 
-// AlibabaNodeAttributes represents metadata about the product used to map to a node.
+// AlibabaNodeAttributes represents metadata about the product pricing information used to map to a node.
 // Basic Attributes needed atleast to get the key, Some attributes from k8s Node response
 // be populated directly into *Node object.
 type AlibabaNodeAttributes struct {
-	InstanceType    string `json:"instanceType"`
+	// InstanceType represents the type of instance.
+	InstanceType string `json:"instanceType"`
+	// MemorySizeInKiB represents the size of memory of instance.
 	MemorySizeInKiB string `json:"memorySizeInKiB"`
-	IsIoOptimized   bool   `json:"isIoOptimized"`
-	OSType          string `json:"osType"`
+	// IsIoOptimized represents the if instance is I/O optimized.
+	IsIoOptimized bool `json:"isIoOptimized"`
+	// OSType represents the OS installed in the Instance.
+	OSType string `json:"osType"`
 }
 
 func NewAlibabaNodeAttributes(node *SlimK8sNode) *AlibabaNodeAttributes {
@@ -145,14 +180,34 @@ func NewAlibabaNodeAttributes(node *SlimK8sNode) *AlibabaNodeAttributes {
 	}
 }
 
-// AlibabaPVAttributes represents metadata about the product used to map to a PV.
-// Basic Attributes needed atleast to get the keys, Some attributes from k8s Node response
+// AlibabaPVAttributes represents metadata about the product pricing information used to map to a PV.
+// Basic Attributes needed atleast to get the keys.Some attributes from k8s PV response
 // be populated directly into *PV object.
-// TO_DO: In next PR improve this
 type AlibabaPVAttributes struct {
-	DiskType         int32  `json:"diskType"`
-	DiskCategory     string `json:"diskCategory"`
-	PerformanceLevel string `json:"performanceLevel"`
+	// PVType can be Cloud Disk, NetWork Attached Storage(NAS) or Object Storage Service (OSS).
+	// Represents the way the PV was attached
+	PVType string `json:"pvType"`
+	// PVSubType represent the sub category of PVType. This is Data in case of Cloud Disk.
+	PVSubType string `json:"pvSubType"`
+	// Example for PVCategory with cloudDisk PVType are cloud, cloud_efficiency, cloud_ssd,
+	// ephemeral_ssd and cloud_essd. If not present returns empty.
+	PVCategory string `json:"pvCategory"`
+	// Example for PerformanceLevel with cloudDisk PVType are PL0,PL1,PL2 &PL3. If not present returns empty.
+	PVPerformanceLevel string `json:"performanceLevel"`
+	// The Size of the PV in terms of GiB
+	SizeInGiB string `json:"sizeInGiB"`
+}
+
+// TO-Do: next iteration of Alibaba provider support NetWork Attached Storage(NAS) and Object Storage Service (OSS type PVs).
+// Currently defaulting to cloudDisk with provision to add work in future.
+func NewAlibabaPVAttributes(disk *SlimK8sDisk) *AlibabaPVAttributes {
+	return &AlibabaPVAttributes{
+		PVType:             ALIBABA_PV_CLOUD_DISK_TYPE,
+		PVSubType:          disk.DiskType,
+		PVCategory:         disk.DiskCategory,
+		PVPerformanceLevel: disk.PerformanceLevel,
+		SizeInGiB:          disk.SizeInGiB,
+	}
 }
 
 // Stage 1 support will be Pay-As-You-Go with HourlyPrice equal to TradePrice with PriceUnit as Hour
@@ -265,6 +320,7 @@ func (alibaba *Alibaba) GetAlibabaAccessKey() (*credentials.AccessKeyCredential,
 	return alibaba.accessKey, nil
 }
 
+// DownloadPricingData satisfies the provider interface and downloads the price for node and PVs.
 func (alibaba *Alibaba) DownloadPricingData() error {
 	alibaba.DownloadPricingDataLock.Lock()
 	defer alibaba.DownloadPricingDataLock.Unlock()
@@ -292,13 +348,13 @@ func (alibaba *Alibaba) DownloadPricingData() error {
 	var client *sdk.Client
 	var signer *signers.AccessKeySigner
 	var ok bool
-	var pricingObj *AlibabaPricing
 	var lookupKey string
 	alibaba.clients = make(map[string]*sdk.Client)
 	alibaba.Pricing = make(map[string]*AlibabaPricing)
 
 	// TO-DO: Add disk price adjustment by parsing the local disk information and putting it as a param in describe Price function.
 	for _, node := range nodeList {
+		pricingObj := &AlibabaPricing{}
 		slimK8sNode := generateSlimK8sNodeFromV1Node(node)
 		lookupKey, err = determineKeyForPricing(slimK8sNode)
 		if _, ok := alibaba.Pricing[lookupKey]; ok {
@@ -322,59 +378,59 @@ func (alibaba *Alibaba) DownloadPricingData() error {
 		alibaba.Pricing[lookupKey] = pricingObj
 	}
 
-	// TO-DO: PV pricing
-	// //get pvList ultimately from Alibaba cloud provider and resemble data from the pvtype to
-	// // Hardcodedk8sNodeDiskStruct
-	// pvList := alibaba.Clientset.GetAllPersistentVolumes()
-
-	// pvList := []*Hardcodedk8sNodeDiskStruct{}
-	// pvList = append(pvList, &Hardcodedk8sNodeDiskStruct{
-	// 	DiskType:         "data",
-	// 	DiskCategory:     "cloud",
-	// 	PerformanceLevel: "",
-	// 	RegionID:         "cn-hangzhou",
-	// 	PriceUnit:        "Hour",
-	// 	SizeInGiB:        60,
-	// 	ProviderID:       "Ali-XXX-pv-01",
-	// }, &Hardcodedk8sNodeDiskStruct{
-	// 	DiskType:         "data",
-	// 	DiskCategory:     "cloud",
-	// 	PerformanceLevel: "P1",
-	// 	RegionID:         "cn-hangzhou",
-	// 	PriceUnit:        "Hour",
-	// 	SizeInGiB:        40,
-	// 	ProviderID:       "Ali-XXX-pv-01",
-	// })
-
-	// for _, pv := range pvList {
-	// 	if client, ok = alibaba.clients[pv.RegionID]; !ok {
-	// 		client, err = sdk.NewClientWithAccessKey(pv.RegionID, aak.AccessKeyId, aak.AccessKeySecret)
-	// 		if err != nil {
-	// 			return fmt.Errorf("access key provided does not have access to location %s", pv.RegionID)
-	// 		}
-	// 		alibaba.clients[pv.RegionID] = client
-	// 	}
-	// 	signer = signers.NewAccessKeySigner(aak)
-	// 	pricingObj, err = processDescribePriceAndCreateAlibabaPricing(client, pv, signer)
-	// 	lookupKey, err = determineKeyForPricing(pv)
-	// 	if err != nil {
-	// 		return err
-	// 	}
-	// 	alibaba.Pricing[lookupKey] = pricingObj
-	// }
-	// log.Infof("Length of pricing is %d", len(alibaba.Pricing))
-	// log.Infof("random value is %v", alibaba.Pricing[lookupKey])
+	// set the first occurance of region from the node
+	if alibaba.clusterRegion == "" {
+		for _, node := range nodeList {
+			if regionID, ok := node.Labels["topology.kubernetes.io/region"]; ok {
+				alibaba.clusterRegion = regionID
+				break
+			}
+		}
+	}
+
+	// PV pricing for only Cloud Disk for now.
+	// TO-DO: Support both NAS(Network Attached storage) and OSS(Object Storage Service) type PVs
+
+	pvList := alibaba.Clientset.GetAllPersistentVolumes()
+
+	for _, pv := range pvList {
+		pvRegion := determinePVRegion(pv)
+		if pvRegion == "" {
+			pvRegion = alibaba.clusterRegion
+		}
+		pricingObj := &AlibabaPricing{}
+		slimK8sDisk := generateSlimK8sDiskFromV1PV(pv, pvRegion)
+		lookupKey, err = determineKeyForPricing(slimK8sDisk)
+		if _, ok := alibaba.Pricing[lookupKey]; ok {
+			log.Debugf("Pricing information for pv with same features %s already exists hence skipping", lookupKey)
+			continue
+		}
+		if client, ok = alibaba.clients[slimK8sDisk.RegionID]; !ok {
+			client, err = sdk.NewClientWithAccessKey(slimK8sDisk.RegionID, aak.AccessKeyId, aak.AccessKeySecret)
+			if err != nil {
+				return fmt.Errorf("unable to initiate alibaba cloud sdk client for region %s : %w", slimK8sDisk.RegionID, err)
+			}
+			alibaba.clients[slimK8sDisk.RegionID] = client
+		}
+		signer = signers.NewAccessKeySigner(aak)
+		pricingObj, err = processDescribePriceAndCreateAlibabaPricing(client, slimK8sDisk, signer, c)
+		if err != nil {
+			return fmt.Errorf("failed to create pricing information for pv with category %s with error: %w", slimK8sDisk.DiskCategory, err)
+		}
+		alibaba.Pricing[lookupKey] = pricingObj
+	}
+
 	return nil
 }
 
-// AllNodePricing returns all the billing data for nodes and pvs
+// AllNodePricing returns all the pricing data for all nodes and pvs
 func (alibaba *Alibaba) AllNodePricing() (interface{}, error) {
 	alibaba.DownloadPricingDataLock.RLock()
 	defer alibaba.DownloadPricingDataLock.RUnlock()
 	return alibaba.Pricing, nil
 }
 
-// NodePricing gives a specific node for the key
+// NodePricing gives pricing information of a specific node given by the key
 func (alibaba *Alibaba) NodePricing(key Key) (*Node, error) {
 	alibaba.DownloadPricingDataLock.RLock()
 	defer alibaba.DownloadPricingDataLock.RUnlock()
@@ -389,10 +445,12 @@ func (alibaba *Alibaba) NodePricing(key Key) (*Node, error) {
 	}
 
 	log.Debugf("returning the node price for the node with feature: %s", keyFeature)
-	return pricing.Node, nil
+	returnNode := pricing.Node
+
+	return returnNode, nil
 }
 
-// PVPricing gives a specific PV price for the PVkey
+// PVPricing gives a pricing information of a specific PV given by PVkey
 func (alibaba *Alibaba) PVPricing(pvk PVKey) (*PV, error) {
 	alibaba.DownloadPricingDataLock.RLock()
 	defer alibaba.DownloadPricingDataLock.RUnlock()
@@ -626,16 +684,37 @@ func (alibaba *Alibaba) GetKey(mapValue map[string]string, node *v1.Node) Key {
 }
 
 type AlibabaPVKey struct {
-	ProviderID       string
-	RegionID         string
-	DiskType         string
-	DiskCategory     string
-	PerformaceLevel  string
-	StorageClassName string
+	ProviderID        string
+	RegionID          string
+	PVType            string
+	PVSubType         string
+	PVCategory        string
+	PVPerformaceLevel string
+	StorageClassName  string
+	SizeInGiB         string
+}
+
+func (alibaba *Alibaba) GetPVKey(pv *v1.PersistentVolume, parameters map[string]string, defaultRegion string) PVKey {
+	regionID := defaultRegion
+	// If default Region is not passed default it to cluster region ID.
+	if defaultRegion == "" {
+		regionID = alibaba.clusterRegion
+	}
+	slimK8sDisk := generateSlimK8sDiskFromV1PV(pv, defaultRegion)
+	return &AlibabaPVKey{
+		ProviderID:        slimK8sDisk.ProviderID,
+		RegionID:          regionID,
+		PVType:            ALIBABA_PV_CLOUD_DISK_TYPE,
+		PVSubType:         slimK8sDisk.DiskType,
+		PVCategory:        slimK8sDisk.DiskCategory,
+		PVPerformaceLevel: slimK8sDisk.PerformanceLevel,
+		StorageClassName:  pv.Spec.StorageClassName,
+		SizeInGiB:         slimK8sDisk.SizeInGiB,
+	}
 }
 
 func (alibabaPVKey *AlibabaPVKey) Features() string {
-	keyLookup := stringutil.DeleteEmptyStringsFromArray([]string{alibabaPVKey.RegionID, alibabaPVKey.DiskType, alibabaPVKey.DiskCategory, alibabaPVKey.PerformaceLevel})
+	keyLookup := stringutil.DeleteEmptyStringsFromArray([]string{alibabaPVKey.RegionID, alibabaPVKey.PVSubType, alibabaPVKey.PVCategory, alibabaPVKey.PVPerformaceLevel, alibabaPVKey.SizeInGiB})
 	return strings.Join(keyLookup, "::")
 }
 
@@ -674,17 +753,21 @@ func createDescribePriceACSRequest(i interface{}) (*requests.CommonRequest, erro
 		// For Enhanced General Purpose Type g6e SystemDisk.Category param doesn't default right,
 		// need it to be specifically assigned to "cloud_ssd" otherwise there's errors
 		if node.InstanceTypeFamily == ALIBABA_ENHANCED_GENERAL_PURPOSE_TYPE {
-			request.QueryParams["SystemDisk.Category"] = ALIBABA_SYSTEMDISK_CLOUD_ESSD_CATEGORY
+			request.QueryParams["SystemDisk.Category"] = ALIBABA_DISK_CLOUD_ESSD_CATEGORY
 		}
 		request.TransToAcsRequest()
 		return request, nil
 	case *SlimK8sDisk:
 		disk := i.(*SlimK8sDisk)
 		request.QueryParams["RegionId"] = disk.RegionID
-		request.QueryParams["ResourceType"] = ALIBABA_DISK_RESOURCE_TYPE
-		request.QueryParams["DataDisk.1.Category"] = disk.DiskCategory
-		request.QueryParams["DataDisk.1.Size"] = fmt.Sprintf("%d", disk.SizeInGiB)
 		request.QueryParams["PriceUnit"] = disk.PriceUnit
+		request.QueryParams["ResourceType"] = ALIBABA_DISK_RESOURCE_TYPE
+		request.QueryParams[fmt.Sprintf("%s.%d.Size", ALIBABA_DATA_DISK_PREFIX, 1)] = disk.SizeInGiB
+		request.QueryParams[fmt.Sprintf("%s.%d.Category", ALIBABA_DATA_DISK_PREFIX, 1)] = disk.DiskCategory
+		// Performance level defaults to PL1 if not present in volume attribute.
+		if disk.PerformanceLevel != "" {
+			request.QueryParams[fmt.Sprintf("%s.%d.PerformanceLevel", ALIBABA_DATA_DISK_PREFIX, 1)] = disk.PerformanceLevel
+		}
 		request.TransToAcsRequest()
 		return request, nil
 	default:
@@ -692,7 +775,8 @@ func createDescribePriceACSRequest(i interface{}) (*requests.CommonRequest, erro
 	}
 }
 
-// determineKeyForPricing generate a unique key from SlimK8sNode object that is construct from v1.Node object.
+// determineKeyForPricing generate a unique key from SlimK8sNode object that is constructed from v1.Node object and
+// SlimK8sDisk that is constructed from v1.PersistentVolume.
 func determineKeyForPricing(i interface{}) (string, error) {
 	if i == nil {
 		return "", fmt.Errorf("nil component passed to determine key")
@@ -709,7 +793,7 @@ func determineKeyForPricing(i interface{}) (string, error) {
 		}
 	case *SlimK8sDisk:
 		disk := i.(*SlimK8sDisk)
-		keyLookup := stringutil.DeleteEmptyStringsFromArray([]string{disk.RegionID, disk.DiskCategory, disk.DiskType, disk.PerformanceLevel})
+		keyLookup := stringutil.DeleteEmptyStringsFromArray([]string{disk.RegionID, disk.DiskType, disk.DiskCategory, disk.PerformanceLevel, disk.SizeInGiB})
 		return strings.Join(keyLookup, "::"), nil
 	default:
 		return "", fmt.Errorf("unsupported ECS type (%T) at this time", i)
@@ -733,10 +817,11 @@ type DescribePriceResponse struct {
 	PriceInfo PriceInfo `json:"PriceInfo"`
 }
 
-// processDescribePriceAndCreateAlibabaPricing processes the DescribePrice API and generates the pricing information for alibaba node resource.
+// processDescribePriceAndCreateAlibabaPricing processes the DescribePrice API and generates the pricing information for alibaba node resource and alibaba pv resource that's backed by cloud disk.
 func processDescribePriceAndCreateAlibabaPricing(client *sdk.Client, i interface{}, signer *signers.AccessKeySigner, custom *CustomPricing) (pricing *AlibabaPricing, err error) {
 	pricing = &AlibabaPricing{}
 	var response DescribePriceResponse
+
 	if i == nil {
 		return nil, fmt.Errorf("nil component passed to process the pricing information")
 	}
@@ -775,19 +860,20 @@ func processDescribePriceAndCreateAlibabaPricing(client *sdk.Client, i interface
 			return nil, err
 		}
 		resp, err := client.ProcessCommonRequestWithSigner(req, signer)
-		if err != nil {
-			return nil, fmt.Errorf("unable to fetch information for disk with DiskType: %v", disk.DiskType)
+		if err != nil || resp.GetHttpStatus() != 200 {
+			return nil, fmt.Errorf("unable to fetch information for disk with DiskType: %v with err: %w", disk.DiskCategory, err)
 		} else {
 			// This is where population of Pricing happens
 			err = json.Unmarshal(resp.GetHttpContentBytes(), &response)
 			if err != nil {
 				return nil, fmt.Errorf("unable to unmarshall json response to custom struct with err: %w", err)
 			}
-			pricing.PVAttributes = &AlibabaPVAttributes{}
+			pricing.PVAttributes = NewAlibabaPVAttributes(disk)
 			pricing.PV = &PV{
 				Cost: fmt.Sprintf("%f", response.PriceInfo.Price.TradePrice),
 			}
-
+			// TO-DO : Disk has support for Hour and Month but pricing API is failing for month for disk(Research why?) and same challenge as node pricing no prepaid/postpaid distinction in v1.PersistentVolume object have to look at APIs for th information.
+			pricing.PricingTerms = NewAlibabaPricingTerms(ALIBABA_PAY_AS_YOU_GO_BILLING, NewAlibabaPricingDetails(response.PriceInfo.Price.TradePrice, ALIBABA_HOUR_PRICE_UNIT, response.PriceInfo.Price.TradePrice, response.PriceInfo.Price.Currency))
 		}
 	default:
 		return nil, fmt.Errorf("unsupported ECS Pricing component of type (%T) at this time", i)
@@ -813,7 +899,7 @@ func getInstanceFamilyFromType(instanceType string) string {
 	return splitinstanceType[1]
 }
 
-// function geenerates SlimK8sNode from v1.Node for better passing slimmed struct between functions
+// generateSlimK8sNodeFromV1Node generates SlimK8sNode struct from v1.Node to fetch pricing information.
 func generateSlimK8sNodeFromV1Node(node *v1.Node) *SlimK8sNode {
 	var regionID, osType, instanceType, providerID, priceUnit, instanceFamily string
 	var memorySizeInKiB string // TO-DO: try to convert it into float
@@ -842,3 +928,123 @@ func generateSlimK8sNodeFromV1Node(node *v1.Node) *SlimK8sNode {
 
 	return NewSlimK8sNode(instanceType, regionID, priceUnit, memorySizeInKiB, osType, providerID, instanceFamily, IsIoOptimized)
 }
+
+// getNumericalValueFromResourceQuantity returns the numericalValue of the resourceQuantity
+// An example is: 20Gi returns to 20. If any error occurs it returns the default value used in describePrice API which is 2000.
+func getNumericalValueFromResourceQuantity(quantity string) (value string) {
+	// defaulting when any panic or empty string occurs.
+	defer func() {
+		log.Debugf("unable to determine the size of the PV so defaulting the size to %s", ALIBABA_DEFAULT_DATADISK_SIZE)
+		if err := recover(); err != nil {
+			value = ALIBABA_DEFAULT_DATADISK_SIZE
+		}
+		if value == "" {
+			value = ALIBABA_DEFAULT_DATADISK_SIZE
+		}
+	}()
+	res := sizeRegEx.FindAllStringSubmatch(quantity, 1)
+	value = res[0][1]
+	return
+}
+
+// generateSlimK8sDiskFromV1PV function generates SlimK8sDisk from v1.PersistentVolume and DescribeDisk API(If required) of alibaba
+// to generate slim disk type that can be used to fetch pricing information.
+func generateSlimK8sDiskFromV1PV(pv *v1.PersistentVolume, regionID string) *SlimK8sDisk {
+
+	// All PVs are data disks while local disk are categorized as system disk
+	diskType := ALIBABA_DATA_DISK_CATEGORY
+
+	//TO-DO: Disk supports month and hour prices , defaulting to hour
+	priceUnit := ALIBABA_HOUR_PRICE_UNIT
+
+	sizeQuantity := fmt.Sprintf("%s", pv.Spec.Capacity.Storage())
+
+	// res := sizeRegEx.FindAllStringSubmatch(sizeQuantity, 1)
+
+	sizeInGiB := getNumericalValueFromResourceQuantity(sizeQuantity)
+
+	providerID := ""
+	if pv.Spec.CSI != nil {
+		providerID = pv.Spec.CSI.VolumeHandle
+	} else {
+		providerID = pv.Name // Looks like pv name is same as providerID in Alibaba k8s cluster
+	}
+
+	// Performance level being empty string gets defaulted in describePrice to PL1.
+	performanceLevel := ""
+	diskCategory := ""
+	if pv.Spec.CSI != nil {
+		if val, ok := pv.Spec.CSI.VolumeAttributes["performanceLevel"]; ok {
+			performanceLevel = val
+		}
+		if val, ok := pv.Spec.CSI.VolumeAttributes["type"]; ok {
+			diskCategory = val
+		}
+	}
+
+	// Highly unlikely that label pv.Spec.CSI.VolumeAttributes["type"] doesn't exist but if occured default to cloud (most basic disk type)
+	if diskCategory == "" {
+		diskCategory = ALIBABA_DISK_CLOUD_CATEGORY
+	}
+
+	return NewSlimK8sDisk(diskType, regionID, priceUnit, diskCategory, performanceLevel, providerID, pv.Spec.StorageClassName, sizeInGiB)
+}
+
+// determinePVRegion determines associated region for a particular PV based on the following priority, which can be changed and any other path to determine region can be added!
+// if topology.diskplugin.csi.alibabacloud.com/region label/annotation is passed during PV creation return that as the PV region.
+// if topology.diskplugin.csi.alibabacloud.com/zone label/annotation is passed during PV creation determine the region based on this pv label.
+// if neither of the above label/annotation is present check node affinity for the zone affinity and determine the region based on this zone.
+// if nether of the above yields a region , return empty string to default it to cluster region.
+func determinePVRegion(pv *v1.PersistentVolume) string {
+	// if "topology.diskplugin.csi.alibabacloud.com/region" is present as a label or annotation return that as the PV region
+	if val, ok := pv.Labels[ALIBABA_DISK_TOPOLOGY_REGION_LABEL]; ok {
+		log.Debugf("determinePVRegion returned a region value of: %s through label: %s for PV name: %s", val, ALIBABA_DISK_TOPOLOGY_REGION_LABEL, pv.Name)
+		return val
+	}
+	if val, ok := pv.Annotations[ALIBABA_DISK_TOPOLOGY_REGION_LABEL]; ok {
+		log.Debugf("determinePVRegion returned a region value of: %s through annotation: %s for PV name: %s", val, ALIBABA_DISK_TOPOLOGY_REGION_LABEL, pv.Name)
+		return val
+	}
+
+	// if "topology.diskplugin.csi.alibabacloud.com/zone" is present as a label or annotation set it as the PV zone before looking at node affinity to determine the region PV belongs too
+	var pvZone string
+
+	if val, ok := pv.Labels[ALIBABA_DISK_TOPOLOGY_ZONE_LABEL]; ok {
+		log.Debugf("determinePVRegion will set zone value to: %s through label: %s for PV name: %s", val, ALIBABA_DISK_TOPOLOGY_ZONE_LABEL, pv.Name)
+		pvZone = val
+	}
+
+	if pvZone == "" {
+		if val, ok := pv.Annotations[ALIBABA_DISK_TOPOLOGY_ZONE_LABEL]; ok {
+			log.Debugf("determinePVRegion will set zone value to: %s through annotation: %s for PV name: %s", val, ALIBABA_DISK_TOPOLOGY_ZONE_LABEL, pv.Name)
+			pvZone = val
+		}
+	}
+
+	if pvZone == "" {
+		// zone and regionID labels are optional in Alibaba PV creation, while UI creation put's a zone associated with PV assign the region of
+		// pv based on this information if available. If pv is provision via yaml and the block is missing default it to clusterRegion.
+		if pv.Spec.NodeAffinity != nil {
+			nodeAffinity := pv.Spec.NodeAffinity
+			if nodeAffinity.Required != nil && nodeAffinity.Required.NodeSelectorTerms != nil {
+				for _, nodeSelectorTerm := range nodeAffinity.Required.NodeSelectorTerms {
+					matchExpression := nodeSelectorTerm.MatchExpressions
+					for _, nodeSelectorRequirement := range matchExpression {
+						if nodeSelectorRequirement.Key == ALIBABA_DISK_TOPOLOGY_ZONE_LABEL {
+							log.Debugf("determinePVRegion will set zone value to: %s through node affinity label: %s for PV name: %s", nodeSelectorRequirement.Values[0], ALIBABA_DISK_TOPOLOGY_ZONE_LABEL, pv.Name)
+							pvZone = nodeSelectorRequirement.Values[0]
+						}
+					}
+				}
+			}
+		}
+	}
+
+	for _, region := range alibabaRegions {
+		if strings.Contains(pvZone, region) {
+			log.Debugf("determinePVRegion determined region of %s through zone affiliation of the PV %s\n", region, pvZone)
+			return region
+		}
+	}
+	return ""
+}

+ 322 - 23
pkg/cloud/aliyunprovider_test.go

@@ -22,9 +22,42 @@ func TestCreateDescribePriceACSRequest(t *testing.T) {
 		ProviderID:         "Ali-XXX-node-01",
 		InstanceTypeFamily: "g6",
 	}
-	_, err := createDescribePriceACSRequest(node)
-	if err != nil {
-		t.Errorf("Error converting to Alibaba cloud request")
+
+	disk := &SlimK8sDisk{
+		DiskType:         "data",
+		RegionID:         "cn-hangzhou",
+		PriceUnit:        "Hour",
+		SizeInGiB:        "20",
+		DiskCategory:     "diskCategory",
+		PerformanceLevel: "cloud_essd",
+		ProviderID:       "d-Ali-XXX-01",
+		StorageClass:     "testStorageClass",
+	}
+
+	cases := []struct {
+		name          string
+		testStruct    interface{}
+		expectedError error
+	}{
+		{
+			name:          "test CreateDescribePriceACSRequest with SlimK8sNode struct Object",
+			testStruct:    node,
+			expectedError: nil,
+		},
+		{
+			name:          "test CreateDescribePriceACSRequest with SlimK8sDisk struct Object",
+			testStruct:    disk,
+			expectedError: nil,
+		},
+	}
+
+	for _, c := range cases {
+		t.Run(c.name, func(t *testing.T) {
+			_, err := createDescribePriceACSRequest(c.testStruct)
+			if err != nil && c.expectedError == nil {
+				t.Fatalf("Case name %s: Error converting to Alibaba cloud request", c.name)
+			}
+		})
 	}
 }
 
@@ -47,12 +80,12 @@ func TestProcessDescribePriceAndCreateAlibabaPricing(t *testing.T) {
 
 	cases := []struct {
 		name          string
-		testNode      *SlimK8sNode
+		teststruct    interface{}
 		expectedError error
 	}{
 		{
 			name: "test Enhanced General Purpose Type g6e instance family",
-			testNode: &SlimK8sNode{
+			teststruct: &SlimK8sNode{
 				InstanceType:       "ecs.g6e.xlarge",
 				RegionID:           "cn-hangzhou",
 				PriceUnit:          "Hour",
@@ -66,7 +99,7 @@ func TestProcessDescribePriceAndCreateAlibabaPricing(t *testing.T) {
 		},
 		{
 			name: "test General Purpose Type g6 instance family",
-			testNode: &SlimK8sNode{
+			teststruct: &SlimK8sNode{
 				InstanceType:       "ecs.g6.3xlarge",
 				RegionID:           "cn-hangzhou",
 				PriceUnit:          "Hour",
@@ -80,7 +113,7 @@ func TestProcessDescribePriceAndCreateAlibabaPricing(t *testing.T) {
 		},
 		{
 			name: "test General Purpose Type g5 instance family",
-			testNode: &SlimK8sNode{
+			teststruct: &SlimK8sNode{
 				InstanceType:       "ecs.g5.2xlarge",
 				RegionID:           "cn-hangzhou",
 				PriceUnit:          "Hour",
@@ -94,7 +127,7 @@ func TestProcessDescribePriceAndCreateAlibabaPricing(t *testing.T) {
 		},
 		{
 			name: "test General Purpose Type sn2 instance family",
-			testNode: &SlimK8sNode{
+			teststruct: &SlimK8sNode{
 				InstanceType:       "ecs.sn2.large",
 				RegionID:           "cn-hangzhou",
 				PriceUnit:          "Hour",
@@ -108,7 +141,7 @@ func TestProcessDescribePriceAndCreateAlibabaPricing(t *testing.T) {
 		},
 		{
 			name: "test General Purpose Type with Enhanced Network Performance sn2ne instance family",
-			testNode: &SlimK8sNode{
+			teststruct: &SlimK8sNode{
 				InstanceType:       "ecs.sn2ne.2xlarge",
 				RegionID:           "cn-hangzhou",
 				PriceUnit:          "Hour",
@@ -122,21 +155,76 @@ func TestProcessDescribePriceAndCreateAlibabaPricing(t *testing.T) {
 		},
 		{
 			name:          "test for a nil information",
-			testNode:      nil,
+			teststruct:    nil,
 			expectedError: fmt.Errorf("unsupported ECS pricing component at this time"),
 		},
+		{
+			name: "test Cloud Disk with Category cloud representing basic disk",
+			teststruct: &SlimK8sDisk{
+				DiskType:     "data",
+				RegionID:     "cn-hangzhou",
+				PriceUnit:    "Hour",
+				SizeInGiB:    "20",
+				DiskCategory: "cloud",
+				ProviderID:   "d-Ali-cloud-XXX-01",
+				StorageClass: "temp",
+			},
+			expectedError: nil,
+		},
+		{
+			name: "test Cloud Disk with Category cloud_efficiency representing ultra disk",
+			teststruct: &SlimK8sDisk{
+				DiskType:     "data",
+				RegionID:     "cn-hangzhou",
+				PriceUnit:    "Hour",
+				SizeInGiB:    "40",
+				DiskCategory: "cloud_efficiency",
+				ProviderID:   "d-Ali-cloud-XXX-02",
+				StorageClass: "temp",
+			},
+			expectedError: nil,
+		},
+		{
+			name: "test Cloud Disk with Category cloud_ssd representing standard SSD",
+			teststruct: &SlimK8sDisk{
+				DiskType:     "data",
+				RegionID:     "cn-hangzhou",
+				PriceUnit:    "Hour",
+				SizeInGiB:    "40",
+				DiskCategory: "cloud_efficiency",
+				ProviderID:   "d-Ali-cloud-XXX-02",
+				StorageClass: "temp",
+			},
+			expectedError: nil,
+		},
+		{
+			name: "test Cloud Disk with Category cloud_essd representing Enhanced SSD with PL2 performance level",
+			teststruct: &SlimK8sDisk{
+				DiskType:         "data",
+				RegionID:         "cn-hangzhou",
+				PriceUnit:        "Hour",
+				SizeInGiB:        "80",
+				DiskCategory:     "cloud_ssd",
+				PerformanceLevel: "PL2",
+				ProviderID:       "d-Ali-cloud-XXX-04",
+				StorageClass:     "temp",
+			},
+			expectedError: nil,
+		},
 	}
 	custom := &CustomPricing{}
 	for _, c := range cases {
 		t.Run(c.name, func(t *testing.T) {
-			pricingObj, err := processDescribePriceAndCreateAlibabaPricing(client, c.testNode, signer, custom)
+			pricingObj, err := processDescribePriceAndCreateAlibabaPricing(client, c.teststruct, signer, custom)
 			if err != nil && c.expectedError == nil {
 				t.Fatalf("Case name %s: got an error %s", c.name, err)
 			}
-			if pricingObj == nil {
-				t.Fatalf("Case name %s: got a nil pricing object", c.name)
+			if c.teststruct != nil {
+				if pricingObj == nil {
+					t.Fatalf("Case name %s: got a nil pricing object", c.name)
+				}
+				t.Logf("Case name %s: Pricing Information gathered for instanceType is %v", c.name, pricingObj.PricingTerms.PricingDetails.TradePrice)
 			}
-			t.Logf("Pricing Information gathered for instanceType %s is %v", c.name, pricingObj.PricingTerms.PricingDetails.TradePrice)
 		})
 	}
 }
@@ -185,7 +273,7 @@ func TestDetermineKeyForPricing(t *testing.T) {
 		expectedError error
 	}{
 		{
-			name: "test when all RegionID, InstanceType, OSType & ALIBABA_OPTIMIZE_KEYWORD words are used to key",
+			name: "test when all RegionID, InstanceType, OSType & ALIBABA_OPTIMIZE_KEYWORD words are used in Node key",
 			testVar: &SlimK8sNode{
 				InstanceType:       "ecs.sn2.large",
 				RegionID:           "cn-hangzhou",
@@ -200,7 +288,7 @@ func TestDetermineKeyForPricing(t *testing.T) {
 			expectedError: nil,
 		},
 		{
-			name: "test missing InstanceType to create key",
+			name: "test missing InstanceType to create Node key",
 			testVar: &SlimK8sNode{
 				RegionID:        "cn-hangzhou",
 				PriceUnit:       "Hour",
@@ -226,6 +314,35 @@ func TestDetermineKeyForPricing(t *testing.T) {
 			expectedKey:   "",
 			expectedError: fmt.Errorf("unsupported ECS type randomK8sStruct for DescribePrice at this time"),
 		},
+		{
+			name: "test when all RegionID, InstanceType, OSType & ALIBABA_OPTIMIZE_KEYWORD words are used to key",
+			testVar: &SlimK8sDisk{
+				DiskType:     "data",
+				RegionID:     "cn-hangzhou",
+				PriceUnit:    "Hour",
+				SizeInGiB:    "40",
+				DiskCategory: "cloud_efficiency",
+				ProviderID:   "d-Ali-cloud-XXX-02",
+				StorageClass: "temp",
+			},
+			expectedKey:   "cn-hangzhou::data::cloud_efficiency::40",
+			expectedError: nil,
+		},
+		{
+			name: "test missing InstanceType to create key",
+			testVar: &SlimK8sDisk{
+				DiskType:         "data",
+				RegionID:         "cn-hangzhou",
+				PriceUnit:        "Hour",
+				SizeInGiB:        "80",
+				DiskCategory:     "cloud_ssd",
+				PerformanceLevel: "PL2",
+				ProviderID:       "d-Ali-cloud-XXX-04",
+				StorageClass:     "temp",
+			},
+			expectedKey:   "cn-hangzhou::data::cloud_ssd::PL2::80",
+			expectedError: nil,
+		},
 	}
 	for _, c := range cases {
 		t.Run(c.name, func(t *testing.T) {
@@ -260,7 +377,7 @@ func TestGenerateSlimK8sNodeFromV1Node(t *testing.T) {
 			expectedSlimNode: &SlimK8sNode{
 				InstanceType:       "ecs.sn2ne.2xlarge",
 				RegionID:           "us-east-1",
-				PriceUnit:          "Hour",
+				PriceUnit:          ALIBABA_HOUR_PRICE_UNIT,
 				MemorySizeInKiB:    "16",
 				IsIoOptimized:      true,
 				OSType:             "linux",
@@ -272,23 +389,205 @@ func TestGenerateSlimK8sNodeFromV1Node(t *testing.T) {
 		t.Run(c.name, func(t *testing.T) {
 			returnSlimK8sNode := generateSlimK8sNodeFromV1Node(c.testNode)
 			if returnSlimK8sNode.InstanceType != c.expectedSlimNode.InstanceType {
-				t.Fatalf("unexpected conversion in function generateSlimK8sNodeFromV1Node expected InstanceType: %s , recieved Instance Type: %s", c.expectedSlimNode.InstanceType, returnSlimK8sNode.InstanceType)
+				t.Fatalf("unexpected conversion in function generateSlimK8sNodeFromV1Node expected InstanceType: %s , recieved InstanceType: %s", c.expectedSlimNode.InstanceType, returnSlimK8sNode.InstanceType)
 			}
 			if returnSlimK8sNode.RegionID != c.expectedSlimNode.RegionID {
-				t.Fatalf("unexpected conversion in function generateSlimK8sNodeFromV1Node expected RegionID: %s , recieved RegionID Type: %s", c.expectedSlimNode.RegionID, returnSlimK8sNode.RegionID)
+				t.Fatalf("unexpected conversion in function generateSlimK8sNodeFromV1Node expected RegionID: %s , recieved RegionID: %s", c.expectedSlimNode.RegionID, returnSlimK8sNode.RegionID)
 			}
 			if returnSlimK8sNode.PriceUnit != c.expectedSlimNode.PriceUnit {
-				t.Fatalf("unexpected conversion in function generateSlimK8sNodeFromV1Node expected PriceUnit: %s , recieved PriceUnit Type: %s", c.expectedSlimNode.PriceUnit, returnSlimK8sNode.PriceUnit)
+				t.Fatalf("unexpected conversion in function generateSlimK8sNodeFromV1Node expected PriceUnit: %s , recieved PriceUnit: %s", c.expectedSlimNode.PriceUnit, returnSlimK8sNode.PriceUnit)
 			}
 			if returnSlimK8sNode.MemorySizeInKiB != c.expectedSlimNode.MemorySizeInKiB {
-				t.Fatalf("unexpected conversion in function generateSlimK8sNodeFromV1Node expected MemorySizeInKiB: %s , recieved MemorySizeInKiB Type: %s", c.expectedSlimNode.MemorySizeInKiB, returnSlimK8sNode.MemorySizeInKiB)
+				t.Fatalf("unexpected conversion in function generateSlimK8sNodeFromV1Node expected MemorySizeInKiB: %s , recieved MemorySizeInKiB: %s", c.expectedSlimNode.MemorySizeInKiB, returnSlimK8sNode.MemorySizeInKiB)
 			}
 			if returnSlimK8sNode.OSType != c.expectedSlimNode.OSType {
-				t.Fatalf("unexpected conversion in function generateSlimK8sNodeFromV1Node expected OSType: %s , recieved OSType Type: %s", c.expectedSlimNode.OSType, returnSlimK8sNode.OSType)
+				t.Fatalf("unexpected conversion in function generateSlimK8sNodeFromV1Node expected OSType: %s , recieved OSType: %s", c.expectedSlimNode.OSType, returnSlimK8sNode.OSType)
 			}
 			if returnSlimK8sNode.InstanceTypeFamily != c.expectedSlimNode.InstanceTypeFamily {
-				t.Fatalf("unexpected conversion in function generateSlimK8sNodeFromV1Node expected InstanceTypeFamily: %s , recieved InstanceTypeFamily Type: %s", c.expectedSlimNode.InstanceTypeFamily, returnSlimK8sNode.InstanceTypeFamily)
+				t.Fatalf("unexpected conversion in function generateSlimK8sNodeFromV1Node expected InstanceTypeFamily: %s , recieved InstanceTypeFamily: %s", c.expectedSlimNode.InstanceTypeFamily, returnSlimK8sNode.InstanceTypeFamily)
+			}
+		})
+	}
+}
+
+func TestGenerateSlimK8sDiskFromV1PV(t *testing.T) {
+	testv1PV := &v1.PersistentVolume{}
+	testv1PV.Spec.Capacity = v1.ResourceList{
+		v1.ResourceStorage: *resource.NewQuantity(16*1024*1024*1024, resource.BinarySI),
+	}
+	testv1PV.Spec.CSI = &v1.CSIPersistentVolumeSource{}
+	testv1PV.Spec.CSI.VolumeHandle = "testPV"
+	testv1PV.Spec.CSI.VolumeAttributes = map[string]string{
+		"performanceLevel": "PL2",
+		"type":             "cloud_essd",
+	}
+	testv1PV.Spec.CSI.VolumeHandle = "testPV"
+	testv1PV.Spec.StorageClassName = "testStorageClass"
+	cases := []struct {
+		name             string
+		testPV           *v1.PersistentVolume
+		expectedSlimDisk *SlimK8sDisk
+		inpRegionID      string
+	}{
+		{
+			name:   "test a generic *v1.Node to *SlimK8sNode Conversion",
+			testPV: testv1PV,
+			expectedSlimDisk: &SlimK8sDisk{
+				DiskType:         ALIBABA_DATA_DISK_CATEGORY,
+				RegionID:         "us-east-1",
+				PriceUnit:        ALIBABA_HOUR_PRICE_UNIT,
+				SizeInGiB:        "16",
+				DiskCategory:     "cloud_essd",
+				PerformanceLevel: "PL2",
+				ProviderID:       "testPV",
+				StorageClass:     "testStorageClass",
+			},
+			inpRegionID: "us-east-1",
+		},
+	}
+	for _, c := range cases {
+		t.Run(c.name, func(t *testing.T) {
+			returnSlimK8sDisk := generateSlimK8sDiskFromV1PV(c.testPV, c.inpRegionID)
+			if returnSlimK8sDisk.DiskType != c.expectedSlimDisk.DiskType {
+				t.Fatalf("unexpected conversion in function generateSlimK8sDiskFromV1PV expected DiskType: %s , recieved DiskType: %s", c.expectedSlimDisk.DiskType, returnSlimK8sDisk.DiskType)
+			}
+			if returnSlimK8sDisk.RegionID != c.expectedSlimDisk.RegionID {
+				t.Fatalf("unexpected conversion in function generateSlimK8sDiskFromV1PV expected RegionID: %s , recieved RegionID Type: %s", c.expectedSlimDisk.RegionID, returnSlimK8sDisk.RegionID)
+			}
+			if returnSlimK8sDisk.PriceUnit != c.expectedSlimDisk.PriceUnit {
+				t.Fatalf("unexpected conversion in function generateSlimK8sDiskFromV1PV expected PriceUnit: %s , recieved PriceUnit Type: %s", c.expectedSlimDisk.PriceUnit, returnSlimK8sDisk.PriceUnit)
+			}
+			if returnSlimK8sDisk.SizeInGiB != c.expectedSlimDisk.SizeInGiB {
+				t.Fatalf("unexpected conversion in function generateSlimK8sDiskFromV1PV expected SizeInGiB: %s , recieved SizeInGiB Type: %s", c.expectedSlimDisk.SizeInGiB, returnSlimK8sDisk.SizeInGiB)
+			}
+			if returnSlimK8sDisk.DiskCategory != c.expectedSlimDisk.DiskCategory {
+				t.Fatalf("unexpected conversion in function generateSlimK8sDiskFromV1PV expected DiskCategory: %s , recieved DiskCategory Type: %s", c.expectedSlimDisk.DiskCategory, returnSlimK8sDisk.DiskCategory)
+			}
+			if returnSlimK8sDisk.PerformanceLevel != c.expectedSlimDisk.PerformanceLevel {
+				t.Fatalf("unexpected conversion in function generateSlimK8sDiskFromV1PV expected PerformanceLevel: %s , recieved PerformanceLevel Type: %s", c.expectedSlimDisk.PerformanceLevel, returnSlimK8sDisk.PerformanceLevel)
+			}
+			if returnSlimK8sDisk.ProviderID != c.expectedSlimDisk.ProviderID {
+				t.Fatalf("unexpected conversion in function generateSlimK8sDiskFromV1PV expected ProviderID: %s , recieved ProviderID Type: %s", c.expectedSlimDisk.ProviderID, returnSlimK8sDisk.ProviderID)
+			}
+			if returnSlimK8sDisk.StorageClass != c.expectedSlimDisk.StorageClass {
+				t.Fatalf("unexpected conversion in function generateSlimK8sDiskFromV1PV expected StorageClass: %s , recieved StorageClass Type: %s", c.expectedSlimDisk.StorageClass, returnSlimK8sDisk.StorageClass)
 			}
 		})
 	}
 }
+
+func TestGetNumericalValueFromResourceQuantity(t *testing.T) {
+	cases := []struct {
+		name                 string
+		inputResourceQuanity string
+		expectedValue        string
+	}{
+		{
+			name:                 "positive scenario: when inputResourceQuantity is 10Gi",
+			inputResourceQuanity: "10Gi",
+			expectedValue:        "10",
+		},
+		{
+			name:                 "negative scenario: when inputResourceQuantity is Gi",
+			inputResourceQuanity: "Gi",
+			expectedValue:        ALIBABA_DEFAULT_DATADISK_SIZE,
+		},
+		{
+			name:                 "negative scenario: when inputResourceQuantity is 10",
+			inputResourceQuanity: "10",
+			expectedValue:        ALIBABA_DEFAULT_DATADISK_SIZE,
+		},
+		{
+			name:                 "negative scenario: when inputResourceQuantity is empty string",
+			inputResourceQuanity: "",
+			expectedValue:        ALIBABA_DEFAULT_DATADISK_SIZE,
+		},
+	}
+	for _, c := range cases {
+		t.Run(c.name, func(t *testing.T) {
+			returnValue := getNumericalValueFromResourceQuantity(c.inputResourceQuanity)
+			if c.expectedValue != returnValue {
+				t.Fatalf("Case name %s: getNumericalValueFromResourceQuantity recieved %s but expected %s", c.name, returnValue, c.expectedValue)
+			}
+		})
+	}
+}
+
+func TestDeterminePVRegion(t *testing.T) {
+	genericNodeAffinityTestStruct := v1.NodeSelectorTerm{
+		MatchExpressions: []v1.NodeSelectorRequirement{
+			{
+				Key:      "topology.diskplugin.csi.alibabacloud.com/zone",
+				Operator: v1.NodeSelectorOpIn,
+				Values:   []string{"us-east-1a"},
+			},
+		},
+		MatchFields: []v1.NodeSelectorRequirement{},
+	}
+
+	// testPV1 contains the Label with region information as well as node affinity in spec
+	testPV1 := &v1.PersistentVolume{}
+	testPV1.Name = "testPV1"
+	testPV1.Labels = make(map[string]string)
+	testPV1.Labels[ALIBABA_DISK_TOPOLOGY_REGION_LABEL] = "us-east-1"
+	testPV1.Spec.NodeAffinity = &v1.VolumeNodeAffinity{
+		Required: &v1.NodeSelector{
+			NodeSelectorTerms: []v1.NodeSelectorTerm{genericNodeAffinityTestStruct},
+		},
+	}
+
+	// testPV2 contains the only zone label
+	testPV2 := &v1.PersistentVolume{}
+	testPV2.Name = "testPV2"
+	testPV2.Labels = make(map[string]string)
+	testPV2.Labels[ALIBABA_DISK_TOPOLOGY_ZONE_LABEL] = "us-east-1a"
+
+	// testPV3 contains only node affinity in spec
+	testPV3 := &v1.PersistentVolume{}
+	testPV3.Name = "testPV3"
+	testPV3.Spec.NodeAffinity = &v1.VolumeNodeAffinity{
+		Required: &v1.NodeSelector{
+			NodeSelectorTerms: []v1.NodeSelectorTerm{genericNodeAffinityTestStruct},
+		},
+	}
+
+	// testPV4 contains no label/annotation or any node affinity
+	testPV4 := &v1.PersistentVolume{}
+	testPV4.Name = "testPV4"
+
+	cases := []struct {
+		name           string
+		inputPV        *v1.PersistentVolume
+		expectedRegion string
+	}{
+		{
+			name:           "When Region label topology.diskplugin.csi.alibabacloud.com/region is present along with node affinity details",
+			inputPV:        testPV1,
+			expectedRegion: "us-east-1",
+		},
+		{
+			name:           "When zone label topology.diskplugin.csi.alibabacloud.com/zone is present function has to determine region",
+			inputPV:        testPV2,
+			expectedRegion: "us-east-1",
+		},
+		{
+			name:           "When only node affinity detail is present function has to determine the region",
+			inputPV:        testPV3,
+			expectedRegion: "us-east-1",
+		},
+		{
+			name:           "When no region/zone information is present function returns empty to default to cluster region",
+			inputPV:        testPV4,
+			expectedRegion: "",
+		},
+	}
+	for _, c := range cases {
+		t.Run(c.name, func(t *testing.T) {
+			returnRegion := determinePVRegion(c.inputPV)
+			if c.expectedRegion != returnRegion {
+				t.Fatalf("Case name %s: determinePVRegion recieved region :%s but expected region: %s", c.name, returnRegion, c.expectedRegion)
+			}
+		})
+	}
+
+}