| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417 |
- package alibaba
- import (
- "errors"
- "fmt"
- "io"
- "os"
- "regexp"
- "strconv"
- "strings"
- "sync"
- "time"
- "github.com/aliyun/alibaba-cloud-sdk-go/sdk"
- "github.com/aliyun/alibaba-cloud-sdk-go/sdk/auth/credentials"
- "github.com/aliyun/alibaba-cloud-sdk-go/sdk/auth/signers"
- "github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests"
- "github.com/opencost/opencost/core/pkg/env"
- "github.com/opencost/opencost/core/pkg/log"
- "github.com/opencost/opencost/core/pkg/opencost"
- "github.com/opencost/opencost/core/pkg/util/fileutil"
- "github.com/opencost/opencost/core/pkg/util/json"
- "github.com/opencost/opencost/core/pkg/util/stringutil"
- "github.com/opencost/opencost/pkg/cloud/models"
- "github.com/opencost/opencost/pkg/cloud/utils"
- "github.com/opencost/opencost/pkg/clustercache"
- ocenv "github.com/opencost/opencost/pkg/env"
- "golang.org/x/exp/slices"
- )
- const (
- ALIBABA_ECS_PRODUCT_CODE = "ecs"
- 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"
- ALIBABA_SUBSCRIPTION_BILLING = "Subscription"
- ALIBABA_PREEMPTIBLE_BILLING = "Preemptible"
- ALIBABA_OPTIMIZE_KEYWORD = "optimize"
- ALIBABA_NON_OPTIMIZE_KEYWORD = "nonoptimize"
- ALIBABA_HOUR_PRICE_UNIT = "Hour"
- ALIBABA_MONTH_PRICE_UNIT = "Month"
- ALIBABA_YEAR_PRICE_UNIT = "Year"
- ALIBABA_UNKNOWN_INSTANCE_FAMILY_TYPE = "unknown"
- ALIBABA_NOT_SUPPORTED_INSTANCE_FAMILY_TYPE = "unsupported"
- 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")
- )
- // Variable to keep track of instance families that fail in DescribePrice API due improper defaulting of systemDisk if the information is not available
- var alibabaDefaultToCloudEssd = []string{"g6e", "r6e"}
- var alibabaRegions = []string{
- "cn-qingdao",
- "cn-beijing",
- "cn-zhangjiakou",
- "cn-huhehaote",
- "cn-wulanchabu",
- "cn-hangzhou",
- "cn-shanghai",
- "cn-nanjing",
- "cn-shenzhen",
- "cn-heyuan",
- "cn-guangzhou",
- "cn-fuzhou",
- "cn-wuhan-lr",
- "cn-chengdu",
- "cn-hongkong",
- "ap-northeast-1",
- "ap-northeast-2",
- "ap-southeast-1",
- "ap-southeast-2",
- "ap-southeast-3",
- "ap-southeast-6",
- "ap-southeast-5",
- "ap-south-1",
- "ap-southeast-7",
- "us-east-1",
- "us-west-1",
- "eu-west-1",
- "me-east-1",
- "me-central-1",
- "eu-central-1",
- }
- // AlibabaInfo contains configuration for Alibaba's CUR integration
- // Deprecated: v1.104 Use BOAConfiguration instead
- type AlibabaInfo struct {
- AlibabaClusterRegion string `json:"ClusterRegion"`
- AlibabaServiceKeyName string `json:"serviceKeyName"`
- AlibabaServiceKeySecret string `json:"serviceKeySecret"`
- AlibabaAccountID string `json:"accountID"`
- }
- // IsEmpty returns true if all fields in config are empty, false if not.
- func (ai *AlibabaInfo) IsEmpty() bool {
- return ai.AlibabaClusterRegion == "" &&
- ai.AlibabaServiceKeyName == "" &&
- ai.AlibabaServiceKeySecret == "" &&
- ai.AlibabaAccountID == ""
- }
- // AlibabaAccessKey holds Alibaba credentials parsing from the service-key.json file.
- // Deprecated: v1.104 Use AccessKey instead
- type AlibabaAccessKey struct {
- AccessKeyID string `json:"alibaba_access_key_id"`
- SecretAccessKey string `json:"alibaba_secret_access_key"`
- }
- // Slim Version of k8s disk assigned to a node or PV.
- type SlimK8sDisk struct {
- DiskType string
- RegionID string
- PriceUnit string
- SizeInGiB string
- DiskCategory string
- PerformanceLevel string
- 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.
- type SlimK8sNode struct {
- InstanceType string
- RegionID string
- PriceUnit string
- MemorySizeInKiB string // TO-DO : Possible to convert to float?
- IsIoOptimized bool
- OSType string
- ProviderID string
- SystemDisk *SlimK8sDisk
- InstanceTypeFamily string // Bug in DescribePrice, doesn't default to enhanced type correctly and you get an error in DescribePrice to get around need the family of the InstanceType.
- }
- func NewSlimK8sNode(instanceType, regionID, priceUnit, memorySizeInKiB, osType, providerID, instanceTypeFamily string, isIOOptimized bool, systemDiskInfo *SlimK8sDisk) *SlimK8sNode {
- return &SlimK8sNode{
- InstanceType: instanceType,
- RegionID: regionID,
- PriceUnit: priceUnit,
- MemorySizeInKiB: memorySizeInKiB,
- IsIoOptimized: isIOOptimized,
- OSType: osType,
- SystemDisk: systemDiskInfo,
- ProviderID: providerID,
- InstanceTypeFamily: instanceTypeFamily,
- }
- }
- // AlibabaNodeAttributes represents metadata about the Node in its pricing information.
- // Basic Attributes needed atleast to get the key, Some attributes from k8s Node response
- // be populated directly into *Node object.
- type AlibabaNodeAttributes struct {
- // InstanceType represents the type of instance.
- InstanceType string `json:"instanceType"`
- // MemorySizeInKiB represents the size of memory of instance.
- MemorySizeInKiB string `json:"memorySizeInKiB"`
- // 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"`
- // SystemDiskCategory represents the exact category of the system disk attached to the node.
- SystemDiskCategory string `json:"systemDiskCategory"`
- // SystemDiskSizeInGiB represents the size of the system disk attached to the node.
- SystemDiskSizeInGiB string `json:"systemDiskSizeInGiB"`
- // SystemDiskPerformanceLevel represents the performance level of the system disk attached to the node.
- SystemDiskPerformanceLevel string `json:"systemPerformanceLevel"`
- }
- func NewAlibabaNodeAttributes(node *SlimK8sNode) *AlibabaNodeAttributes {
- if node == nil {
- return nil
- }
- var diskCategory, sizeInGiB, performanceLevel string
- if node.SystemDisk != nil {
- diskCategory = node.SystemDisk.DiskCategory
- sizeInGiB = node.SystemDisk.SizeInGiB
- performanceLevel = node.SystemDisk.PerformanceLevel
- }
- return &AlibabaNodeAttributes{
- InstanceType: node.InstanceType,
- MemorySizeInKiB: node.MemorySizeInKiB,
- IsIoOptimized: node.IsIoOptimized,
- OSType: node.OSType,
- SystemDiskCategory: diskCategory,
- SystemDiskSizeInGiB: sizeInGiB,
- SystemDiskPerformanceLevel: performanceLevel,
- }
- }
- // AlibabaPVAttributes represents metadata the PV in its pricing information.
- // Basic Attributes needed atleast to get the keys. Some attributes from k8s PV response
- // be populated directly into *PV object.
- type AlibabaPVAttributes struct {
- // 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 {
- if disk == nil {
- return nil
- }
- 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
- // TO-DO: Subscription and Premptible support, Information can be gathered from describing instance for subscription type
- // and spotprice can be gather from DescribeSpotPriceHistory API.
- // TO-DO: how would you calculate hourly price for subscription type, is it PRICE_YEARLY/HOURS_IN_THE_YEAR|MONTH?
- type AlibabaPricingDetails struct {
- // Represents hourly price for the given Alibaba cloud Product.
- HourlyPrice float32 `json:"hourlyPrice"`
- // Represents the unit in which Alibaba Product is billed can be Hour, Month or Year based on the billingMethod.
- PriceUnit string `json:"priceUnit"`
- // Original Price paid to acquire the Alibaba Product.
- TradePrice float32 `json:"tradePrice"`
- // Represents the currency unit of the price for billing Alibaba Product.
- CurrencyCode string `json:"currencyCode"`
- }
- func NewAlibabaPricingDetails(hourlyPrice float32, priceUnit string, tradePrice float32, currencyCode string) *AlibabaPricingDetails {
- return &AlibabaPricingDetails{
- HourlyPrice: hourlyPrice,
- PriceUnit: priceUnit,
- TradePrice: tradePrice,
- CurrencyCode: currencyCode,
- }
- }
- // AlibabaPricingTerms can have three types of supported billing method Pay-As-You-Go, Subscription and Premptible
- type AlibabaPricingTerms struct {
- BillingMethod string `json:"billingMethod"`
- PricingDetails *AlibabaPricingDetails `json:"pricingDetails"`
- }
- func NewAlibabaPricingTerms(billingMethod string, pricingDetails *AlibabaPricingDetails) *AlibabaPricingTerms {
- return &AlibabaPricingTerms{
- BillingMethod: billingMethod,
- PricingDetails: pricingDetails,
- }
- }
- // Alibaba Pricing struct carry the Attributes and pricing information for Node or PV
- type AlibabaPricing struct {
- NodeAttributes *AlibabaNodeAttributes
- PVAttributes *AlibabaPVAttributes
- PricingTerms *AlibabaPricingTerms
- Node *models.Node
- PV *models.PV
- }
- // Alibaba cloud's Provider struct
- type Alibaba struct {
- // Data to store Alibaba cloud's pricing struct, key in the map represents exact match to
- // node.features() or pv.features for easy lookup
- Pricing map[string]*AlibabaPricing
- // Lock Needed to provide thread safe
- DownloadPricingDataLock sync.RWMutex
- Clientset clustercache.ClusterCache
- Config models.ProviderConfig
- ServiceAccountChecks *models.ServiceAccountChecks
- ClusterAccountId string
- ClusterRegion string
- // The following fields are unexported because of avoiding any leak of secrets of these keys.
- // Alibaba Access key used specifically in signer interface used to sign API calls
- accessKey *credentials.AccessKeyCredential
- // Map of regionID to sdk.client to call API for that region
- clients map[string]*sdk.Client
- }
- // GetAlibabaAccessKey return the Access Key used to interact with the Alibaba cloud, if not set it
- // set it first by looking at env variables else load it from secret files.
- func (alibaba *Alibaba) GetAlibabaAccessKey() (*credentials.AccessKeyCredential, error) {
- if alibaba.accessKeyisLoaded() {
- return alibaba.accessKey, nil
- }
- config, err := alibaba.GetConfig()
- if err != nil {
- return nil, fmt.Errorf("error getting the default config for Alibaba Cloud provider: %w", err)
- }
- if config.AlibabaServiceKeyName == "" {
- config.AlibabaServiceKeyName = ocenv.GetAlibabaAccessKeyID()
- }
- if config.AlibabaServiceKeySecret == "" {
- config.AlibabaServiceKeySecret = ocenv.GetAlibabaAccessKeySecret()
- }
- if config.AlibabaServiceKeyName == "" && config.AlibabaServiceKeySecret == "" {
- log.Debugf("missing service key values for Alibaba cloud integration attempting to use service account integration")
- err := alibaba.loadAlibabaAuthSecretAndSetEnv(true)
- if err != nil {
- return nil, fmt.Errorf("unable to set the Alibaba Cloud key/secret from config file %w", err)
- }
- config.AlibabaServiceKeyName = ocenv.GetAlibabaAccessKeyID()
- config.AlibabaServiceKeySecret = ocenv.GetAlibabaAccessKeySecret()
- }
- if config.AlibabaServiceKeyName == "" && config.AlibabaServiceKeySecret == "" {
- return nil, fmt.Errorf("failed to get the access key for the current alibaba account")
- }
- // At this point either user is using the alibaba key and secret from secret passed in helm config if not he will use the secret that is passed in custom pricing
- // There's no check at this time for if the custom pricing key and secret is valid and that's on the user else there will be errors recorded.
- // Key and secret passed in config will supersede key and secret passed while installing Closed source helm chart.
- alibaba.accessKey = &credentials.AccessKeyCredential{AccessKeyId: config.AlibabaServiceKeyName, AccessKeySecret: config.AlibabaServiceKeySecret}
- return alibaba.accessKey, nil
- }
- func (alibaba *Alibaba) GetAlibabaCloudInfo() (*AlibabaInfo, error) {
- config, err := alibaba.GetConfig()
- if err != nil {
- return nil, fmt.Errorf("could not retrieve AlibabaCloudInfo %s", err)
- }
- aak, err := alibaba.GetAlibabaAccessKey()
- if err != nil {
- return nil, err
- }
- return &AlibabaInfo{
- AlibabaClusterRegion: config.AlibabaClusterRegion,
- AlibabaServiceKeyName: aak.AccessKeyId,
- AlibabaServiceKeySecret: aak.AccessKeySecret,
- AlibabaAccountID: config.ProjectID,
- }, nil
- }
- // DownloadPricingData satisfies the provider interface and downloads the prices for Node instances and PVs.
- func (alibaba *Alibaba) DownloadPricingData() error {
- alibaba.DownloadPricingDataLock.Lock()
- defer alibaba.DownloadPricingDataLock.Unlock()
- var aak *credentials.AccessKeyCredential
- var err error
- if !alibaba.accessKeyisLoaded() {
- aak, err = alibaba.GetAlibabaAccessKey()
- if err != nil {
- return fmt.Errorf("unable to get the access key information: %w", err)
- }
- } else {
- aak = alibaba.accessKey
- }
- c, err := alibaba.Config.GetCustomPricingData()
- if err != nil {
- return fmt.Errorf("error downloading default pricing data: %w", err)
- }
- // Get all the nodes from Alibaba cluster.
- nodeList := alibaba.Clientset.GetAllNodes()
- var client *sdk.Client
- var signer *signers.AccessKeySigner
- var ok bool
- var lookupKey string
- alibaba.clients = make(map[string]*sdk.Client)
- alibaba.Pricing = make(map[string]*AlibabaPricing)
- for _, node := range nodeList {
- pricingObj := &AlibabaPricing{}
- slimK8sNode := generateSlimK8sNodeFromV1Node(node)
- if client, ok = alibaba.clients[slimK8sNode.RegionID]; !ok {
- client, err = sdk.NewClientWithAccessKey(slimK8sNode.RegionID, aak.AccessKeyId, aak.AccessKeySecret)
- if err != nil {
- return fmt.Errorf("unable to initiate alibaba cloud sdk client for region %s : %w", slimK8sNode.RegionID, err)
- }
- alibaba.clients[slimK8sNode.RegionID] = client
- }
- signer = signers.NewAccessKeySigner(aak)
- // Adjust the system Disk information of a Node by retrieving the details of associated disk. If unable to retrieve set it to empty
- // system disk to pass through and use defaults with Alibaba pricing API.
- instanceID := getInstanceIDFromProviderID(slimK8sNode.ProviderID)
- slimK8sNode.SystemDisk = getSystemDiskInfoOfANode(instanceID, slimK8sNode.RegionID, client, signer)
- lookupKey, err = determineKeyForPricing(slimK8sNode)
- if err != nil {
- return fmt.Errorf("unable to determine key for pricing: %w", err)
- }
- if _, ok := alibaba.Pricing[lookupKey]; ok {
- log.Debugf("Pricing information for node with same features %s already exists hence skipping", lookupKey)
- continue
- }
- pricingObj, err = processDescribePriceAndCreateAlibabaPricing(client, slimK8sNode, signer, c)
- if err != nil {
- return fmt.Errorf("failed to create pricing information for node with type %s with error: %w", slimK8sNode.InstanceType, err)
- }
- alibaba.Pricing[lookupKey] = pricingObj
- }
- // set the first occurrence 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 err != nil {
- return fmt.Errorf("unable to determine key for pricing: %w", err)
- }
- 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 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 pricing information of a specific node given by the key
- func (alibaba *Alibaba) NodePricing(key models.Key) (*models.Node, models.PricingMetadata, error) {
- alibaba.DownloadPricingDataLock.RLock()
- defer alibaba.DownloadPricingDataLock.RUnlock()
- // Get node features for the key
- keyFeature := key.Features()
- meta := models.PricingMetadata{}
- pricing, ok := alibaba.Pricing[keyFeature]
- if !ok {
- keys := make([]string, 0, len(alibaba.Pricing))
- for k := range alibaba.Pricing {
- keys = append(keys, k)
- }
- kf := key.(*AlibabaNodeKey)
- // Try to look up pricing with no disk attached
- pricing, ok = alibaba.Pricing[kf.FeaturesWithOtherDisk("")]
- if !ok {
- log.Errorf("Node pricing information not found for node with feature: %s . Existing keys are: %+v", keyFeature, keys)
- return nil, meta, fmt.Errorf("Node pricing information not found for node with feature: %s letting it use default values", keyFeature)
- }
- }
- log.Debugf("returning the node price for the node with feature: %s", keyFeature)
- returnNode := pricing.Node
- return returnNode, meta, nil
- }
- // PVPricing gives a pricing information of a specific PV given by PVkey
- func (alibaba *Alibaba) PVPricing(pvk models.PVKey) (*models.PV, error) {
- alibaba.DownloadPricingDataLock.RLock()
- defer alibaba.DownloadPricingDataLock.RUnlock()
- keyFeature := pvk.Features()
- pricing, ok := alibaba.Pricing[keyFeature]
- if !ok {
- log.Debugf("Persistent Volume pricing not found for PV with feature: %s", keyFeature)
- return nil, fmt.Errorf("Persistent Volume pricing not found for PV with feature: %s letting it use default values", keyFeature)
- }
- log.Debugf("returning the PV price for the node with feature: %s", keyFeature)
- return pricing.PV, nil
- }
- // Inter zone and Inter region network cost are defaulted based on https://www.alibabacloud.com/help/en/cloud-data-transmission/latest/cross-region-data-transfers
- // Internet cost is default based on https://www.alibabacloud.com/help/en/elastic-compute-service/latest/public-bandwidth to $0.123
- func (alibaba *Alibaba) NetworkPricing() (*models.Network, error) {
- cpricing, err := alibaba.Config.GetCustomPricingData()
- if err != nil {
- return nil, err
- }
- znec, err := strconv.ParseFloat(cpricing.ZoneNetworkEgress, 64)
- if err != nil {
- return nil, err
- }
- rnec, err := strconv.ParseFloat(cpricing.RegionNetworkEgress, 64)
- if err != nil {
- return nil, err
- }
- inec, err := strconv.ParseFloat(cpricing.InternetNetworkEgress, 64)
- if err != nil {
- return nil, err
- }
- return &models.Network{
- ZoneNetworkEgressCost: znec,
- RegionNetworkEgressCost: rnec,
- InternetNetworkEgressCost: inec,
- }, nil
- }
- // Alibaba loadbalancer has three different types https://www.alibabacloud.com/product/server-load-balancer,
- // defaulted price to classic load balancer https://www.alibabacloud.com/help/en/server-load-balancer/latest/pay-as-you-go.
- func (alibaba *Alibaba) LoadBalancerPricing() (*models.LoadBalancer, error) {
- cpricing, err := alibaba.Config.GetCustomPricingData()
- if err != nil {
- return nil, err
- }
- lbPricing, err := strconv.ParseFloat(cpricing.DefaultLBPrice, 64)
- if err != nil {
- return nil, err
- }
- return &models.LoadBalancer{
- Cost: lbPricing,
- }, nil
- }
- func (alibaba *Alibaba) GetConfig() (*models.CustomPricing, error) {
- c, err := alibaba.Config.GetCustomPricingData()
- if err != nil {
- return nil, err
- }
- if c.Discount == "" {
- c.Discount = "0%"
- }
- if c.NegotiatedDiscount == "" {
- c.NegotiatedDiscount = "0%"
- }
- if c.ShareTenancyCosts == "" {
- c.ShareTenancyCosts = models.DefaultShareTenancyCost
- }
- return c, nil
- }
- // Load once and cache the result (even on failure). This is an install time secret, so
- // we don't expect the secret to change. If it does, however, we can force reload using
- // the input parameter.
- func (alibaba *Alibaba) loadAlibabaAuthSecretAndSetEnv(force bool) error {
- if !force && alibaba.accessKeyisLoaded() {
- return nil
- }
- exists, err := fileutil.FileExists(models.AuthSecretPath)
- if !exists || err != nil {
- return fmt.Errorf("failed to locate service account file: %s with err: %w", models.AuthSecretPath, err)
- }
- result, err := os.ReadFile(models.AuthSecretPath)
- if err != nil {
- return fmt.Errorf("failed to read service account file: %s with err: %w", models.AuthSecretPath, err)
- }
- var ak *AlibabaAccessKey
- err = json.Unmarshal(result, &ak)
- if err != nil {
- return fmt.Errorf("failed to unmarshall access key id and access key secret with err: %w", err)
- }
- err = env.Set(ocenv.AlibabaAccessKeyIDEnvVar, ak.AccessKeyID)
- if err != nil {
- return fmt.Errorf("failed to set environment variable: %s with err: %w", ocenv.AlibabaAccessKeyIDEnvVar, err)
- }
- err = env.Set(ocenv.AlibabaAccessKeySecretEnvVar, ak.SecretAccessKey)
- if err != nil {
- return fmt.Errorf("failed to set environment variable: %s with err: %w", ocenv.AlibabaAccessKeySecretEnvVar, err)
- }
- alibaba.accessKey = &credentials.AccessKeyCredential{
- AccessKeyId: ak.AccessKeyID,
- AccessKeySecret: ak.SecretAccessKey,
- }
- return nil
- }
- // Regions returns a current supported list of Alibaba regions
- func (alibaba *Alibaba) Regions() []string {
- regionOverrides := ocenv.GetRegionOverrideList()
- if len(regionOverrides) > 0 {
- log.Debugf("Overriding Alibaba regions with configured region list: %+v", regionOverrides)
- return regionOverrides
- }
- return alibabaRegions
- }
- // ClusterInfo returns information about Alibaba Cloud cluster, as provided by metadata.
- func (alibaba *Alibaba) ClusterInfo() (map[string]string, error) {
- c, err := alibaba.GetConfig()
- if err != nil {
- return nil, fmt.Errorf("failed to getConfig with err: %w", err)
- }
- var clusterName string
- if c.ClusterName != "" {
- clusterName = c.ClusterName
- }
- // Set it to environment clusterID if not set at this point
- if clusterName == "" {
- clusterName = ocenv.GetClusterID()
- }
- m := make(map[string]string)
- m["name"] = clusterName
- m["provider"] = opencost.AlibabaProvider
- m["project"] = alibaba.ClusterAccountId
- m["region"] = alibaba.ClusterRegion
- m["id"] = ocenv.GetClusterID()
- return m, nil
- }
- // Will look at this in Next PR if needed
- func (alibaba *Alibaba) GetAddresses() ([]byte, error) {
- return nil, nil
- }
- // Will look at this in Next PR if needed
- func (alibaba *Alibaba) GetDisks() ([]byte, error) {
- return nil, nil
- }
- func (alibaba *Alibaba) GetOrphanedResources() ([]models.OrphanedResource, error) {
- return nil, errors.New("not implemented")
- }
- func (alibaba *Alibaba) UpdateConfig(r io.Reader, updateType string) (*models.CustomPricing, error) {
- return alibaba.Config.Update(func(c *models.CustomPricing) error {
- if updateType != "" {
- return fmt.Errorf("UpdateConfig for Alibaba Provider doesn't support updateType %s at this time", updateType)
- } else {
- a := make(map[string]interface{})
- err := json.NewDecoder(r).Decode(&a)
- if err != nil {
- return err
- }
- for k, v := range a {
- kUpper := utils.ToTitle.String(k) // Just so we consistently supply / receive the same values, uppercase the first letter.
- vstr, ok := v.(string)
- if ok {
- err := models.SetCustomPricingField(c, kUpper, vstr)
- if err != nil {
- return fmt.Errorf("error setting custom pricing field: %w", err)
- }
- } else {
- return fmt.Errorf("type error while updating config for %s", kUpper)
- }
- }
- }
- if ocenv.IsRemoteEnabled() {
- err := utils.UpdateClusterMeta(ocenv.GetClusterID(), c.ClusterName)
- if err != nil {
- return err
- }
- }
- return nil
- })
- }
- func (alibaba *Alibaba) UpdateConfigFromConfigMap(cm map[string]string) (*models.CustomPricing, error) {
- return alibaba.Config.UpdateFromMap(cm)
- }
- // Will look at this in Next PR if needed
- func (alibaba *Alibaba) GetManagementPlatform() (string, error) {
- return "", nil
- }
- // Will look at this in Next PR if needed
- func (alibaba *Alibaba) GetLocalStorageQuery(window, offset time.Duration, rate bool, used bool) string {
- return ""
- }
- // Will look at this in Next PR if needed
- func (alibaba *Alibaba) ApplyReservedInstancePricing(nodes map[string]*models.Node) {
- }
- // Will look at this in Next PR if needed
- func (alibaba *Alibaba) ServiceAccountStatus() *models.ServiceAccountStatus {
- return &models.ServiceAccountStatus{}
- }
- // Will look at this in Next PR if needed
- func (alibaba *Alibaba) PricingSourceStatus() map[string]*models.PricingSource {
- return map[string]*models.PricingSource{}
- }
- // Will look at this in Next PR if needed
- func (alibaba *Alibaba) ClusterManagementPricing() (string, float64, error) {
- return "", 0.0, nil
- }
- // Will look at this in Next PR if needed
- func (alibaba *Alibaba) CombinedDiscountForNode(string, bool, float64, float64) float64 {
- return 0.0
- }
- func (alibaba *Alibaba) accessKeyisLoaded() bool {
- if alibaba.accessKey == nil {
- return false
- }
- if alibaba.accessKey.AccessKeyId == "" {
- return false
- }
- if alibaba.accessKey.AccessKeySecret == "" {
- return false
- }
- return true
- }
- type AlibabaNodeKey struct {
- ProviderID string
- RegionID string
- InstanceType string
- OSType string
- OptimizedKeyword string //If IsIoOptimized is true use the word optimize in the Node key and if its not optimized use the word nonoptimize
- SystemDiskCategory string
- SystemDiskSizeInGiB string
- SystemDiskPerformanceLevel string
- }
- func NewAlibabaNodeKey(node *SlimK8sNode, optimizedKeyword, systemDiskCategory, systemDiskSizeInGiB, systemDiskPerfromanceLevel string) *AlibabaNodeKey {
- var providerID, regionID, instanceType, osType string
- if node != nil {
- providerID = node.ProviderID
- regionID = node.RegionID
- instanceType = node.InstanceType
- osType = node.OSType
- }
- return &AlibabaNodeKey{
- ProviderID: providerID,
- RegionID: regionID,
- InstanceType: instanceType,
- OSType: osType,
- OptimizedKeyword: optimizedKeyword,
- SystemDiskCategory: systemDiskCategory,
- SystemDiskSizeInGiB: systemDiskSizeInGiB,
- SystemDiskPerformanceLevel: systemDiskPerfromanceLevel,
- }
- }
- func (alibabaNodeKey *AlibabaNodeKey) ID() string {
- return alibabaNodeKey.ProviderID
- }
- func (alibabaNodeKey *AlibabaNodeKey) Features() string {
- keyLookup := stringutil.DeleteEmptyStringsFromArray([]string{alibabaNodeKey.RegionID, alibabaNodeKey.InstanceType, alibabaNodeKey.OSType,
- alibabaNodeKey.OptimizedKeyword, alibabaNodeKey.SystemDiskCategory, alibabaNodeKey.SystemDiskSizeInGiB, alibabaNodeKey.SystemDiskPerformanceLevel})
- return strings.Join(keyLookup, "::")
- }
- func (alibabaNodeKey *AlibabaNodeKey) FeaturesWithOtherDisk(overrideDiskCategory string) string {
- keyLookup := stringutil.DeleteEmptyStringsFromArray([]string{alibabaNodeKey.RegionID, alibabaNodeKey.InstanceType, alibabaNodeKey.OSType,
- alibabaNodeKey.OptimizedKeyword, overrideDiskCategory, alibabaNodeKey.SystemDiskSizeInGiB, alibabaNodeKey.SystemDiskPerformanceLevel})
- return strings.Join(keyLookup, "::")
- }
- func (alibabaNodeKey *AlibabaNodeKey) GPUType() string {
- return ""
- }
- func (alibabaNodeKey *AlibabaNodeKey) GPUCount() int {
- return 0
- }
- // Get's the key for the k8s node input
- func (alibaba *Alibaba) GetKey(mapValue map[string]string, node *clustercache.Node) models.Key {
- slimK8sNode := generateSlimK8sNodeFromV1Node(node)
- var aak *credentials.AccessKeyCredential
- var err error
- var ok bool
- var client *sdk.Client
- var signer *signers.AccessKeySigner
- optimizedKeyword := ""
- if slimK8sNode.IsIoOptimized {
- optimizedKeyword = ALIBABA_OPTIMIZE_KEYWORD
- } else {
- optimizedKeyword = ALIBABA_NON_OPTIMIZE_KEYWORD
- }
- var diskCategory, diskSizeInGiB, diskPerformanceLevel string
- if !alibaba.accessKeyisLoaded() {
- aak, err = alibaba.GetAlibabaAccessKey()
- if err != nil {
- log.Warnf("unable to set the signer for node with providerID %s to retrieve the key skipping SystemDisk Retrieval with err: %v", slimK8sNode.ProviderID, err)
- return NewAlibabaNodeKey(slimK8sNode, optimizedKeyword, diskCategory, diskSizeInGiB, diskPerformanceLevel)
- }
- } else {
- aak = alibaba.accessKey
- }
- signer = signers.NewAccessKeySigner(aak)
- if aak == nil {
- log.Warnf("unable to retrieve the Alibaba API keys for node with providerID %s hence skipping SystemDisk Retrieval", slimK8sNode.ProviderID)
- return NewAlibabaNodeKey(slimK8sNode, optimizedKeyword, diskCategory, diskSizeInGiB, diskPerformanceLevel)
- }
- if client, ok = alibaba.clients[slimK8sNode.RegionID]; !ok {
- client, err = sdk.NewClientWithAccessKey(slimK8sNode.RegionID, aak.AccessKeyId, aak.AccessKeySecret)
- if err != nil {
- log.Warnf("unable to set the client for node with providerID %s to retrieve the key skipping SystemDisk Retrieval with err: %v", slimK8sNode.ProviderID, err)
- return NewAlibabaNodeKey(slimK8sNode, optimizedKeyword, diskCategory, diskSizeInGiB, diskPerformanceLevel)
- }
- alibaba.clients[slimK8sNode.RegionID] = client
- }
- instanceID := getInstanceIDFromProviderID(slimK8sNode.ProviderID)
- slimK8sNode.SystemDisk = getSystemDiskInfoOfANode(instanceID, slimK8sNode.RegionID, client, signer)
- if slimK8sNode.SystemDisk != nil {
- diskCategory = slimK8sNode.SystemDisk.DiskCategory
- diskSizeInGiB = slimK8sNode.SystemDisk.SizeInGiB
- diskPerformanceLevel = slimK8sNode.SystemDisk.PerformanceLevel
- }
- return NewAlibabaNodeKey(slimK8sNode, optimizedKeyword, diskCategory, diskSizeInGiB, diskPerformanceLevel)
- }
- type AlibabaPVKey struct {
- ProviderID string
- RegionID string
- PVType string
- PVSubType string
- PVCategory string
- PVPerformaceLevel string
- StorageClassName string
- SizeInGiB string
- }
- func (alibaba *Alibaba) GetPVKey(pv *clustercache.PersistentVolume, parameters map[string]string, defaultRegion string) models.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.PVSubType, alibabaPVKey.PVCategory, alibabaPVKey.PVPerformaceLevel, alibabaPVKey.SizeInGiB})
- return strings.Join(keyLookup, "::")
- }
- func (alibabaPVKey *AlibabaPVKey) ID() string {
- return alibabaPVKey.ProviderID
- }
- // Get storage class information for PV.
- func (alibabaPVKey *AlibabaPVKey) GetStorageClass() string {
- return alibabaPVKey.StorageClassName
- }
- // Helper functions for alibabaprovider.go
- // createDescribePriceACSRequest creates the HTTP GET request for the required resources' Price information,
- // When supporting subscription and Premptible resources this HTTP call needs to be modified with PriceUnit information
- // When supporting different new type of instances like Compute Optimized, Memory Optimized etc make sure you add the instance type
- // in unit test and check if it works or not to create the ack request and processDescribePriceAndCreateAlibabaPricing function
- // else more parameters need to be pulled from kubernetes node response or gather information from elsewhere and function modified.
- func createDescribePriceACSRequest(i interface{}) (*requests.CommonRequest, error) {
- request := requests.NewCommonRequest()
- request.Method = requests.GET
- request.Product = ALIBABA_ECS_PRODUCT_CODE
- request.Domain = ALIBABA_ECS_DOMAIN
- request.Version = ALIBABA_ECS_VERSION
- request.Scheme = requests.HTTPS
- request.ApiName = ALIBABA_DESCRIBE_PRICE_API_ACTION
- switch i.(type) {
- case *SlimK8sNode:
- node := i.(*SlimK8sNode)
- request.QueryParams["RegionId"] = node.RegionID
- request.QueryParams["ResourceType"] = ALIBABA_INSTANCE_RESOURCE_TYPE
- request.QueryParams["InstanceType"] = node.InstanceType
- request.QueryParams["PriceUnit"] = node.PriceUnit
- if node.SystemDisk != nil {
- // Only if the required information is present it should be overridden else default it via the API
- if node.SystemDisk.DiskCategory != "" {
- request.QueryParams["SystemDisk.Category"] = node.SystemDisk.DiskCategory
- }
- if node.SystemDisk.SizeInGiB != "" {
- request.QueryParams["SystemDisk.Size"] = node.SystemDisk.SizeInGiB
- }
- if node.SystemDisk.PerformanceLevel != "" {
- request.QueryParams["SystemDisk.PerformanceLevel"] = node.SystemDisk.PerformanceLevel
- }
- } else {
- // When the system disk information is not available, and the instance family is g6e or r6e,
- // or the instance generation is 6 or above, the default disk category in DescribePrice should be cloud_essd.
- if slices.Contains(alibabaDefaultToCloudEssd, node.InstanceTypeFamily) || getInstanceFamilyGenerationFromType(node.InstanceType) > 6 {
- 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["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:
- return nil, fmt.Errorf("unsupported ECS type (%T) for DescribePrice at this time", i)
- }
- }
- // createDescribeDisksCSRequest creates the HTTP GET Request to map the system disk to the InstanceID
- func createDescribeDisksACSRequest(instanceID, regionID, diskType string) (*requests.CommonRequest, error) {
- request := requests.NewCommonRequest()
- request.Method = requests.GET
- request.Product = ALIBABA_ECS_PRODUCT_CODE
- request.Domain = ALIBABA_ECS_DOMAIN
- request.Version = ALIBABA_ECS_VERSION
- request.Scheme = requests.HTTPS
- request.ApiName = ALIBABA_DESCRIBE_DISK_API_ACTION
- request.QueryParams["RegionId"] = regionID
- request.QueryParams["InstanceId"] = instanceID
- request.QueryParams["DiskType"] = diskType
- request.TransToAcsRequest()
- return request, nil
- }
- // 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")
- }
- switch i.(type) {
- case *SlimK8sNode:
- node := i.(*SlimK8sNode)
- var diskCategory, diskSizeInGiB, diskPerformanceLevel string
- if node.SystemDisk != nil {
- diskCategory = node.SystemDisk.DiskCategory
- diskSizeInGiB = node.SystemDisk.SizeInGiB
- diskPerformanceLevel = node.SystemDisk.PerformanceLevel
- }
- if node.IsIoOptimized {
- keyLookup := stringutil.DeleteEmptyStringsFromArray([]string{node.RegionID, node.InstanceType, node.OSType, ALIBABA_OPTIMIZE_KEYWORD, diskCategory, diskSizeInGiB, diskPerformanceLevel})
- return strings.Join(keyLookup, "::"), nil
- } else {
- keyLookup := stringutil.DeleteEmptyStringsFromArray([]string{node.RegionID, node.InstanceType, node.OSType, ALIBABA_NON_OPTIMIZE_KEYWORD, diskCategory, diskSizeInGiB, diskPerformanceLevel})
- return strings.Join(keyLookup, "::"), nil
- }
- case *SlimK8sDisk:
- disk := i.(*SlimK8sDisk)
- 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)
- }
- }
- // Below structs are used to unmarshal json response of Alibaba cloud's API DescribePrice
- type Price struct {
- OriginalPrice float32 `json:"OriginalPrice"`
- ReservedInstanceHourPrice float32 `json:"ReservedInstanceHourPrice"`
- DiscountPrice float32 `json:"DiscountPrice"`
- Currency string `json:"Currency"`
- TradePrice float32 `json:"TradePrice"`
- }
- type PriceInfo struct {
- Price Price `json:"Price"`
- }
- type DescribePriceResponse struct {
- RequestId string `json:"RequestId"`
- PriceInfo PriceInfo `json:"PriceInfo"`
- }
- // 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 *models.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")
- }
- switch i.(type) {
- case *SlimK8sNode:
- node := i.(*SlimK8sNode)
- req, err := createDescribePriceACSRequest(node)
- if err != nil {
- return nil, err
- }
- resp, err := client.ProcessCommonRequestWithSigner(req, signer)
- pricing.NodeAttributes = NewAlibabaNodeAttributes(node)
- if err != nil || resp.GetHttpStatus() != 200 {
- // Try again but default the disk to something else
- return nil, fmt.Errorf("unable to fetch information for node with InstanceType: %v", node.InstanceType)
- } 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)
- }
- // TO-DO : Ask in PR How to get the defaults is it equal to AWS/GCP defaults? And what needs to be returned
- pricing.Node = &models.Node{
- Cost: fmt.Sprintf("%f", response.PriceInfo.Price.TradePrice),
- BaseCPUPrice: custom.CPU,
- BaseRAMPrice: custom.RAM,
- BaseGPUPrice: custom.GPU,
- }
- // TO-DO : Currently with Pay-As-You-go Offering TradePrice = HourlyPrice , When support happens to other type HourlyPrice Need to be determined.
- 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))
- }
- case *SlimK8sDisk:
- disk := i.(*SlimK8sDisk)
- req, err := createDescribePriceACSRequest(disk)
- if err != nil {
- return nil, err
- }
- resp, err := client.ProcessCommonRequestWithSigner(req, signer)
- 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 = NewAlibabaPVAttributes(disk)
- pricing.PV = &models.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)
- }
- return pricing, nil
- }
- // This function is to get the InstanceFamily from the InstanceType , convention followed in
- // instance type is ecs.[FamilyName].[DifferentSize], it gets the familyName , if it is unable to get it
- // it lists the instance family name as Unknown.
- func getInstanceFamilyFromType(instanceType string) string {
- splitinstanceType := strings.Split(instanceType, ".")
- if len(splitinstanceType) != 3 {
- log.Warnf("unable to find the family of the instance type %s, returning its family type unknown", instanceType)
- return ALIBABA_UNKNOWN_INSTANCE_FAMILY_TYPE
- }
- return splitinstanceType[1]
- }
- // This function is used to obtain the generation of the instance family from the InstanceType,
- // because when the generation is higher than or equal to 7, the instance disk type will not support cloud_efficiency.
- // In such cases, when calling the DescribePrice interface, the system disk type will default to cloud_essd.
- func getInstanceFamilyGenerationFromType(instanceType string) int {
- // FamilyName format: g7ne or g7 or r7 or r6e,
- familyName := getInstanceFamilyFromType(instanceType)
- re := regexp.MustCompile(`(\d+)`)
- match := re.FindString(familyName)
- if match != "" {
- generation, err := strconv.Atoi(match)
- if err != nil {
- log.Errorf("unable to convert the generation of the instance type %s to integer", instanceType)
- } else {
- return generation
- }
- }
- log.Warnf("unable to find the generation of the instance type %s,", instanceType)
- return -1
- }
- // getInstanceIDFromProviderID returns the instance ID associated with the Node. A *v1.Node providerID in Alibaba cloud
- // is of <REGION-ID>.<INSTANCE-ID>. This function returns the Instance ID for the given ProviderID. if its unable to interpret
- // it defaults to empty string.
- func getInstanceIDFromProviderID(providerID string) string {
- if providerID == "" {
- return ""
- }
- splitStrings := strings.Split(providerID, ".")
- if len(splitStrings) < 2 {
- return ""
- }
- return splitStrings[1]
- }
- type Disk struct {
- Category string `json:"Category"`
- Size int `json:"Size"`
- PerformanceLevel string `json:"PerformanceLevel"`
- Type string `json:"Type"`
- RegionId string `json:"RegionId"`
- DiskId string `json:"DiskId"`
- DiskChargeType string `json:"DiskChargeType"`
- }
- type Disks struct {
- Disk []*Disk `json:"Disk"`
- }
- type DescribeDiskResponse struct {
- TotalCount int `json:"TotalCount"`
- Disks *Disks `json:"Disks"`
- }
- // getSystemDiskInfoOfANode gets the relevant System disk information associated with the Node given by the instanceID
- // in form of a SlimK8sDisk with only relevant information that can adjust the node pricing. If any error occurs return
- // an empty disk to not impact any default set at the price retrieval of the node.
- func getSystemDiskInfoOfANode(instanceID, regionID string, client *sdk.Client, signer *signers.AccessKeySigner) (systemDisk *SlimK8sDisk) {
- systemDisk = &SlimK8sDisk{}
- var response DescribeDiskResponse
- // if instanceID is empty string return an empty k8s
- if instanceID == "" {
- return
- }
- req, err := createDescribeDisksACSRequest(instanceID, regionID, ALIBABA_SYSTEM_DISK_CATEGORY)
- // if any error occurs return an empty disk to not impact default pricing.
- if err != nil {
- log.Warnf("Unable to create Describe Disk Request with err: %v for node with InstanceID: %s, hence defaulting it to an empty system disk to pass through to defaults", err, instanceID)
- return
- }
- resp, err := client.ProcessCommonRequestWithSigner(req, signer)
- if err != nil || resp.GetHttpStatus() != 200 {
- log.Warnf("Unable to process Describe Disk request with err: %v and errcode: %d for the node with InstanceID: %s, hence defaulting it to an empty system disk to pass through to defaults", err, resp.GetHttpStatus(), instanceID)
- return
- } else {
- // This is where population of Pricing happens
- err = json.Unmarshal(resp.GetHttpContentBytes(), &response)
- if err != nil {
- log.Warnf("Unable to unmarshall Describe Disk response with err: %v for the node with InstanceID: %s, hence defaulting it to an empty system disk to pass through to defaults", err, instanceID)
- return
- }
- // Every instance should only have one system disk per Alibaba Cloud documentation https://www.alibabacloud.com/help/en/elastic-compute-service/latest/block-storage-overview-disks,
- // if TotalCount is not 1 just return empty and let it not impact default pricing.
- if response.TotalCount != 1 {
- log.Warnf("Total count of system disk for node with InstanceID: %s is not 1, hence defaulting it to an empty system disk to pass through to defaults", instanceID)
- return
- }
- if response.Disks == nil {
- log.Warnf("Disks information missing for node with InstanceID: %s, hence defaulting it to an empty system disk to pass through to defaults", instanceID)
- return
- }
- if len(response.Disks.Disk) < 1 {
- log.Warnf("Total number of system disk for node with InstanceID: %s is less than 1, hence defaulting it to an empty system disk to pass through to defaults", instanceID)
- return
- }
- // TO-DO: When supporting Subscription type disk, you can leverge the disk.DiskChargeType here to map it to subscription type.
- systemDisk := response.Disks.Disk[0]
- return NewSlimK8sDisk(systemDisk.Type, systemDisk.RegionId, ALIBABA_HOUR_PRICE_UNIT, systemDisk.Category, systemDisk.PerformanceLevel, systemDisk.DiskId, "", fmt.Sprintf("%d", systemDisk.Size))
- }
- }
- // generateSlimK8sNodeFromV1Node generates SlimK8sNode struct from v1.Node to fetch pricing information and call alibaba API.
- func generateSlimK8sNodeFromV1Node(node *clustercache.Node) *SlimK8sNode {
- var regionID, osType, instanceType, providerID, priceUnit, instanceFamily string
- var memorySizeInKiB string // TO-DO: try to convert it into float
- var ok, IsIoOptimized bool
- if regionID, ok = node.Labels["topology.kubernetes.io/region"]; !ok {
- // HIGHLY UNLIKELY THAT THIS LABEL WONT BE THERE.
- log.Debugf("No RegionID label for the node: %s", node.Name)
- }
- if osType, ok = node.Labels["beta.kubernetes.io/os"]; !ok {
- // HIGHLY UNLIKELY THAT THIS LABEL WONT BE THERE.
- log.Debugf("OS type undetected for the node: %s", node.Name)
- }
- if instanceType, ok = node.Labels["node.kubernetes.io/instance-type"]; !ok {
- // HIGHLY UNLIKELY THAT THIS LABEL WONT BE THERE.
- log.Debugf("Instance Type undetected for the node: %s", node.Name)
- }
- instanceFamily = getInstanceFamilyFromType(instanceType)
- memorySizeInKiB = fmt.Sprintf("%s", node.Status.Capacity.Memory())
- providerID = node.SpecProviderID // Alibaba Cloud provider doesnt follow convention of prefix with cloud provider name
- // Looking at current Instance offering , all of the Instances seem to be I/O optimized - https://www.alibabacloud.com/help/en/elastic-compute-service/latest/instance-family
- // Basic price Json has it as part of the key so defaulting to true.
- IsIoOptimized = true
- priceUnit = ALIBABA_HOUR_PRICE_UNIT
- systemDisk := &SlimK8sDisk{}
- return NewSlimK8sNode(instanceType, regionID, priceUnit, memorySizeInKiB, osType, providerID, instanceFamily, IsIoOptimized, systemDisk)
- }
- // 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
- // to generate slim disk type that can be used to fetch pricing information for Data disk type.
- func generateSlimK8sDiskFromV1PV(pv *clustercache.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 occurred 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 *clustercache.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 PV through UI creation put's a zone PV is associated with and the region
- // can be determined from this information. If pv is provision via yaml and the block is missing that's the only time it gets defaulted 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]
- }
- }
- }
- }
- }
- }
- regionOverrides := ocenv.GetRegionOverrideList()
- regions := alibabaRegions
- if len(regionOverrides) > 0 {
- regions = regionOverrides
- }
- for _, region := range regions {
- 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 ""
- }
- // PricingSourceSummary returns the pricing source summary for the provider.
- // The summary represents what was _parsed_ from the pricing source, not
- // everything that was _available_ in the pricing source.
- func (a *Alibaba) PricingSourceSummary() interface{} {
- return a.Pricing
- }
|