Răsfoiți Sursa

Add Scaleway as a provider

Signed-off-by: Patrik Cyvoct <patrik@ptrk.io>
Patrik Cyvoct 3 ani în urmă
părinte
comite
0f0996a9cc
6 a modificat fișierele cu 229 adăugiri și 1 ștergeri
  1. 1 0
      go.mod
  2. 2 0
      go.sum
  3. 12 1
      pkg/cloud/provider.go
  4. 199 0
      pkg/cloud/scalewayprovider.go
  5. 5 0
      pkg/kubecost/assetprops.go
  6. 10 0
      pkg/util/compat.go

+ 1 - 0
go.mod

@@ -109,6 +109,7 @@ require (
 	github.com/pelletier/go-toml v1.9.3 // indirect
 	github.com/prometheus/procfs v0.0.2 // indirect
 	github.com/rs/xid v1.3.0 // indirect
+	github.com/scaleway/scaleway-sdk-go v1.0.0-beta.9 // indirect
 	github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 // indirect
 	github.com/sirupsen/logrus v1.8.1 // indirect
 	github.com/spf13/afero v1.6.0 // indirect

+ 2 - 0
go.sum

@@ -514,6 +514,8 @@ github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
 github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
+github.com/scaleway/scaleway-sdk-go v1.0.0-beta.9 h1:0roa6gXKgyta64uqh52AQG3wzZXH21unn+ltzQSXML0=
+github.com/scaleway/scaleway-sdk-go v1.0.0-beta.9/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg=
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
 github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
 github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 h1:pntxY8Ary0t43dCZ5dqY4YTJCObLY1kIXl0uzMv+7DE=

+ 12 - 1
pkg/cloud/provider.go

@@ -4,7 +4,6 @@ import (
 	"database/sql"
 	"errors"
 	"fmt"
-	"github.com/kubecost/opencost/pkg/kubecost"
 	"io"
 	"regexp"
 	"strconv"
@@ -12,6 +11,8 @@ import (
 	"sync"
 	"time"
 
+	"github.com/kubecost/opencost/pkg/kubecost"
+
 	"github.com/kubecost/opencost/pkg/util"
 
 	"cloud.google.com/go/compute/metadata"
@@ -475,6 +476,13 @@ func NewProvider(cache clustercache.ClusterCache, apiKey string, config *config.
 			clusterAccountId:     cp.accountID,
 			serviceAccountChecks: NewServiceAccountChecks(),
 		}, nil
+	case kubecost.ScalewayProvider:
+		log.Info("Found ProviderID starting with \"scaleway\", using Scaleway Provider")
+		return &Scaleway{
+			Clientset: cache,
+			Config:    NewProviderConfig(config, cp.configFileName),
+		}, nil
+
 	default:
 		log.Info("Unsupported provider, falling back to default")
 		return &CustomProvider{
@@ -513,6 +521,9 @@ func getClusterProperties(node *v1.Node) clusterProperties {
 		cp.provider = kubecost.AzureProvider
 		cp.configFileName = "azure.json"
 		cp.accountID = parseAzureSubscriptionID(providerID)
+	} else if strings.HasPrefix(providerID, "scaleway") {
+		cp.provider = kubecost.ScalewayProvider
+		cp.configFileName = "scaleway.json"
 	}
 	if env.IsUseCSVProvider() {
 		cp.provider = kubecost.CSVProvider

+ 199 - 0
pkg/cloud/scalewayprovider.go

@@ -0,0 +1,199 @@
+package cloud
+
+import (
+	"fmt"
+	"strings"
+	"sync"
+
+	"github.com/kubecost/opencost/pkg/clustercache"
+	"github.com/kubecost/opencost/pkg/util"
+
+	"github.com/kubecost/opencost/pkg/log"
+	v1 "k8s.io/api/core/v1"
+
+	"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
+	"github.com/scaleway/scaleway-sdk-go/scw"
+)
+
+type ScalewayPricing struct {
+	NodesInfos map[string]*instance.ServerType
+	PVCost     float64
+}
+
+type Scaleway struct {
+	Clientset               clustercache.ClusterCache
+	Config                  *ProviderConfig
+	Pricing                 map[string]*ScalewayPricing
+	DownloadPricingDataLock sync.RWMutex
+}
+
+func (c *Scaleway) DownloadPricingData() error {
+	c.DownloadPricingDataLock.Lock()
+	defer c.DownloadPricingDataLock.Unlock()
+
+	// TODO wait for an official Pricing API from Scaleway
+	// Let's use a static map and an old API
+
+	if len(c.Pricing) != 0 {
+		// Already initialized
+		return nil
+	}
+
+	// PV pricing per AZ
+	pvPrice := map[string]float64{
+		"fr-par-1": 0.00011,
+		"fr-par-2": 0.00011,
+		"fr-par-3": 0.00032,
+		"nl-ams-1": 0.00008,
+		"nl-ams-2": 0.00008,
+		"pl-waw-1": 0.00011,
+	}
+
+	c.Pricing = make(map[string]*ScalewayPricing)
+
+	client, err := scw.NewClient(scw.WithAuth("SCWXXXXXXXXXXXXXXXXX", "00000000-0000-0000-0000-000000000000"), scw.WithDefaultProjectID("00000000-0000-0000-0000-000000000000"))
+	if err != nil {
+		return err
+	}
+
+	instanceAPI := instance.NewAPI(client)
+
+	for _, zone := range scw.AllZones {
+		resp, err := instanceAPI.ListServersTypes(&instance.ListServersTypesRequest{Zone: zone})
+		if err != nil {
+			log.Errorf("Could not get Scaleway pricing data from instance API in zone %s: %+v", zone, err)
+			continue
+		}
+		c.Pricing[zone.String()] = &ScalewayPricing{
+			PVCost:     pvPrice[zone.String()],
+			NodesInfos: map[string]*instance.ServerType{},
+		}
+
+		for name, infos := range resp.Servers {
+			c.Pricing[zone.String()].NodesInfos[name] = infos
+		}
+	}
+
+	return nil
+}
+
+type scalewayKey struct {
+	Labels map[string]string
+}
+
+func (k *scalewayKey) Features() string {
+	instanceType, _ := util.GetInstanceType(k.Labels)
+	zone, _ := util.GetZone(k.Labels)
+
+	return zone + "," + instanceType
+}
+
+func (k *scalewayKey) GPUType() string {
+	instanceType, _ := util.GetInstanceType(k.Labels)
+	if strings.HasPrefix(instanceType, "RENDER") || strings.HasPrefix(instanceType, "GPU") {
+		return instanceType
+	}
+	return ""
+}
+func (k *scalewayKey) ID() string {
+	return ""
+}
+
+func (c *Scaleway) NodePricing(key Key) (*Node, error) {
+	// There is only the zone and the instance ID in the providerID, hence we must use the features
+	split := strings.Split(key.Features(), ",")
+	if pricing, ok := c.Pricing[split[0]]; ok {
+		if info, ok := pricing.NodesInfos[split[1]]; ok {
+			log.Infof("Using features:`%s`", key.Features())
+			return &Node{
+				Cost:        fmt.Sprintf("%f", info.HourlyPrice),
+				PricingType: DefaultPrices,
+				VCPU:        fmt.Sprintf("%d", info.Ncpus),
+				RAM:         fmt.Sprintf("%d", info.RAM),
+				// This is tricky, as instances can have local volumes or not
+				Storage:      fmt.Sprintf("%d", info.PerVolumeConstraint.LSSD.MinSize),
+				GPU:          fmt.Sprintf("%d", info.Gpu),
+				InstanceType: split[1],
+				Region:       split[0],
+				GPUName:      key.GPUType(),
+			}, nil
+
+		}
+
+	}
+	return nil, fmt.Errorf("Unable to find Node matching `%s`:`%s`", key.ID(), key.Features())
+}
+
+func (c *Scaleway) GetKey(l map[string]string, n *v1.Node) Key {
+	return &scalewayKey{
+		Labels: l,
+	}
+}
+
+type scalewayPVKey struct {
+	Labels                 map[string]string
+	StorageClassName       string
+	StorageClassParameters map[string]string
+	Name                   string
+	Zone                   string
+}
+
+func (key *scalewayPVKey) ID() string {
+	return ""
+}
+
+func (key *scalewayPVKey) GetStorageClass() string {
+	return key.StorageClassName
+}
+
+func (key *scalewayPVKey) Features() string {
+	// Only 1 type of PV for now
+	return key.Zone
+}
+
+func (c *Scaleway) GetPVKey(pv *v1.PersistentVolume, parameters map[string]string, defaultRegion string) PVKey {
+	// the csi volume handle is the form <az>/<volume-id>
+	zone := strings.Split(pv.Spec.CSI.VolumeHandle, "/")[0]
+	return &scalewayPVKey{
+		Labels:                 pv.Labels,
+		StorageClassName:       pv.Spec.StorageClassName,
+		StorageClassParameters: parameters,
+		Name:                   pv.Name,
+		Zone:                   zone,
+	}
+}
+
+func (c *Scaleway) PVPricing(pvk PVKey) (*PV, error) {
+	pricing, ok := c.Pricing[pvk.Features()]
+	if !ok {
+		log.Infof("Persistent Volume pricing not found for %s: %s", pvk.GetStorageClass(), pvk.Features())
+		return &PV{}, nil
+	}
+	return &PV{
+		Cost:  fmt.Sprintf("%f", pricing.PVCost),
+		Class: pvk.GetStorageClass(),
+	}, nil
+}
+
+func (c *Scaleway) ServiceAccountStatus() *ServiceAccountStatus {
+	return &ServiceAccountStatus{
+		Checks: []*ServiceAccountCheck{},
+	}
+}
+
+func (*Scaleway) ClusterManagementPricing() (string, float64, error) {
+	return "", 0.0, nil
+}
+
+func (c *Scaleway) CombinedDiscountForNode(instanceType string, isPreemptible bool, defaultDiscount, negotiatedDiscount float64) float64 {
+	return 1.0 - ((1.0 - defaultDiscount) * (1.0 - negotiatedDiscount))
+}
+
+func (c *Scaleway) Regions() []string {
+	// These are zones but hey, its 2022
+	zones := []string{}
+	for _, zone := range scw.AllZones {
+		zones = append(zones, zone.String())
+	}
+	return zones
+}

+ 5 - 0
pkg/kubecost/assetprops.go

@@ -102,6 +102,9 @@ const AzureProvider = "Azure"
 // CSVProvider describes the provider a CSV
 const CSVProvider = "CSV"
 
+// ScalewayProvider describes the provider Scaleway
+const ScalewayProvider = "Scaleway"
+
 // NilProvider describes unknown provider
 const NilProvider = "-"
 
@@ -118,6 +121,8 @@ func ParseProvider(str string) string {
 		return GCPProvider
 	case "azure":
 		return AzureProvider
+	case "scaleway", "scw", "kapsule":
+		return ScalewayProvider
 	default:
 		return NilProvider
 	}

+ 10 - 0
pkg/util/compat.go

@@ -6,6 +6,16 @@ import (
 
 // See https://kubernetes.io/docs/reference/labels-annotations-taints/
 
+func GetZone(labels map[string]string) (string, bool) {
+	if _, ok := labels[v1.LabelTopologyZone]; ok { // Label as of 1.17
+		return labels[v1.LabelTopologyZone], true
+	} else if _, ok := labels[v1.LabelZoneFailureDomain]; ok { // deprecated label
+		return labels[v1.LabelZoneFailureDomain], true
+	} else {
+		return "", false
+	}
+}
+
 func GetRegion(labels map[string]string) (string, bool) {
 	if _, ok := labels[v1.LabelTopologyRegion]; ok { // Label as of 1.17
 		return labels[v1.LabelTopologyRegion], true