From fb75e3485aa801266ac13430fc395d0f8fd465c5 Mon Sep 17 00:00:00 2001 From: titanventura Date: Fri, 24 Nov 2023 21:59:21 +0530 Subject: [PATCH 1/2] [FEAT-778] Calculate cost for GCP Redis instances Signed-off-by: titanventura --- providers/gcp/redis/instances.go | 98 +++++++++++++++++++++++++++++++- 1 file changed, 97 insertions(+), 1 deletion(-) diff --git a/providers/gcp/redis/instances.go b/providers/gcp/redis/instances.go index 3af7c20d8..b6939c5a1 100644 --- a/providers/gcp/redis/instances.go +++ b/providers/gcp/redis/instances.go @@ -2,6 +2,10 @@ package redis import ( "context" + "encoding/json" + "math" + "net/http" + "fmt" "regexp" "time" @@ -18,7 +22,97 @@ import ( "github.com/tailwarden/komiser/utils" ) +type RedisPrice []struct { + Val int `json:"val"` + Currency string `json:"currnecy"` + Nanos float64 `json:"nanos"` +} + +type RegionBasedPricing struct { + Regions map[string]struct { + Price RedisPrice `json:"price"` + } `json:"regions"` +} + +type GcpDatabasePricing struct { + Gcp struct { + Databases struct { + CloudMemorystore struct { + Redis struct { + Basic map[string]RegionBasedPricing `json:"basic"` + Standard map[string]RegionBasedPricing `json:"standard"` + } `json:"redis"` + } `json:"cloud_memorystore"` + } `json:"databases"` + } `json:"gcp"` +} + +func calculateRedisCost(redis *redispb.Instance, pricing GcpDatabasePricing) float64 { + var priceMap map[string]RegionBasedPricing + var priceKey string + + prices := []int32{4, 10, 35, 100} + capacityTier := getCapacityTier(redis.MemorySizeGb, prices) + + if redis.Tier == redispb.Instance_BASIC { + priceMap = pricing.Gcp.Databases.CloudMemorystore.Redis.Basic + priceKey = fmt.Sprintf("Rediscapacitybasicm%ddefault", capacityTier) + } else if redis.Tier == redispb.Instance_STANDARD_HA { + priceMap = pricing.Gcp.Databases.CloudMemorystore.Redis.Standard + if redis.ReadReplicasMode == redispb.Instance_READ_REPLICAS_DISABLED { + priceKey = fmt.Sprintf("Rediscapacitystandardm%ddefault", capacityTier) + } else { + priceKey = fmt.Sprintf("Rediscapacitystandardnodem%d", capacityTier) + } + } + + pricePerHrPerGbInNanos := priceMap[priceKey].Regions[redis.LocationId].Price[0].Nanos + pricePerHrPerGbInDollars := pricePerHrPerGbInNanos / math.Pow(10, 9) + + now := time.Now() + startTime := getStartTime(redis.GetCreateTime().AsTime(), now) + + hours := now.Sub(startTime).Hours() + + cost := hours * pricePerHrPerGbInDollars + + if redis.ReadReplicasMode == redispb.Instance_READ_REPLICAS_ENABLED { + cost *= float64(redis.ReplicaCount) + } + + return cost +} + +func getCapacityTier(memorySizeGb int32, prices []int32) int { + capacityTier := 5 + for idx, price := range prices { + if memorySizeGb <= price { + capacityTier = idx + 1 + } + } + return capacityTier +} + +func getStartTime(createTime, now time.Time) time.Time { + firstOfCurrentMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) + if createTime.After(firstOfCurrentMonth) { + return createTime + } + return firstOfCurrentMonth +} + func Instances(ctx context.Context, client providers.ProviderClient) ([]models.Resource, error) { + res, err := http.Get("https://www.gstatic.com/cloud-site-ux/pricing/data/gcp-databases.json") + if err != nil { + return nil, err + } + + var pricing GcpDatabasePricing + err = json.NewDecoder(res.Body).Decode(&pricing) + if err != nil { + return nil, err + } + resources := make([]models.Resource, 0) regions, err := utils.FetchGCPRegionsInRealtime(client.GCPClient.Credentials.ProjectID, option.WithCredentials(client.GCPClient.Credentials)) @@ -57,6 +151,8 @@ RegionsLoop: re := regexp.MustCompile(`instances\/(.+)$`) redisInstanceName := re.FindStringSubmatch(redis.Name)[1] + cost := calculateRedisCost(redis, pricing) + resources = append(resources, models.Resource{ Provider: "GCP", Account: client.Name, @@ -65,7 +161,7 @@ RegionsLoop: Name: redis.DisplayName, Region: regionName, CreatedAt: redis.CreateTime.AsTime(), - Cost: 0, + Cost: cost, FetchedAt: time.Now(), Link: fmt.Sprintf("https://console.cloud.google.com/memorystore/redis/locations/%s/instances/%s/details/overview?project=%s", regionName, redisInstanceName, client.GCPClient.Credentials.ProjectID), }) From 6d7bc4d4ea6a83fc90ce9427d5debd9e875a01b7 Mon Sep 17 00:00:00 2001 From: titanventura Date: Wed, 6 Dec 2023 09:09:17 +0530 Subject: [PATCH 2/2] [FEAT-778] gcp redis instance - fix magic numbers and extract price fetching into a function Signed-off-by: titanventura --- providers/gcp/redis/instances.go | 88 +----------------------- providers/gcp/redis/pricing.go | 112 +++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 87 deletions(-) create mode 100644 providers/gcp/redis/pricing.go diff --git a/providers/gcp/redis/instances.go b/providers/gcp/redis/instances.go index b6939c5a1..65b115c28 100644 --- a/providers/gcp/redis/instances.go +++ b/providers/gcp/redis/instances.go @@ -2,9 +2,6 @@ package redis import ( "context" - "encoding/json" - "math" - "net/http" "fmt" "regexp" @@ -22,93 +19,10 @@ import ( "github.com/tailwarden/komiser/utils" ) -type RedisPrice []struct { - Val int `json:"val"` - Currency string `json:"currnecy"` - Nanos float64 `json:"nanos"` -} - -type RegionBasedPricing struct { - Regions map[string]struct { - Price RedisPrice `json:"price"` - } `json:"regions"` -} - -type GcpDatabasePricing struct { - Gcp struct { - Databases struct { - CloudMemorystore struct { - Redis struct { - Basic map[string]RegionBasedPricing `json:"basic"` - Standard map[string]RegionBasedPricing `json:"standard"` - } `json:"redis"` - } `json:"cloud_memorystore"` - } `json:"databases"` - } `json:"gcp"` -} - -func calculateRedisCost(redis *redispb.Instance, pricing GcpDatabasePricing) float64 { - var priceMap map[string]RegionBasedPricing - var priceKey string - - prices := []int32{4, 10, 35, 100} - capacityTier := getCapacityTier(redis.MemorySizeGb, prices) - - if redis.Tier == redispb.Instance_BASIC { - priceMap = pricing.Gcp.Databases.CloudMemorystore.Redis.Basic - priceKey = fmt.Sprintf("Rediscapacitybasicm%ddefault", capacityTier) - } else if redis.Tier == redispb.Instance_STANDARD_HA { - priceMap = pricing.Gcp.Databases.CloudMemorystore.Redis.Standard - if redis.ReadReplicasMode == redispb.Instance_READ_REPLICAS_DISABLED { - priceKey = fmt.Sprintf("Rediscapacitystandardm%ddefault", capacityTier) - } else { - priceKey = fmt.Sprintf("Rediscapacitystandardnodem%d", capacityTier) - } - } - - pricePerHrPerGbInNanos := priceMap[priceKey].Regions[redis.LocationId].Price[0].Nanos - pricePerHrPerGbInDollars := pricePerHrPerGbInNanos / math.Pow(10, 9) - - now := time.Now() - startTime := getStartTime(redis.GetCreateTime().AsTime(), now) - - hours := now.Sub(startTime).Hours() - - cost := hours * pricePerHrPerGbInDollars - - if redis.ReadReplicasMode == redispb.Instance_READ_REPLICAS_ENABLED { - cost *= float64(redis.ReplicaCount) - } - return cost -} - -func getCapacityTier(memorySizeGb int32, prices []int32) int { - capacityTier := 5 - for idx, price := range prices { - if memorySizeGb <= price { - capacityTier = idx + 1 - } - } - return capacityTier -} - -func getStartTime(createTime, now time.Time) time.Time { - firstOfCurrentMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) - if createTime.After(firstOfCurrentMonth) { - return createTime - } - return firstOfCurrentMonth -} func Instances(ctx context.Context, client providers.ProviderClient) ([]models.Resource, error) { - res, err := http.Get("https://www.gstatic.com/cloud-site-ux/pricing/data/gcp-databases.json") - if err != nil { - return nil, err - } - - var pricing GcpDatabasePricing - err = json.NewDecoder(res.Body).Decode(&pricing) + pricing, err := FetchPricing() if err != nil { return nil, err } diff --git a/providers/gcp/redis/pricing.go b/providers/gcp/redis/pricing.go new file mode 100644 index 000000000..8d8cd99c4 --- /dev/null +++ b/providers/gcp/redis/pricing.go @@ -0,0 +1,112 @@ +package redis + +import ( + "encoding/json" + "fmt" + "math" + "net/http" + "time" + + "cloud.google.com/go/redis/apiv1/redispb" +) + +const ( + M1GbLimit = 4 + M2GbLimit = 10 + M3GbLimit = 35 + M4GbLimit = 100 +) + +type RedisPrice []struct { + Val int `json:"val"` + Currency string `json:"currnecy"` + Nanos float64 `json:"nanos"` +} + +type RegionBasedPricing struct { + Regions map[string]struct { + Price RedisPrice `json:"price"` + } `json:"regions"` +} + +type GcpDatabasePricing struct { + Gcp struct { + Databases struct { + CloudMemorystore struct { + Redis struct { + Basic map[string]RegionBasedPricing `json:"basic"` + Standard map[string]RegionBasedPricing `json:"standard"` + } `json:"redis"` + } `json:"cloud_memorystore"` + } `json:"databases"` + } `json:"gcp"` +} + +func FetchPricing() (*GcpDatabasePricing, error) { + res, err := http.Get("https://www.gstatic.com/cloud-site-ux/pricing/data/gcp-databases.json") + if err != nil { + return nil, err + } + + var pricing GcpDatabasePricing + err = json.NewDecoder(res.Body).Decode(&pricing) + if err != nil { + return nil, err + } + + return &pricing, nil +} + +func calculateRedisCost(redis *redispb.Instance, pricing *GcpDatabasePricing) float64 { + var priceMap map[string]RegionBasedPricing + var priceKey string + + prices := []int32{M1GbLimit, M2GbLimit, M3GbLimit, M4GbLimit} + capacityTier := getCapacityTier(redis.MemorySizeGb, prices) + + if redis.Tier == redispb.Instance_BASIC { + priceMap = pricing.Gcp.Databases.CloudMemorystore.Redis.Basic + priceKey = fmt.Sprintf("Rediscapacitybasicm%ddefault", capacityTier) + } else if redis.Tier == redispb.Instance_STANDARD_HA { + priceMap = pricing.Gcp.Databases.CloudMemorystore.Redis.Standard + if redis.ReadReplicasMode == redispb.Instance_READ_REPLICAS_DISABLED { + priceKey = fmt.Sprintf("Rediscapacitystandardm%ddefault", capacityTier) + } else { + priceKey = fmt.Sprintf("Rediscapacitystandardnodem%d", capacityTier) + } + } + + pricePerHrPerGbInNanos := priceMap[priceKey].Regions[redis.LocationId].Price[0].Nanos + pricePerHrPerGbInDollars := pricePerHrPerGbInNanos / math.Pow(10, 9) + + now := time.Now().UTC() + startTime := getStartTime(redis.GetCreateTime().AsTime(), now) + + hours := now.Sub(startTime).Hours() + + cost := hours * pricePerHrPerGbInDollars + + if redis.ReadReplicasMode == redispb.Instance_READ_REPLICAS_ENABLED { + cost *= float64(redis.ReplicaCount) + } + + return cost +} + +func getStartTime(createTime, now time.Time) time.Time { + firstOfCurrentMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC) + if createTime.After(firstOfCurrentMonth) { + return createTime + } + return firstOfCurrentMonth +} + +func getCapacityTier(memorySizeGb int32, prices []int32) int { + capacityTier := 5 + for idx, price := range prices { + if memorySizeGb <= price { + capacityTier = idx + 1 + } + } + return capacityTier +}