Просмотр исходного кода

Merge branch 'develop' into cncf-ip-policy

Matt Ray 3 лет назад
Родитель
Сommit
339d6aff4e

+ 23 - 1
pkg/cloud/aliyunprovider.go

@@ -620,6 +620,14 @@ func (alibaba *Alibaba) loadAlibabaAuthSecretAndSetEnv(force bool) error {
 
 // Regions returns a current supported list of Alibaba regions
 func (alibaba *Alibaba) Regions() []string {
+
+	regionOverrides := env.GetRegionOverrideList()
+
+	if len(regionOverrides) > 0 {
+		log.Debugf("Overriding Alibaba regions with configured region list: %+v", regionOverrides)
+		return regionOverrides
+	}
+
 	return alibabaRegions
 }
 
@@ -1323,7 +1331,14 @@ func determinePVRegion(pv *v1.PersistentVolume) string {
 		}
 	}
 
-	for _, region := range alibabaRegions {
+	regionOverrides := env.GetRegionOverrideList()
+	regions := alibabaRegions
+
+	if len(regionOverrides) > 0 {
+		regions = regionOverrides
+	}
+
+	for _, region := range regions {
 		if strings.Contains(pvZone, region) {
 			log.Debugf("determinePVRegion determined region of %s through zone affiliation of the PV %s\n", region, pvZone)
 			return region
@@ -1331,3 +1346,10 @@ func determinePVRegion(pv *v1.PersistentVolume) string {
 	}
 	return ""
 }
+
+// PricingSourceSummary returns the pricing source summary for the provider.
+// The summary represents what was _parsed_ from the pricing source, not
+// everything that was _available_ in the pricing source.
+func (a *Alibaba) PricingSourceSummary() interface{} {
+	return a.Pricing
+}

+ 29 - 9
pkg/cloud/awsprovider.go

@@ -114,7 +114,7 @@ func (aws *AWS) PricingSourceStatus() map[string]*PricingSource {
 
 }
 
-// How often spot data is refreshed
+// SpotRefreshDuration represents how much time must pass before we refresh
 const SpotRefreshDuration = 15 * time.Minute
 
 var awsRegions = []string{
@@ -1484,14 +1484,16 @@ func (aws *AWS) getAddressesForRegion(ctx context.Context, region string) (*ec2.
 func (aws *AWS) getAllAddresses() ([]*ec2Types.Address, error) {
 	aws.ConfigureAuth() // load authentication data into env vars
 
-	addressCh := make(chan *ec2.DescribeAddressesOutput, len(awsRegions))
-	errorCh := make(chan error, len(awsRegions))
+	regions := aws.Regions()
+
+	addressCh := make(chan *ec2.DescribeAddressesOutput, len(regions))
+	errorCh := make(chan error, len(regions))
 
 	var wg sync.WaitGroup
-	wg.Add(len(awsRegions))
+	wg.Add(len(regions))
 
 	// Get volumes from each AWS region
-	for _, r := range awsRegions {
+	for _, r := range regions {
 		// Fetch IP address response and send results and errors to their
 		// respective channels
 		go func(region string) {
@@ -1584,14 +1586,16 @@ func (aws *AWS) getDisksForRegion(ctx context.Context, region string, maxResults
 func (aws *AWS) getAllDisks() ([]*ec2Types.Volume, error) {
 	aws.ConfigureAuth() // load authentication data into env vars
 
-	volumeCh := make(chan *ec2.DescribeVolumesOutput, len(awsRegions))
-	errorCh := make(chan error, len(awsRegions))
+	regions := aws.Regions()
+
+	volumeCh := make(chan *ec2.DescribeVolumesOutput, len(regions))
+	errorCh := make(chan error, len(regions))
 
 	var wg sync.WaitGroup
-	wg.Add(len(awsRegions))
+	wg.Add(len(regions))
 
 	// Get volumes from each AWS region
-	for _, r := range awsRegions {
+	for _, r := range regions {
 		// Fetch volume response and send results and errors to their
 		// respective channels
 		go func(region string) {
@@ -2297,5 +2301,21 @@ func (aws *AWS) CombinedDiscountForNode(instanceType string, isPreemptible bool,
 
 // Regions returns a predefined list of AWS regions
 func (aws *AWS) Regions() []string {
+
+	regionOverrides := env.GetRegionOverrideList()
+
+	if len(regionOverrides) > 0 {
+		log.Debugf("Overriding AWS regions with configured region list: %+v", regionOverrides)
+		return regionOverrides
+	}
+
 	return awsRegions
 }
+
+// PricingSourceSummary returns the pricing source summary for the provider.
+// The summary represents what was _parsed_ from the pricing source, not
+// everything that was _available_ in the pricing source.
+func (aws *AWS) PricingSourceSummary() interface{} {
+	// encode the pricing source summary as a JSON string
+	return aws.Pricing
+}

+ 15 - 0
pkg/cloud/azureprovider.go

@@ -404,6 +404,13 @@ type Azure struct {
 	azureStorageConfig             *AzureStorageConfig
 }
 
+// PricingSourceSummary returns the pricing source summary for the provider.
+// The summary represents what was _parsed_ from the pricing source, not
+// everything that was _available_ in the pricing source.
+func (az *Azure) PricingSourceSummary() interface{} {
+	return az.Pricing
+}
+
 type azureKey struct {
 	Labels        map[string]string
 	GPULabel      string
@@ -1500,6 +1507,14 @@ func (az *Azure) CombinedDiscountForNode(instanceType string, isPreemptible bool
 }
 
 func (az *Azure) Regions() []string {
+
+	regionOverrides := env.GetRegionOverrideList()
+
+	if len(regionOverrides) > 0 {
+		log.Debugf("Overriding Azure regions with configured region list: %+v", regionOverrides)
+		return regionOverrides
+	}
+
 	return azureRegions
 }
 

+ 3 - 0
pkg/cloud/csvprovider.go

@@ -425,3 +425,6 @@ func (c *CSVProvider) Regions() []string {
 	return []string{}
 }
 
+func (c *CSVProvider) PricingSourceSummary() interface{} {
+	return c.Pricing
+}

+ 7 - 0
pkg/cloud/customprovider.go

@@ -34,6 +34,13 @@ type CustomProvider struct {
 	Config                  *ProviderConfig
 }
 
+// PricingSourceSummary returns the pricing source summary for the provider.
+// The summary represents what was _parsed_ from the pricing source, not what
+// was returned from the relevant API.
+func (cp *CustomProvider) PricingSourceSummary() interface{} {
+	return cp.Pricing
+}
+
 type customProviderKey struct {
 	SpotLabel      string
 	SpotLabelValue string

+ 16 - 1
pkg/cloud/gcpprovider.go

@@ -28,7 +28,7 @@ import (
 	"cloud.google.com/go/bigquery"
 	"cloud.google.com/go/compute/metadata"
 	"golang.org/x/oauth2/google"
-	compute "google.golang.org/api/compute/v1"
+	"google.golang.org/api/compute/v1"
 	v1 "k8s.io/api/core/v1"
 )
 
@@ -1538,6 +1538,14 @@ func (gcp *GCP) CombinedDiscountForNode(instanceType string, isPreemptible bool,
 }
 
 func (gcp *GCP) Regions() []string {
+
+	regionOverrides := env.GetRegionOverrideList()
+
+	if len(regionOverrides) > 0 {
+		log.Debugf("Overriding GCP regions with configured region list: %+v", regionOverrides)
+		return regionOverrides
+	}
+
 	return gcpRegions
 }
 
@@ -1577,3 +1585,10 @@ func getUsageType(labels map[string]string) string {
 	}
 	return "ondemand"
 }
+
+// PricingSourceSummary returns the pricing source summary for the provider.
+// The summary represents what was _parsed_ from the pricing source, not
+// everything that was _available_ in the pricing source.
+func (gcp *GCP) PricingSourceSummary() interface{} {
+	return gcp.Pricing
+}

+ 4 - 2
pkg/cloud/provider.go

@@ -346,6 +346,7 @@ type Provider interface {
 	ClusterManagementPricing() (string, float64, error)
 	CombinedDiscountForNode(string, bool, float64, float64) float64
 	Regions() []string
+	PricingSourceSummary() interface{}
 }
 
 // ClusterName returns the name defined in cluster info, defaulting to the
@@ -486,7 +487,7 @@ func NewProvider(cache clustercache.ClusterCache, apiKey string, config *config.
 			},
 		}, nil
 	case kubecost.GCPProvider:
-		log.Info("metadata reports we are in GCE")
+		log.Info("Found ProviderID starting with \"gce\", using GCP Provider")
 		if apiKey == "" {
 			return nil, errors.New("Supply a GCP Key to start getting data")
 		}
@@ -561,7 +562,8 @@ func getClusterProperties(node *v1.Node) clusterProperties {
 		accountID:      "",
 		projectID:      "",
 	}
-	if metadata.OnGCE() {
+	// The second conditional is mainly if you're running opencost outside of GCE, say in a local environment.
+	if metadata.OnGCE() || strings.HasPrefix(providerID, "gce") {
 		cp.provider = kubecost.GCPProvider
 		cp.configFileName = "gcp.json"
 		cp.projectID = parseGCPProjectID(providerID)

+ 17 - 1
pkg/cloud/scalewayprovider.go

@@ -3,13 +3,14 @@ package cloud
 import (
 	"errors"
 	"fmt"
-	"github.com/opencost/opencost/pkg/kubecost"
 	"io"
 	"strconv"
 	"strings"
 	"sync"
 	"time"
 
+	"github.com/opencost/opencost/pkg/kubecost"
+
 	"github.com/opencost/opencost/pkg/clustercache"
 	"github.com/opencost/opencost/pkg/env"
 	"github.com/opencost/opencost/pkg/util"
@@ -38,6 +39,13 @@ type Scaleway struct {
 	DownloadPricingDataLock sync.RWMutex
 }
 
+// PricingSourceSummary returns the pricing source summary for the provider.
+// The summary represents what was _parsed_ from the pricing source, not
+// everything that was _available_ in the pricing source.
+func (c *Scaleway) PricingSourceSummary() interface{} {
+	return c.Pricing
+}
+
 func (c *Scaleway) DownloadPricingData() error {
 	c.DownloadPricingDataLock.Lock()
 	defer c.DownloadPricingDataLock.Unlock()
@@ -234,6 +242,14 @@ func (c *Scaleway) CombinedDiscountForNode(instanceType string, isPreemptible bo
 }
 
 func (c *Scaleway) Regions() []string {
+
+	regionOverrides := env.GetRegionOverrideList()
+
+	if len(regionOverrides) > 0 {
+		log.Debugf("Overriding Scaleway regions with configured region list: %+v", regionOverrides)
+		return regionOverrides
+	}
+
 	// These are zones but hey, its 2022
 	zones := []string{}
 	for _, zone := range scw.AllZones {

+ 8 - 2
pkg/costmodel/costmodel.go

@@ -1107,12 +1107,15 @@ func (cm *CostModel) GetNodeCost(cp costAnalyzerCloud.Provider) (map[string]*cos
 					log.Errorf("Could not parse total node price")
 					return nil, err
 				}
-			} else {
+			} else if newCnode.VCPUCost != "" {
 				nodePrice, err = strconv.ParseFloat(newCnode.VCPUCost, 64) // all the price was allocated to the CPU
 				if err != nil {
 					log.Errorf("Could not parse node vcpu price")
 					return nil, err
 				}
+			} else { // add case to use default pricing model when API data fails.
+				log.Debugf("No node price or CPUprice found, falling back to default")
+				nodePrice = defaultCPU*cpu + defaultRAM*ram + gpuc*defaultGPU
 			}
 			if math.IsNaN(nodePrice) {
 				log.Warnf("nodePrice parsed as NaN. Setting to 0.")
@@ -1189,12 +1192,15 @@ func (cm *CostModel) GetNodeCost(cp costAnalyzerCloud.Provider) (map[string]*cos
 					}
 					nodePrice = nodePrice - gpuPrice // remove the gpuPrice from the total, we're just costing out RAM and CPU.
 				}
-			} else {
+			} else if newCnode.VCPUCost != "" {
 				nodePrice, err = strconv.ParseFloat(newCnode.VCPUCost, 64) // all the price was allocated to the CPU
 				if err != nil {
 					log.Warnf("Could not parse node vcpu price")
 					return nil, err
 				}
+			} else { // add case to use default pricing model when API data fails.
+				log.Debugf("No node price or CPUprice found, falling back to default")
+				nodePrice = defaultCPU*cpu + defaultRAM*ram
 			}
 			if math.IsNaN(nodePrice) {
 				log.Warnf("nodePrice parsed as NaN. Setting to 0.")

+ 9 - 0
pkg/costmodel/router.go

@@ -692,6 +692,14 @@ func (a *Accesses) GetPricingSourceCounts(w http.ResponseWriter, _ *http.Request
 	w.Write(WrapData(a.Model.GetPricingSourceCounts()))
 }
 
+func (a *Accesses) GetPricingSourceSummary(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
+	w.Header().Set("Content-Type", "application/json")
+	w.Header().Set("Access-Control-Allow-Origin", "*")
+
+	data := a.CloudProvider.PricingSourceSummary()
+	w.Write(WrapData(data, nil))
+}
+
 func (a *Accesses) GetPrometheusMetadata(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
 	w.Header().Set("Content-Type", "application/json")
 	w.Header().Set("Access-Control-Allow-Origin", "*")
@@ -1757,6 +1765,7 @@ func Initialize(additionalConfigWatchers ...*watcher.ConfigMapWatcher) *Accesses
 	a.Router.GET("/clusterInfoMap", a.GetClusterInfoMap)
 	a.Router.GET("/serviceAccountStatus", a.GetServiceAccountStatus)
 	a.Router.GET("/pricingSourceStatus", a.GetPricingSourceStatus)
+	a.Router.GET("/pricingSourceSummary", a.GetPricingSourceSummary)
 	a.Router.GET("/pricingSourceCounts", a.GetPricingSourceCounts)
 
 	// endpoints migrated from server

+ 12 - 0
pkg/env/costmodelenv.go

@@ -95,6 +95,8 @@ const (
 
 	AllocationNodeLabelsEnabled     = "ALLOCATION_NODE_LABELS_ENABLED"
 	AllocationNodeLabelsIncludeList = "ALLOCATION_NODE_LABELS_INCLUDE_LIST"
+
+	regionOverrideList = "REGION_OVERRIDE_LIST"
 )
 
 var offsetRegex = regexp.MustCompile(`^(\+|-)(\d\d):(\d\d)$`)
@@ -535,3 +537,13 @@ func GetAllocationNodeLabelsIncludeList() []string {
 
 	return list
 }
+
+func GetRegionOverrideList() []string {
+	regionList := GetList(regionOverrideList, ",")
+
+	if regionList == nil {
+		return []string{}
+	}
+
+	return regionList
+}

+ 39 - 4
pkg/kubecost/window.go

@@ -2,17 +2,17 @@ package kubecost
 
 import (
 	"bytes"
+	"encoding/json"
 	"fmt"
-	"github.com/opencost/opencost/pkg/log"
 	"math"
 	"regexp"
 	"strconv"
 	"time"
 
-	"github.com/opencost/opencost/pkg/util/timeutil"
-
 	"github.com/opencost/opencost/pkg/env"
+	"github.com/opencost/opencost/pkg/log"
 	"github.com/opencost/opencost/pkg/thanos"
+	"github.com/opencost/opencost/pkg/util/timeutil"
 )
 
 const (
@@ -479,7 +479,6 @@ func (w Window) IsOpen() bool {
 	return w.start == nil || w.end == nil
 }
 
-// TODO:CLEANUP make this unmarshalable (make Start and End public)
 func (w Window) MarshalJSON() ([]byte, error) {
 	buffer := bytes.NewBufferString("{")
 	if w.start != nil {
@@ -496,6 +495,42 @@ func (w Window) MarshalJSON() ([]byte, error) {
 	return buffer.Bytes(), nil
 }
 
+func (w *Window) UnmarshalJSON(bs []byte) error {
+	// Due to the behavior of our custom MarshalJSON, we unmarshal as strings
+	// and then manually handle the weird quoted "null" case.
+	type PubWindow struct {
+		Start string `json:"start"`
+		End   string `json:"end"`
+	}
+	var pw PubWindow
+	err := json.Unmarshal(bs, &pw)
+	if err != nil {
+		return fmt.Errorf("half unmarshal: %w", err)
+	}
+
+	var start *time.Time
+	var end *time.Time
+
+	if pw.Start != "null" {
+		t, err := time.Parse(time.RFC3339, pw.Start)
+		if err != nil {
+			return fmt.Errorf("parsing start as RFC3339: %w", err)
+		}
+		start = &t
+	}
+	if pw.End != "null" {
+		t, err := time.Parse(time.RFC3339, pw.End)
+		if err != nil {
+			return fmt.Errorf("parsing end as RFC3339: %w", err)
+		}
+		end = &t
+	}
+
+	w.start = start
+	w.end = end
+	return nil
+}
+
 func (w Window) Minutes() float64 {
 	if w.IsOpen() {
 		return math.Inf(1)

+ 45 - 1
pkg/kubecost/window_test.go

@@ -1,12 +1,15 @@
 package kubecost
 
 import (
+	"encoding/json"
 	"fmt"
-	"github.com/opencost/opencost/pkg/util/timeutil"
 	"strings"
 	"testing"
 	"time"
 
+	"github.com/google/go-cmp/cmp"
+	"github.com/opencost/opencost/pkg/util/timeutil"
+
 	"github.com/opencost/opencost/pkg/env"
 )
 
@@ -1146,3 +1149,44 @@ func TestWindow_GetWindowsForQueryWindow(t *testing.T) {
 		})
 	}
 }
+
+func TestMarshalUnmarshal(t *testing.T) {
+	t1 := time.Date(2023, 03, 11, 01, 29, 15, 0, time.UTC)
+	t2 := t1.Add(8 * time.Minute)
+	cases := []struct {
+		w Window
+	}{
+		{
+			w: NewClosedWindow(t1, t2),
+		},
+		{
+			w: NewWindow(&t1, nil),
+		},
+		{
+			w: NewWindow(nil, &t2),
+		},
+		{
+			w: NewWindow(nil, nil),
+		},
+	}
+
+	for _, c := range cases {
+		name := c.w.String()
+		t.Run(name, func(t *testing.T) {
+			marshaled, err := json.Marshal(c.w)
+			if err != nil {
+				t.Fatalf("marshaling: %s", err)
+			}
+
+			var unmarshaledW Window
+			err = json.Unmarshal(marshaled, &unmarshaledW)
+			if err != nil {
+				t.Fatalf("unmarshaling: %s", err)
+			}
+
+			if diff := cmp.Diff(c.w, unmarshaledW); len(diff) > 0 {
+				t.Errorf(diff)
+			}
+		})
+	}
+}