cost-optimization/gke-shift-left-cost/api/cost.go (290 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 (
"fmt"
"math"
"strings"
"github.com/leekchan/accounting"
"github.com/olekukonko/tablewriter"
)
const (
upArrow = "↑"
downArrow = "↓"
)
var headers = []string{"MIN REQUESTED",
"MIN REQ + HPA CPU BUFFER",
"MAX REQUESTED",
"MIN LIMITED",
"MAX LIMITED"}
// Cost groups cost range by kinda
type Cost struct {
MonthlyRanges []CostRange
}
// CostRange represent the range of estimated value
type CostRange struct {
Kind string `json:"kind"`
MinRequested float64 `json:"minRequested"`
MaxRequested float64 `json:"maxRequested"`
HPABuffer float64 `json:"hpaBuffer"` //Note: currently only supports CPU Target utilizaiton
MinLimited float64 `json:"minLimited"`
MaxLimited float64 `json:"maxLimited"`
}
// DiffCost holds the total difference between two costs
type DiffCost struct {
Summary string
hascostIncd bool
CostCurr Cost
CostPrev Cost
MonthlyDiffRange DiffCostRange
}
// DiffCostRange holds the total difference between two costs
type DiffCostRange struct {
Kind string
CostCurr CostRange
CostPrev CostRange
DiffValue CostRange
DiffPercentage CostRange
}
// MonthlyTotal returns the sum for all MonthlyRanges
func (c *Cost) MonthlyTotal() CostRange {
totalMonthlyRange := CostRange{Kind: "MonthlyTotal"}
for _, monthlyRange := range c.MonthlyRanges {
totalMonthlyRange = totalMonthlyRange.Add(monthlyRange)
}
return totalMonthlyRange
}
// Subtract current total cost from previous total cost
func (c *Cost) Subtract(costPrev Cost) DiffCost {
cr := c.MonthlyTotal()
diff := cr.Subtract(costPrev.MonthlyTotal())
summary, hascostIncd := diff.status()
return DiffCost{
Summary: summary,
hascostIncd: hascostIncd,
CostCurr: *c,
CostPrev: costPrev,
MonthlyDiffRange: diff,
}
}
// ToMarkdown convert to Markdown string
func (c *Cost) ToMarkdown() string {
data := [][]string{}
total := CostRange{Kind: bold("TOTAL")}
for _, mr := range c.MonthlyRanges {
data = append(data,
[]string{mr.Kind,
currency(mr.MinRequested),
currency(mr.HPABuffer),
currency(mr.MaxRequested),
currency(mr.MinLimited),
currency(mr.MaxLimited)})
total.MinRequested = total.MinRequested + mr.MinRequested
total.HPABuffer = total.HPABuffer + mr.HPABuffer
total.MaxRequested = total.MaxRequested + mr.MaxRequested
total.MinLimited = total.MinLimited + mr.MinLimited
total.MaxLimited = total.MaxLimited + mr.MaxLimited
}
data = append(data,
[]string{total.Kind,
bold(currency(total.MinRequested)),
bold(currency(total.HPABuffer)),
bold(currency(total.MaxRequested)),
bold(currency(total.MinLimited)),
bold(currency(total.MaxLimited))})
out := &strings.Builder{}
table := tablewriter.NewWriter(out)
table.SetHeader(
[]string{"Kind",
headers[0] + " (USD)",
headers[1] + " (USD)",
headers[2] + " (USD)",
headers[3] + " (USD)",
headers[4] + " (USD)"})
table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false})
table.SetCenterSeparator("|")
table.SetColumnAlignment([]int{0, 2, 2, 2, 2, 2})
table.AppendBulk(data)
table.Render()
return out.String()
}
func bold(val string) string {
return fmt.Sprintf("**%s**", val)
}
// Add sums the given costrange to the current costrange
func (c *CostRange) Add(costRange CostRange) CostRange {
ret := CostRange{Kind: c.Kind}
ret.MinRequested = c.MinRequested + costRange.MinRequested
ret.MaxRequested = c.MaxRequested + costRange.MaxRequested
ret.HPABuffer = c.HPABuffer + costRange.HPABuffer
ret.MinLimited = c.MinLimited + costRange.MinLimited
ret.MaxLimited = c.MaxLimited + costRange.MaxLimited
return ret
}
// Subtract subtracts the given costrange to the current costrange
func (c *CostRange) Subtract(costRangePrev CostRange) DiffCostRange {
diff := CostRange{Kind: c.Kind}
diff.MinRequested = c.MinRequested - costRangePrev.MinRequested
diff.MaxRequested = c.MaxRequested - costRangePrev.MaxRequested
diff.HPABuffer = c.HPABuffer - costRangePrev.HPABuffer
diff.MinLimited = c.MinLimited - costRangePrev.MinLimited
diff.MaxLimited = c.MaxLimited - costRangePrev.MaxLimited
diffP := CostRange{Kind: c.Kind}
diffP.MinRequested = diff.MinRequested * 100 / c.MinRequested
diffP.MaxRequested = diff.MaxRequested * 100 / c.MaxRequested
diffP.HPABuffer = diff.HPABuffer * 100 / c.HPABuffer
diffP.MinLimited = diff.MinLimited * 100 / c.MinLimited
diffP.MaxLimited = diff.MaxLimited * 100 / c.MaxLimited
return DiffCostRange{
Kind: c.Kind,
CostCurr: *c,
CostPrev: costRangePrev,
DiffValue: diff,
DiffPercentage: diffP,
}
}
func (c *CostRange) max() float64 {
max := c.MinRequested
if c.MaxRequested > max {
max = c.MaxRequested
}
if c.HPABuffer > max {
max = c.HPABuffer
}
if c.MinLimited > max {
max = c.MinLimited
}
if c.MaxLimited > max {
max = c.MaxLimited
}
return max
}
// ToMarkdown convert to Markdown string
func (d *DiffCost) ToMarkdown() string {
current := fmt.Sprintf("## Current Monthly Cost\n\n%s", d.CostCurr.ToMarkdown())
previous := fmt.Sprintf("## Previous Monthly Cost\n\n%s", d.CostPrev.ToMarkdown())
diff := d.MonthlyDiffRange.ToMarkdown()
total := fmt.Sprintf("## Difference in Costs\n\n**Summary:** %s\n\n%s", d.Summary, diff)
return fmt.Sprintf("%s\n\n%s\n\n%s", previous, current, total)
}
//---
// PriceMaxDiff ...
type PriceMaxDiff struct {
USD float64 `json:"usd"`
Perc float64 `json:"perc"`
}
// PriceSummary ...
type PriceSummary struct {
PossiblyCostIncrease bool `json:"possiblyCostIncrease"`
MaxDiff PriceMaxDiff `json:"maxDiff"`
}
// PriceDetails ...
type PriceDetails struct {
USD CostRange `json:"usd"`
Perc CostRange `json:"perc"`
}
// PriceDiff ...
type PriceDiff struct {
Summary PriceSummary `json:"summary"`
Details PriceDetails `json:"details"`
}
// ToPriceDiff struct
func (d *DiffCostRange) ToPriceDiff() PriceDiff {
return PriceDiff{
Summary: d.ToPriceSummary(),
Details: d.ToPriceDetails(),
}
}
// ToPriceSummary struct
func (d *DiffCostRange) ToPriceSummary() PriceSummary {
_, costIncrease := d.status()
return PriceSummary{
PossiblyCostIncrease: costIncrease,
MaxDiff: d.ToPriceMaxDiff(),
}
}
// ToPriceMaxDiff struct
func (d *DiffCostRange) ToPriceMaxDiff() PriceMaxDiff {
return PriceMaxDiff{
USD: math.Floor(d.DiffValue.max()*100) / 100,
Perc: math.Floor(d.DiffPercentage.max()*100) / 100,
}
}
// ToPriceDetails struct
func (d *DiffCostRange) ToPriceDetails() PriceDetails {
return PriceDetails{
USD: d.DiffValue,
Perc: d.DiffPercentage,
}
}
// ToMarkdown convert to Markdown string
func (d *DiffCostRange) ToMarkdown() string {
data := [][]string{
{bold(headers[0]), currency(d.CostPrev.MinRequested), currency(d.CostCurr.MinRequested), currencyDiff(d.DiffValue.MinRequested), percDiff(d.DiffPercentage.MinRequested)},
{bold(headers[1]), currency(d.CostPrev.HPABuffer), currency(d.CostCurr.HPABuffer), currencyDiff(d.DiffValue.HPABuffer), percDiff(d.DiffPercentage.HPABuffer)},
{bold(headers[2]), currency(d.CostPrev.MaxRequested), currency(d.CostCurr.MaxRequested), currencyDiff(d.DiffValue.MaxRequested), percDiff(d.DiffPercentage.MaxRequested)},
{bold(headers[3]), currency(d.CostPrev.MinLimited), currency(d.CostCurr.MinLimited), currencyDiff(d.DiffValue.MinLimited), percDiff(d.DiffPercentage.MinLimited)},
{bold(headers[4]), currency(d.CostPrev.MaxLimited), currency(d.CostCurr.MaxLimited), currencyDiff(d.DiffValue.MaxLimited), percDiff(d.DiffPercentage.MaxLimited)},
}
out := &strings.Builder{}
table := tablewriter.NewWriter(out)
table.SetHeader([]string{"Cost Variation", "Previous (USD)", "Current (USD)", "Difference (USD)", "Difference (%)"})
table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false})
table.SetCenterSeparator("|")
table.SetColumnAlignment([]int{0, 2, 2, 2, 2})
table.AppendBulk(data)
table.Render()
return out.String()
}
func (d *DiffCostRange) status() (summary string, costIncrease bool) {
var costInc, costDec []string
if d.DiffPercentage.MinRequested > 0 {
costInc = append(costInc, headers[0])
}
if d.DiffPercentage.HPABuffer > 0 {
costInc = append(costInc, headers[1])
}
if d.DiffPercentage.MaxRequested > 0 {
costInc = append(costInc, headers[2])
}
if d.DiffPercentage.MinLimited > 0 {
costInc = append(costInc, headers[3])
}
if d.DiffPercentage.MaxLimited > 0 {
costInc = append(costInc, headers[4])
}
if d.DiffPercentage.MinRequested < 0 {
costDec = append(costDec, headers[0])
}
if d.DiffPercentage.HPABuffer < 0 {
costDec = append(costDec, headers[1])
}
if d.DiffPercentage.MaxRequested < 0 {
costDec = append(costDec, headers[2])
}
if d.DiffPercentage.MinLimited < 0 {
costDec = append(costDec, headers[3])
}
if d.DiffPercentage.MaxLimited < 0 {
costDec = append(costDec, headers[4])
}
if len(costInc) > 0 {
costIncrease = true
summary = summary + fmt.Sprintf("There are increase in costs on: '%s'", strings.Join(costInc, "', '"))
}
if len(costDec) > 0 {
start := "There"
if len(summary) > 0 {
start = ". And there"
}
summary = summary + fmt.Sprintf("%s are decrease in costs on: '%s'", start, strings.Join(costDec, "', '"))
}
if len(costInc) == 0 && len(costDec) == 0 {
summary = "No cost change found!"
}
return
}
// --- helper functions ---
func currency(value float64) string {
ac := accounting.Accounting{Symbol: "$", Precision: 2}
return ac.FormatMoneyFloat64(value)
}
func currencyDiff(value float64) string {
ac := accounting.Accounting{Symbol: "$", Precision: 2, FormatZero: " "}
valueFormated := ac.FormatMoneyFloat64(value)
if value > 0 {
return fmt.Sprintf("**+%s (%s)**", valueFormated, upArrow)
} else if value < 0 {
return fmt.Sprintf("%s (%s)", valueFormated, downArrow)
}
return valueFormated
}
func percDiff(perc float64) string {
if perc != 0 {
percFormated := fmt.Sprintf("%.2f%%", perc)
if perc > 0 {
return fmt.Sprintf("**+%s (%s)**", percFormated, upArrow)
} else if perc < 0 {
return fmt.Sprintf("%s (%s)", percFormated, downArrow)
}
return percFormated
}
return " "
}