cost-optimization/gke-shift-left-cost/api/resource_price.go (125 lines of code) (raw):
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package api
import (
"context"
"fmt"
"strings"
billing "cloud.google.com/go/billing/apiv1"
"google.golang.org/api/iterator"
"google.golang.org/api/option"
billingpb "google.golang.org/genproto/googleapis/cloud/billing/v1"
)
var cpuPrefixes = map[MachineFamily]string{
N1: "N1 Predefined Instance Core",
N2: "N2 Instance Core",
E2: "E2 Instance Core",
N2D: "N2D AMD Custom Instance Core",
}
var memoryPrefixes = map[MachineFamily]string{
N1: "N1 Predefined Instance Ram",
N2: "N2 Instance Ram",
E2: "E2 Instance Ram",
N2D: "N2D AMD Instance Ram",
}
var pdStandardPrefix = "Regional Storage PD Capacity"
// NewGCPPriceCatalog creates a gcpResourcePrice struct with Monthly prices for cpu and memory
// If credentials is nil, then the default service account will be used
func NewGCPPriceCatalog(credentials []byte, conf CostimatorConfig) (GCPPriceCatalog, error) {
conf = populateConfigNotProvided(conf)
var client *billing.CloudCatalogClient
var err error
if credentials == nil {
client, err = billing.NewCloudCatalogClient(context.Background())
} else {
client, err = billing.NewCloudCatalogClient(context.Background(), option.WithCredentialsJSON(credentials))
}
if err != nil {
return GCPPriceCatalog{}, err
}
return retrievePrices(client, conf)
}
func retrievePrices(client *billing.CloudCatalogClient, conf CostimatorConfig) (GCPPriceCatalog, error) {
skuIter, err := retrieveAllSKUs(client)
var cpuPi, memoryPi, storagePdPi *billingpb.PricingInfo
for {
sku, err := skuIter.Next()
if err == iterator.Done ||
(cpuPi != nil && memoryPi != nil && storagePdPi != nil) {
break
}
if err != nil {
return GCPPriceCatalog{}, err
}
if cpuPi == nil && matchCPU(sku, conf) {
cpuPi = sku.GetPricingInfo()[0]
} else if memoryPi == nil && matchMemory(sku, conf) {
memoryPi = sku.GetPricingInfo()[0]
} else if storagePdPi == nil && matchGCEPersistentDisk(sku, conf) {
storagePdPi = sku.GetPricingInfo()[0]
}
}
if err == nil && (cpuPi == nil || memoryPi == nil || storagePdPi == nil) {
return GCPPriceCatalog{}, fmt.Errorf("Couldn't find all Price Infos: %+v", conf)
}
cpuPrice, err := calculateMonthlyPrice(cpuPi)
if err != nil {
return GCPPriceCatalog{}, err
}
memoryPrice, err := calculateMonthlyPrice(memoryPi)
if err != nil {
return GCPPriceCatalog{}, err
}
pdStandardPrice, err := calculateMonthlyPrice(storagePdPi)
if err != nil {
return GCPPriceCatalog{}, err
}
return GCPPriceCatalog{
cpuPrice: cpuPrice,
memoryPrice: memoryPrice,
pdStandardPrice: pdStandardPrice}, nil
}
func retrieveAllSKUs(client *billing.CloudCatalogClient) (*billing.SkuIterator, error) {
ctx := context.Background()
req := &billingpb.ListSkusRequest{
Parent: "services/6F81-5844-456A",
}
return client.ListSkus(ctx, req), nil
}
func matchCPU(sku *billingpb.Sku, conf CostimatorConfig) bool {
prefix, _ := cpuPrefixes[conf.ResourceConf.MachineFamily]
return skuMatcher(sku, prefix, conf)
}
func matchMemory(sku *billingpb.Sku, conf CostimatorConfig) bool {
prefix, _ := memoryPrefixes[conf.ResourceConf.MachineFamily]
return skuMatcher(sku, prefix, conf)
}
func matchGCEPersistentDisk(sku *billingpb.Sku, conf CostimatorConfig) bool {
return skuMatcher(sku, pdStandardPrefix, conf)
}
func skuMatcher(sku *billingpb.Sku, skuPrefix string, conf CostimatorConfig) bool {
return strings.HasPrefix(sku.GetDescription(), skuPrefix) &&
contains(sku.GetServiceRegions(), conf.ResourceConf.Region)
}
func calculateMonthlyPrice(pi *billingpb.PricingInfo) (float32, error) {
pe := pi.GetPricingExpression()
pu := pe.GetTieredRates()[0].GetUnitPrice()
unit := pe.GetUsageUnit()
switch unit {
case "h":
// 1 vcpu core per hour rate
hourlyPrice := float32(pu.GetUnits()) + float32(pu.GetNanos())/1000000000.0
return hourlyPrice * float32(24) * float32(31), nil
case "GiBy.h":
// 1 Byte per hour pricing
hourlyPrice := (float32(pu.GetUnits()) + float32(pu.GetNanos())/1000000000.0) / (1024 * 1024 * 1024)
return hourlyPrice * float32(24) * float32(31), nil
case "GiBy.mo":
// 1 Byte per month pricing
monthlyPrice := (float32(pu.GetUnits()) + float32(pu.GetNanos())/1000000000.0) / (1024 * 1024 * 1024)
return monthlyPrice, nil
default:
return 0, fmt.Errorf("Price UsageUnit Not implemented: %s", unit)
}
}
func contains(items []string, s string) bool {
for _, item := range items {
if strings.EqualFold(item, s) {
return true
}
}
return false
}