|
|
@@ -1,4 +1,4 @@
|
|
|
-package cloud
|
|
|
+package gcp
|
|
|
|
|
|
import (
|
|
|
"context"
|
|
|
@@ -14,6 +14,9 @@ import (
|
|
|
"sync"
|
|
|
"time"
|
|
|
|
|
|
+ "github.com/opencost/opencost/pkg/cloud/aws"
|
|
|
+ "github.com/opencost/opencost/pkg/cloud/models"
|
|
|
+ "github.com/opencost/opencost/pkg/cloud/utils"
|
|
|
"github.com/opencost/opencost/pkg/kubecost"
|
|
|
|
|
|
"github.com/opencost/opencost/pkg/clustercache"
|
|
|
@@ -27,14 +30,14 @@ import (
|
|
|
|
|
|
"cloud.google.com/go/bigquery"
|
|
|
"cloud.google.com/go/compute/metadata"
|
|
|
- "golang.org/x/oauth2"
|
|
|
"golang.org/x/oauth2/google"
|
|
|
- compute "google.golang.org/api/compute/v1"
|
|
|
+ "google.golang.org/api/compute/v1"
|
|
|
v1 "k8s.io/api/core/v1"
|
|
|
)
|
|
|
|
|
|
const GKE_GPU_TAG = "cloud.google.com/gke-accelerator"
|
|
|
const BigqueryUpdateType = "bigqueryupdate"
|
|
|
+const BillingAPIURLFmt = "https://cloudbilling.googleapis.com/v1/services/6F81-5844-456A/skus?key=%s¤cyCode=%s"
|
|
|
|
|
|
const (
|
|
|
GCPHourlyPublicIPCost = 0.01
|
|
|
@@ -98,15 +101,15 @@ type GCP struct {
|
|
|
BillingDataDataset string
|
|
|
DownloadPricingDataLock sync.RWMutex
|
|
|
ReservedInstances []*GCPReservedInstance
|
|
|
- Config *ProviderConfig
|
|
|
+ Config models.ProviderConfig
|
|
|
ServiceKeyProvided bool
|
|
|
ValidPricingKeys map[string]bool
|
|
|
- metadataClient *metadata.Client
|
|
|
+ MetadataClient *metadata.Client
|
|
|
clusterManagementPrice float64
|
|
|
- clusterProjectId string
|
|
|
- clusterRegion string
|
|
|
+ ClusterRegion string
|
|
|
+ ClusterAccountID string
|
|
|
+ ClusterProjectID string
|
|
|
clusterProvisioner string
|
|
|
- *CustomProvider
|
|
|
}
|
|
|
|
|
|
type gcpAllocation struct {
|
|
|
@@ -137,11 +140,11 @@ func (gcp *GCP) GetLocalStorageQuery(window, offset time.Duration, rate bool, us
|
|
|
fmtOffset := timeutil.DurationToPromOffsetString(offset)
|
|
|
|
|
|
fmtCumulativeQuery := `sum(
|
|
|
- sum_over_time(%s{device!="tmpfs", id="/"}[%s:1m]%s)
|
|
|
+ sum_over_time(%s{device!="tmpfs", id="/", %s}[%s:1m]%s)
|
|
|
) by (%s) / 60 / 730 / 1024 / 1024 / 1024 * %f`
|
|
|
|
|
|
fmtMonthlyQuery := `sum(
|
|
|
- avg_over_time(%s{device!="tmpfs", id="/"}[%s:1m]%s)
|
|
|
+ avg_over_time(%s{device!="tmpfs", id="/", %s}[%s:1m]%s)
|
|
|
) by (%s) / 1024 / 1024 / 1024 * %f`
|
|
|
|
|
|
fmtQuery := fmtCumulativeQuery
|
|
|
@@ -150,10 +153,10 @@ func (gcp *GCP) GetLocalStorageQuery(window, offset time.Duration, rate bool, us
|
|
|
}
|
|
|
fmtWindow := timeutil.DurationString(window)
|
|
|
|
|
|
- return fmt.Sprintf(fmtQuery, baseMetric, fmtWindow, fmtOffset, env.GetPromClusterLabel(), localStorageCost)
|
|
|
+ return fmt.Sprintf(fmtQuery, baseMetric, env.GetPromClusterFilter(), fmtWindow, fmtOffset, env.GetPromClusterLabel(), localStorageCost)
|
|
|
}
|
|
|
|
|
|
-func (gcp *GCP) GetConfig() (*CustomPricing, error) {
|
|
|
+func (gcp *GCP) GetConfig() (*models.CustomPricing, error) {
|
|
|
c, err := gcp.Config.GetCustomPricingData()
|
|
|
if err != nil {
|
|
|
return nil, err
|
|
|
@@ -168,12 +171,13 @@ func (gcp *GCP) GetConfig() (*CustomPricing, error) {
|
|
|
c.CurrencyCode = "USD"
|
|
|
}
|
|
|
if c.ShareTenancyCosts == "" {
|
|
|
- c.ShareTenancyCosts = defaultShareTenancyCost
|
|
|
+ c.ShareTenancyCosts = models.DefaultShareTenancyCost
|
|
|
}
|
|
|
return c, nil
|
|
|
}
|
|
|
|
|
|
// BigQueryConfig contain the required config and credentials to access OOC resources for GCP
|
|
|
+// Deprecated: v1.104 Use BigQueryConfiguration instead
|
|
|
type BigQueryConfig struct {
|
|
|
ProjectID string `json:"projectID"`
|
|
|
BillingDataDataset string `json:"billingDataDataset"`
|
|
|
@@ -210,7 +214,7 @@ func (*GCP) loadGCPAuthSecret() {
|
|
|
return
|
|
|
}
|
|
|
|
|
|
- exists, err := fileutil.FileExists(authSecretPath)
|
|
|
+ exists, err := fileutil.FileExists(models.AuthSecretPath)
|
|
|
if !exists || err != nil {
|
|
|
errMessage := "Secret does not exist"
|
|
|
if err != nil {
|
|
|
@@ -221,7 +225,7 @@ func (*GCP) loadGCPAuthSecret() {
|
|
|
return
|
|
|
}
|
|
|
|
|
|
- result, err := os.ReadFile(authSecretPath)
|
|
|
+ result, err := os.ReadFile(models.AuthSecretPath)
|
|
|
if err != nil {
|
|
|
log.Warnf("Failed to load auth secret, or was not mounted: %s", err.Error())
|
|
|
return
|
|
|
@@ -233,12 +237,12 @@ func (*GCP) loadGCPAuthSecret() {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-func (gcp *GCP) UpdateConfigFromConfigMap(a map[string]string) (*CustomPricing, error) {
|
|
|
+func (gcp *GCP) UpdateConfigFromConfigMap(a map[string]string) (*models.CustomPricing, error) {
|
|
|
return gcp.Config.UpdateFromMap(a)
|
|
|
}
|
|
|
|
|
|
-func (gcp *GCP) UpdateConfig(r io.Reader, updateType string) (*CustomPricing, error) {
|
|
|
- return gcp.Config.Update(func(c *CustomPricing) error {
|
|
|
+func (gcp *GCP) UpdateConfig(r io.Reader, updateType string) (*models.CustomPricing, error) {
|
|
|
+ return gcp.Config.Update(func(c *models.CustomPricing) error {
|
|
|
if updateType == BigqueryUpdateType {
|
|
|
a := BigQueryConfig{}
|
|
|
err := json.NewDecoder(r).Decode(&a)
|
|
|
@@ -264,8 +268,8 @@ func (gcp *GCP) UpdateConfig(r io.Reader, updateType string) (*CustomPricing, er
|
|
|
}
|
|
|
gcp.ServiceKeyProvided = true
|
|
|
}
|
|
|
- } else if updateType == AthenaInfoUpdateType {
|
|
|
- a := AwsAthenaInfo{}
|
|
|
+ } else if updateType == aws.AthenaInfoUpdateType {
|
|
|
+ a := aws.AwsAthenaInfo{}
|
|
|
err := json.NewDecoder(r).Decode(&a)
|
|
|
if err != nil {
|
|
|
return err
|
|
|
@@ -273,6 +277,7 @@ func (gcp *GCP) UpdateConfig(r io.Reader, updateType string) (*CustomPricing, er
|
|
|
c.AthenaBucketName = a.AthenaBucketName
|
|
|
c.AthenaRegion = a.AthenaRegion
|
|
|
c.AthenaDatabase = a.AthenaDatabase
|
|
|
+ c.AthenaCatalog = a.AthenaCatalog
|
|
|
c.AthenaTable = a.AthenaTable
|
|
|
c.AthenaWorkgroup = a.AthenaWorkgroup
|
|
|
c.ServiceKeyName = a.ServiceKeyName
|
|
|
@@ -285,10 +290,10 @@ func (gcp *GCP) UpdateConfig(r io.Reader, updateType string) (*CustomPricing, er
|
|
|
return err
|
|
|
}
|
|
|
for k, v := range a {
|
|
|
- kUpper := strings.Title(k) // Just so we consistently supply / receive the same values, uppercase the first letter.
|
|
|
+ kUpper := utils.ToTitle.String(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)
|
|
|
+ err := models.SetCustomPricingField(c, kUpper, vstr)
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
@@ -299,7 +304,7 @@ func (gcp *GCP) UpdateConfig(r io.Reader, updateType string) (*CustomPricing, er
|
|
|
}
|
|
|
|
|
|
if env.IsRemoteEnabled() {
|
|
|
- err := UpdateClusterMeta(env.GetClusterID(), c.ClusterName)
|
|
|
+ err := utils.UpdateClusterMeta(env.GetClusterID(), c.ClusterName)
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
@@ -313,7 +318,7 @@ func (gcp *GCP) UpdateConfig(r io.Reader, updateType string) (*CustomPricing, er
|
|
|
func (gcp *GCP) ClusterInfo() (map[string]string, error) {
|
|
|
remoteEnabled := env.IsRemoteEnabled()
|
|
|
|
|
|
- attribute, err := gcp.metadataClient.InstanceAttributeValue("cluster-name")
|
|
|
+ attribute, err := gcp.MetadataClient.InstanceAttributeValue("cluster-name")
|
|
|
if err != nil {
|
|
|
log.Infof("Error loading metadata cluster-name: %s", err.Error())
|
|
|
}
|
|
|
@@ -334,8 +339,9 @@ func (gcp *GCP) ClusterInfo() (map[string]string, error) {
|
|
|
m := make(map[string]string)
|
|
|
m["name"] = attribute
|
|
|
m["provider"] = kubecost.GCPProvider
|
|
|
- m["project"] = gcp.clusterProjectId
|
|
|
- m["region"] = gcp.clusterRegion
|
|
|
+ m["region"] = gcp.ClusterRegion
|
|
|
+ m["account"] = gcp.ClusterAccountID
|
|
|
+ m["project"] = gcp.ClusterProjectID
|
|
|
m["provisioner"] = gcp.clusterProvisioner
|
|
|
m["id"] = env.GetClusterID()
|
|
|
m["remoteReadEnabled"] = strconv.FormatBool(remoteEnabled)
|
|
|
@@ -347,12 +353,12 @@ func (gcp *GCP) ClusterManagementPricing() (string, float64, error) {
|
|
|
}
|
|
|
|
|
|
func (gcp *GCP) getAllAddresses() (*compute.AddressAggregatedList, error) {
|
|
|
- projID, err := gcp.metadataClient.ProjectID()
|
|
|
+ projID, err := gcp.MetadataClient.ProjectID()
|
|
|
if err != nil {
|
|
|
return nil, err
|
|
|
}
|
|
|
|
|
|
- client, err := google.DefaultClient(oauth2.NoContext,
|
|
|
+ client, err := google.DefaultClient(context.TODO(),
|
|
|
"https://www.googleapis.com/auth/compute.readonly")
|
|
|
if err != nil {
|
|
|
return nil, err
|
|
|
@@ -387,12 +393,12 @@ func (gcp *GCP) isAddressOrphaned(address *compute.Address) bool {
|
|
|
}
|
|
|
|
|
|
func (gcp *GCP) getAllDisks() (*compute.DiskAggregatedList, error) {
|
|
|
- projID, err := gcp.metadataClient.ProjectID()
|
|
|
+ projID, err := gcp.MetadataClient.ProjectID()
|
|
|
if err != nil {
|
|
|
return nil, err
|
|
|
}
|
|
|
|
|
|
- client, err := google.DefaultClient(oauth2.NoContext,
|
|
|
+ client, err := google.DefaultClient(context.TODO(),
|
|
|
"https://www.googleapis.com/auth/compute.readonly")
|
|
|
if err != nil {
|
|
|
return nil, err
|
|
|
@@ -443,7 +449,7 @@ func (gcp *GCP) isDiskOrphaned(disk *compute.Disk) (bool, error) {
|
|
|
return true, nil
|
|
|
}
|
|
|
|
|
|
-func (gcp *GCP) GetOrphanedResources() ([]OrphanedResource, error) {
|
|
|
+func (gcp *GCP) GetOrphanedResources() ([]models.OrphanedResource, error) {
|
|
|
disks, err := gcp.getAllDisks()
|
|
|
if err != nil {
|
|
|
return nil, err
|
|
|
@@ -454,7 +460,7 @@ func (gcp *GCP) GetOrphanedResources() ([]OrphanedResource, error) {
|
|
|
return nil, err
|
|
|
}
|
|
|
|
|
|
- var orphanedResources []OrphanedResource
|
|
|
+ var orphanedResources []models.OrphanedResource
|
|
|
|
|
|
for _, diskList := range disks.Items {
|
|
|
if len(diskList.Disks) == 0 {
|
|
|
@@ -487,7 +493,7 @@ func (gcp *GCP) GetOrphanedResources() ([]OrphanedResource, error) {
|
|
|
zone = ""
|
|
|
}
|
|
|
|
|
|
- or := OrphanedResource{
|
|
|
+ or := models.OrphanedResource{
|
|
|
Kind: "disk",
|
|
|
Region: zone,
|
|
|
Description: desc,
|
|
|
@@ -517,7 +523,7 @@ func (gcp *GCP) GetOrphanedResources() ([]OrphanedResource, error) {
|
|
|
region = ""
|
|
|
}
|
|
|
|
|
|
- or := OrphanedResource{
|
|
|
+ or := models.OrphanedResource{
|
|
|
Kind: "address",
|
|
|
Region: region,
|
|
|
Description: map[string]string{
|
|
|
@@ -571,8 +577,8 @@ type GCPPricing struct {
|
|
|
ServiceRegions []string `json:"serviceRegions"`
|
|
|
PricingInfo []*PricingInfo `json:"pricingInfo"`
|
|
|
ServiceProviderName string `json:"serviceProviderName"`
|
|
|
- Node *Node `json:"node"`
|
|
|
- PV *PV `json:"pv"`
|
|
|
+ Node *models.Node `json:"node"`
|
|
|
+ PV *models.PV `json:"pv"`
|
|
|
}
|
|
|
|
|
|
// PricingInfo contains metadata about a cost.
|
|
|
@@ -614,7 +620,7 @@ type GCPResourceInfo struct {
|
|
|
UsageType string `json:"usageType"`
|
|
|
}
|
|
|
|
|
|
-func (gcp *GCP) parsePage(r io.Reader, inputKeys map[string]Key, pvKeys map[string]PVKey) (map[string]*GCPPricing, string, error) {
|
|
|
+func (gcp *GCP) parsePage(r io.Reader, inputKeys map[string]models.Key, pvKeys map[string]models.PVKey) (map[string]*GCPPricing, string, error) {
|
|
|
gcpPricingList := make(map[string]*GCPPricing)
|
|
|
var nextPageToken string
|
|
|
dec := json.NewDecoder(r)
|
|
|
@@ -623,7 +629,7 @@ func (gcp *GCP) parsePage(r io.Reader, inputKeys map[string]Key, pvKeys map[stri
|
|
|
if err == io.EOF {
|
|
|
break
|
|
|
} else if err != nil {
|
|
|
- return nil, "", fmt.Errorf("Error parsing GCP pricing page: %s", err)
|
|
|
+ return nil, "", fmt.Errorf("error parsing GCP pricing page: %s", err)
|
|
|
}
|
|
|
if t == "skus" {
|
|
|
_, err := dec.Token() // consumes [
|
|
|
@@ -637,6 +643,7 @@ func (gcp *GCP) parsePage(r io.Reader, inputKeys map[string]Key, pvKeys map[stri
|
|
|
if err != nil {
|
|
|
return nil, "", err
|
|
|
}
|
|
|
+
|
|
|
usageType := strings.ToLower(product.Category.UsageType)
|
|
|
instanceType := strings.ToLower(product.Category.ResourceGroup)
|
|
|
|
|
|
@@ -654,7 +661,7 @@ func (gcp *GCP) parsePage(r io.Reader, inputKeys map[string]Key, pvKeys map[stri
|
|
|
region := sr
|
|
|
candidateKey := region + "," + "ssd"
|
|
|
if _, ok := pvKeys[candidateKey]; ok {
|
|
|
- product.PV = &PV{
|
|
|
+ product.PV = &models.PV{
|
|
|
Cost: strconv.FormatFloat(hourlyPrice, 'f', -1, 64),
|
|
|
}
|
|
|
gcpPricingList[candidateKey] = product
|
|
|
@@ -676,7 +683,7 @@ func (gcp *GCP) parsePage(r io.Reader, inputKeys map[string]Key, pvKeys map[stri
|
|
|
region := sr
|
|
|
candidateKey := region + "," + "ssd" + "," + "regional"
|
|
|
if _, ok := pvKeys[candidateKey]; ok {
|
|
|
- product.PV = &PV{
|
|
|
+ product.PV = &models.PV{
|
|
|
Cost: strconv.FormatFloat(hourlyPrice, 'f', -1, 64),
|
|
|
}
|
|
|
gcpPricingList[candidateKey] = product
|
|
|
@@ -697,7 +704,7 @@ func (gcp *GCP) parsePage(r io.Reader, inputKeys map[string]Key, pvKeys map[stri
|
|
|
region := sr
|
|
|
candidateKey := region + "," + "pdstandard"
|
|
|
if _, ok := pvKeys[candidateKey]; ok {
|
|
|
- product.PV = &PV{
|
|
|
+ product.PV = &models.PV{
|
|
|
Cost: strconv.FormatFloat(hourlyPrice, 'f', -1, 64),
|
|
|
}
|
|
|
gcpPricingList[candidateKey] = product
|
|
|
@@ -718,7 +725,7 @@ func (gcp *GCP) parsePage(r io.Reader, inputKeys map[string]Key, pvKeys map[stri
|
|
|
region := sr
|
|
|
candidateKey := region + "," + "pdstandard" + "," + "regional"
|
|
|
if _, ok := pvKeys[candidateKey]; ok {
|
|
|
- product.PV = &PV{
|
|
|
+ product.PV = &models.PV{
|
|
|
Cost: strconv.FormatFloat(hourlyPrice, 'f', -1, 64),
|
|
|
}
|
|
|
gcpPricingList[candidateKey] = product
|
|
|
@@ -740,6 +747,10 @@ func (gcp *GCP) parsePage(r io.Reader, inputKeys map[string]Key, pvKeys map[stri
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ if (instanceType == "ram" || instanceType == "cpu") && strings.Contains(strings.ToUpper(product.Description), "A2 INSTANCE") {
|
|
|
+ instanceType = "a2"
|
|
|
+ }
|
|
|
+
|
|
|
if (instanceType == "ram" || instanceType == "cpu") && strings.Contains(strings.ToUpper(product.Description), "COMPUTE OPTIMIZED") {
|
|
|
instanceType = "c2standard"
|
|
|
}
|
|
|
@@ -751,19 +762,19 @@ func (gcp *GCP) parsePage(r io.Reader, inputKeys map[string]Key, pvKeys map[stri
|
|
|
partialCPUMap["e2micro"] = 0.25
|
|
|
partialCPUMap["e2small"] = 0.5
|
|
|
partialCPUMap["e2medium"] = 1
|
|
|
- /*
|
|
|
- var partialCPU float64
|
|
|
- if strings.ToLower(instanceType) == "f1micro" {
|
|
|
- partialCPU = 0.2
|
|
|
- } else if strings.ToLower(instanceType) == "g1small" {
|
|
|
- partialCPU = 0.5
|
|
|
- }
|
|
|
- */
|
|
|
+
|
|
|
+ if (instanceType == "ram" || instanceType == "cpu") && strings.Contains(strings.ToUpper(product.Description), "T2D AMD") {
|
|
|
+ instanceType = "t2dstandard"
|
|
|
+ }
|
|
|
+ if (instanceType == "ram" || instanceType == "cpu") && strings.Contains(strings.ToUpper(product.Description), "T2A ARM") {
|
|
|
+ instanceType = "t2astandard"
|
|
|
+ }
|
|
|
+
|
|
|
var gpuType string
|
|
|
for matchnum, group := range nvidiaGPURegex.FindStringSubmatch(product.Description) {
|
|
|
if matchnum == 1 {
|
|
|
gpuType = strings.ToLower(strings.Join(strings.Split(group, " "), "-"))
|
|
|
- log.Debug("GPU type found: " + gpuType)
|
|
|
+ log.Debugf("GCP Billing API: GPU type found: '%s'", gpuType)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -773,20 +784,24 @@ func (gcp *GCP) parsePage(r io.Reader, inputKeys map[string]Key, pvKeys map[stri
|
|
|
}
|
|
|
|
|
|
for _, region := range product.ServiceRegions {
|
|
|
- if instanceType == "e2" { // this needs to be done to handle a partial cpu mapping
|
|
|
+ switch instanceType {
|
|
|
+ case "e2":
|
|
|
candidateKeys = append(candidateKeys, region+","+"e2micro"+","+usageType)
|
|
|
candidateKeys = append(candidateKeys, region+","+"e2small"+","+usageType)
|
|
|
candidateKeys = append(candidateKeys, region+","+"e2medium"+","+usageType)
|
|
|
candidateKeys = append(candidateKeys, region+","+"e2standard"+","+usageType)
|
|
|
candidateKeys = append(candidateKeys, region+","+"e2custom"+","+usageType)
|
|
|
- } else {
|
|
|
+ case "a2":
|
|
|
+ candidateKeys = append(candidateKeys, region+","+"a2highgpu"+","+usageType)
|
|
|
+ candidateKeys = append(candidateKeys, region+","+"a2megagpu"+","+usageType)
|
|
|
+ default:
|
|
|
candidateKey := region + "," + instanceType + "," + usageType
|
|
|
candidateKeys = append(candidateKeys, candidateKey)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
for _, candidateKey := range candidateKeys {
|
|
|
- instanceType = strings.Split(candidateKey, ",")[1] // we may have overriden this while generating candidate keys
|
|
|
+ instanceType = strings.Split(candidateKey, ",")[1] // we may have overridden this while generating candidate keys
|
|
|
region := strings.Split(candidateKey, ",")[0]
|
|
|
candidateKeyGPU := candidateKey + ",gpu"
|
|
|
gcp.ValidPricingKeys[candidateKey] = true
|
|
|
@@ -794,32 +809,46 @@ func (gcp *GCP) parsePage(r io.Reader, inputKeys map[string]Key, pvKeys map[stri
|
|
|
if gpuType != "" {
|
|
|
lastRateIndex := len(product.PricingInfo[0].PricingExpression.TieredRates) - 1
|
|
|
var nanos float64
|
|
|
+ var unitsBaseCurrency int
|
|
|
if lastRateIndex > -1 && len(product.PricingInfo) > 0 {
|
|
|
nanos = product.PricingInfo[0].PricingExpression.TieredRates[lastRateIndex].UnitPrice.Nanos
|
|
|
+ unitsBaseCurrency, err = strconv.Atoi(product.PricingInfo[0].PricingExpression.TieredRates[lastRateIndex].UnitPrice.Units)
|
|
|
+ if err != nil {
|
|
|
+ return nil, "", fmt.Errorf("error parsing base unit price for gpu: %w", err)
|
|
|
+ }
|
|
|
} else {
|
|
|
continue
|
|
|
}
|
|
|
- hourlyPrice := nanos * math.Pow10(-9)
|
|
|
+
|
|
|
+ // as per https://cloud.google.com/billing/v1/how-tos/catalog-api
|
|
|
+ // the hourly price is the whole currency price + the fractional currency price
|
|
|
+ hourlyPrice := (nanos * math.Pow10(-9)) + float64(unitsBaseCurrency)
|
|
|
+
|
|
|
+ // GPUs with an hourly price of 0 are reserved versions of GPUs
|
|
|
+ // (E.g., SKU "2013-37B4-22EA")
|
|
|
+ // and are excluded from cost computations
|
|
|
+ if hourlyPrice == 0 {
|
|
|
+ log.Debugf("GCP Billing API: excluding reserved GPU SKU #%s", product.SKUID)
|
|
|
+ continue
|
|
|
+ }
|
|
|
|
|
|
for k, key := range inputKeys {
|
|
|
if key.GPUType() == gpuType+","+usageType {
|
|
|
if region == strings.Split(k, ",")[0] {
|
|
|
- log.Infof("Matched GPU to node in region \"%s\"", region)
|
|
|
- log.Debugf("PRODUCT DESCRIPTION: %s", product.Description)
|
|
|
matchedKey := key.Features()
|
|
|
+ log.Debugf("GCP Billing API: matched GPU to node: %s: %s", matchedKey, product.Description)
|
|
|
if pl, ok := gcpPricingList[matchedKey]; ok {
|
|
|
pl.Node.GPUName = gpuType
|
|
|
pl.Node.GPUCost = strconv.FormatFloat(hourlyPrice, 'f', -1, 64)
|
|
|
pl.Node.GPU = "1"
|
|
|
} else {
|
|
|
- product.Node = &Node{
|
|
|
+ product.Node = &models.Node{
|
|
|
GPUName: gpuType,
|
|
|
GPUCost: strconv.FormatFloat(hourlyPrice, 'f', -1, 64),
|
|
|
GPU: "1",
|
|
|
}
|
|
|
gcpPricingList[matchedKey] = product
|
|
|
}
|
|
|
- log.Infof("Added data for " + matchedKey)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -827,82 +856,98 @@ func (gcp *GCP) parsePage(r io.Reader, inputKeys map[string]Key, pvKeys map[stri
|
|
|
_, ok := inputKeys[candidateKey]
|
|
|
_, ok2 := inputKeys[candidateKeyGPU]
|
|
|
if ok || ok2 {
|
|
|
- lastRateIndex := len(product.PricingInfo[0].PricingExpression.TieredRates) - 1
|
|
|
var nanos float64
|
|
|
- if lastRateIndex > -1 && len(product.PricingInfo) > 0 {
|
|
|
- nanos = product.PricingInfo[0].PricingExpression.TieredRates[lastRateIndex].UnitPrice.Nanos
|
|
|
+ var unitsBaseCurrency int
|
|
|
+ if len(product.PricingInfo) > 0 {
|
|
|
+ lastRateIndex := len(product.PricingInfo[0].PricingExpression.TieredRates) - 1
|
|
|
+ if lastRateIndex >= 0 {
|
|
|
+ nanos = product.PricingInfo[0].PricingExpression.TieredRates[lastRateIndex].UnitPrice.Nanos
|
|
|
+ unitsBaseCurrency, err = strconv.Atoi(product.PricingInfo[0].PricingExpression.TieredRates[lastRateIndex].UnitPrice.Units)
|
|
|
+ if err != nil {
|
|
|
+ return nil, "", fmt.Errorf("error parsing base unit price for instance: %w", err)
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ continue
|
|
|
+ }
|
|
|
} else {
|
|
|
continue
|
|
|
}
|
|
|
- hourlyPrice := nanos * math.Pow10(-9)
|
|
|
+
|
|
|
+ // as per https://cloud.google.com/billing/v1/how-tos/catalog-api
|
|
|
+ // the hourly price is the whole currency price + the fractional currency price
|
|
|
+ hourlyPrice := (nanos * math.Pow10(-9)) + float64(unitsBaseCurrency)
|
|
|
|
|
|
if hourlyPrice == 0 {
|
|
|
continue
|
|
|
} else if strings.Contains(strings.ToUpper(product.Description), "RAM") {
|
|
|
if instanceType == "custom" {
|
|
|
- log.Debug("RAM custom sku is: " + product.Name)
|
|
|
+ log.Debugf("GCP Billing API: RAM custom sku '%s'", product.Name)
|
|
|
}
|
|
|
if _, ok := gcpPricingList[candidateKey]; ok {
|
|
|
+ log.Debugf("GCP Billing API: key '%s': RAM price: %f", candidateKey, hourlyPrice)
|
|
|
gcpPricingList[candidateKey].Node.RAMCost = strconv.FormatFloat(hourlyPrice, 'f', -1, 64)
|
|
|
} else {
|
|
|
- product = &GCPPricing{}
|
|
|
- product.Node = &Node{
|
|
|
+ log.Debugf("GCP Billing API: key '%s': RAM price: %f", candidateKey, hourlyPrice)
|
|
|
+ pricing := &GCPPricing{}
|
|
|
+ pricing.Node = &models.Node{
|
|
|
RAMCost: strconv.FormatFloat(hourlyPrice, 'f', -1, 64),
|
|
|
}
|
|
|
partialCPU, pcok := partialCPUMap[instanceType]
|
|
|
if pcok {
|
|
|
- product.Node.VCPU = fmt.Sprintf("%f", partialCPU)
|
|
|
+ pricing.Node.VCPU = fmt.Sprintf("%f", partialCPU)
|
|
|
}
|
|
|
- product.Node.UsageType = usageType
|
|
|
- gcpPricingList[candidateKey] = product
|
|
|
+ pricing.Node.UsageType = usageType
|
|
|
+ gcpPricingList[candidateKey] = pricing
|
|
|
}
|
|
|
if _, ok := gcpPricingList[candidateKeyGPU]; ok {
|
|
|
- log.Infof("Adding RAM %f for %s", hourlyPrice, candidateKeyGPU)
|
|
|
+ log.Debugf("GCP Billing API: key '%s': RAM price: %f", candidateKeyGPU, hourlyPrice)
|
|
|
gcpPricingList[candidateKeyGPU].Node.RAMCost = strconv.FormatFloat(hourlyPrice, 'f', -1, 64)
|
|
|
} else {
|
|
|
- log.Infof("Adding RAM %f for %s", hourlyPrice, candidateKeyGPU)
|
|
|
- product = &GCPPricing{}
|
|
|
- product.Node = &Node{
|
|
|
+ log.Debugf("GCP Billing API: key '%s': RAM price: %f", candidateKeyGPU, hourlyPrice)
|
|
|
+ pricing := &GCPPricing{}
|
|
|
+ pricing.Node = &models.Node{
|
|
|
RAMCost: strconv.FormatFloat(hourlyPrice, 'f', -1, 64),
|
|
|
}
|
|
|
partialCPU, pcok := partialCPUMap[instanceType]
|
|
|
if pcok {
|
|
|
- product.Node.VCPU = fmt.Sprintf("%f", partialCPU)
|
|
|
+ pricing.Node.VCPU = fmt.Sprintf("%f", partialCPU)
|
|
|
}
|
|
|
- product.Node.UsageType = usageType
|
|
|
- gcpPricingList[candidateKeyGPU] = product
|
|
|
+ pricing.Node.UsageType = usageType
|
|
|
+ gcpPricingList[candidateKeyGPU] = pricing
|
|
|
}
|
|
|
- break
|
|
|
} else {
|
|
|
if _, ok := gcpPricingList[candidateKey]; ok {
|
|
|
+ log.Debugf("GCP Billing API: key '%s': CPU price: %f", candidateKey, hourlyPrice)
|
|
|
gcpPricingList[candidateKey].Node.VCPUCost = strconv.FormatFloat(hourlyPrice, 'f', -1, 64)
|
|
|
} else {
|
|
|
- product = &GCPPricing{}
|
|
|
- product.Node = &Node{
|
|
|
+ log.Debugf("GCP Billing API: key '%s': CPU price: %f", candidateKey, hourlyPrice)
|
|
|
+ pricing := &GCPPricing{}
|
|
|
+ pricing.Node = &models.Node{
|
|
|
VCPUCost: strconv.FormatFloat(hourlyPrice, 'f', -1, 64),
|
|
|
}
|
|
|
partialCPU, pcok := partialCPUMap[instanceType]
|
|
|
if pcok {
|
|
|
- product.Node.VCPU = fmt.Sprintf("%f", partialCPU)
|
|
|
+ pricing.Node.VCPU = fmt.Sprintf("%f", partialCPU)
|
|
|
}
|
|
|
- product.Node.UsageType = usageType
|
|
|
- gcpPricingList[candidateKey] = product
|
|
|
+ pricing.Node.UsageType = usageType
|
|
|
+ gcpPricingList[candidateKey] = pricing
|
|
|
}
|
|
|
if _, ok := gcpPricingList[candidateKeyGPU]; ok {
|
|
|
+ log.Debugf("GCP Billing API: key '%s': CPU price: %f", candidateKeyGPU, hourlyPrice)
|
|
|
gcpPricingList[candidateKeyGPU].Node.VCPUCost = strconv.FormatFloat(hourlyPrice, 'f', -1, 64)
|
|
|
} else {
|
|
|
- product = &GCPPricing{}
|
|
|
- product.Node = &Node{
|
|
|
+ log.Debugf("GCP Billing API: key '%s': CPU price: %f", candidateKeyGPU, hourlyPrice)
|
|
|
+ pricing := &GCPPricing{}
|
|
|
+ pricing.Node = &models.Node{
|
|
|
VCPUCost: strconv.FormatFloat(hourlyPrice, 'f', -1, 64),
|
|
|
}
|
|
|
partialCPU, pcok := partialCPUMap[instanceType]
|
|
|
if pcok {
|
|
|
- product.Node.VCPU = fmt.Sprintf("%f", partialCPU)
|
|
|
+ pricing.Node.VCPU = fmt.Sprintf("%f", partialCPU)
|
|
|
}
|
|
|
- product.Node.UsageType = usageType
|
|
|
- gcpPricingList[candidateKeyGPU] = product
|
|
|
+ pricing.Node.UsageType = usageType
|
|
|
+ gcpPricingList[candidateKeyGPU] = pricing
|
|
|
}
|
|
|
- break
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -925,13 +970,19 @@ func (gcp *GCP) parsePage(r io.Reader, inputKeys map[string]Key, pvKeys map[stri
|
|
|
return gcpPricingList, nextPageToken, nil
|
|
|
}
|
|
|
|
|
|
-func (gcp *GCP) parsePages(inputKeys map[string]Key, pvKeys map[string]PVKey) (map[string]*GCPPricing, error) {
|
|
|
+func (gcp *GCP) getBillingAPIURL(apiKey, currencyCode string) string {
|
|
|
+ return fmt.Sprintf(BillingAPIURLFmt, apiKey, currencyCode)
|
|
|
+}
|
|
|
+
|
|
|
+func (gcp *GCP) parsePages(inputKeys map[string]models.Key, pvKeys map[string]models.PVKey) (map[string]*GCPPricing, error) {
|
|
|
var pages []map[string]*GCPPricing
|
|
|
c, err := gcp.GetConfig()
|
|
|
if err != nil {
|
|
|
return nil, err
|
|
|
}
|
|
|
- url := "https://cloudbilling.googleapis.com/v1/services/6F81-5844-456A/skus?key=" + gcp.APIKey + "¤cyCode=" + c.CurrencyCode
|
|
|
+
|
|
|
+ url := gcp.getBillingAPIURL(gcp.APIKey, c.CurrencyCode)
|
|
|
+
|
|
|
log.Infof("Fetch GCP Billing Data from URL: %s", url)
|
|
|
var parsePagesHelper func(string) error
|
|
|
parsePagesHelper = func(pageToken string) error {
|
|
|
@@ -982,6 +1033,7 @@ func (gcp *GCP) parsePages(inputKeys map[string]Key, pvKeys map[string]PVKey) (m
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
log.Debugf("ALL PAGES: %+v", returnPages)
|
|
|
for k, v := range returnPages {
|
|
|
if v.Node != nil {
|
|
|
@@ -1010,7 +1062,7 @@ func (gcp *GCP) DownloadPricingData() error {
|
|
|
gcp.BillingDataDataset = c.BillingDataDataset
|
|
|
|
|
|
nodeList := gcp.Clientset.GetAllNodes()
|
|
|
- inputkeys := make(map[string]Key)
|
|
|
+ inputkeys := make(map[string]models.Key)
|
|
|
|
|
|
defaultRegion := "" // Sometimes, PVs may be missing the region label. In that case assume that they are in the same region as the nodes
|
|
|
for _, n := range nodeList {
|
|
|
@@ -1039,7 +1091,7 @@ func (gcp *GCP) DownloadPricingData() error {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- pvkeys := make(map[string]PVKey)
|
|
|
+ pvkeys := make(map[string]models.PVKey)
|
|
|
for _, pv := range pvList {
|
|
|
params, ok := storageClassMap[pv.Spec.StorageClassName]
|
|
|
if !ok {
|
|
|
@@ -1073,19 +1125,19 @@ func (gcp *GCP) DownloadPricingData() error {
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
-func (gcp *GCP) PVPricing(pvk PVKey) (*PV, error) {
|
|
|
+func (gcp *GCP) PVPricing(pvk models.PVKey) (*models.PV, error) {
|
|
|
gcp.DownloadPricingDataLock.RLock()
|
|
|
defer gcp.DownloadPricingDataLock.RUnlock()
|
|
|
pricing, ok := gcp.Pricing[pvk.Features()]
|
|
|
if !ok {
|
|
|
log.Infof("Persistent Volume pricing not found for %s: %s", pvk.GetStorageClass(), pvk.Features())
|
|
|
- return &PV{}, nil
|
|
|
+ return &models.PV{}, nil
|
|
|
}
|
|
|
return pricing.PV, nil
|
|
|
}
|
|
|
|
|
|
// Stubbed NetworkPricing for GCP. Pull directly from gcp.json for now
|
|
|
-func (gcp *GCP) NetworkPricing() (*Network, error) {
|
|
|
+func (gcp *GCP) NetworkPricing() (*models.Network, error) {
|
|
|
cpricing, err := gcp.Config.GetCustomPricingData()
|
|
|
if err != nil {
|
|
|
return nil, err
|
|
|
@@ -1103,14 +1155,14 @@ func (gcp *GCP) NetworkPricing() (*Network, error) {
|
|
|
return nil, err
|
|
|
}
|
|
|
|
|
|
- return &Network{
|
|
|
+ return &models.Network{
|
|
|
ZoneNetworkEgressCost: znec,
|
|
|
RegionNetworkEgressCost: rnec,
|
|
|
InternetNetworkEgressCost: inec,
|
|
|
}, nil
|
|
|
}
|
|
|
|
|
|
-func (gcp *GCP) LoadBalancerPricing() (*LoadBalancer, error) {
|
|
|
+func (gcp *GCP) LoadBalancerPricing() (*models.LoadBalancer, error) {
|
|
|
fffrc := 0.025
|
|
|
afrc := 0.010
|
|
|
lbidc := 0.008
|
|
|
@@ -1124,7 +1176,7 @@ func (gcp *GCP) LoadBalancerPricing() (*LoadBalancer, error) {
|
|
|
} else {
|
|
|
totalCost = fffrc*5 + afrc*(numForwardingRules-5) + lbidc*dataIngressGB
|
|
|
}
|
|
|
- return &LoadBalancer{
|
|
|
+ return &models.LoadBalancer{
|
|
|
Cost: totalCost,
|
|
|
}, nil
|
|
|
}
|
|
|
@@ -1172,19 +1224,19 @@ func newReservedCounter(instance *GCPReservedInstance) *GCPReservedCounter {
|
|
|
|
|
|
// Two available Reservation plans for GCP, 1-year and 3-year
|
|
|
var gcpReservedInstancePlans map[string]*GCPReservedInstancePlan = map[string]*GCPReservedInstancePlan{
|
|
|
- GCPReservedInstancePlanOneYear: &GCPReservedInstancePlan{
|
|
|
+ GCPReservedInstancePlanOneYear: {
|
|
|
Name: GCPReservedInstancePlanOneYear,
|
|
|
CPUCost: 0.019915,
|
|
|
RAMCost: 0.002669,
|
|
|
},
|
|
|
- GCPReservedInstancePlanThreeYear: &GCPReservedInstancePlan{
|
|
|
+ GCPReservedInstancePlanThreeYear: {
|
|
|
Name: GCPReservedInstancePlanThreeYear,
|
|
|
CPUCost: 0.014225,
|
|
|
RAMCost: 0.001907,
|
|
|
},
|
|
|
}
|
|
|
|
|
|
-func (gcp *GCP) ApplyReservedInstancePricing(nodes map[string]*Node) {
|
|
|
+func (gcp *GCP) ApplyReservedInstancePricing(nodes map[string]*models.Node) {
|
|
|
numReserved := len(gcp.ReservedInstances)
|
|
|
|
|
|
// Early return if no reserved instance data loaded
|
|
|
@@ -1242,7 +1294,7 @@ func (gcp *GCP) ApplyReservedInstancePricing(nodes map[string]*Node) {
|
|
|
continue
|
|
|
}
|
|
|
|
|
|
- node.Reserved = &ReservedInstanceData{
|
|
|
+ node.Reserved = &models.ReservedInstanceData{
|
|
|
ReservedCPU: 0,
|
|
|
ReservedRAM: 0,
|
|
|
}
|
|
|
@@ -1368,7 +1420,7 @@ func (key *pvKey) GetStorageClass() string {
|
|
|
return key.StorageClass
|
|
|
}
|
|
|
|
|
|
-func (gcp *GCP) GetPVKey(pv *v1.PersistentVolume, parameters map[string]string, defaultRegion string) PVKey {
|
|
|
+func (gcp *GCP) GetPVKey(pv *v1.PersistentVolume, parameters map[string]string, defaultRegion string) models.PVKey {
|
|
|
providerID := ""
|
|
|
if pv.Spec.GCEPersistentDisk != nil {
|
|
|
providerID = pv.Spec.GCEPersistentDisk.PDName
|
|
|
@@ -1407,7 +1459,7 @@ type gcpKey struct {
|
|
|
Labels map[string]string
|
|
|
}
|
|
|
|
|
|
-func (gcp *GCP) GetKey(labels map[string]string, n *v1.Node) Key {
|
|
|
+func (gcp *GCP) GetKey(labels map[string]string, n *v1.Node) models.Key {
|
|
|
return &gcpKey{
|
|
|
Labels: labels,
|
|
|
}
|
|
|
@@ -1449,6 +1501,8 @@ func parseGCPInstanceTypeLabel(it string) string {
|
|
|
instanceType = "n2standard"
|
|
|
} else if instanceType == "e2highmem" || instanceType == "e2highcpu" {
|
|
|
instanceType = "e2standard"
|
|
|
+ } else if instanceType == "n2dhighmem" || instanceType == "n2dhighcpu" {
|
|
|
+ instanceType = "n2dstandard"
|
|
|
} else if strings.HasPrefix(instanceType, "custom") {
|
|
|
instanceType = "custom" // The suffix of custom does not matter
|
|
|
}
|
|
|
@@ -1486,13 +1540,13 @@ func (gcp *GCP) AllNodePricing() (interface{}, error) {
|
|
|
return gcp.Pricing, nil
|
|
|
}
|
|
|
|
|
|
-func (gcp *GCP) getPricing(key Key) (*GCPPricing, bool) {
|
|
|
+func (gcp *GCP) getPricing(key models.Key) (*GCPPricing, bool) {
|
|
|
gcp.DownloadPricingDataLock.RLock()
|
|
|
defer gcp.DownloadPricingDataLock.RUnlock()
|
|
|
n, ok := gcp.Pricing[key.Features()]
|
|
|
return n, ok
|
|
|
}
|
|
|
-func (gcp *GCP) isValidPricingKey(key Key) bool {
|
|
|
+func (gcp *GCP) isValidPricingKey(key models.Key) bool {
|
|
|
gcp.DownloadPricingDataLock.RLock()
|
|
|
defer gcp.DownloadPricingDataLock.RUnlock()
|
|
|
_, ok := gcp.ValidPricingKeys[key.Features()]
|
|
|
@@ -1500,35 +1554,67 @@ func (gcp *GCP) isValidPricingKey(key Key) bool {
|
|
|
}
|
|
|
|
|
|
// NodePricing returns GCP pricing data for a single node
|
|
|
-func (gcp *GCP) NodePricing(key Key) (*Node, error) {
|
|
|
+func (gcp *GCP) NodePricing(key models.Key) (*models.Node, models.PricingMetadata, error) {
|
|
|
+ meta := models.PricingMetadata{}
|
|
|
+
|
|
|
+ c, err := gcp.Config.GetCustomPricingData()
|
|
|
+ if err != nil {
|
|
|
+ meta.Warnings = append(meta.Warnings, fmt.Sprintf("failed to detect currency: %s", err))
|
|
|
+ } else {
|
|
|
+ meta.Currency = c.CurrencyCode
|
|
|
+ }
|
|
|
+
|
|
|
if n, ok := gcp.getPricing(key); ok {
|
|
|
log.Debugf("Returning pricing for node %s: %+v from SKU %s", key, n.Node, n.Name)
|
|
|
+
|
|
|
+ // Add pricing URL, but redact the key (hence, "***"")
|
|
|
+ meta.Source = fmt.Sprintf("Downloaded pricing from %s", gcp.getBillingAPIURL("***", c.CurrencyCode))
|
|
|
+
|
|
|
n.Node.BaseCPUPrice = gcp.BaseCPUPrice
|
|
|
- return n.Node, nil
|
|
|
+
|
|
|
+ return n.Node, meta, nil
|
|
|
} else if ok := gcp.isValidPricingKey(key); ok {
|
|
|
+ meta.Warnings = append(meta.Warnings, fmt.Sprintf("No pricing found, but key is valid: %s", key.Features()))
|
|
|
+
|
|
|
err := gcp.DownloadPricingData()
|
|
|
if err != nil {
|
|
|
- return nil, fmt.Errorf("Download pricing data failed: %s", err.Error())
|
|
|
+ log.Warnf("no pricing data found for %s", key.Features())
|
|
|
+
|
|
|
+ meta.Warnings = append(meta.Warnings, "Failed to download pricing data")
|
|
|
+
|
|
|
+ return nil, meta, fmt.Errorf("failed to download pricing data: %w", err)
|
|
|
}
|
|
|
if n, ok := gcp.getPricing(key); ok {
|
|
|
log.Debugf("Returning pricing for node %s: %+v from SKU %s", key, n.Node, n.Name)
|
|
|
+
|
|
|
+ // Add pricing URL, but redact the key (hence, "***"")
|
|
|
+ meta.Source = fmt.Sprintf("Downloaded pricing from %s", gcp.getBillingAPIURL("***", c.CurrencyCode))
|
|
|
+
|
|
|
n.Node.BaseCPUPrice = gcp.BaseCPUPrice
|
|
|
- return n.Node, nil
|
|
|
+
|
|
|
+ return n.Node, meta, nil
|
|
|
}
|
|
|
- log.Warnf("no pricing data found for %s: %s", key.Features(), key)
|
|
|
- return nil, fmt.Errorf("Warning: no pricing data found for %s", key)
|
|
|
+
|
|
|
+ log.Warnf("no pricing data found for %s", key.Features())
|
|
|
+
|
|
|
+ meta.Warnings = append(meta.Warnings, "Failed to find pricing after downloading data, but key is valid")
|
|
|
+
|
|
|
+ return nil, meta, fmt.Errorf("failed to find pricing data: %s", key.Features())
|
|
|
}
|
|
|
- return nil, fmt.Errorf("Warning: no pricing data found for %s", key)
|
|
|
+
|
|
|
+ meta.Warnings = append(meta.Warnings, fmt.Sprintf("No pricing found, and key is not valid: %s", key.Features()))
|
|
|
+
|
|
|
+ return nil, meta, fmt.Errorf("no pricing data found for %s", key.Features())
|
|
|
}
|
|
|
|
|
|
-func (gcp *GCP) ServiceAccountStatus() *ServiceAccountStatus {
|
|
|
- return &ServiceAccountStatus{
|
|
|
- Checks: []*ServiceAccountCheck{},
|
|
|
+func (gcp *GCP) ServiceAccountStatus() *models.ServiceAccountStatus {
|
|
|
+ return &models.ServiceAccountStatus{
|
|
|
+ Checks: []*models.ServiceAccountCheck{},
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-func (gcp *GCP) PricingSourceStatus() map[string]*PricingSource {
|
|
|
- return make(map[string]*PricingSource)
|
|
|
+func (gcp *GCP) PricingSourceStatus() map[string]*models.PricingSource {
|
|
|
+ return make(map[string]*models.PricingSource)
|
|
|
}
|
|
|
|
|
|
func (gcp *GCP) CombinedDiscountForNode(instanceType string, isPreemptible bool, defaultDiscount, negotiatedDiscount float64) float64 {
|
|
|
@@ -1537,6 +1623,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
|
|
|
}
|
|
|
|
|
|
@@ -1554,7 +1648,7 @@ func sustainedUseDiscount(class string, defaultDiscount float64, isPreemptible b
|
|
|
return discount
|
|
|
}
|
|
|
|
|
|
-func parseGCPProjectID(id string) string {
|
|
|
+func ParseGCPProjectID(id string) string {
|
|
|
// gce://guestbook-12345/...
|
|
|
// => guestbook-12345
|
|
|
match := gceRegex.FindStringSubmatch(id)
|
|
|
@@ -1571,8 +1665,15 @@ func getUsageType(labels map[string]string) string {
|
|
|
} else if t, ok := labels[GKESpotLabel]; ok && t == "true" {
|
|
|
// https://cloud.google.com/kubernetes-engine/docs/concepts/spot-vms
|
|
|
return "preemptible"
|
|
|
- } else if t, ok := labels[KarpenterCapacityTypeLabel]; ok && t == KarpenterCapacitySpotTypeValue {
|
|
|
+ } else if t, ok := labels[models.KarpenterCapacityTypeLabel]; ok && t == models.KarpenterCapacitySpotTypeValue {
|
|
|
return "preemptible"
|
|
|
}
|
|
|
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
|
|
|
+}
|