aliyunprovider.go 51 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333
  1. package cloud
  2. import (
  3. "errors"
  4. "fmt"
  5. "io"
  6. "io/ioutil"
  7. "regexp"
  8. "strings"
  9. "sync"
  10. "time"
  11. "github.com/aliyun/alibaba-cloud-sdk-go/sdk"
  12. "github.com/aliyun/alibaba-cloud-sdk-go/sdk/auth/credentials"
  13. "github.com/aliyun/alibaba-cloud-sdk-go/sdk/auth/signers"
  14. "github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests"
  15. "github.com/opencost/opencost/pkg/clustercache"
  16. "github.com/opencost/opencost/pkg/env"
  17. "github.com/opencost/opencost/pkg/kubecost"
  18. "github.com/opencost/opencost/pkg/log"
  19. "github.com/opencost/opencost/pkg/util/fileutil"
  20. "github.com/opencost/opencost/pkg/util/json"
  21. "github.com/opencost/opencost/pkg/util/stringutil"
  22. "golang.org/x/exp/slices"
  23. v1 "k8s.io/api/core/v1"
  24. )
  25. const (
  26. ALIBABA_ECS_PRODUCT_CODE = "ecs"
  27. ALIBABA_ECS_VERSION = "2014-05-26"
  28. ALIBABA_ECS_DOMAIN = "ecs.aliyuncs.com"
  29. ALIBABA_DESCRIBE_PRICE_API_ACTION = "DescribePrice"
  30. ALIBABA_DESCRIBE_DISK_API_ACTION = "DescribeDisks"
  31. ALIBABA_INSTANCE_RESOURCE_TYPE = "instance"
  32. ALIBABA_DISK_RESOURCE_TYPE = "disk"
  33. ALIBABA_PAY_AS_YOU_GO_BILLING = "Pay-As-You-Go"
  34. ALIBABA_SUBSCRIPTION_BILLING = "Subscription"
  35. ALIBABA_PREEMPTIBLE_BILLING = "Preemptible"
  36. ALIBABA_OPTIMIZE_KEYWORD = "optimize"
  37. ALIBABA_NON_OPTIMIZE_KEYWORD = "nonoptimize"
  38. ALIBABA_HOUR_PRICE_UNIT = "Hour"
  39. ALIBABA_MONTH_PRICE_UNIT = "Month"
  40. ALIBABA_YEAR_PRICE_UNIT = "Year"
  41. ALIBABA_UNKNOWN_INSTANCE_FAMILY_TYPE = "unknown"
  42. ALIBABA_NOT_SUPPORTED_INSTANCE_FAMILY_TYPE = "unsupported"
  43. ALIBABA_DISK_CLOUD_ESSD_CATEGORY = "cloud_essd"
  44. ALIBABA_DISK_CLOUD_CATEGORY = "cloud"
  45. ALIBABA_DATA_DISK_CATEGORY = "data"
  46. ALIBABA_SYSTEM_DISK_CATEGORY = "system"
  47. ALIBABA_DATA_DISK_PREFIX = "DataDisk"
  48. ALIBABA_PV_CLOUD_DISK_TYPE = "CloudDisk"
  49. ALIBABA_PV_NAS_TYPE = "NAS"
  50. ALIBABA_PV_OSS_TYPE = "OSS"
  51. ALIBABA_DEFAULT_DATADISK_SIZE = "2000"
  52. ALIBABA_DISK_TOPOLOGY_REGION_LABEL = "topology.diskplugin.csi.alibabacloud.com/region"
  53. ALIBABA_DISK_TOPOLOGY_ZONE_LABEL = "topology.diskplugin.csi.alibabacloud.com/zone"
  54. )
  55. var (
  56. // Regular expression to get the numerical value of PV suffix with GiB from *v1.PersistentVolume.
  57. sizeRegEx = regexp.MustCompile("(.*?)Gi")
  58. )
  59. // Variable to keep track of instance families that fail in DescribePrice API due improper defaulting of systemDisk if the information is not available
  60. var alibabaDefaultToCloudEssd = []string{"g6e", "r6e", "r7", "g7", "g7a", "r7a"}
  61. // Why predefined and dependency on code? Can be converted to API call - https://www.alibabacloud.com/help/en/elastic-compute-service/latest/regions-describeregions
  62. var alibabaRegions = []string{
  63. "cn-qingdao",
  64. "cn-beijing",
  65. "cn-zhangjiakou",
  66. "cn-huhehaote",
  67. "cn-wulanchabu",
  68. "cn-hangzhou",
  69. "cn-shanghai",
  70. "cn-nanjing",
  71. "cn-fuzhou",
  72. "cn-shenzhen",
  73. "cn-guangzhou",
  74. "cn-chengdu",
  75. "cn-hongkong",
  76. "ap-southeast-1",
  77. "ap-southeast-2",
  78. "ap-southeast-3",
  79. "ap-southeast-5",
  80. "ap-southeast-6",
  81. "ap-southeast-7",
  82. "ap-south-1",
  83. "ap-northeast-1",
  84. "ap-northeast-2",
  85. "us-west-1",
  86. "us-east-1",
  87. "eu-central-1",
  88. "me-east-1",
  89. }
  90. // To-Do: Convert to API call - https://www.alibabacloud.com/help/en/elastic-compute-service/latest/describeinstancetypefamilies
  91. // Also first pass only completely tested pricing API for General pupose instances families & memory optimized instance families
  92. var alibabaInstanceFamilies = []string{
  93. "g7",
  94. "g7a",
  95. "g6e",
  96. "g6",
  97. "g5",
  98. "sn2",
  99. "sn2ne",
  100. "r7",
  101. "r7a",
  102. "r6e",
  103. "r6a",
  104. "r6",
  105. "r5",
  106. "se1",
  107. "se1ne",
  108. "re6",
  109. "re6p",
  110. "re4",
  111. "se1",
  112. }
  113. // AlibabaInfo contains configuration for Alibaba's CUR integration
  114. type AlibabaInfo struct {
  115. AlibabaClusterRegion string `json:"clusterRegion"`
  116. AlibabaServiceKeyName string `json:"serviceKeyName"`
  117. AlibabaServiceKeySecret string `json:"serviceKeySecret"`
  118. AlibabaAccountID string `json:"accountID"`
  119. }
  120. // IsEmpty returns true if all fields in config are empty, false if not.
  121. func (ai *AlibabaInfo) IsEmpty() bool {
  122. return ai.AlibabaClusterRegion == "" &&
  123. ai.AlibabaServiceKeyName == "" &&
  124. ai.AlibabaServiceKeySecret == "" &&
  125. ai.AlibabaAccountID == ""
  126. }
  127. // AlibabaAccessKey holds Alibaba credentials parsing from the service-key.json file.
  128. type AlibabaAccessKey struct {
  129. AccessKeyID string `json:"alibaba_access_key_id"`
  130. SecretAccessKey string `json:"alibaba_secret_access_key"`
  131. }
  132. // Slim Version of k8s disk assigned to a node or PV.
  133. type SlimK8sDisk struct {
  134. DiskType string
  135. RegionID string
  136. PriceUnit string
  137. SizeInGiB string
  138. DiskCategory string
  139. PerformanceLevel string
  140. ProviderID string
  141. StorageClass string
  142. }
  143. func NewSlimK8sDisk(diskType, regionID, priceUnit, diskCategory, performanceLevel, providerID, storageClass, sizeInGiB string) *SlimK8sDisk {
  144. return &SlimK8sDisk{
  145. DiskType: diskType,
  146. RegionID: regionID,
  147. PriceUnit: priceUnit,
  148. SizeInGiB: sizeInGiB,
  149. DiskCategory: diskCategory,
  150. PerformanceLevel: performanceLevel,
  151. ProviderID: providerID,
  152. StorageClass: storageClass,
  153. }
  154. }
  155. // 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.
  156. type SlimK8sNode struct {
  157. InstanceType string
  158. RegionID string
  159. PriceUnit string
  160. MemorySizeInKiB string // TO-DO : Possible to convert to float?
  161. IsIoOptimized bool
  162. OSType string
  163. ProviderID string
  164. SystemDisk *SlimK8sDisk
  165. 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.
  166. }
  167. func NewSlimK8sNode(instanceType, regionID, priceUnit, memorySizeInKiB, osType, providerID, instanceTypeFamily string, isIOOptimized bool, systemDiskInfo *SlimK8sDisk) *SlimK8sNode {
  168. return &SlimK8sNode{
  169. InstanceType: instanceType,
  170. RegionID: regionID,
  171. PriceUnit: priceUnit,
  172. MemorySizeInKiB: memorySizeInKiB,
  173. IsIoOptimized: isIOOptimized,
  174. OSType: osType,
  175. SystemDisk: systemDiskInfo,
  176. ProviderID: providerID,
  177. InstanceTypeFamily: instanceTypeFamily,
  178. }
  179. }
  180. // AlibabaNodeAttributes represents metadata about the Node in its pricing information.
  181. // Basic Attributes needed atleast to get the key, Some attributes from k8s Node response
  182. // be populated directly into *Node object.
  183. type AlibabaNodeAttributes struct {
  184. // InstanceType represents the type of instance.
  185. InstanceType string `json:"instanceType"`
  186. // MemorySizeInKiB represents the size of memory of instance.
  187. MemorySizeInKiB string `json:"memorySizeInKiB"`
  188. // IsIoOptimized represents the if instance is I/O optimized.
  189. IsIoOptimized bool `json:"isIoOptimized"`
  190. // OSType represents the OS installed in the Instance.
  191. OSType string `json:"osType"`
  192. // SystemDiskCategory represents the exact category of the system disk attached to the node.
  193. SystemDiskCategory string `json:"systemDiskCategory"`
  194. // SystemDiskSizeInGiB represents the size of the system disk attached to the node.
  195. SystemDiskSizeInGiB string `json:"systemDiskSizeInGiB"`
  196. // SystemDiskPerformanceLevel represents the performance level of the system disk attached to the node.
  197. SystemDiskPerformanceLevel string `json:"systemPerformanceLevel"`
  198. }
  199. func NewAlibabaNodeAttributes(node *SlimK8sNode) *AlibabaNodeAttributes {
  200. if node == nil {
  201. return nil
  202. }
  203. var diskCategory, sizeInGiB, performanceLevel string
  204. if node.SystemDisk != nil {
  205. diskCategory = node.SystemDisk.DiskCategory
  206. sizeInGiB = node.SystemDisk.SizeInGiB
  207. performanceLevel = node.SystemDisk.PerformanceLevel
  208. }
  209. return &AlibabaNodeAttributes{
  210. InstanceType: node.InstanceType,
  211. MemorySizeInKiB: node.MemorySizeInKiB,
  212. IsIoOptimized: node.IsIoOptimized,
  213. OSType: node.OSType,
  214. SystemDiskCategory: diskCategory,
  215. SystemDiskSizeInGiB: sizeInGiB,
  216. SystemDiskPerformanceLevel: performanceLevel,
  217. }
  218. }
  219. // AlibabaPVAttributes represents metadata the PV in its pricing information.
  220. // Basic Attributes needed atleast to get the keys. Some attributes from k8s PV response
  221. // be populated directly into *PV object.
  222. type AlibabaPVAttributes struct {
  223. // PVType can be Cloud Disk, NetWork Attached Storage(NAS) or Object Storage Service (OSS).
  224. // Represents the way the PV was attached
  225. PVType string `json:"pvType"`
  226. // PVSubType represent the sub category of PVType. This is Data in case of Cloud Disk.
  227. PVSubType string `json:"pvSubType"`
  228. // Example for PVCategory with cloudDisk PVType are cloud, cloud_efficiency, cloud_ssd,
  229. // ephemeral_ssd and cloud_essd. If not present returns empty.
  230. PVCategory string `json:"pvCategory"`
  231. // Example for PerformanceLevel with cloudDisk PVType are PL0,PL1,PL2 &PL3. If not present returns empty.
  232. PVPerformanceLevel string `json:"performanceLevel"`
  233. // The Size of the PV in terms of GiB
  234. SizeInGiB string `json:"sizeInGiB"`
  235. }
  236. // TO-Do: next iteration of Alibaba provider support NetWork Attached Storage(NAS) and Object Storage Service (OSS type PVs).
  237. // Currently defaulting to cloudDisk with provision to add work in future.
  238. func NewAlibabaPVAttributes(disk *SlimK8sDisk) *AlibabaPVAttributes {
  239. if disk == nil {
  240. return nil
  241. }
  242. return &AlibabaPVAttributes{
  243. PVType: ALIBABA_PV_CLOUD_DISK_TYPE,
  244. PVSubType: disk.DiskType,
  245. PVCategory: disk.DiskCategory,
  246. PVPerformanceLevel: disk.PerformanceLevel,
  247. SizeInGiB: disk.SizeInGiB,
  248. }
  249. }
  250. // Stage 1 support will be Pay-As-You-Go with HourlyPrice equal to TradePrice with PriceUnit as Hour
  251. // TO-DO: Subscription and Premptible support, Information can be gathered from describing instance for subscription type
  252. // and spotprice can be gather from DescribeSpotPriceHistory API.
  253. // TO-DO: how would you calculate hourly price for subscription type, is it PRICE_YEARLY/HOURS_IN_THE_YEAR|MONTH?
  254. type AlibabaPricingDetails struct {
  255. // Represents hourly price for the given Alibaba cloud Product.
  256. HourlyPrice float32 `json:"hourlyPrice"`
  257. // Represents the unit in which Alibaba Product is billed can be Hour, Month or Year based on the billingMethod.
  258. PriceUnit string `json:"priceUnit"`
  259. // Original Price paid to acquire the Alibaba Product.
  260. TradePrice float32 `json:"tradePrice"`
  261. // Represents the currency unit of the price for billing Alibaba Product.
  262. CurrencyCode string `json:"currencyCode"`
  263. }
  264. func NewAlibabaPricingDetails(hourlyPrice float32, priceUnit string, tradePrice float32, currencyCode string) *AlibabaPricingDetails {
  265. return &AlibabaPricingDetails{
  266. HourlyPrice: hourlyPrice,
  267. PriceUnit: priceUnit,
  268. TradePrice: tradePrice,
  269. CurrencyCode: currencyCode,
  270. }
  271. }
  272. // AlibabaPricingTerms can have three types of supported billing method Pay-As-You-Go, Subscription and Premptible
  273. type AlibabaPricingTerms struct {
  274. BillingMethod string `json:"billingMethod"`
  275. PricingDetails *AlibabaPricingDetails `json:"pricingDetails"`
  276. }
  277. func NewAlibabaPricingTerms(billingMethod string, pricingDetails *AlibabaPricingDetails) *AlibabaPricingTerms {
  278. return &AlibabaPricingTerms{
  279. BillingMethod: billingMethod,
  280. PricingDetails: pricingDetails,
  281. }
  282. }
  283. // Alibaba Pricing struct carry the Attributes and pricing information for Node or PV
  284. type AlibabaPricing struct {
  285. NodeAttributes *AlibabaNodeAttributes
  286. PVAttributes *AlibabaPVAttributes
  287. PricingTerms *AlibabaPricingTerms
  288. Node *Node
  289. PV *PV
  290. }
  291. // Alibaba cloud's Provider struct
  292. type Alibaba struct {
  293. // Data to store Alibaba cloud's pricing struct, key in the map represents exact match to
  294. // node.features() or pv.features for easy lookup
  295. Pricing map[string]*AlibabaPricing
  296. // Lock Needed to provide thread safe
  297. DownloadPricingDataLock sync.RWMutex
  298. Clientset clustercache.ClusterCache
  299. Config *ProviderConfig
  300. *CustomProvider
  301. // The following fields are unexported because of avoiding any leak of secrets of these keys.
  302. // Alibaba Access key used specifically in signer interface used to sign API calls
  303. serviceAccountChecks *ServiceAccountChecks
  304. clusterAccountId string
  305. clusterRegion string
  306. accessKey *credentials.AccessKeyCredential
  307. // Map of regionID to sdk.client to call API for that region
  308. clients map[string]*sdk.Client
  309. }
  310. // GetAlibabaAccessKey return the Access Key used to interact with the Alibaba cloud, if not set it
  311. // set it first by looking at env variables else load it from secret files.
  312. func (alibaba *Alibaba) GetAlibabaAccessKey() (*credentials.AccessKeyCredential, error) {
  313. if alibaba.accessKeyisLoaded() {
  314. return alibaba.accessKey, nil
  315. }
  316. config, err := alibaba.GetConfig()
  317. if err != nil {
  318. return nil, fmt.Errorf("error getting the default config for Alibaba Cloud provider: %w", err)
  319. }
  320. if config.AlibabaServiceKeyName == "" {
  321. config.AlibabaServiceKeyName = env.GetAlibabaAccessKeyID()
  322. }
  323. if config.AlibabaServiceKeySecret == "" {
  324. config.AlibabaServiceKeySecret = env.GetAlibabaAccessKeySecret()
  325. }
  326. if config.AlibabaServiceKeyName == "" && config.AlibabaServiceKeySecret == "" {
  327. log.Debugf("missing service key values for Alibaba cloud integration attempting to use service account integration")
  328. err := alibaba.loadAlibabaAuthSecretAndSetEnv(true)
  329. if err != nil {
  330. return nil, fmt.Errorf("unable to set the Alibaba Cloud key/secret from config file %w", err)
  331. }
  332. config.AlibabaServiceKeyName = env.GetAlibabaAccessKeyID()
  333. config.AlibabaServiceKeySecret = env.GetAlibabaAccessKeySecret()
  334. }
  335. if config.AlibabaServiceKeyName == "" && config.AlibabaServiceKeySecret == "" {
  336. return nil, fmt.Errorf("failed to get the access key for the current alibaba account")
  337. }
  338. alibaba.accessKey = &credentials.AccessKeyCredential{AccessKeyId: env.GetAlibabaAccessKeyID(), AccessKeySecret: env.GetAlibabaAccessKeySecret()}
  339. return alibaba.accessKey, nil
  340. }
  341. func (alibaba *Alibaba) GetAlibabaCloudInfo() (*AlibabaInfo, error) {
  342. config, err := alibaba.GetConfig()
  343. if err != nil {
  344. return nil, fmt.Errorf("could not retrieve AlibabaCloudInfo %s", err)
  345. }
  346. aak, err := alibaba.GetAlibabaAccessKey()
  347. if err != nil {
  348. return nil, err
  349. }
  350. return &AlibabaInfo{
  351. AlibabaClusterRegion: config.AlibabaClusterRegion,
  352. AlibabaServiceKeyName: aak.AccessKeyId,
  353. AlibabaServiceKeySecret: aak.AccessKeySecret,
  354. AlibabaAccountID: config.ProjectID,
  355. }, nil
  356. }
  357. // DownloadPricingData satisfies the provider interface and downloads the prices for Node instances and PVs.
  358. func (alibaba *Alibaba) DownloadPricingData() error {
  359. alibaba.DownloadPricingDataLock.Lock()
  360. defer alibaba.DownloadPricingDataLock.Unlock()
  361. var aak *credentials.AccessKeyCredential
  362. var err error
  363. if !alibaba.accessKeyisLoaded() {
  364. aak, err = alibaba.GetAlibabaAccessKey()
  365. if err != nil {
  366. return fmt.Errorf("unable to get the access key information: %w", err)
  367. }
  368. } else {
  369. aak = alibaba.accessKey
  370. }
  371. c, err := alibaba.Config.GetCustomPricingData()
  372. if err != nil {
  373. return fmt.Errorf("error downloading default pricing data: %w", err)
  374. }
  375. // Get all the nodes from Alibaba cluster.
  376. nodeList := alibaba.Clientset.GetAllNodes()
  377. var client *sdk.Client
  378. var signer *signers.AccessKeySigner
  379. var ok bool
  380. var lookupKey string
  381. alibaba.clients = make(map[string]*sdk.Client)
  382. alibaba.Pricing = make(map[string]*AlibabaPricing)
  383. for _, node := range nodeList {
  384. pricingObj := &AlibabaPricing{}
  385. slimK8sNode := generateSlimK8sNodeFromV1Node(node)
  386. if client, ok = alibaba.clients[slimK8sNode.RegionID]; !ok {
  387. client, err = sdk.NewClientWithAccessKey(slimK8sNode.RegionID, aak.AccessKeyId, aak.AccessKeySecret)
  388. if err != nil {
  389. return fmt.Errorf("unable to initiate alibaba cloud sdk client for region %s : %w", slimK8sNode.RegionID, err)
  390. }
  391. alibaba.clients[slimK8sNode.RegionID] = client
  392. }
  393. signer = signers.NewAccessKeySigner(aak)
  394. // Adjust the system Disk information of a Node by retrieving the details of associated disk. If unable to retrieve set it to empty
  395. // system disk to pass through and use defaults with Alibaba pricing API.
  396. instanceID := getInstanceIDFromProviderID(slimK8sNode.ProviderID)
  397. slimK8sNode.SystemDisk = getSystemDiskInfoOfANode(instanceID, slimK8sNode.RegionID, client, signer)
  398. lookupKey, err = determineKeyForPricing(slimK8sNode)
  399. if _, ok := alibaba.Pricing[lookupKey]; ok {
  400. log.Debugf("Pricing information for node with same features %s already exists hence skipping", lookupKey)
  401. continue
  402. }
  403. pricingObj, err = processDescribePriceAndCreateAlibabaPricing(client, slimK8sNode, signer, c)
  404. if err != nil {
  405. return fmt.Errorf("failed to create pricing information for node with type %s with error: %w", slimK8sNode.InstanceType, err)
  406. }
  407. alibaba.Pricing[lookupKey] = pricingObj
  408. }
  409. // set the first occurance of region from the node
  410. if alibaba.clusterRegion == "" {
  411. for _, node := range nodeList {
  412. if regionID, ok := node.Labels["topology.kubernetes.io/region"]; ok {
  413. alibaba.clusterRegion = regionID
  414. break
  415. }
  416. }
  417. }
  418. // PV pricing for only Cloud Disk for now.
  419. // TO-DO: Support both NAS(Network Attached storage) and OSS(Object Storage Service) type PVs
  420. pvList := alibaba.Clientset.GetAllPersistentVolumes()
  421. for _, pv := range pvList {
  422. pvRegion := determinePVRegion(pv)
  423. if pvRegion == "" {
  424. pvRegion = alibaba.clusterRegion
  425. }
  426. pricingObj := &AlibabaPricing{}
  427. slimK8sDisk := generateSlimK8sDiskFromV1PV(pv, pvRegion)
  428. lookupKey, err = determineKeyForPricing(slimK8sDisk)
  429. if _, ok := alibaba.Pricing[lookupKey]; ok {
  430. log.Debugf("Pricing information for pv with same features %s already exists hence skipping", lookupKey)
  431. continue
  432. }
  433. if client, ok = alibaba.clients[slimK8sDisk.RegionID]; !ok {
  434. client, err = sdk.NewClientWithAccessKey(slimK8sDisk.RegionID, aak.AccessKeyId, aak.AccessKeySecret)
  435. if err != nil {
  436. return fmt.Errorf("unable to initiate alibaba cloud sdk client for region %s : %w", slimK8sDisk.RegionID, err)
  437. }
  438. alibaba.clients[slimK8sDisk.RegionID] = client
  439. }
  440. signer = signers.NewAccessKeySigner(aak)
  441. pricingObj, err = processDescribePriceAndCreateAlibabaPricing(client, slimK8sDisk, signer, c)
  442. if err != nil {
  443. return fmt.Errorf("failed to create pricing information for pv with category %s with error: %w", slimK8sDisk.DiskCategory, err)
  444. }
  445. alibaba.Pricing[lookupKey] = pricingObj
  446. }
  447. return nil
  448. }
  449. // AllNodePricing returns all the pricing data for all nodes and pvs
  450. func (alibaba *Alibaba) AllNodePricing() (interface{}, error) {
  451. alibaba.DownloadPricingDataLock.RLock()
  452. defer alibaba.DownloadPricingDataLock.RUnlock()
  453. return alibaba.Pricing, nil
  454. }
  455. // NodePricing gives pricing information of a specific node given by the key
  456. func (alibaba *Alibaba) NodePricing(key Key) (*Node, error) {
  457. alibaba.DownloadPricingDataLock.RLock()
  458. defer alibaba.DownloadPricingDataLock.RUnlock()
  459. // Get node features for the key
  460. keyFeature := key.Features()
  461. pricing, ok := alibaba.Pricing[keyFeature]
  462. if !ok {
  463. log.Errorf("Node pricing information not found for node with feature: %s", keyFeature)
  464. return nil, fmt.Errorf("Node pricing information not found for node with feature: %s letting it use default values", keyFeature)
  465. }
  466. log.Debugf("returning the node price for the node with feature: %s", keyFeature)
  467. returnNode := pricing.Node
  468. return returnNode, nil
  469. }
  470. // PVPricing gives a pricing information of a specific PV given by PVkey
  471. func (alibaba *Alibaba) PVPricing(pvk PVKey) (*PV, error) {
  472. alibaba.DownloadPricingDataLock.RLock()
  473. defer alibaba.DownloadPricingDataLock.RUnlock()
  474. keyFeature := pvk.Features()
  475. pricing, ok := alibaba.Pricing[keyFeature]
  476. if !ok {
  477. log.Errorf("Persistent Volume pricing not found for PV with feature: %s", keyFeature)
  478. return nil, fmt.Errorf("Persistent Volume pricing not found for PV with feature: %s letting it use default values", keyFeature)
  479. }
  480. log.Debugf("returning the PV price for the node with feature: %s", keyFeature)
  481. return pricing.PV, nil
  482. }
  483. // Stubbed NetworkPricing for Alibaba Cloud. Will look at this in Next PR
  484. func (alibaba *Alibaba) NetworkPricing() (*Network, error) {
  485. return &Network{
  486. ZoneNetworkEgressCost: 0.0,
  487. RegionNetworkEgressCost: 0.0,
  488. InternetNetworkEgressCost: 0.0,
  489. }, nil
  490. }
  491. // Stubbed LoadBalancerPricing for Alibaba Cloud. Will look at this in Next PR
  492. func (alibaba *Alibaba) LoadBalancerPricing() (*LoadBalancer, error) {
  493. return &LoadBalancer{
  494. Cost: 0.0,
  495. }, nil
  496. }
  497. func (alibaba *Alibaba) GetConfig() (*CustomPricing, error) {
  498. c, err := alibaba.Config.GetCustomPricingData()
  499. if err != nil {
  500. return nil, err
  501. }
  502. if c.Discount == "" {
  503. c.Discount = "0%"
  504. }
  505. if c.NegotiatedDiscount == "" {
  506. c.NegotiatedDiscount = "0%"
  507. }
  508. if c.ShareTenancyCosts == "" {
  509. c.ShareTenancyCosts = defaultShareTenancyCost
  510. }
  511. return c, nil
  512. }
  513. // Load once and cache the result (even on failure). This is an install time secret, so
  514. // we don't expect the secret to change. If it does, however, we can force reload using
  515. // the input parameter.
  516. func (alibaba *Alibaba) loadAlibabaAuthSecretAndSetEnv(force bool) error {
  517. if !force && alibaba.accessKeyisLoaded() {
  518. return nil
  519. }
  520. exists, err := fileutil.FileExists(authSecretPath)
  521. if !exists || err != nil {
  522. return fmt.Errorf("failed to locate service account file: %s with err: %w", authSecretPath, err)
  523. }
  524. result, err := ioutil.ReadFile(authSecretPath)
  525. if err != nil {
  526. return fmt.Errorf("failed to read service account file: %s with err: %w", authSecretPath, err)
  527. }
  528. var ak *AlibabaAccessKey
  529. err = json.Unmarshal(result, &ak)
  530. if err != nil {
  531. return fmt.Errorf("failed to unmarshall access key id and access key secret with err: %w", err)
  532. }
  533. err = env.Set(env.AlibabaAccessKeyIDEnvVar, ak.AccessKeyID)
  534. if err != nil {
  535. return fmt.Errorf("failed to set environment variable: %s with err: %w", env.AlibabaAccessKeyIDEnvVar, err)
  536. }
  537. err = env.Set(env.AlibabaAccessKeySecretEnvVar, ak.SecretAccessKey)
  538. if err != nil {
  539. return fmt.Errorf("failed to set environment variable: %s with err: %w", env.AlibabaAccessKeySecretEnvVar, err)
  540. }
  541. alibaba.accessKey = &credentials.AccessKeyCredential{
  542. AccessKeyId: ak.AccessKeyID,
  543. AccessKeySecret: ak.SecretAccessKey,
  544. }
  545. return nil
  546. }
  547. // Regions returns a current supported list of Alibaba regions
  548. func (alibaba *Alibaba) Regions() []string {
  549. return alibabaRegions
  550. }
  551. // ClusterInfo returns information about Alibaba Cloud cluster, as provided by metadata.
  552. func (alibaba *Alibaba) ClusterInfo() (map[string]string, error) {
  553. c, err := alibaba.GetConfig()
  554. if err != nil {
  555. return nil, fmt.Errorf("failed to getConfig with err: %w", err)
  556. }
  557. var clusterName string
  558. if c.ClusterName != "" {
  559. clusterName = c.ClusterName
  560. }
  561. // Set it to environment clusterID if not set at this point
  562. if clusterName == "" {
  563. clusterName = env.GetClusterID()
  564. }
  565. m := make(map[string]string)
  566. m["name"] = clusterName
  567. m["provider"] = kubecost.AlibabaProvider
  568. m["project"] = alibaba.clusterAccountId
  569. m["region"] = alibaba.clusterRegion
  570. m["id"] = env.GetClusterID()
  571. return m, nil
  572. }
  573. // Will look at this in Next PR if needed
  574. func (alibaba *Alibaba) GetAddresses() ([]byte, error) {
  575. return nil, nil
  576. }
  577. // Will look at this in Next PR if needed
  578. func (alibaba *Alibaba) GetDisks() ([]byte, error) {
  579. return nil, nil
  580. }
  581. func (alibaba *Alibaba) GetOrphanedResources() ([]OrphanedResource, error) {
  582. return nil, errors.New("not implemented")
  583. }
  584. func (alibaba *Alibaba) UpdateConfig(r io.Reader, updateType string) (*CustomPricing, error) {
  585. return alibaba.Config.Update(func(c *CustomPricing) error {
  586. if updateType != "" {
  587. return fmt.Errorf("UpdateConfig for Alibaba Provider doesn't support updateType %s at this time", updateType)
  588. } else {
  589. a := make(map[string]interface{})
  590. err := json.NewDecoder(r).Decode(&a)
  591. if err != nil {
  592. return err
  593. }
  594. for k, v := range a {
  595. kUpper := strings.Title(k) // Just so we consistently supply / receive the same values, uppercase the first letter.
  596. vstr, ok := v.(string)
  597. if ok {
  598. err := SetCustomPricingField(c, kUpper, vstr)
  599. if err != nil {
  600. return err
  601. }
  602. } else {
  603. return fmt.Errorf("type error while updating config for %s", kUpper)
  604. }
  605. }
  606. }
  607. if env.IsRemoteEnabled() {
  608. err := UpdateClusterMeta(env.GetClusterID(), c.ClusterName)
  609. if err != nil {
  610. return err
  611. }
  612. }
  613. return nil
  614. })
  615. }
  616. func (alibaba *Alibaba) UpdateConfigFromConfigMap(cm map[string]string) (*CustomPricing, error) {
  617. return alibaba.Config.UpdateFromMap(cm)
  618. }
  619. // Will look at this in Next PR if needed
  620. func (alibaba *Alibaba) GetManagementPlatform() (string, error) {
  621. return "", nil
  622. }
  623. // Will look at this in Next PR if needed
  624. func (alibaba *Alibaba) GetLocalStorageQuery(window, offset time.Duration, rate bool, used bool) string {
  625. return ""
  626. }
  627. // Will look at this in Next PR if needed
  628. func (alibaba *Alibaba) ApplyReservedInstancePricing(nodes map[string]*Node) {
  629. }
  630. // Will look at this in Next PR if needed
  631. func (alibaba *Alibaba) ServiceAccountStatus() *ServiceAccountStatus {
  632. return &ServiceAccountStatus{}
  633. }
  634. // Will look at this in Next PR if needed
  635. func (alibaba *Alibaba) PricingSourceStatus() map[string]*PricingSource {
  636. return map[string]*PricingSource{}
  637. }
  638. // Will look at this in Next PR if needed
  639. func (alibaba *Alibaba) ClusterManagementPricing() (string, float64, error) {
  640. return "", 0.0, nil
  641. }
  642. // Will look at this in Next PR if needed
  643. func (alibaba *Alibaba) CombinedDiscountForNode(string, bool, float64, float64) float64 {
  644. return 0.0
  645. }
  646. func (alibaba *Alibaba) accessKeyisLoaded() bool {
  647. return alibaba.accessKey != nil
  648. }
  649. type AlibabaNodeKey struct {
  650. ProviderID string
  651. RegionID string
  652. InstanceType string
  653. OSType string
  654. OptimizedKeyword string //If IsIoOptimized is true use the word optimize in the Node key and if its not optimized use the word nonoptimize
  655. SystemDiskCategory string
  656. SystemDiskSizeInGiB string
  657. SystemDiskPerformanceLevel string
  658. }
  659. func NewAlibabaNodeKey(node *SlimK8sNode, optimizedKeyword, systemDiskCategory, systemDiskSizeInGiB, systemDiskPerfromanceLevel string) *AlibabaNodeKey {
  660. var providerID, regionID, instanceType, osType string
  661. if node != nil {
  662. providerID = node.ProviderID
  663. regionID = node.RegionID
  664. instanceType = node.InstanceType
  665. osType = node.OSType
  666. }
  667. return &AlibabaNodeKey{
  668. ProviderID: providerID,
  669. RegionID: regionID,
  670. InstanceType: instanceType,
  671. OSType: osType,
  672. OptimizedKeyword: optimizedKeyword,
  673. SystemDiskCategory: systemDiskCategory,
  674. SystemDiskSizeInGiB: systemDiskSizeInGiB,
  675. SystemDiskPerformanceLevel: systemDiskPerfromanceLevel,
  676. }
  677. }
  678. func (alibabaNodeKey *AlibabaNodeKey) ID() string {
  679. return alibabaNodeKey.ProviderID
  680. }
  681. func (alibabaNodeKey *AlibabaNodeKey) Features() string {
  682. keyLookup := stringutil.DeleteEmptyStringsFromArray([]string{alibabaNodeKey.RegionID, alibabaNodeKey.InstanceType, alibabaNodeKey.OSType,
  683. alibabaNodeKey.OptimizedKeyword, alibabaNodeKey.SystemDiskCategory, alibabaNodeKey.SystemDiskSizeInGiB, alibabaNodeKey.SystemDiskPerformanceLevel})
  684. return strings.Join(keyLookup, "::")
  685. }
  686. func (alibabaNodeKey *AlibabaNodeKey) GPUType() string {
  687. return ""
  688. }
  689. func (alibabaNodeKey *AlibabaNodeKey) GPUCount() int {
  690. return 0
  691. }
  692. // Get's the key for the k8s node input
  693. func (alibaba *Alibaba) GetKey(mapValue map[string]string, node *v1.Node) Key {
  694. slimK8sNode := generateSlimK8sNodeFromV1Node(node)
  695. var aak *credentials.AccessKeyCredential
  696. var err error
  697. var ok bool
  698. var client *sdk.Client
  699. var signer *signers.AccessKeySigner
  700. optimizedKeyword := ""
  701. if slimK8sNode.IsIoOptimized {
  702. optimizedKeyword = ALIBABA_OPTIMIZE_KEYWORD
  703. } else {
  704. optimizedKeyword = ALIBABA_NON_OPTIMIZE_KEYWORD
  705. }
  706. var diskCategory, diskSizeInGiB, diskPerformanceLevel string
  707. if !alibaba.accessKeyisLoaded() {
  708. aak, err = alibaba.GetAlibabaAccessKey()
  709. if err != nil {
  710. 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)
  711. return NewAlibabaNodeKey(slimK8sNode, optimizedKeyword, diskCategory, diskSizeInGiB, diskPerformanceLevel)
  712. }
  713. } else {
  714. aak = alibaba.accessKey
  715. }
  716. signer = signers.NewAccessKeySigner(aak)
  717. if aak == nil {
  718. log.Warnf("unable to retrieve the Alibaba API keys for node with providerID %s hence skipping SystemDisk Retrieval", slimK8sNode.ProviderID)
  719. return NewAlibabaNodeKey(slimK8sNode, optimizedKeyword, diskCategory, diskSizeInGiB, diskPerformanceLevel)
  720. }
  721. if client, ok = alibaba.clients[slimK8sNode.RegionID]; !ok {
  722. client, err = sdk.NewClientWithAccessKey(slimK8sNode.RegionID, aak.AccessKeyId, aak.AccessKeySecret)
  723. if err != nil {
  724. 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)
  725. return NewAlibabaNodeKey(slimK8sNode, optimizedKeyword, diskCategory, diskSizeInGiB, diskPerformanceLevel)
  726. }
  727. alibaba.clients[slimK8sNode.RegionID] = client
  728. }
  729. instanceID := getInstanceIDFromProviderID(slimK8sNode.ProviderID)
  730. slimK8sNode.SystemDisk = getSystemDiskInfoOfANode(instanceID, slimK8sNode.RegionID, client, signer)
  731. if slimK8sNode.SystemDisk != nil {
  732. diskCategory = slimK8sNode.SystemDisk.DiskCategory
  733. diskSizeInGiB = slimK8sNode.SystemDisk.SizeInGiB
  734. diskPerformanceLevel = slimK8sNode.SystemDisk.PerformanceLevel
  735. }
  736. return NewAlibabaNodeKey(slimK8sNode, optimizedKeyword, diskCategory, diskSizeInGiB, diskPerformanceLevel)
  737. }
  738. type AlibabaPVKey struct {
  739. ProviderID string
  740. RegionID string
  741. PVType string
  742. PVSubType string
  743. PVCategory string
  744. PVPerformaceLevel string
  745. StorageClassName string
  746. SizeInGiB string
  747. }
  748. func (alibaba *Alibaba) GetPVKey(pv *v1.PersistentVolume, parameters map[string]string, defaultRegion string) PVKey {
  749. regionID := defaultRegion
  750. // If default Region is not passed default it to cluster region ID.
  751. if defaultRegion == "" {
  752. regionID = alibaba.clusterRegion
  753. }
  754. slimK8sDisk := generateSlimK8sDiskFromV1PV(pv, defaultRegion)
  755. return &AlibabaPVKey{
  756. ProviderID: slimK8sDisk.ProviderID,
  757. RegionID: regionID,
  758. PVType: ALIBABA_PV_CLOUD_DISK_TYPE,
  759. PVSubType: slimK8sDisk.DiskType,
  760. PVCategory: slimK8sDisk.DiskCategory,
  761. PVPerformaceLevel: slimK8sDisk.PerformanceLevel,
  762. StorageClassName: pv.Spec.StorageClassName,
  763. SizeInGiB: slimK8sDisk.SizeInGiB,
  764. }
  765. }
  766. func (alibabaPVKey *AlibabaPVKey) Features() string {
  767. keyLookup := stringutil.DeleteEmptyStringsFromArray([]string{alibabaPVKey.RegionID, alibabaPVKey.PVSubType, alibabaPVKey.PVCategory, alibabaPVKey.PVPerformaceLevel, alibabaPVKey.SizeInGiB})
  768. return strings.Join(keyLookup, "::")
  769. }
  770. func (alibabaPVKey *AlibabaPVKey) ID() string {
  771. return alibabaPVKey.ProviderID
  772. }
  773. // Get storage class information for PV.
  774. func (alibabaPVKey *AlibabaPVKey) GetStorageClass() string {
  775. return alibabaPVKey.StorageClassName
  776. }
  777. // Helper functions for alibabaprovider.go
  778. // createDescribePriceACSRequest creates the HTTP GET request for the required resources' Price information,
  779. // When supporting subscription and Premptible resources this HTTP call needs to be modified with PriceUnit information
  780. // When supporting different new type of instances like Compute Optimized, Memory Optimized etc make sure you add the instance type
  781. // in unit test and check if it works or not to create the ack request and processDescribePriceAndCreateAlibabaPricing function
  782. // else more paramters need to be pulled from kubernetes node response or gather infromation from elsewhere and function modified.
  783. func createDescribePriceACSRequest(i interface{}) (*requests.CommonRequest, error) {
  784. request := requests.NewCommonRequest()
  785. request.Method = requests.GET
  786. request.Product = ALIBABA_ECS_PRODUCT_CODE
  787. request.Domain = ALIBABA_ECS_DOMAIN
  788. request.Version = ALIBABA_ECS_VERSION
  789. request.Scheme = requests.HTTPS
  790. request.ApiName = ALIBABA_DESCRIBE_PRICE_API_ACTION
  791. switch i.(type) {
  792. case *SlimK8sNode:
  793. node := i.(*SlimK8sNode)
  794. request.QueryParams["RegionId"] = node.RegionID
  795. request.QueryParams["ResourceType"] = ALIBABA_INSTANCE_RESOURCE_TYPE
  796. request.QueryParams["InstanceType"] = node.InstanceType
  797. request.QueryParams["PriceUnit"] = node.PriceUnit
  798. if node.SystemDisk != nil {
  799. // Only if the required information is present it should be overridden else default it via the API
  800. if node.SystemDisk.DiskCategory != "" {
  801. request.QueryParams["SystemDisk.Category"] = node.SystemDisk.DiskCategory
  802. }
  803. if node.SystemDisk.SizeInGiB != "" {
  804. request.QueryParams["SystemDisk.Size"] = node.SystemDisk.SizeInGiB
  805. }
  806. if node.SystemDisk.PerformanceLevel != "" {
  807. request.QueryParams["SystemDisk.PerformanceLevel"] = node.SystemDisk.PerformanceLevel
  808. }
  809. } else {
  810. // When System Disk information is not available for instance family g6e, r7 and r6e the defaults in
  811. // DescribePrice dont default rightly to cloud_essd for these instances.
  812. if slices.Contains(alibabaDefaultToCloudEssd, node.InstanceTypeFamily) {
  813. request.QueryParams["SystemDisk.Category"] = ALIBABA_DISK_CLOUD_ESSD_CATEGORY
  814. }
  815. }
  816. request.TransToAcsRequest()
  817. return request, nil
  818. case *SlimK8sDisk:
  819. disk := i.(*SlimK8sDisk)
  820. request.QueryParams["RegionId"] = disk.RegionID
  821. request.QueryParams["PriceUnit"] = disk.PriceUnit
  822. request.QueryParams["ResourceType"] = ALIBABA_DISK_RESOURCE_TYPE
  823. request.QueryParams[fmt.Sprintf("%s.%d.Size", ALIBABA_DATA_DISK_PREFIX, 1)] = disk.SizeInGiB
  824. request.QueryParams[fmt.Sprintf("%s.%d.Category", ALIBABA_DATA_DISK_PREFIX, 1)] = disk.DiskCategory
  825. // Performance level defaults to PL1 if not present in volume attribute.
  826. if disk.PerformanceLevel != "" {
  827. request.QueryParams[fmt.Sprintf("%s.%d.PerformanceLevel", ALIBABA_DATA_DISK_PREFIX, 1)] = disk.PerformanceLevel
  828. }
  829. request.TransToAcsRequest()
  830. return request, nil
  831. default:
  832. return nil, fmt.Errorf("unsupported ECS type (%T) for DescribePrice at this time", i)
  833. }
  834. }
  835. // createDescribeDisksCSRequest creates the HTTP GET Request to map the system disk to the InstanceID
  836. func createDescribeDisksACSRequest(instanceID, regionID, diskType string) (*requests.CommonRequest, error) {
  837. request := requests.NewCommonRequest()
  838. request.Method = requests.GET
  839. request.Product = ALIBABA_ECS_PRODUCT_CODE
  840. request.Domain = ALIBABA_ECS_DOMAIN
  841. request.Version = ALIBABA_ECS_VERSION
  842. request.Scheme = requests.HTTPS
  843. request.ApiName = ALIBABA_DESCRIBE_DISK_API_ACTION
  844. request.QueryParams["RegionId"] = regionID
  845. request.QueryParams["InstanceId"] = instanceID
  846. request.QueryParams["DiskType"] = diskType
  847. request.TransToAcsRequest()
  848. return request, nil
  849. }
  850. // determineKeyForPricing generate a unique key from SlimK8sNode object that is constructed from v1.Node object and
  851. // SlimK8sDisk that is constructed from v1.PersistentVolume.
  852. func determineKeyForPricing(i interface{}) (string, error) {
  853. if i == nil {
  854. return "", fmt.Errorf("nil component passed to determine key")
  855. }
  856. switch i.(type) {
  857. case *SlimK8sNode:
  858. node := i.(*SlimK8sNode)
  859. var diskCategory, diskSizeInGiB, diskPerformanceLevel string
  860. if node.SystemDisk != nil {
  861. diskCategory = node.SystemDisk.DiskCategory
  862. diskSizeInGiB = node.SystemDisk.SizeInGiB
  863. diskPerformanceLevel = node.SystemDisk.PerformanceLevel
  864. }
  865. if node.IsIoOptimized {
  866. keyLookup := stringutil.DeleteEmptyStringsFromArray([]string{node.RegionID, node.InstanceType, node.OSType, ALIBABA_OPTIMIZE_KEYWORD, diskCategory, diskSizeInGiB, diskPerformanceLevel})
  867. return strings.Join(keyLookup, "::"), nil
  868. } else {
  869. keyLookup := stringutil.DeleteEmptyStringsFromArray([]string{node.RegionID, node.InstanceType, node.OSType, ALIBABA_NON_OPTIMIZE_KEYWORD, diskCategory, diskSizeInGiB, diskPerformanceLevel})
  870. return strings.Join(keyLookup, "::"), nil
  871. }
  872. case *SlimK8sDisk:
  873. disk := i.(*SlimK8sDisk)
  874. keyLookup := stringutil.DeleteEmptyStringsFromArray([]string{disk.RegionID, disk.DiskType, disk.DiskCategory, disk.PerformanceLevel, disk.SizeInGiB})
  875. return strings.Join(keyLookup, "::"), nil
  876. default:
  877. return "", fmt.Errorf("unsupported ECS type (%T) at this time", i)
  878. }
  879. }
  880. // Below structs are used to unmarshal json response of Alibaba cloud's API DescribePrice
  881. type Price struct {
  882. OriginalPrice float32 `json:"OriginalPrice"`
  883. ReservedInstanceHourPrice float32 `json:"ReservedInstanceHourPrice"`
  884. DiscountPrice float32 `json:"DiscountPrice"`
  885. Currency string `json:"Currency"`
  886. TradePrice float32 `json:"TradePrice"`
  887. }
  888. type PriceInfo struct {
  889. Price Price `json:"Price"`
  890. }
  891. type DescribePriceResponse struct {
  892. RequestId string `json:"RequestId"`
  893. PriceInfo PriceInfo `json:"PriceInfo"`
  894. }
  895. // processDescribePriceAndCreateAlibabaPricing processes the DescribePrice API and generates the pricing information for alibaba node resource and alibaba pv resource that's backed by cloud disk.
  896. func processDescribePriceAndCreateAlibabaPricing(client *sdk.Client, i interface{}, signer *signers.AccessKeySigner, custom *CustomPricing) (pricing *AlibabaPricing, err error) {
  897. pricing = &AlibabaPricing{}
  898. var response DescribePriceResponse
  899. if i == nil {
  900. return nil, fmt.Errorf("nil component passed to process the pricing information")
  901. }
  902. switch i.(type) {
  903. case *SlimK8sNode:
  904. node := i.(*SlimK8sNode)
  905. req, err := createDescribePriceACSRequest(node)
  906. if err != nil {
  907. return nil, err
  908. }
  909. resp, err := client.ProcessCommonRequestWithSigner(req, signer)
  910. pricing.NodeAttributes = NewAlibabaNodeAttributes(node)
  911. if err != nil || resp.GetHttpStatus() != 200 {
  912. // Can be defaulted to some value here?
  913. return nil, fmt.Errorf("unable to fetch information for node with InstanceType: %v", node.InstanceType)
  914. } else {
  915. // This is where population of Pricing happens
  916. err = json.Unmarshal(resp.GetHttpContentBytes(), &response)
  917. if err != nil {
  918. return nil, fmt.Errorf("unable to unmarshall json response to custom struct with err: %w", err)
  919. }
  920. // TO-DO : Ask in PR How to get the defaults is it equal to AWS/GCP defaults? And what needs to be returned
  921. pricing.Node = &Node{
  922. Cost: fmt.Sprintf("%f", response.PriceInfo.Price.TradePrice),
  923. BaseCPUPrice: custom.CPU,
  924. BaseRAMPrice: custom.RAM,
  925. BaseGPUPrice: custom.GPU,
  926. }
  927. // TO-DO : Currently with Pay-As-You-go Offering TradePrice = HourlyPrice , When support happens to other type HourlyPrice Need to be determined.
  928. 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))
  929. }
  930. case *SlimK8sDisk:
  931. disk := i.(*SlimK8sDisk)
  932. req, err := createDescribePriceACSRequest(disk)
  933. if err != nil {
  934. return nil, err
  935. }
  936. resp, err := client.ProcessCommonRequestWithSigner(req, signer)
  937. if err != nil || resp.GetHttpStatus() != 200 {
  938. return nil, fmt.Errorf("unable to fetch information for disk with DiskType: %v with err: %w", disk.DiskCategory, err)
  939. } else {
  940. // This is where population of Pricing happens
  941. err = json.Unmarshal(resp.GetHttpContentBytes(), &response)
  942. if err != nil {
  943. return nil, fmt.Errorf("unable to unmarshall json response to custom struct with err: %w", err)
  944. }
  945. pricing.PVAttributes = NewAlibabaPVAttributes(disk)
  946. pricing.PV = &PV{
  947. Cost: fmt.Sprintf("%f", response.PriceInfo.Price.TradePrice),
  948. }
  949. // 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.
  950. 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))
  951. }
  952. default:
  953. return nil, fmt.Errorf("unsupported ECS Pricing component of type (%T) at this time", i)
  954. }
  955. return pricing, nil
  956. }
  957. // This function is to get the InstanceFamily from the InstanceType , convention followed in
  958. // instance type is ecs.[FamilyName].[DifferentSize], it gets the familyName , if it is unable to get it
  959. // it lists the instance family name as Unknown.
  960. func getInstanceFamilyFromType(instanceType string) string {
  961. splitinstanceType := strings.Split(instanceType, ".")
  962. if len(splitinstanceType) != 3 {
  963. log.Warnf("unable to find the family of the instance type %s, returning its family type unknown", instanceType)
  964. return ALIBABA_UNKNOWN_INSTANCE_FAMILY_TYPE
  965. }
  966. if !slices.Contains(alibabaInstanceFamilies, splitinstanceType[1]) {
  967. log.Warnf("currently the instance family type %s is not valid or not tested completely for pricing API", instanceType)
  968. return ALIBABA_NOT_SUPPORTED_INSTANCE_FAMILY_TYPE
  969. }
  970. return splitinstanceType[1]
  971. }
  972. // getInstanceIDFromProviderID returns the instance ID associated with the Node. A *v1.Node providerID in Alibaba cloud
  973. // is of <REGION-ID>.<INSTANCE-ID>. This function returns the Instance ID for the given ProviderID. if its unable to interpret
  974. // it defaults to empty string.
  975. func getInstanceIDFromProviderID(providerID string) string {
  976. if providerID == "" {
  977. return ""
  978. }
  979. splitStrings := strings.Split(providerID, ".")
  980. if len(splitStrings) < 2 {
  981. return ""
  982. }
  983. return splitStrings[1]
  984. }
  985. type Disk struct {
  986. Category string `json:"Category"`
  987. Size int `json:"Size"`
  988. PerformanceLevel string `json:"PerformanceLevel"`
  989. Type string `json:"Type"`
  990. RegionId string `json:"RegionId"`
  991. DiskId string `json:"DiskId"`
  992. DiskChargeType string `json:"DiskChargeType"`
  993. }
  994. type Disks struct {
  995. Disk []*Disk `json:"Disk"`
  996. }
  997. type DescribeDiskResponse struct {
  998. TotalCount int `json:"TotalCount"`
  999. Disks *Disks `json:"Disks"`
  1000. }
  1001. // getSystemDiskInfoOfANode gets the relevant System disk information associated with the Node given by the instanceID
  1002. // in form of a SlimK8sDisk with only relevant information that can adjust the node pricing. If any error occurs return
  1003. // an empty disk to not impact any default set at the price retrieval of the node.
  1004. func getSystemDiskInfoOfANode(instanceID, regionID string, client *sdk.Client, signer *signers.AccessKeySigner) (systemDisk *SlimK8sDisk) {
  1005. systemDisk = &SlimK8sDisk{}
  1006. var response DescribeDiskResponse
  1007. // if instanceID is empty string return an empty k8s
  1008. if instanceID == "" {
  1009. return
  1010. }
  1011. req, err := createDescribeDisksACSRequest(instanceID, regionID, ALIBABA_SYSTEM_DISK_CATEGORY)
  1012. // if any error occurs return an empty disk to not impact default pricing.
  1013. if err != nil {
  1014. 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)
  1015. return
  1016. }
  1017. resp, err := client.ProcessCommonRequestWithSigner(req, signer)
  1018. if err != nil || resp.GetHttpStatus() != 200 {
  1019. 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)
  1020. return
  1021. } else {
  1022. // This is where population of Pricing happens
  1023. err = json.Unmarshal(resp.GetHttpContentBytes(), &response)
  1024. if err != nil {
  1025. 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)
  1026. return
  1027. }
  1028. // 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,
  1029. // if TotalCount is not 1 just return empty and let it not impact default pricing.
  1030. if response.TotalCount != 1 {
  1031. 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)
  1032. return
  1033. }
  1034. if response.Disks == nil {
  1035. log.Warnf("Disks information missing for node with InstanceID: %s, hence defaulting it to an empty system disk to pass through to defaults", instanceID)
  1036. return
  1037. }
  1038. if len(response.Disks.Disk) < 1 {
  1039. 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)
  1040. return
  1041. }
  1042. // TO-DO: When supporting Subscription type disk, you can leverge the disk.DiskChargeType here to map it to subscription type.
  1043. systemDisk := response.Disks.Disk[0]
  1044. return NewSlimK8sDisk(systemDisk.Type, systemDisk.RegionId, ALIBABA_HOUR_PRICE_UNIT, systemDisk.Category, systemDisk.PerformanceLevel, systemDisk.DiskId, "", fmt.Sprintf("%d", systemDisk.Size))
  1045. }
  1046. }
  1047. // generateSlimK8sNodeFromV1Node generates SlimK8sNode struct from v1.Node to fetch pricing information and call alibaba API.
  1048. func generateSlimK8sNodeFromV1Node(node *v1.Node) *SlimK8sNode {
  1049. var regionID, osType, instanceType, providerID, priceUnit, instanceFamily string
  1050. var memorySizeInKiB string // TO-DO: try to convert it into float
  1051. var ok, IsIoOptimized bool
  1052. if regionID, ok = node.Labels["topology.kubernetes.io/region"]; !ok {
  1053. // HIGHLY UNLIKELY THAT THIS LABEL WONT BE THERE.
  1054. log.Debugf("No RegionID label for the node: %s", node.Name)
  1055. }
  1056. if osType, ok = node.Labels["beta.kubernetes.io/os"]; !ok {
  1057. // HIGHLY UNLIKELY THAT THIS LABEL WONT BE THERE.
  1058. log.Debugf("OS type undetected for the node: %s", node.Name)
  1059. }
  1060. if instanceType, ok = node.Labels["node.kubernetes.io/instance-type"]; !ok {
  1061. // HIGHLY UNLIKELY THAT THIS LABEL WONT BE THERE.
  1062. log.Debugf("Instance Type undetected for the node: %s", node.Name)
  1063. }
  1064. instanceFamily = getInstanceFamilyFromType(instanceType)
  1065. memorySizeInKiB = fmt.Sprintf("%s", node.Status.Capacity.Memory())
  1066. providerID = node.Spec.ProviderID // Alibaba Cloud provider doesnt follow convention of prefix with cloud provider name
  1067. // 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
  1068. // Basic price Json has it as part of the key so defaulting to true.
  1069. IsIoOptimized = true
  1070. priceUnit = ALIBABA_HOUR_PRICE_UNIT
  1071. systemDisk := &SlimK8sDisk{}
  1072. return NewSlimK8sNode(instanceType, regionID, priceUnit, memorySizeInKiB, osType, providerID, instanceFamily, IsIoOptimized, systemDisk)
  1073. }
  1074. // getNumericalValueFromResourceQuantity returns the numericalValue of the resourceQuantity
  1075. // An example is: 20Gi returns to 20. If any error occurs it returns the default value used in describePrice API which is 2000.
  1076. func getNumericalValueFromResourceQuantity(quantity string) (value string) {
  1077. // defaulting when any panic or empty string occurs.
  1078. defer func() {
  1079. log.Debugf("unable to determine the size of the PV so defaulting the size to %s", ALIBABA_DEFAULT_DATADISK_SIZE)
  1080. if err := recover(); err != nil {
  1081. value = ALIBABA_DEFAULT_DATADISK_SIZE
  1082. }
  1083. if value == "" {
  1084. value = ALIBABA_DEFAULT_DATADISK_SIZE
  1085. }
  1086. }()
  1087. res := sizeRegEx.FindAllStringSubmatch(quantity, 1)
  1088. value = res[0][1]
  1089. return
  1090. }
  1091. // generateSlimK8sDiskFromV1PV function generates SlimK8sDisk from v1.PersistentVolume
  1092. // to generate slim disk type that can be used to fetch pricing information for Data disk type.
  1093. func generateSlimK8sDiskFromV1PV(pv *v1.PersistentVolume, regionID string) *SlimK8sDisk {
  1094. // All PVs are data disks while local disk are categorized as system disk
  1095. diskType := ALIBABA_DATA_DISK_CATEGORY
  1096. //TO-DO: Disk supports month and hour prices , defaulting to hour
  1097. priceUnit := ALIBABA_HOUR_PRICE_UNIT
  1098. sizeQuantity := fmt.Sprintf("%s", pv.Spec.Capacity.Storage())
  1099. // res := sizeRegEx.FindAllStringSubmatch(sizeQuantity, 1)
  1100. sizeInGiB := getNumericalValueFromResourceQuantity(sizeQuantity)
  1101. providerID := ""
  1102. if pv.Spec.CSI != nil {
  1103. providerID = pv.Spec.CSI.VolumeHandle
  1104. } else {
  1105. providerID = pv.Name // Looks like pv name is same as providerID in Alibaba k8s cluster
  1106. }
  1107. // Performance level being empty string gets defaulted in describePrice to PL1.
  1108. performanceLevel := ""
  1109. diskCategory := ""
  1110. if pv.Spec.CSI != nil {
  1111. if val, ok := pv.Spec.CSI.VolumeAttributes["performanceLevel"]; ok {
  1112. performanceLevel = val
  1113. }
  1114. if val, ok := pv.Spec.CSI.VolumeAttributes["type"]; ok {
  1115. diskCategory = val
  1116. }
  1117. }
  1118. // Highly unlikely that label pv.Spec.CSI.VolumeAttributes["type"] doesn't exist but if occured default to cloud (most basic disk type)
  1119. if diskCategory == "" {
  1120. diskCategory = ALIBABA_DISK_CLOUD_CATEGORY
  1121. }
  1122. return NewSlimK8sDisk(diskType, regionID, priceUnit, diskCategory, performanceLevel, providerID, pv.Spec.StorageClassName, sizeInGiB)
  1123. }
  1124. // 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!
  1125. // if topology.diskplugin.csi.alibabacloud.com/region label/annotation is passed during PV creation return that as the PV region.
  1126. // if topology.diskplugin.csi.alibabacloud.com/zone label/annotation is passed during PV creation determine the region based on this pv label.
  1127. // if neither of the above label/annotation is present check node affinity for the zone affinity and determine the region based on this zone.
  1128. // if nether of the above yields a region , return empty string to default it to cluster region.
  1129. func determinePVRegion(pv *v1.PersistentVolume) string {
  1130. // if "topology.diskplugin.csi.alibabacloud.com/region" is present as a label or annotation return that as the PV region
  1131. if val, ok := pv.Labels[ALIBABA_DISK_TOPOLOGY_REGION_LABEL]; ok {
  1132. log.Debugf("determinePVRegion returned a region value of: %s through label: %s for PV name: %s", val, ALIBABA_DISK_TOPOLOGY_REGION_LABEL, pv.Name)
  1133. return val
  1134. }
  1135. if val, ok := pv.Annotations[ALIBABA_DISK_TOPOLOGY_REGION_LABEL]; ok {
  1136. log.Debugf("determinePVRegion returned a region value of: %s through annotation: %s for PV name: %s", val, ALIBABA_DISK_TOPOLOGY_REGION_LABEL, pv.Name)
  1137. return val
  1138. }
  1139. // 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
  1140. var pvZone string
  1141. if val, ok := pv.Labels[ALIBABA_DISK_TOPOLOGY_ZONE_LABEL]; ok {
  1142. log.Debugf("determinePVRegion will set zone value to: %s through label: %s for PV name: %s", val, ALIBABA_DISK_TOPOLOGY_ZONE_LABEL, pv.Name)
  1143. pvZone = val
  1144. }
  1145. if pvZone == "" {
  1146. if val, ok := pv.Annotations[ALIBABA_DISK_TOPOLOGY_ZONE_LABEL]; ok {
  1147. log.Debugf("determinePVRegion will set zone value to: %s through annotation: %s for PV name: %s", val, ALIBABA_DISK_TOPOLOGY_ZONE_LABEL, pv.Name)
  1148. pvZone = val
  1149. }
  1150. }
  1151. if pvZone == "" {
  1152. // 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
  1153. // 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.
  1154. if pv.Spec.NodeAffinity != nil {
  1155. nodeAffinity := pv.Spec.NodeAffinity
  1156. if nodeAffinity.Required != nil && nodeAffinity.Required.NodeSelectorTerms != nil {
  1157. for _, nodeSelectorTerm := range nodeAffinity.Required.NodeSelectorTerms {
  1158. matchExpression := nodeSelectorTerm.MatchExpressions
  1159. for _, nodeSelectorRequirement := range matchExpression {
  1160. if nodeSelectorRequirement.Key == ALIBABA_DISK_TOPOLOGY_ZONE_LABEL {
  1161. 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)
  1162. pvZone = nodeSelectorRequirement.Values[0]
  1163. }
  1164. }
  1165. }
  1166. }
  1167. }
  1168. }
  1169. for _, region := range alibabaRegions {
  1170. if strings.Contains(pvZone, region) {
  1171. log.Debugf("determinePVRegion determined region of %s through zone affiliation of the PV %s\n", region, pvZone)
  1172. return region
  1173. }
  1174. }
  1175. return ""
  1176. }