scalewayprovider.go 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. package cloud
  2. import (
  3. "fmt"
  4. "io"
  5. "strconv"
  6. "strings"
  7. "sync"
  8. "time"
  9. "github.com/opencost/opencost/pkg/clustercache"
  10. "github.com/opencost/opencost/pkg/env"
  11. "github.com/opencost/opencost/pkg/util"
  12. "github.com/opencost/opencost/pkg/util/json"
  13. "github.com/opencost/opencost/pkg/log"
  14. v1 "k8s.io/api/core/v1"
  15. "github.com/scaleway/scaleway-sdk-go/api/instance/v1"
  16. "github.com/scaleway/scaleway-sdk-go/scw"
  17. )
  18. const (
  19. InstanceAPIPricing = "Instance API Pricing"
  20. )
  21. type ScalewayPricing struct {
  22. NodesInfos map[string]*instance.ServerType
  23. PVCost float64
  24. }
  25. type Scaleway struct {
  26. Clientset clustercache.ClusterCache
  27. Config *ProviderConfig
  28. Pricing map[string]*ScalewayPricing
  29. DownloadPricingDataLock sync.RWMutex
  30. }
  31. func (c *Scaleway) DownloadPricingData() error {
  32. c.DownloadPricingDataLock.Lock()
  33. defer c.DownloadPricingDataLock.Unlock()
  34. // TODO wait for an official Pricing API from Scaleway
  35. // Let's use a static map and an old API
  36. if len(c.Pricing) != 0 {
  37. // Already initialized
  38. return nil
  39. }
  40. // PV pricing per AZ
  41. pvPrice := map[string]float64{
  42. "fr-par-1": 0.00011,
  43. "fr-par-2": 0.00011,
  44. "fr-par-3": 0.00032,
  45. "nl-ams-1": 0.00008,
  46. "nl-ams-2": 0.00008,
  47. "pl-waw-1": 0.00011,
  48. }
  49. c.Pricing = make(map[string]*ScalewayPricing)
  50. // 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)
  51. client, err := scw.NewClient(scw.WithAuth("SCWXXXXXXXXXXXXXXXXX", "00000000-0000-0000-0000-000000000000"), scw.WithDefaultProjectID("00000000-0000-0000-0000-000000000000"))
  52. if err != nil {
  53. return err
  54. }
  55. instanceAPI := instance.NewAPI(client)
  56. for _, zone := range scw.AllZones {
  57. resp, err := instanceAPI.ListServersTypes(&instance.ListServersTypesRequest{Zone: zone})
  58. if err != nil {
  59. log.Errorf("Could not get Scaleway pricing data from instance API in zone %s: %+v", zone, err)
  60. continue
  61. }
  62. c.Pricing[zone.String()] = &ScalewayPricing{
  63. PVCost: pvPrice[zone.String()],
  64. NodesInfos: map[string]*instance.ServerType{},
  65. }
  66. for name, infos := range resp.Servers {
  67. c.Pricing[zone.String()].NodesInfos[name] = infos
  68. }
  69. }
  70. return nil
  71. }
  72. func (c *Scaleway) AllNodePricing() (interface{}, error) {
  73. c.DownloadPricingDataLock.RLock()
  74. defer c.DownloadPricingDataLock.RUnlock()
  75. return c.Pricing, nil
  76. }
  77. type scalewayKey struct {
  78. Labels map[string]string
  79. }
  80. func (k *scalewayKey) Features() string {
  81. instanceType, _ := util.GetInstanceType(k.Labels)
  82. zone, _ := util.GetZone(k.Labels)
  83. return zone + "," + instanceType
  84. }
  85. func (k *scalewayKey) GPUType() string {
  86. instanceType, _ := util.GetInstanceType(k.Labels)
  87. if strings.HasPrefix(instanceType, "RENDER") || strings.HasPrefix(instanceType, "GPU") {
  88. return instanceType
  89. }
  90. return ""
  91. }
  92. func (k *scalewayKey) ID() string {
  93. return ""
  94. }
  95. func (c *Scaleway) NodePricing(key Key) (*Node, error) {
  96. // There is only the zone and the instance ID in the providerID, hence we must use the features
  97. split := strings.Split(key.Features(), ",")
  98. if pricing, ok := c.Pricing[split[0]]; ok {
  99. if info, ok := pricing.NodesInfos[split[1]]; ok {
  100. return &Node{
  101. Cost: fmt.Sprintf("%f", info.HourlyPrice),
  102. PricingType: DefaultPrices,
  103. VCPU: fmt.Sprintf("%d", info.Ncpus),
  104. RAM: fmt.Sprintf("%d", info.RAM),
  105. // This is tricky, as instances can have local volumes or not
  106. Storage: fmt.Sprintf("%d", info.PerVolumeConstraint.LSSD.MinSize),
  107. GPU: fmt.Sprintf("%d", info.Gpu),
  108. InstanceType: split[1],
  109. Region: split[0],
  110. GPUName: key.GPUType(),
  111. }, nil
  112. }
  113. }
  114. return nil, fmt.Errorf("Unable to find Node matching `%s`:`%s`", key.ID(), key.Features())
  115. }
  116. func (c *Scaleway) LoadBalancerPricing() (*LoadBalancer, error) {
  117. // Different LB types, lets take the cheaper for now, we can't get the type
  118. // without a service specifying the type in the annotations
  119. return &LoadBalancer{
  120. Cost: 0.014,
  121. }, nil
  122. }
  123. func (c *Scaleway) NetworkPricing() (*Network, error) {
  124. // it's free baby!
  125. return &Network{
  126. ZoneNetworkEgressCost: 0,
  127. RegionNetworkEgressCost: 0,
  128. InternetNetworkEgressCost: 0,
  129. }, nil
  130. }
  131. func (c *Scaleway) GetKey(l map[string]string, n *v1.Node) Key {
  132. return &scalewayKey{
  133. Labels: l,
  134. }
  135. }
  136. type scalewayPVKey struct {
  137. Labels map[string]string
  138. StorageClassName string
  139. StorageClassParameters map[string]string
  140. Name string
  141. Zone string
  142. }
  143. func (key *scalewayPVKey) ID() string {
  144. return ""
  145. }
  146. func (key *scalewayPVKey) GetStorageClass() string {
  147. return key.StorageClassName
  148. }
  149. func (key *scalewayPVKey) Features() string {
  150. // Only 1 type of PV for now
  151. return key.Zone
  152. }
  153. func (c *Scaleway) GetPVKey(pv *v1.PersistentVolume, parameters map[string]string, defaultRegion string) PVKey {
  154. // the csi volume handle is the form <az>/<volume-id>
  155. zone := strings.Split(pv.Spec.CSI.VolumeHandle, "/")[0]
  156. return &scalewayPVKey{
  157. Labels: pv.Labels,
  158. StorageClassName: pv.Spec.StorageClassName,
  159. StorageClassParameters: parameters,
  160. Name: pv.Name,
  161. Zone: zone,
  162. }
  163. }
  164. func (c *Scaleway) PVPricing(pvk PVKey) (*PV, error) {
  165. pricing, ok := c.Pricing[pvk.Features()]
  166. if !ok {
  167. log.Infof("Persistent Volume pricing not found for %s: %s", pvk.GetStorageClass(), pvk.Features())
  168. return &PV{}, nil
  169. }
  170. return &PV{
  171. Cost: fmt.Sprintf("%f", pricing.PVCost),
  172. Class: pvk.GetStorageClass(),
  173. }, nil
  174. }
  175. func (c *Scaleway) ServiceAccountStatus() *ServiceAccountStatus {
  176. return &ServiceAccountStatus{
  177. Checks: []*ServiceAccountCheck{},
  178. }
  179. }
  180. func (*Scaleway) ClusterManagementPricing() (string, float64, error) {
  181. return "", 0.0, nil
  182. }
  183. func (c *Scaleway) CombinedDiscountForNode(instanceType string, isPreemptible bool, defaultDiscount, negotiatedDiscount float64) float64 {
  184. return 1.0 - ((1.0 - defaultDiscount) * (1.0 - negotiatedDiscount))
  185. }
  186. func (c *Scaleway) Regions() []string {
  187. // These are zones but hey, its 2022
  188. zones := []string{}
  189. for _, zone := range scw.AllZones {
  190. zones = append(zones, zone.String())
  191. }
  192. return zones
  193. }
  194. func (*Scaleway) ApplyReservedInstancePricing(map[string]*Node) {}
  195. func (*Scaleway) GetAddresses() ([]byte, error) {
  196. return nil, nil
  197. }
  198. func (*Scaleway) GetDisks() ([]byte, error) {
  199. return nil, nil
  200. }
  201. func (scw *Scaleway) ClusterInfo() (map[string]string, error) {
  202. remoteEnabled := env.IsRemoteEnabled()
  203. m := make(map[string]string)
  204. m["name"] = "Scaleway Cluster #1"
  205. c, err := scw.GetConfig()
  206. if err != nil {
  207. return nil, err
  208. }
  209. if c.ClusterName != "" {
  210. m["name"] = c.ClusterName
  211. }
  212. m["provider"] = "Scaleway"
  213. m["remoteReadEnabled"] = strconv.FormatBool(remoteEnabled)
  214. m["id"] = env.GetClusterID()
  215. return m, nil
  216. }
  217. func (c *Scaleway) UpdateConfigFromConfigMap(a map[string]string) (*CustomPricing, error) {
  218. return c.Config.UpdateFromMap(a)
  219. }
  220. func (c *Scaleway) UpdateConfig(r io.Reader, updateType string) (*CustomPricing, error) {
  221. defer c.DownloadPricingData()
  222. return c.Config.Update(func(c *CustomPricing) error {
  223. a := make(map[string]interface{})
  224. err := json.NewDecoder(r).Decode(&a)
  225. if err != nil {
  226. return err
  227. }
  228. for k, v := range a {
  229. kUpper := strings.Title(k) // Just so we consistently supply / receive the same values, uppercase the first letter.
  230. vstr, ok := v.(string)
  231. if ok {
  232. err := SetCustomPricingField(c, kUpper, vstr)
  233. if err != nil {
  234. return err
  235. }
  236. } else {
  237. return fmt.Errorf("type error while updating config for %s", kUpper)
  238. }
  239. }
  240. if env.IsRemoteEnabled() {
  241. err := UpdateClusterMeta(env.GetClusterID(), c.ClusterName)
  242. if err != nil {
  243. return err
  244. }
  245. }
  246. return nil
  247. })
  248. }
  249. func (scw *Scaleway) GetConfig() (*CustomPricing, error) {
  250. c, err := scw.Config.GetCustomPricingData()
  251. if err != nil {
  252. return nil, err
  253. }
  254. if c.Discount == "" {
  255. c.Discount = "0%"
  256. }
  257. if c.NegotiatedDiscount == "" {
  258. c.NegotiatedDiscount = "0%"
  259. }
  260. if c.CurrencyCode == "" {
  261. c.CurrencyCode = "EUR"
  262. }
  263. return c, nil
  264. }
  265. func (*Scaleway) GetLocalStorageQuery(window, offset time.Duration, rate bool, used bool) string {
  266. return ""
  267. }
  268. func (scw *Scaleway) GetManagementPlatform() (string, error) {
  269. nodes := scw.Clientset.GetAllNodes()
  270. if len(nodes) > 0 {
  271. n := nodes[0]
  272. if _, ok := n.Labels["k8s.scaleway.com/kapsule"]; ok {
  273. return "kapsule", nil
  274. }
  275. if _, ok := n.Labels["kops.k8s.io/instancegroup"]; ok {
  276. return "kops", nil
  277. }
  278. }
  279. return "", nil
  280. }
  281. func (c *Scaleway) PricingSourceStatus() map[string]*PricingSource {
  282. return map[string]*PricingSource{
  283. InstanceAPIPricing: &PricingSource{
  284. Name: InstanceAPIPricing,
  285. Enabled: true,
  286. Available: true,
  287. },
  288. }
  289. }