scalewayprovider.go 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. package cloud
  2. import (
  3. "fmt"
  4. "strings"
  5. "sync"
  6. "github.com/opencost/opencost/pkg/clustercache"
  7. "github.com/opencost/opencost/pkg/util"
  8. "github.com/opencost/opencost/pkg/log"
  9. v1 "k8s.io/api/core/v1"
  10. "github.com/scaleway/scaleway-sdk-go/api/instance/v1"
  11. "github.com/scaleway/scaleway-sdk-go/scw"
  12. )
  13. type ScalewayPricing struct {
  14. NodesInfos map[string]*instance.ServerType
  15. PVCost float64
  16. }
  17. type Scaleway struct {
  18. Clientset clustercache.ClusterCache
  19. Config *ProviderConfig
  20. Pricing map[string]*ScalewayPricing
  21. DownloadPricingDataLock sync.RWMutex
  22. }
  23. func (c *Scaleway) DownloadPricingData() error {
  24. c.DownloadPricingDataLock.Lock()
  25. defer c.DownloadPricingDataLock.Unlock()
  26. // TODO wait for an official Pricing API from Scaleway
  27. // Let's use a static map and an old API
  28. if len(c.Pricing) != 0 {
  29. // Already initialized
  30. return nil
  31. }
  32. // PV pricing per AZ
  33. pvPrice := map[string]float64{
  34. "fr-par-1": 0.00011,
  35. "fr-par-2": 0.00011,
  36. "fr-par-3": 0.00032,
  37. "nl-ams-1": 0.00008,
  38. "nl-ams-2": 0.00008,
  39. "pl-waw-1": 0.00011,
  40. }
  41. c.Pricing = make(map[string]*ScalewayPricing)
  42. // The endpoint we are trying to hit does not have authentication, but the scw client needs to be created with some default, hence these dummy values (regex parsing ftw)
  43. client, err := scw.NewClient(scw.WithAuth("SCWXXXXXXXXXXXXXXXXX", "00000000-0000-0000-0000-000000000000"), scw.WithDefaultProjectID("00000000-0000-0000-0000-000000000000"))
  44. if err != nil {
  45. return err
  46. }
  47. instanceAPI := instance.NewAPI(client)
  48. for _, zone := range scw.AllZones {
  49. resp, err := instanceAPI.ListServersTypes(&instance.ListServersTypesRequest{Zone: zone})
  50. if err != nil {
  51. log.Errorf("Could not get Scaleway pricing data from instance API in zone %s: %+v", zone, err)
  52. continue
  53. }
  54. c.Pricing[zone.String()] = &ScalewayPricing{
  55. PVCost: pvPrice[zone.String()],
  56. NodesInfos: map[string]*instance.ServerType{},
  57. }
  58. for name, infos := range resp.Servers {
  59. c.Pricing[zone.String()].NodesInfos[name] = infos
  60. }
  61. }
  62. return nil
  63. }
  64. type scalewayKey struct {
  65. Labels map[string]string
  66. }
  67. func (k *scalewayKey) Features() string {
  68. instanceType, _ := util.GetInstanceType(k.Labels)
  69. zone, _ := util.GetZone(k.Labels)
  70. return zone + "," + instanceType
  71. }
  72. func (k *scalewayKey) GPUType() string {
  73. instanceType, _ := util.GetInstanceType(k.Labels)
  74. if strings.HasPrefix(instanceType, "RENDER") || strings.HasPrefix(instanceType, "GPU") {
  75. return instanceType
  76. }
  77. return ""
  78. }
  79. func (k *scalewayKey) ID() string {
  80. return ""
  81. }
  82. func (c *Scaleway) NodePricing(key Key) (*Node, error) {
  83. // There is only the zone and the instance ID in the providerID, hence we must use the features
  84. split := strings.Split(key.Features(), ",")
  85. if pricing, ok := c.Pricing[split[0]]; ok {
  86. if info, ok := pricing.NodesInfos[split[1]]; ok {
  87. return &Node{
  88. Cost: fmt.Sprintf("%f", info.HourlyPrice),
  89. PricingType: DefaultPrices,
  90. VCPU: fmt.Sprintf("%d", info.Ncpus),
  91. RAM: fmt.Sprintf("%d", info.RAM),
  92. // This is tricky, as instances can have local volumes or not
  93. Storage: fmt.Sprintf("%d", info.PerVolumeConstraint.LSSD.MinSize),
  94. GPU: fmt.Sprintf("%d", info.Gpu),
  95. InstanceType: split[1],
  96. Region: split[0],
  97. GPUName: key.GPUType(),
  98. }, nil
  99. }
  100. }
  101. return nil, fmt.Errorf("Unable to find Node matching `%s`:`%s`", key.ID(), key.Features())
  102. }
  103. func (c *Scaleway) GetKey(l map[string]string, n *v1.Node) Key {
  104. return &scalewayKey{
  105. Labels: l,
  106. }
  107. }
  108. type scalewayPVKey struct {
  109. Labels map[string]string
  110. StorageClassName string
  111. StorageClassParameters map[string]string
  112. Name string
  113. Zone string
  114. }
  115. func (key *scalewayPVKey) ID() string {
  116. return ""
  117. }
  118. func (key *scalewayPVKey) GetStorageClass() string {
  119. return key.StorageClassName
  120. }
  121. func (key *scalewayPVKey) Features() string {
  122. // Only 1 type of PV for now
  123. return key.Zone
  124. }
  125. func (c *Scaleway) GetPVKey(pv *v1.PersistentVolume, parameters map[string]string, defaultRegion string) PVKey {
  126. // the csi volume handle is the form <az>/<volume-id>
  127. zone := strings.Split(pv.Spec.CSI.VolumeHandle, "/")[0]
  128. return &scalewayPVKey{
  129. Labels: pv.Labels,
  130. StorageClassName: pv.Spec.StorageClassName,
  131. StorageClassParameters: parameters,
  132. Name: pv.Name,
  133. Zone: zone,
  134. }
  135. }
  136. func (c *Scaleway) PVPricing(pvk PVKey) (*PV, error) {
  137. pricing, ok := c.Pricing[pvk.Features()]
  138. if !ok {
  139. log.Infof("Persistent Volume pricing not found for %s: %s", pvk.GetStorageClass(), pvk.Features())
  140. return &PV{}, nil
  141. }
  142. return &PV{
  143. Cost: fmt.Sprintf("%f", pricing.PVCost),
  144. Class: pvk.GetStorageClass(),
  145. }, nil
  146. }
  147. func (c *Scaleway) ServiceAccountStatus() *ServiceAccountStatus {
  148. return &ServiceAccountStatus{
  149. Checks: []*ServiceAccountCheck{},
  150. }
  151. }
  152. func (*Scaleway) ClusterManagementPricing() (string, float64, error) {
  153. return "", 0.0, nil
  154. }
  155. func (c *Scaleway) CombinedDiscountForNode(instanceType string, isPreemptible bool, defaultDiscount, negotiatedDiscount float64) float64 {
  156. return 1.0 - ((1.0 - defaultDiscount) * (1.0 - negotiatedDiscount))
  157. }
  158. func (c *Scaleway) Regions() []string {
  159. // These are zones but hey, its 2022
  160. zones := []string{}
  161. for _, zone := range scw.AllZones {
  162. zones = append(zones, zone.String())
  163. }
  164. return zones
  165. }