Procházet zdrojové kódy

Merge pull request #1275 from Sh4d1/scaleway

Add Scaleway as a provider
Ajay Tripathy před 3 roky
rodič
revize
0094cba41f
6 změnil soubory, kde provedl 377 přidání a 0 odebrání
  1. 1 0
      go.mod
  2. 2 0
      go.sum
  3. 11 0
      pkg/cloud/provider.go
  4. 348 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=

+ 11 - 0
pkg/cloud/provider.go

@@ -11,6 +11,7 @@ import (
 	"sync"
 	"time"
 
+
 	"github.com/opencost/opencost/pkg/kubecost"
 
 	"github.com/opencost/opencost/pkg/util"
@@ -482,6 +483,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{
@@ -520,6 +528,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") { // the scaleway provider ID looks like scaleway://instance/<instance_id>
+		cp.provider = kubecost.ScalewayProvider
+		cp.configFileName = "scaleway.json"
 	}
 	if env.IsUseCSVProvider() {
 		cp.provider = kubecost.CSVProvider

+ 348 - 0
pkg/cloud/scalewayprovider.go

@@ -0,0 +1,348 @@
+package cloud
+
+import (
+	"fmt"
+	"io"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/opencost/opencost/pkg/clustercache"
+	"github.com/opencost/opencost/pkg/env"
+	"github.com/opencost/opencost/pkg/util"
+	"github.com/opencost/opencost/pkg/util/json"
+
+	"github.com/opencost/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"
+)
+
+const (
+	InstanceAPIPricing = "Instance API Pricing"
+)
+
+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)
+
+	// The endpoint we are trying to hit does not have authentication
+	client, err := scw.NewClient(scw.WithoutAuth())
+	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
+}
+
+func (c *Scaleway) AllNodePricing() (interface{}, error) {
+	c.DownloadPricingDataLock.RLock()
+	defer c.DownloadPricingDataLock.RUnlock()
+	return c.Pricing, 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) {
+	c.DownloadPricingDataLock.RLock()
+	defer c.DownloadPricingDataLock.RUnlock()
+
+	// 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 {
+			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 pricing matching thes features `%s`", key.Features())
+}
+
+func (c *Scaleway) LoadBalancerPricing() (*LoadBalancer, error) {
+	// Different LB types, lets take the cheaper for now, we can't get the type
+	// without a service specifying the type in the annotations
+	return &LoadBalancer{
+		Cost: 0.014,
+	}, nil
+}
+
+func (c *Scaleway) NetworkPricing() (*Network, error) {
+	// it's free baby!
+	return &Network{
+		ZoneNetworkEgressCost:     0,
+		RegionNetworkEgressCost:   0,
+		InternetNetworkEgressCost: 0,
+	}, nil
+}
+
+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) {
+	c.DownloadPricingDataLock.RLock()
+	defer c.DownloadPricingDataLock.RUnlock()
+
+	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
+}
+
+func (*Scaleway) ApplyReservedInstancePricing(map[string]*Node) {}
+
+func (*Scaleway) GetAddresses() ([]byte, error) {
+	return nil, nil
+}
+
+func (*Scaleway) GetDisks() ([]byte, error) {
+	return nil, nil
+}
+
+func (scw *Scaleway) ClusterInfo() (map[string]string, error) {
+	remoteEnabled := env.IsRemoteEnabled()
+
+	m := make(map[string]string)
+	m["name"] = "Scaleway Cluster #1"
+	c, err := scw.GetConfig()
+	if err != nil {
+		return nil, err
+	}
+	if c.ClusterName != "" {
+		m["name"] = c.ClusterName
+	}
+	m["provider"] = "Scaleway"
+	m["remoteReadEnabled"] = strconv.FormatBool(remoteEnabled)
+	m["id"] = env.GetClusterID()
+	return m, nil
+
+}
+
+func (c *Scaleway) UpdateConfigFromConfigMap(a map[string]string) (*CustomPricing, error) {
+	return c.Config.UpdateFromMap(a)
+}
+
+func (c *Scaleway) UpdateConfig(r io.Reader, updateType string) (*CustomPricing, error) {
+	defer c.DownloadPricingData()
+
+	return c.Config.Update(func(c *CustomPricing) error {
+		a := make(map[string]interface{})
+		err := json.NewDecoder(r).Decode(&a)
+		if err != nil {
+			return err
+		}
+		for k, v := range a {
+			kUpper := strings.Title(k) // Just so we consistently supply / receive the same values, uppercase the first letter.
+			vstr, ok := v.(string)
+			if ok {
+				err := SetCustomPricingField(c, kUpper, vstr)
+				if err != nil {
+					return err
+				}
+			} else {
+				return fmt.Errorf("type error while updating config for %s", kUpper)
+			}
+		}
+
+		if env.IsRemoteEnabled() {
+			err := UpdateClusterMeta(env.GetClusterID(), c.ClusterName)
+			if err != nil {
+				return err
+			}
+		}
+
+		return nil
+	})
+}
+func (scw *Scaleway) GetConfig() (*CustomPricing, error) {
+	c, err := scw.Config.GetCustomPricingData()
+	if err != nil {
+		return nil, err
+	}
+	if c.Discount == "" {
+		c.Discount = "0%"
+	}
+	if c.NegotiatedDiscount == "" {
+		c.NegotiatedDiscount = "0%"
+	}
+	if c.CurrencyCode == "" {
+		c.CurrencyCode = "EUR"
+	}
+	return c, nil
+}
+
+func (*Scaleway) GetLocalStorageQuery(window, offset time.Duration, rate bool, used bool) string {
+	return ""
+}
+
+func (scw *Scaleway) GetManagementPlatform() (string, error) {
+	nodes := scw.Clientset.GetAllNodes()
+
+	if len(nodes) > 0 {
+		n := nodes[0]
+		if _, ok := n.Labels["k8s.scaleway.com/kapsule"]; ok {
+			return "kapsule", nil
+		}
+		if _, ok := n.Labels["kops.k8s.io/instancegroup"]; ok {
+			return "kops", nil
+		}
+	}
+	return "", nil
+}
+
+func (c *Scaleway) PricingSourceStatus() map[string]*PricingSource {
+	return map[string]*PricingSource{
+		InstanceAPIPricing: &PricingSource{
+			Name:      InstanceAPIPricing,
+			Enabled:   true,
+			Available: true,
+		},
+	}
+}

+ 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