Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for cost difference in "predict" using new API #141

Merged
merged 2 commits into from
Feb 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ Example output:
+-------------------+-----------+----------+----------+-------------+----------+----------+----------+-------------+--------------------+
```

Predict the cost of a YAML spec based on its requests:
Predict the cost impact of a YAML spec based on its requests:
``` sh
read -r -d '' DEF << EndOfMessage
apiVersion: apps/v1
Expand Down Expand Up @@ -110,11 +110,11 @@ echo "$DEF" | kubectl cost predict -f -
```
Example output:
```
+-----------------------------+-----+-----+------------+-----------+------------+
| WORKLOAD | CPU | MEM | CPU/MO | MEM/MO | TOTAL/MO |
+-----------------------------+-----+-----+------------+-----------+------------+
| Deployment/nginx-deployment | 9 | 6Gi | 209.51 USD | 18.73 USD | 228.24 USD |
+-----------------------------+-----+-----+------------+-----------+------------+
+-------------------------------------+-----+-----+-----+------------+-----------+----------+-----------+-----------+------------+
| WORKLOAD | CPU | MEM | GPU | CPU/MO | MEM/MO | GPU/MO | Δ CPU/MO | Δ MEM/MO | TOTAL/MO |
+-------------------------------------+-----+-----+-----+------------+-----------+----------+-----------+-----------+------------+
| default/Deployment/nginx-deployment | 9 | 6Gi | 0 | 207.68 USD | 18.56 USD | 0.00 USD | 38.30 USD | 11.51 USD | 226.24 USD |
+-------------------------------------+-----+-----+-----+------------+-----------+----------+-----------+-----------+------------+
```

Show how much each namespace cost over the past 5 days
Expand Down Expand Up @@ -227,6 +227,8 @@ Kubecost/OpenCost APIs:

--allocation-path string URL path at which Allocation queries can be served from the configured service. If using OpenCost, you may want to set this to '/allocation/compute' (default "/model/allocation")
--predict-resource-cost-path string URL path at which Resource Cost Prediction queries can be served from the configured service. (default "/model/prediction/resourcecost")
--predict-resource-cost-diff-path string URL path at which Resource Cost Prediction diff queries can be served from the configured service. (default "/model/prediction/resourcecostdiff")
--no-diff Set true to not attempt a cost difference with a matching in-cluster workload, if one can be found.
```


