provider.go 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. package stackit
  2. import (
  3. "errors"
  4. "fmt"
  5. "io"
  6. "strconv"
  7. "strings"
  8. "sync"
  9. coreenv "github.com/opencost/opencost/core/pkg/env"
  10. "github.com/opencost/opencost/pkg/cloud/models"
  11. "github.com/opencost/opencost/pkg/cloud/utils"
  12. "github.com/opencost/opencost/core/pkg/clustercache"
  13. "github.com/opencost/opencost/core/pkg/opencost"
  14. "github.com/opencost/opencost/core/pkg/util"
  15. "github.com/opencost/opencost/core/pkg/util/json"
  16. "github.com/opencost/opencost/pkg/env"
  17. "github.com/opencost/opencost/core/pkg/log"
  18. )
  19. const (
  20. StackitPIMPricingSource = "STACKIT PIM API Pricing"
  21. )
  22. type STACKIT struct {
  23. Clientset clustercache.ClusterCache
  24. Config models.ProviderConfig
  25. ClusterRegion string
  26. ClusterAccountID string
  27. DownloadPricingDataLock sync.RWMutex
  28. // PIM API pricing cache (protected by DownloadPricingDataLock)
  29. pimFlavors map[string]*pimFlavorPricing
  30. pimStorage map[string]*pimStoragePricing
  31. }
  32. func (s *STACKIT) PricingSourceSummary() interface{} {
  33. s.DownloadPricingDataLock.RLock()
  34. defer s.DownloadPricingDataLock.RUnlock()
  35. return s.pimFlavors
  36. }
  37. func (s *STACKIT) DownloadPricingData() error {
  38. s.DownloadPricingDataLock.Lock()
  39. defer s.DownloadPricingDataLock.Unlock()
  40. flavors, storage, err := downloadPIMPricing()
  41. if err != nil {
  42. return fmt.Errorf("STACKIT: failed to download pricing from PIM API: %w", err)
  43. }
  44. if len(flavors) == 0 {
  45. return fmt.Errorf("STACKIT: PIM API returned no VM flavor pricing data")
  46. }
  47. s.pimFlavors = flavors
  48. s.pimStorage = storage
  49. return nil
  50. }
  51. func (s *STACKIT) AllNodePricing() (interface{}, error) {
  52. s.DownloadPricingDataLock.RLock()
  53. defer s.DownloadPricingDataLock.RUnlock()
  54. return s.pimFlavors, nil
  55. }
  56. type stackitKey struct {
  57. Labels map[string]string
  58. }
  59. func (k *stackitKey) Features() string {
  60. instanceType, _ := util.GetInstanceType(k.Labels)
  61. zone, _ := util.GetZone(k.Labels)
  62. return zone + "," + instanceType
  63. }
  64. func (k *stackitKey) GPUCount() int {
  65. return 0
  66. }
  67. func (k *stackitKey) GPUType() string {
  68. return ""
  69. }
  70. func (k *stackitKey) ID() string {
  71. return ""
  72. }
  73. func (s *STACKIT) NodePricing(key models.Key) (*models.Node, models.PricingMetadata, error) {
  74. s.DownloadPricingDataLock.RLock()
  75. defer s.DownloadPricingDataLock.RUnlock()
  76. meta := models.PricingMetadata{}
  77. // Extract instance type from key features ("zone,instanceType")
  78. features := key.Features()
  79. parts := strings.Split(features, ",")
  80. instanceType := ""
  81. if len(parts) >= 2 {
  82. instanceType = parts[1]
  83. }
  84. pf, ok := s.pimFlavors[instanceType]
  85. if !ok {
  86. return nil, meta, fmt.Errorf("STACKIT: no pricing data found for instance type %q", instanceType)
  87. }
  88. ramBytes := int64(pf.RAMGB * 1024 * 1024 * 1024)
  89. return &models.Node{
  90. Cost: pf.HourlyCost,
  91. VCPU: fmt.Sprintf("%d", pf.VCPU),
  92. RAM: fmt.Sprintf("%g", pf.RAMGB),
  93. RAMBytes: fmt.Sprintf("%d", ramBytes),
  94. GPU: fmt.Sprintf("%d", pf.GPUCount),
  95. GPUName: pf.GPUType,
  96. InstanceType: instanceType,
  97. Region: s.ClusterRegion,
  98. PricingType: models.DefaultPrices,
  99. }, meta, nil
  100. }
  101. func (s *STACKIT) LoadBalancerPricing() (*models.LoadBalancer, error) {
  102. config, err := s.GetConfig()
  103. if err != nil {
  104. return nil, fmt.Errorf("unable to get config: %w", err)
  105. }
  106. lbPrice := 0.0
  107. if config.DefaultLBPrice != "" {
  108. lbPrice, _ = strconv.ParseFloat(config.DefaultLBPrice, 64)
  109. }
  110. return &models.LoadBalancer{
  111. Cost: lbPrice,
  112. }, nil
  113. }
  114. func (s *STACKIT) NetworkPricing() (*models.Network, error) {
  115. config, err := s.GetConfig()
  116. if err != nil {
  117. return nil, fmt.Errorf("unable to get config: %w", err)
  118. }
  119. zoneEgress, _ := strconv.ParseFloat(config.ZoneNetworkEgress, 64)
  120. regionEgress, _ := strconv.ParseFloat(config.RegionNetworkEgress, 64)
  121. internetEgress, _ := strconv.ParseFloat(config.InternetNetworkEgress, 64)
  122. natEgress, _ := strconv.ParseFloat(config.NatGatewayEgress, 64)
  123. natIngress, _ := strconv.ParseFloat(config.NatGatewayIngress, 64)
  124. return &models.Network{
  125. ZoneNetworkEgressCost: zoneEgress,
  126. RegionNetworkEgressCost: regionEgress,
  127. InternetNetworkEgressCost: internetEgress,
  128. NatGatewayEgressCost: natEgress,
  129. NatGatewayIngressCost: natIngress,
  130. }, nil
  131. }
  132. func (s *STACKIT) GetKey(l map[string]string, n *clustercache.Node) models.Key {
  133. return &stackitKey{
  134. Labels: l,
  135. }
  136. }
  137. type stackitPVKey struct {
  138. Labels map[string]string
  139. StorageClassName string
  140. StorageClassParameters map[string]string
  141. Name string
  142. Zone string
  143. }
  144. func (key *stackitPVKey) ID() string {
  145. return ""
  146. }
  147. func (key *stackitPVKey) GetStorageClass() string {
  148. return key.StorageClassName
  149. }
  150. func (key *stackitPVKey) Features() string {
  151. return key.Zone
  152. }
  153. func (s *STACKIT) GetPVKey(pv *clustercache.PersistentVolume, parameters map[string]string, defaultRegion string) models.PVKey {
  154. zone := defaultRegion
  155. if pv.Spec.CSI != nil {
  156. parts := strings.Split(pv.Spec.CSI.VolumeHandle, "/")
  157. if len(parts) >= 2 && parts[0] != "" {
  158. zone = parts[0]
  159. }
  160. }
  161. return &stackitPVKey{
  162. Labels: pv.Labels,
  163. StorageClassName: pv.Spec.StorageClassName,
  164. StorageClassParameters: parameters,
  165. Name: pv.Name,
  166. Zone: zone,
  167. }
  168. }
  169. func (s *STACKIT) GpuPricing(nodeLabels map[string]string) (string, error) {
  170. s.DownloadPricingDataLock.RLock()
  171. defer s.DownloadPricingDataLock.RUnlock()
  172. instanceType, _ := util.GetInstanceType(nodeLabels)
  173. pf, ok := s.pimFlavors[instanceType]
  174. if !ok || pf.GPUCount == 0 || pf.HourlyCost == "" {
  175. return "", nil
  176. }
  177. hourlyCost, err := strconv.ParseFloat(pf.HourlyCost, 64)
  178. if err != nil {
  179. return "", fmt.Errorf("parsing STACKIT GPU hourly cost %q for %q: %w", pf.HourlyCost, instanceType, err)
  180. }
  181. perGPUCost := hourlyCost / float64(pf.GPUCount)
  182. return strconv.FormatFloat(perGPUCost, 'f', -1, 64), nil
  183. }
  184. func (s *STACKIT) PVPricing(pvk models.PVKey) (*models.PV, error) {
  185. s.DownloadPricingDataLock.RLock()
  186. defer s.DownloadPricingDataLock.RUnlock()
  187. storageClass := pvk.GetStorageClass()
  188. if len(s.pimStorage) > 0 {
  189. // Exact storage class match
  190. if sp, ok := s.pimStorage[storageClass]; ok {
  191. return &models.PV{
  192. Cost: sp.CostPerGBHr,
  193. Class: storageClass,
  194. }, nil
  195. }
  196. // Default to cheapest capacity-based storage
  197. if sp, ok := s.pimStorage["default"]; ok {
  198. return &models.PV{
  199. Cost: sp.CostPerGBHr,
  200. Class: storageClass,
  201. }, nil
  202. }
  203. }
  204. log.Debugf("STACKIT: no PV pricing found for storage class %q", storageClass)
  205. return &models.PV{}, nil
  206. }
  207. func (s *STACKIT) ServiceAccountStatus() *models.ServiceAccountStatus {
  208. return &models.ServiceAccountStatus{
  209. Checks: []*models.ServiceAccountCheck{},
  210. }
  211. }
  212. func (*STACKIT) ClusterManagementPricing() (string, float64, error) {
  213. return "", 0.0, nil
  214. }
  215. func (s *STACKIT) CombinedDiscountForNode(instanceType string, isPreemptible bool, defaultDiscount, negotiatedDiscount float64) float64 {
  216. return 1.0 - ((1.0 - defaultDiscount) * (1.0 - negotiatedDiscount))
  217. }
  218. func (s *STACKIT) Regions() []string {
  219. regionOverrides := env.GetRegionOverrideList()
  220. if len(regionOverrides) > 0 {
  221. log.Debugf("Overriding STACKIT regions with configured region list: %+v", regionOverrides)
  222. return regionOverrides
  223. }
  224. return []string{"eu01"}
  225. }
  226. func (*STACKIT) ApplyReservedInstancePricing(map[string]*models.Node) {}
  227. func (*STACKIT) GetAddresses() ([]byte, error) {
  228. return nil, nil
  229. }
  230. func (*STACKIT) GetDisks() ([]byte, error) {
  231. return nil, nil
  232. }
  233. func (*STACKIT) GetOrphanedResources() ([]models.OrphanedResource, error) {
  234. return nil, errors.New("not implemented")
  235. }
  236. func (s *STACKIT) ClusterInfo() (map[string]string, error) {
  237. remoteEnabled := env.IsRemoteEnabled()
  238. m := make(map[string]string)
  239. m["name"] = "STACKIT Cluster #1"
  240. c, err := s.GetConfig()
  241. if err != nil {
  242. return nil, err
  243. }
  244. if c.ClusterName != "" {
  245. m["name"] = c.ClusterName
  246. }
  247. m["provider"] = opencost.STACKITProvider
  248. m["region"] = s.ClusterRegion
  249. m["account"] = s.ClusterAccountID
  250. m["remoteReadEnabled"] = strconv.FormatBool(remoteEnabled)
  251. m["id"] = coreenv.GetClusterID()
  252. return m, nil
  253. }
  254. func (s *STACKIT) UpdateConfigFromConfigMap(a map[string]string) (*models.CustomPricing, error) {
  255. return s.Config.UpdateFromMap(a)
  256. }
  257. func (s *STACKIT) UpdateConfig(r io.Reader, updateType string) (*models.CustomPricing, error) {
  258. cp, err := s.Config.Update(func(c *models.CustomPricing) error {
  259. a := make(map[string]interface{})
  260. err := json.NewDecoder(r).Decode(&a)
  261. if err != nil {
  262. return err
  263. }
  264. for k, v := range a {
  265. kUpper := utils.ToTitle.String(k)
  266. vstr, ok := v.(string)
  267. if ok {
  268. err := models.SetCustomPricingField(c, kUpper, vstr)
  269. if err != nil {
  270. return fmt.Errorf("error setting custom pricing field: %w", err)
  271. }
  272. } else {
  273. return fmt.Errorf("type error while updating config for %s", kUpper)
  274. }
  275. }
  276. if env.IsRemoteEnabled() {
  277. err := utils.UpdateClusterMeta(coreenv.GetClusterID(), c.ClusterName)
  278. if err != nil {
  279. return err
  280. }
  281. }
  282. return nil
  283. })
  284. if err != nil {
  285. return cp, err
  286. }
  287. if refreshErr := s.DownloadPricingData(); refreshErr != nil {
  288. log.Warnf("STACKIT: failed to refresh pricing after config update: %v", refreshErr)
  289. }
  290. return cp, nil
  291. }
  292. func (s *STACKIT) GetConfig() (*models.CustomPricing, error) {
  293. c, err := s.Config.GetCustomPricingData()
  294. if err != nil {
  295. return nil, err
  296. }
  297. if c.Discount == "" {
  298. c.Discount = "0%"
  299. }
  300. if c.NegotiatedDiscount == "" {
  301. c.NegotiatedDiscount = "0%"
  302. }
  303. if c.CurrencyCode == "" {
  304. c.CurrencyCode = "EUR"
  305. }
  306. return c, nil
  307. }
  308. func (s *STACKIT) GetManagementPlatform() (string, error) {
  309. nodes := s.Clientset.GetAllNodes()
  310. if len(nodes) > 0 {
  311. n := nodes[0]
  312. if _, ok := n.Labels["node.stackit.cloud/ske"]; ok {
  313. return "ske", nil
  314. }
  315. }
  316. return "", nil
  317. }
  318. func (s *STACKIT) PricingSourceStatus() map[string]*models.PricingSource {
  319. s.DownloadPricingDataLock.RLock()
  320. defer s.DownloadPricingDataLock.RUnlock()
  321. return map[string]*models.PricingSource{
  322. StackitPIMPricingSource: {
  323. Name: StackitPIMPricingSource,
  324. Enabled: true,
  325. Available: len(s.pimFlavors) > 0,
  326. },
  327. }
  328. }