aliyunprovider.go 52 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355
  1. package cloud
  2. import (
  3. "errors"
  4. "fmt"
  5. "io"
  6. "os"
  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 := os.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. regionOverrides := env.GetRegionOverrideList()
  550. if len(regionOverrides) > 0 {
  551. log.Debugf("Overriding Alibaba regions with configured region list: %+v", regionOverrides)
  552. return regionOverrides
  553. }
  554. return alibabaRegions
  555. }
  556. // ClusterInfo returns information about Alibaba Cloud cluster, as provided by metadata.
  557. func (alibaba *Alibaba) ClusterInfo() (map[string]string, error) {
  558. c, err := alibaba.GetConfig()
  559. if err != nil {
  560. return nil, fmt.Errorf("failed to getConfig with err: %w", err)
  561. }
  562. var clusterName string
  563. if c.ClusterName != "" {
  564. clusterName = c.ClusterName
  565. }
  566. // Set it to environment clusterID if not set at this point
  567. if clusterName == "" {
  568. clusterName = env.GetClusterID()
  569. }
  570. m := make(map[string]string)
  571. m["name"] = clusterName
  572. m["provider"] = kubecost.AlibabaProvider
  573. m["project"] = alibaba.clusterAccountId
  574. m["region"] = alibaba.clusterRegion
  575. m["id"] = env.GetClusterID()
  576. return m, nil
  577. }
  578. // Will look at this in Next PR if needed
  579. func (alibaba *Alibaba) GetAddresses() ([]byte, error) {
  580. return nil, nil
  581. }
  582. // Will look at this in Next PR if needed
  583. func (alibaba *Alibaba) GetDisks() ([]byte, error) {
  584. return nil, nil
  585. }
  586. func (alibaba *Alibaba) GetOrphanedResources() ([]OrphanedResource, error) {
  587. return nil, errors.New("not implemented")
  588. }
  589. func (alibaba *Alibaba) UpdateConfig(r io.Reader, updateType string) (*CustomPricing, error) {
  590. return alibaba.Config.Update(func(c *CustomPricing) error {
  591. if updateType != "" {
  592. return fmt.Errorf("UpdateConfig for Alibaba Provider doesn't support updateType %s at this time", updateType)
  593. } else {
  594. a := make(map[string]interface{})
  595. err := json.NewDecoder(r).Decode(&a)
  596. if err != nil {
  597. return err
  598. }
  599. for k, v := range a {
  600. kUpper := toTitle.String(k) // Just so we consistently supply / receive the same values, uppercase the first letter.
  601. vstr, ok := v.(string)
  602. if ok {
  603. err := SetCustomPricingField(c, kUpper, vstr)
  604. if err != nil {
  605. return err
  606. }
  607. } else {
  608. return fmt.Errorf("type error while updating config for %s", kUpper)
  609. }
  610. }
  611. }
  612. if env.IsRemoteEnabled() {
  613. err := UpdateClusterMeta(env.GetClusterID(), c.ClusterName)
  614. if err != nil {
  615. return err
  616. }
  617. }
  618. return nil
  619. })
  620. }
  621. func (alibaba *Alibaba) UpdateConfigFromConfigMap(cm map[string]string) (*CustomPricing, error) {
  622. return alibaba.Config.UpdateFromMap(cm)
  623. }
  624. // Will look at this in Next PR if needed
  625. func (alibaba *Alibaba) GetManagementPlatform() (string, error) {
  626. return "", nil
  627. }
  628. // Will look at this in Next PR if needed
  629. func (alibaba *Alibaba) GetLocalStorageQuery(window, offset time.Duration, rate bool, used bool) string {
  630. return ""
  631. }
  632. // Will look at this in Next PR if needed
  633. func (alibaba *Alibaba) ApplyReservedInstancePricing(nodes map[string]*Node) {
  634. }
  635. // Will look at this in Next PR if needed
  636. func (alibaba *Alibaba) ServiceAccountStatus() *ServiceAccountStatus {
  637. return &ServiceAccountStatus{}
  638. }
  639. // Will look at this in Next PR if needed
  640. func (alibaba *Alibaba) PricingSourceStatus() map[string]*PricingSource {
  641. return map[string]*PricingSource{}
  642. }
  643. // Will look at this in Next PR if needed
  644. func (alibaba *Alibaba) ClusterManagementPricing() (string, float64, error) {
  645. return "", 0.0, nil
  646. }
  647. // Will look at this in Next PR if needed
  648. func (alibaba *Alibaba) CombinedDiscountForNode(string, bool, float64, float64) float64 {
  649. return 0.0
  650. }
  651. func (alibaba *Alibaba) accessKeyisLoaded() bool {
  652. return alibaba.accessKey != nil
  653. }
  654. type AlibabaNodeKey struct {
  655. ProviderID string
  656. RegionID string
  657. InstanceType string
  658. OSType string
  659. OptimizedKeyword string //If IsIoOptimized is true use the word optimize in the Node key and if its not optimized use the word nonoptimize
  660. SystemDiskCategory string
  661. SystemDiskSizeInGiB string
  662. SystemDiskPerformanceLevel string
  663. }
  664. func NewAlibabaNodeKey(node *SlimK8sNode, optimizedKeyword, systemDiskCategory, systemDiskSizeInGiB, systemDiskPerfromanceLevel string) *AlibabaNodeKey {
  665. var providerID, regionID, instanceType, osType string
  666. if node != nil {
  667. providerID = node.ProviderID
  668. regionID = node.RegionID
  669. instanceType = node.InstanceType
  670. osType = node.OSType
  671. }
  672. return &AlibabaNodeKey{
  673. ProviderID: providerID,
  674. RegionID: regionID,
  675. InstanceType: instanceType,
  676. OSType: osType,
  677. OptimizedKeyword: optimizedKeyword,
  678. SystemDiskCategory: systemDiskCategory,
  679. SystemDiskSizeInGiB: systemDiskSizeInGiB,
  680. SystemDiskPerformanceLevel: systemDiskPerfromanceLevel,
  681. }
  682. }
  683. func (alibabaNodeKey *AlibabaNodeKey) ID() string {
  684. return alibabaNodeKey.ProviderID
  685. }
  686. func (alibabaNodeKey *AlibabaNodeKey) Features() string {
  687. keyLookup := stringutil.DeleteEmptyStringsFromArray([]string{alibabaNodeKey.RegionID, alibabaNodeKey.InstanceType, alibabaNodeKey.OSType,
  688. alibabaNodeKey.OptimizedKeyword, alibabaNodeKey.SystemDiskCategory, alibabaNodeKey.SystemDiskSizeInGiB, alibabaNodeKey.SystemDiskPerformanceLevel})
  689. return strings.Join(keyLookup, "::")
  690. }
  691. func (alibabaNodeKey *AlibabaNodeKey) GPUType() string {
  692. return ""
  693. }
  694. func (alibabaNodeKey *AlibabaNodeKey) GPUCount() int {
  695. return 0
  696. }
  697. // Get's the key for the k8s node input
  698. func (alibaba *Alibaba) GetKey(mapValue map[string]string, node *v1.Node) Key {
  699. slimK8sNode := generateSlimK8sNodeFromV1Node(node)
  700. var aak *credentials.AccessKeyCredential
  701. var err error
  702. var ok bool
  703. var client *sdk.Client
  704. var signer *signers.AccessKeySigner
  705. optimizedKeyword := ""
  706. if slimK8sNode.IsIoOptimized {
  707. optimizedKeyword = ALIBABA_OPTIMIZE_KEYWORD
  708. } else {
  709. optimizedKeyword = ALIBABA_NON_OPTIMIZE_KEYWORD
  710. }
  711. var diskCategory, diskSizeInGiB, diskPerformanceLevel string
  712. if !alibaba.accessKeyisLoaded() {
  713. aak, err = alibaba.GetAlibabaAccessKey()
  714. if err != nil {
  715. 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)
  716. return NewAlibabaNodeKey(slimK8sNode, optimizedKeyword, diskCategory, diskSizeInGiB, diskPerformanceLevel)
  717. }
  718. } else {
  719. aak = alibaba.accessKey
  720. }
  721. signer = signers.NewAccessKeySigner(aak)
  722. if aak == nil {
  723. log.Warnf("unable to retrieve the Alibaba API keys for node with providerID %s hence skipping SystemDisk Retrieval", slimK8sNode.ProviderID)
  724. return NewAlibabaNodeKey(slimK8sNode, optimizedKeyword, diskCategory, diskSizeInGiB, diskPerformanceLevel)
  725. }
  726. if client, ok = alibaba.clients[slimK8sNode.RegionID]; !ok {
  727. client, err = sdk.NewClientWithAccessKey(slimK8sNode.RegionID, aak.AccessKeyId, aak.AccessKeySecret)
  728. if err != nil {
  729. 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)
  730. return NewAlibabaNodeKey(slimK8sNode, optimizedKeyword, diskCategory, diskSizeInGiB, diskPerformanceLevel)
  731. }
  732. alibaba.clients[slimK8sNode.RegionID] = client
  733. }
  734. instanceID := getInstanceIDFromProviderID(slimK8sNode.ProviderID)
  735. slimK8sNode.SystemDisk = getSystemDiskInfoOfANode(instanceID, slimK8sNode.RegionID, client, signer)
  736. if slimK8sNode.SystemDisk != nil {
  737. diskCategory = slimK8sNode.SystemDisk.DiskCategory
  738. diskSizeInGiB = slimK8sNode.SystemDisk.SizeInGiB
  739. diskPerformanceLevel = slimK8sNode.SystemDisk.PerformanceLevel
  740. }
  741. return NewAlibabaNodeKey(slimK8sNode, optimizedKeyword, diskCategory, diskSizeInGiB, diskPerformanceLevel)
  742. }
  743. type AlibabaPVKey struct {
  744. ProviderID string
  745. RegionID string
  746. PVType string
  747. PVSubType string
  748. PVCategory string
  749. PVPerformaceLevel string
  750. StorageClassName string
  751. SizeInGiB string
  752. }
  753. func (alibaba *Alibaba) GetPVKey(pv *v1.PersistentVolume, parameters map[string]string, defaultRegion string) PVKey {
  754. regionID := defaultRegion
  755. // If default Region is not passed default it to cluster region ID.
  756. if defaultRegion == "" {
  757. regionID = alibaba.clusterRegion
  758. }
  759. slimK8sDisk := generateSlimK8sDiskFromV1PV(pv, defaultRegion)
  760. return &AlibabaPVKey{
  761. ProviderID: slimK8sDisk.ProviderID,
  762. RegionID: regionID,
  763. PVType: ALIBABA_PV_CLOUD_DISK_TYPE,
  764. PVSubType: slimK8sDisk.DiskType,
  765. PVCategory: slimK8sDisk.DiskCategory,
  766. PVPerformaceLevel: slimK8sDisk.PerformanceLevel,
  767. StorageClassName: pv.Spec.StorageClassName,
  768. SizeInGiB: slimK8sDisk.SizeInGiB,
  769. }
  770. }
  771. func (alibabaPVKey *AlibabaPVKey) Features() string {
  772. keyLookup := stringutil.DeleteEmptyStringsFromArray([]string{alibabaPVKey.RegionID, alibabaPVKey.PVSubType, alibabaPVKey.PVCategory, alibabaPVKey.PVPerformaceLevel, alibabaPVKey.SizeInGiB})
  773. return strings.Join(keyLookup, "::")
  774. }
  775. func (alibabaPVKey *AlibabaPVKey) ID() string {
  776. return alibabaPVKey.ProviderID
  777. }
  778. // Get storage class information for PV.
  779. func (alibabaPVKey *AlibabaPVKey) GetStorageClass() string {
  780. return alibabaPVKey.StorageClassName
  781. }
  782. // Helper functions for alibabaprovider.go
  783. // createDescribePriceACSRequest creates the HTTP GET request for the required resources' Price information,
  784. // When supporting subscription and Premptible resources this HTTP call needs to be modified with PriceUnit information
  785. // When supporting different new type of instances like Compute Optimized, Memory Optimized etc make sure you add the instance type
  786. // in unit test and check if it works or not to create the ack request and processDescribePriceAndCreateAlibabaPricing function
  787. // else more paramters need to be pulled from kubernetes node response or gather infromation from elsewhere and function modified.
  788. func createDescribePriceACSRequest(i interface{}) (*requests.CommonRequest, error) {
  789. request := requests.NewCommonRequest()
  790. request.Method = requests.GET
  791. request.Product = ALIBABA_ECS_PRODUCT_CODE
  792. request.Domain = ALIBABA_ECS_DOMAIN
  793. request.Version = ALIBABA_ECS_VERSION
  794. request.Scheme = requests.HTTPS
  795. request.ApiName = ALIBABA_DESCRIBE_PRICE_API_ACTION
  796. switch i.(type) {
  797. case *SlimK8sNode:
  798. node := i.(*SlimK8sNode)
  799. request.QueryParams["RegionId"] = node.RegionID
  800. request.QueryParams["ResourceType"] = ALIBABA_INSTANCE_RESOURCE_TYPE
  801. request.QueryParams["InstanceType"] = node.InstanceType
  802. request.QueryParams["PriceUnit"] = node.PriceUnit
  803. if node.SystemDisk != nil {
  804. // Only if the required information is present it should be overridden else default it via the API
  805. if node.SystemDisk.DiskCategory != "" {
  806. request.QueryParams["SystemDisk.Category"] = node.SystemDisk.DiskCategory
  807. }
  808. if node.SystemDisk.SizeInGiB != "" {
  809. request.QueryParams["SystemDisk.Size"] = node.SystemDisk.SizeInGiB
  810. }
  811. if node.SystemDisk.PerformanceLevel != "" {
  812. request.QueryParams["SystemDisk.PerformanceLevel"] = node.SystemDisk.PerformanceLevel
  813. }
  814. } else {
  815. // When System Disk information is not available for instance family g6e, r7 and r6e the defaults in
  816. // DescribePrice dont default rightly to cloud_essd for these instances.
  817. if slices.Contains(alibabaDefaultToCloudEssd, node.InstanceTypeFamily) {
  818. request.QueryParams["SystemDisk.Category"] = ALIBABA_DISK_CLOUD_ESSD_CATEGORY
  819. }
  820. }
  821. request.TransToAcsRequest()
  822. return request, nil
  823. case *SlimK8sDisk:
  824. disk := i.(*SlimK8sDisk)
  825. request.QueryParams["RegionId"] = disk.RegionID
  826. request.QueryParams["PriceUnit"] = disk.PriceUnit
  827. request.QueryParams["ResourceType"] = ALIBABA_DISK_RESOURCE_TYPE
  828. request.QueryParams[fmt.Sprintf("%s.%d.Size", ALIBABA_DATA_DISK_PREFIX, 1)] = disk.SizeInGiB
  829. request.QueryParams[fmt.Sprintf("%s.%d.Category", ALIBABA_DATA_DISK_PREFIX, 1)] = disk.DiskCategory
  830. // Performance level defaults to PL1 if not present in volume attribute.
  831. if disk.PerformanceLevel != "" {
  832. request.QueryParams[fmt.Sprintf("%s.%d.PerformanceLevel", ALIBABA_DATA_DISK_PREFIX, 1)] = disk.PerformanceLevel
  833. }
  834. request.TransToAcsRequest()
  835. return request, nil
  836. default:
  837. return nil, fmt.Errorf("unsupported ECS type (%T) for DescribePrice at this time", i)
  838. }
  839. }
  840. // createDescribeDisksCSRequest creates the HTTP GET Request to map the system disk to the InstanceID
  841. func createDescribeDisksACSRequest(instanceID, regionID, diskType string) (*requests.CommonRequest, error) {
  842. request := requests.NewCommonRequest()
  843. request.Method = requests.GET
  844. request.Product = ALIBABA_ECS_PRODUCT_CODE
  845. request.Domain = ALIBABA_ECS_DOMAIN
  846. request.Version = ALIBABA_ECS_VERSION
  847. request.Scheme = requests.HTTPS
  848. request.ApiName = ALIBABA_DESCRIBE_DISK_API_ACTION
  849. request.QueryParams["RegionId"] = regionID
  850. request.QueryParams["InstanceId"] = instanceID
  851. request.QueryParams["DiskType"] = diskType
  852. request.TransToAcsRequest()
  853. return request, nil
  854. }
  855. // determineKeyForPricing generate a unique key from SlimK8sNode object that is constructed from v1.Node object and
  856. // SlimK8sDisk that is constructed from v1.PersistentVolume.
  857. func determineKeyForPricing(i interface{}) (string, error) {
  858. if i == nil {
  859. return "", fmt.Errorf("nil component passed to determine key")
  860. }
  861. switch i.(type) {
  862. case *SlimK8sNode:
  863. node := i.(*SlimK8sNode)
  864. var diskCategory, diskSizeInGiB, diskPerformanceLevel string
  865. if node.SystemDisk != nil {
  866. diskCategory = node.SystemDisk.DiskCategory
  867. diskSizeInGiB = node.SystemDisk.SizeInGiB
  868. diskPerformanceLevel = node.SystemDisk.PerformanceLevel
  869. }
  870. if node.IsIoOptimized {
  871. keyLookup := stringutil.DeleteEmptyStringsFromArray([]string{node.RegionID, node.InstanceType, node.OSType, ALIBABA_OPTIMIZE_KEYWORD, diskCategory, diskSizeInGiB, diskPerformanceLevel})
  872. return strings.Join(keyLookup, "::"), nil
  873. } else {
  874. keyLookup := stringutil.DeleteEmptyStringsFromArray([]string{node.RegionID, node.InstanceType, node.OSType, ALIBABA_NON_OPTIMIZE_KEYWORD, diskCategory, diskSizeInGiB, diskPerformanceLevel})
  875. return strings.Join(keyLookup, "::"), nil
  876. }
  877. case *SlimK8sDisk:
  878. disk := i.(*SlimK8sDisk)
  879. keyLookup := stringutil.DeleteEmptyStringsFromArray([]string{disk.RegionID, disk.DiskType, disk.DiskCategory, disk.PerformanceLevel, disk.SizeInGiB})
  880. return strings.Join(keyLookup, "::"), nil
  881. default:
  882. return "", fmt.Errorf("unsupported ECS type (%T) at this time", i)
  883. }
  884. }
  885. // Below structs are used to unmarshal json response of Alibaba cloud's API DescribePrice
  886. type Price struct {
  887. OriginalPrice float32 `json:"OriginalPrice"`
  888. ReservedInstanceHourPrice float32 `json:"ReservedInstanceHourPrice"`
  889. DiscountPrice float32 `json:"DiscountPrice"`
  890. Currency string `json:"Currency"`
  891. TradePrice float32 `json:"TradePrice"`
  892. }
  893. type PriceInfo struct {
  894. Price Price `json:"Price"`
  895. }
  896. type DescribePriceResponse struct {
  897. RequestId string `json:"RequestId"`
  898. PriceInfo PriceInfo `json:"PriceInfo"`
  899. }
  900. // processDescribePriceAndCreateAlibabaPricing processes the DescribePrice API and generates the pricing information for alibaba node resource and alibaba pv resource that's backed by cloud disk.
  901. func processDescribePriceAndCreateAlibabaPricing(client *sdk.Client, i interface{}, signer *signers.AccessKeySigner, custom *CustomPricing) (pricing *AlibabaPricing, err error) {
  902. pricing = &AlibabaPricing{}
  903. var response DescribePriceResponse
  904. if i == nil {
  905. return nil, fmt.Errorf("nil component passed to process the pricing information")
  906. }
  907. switch i.(type) {
  908. case *SlimK8sNode:
  909. node := i.(*SlimK8sNode)
  910. req, err := createDescribePriceACSRequest(node)
  911. if err != nil {
  912. return nil, err
  913. }
  914. resp, err := client.ProcessCommonRequestWithSigner(req, signer)
  915. pricing.NodeAttributes = NewAlibabaNodeAttributes(node)
  916. if err != nil || resp.GetHttpStatus() != 200 {
  917. // Can be defaulted to some value here?
  918. return nil, fmt.Errorf("unable to fetch information for node with InstanceType: %v", node.InstanceType)
  919. } else {
  920. // This is where population of Pricing happens
  921. err = json.Unmarshal(resp.GetHttpContentBytes(), &response)
  922. if err != nil {
  923. return nil, fmt.Errorf("unable to unmarshall json response to custom struct with err: %w", err)
  924. }
  925. // TO-DO : Ask in PR How to get the defaults is it equal to AWS/GCP defaults? And what needs to be returned
  926. pricing.Node = &Node{
  927. Cost: fmt.Sprintf("%f", response.PriceInfo.Price.TradePrice),
  928. BaseCPUPrice: custom.CPU,
  929. BaseRAMPrice: custom.RAM,
  930. BaseGPUPrice: custom.GPU,
  931. }
  932. // TO-DO : Currently with Pay-As-You-go Offering TradePrice = HourlyPrice , When support happens to other type HourlyPrice Need to be determined.
  933. 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))
  934. }
  935. case *SlimK8sDisk:
  936. disk := i.(*SlimK8sDisk)
  937. req, err := createDescribePriceACSRequest(disk)
  938. if err != nil {
  939. return nil, err
  940. }
  941. resp, err := client.ProcessCommonRequestWithSigner(req, signer)
  942. if err != nil || resp.GetHttpStatus() != 200 {
  943. return nil, fmt.Errorf("unable to fetch information for disk with DiskType: %v with err: %w", disk.DiskCategory, err)
  944. } else {
  945. // This is where population of Pricing happens
  946. err = json.Unmarshal(resp.GetHttpContentBytes(), &response)
  947. if err != nil {
  948. return nil, fmt.Errorf("unable to unmarshall json response to custom struct with err: %w", err)
  949. }
  950. pricing.PVAttributes = NewAlibabaPVAttributes(disk)
  951. pricing.PV = &PV{
  952. Cost: fmt.Sprintf("%f", response.PriceInfo.Price.TradePrice),
  953. }
  954. // 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.
  955. 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))
  956. }
  957. default:
  958. return nil, fmt.Errorf("unsupported ECS Pricing component of type (%T) at this time", i)
  959. }
  960. return pricing, nil
  961. }
  962. // This function is to get the InstanceFamily from the InstanceType , convention followed in
  963. // instance type is ecs.[FamilyName].[DifferentSize], it gets the familyName , if it is unable to get it
  964. // it lists the instance family name as Unknown.
  965. func getInstanceFamilyFromType(instanceType string) string {
  966. splitinstanceType := strings.Split(instanceType, ".")
  967. if len(splitinstanceType) != 3 {
  968. log.Warnf("unable to find the family of the instance type %s, returning its family type unknown", instanceType)
  969. return ALIBABA_UNKNOWN_INSTANCE_FAMILY_TYPE
  970. }
  971. if !slices.Contains(alibabaInstanceFamilies, splitinstanceType[1]) {
  972. log.Warnf("currently the instance family type %s is not valid or not tested completely for pricing API", instanceType)
  973. return ALIBABA_NOT_SUPPORTED_INSTANCE_FAMILY_TYPE
  974. }
  975. return splitinstanceType[1]
  976. }
  977. // getInstanceIDFromProviderID returns the instance ID associated with the Node. A *v1.Node providerID in Alibaba cloud
  978. // is of <REGION-ID>.<INSTANCE-ID>. This function returns the Instance ID for the given ProviderID. if its unable to interpret
  979. // it defaults to empty string.
  980. func getInstanceIDFromProviderID(providerID string) string {
  981. if providerID == "" {
  982. return ""
  983. }
  984. splitStrings := strings.Split(providerID, ".")
  985. if len(splitStrings) < 2 {
  986. return ""
  987. }
  988. return splitStrings[1]
  989. }
  990. type Disk struct {
  991. Category string `json:"Category"`
  992. Size int `json:"Size"`
  993. PerformanceLevel string `json:"PerformanceLevel"`
  994. Type string `json:"Type"`
  995. RegionId string `json:"RegionId"`
  996. DiskId string `json:"DiskId"`
  997. DiskChargeType string `json:"DiskChargeType"`
  998. }
  999. type Disks struct {
  1000. Disk []*Disk `json:"Disk"`
  1001. }
  1002. type DescribeDiskResponse struct {
  1003. TotalCount int `json:"TotalCount"`
  1004. Disks *Disks `json:"Disks"`
  1005. }
  1006. // getSystemDiskInfoOfANode gets the relevant System disk information associated with the Node given by the instanceID
  1007. // in form of a SlimK8sDisk with only relevant information that can adjust the node pricing. If any error occurs return
  1008. // an empty disk to not impact any default set at the price retrieval of the node.
  1009. func getSystemDiskInfoOfANode(instanceID, regionID string, client *sdk.Client, signer *signers.AccessKeySigner) (systemDisk *SlimK8sDisk) {
  1010. systemDisk = &SlimK8sDisk{}
  1011. var response DescribeDiskResponse
  1012. // if instanceID is empty string return an empty k8s
  1013. if instanceID == "" {
  1014. return
  1015. }
  1016. req, err := createDescribeDisksACSRequest(instanceID, regionID, ALIBABA_SYSTEM_DISK_CATEGORY)
  1017. // if any error occurs return an empty disk to not impact default pricing.
  1018. if err != nil {
  1019. 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)
  1020. return
  1021. }
  1022. resp, err := client.ProcessCommonRequestWithSigner(req, signer)
  1023. if err != nil || resp.GetHttpStatus() != 200 {
  1024. 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)
  1025. return
  1026. } else {
  1027. // This is where population of Pricing happens
  1028. err = json.Unmarshal(resp.GetHttpContentBytes(), &response)
  1029. if err != nil {
  1030. 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)
  1031. return
  1032. }
  1033. // 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,
  1034. // if TotalCount is not 1 just return empty and let it not impact default pricing.
  1035. if response.TotalCount != 1 {
  1036. 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)
  1037. return
  1038. }
  1039. if response.Disks == nil {
  1040. log.Warnf("Disks information missing for node with InstanceID: %s, hence defaulting it to an empty system disk to pass through to defaults", instanceID)
  1041. return
  1042. }
  1043. if len(response.Disks.Disk) < 1 {
  1044. 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)
  1045. return
  1046. }
  1047. // TO-DO: When supporting Subscription type disk, you can leverge the disk.DiskChargeType here to map it to subscription type.
  1048. systemDisk := response.Disks.Disk[0]
  1049. return NewSlimK8sDisk(systemDisk.Type, systemDisk.RegionId, ALIBABA_HOUR_PRICE_UNIT, systemDisk.Category, systemDisk.PerformanceLevel, systemDisk.DiskId, "", fmt.Sprintf("%d", systemDisk.Size))
  1050. }
  1051. }
  1052. // generateSlimK8sNodeFromV1Node generates SlimK8sNode struct from v1.Node to fetch pricing information and call alibaba API.
  1053. func generateSlimK8sNodeFromV1Node(node *v1.Node) *SlimK8sNode {
  1054. var regionID, osType, instanceType, providerID, priceUnit, instanceFamily string
  1055. var memorySizeInKiB string // TO-DO: try to convert it into float
  1056. var ok, IsIoOptimized bool
  1057. if regionID, ok = node.Labels["topology.kubernetes.io/region"]; !ok {
  1058. // HIGHLY UNLIKELY THAT THIS LABEL WONT BE THERE.
  1059. log.Debugf("No RegionID label for the node: %s", node.Name)
  1060. }
  1061. if osType, ok = node.Labels["beta.kubernetes.io/os"]; !ok {
  1062. // HIGHLY UNLIKELY THAT THIS LABEL WONT BE THERE.
  1063. log.Debugf("OS type undetected for the node: %s", node.Name)
  1064. }
  1065. if instanceType, ok = node.Labels["node.kubernetes.io/instance-type"]; !ok {
  1066. // HIGHLY UNLIKELY THAT THIS LABEL WONT BE THERE.
  1067. log.Debugf("Instance Type undetected for the node: %s", node.Name)
  1068. }
  1069. instanceFamily = getInstanceFamilyFromType(instanceType)
  1070. memorySizeInKiB = fmt.Sprintf("%s", node.Status.Capacity.Memory())
  1071. providerID = node.Spec.ProviderID // Alibaba Cloud provider doesnt follow convention of prefix with cloud provider name
  1072. // 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
  1073. // Basic price Json has it as part of the key so defaulting to true.
  1074. IsIoOptimized = true
  1075. priceUnit = ALIBABA_HOUR_PRICE_UNIT
  1076. systemDisk := &SlimK8sDisk{}
  1077. return NewSlimK8sNode(instanceType, regionID, priceUnit, memorySizeInKiB, osType, providerID, instanceFamily, IsIoOptimized, systemDisk)
  1078. }
  1079. // getNumericalValueFromResourceQuantity returns the numericalValue of the resourceQuantity
  1080. // An example is: 20Gi returns to 20. If any error occurs it returns the default value used in describePrice API which is 2000.
  1081. func getNumericalValueFromResourceQuantity(quantity string) (value string) {
  1082. // defaulting when any panic or empty string occurs.
  1083. defer func() {
  1084. log.Debugf("unable to determine the size of the PV so defaulting the size to %s", ALIBABA_DEFAULT_DATADISK_SIZE)
  1085. if err := recover(); err != nil {
  1086. value = ALIBABA_DEFAULT_DATADISK_SIZE
  1087. }
  1088. if value == "" {
  1089. value = ALIBABA_DEFAULT_DATADISK_SIZE
  1090. }
  1091. }()
  1092. res := sizeRegEx.FindAllStringSubmatch(quantity, 1)
  1093. value = res[0][1]
  1094. return
  1095. }
  1096. // generateSlimK8sDiskFromV1PV function generates SlimK8sDisk from v1.PersistentVolume
  1097. // to generate slim disk type that can be used to fetch pricing information for Data disk type.
  1098. func generateSlimK8sDiskFromV1PV(pv *v1.PersistentVolume, regionID string) *SlimK8sDisk {
  1099. // All PVs are data disks while local disk are categorized as system disk
  1100. diskType := ALIBABA_DATA_DISK_CATEGORY
  1101. //TO-DO: Disk supports month and hour prices , defaulting to hour
  1102. priceUnit := ALIBABA_HOUR_PRICE_UNIT
  1103. sizeQuantity := fmt.Sprintf("%s", pv.Spec.Capacity.Storage())
  1104. // res := sizeRegEx.FindAllStringSubmatch(sizeQuantity, 1)
  1105. sizeInGiB := getNumericalValueFromResourceQuantity(sizeQuantity)
  1106. providerID := ""
  1107. if pv.Spec.CSI != nil {
  1108. providerID = pv.Spec.CSI.VolumeHandle
  1109. } else {
  1110. providerID = pv.Name // Looks like pv name is same as providerID in Alibaba k8s cluster
  1111. }
  1112. // Performance level being empty string gets defaulted in describePrice to PL1.
  1113. performanceLevel := ""
  1114. diskCategory := ""
  1115. if pv.Spec.CSI != nil {
  1116. if val, ok := pv.Spec.CSI.VolumeAttributes["performanceLevel"]; ok {
  1117. performanceLevel = val
  1118. }
  1119. if val, ok := pv.Spec.CSI.VolumeAttributes["type"]; ok {
  1120. diskCategory = val
  1121. }
  1122. }
  1123. // Highly unlikely that label pv.Spec.CSI.VolumeAttributes["type"] doesn't exist but if occured default to cloud (most basic disk type)
  1124. if diskCategory == "" {
  1125. diskCategory = ALIBABA_DISK_CLOUD_CATEGORY
  1126. }
  1127. return NewSlimK8sDisk(diskType, regionID, priceUnit, diskCategory, performanceLevel, providerID, pv.Spec.StorageClassName, sizeInGiB)
  1128. }
  1129. // 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!
  1130. // if topology.diskplugin.csi.alibabacloud.com/region label/annotation is passed during PV creation return that as the PV region.
  1131. // if topology.diskplugin.csi.alibabacloud.com/zone label/annotation is passed during PV creation determine the region based on this pv label.
  1132. // if neither of the above label/annotation is present check node affinity for the zone affinity and determine the region based on this zone.
  1133. // if nether of the above yields a region , return empty string to default it to cluster region.
  1134. func determinePVRegion(pv *v1.PersistentVolume) string {
  1135. // if "topology.diskplugin.csi.alibabacloud.com/region" is present as a label or annotation return that as the PV region
  1136. if val, ok := pv.Labels[ALIBABA_DISK_TOPOLOGY_REGION_LABEL]; ok {
  1137. log.Debugf("determinePVRegion returned a region value of: %s through label: %s for PV name: %s", val, ALIBABA_DISK_TOPOLOGY_REGION_LABEL, pv.Name)
  1138. return val
  1139. }
  1140. if val, ok := pv.Annotations[ALIBABA_DISK_TOPOLOGY_REGION_LABEL]; ok {
  1141. log.Debugf("determinePVRegion returned a region value of: %s through annotation: %s for PV name: %s", val, ALIBABA_DISK_TOPOLOGY_REGION_LABEL, pv.Name)
  1142. return val
  1143. }
  1144. // 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
  1145. var pvZone string
  1146. if val, ok := pv.Labels[ALIBABA_DISK_TOPOLOGY_ZONE_LABEL]; ok {
  1147. log.Debugf("determinePVRegion will set zone value to: %s through label: %s for PV name: %s", val, ALIBABA_DISK_TOPOLOGY_ZONE_LABEL, pv.Name)
  1148. pvZone = val
  1149. }
  1150. if pvZone == "" {
  1151. if val, ok := pv.Annotations[ALIBABA_DISK_TOPOLOGY_ZONE_LABEL]; ok {
  1152. log.Debugf("determinePVRegion will set zone value to: %s through annotation: %s for PV name: %s", val, ALIBABA_DISK_TOPOLOGY_ZONE_LABEL, pv.Name)
  1153. pvZone = val
  1154. }
  1155. }
  1156. if pvZone == "" {
  1157. // 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
  1158. // 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.
  1159. if pv.Spec.NodeAffinity != nil {
  1160. nodeAffinity := pv.Spec.NodeAffinity
  1161. if nodeAffinity.Required != nil && nodeAffinity.Required.NodeSelectorTerms != nil {
  1162. for _, nodeSelectorTerm := range nodeAffinity.Required.NodeSelectorTerms {
  1163. matchExpression := nodeSelectorTerm.MatchExpressions
  1164. for _, nodeSelectorRequirement := range matchExpression {
  1165. if nodeSelectorRequirement.Key == ALIBABA_DISK_TOPOLOGY_ZONE_LABEL {
  1166. 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)
  1167. pvZone = nodeSelectorRequirement.Values[0]
  1168. }
  1169. }
  1170. }
  1171. }
  1172. }
  1173. }
  1174. regionOverrides := env.GetRegionOverrideList()
  1175. regions := alibabaRegions
  1176. if len(regionOverrides) > 0 {
  1177. regions = regionOverrides
  1178. }
  1179. for _, region := range regions {
  1180. if strings.Contains(pvZone, region) {
  1181. log.Debugf("determinePVRegion determined region of %s through zone affiliation of the PV %s\n", region, pvZone)
  1182. return region
  1183. }
  1184. }
  1185. return ""
  1186. }
  1187. // PricingSourceSummary returns the pricing source summary for the provider.
  1188. // The summary represents what was _parsed_ from the pricing source, not
  1189. // everything that was _available_ in the pricing source.
  1190. func (a *Alibaba) PricingSourceSummary() interface{} {
  1191. return a.Pricing
  1192. }