Expand Down
1 change: 1 addition & 0 deletions pkg/cmd/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ func addQueryBackendOptionsFlags(cmd *cobra.Command, options *query.QueryBackend
cmd.Flags().BoolVar(&options.UseProxy, "use-proxy", false, "Instead of temporarily port-forwarding, proxy a request to Kubecost through the Kubernetes API server.")
cmd.Flags().StringVar(&options.AllocationPath, "allocation-path", "/model/allocation", "URL path at which Allocation queries can be served from the configured service. If using OpenCost, you may want to set this to '/allocation/compute'")
cmd.Flags().StringVar(&options.PredictResourceCostPath, "predict-resource-cost-path", "/model/prediction/resourcecost", "URL path at which Resource Cost Prediction queries can be served from the configured service.")
cmd.Flags().StringVar(&options.PredictResourceCostDiffPath, "predict-resource-cost-diff-path", "/model/prediction/resourcecostdiff", "URL path at which Resource Cost Prediction diff queries can be served from the configured service.")

//Check if environment variable KUBECTL_COST_USE_PROXY is set, it defaults to false
v := viper.New()
Expand Down
12 changes: 11 additions & 1 deletion pkg/cmd/cost.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ type KubeOptions struct {
restConfig *rest.Config
args []string

// Namespace should be the currently-configured defaultNamespace of the client.
// This allows e.g. predict to fill in the defaultNamespace if one is not provided
// in the workload spec.
defaultNamespace string

genericclioptions.IOStreams
}

Expand Down Expand Up @@ -194,7 +199,12 @@ func (o *KubeOptions) Complete(cmd *cobra.Command, args []string) error {

o.restConfig, err = o.configFlags.ToRESTConfig()
if err != nil {
return err
return fmt.Errorf("converting to REST config: %s", err)
}

o.defaultNamespace, _, err = o.configFlags.ToRawKubeConfigLoader().Namespace()
if err != nil {
return fmt.Errorf("retrieving default namespace: %s", err)
}

return nil
Expand Down
186 changes: 98 additions & 88 deletions pkg/cmd/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,111 +29,117 @@ const (
CPUCostCol = "CPU Cost"
RAMCostCol = "RAM Cost"

PredictColWorkload = "Workload"
PredictColReqCPU = "CPU"
PredictColReqMemory = "Mem"
PredictColReqGPU = "GPU"
PredictColMoCoreHours = "Mo. core-hrs"
PredictColMoGibHours = "Mo. GiB-hrs"
PredictColMoGPUHours = "Mo. GPU-hrs"
PredictColCostCoreHr = "Cost/core-hr"
PredictColCostGiBHr = "Cost/GiB-hr"
PredictColCostGPUHr = "Cost/GPU-hr"
PredictColMoCostCPU = "CPU/mo"
PredictColMoCostMemory = "Mem/mo"
PredictColMoCostGPU = "GPU/mo"
PredictColMoCostTotal = "Total/mo"
PredictColWorkload = "Workload"
PredictColReqCPU = "CPU"
PredictColReqMemory = "Mem"
PredictColReqGPU = "GPU"
PredictColMoCoreHours = "Mo. core-hrs"
PredictColMoGibHours = "Mo. GiB-hrs"
PredictColMoGPUHours = "Mo. GPU-hrs"
PredictColCostCoreHr = "Cost/core-hr"
PredictColCostGiBHr = "Cost/GiB-hr"
PredictColCostGPUHr = "Cost/GPU-hr"
PredictColMoCostCPU = "CPU/mo"
PredictColMoCostMemory = "Mem/mo"
PredictColMoCostGPU = "GPU/mo"
PredictColMoCostTotal = "Total/mo"
PredictColMoCostDiffCPU = "Δ CPU/mo"
PredictColMoCostDiffMemory = "Δ Mem/mo"
)

func formatFloat(f float64) string {
return fmt.Sprintf("%.6f", f)
}

func writePredictionTable(out io.Writer, rowData []predictRowData, currencyCode string, showCostPerResourceHr bool) {
t := makePredictionTable(rowData, currencyCode, showCostPerResourceHr)
type predictionTableOptions struct {
currencyCode string
showCostPerResourceHr bool
noDiff bool
}

func writePredictionTable(out io.Writer, rowData []predictRowData, opts predictionTableOptions) {
t := makePredictionTable(rowData, opts)
t.SetOutputMirror(out)
t.Render()
}

func makePredictionTable(rowData []predictRowData, currencyCode string, showCostPerResourceHr bool) table.Writer {
func makePredictionTable(rowData []predictRowData, opts predictionTableOptions) table.Writer {
t := table.NewWriter()

columnConfigs := []table.ColumnConfig{
table.ColumnConfig{
t.SetColumnConfigs([]table.ColumnConfig{
{
Name: PredictColWorkload,
},
table.ColumnConfig{
{
Name: PredictColReqCPU,
},
table.ColumnConfig{
{
Name: PredictColReqMemory,
},
table.ColumnConfig{
{
Name: PredictColReqGPU,
},
}

if showCostPerResourceHr {
columnConfigs = append(columnConfigs, []table.ColumnConfig{
table.ColumnConfig{
Name: PredictColCostCoreHr,
},
table.ColumnConfig{
Name: PredictColCostGiBHr,
},
table.ColumnConfig{
Name: PredictColCostGPUHr,
},
}...)
}

columnConfigs = append(columnConfigs, []table.ColumnConfig{
table.ColumnConfig{
{
Name: PredictColCostCoreHr,
Hidden: !opts.showCostPerResourceHr,
},
{
Name: PredictColCostGiBHr,
Hidden: !opts.showCostPerResourceHr,
},
{
Name: PredictColCostGPUHr,
Hidden: !opts.showCostPerResourceHr,
},
{
Name: PredictColMoCostCPU,
Align: text.AlignRight,
AlignFooter: text.AlignRight,
},
table.ColumnConfig{
{
Name: PredictColMoCostMemory,
Align: text.AlignRight,
AlignFooter: text.AlignRight,
},
table.ColumnConfig{
{
Name: PredictColMoCostGPU,
Align: text.AlignRight,
AlignFooter: text.AlignRight,
},
table.ColumnConfig{
{
Name: PredictColMoCostDiffCPU,
Hidden: opts.noDiff,
Align: text.AlignRight,
AlignFooter: text.AlignRight,
},
{
Name: PredictColMoCostDiffMemory,
Hidden: opts.noDiff,
Align: text.AlignRight,
AlignFooter: text.AlignRight,
},
{
Name: PredictColMoCostTotal,
Align: text.AlignRight,
AlignFooter: text.AlignRight,
},
}...)
t.SetColumnConfigs(columnConfigs)
})

headerRow := table.Row{
t.AppendHeader(table.Row{
PredictColWorkload,
PredictColReqCPU,
PredictColReqMemory,
PredictColReqGPU,
}

if showCostPerResourceHr {
headerRow = append(headerRow,
PredictColCostCoreHr,
PredictColCostGiBHr,
PredictColCostGPUHr,
)
}

headerRow = append(headerRow,
PredictColCostCoreHr,
PredictColCostGiBHr,
PredictColCostGPUHr,
PredictColMoCostCPU,
PredictColMoCostMemory,
PredictColMoCostGPU,
PredictColMoCostDiffCPU,
PredictColMoCostDiffMemory,
PredictColMoCostTotal,
)

t.AppendHeader(headerRow)
})

t.SortBy([]table.SortBy{
{
Expand All @@ -145,48 +151,52 @@ func makePredictionTable(rowData []predictRowData, currencyCode string, showCost
var summedMonthlyCPU float64
var summedMonthlyMem float64
var summedMonthlyGPU float64
var summedMonthlyDiffCPU float64
var summedMonthlyDiffMemory float64
var summedMonthlyTotal float64

for _, rowDatum := range rowData {
row := table.Row{}
row = append(row, fmt.Sprintf("%s/%s", rowDatum.workloadType, rowDatum.workloadName))
row = append(row, rowDatum.cpuStr)
row = append(row, rowDatum.memStr)
row = append(row, rowDatum.gpuStr)

if showCostPerResourceHr {
row = append(row, fmt.Sprintf("%.4f %s", rowDatum.prediction.DerivedCostPerCPUCoreHour, currencyCode))
row = append(row, fmt.Sprintf("%.4f %s", rowDatum.prediction.DerivedCostPerMemoryByteHour*1024*1024*1024, currencyCode))
row = append(row, fmt.Sprintf("%.4f %s", rowDatum.prediction.DerivedCostPerGPUHour, currencyCode))
}

row = append(row, fmt.Sprintf("%.2f %s", rowDatum.prediction.MonthlyCostCPU, currencyCode))
row = append(row, fmt.Sprintf("%.2f %s", rowDatum.prediction.MonthlyCostMemory, currencyCode))
row = append(row, fmt.Sprintf("%.2f %s", rowDatum.prediction.MonthlyCostGPU, currencyCode))
row = append(row, fmt.Sprintf("%.2f %s", rowDatum.prediction.MonthlyCostTotal, currencyCode))

summedMonthlyCPU += rowDatum.prediction.MonthlyCostCPU
summedMonthlyMem += rowDatum.prediction.MonthlyCostMemory
summedMonthlyGPU += rowDatum.prediction.MonthlyCostGPU
summedMonthlyTotal += rowDatum.prediction.MonthlyCostTotal
row = append(row, fmt.Sprintf("%s/%s/%s", rowDatum.workloadNamespace, rowDatum.workloadType, rowDatum.workloadName))
row = append(row, rowDatum.totalCPURequested)
row = append(row, rowDatum.totalMemoryRequested)
row = append(row, rowDatum.totalGPURequested)

row = append(row, fmt.Sprintf("%.4f %s", rowDatum.cpuCostMonthly/rowDatum.requestedCPUCoreHours, opts.currencyCode))
row = append(row, fmt.Sprintf("%.4f %s", (rowDatum.memoryCostMonthly/rowDatum.requestedMemoryByteHours)*1024*1024*1024, opts.currencyCode))
row = append(row, fmt.Sprintf("%.4f %s", rowDatum.gpuCostMonthly/rowDatum.requestedGPUHours, opts.currencyCode))

row = append(row, fmt.Sprintf("%.2f %s", rowDatum.cpuCostMonthly, opts.currencyCode))
row = append(row, fmt.Sprintf("%.2f %s", rowDatum.memoryCostMonthly, opts.currencyCode))
row = append(row, fmt.Sprintf("%.2f %s", rowDatum.gpuCostMonthly, opts.currencyCode))
row = append(row, fmt.Sprintf("%.2f %s", rowDatum.cpuCostChangeMonthly, opts.currencyCode))
row = append(row, fmt.Sprintf("%.2f %s", rowDatum.memoryCostChangeMonthly, opts.currencyCode))
row = append(row, fmt.Sprintf("%.2f %s", rowDatum.totalCostMonthly, opts.currencyCode))

summedMonthlyCPU += rowDatum.cpuCostMonthly
summedMonthlyMem += rowDatum.memoryCostMonthly
summedMonthlyGPU += rowDatum.gpuCostMonthly
summedMonthlyDiffCPU += rowDatum.cpuCostChangeMonthly
summedMonthlyDiffMemory += rowDatum.memoryCostChangeMonthly
summedMonthlyTotal += rowDatum.totalCostMonthly

t.AppendRow(row)
}

// A summary footer is redundant if there is only one row
if len(rowData) > 1 {
footerRow := table.Row{}
blankRows := 4
if showCostPerResourceHr {
blankRows += 2
}
blankRows := 7

for i := 0; i < blankRows; i++ {
footerRow = append(footerRow, "")
}
footerRow = append(footerRow, fmt.Sprintf("%.2f %s", summedMonthlyCPU, currencyCode))
footerRow = append(footerRow, fmt.Sprintf("%.2f %s", summedMonthlyMem, currencyCode))
footerRow = append(footerRow, fmt.Sprintf("%.2f %s", summedMonthlyGPU, currencyCode))
footerRow = append(footerRow, fmt.Sprintf("%.2f %s", summedMonthlyTotal, currencyCode))
footerRow = append(footerRow, fmt.Sprintf("%.2f %s", summedMonthlyCPU, opts.currencyCode))
footerRow = append(footerRow, fmt.Sprintf("%.2f %s", summedMonthlyMem, opts.currencyCode))
footerRow = append(footerRow, fmt.Sprintf("%.2f %s", summedMonthlyGPU, opts.currencyCode))
footerRow = append(footerRow, fmt.Sprintf("%.2f %s", summedMonthlyDiffCPU, opts.currencyCode))
footerRow = append(footerRow, fmt.Sprintf("%.2f %s", summedMonthlyDiffMemory, opts.currencyCode))
footerRow = append(footerRow, fmt.Sprintf("%.2f %s", summedMonthlyTotal, opts.currencyCode))
t.AppendFooter(footerRow)
}

Expand Down
Loading