Skip to content

Commit 47e107c

Browse files
committed
feat: add the show-stats flag
When this flag is used, the plugin writes another table for each namespace that contains various statistics measurements about resource requests and VPA recommendations.
1 parent 70769fe commit 47e107c

File tree

6 files changed

+146
-19
lines changed

6 files changed

+146
-19
lines changed

.codecov.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ coverage:
44
# Prevent small variations of coverage from failing CI.
55
project:
66
default:
7-
threshold: 1%
7+
threshold: 10% # TODO: reduce once we have a better coverage
88
patch: off
99
ignore:
10-
- internal/version
10+
- internal/version

cli/command.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ func (co *CommandOptions) Execute() error {
215215
}
216216

217217
// bindRecommendationsAndRequests returns a table that bind the
218-
// recommendations of the VPA in the list to the actual resource
218+
// recommendation of the VPA(s) in the list to the actual resource
219219
// requests of their target controller's pods.
220220
func (co *CommandOptions) bindRecommendationsAndRequests(list []*vpav1.VerticalPodAutoscaler) table {
221221
var table table

cli/flags.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const (
1818
flagShowKindShorthand = "k"
1919
flagShowContainers = "show-containers"
2020
flagShowContainersShorthand = "c"
21+
flagShowStats = "show-stats"
2122
flagNoColors = "no-colors"
2223
flagNoHeaders = "no-headers"
2324
flagSortOrder = "sort-order"
@@ -48,6 +49,7 @@ type Flags struct {
4849
ShowNamespace bool
4950
ShowKind bool
5051
ShowContainers bool
52+
ShowStats bool
5153
NoColors bool
5254
NoHeaders bool
5355
SortOrder sortOrder
@@ -108,6 +110,9 @@ func (f *Flags) AddFlags(flags *pflag.FlagSet) {
108110

109111
flags.Var(&f.RecommendationType, flagRecommendationType,
110112
fmt.Sprintf("The type of recommendation to use in comparisons. One of: %s", strings.Join(recommendationTypeFlagValues(), ", ")))
113+
114+
flags.BoolVar(&f.ShowStats, flagShowStats, f.ShowStats,
115+
"Show statistics about all VPA recommendations and requests")
111116
}
112117

113118
// Tidy post-processes the flags.

cli/table.go

Lines changed: 135 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@ import (
44
"fmt"
55
"io"
66
"math"
7+
"os"
78
"sort"
89
"strings"
910

11+
"github.com/dustin/go-humanize"
1012
"github.com/muesli/termenv"
1113
"github.com/olekukonko/tablewriter"
14+
"gopkg.in/inf.v0"
1215
"k8s.io/apimachinery/pkg/api/resource"
1316
"k8s.io/apimachinery/pkg/runtime/schema"
1417

@@ -141,8 +144,7 @@ const (
141144

142145
// Print writes the table to w.
143146
func (t table) Print(w io.Writer, flags *Flags) error {
144-
tw := tablewriter.NewWriter(w)
145-
setKubectlTableFormat(tw)
147+
tw := newKubectlTableWriter(w)
146148

147149
if !flags.NoHeaders {
148150
var headers []string
@@ -168,23 +170,140 @@ func (t table) Print(w io.Writer, flags *Flags) error {
168170
}
169171
tw.Render()
170172

173+
if flags.ShowStats {
174+
_, err := os.Stdout.WriteString("\n")
175+
if err != nil {
176+
return err
177+
}
178+
return t.printStats(w)
179+
}
171180
return nil
172181
}
173182

174-
// setKubectlTableFormat configures the given writer
175-
// to print according to the Kubectl output format.
176-
func setKubectlTableFormat(t *tablewriter.Table) {
177-
t.SetAutoWrapText(false)
178-
t.SetAutoFormatHeaders(true)
179-
t.SetNoWhiteSpace(true)
180-
t.SetHeaderLine(false)
181-
t.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
182-
t.SetAlignment(tablewriter.ALIGN_LEFT)
183-
t.SetCenterSeparator("")
184-
t.SetColumnSeparator("")
185-
t.SetRowSeparator("")
186-
t.SetTablePadding(" ")
187-
t.SetBorder(false)
183+
type tableStatFn func(column func(i int) *resource.Quantity) *resource.Quantity
184+
185+
func (t table) printStats(w io.Writer) error {
186+
tw := newKubectlTableWriter(w)
187+
188+
statFuncs := []tableStatFn{
189+
t.sumQuantities,
190+
t.meanQuantities,
191+
t.medianQuantities,
192+
}
193+
rows := []struct {
194+
name string
195+
getter func(i int) *resource.Quantity
196+
asBytes bool
197+
}{
198+
{"CPU Recommendations (# cores)", func(i int) *resource.Quantity { return t[i].Recommendations.CPU }, false},
199+
{"CPU Requests (# cores)", func(i int) *resource.Quantity { return t[i].Requests.CPU }, false},
200+
{"MEM Recommendations (IEC/SI)", func(i int) *resource.Quantity { return t[i].Recommendations.Memory }, true},
201+
{"MEM Requests (IEC/SI)", func(i int) *resource.Quantity { return t[i].Requests.Memory }, true},
202+
}
203+
for _, row := range rows {
204+
values := make([]string, 0, len(statFuncs))
205+
for _, fn := range statFuncs {
206+
q := fn(row.getter)
207+
208+
var str string
209+
if q == nil {
210+
str = tableUnsetCell
211+
} else {
212+
if row.asBytes {
213+
tmp := inf.Dec{}
214+
tmp.Round(q.AsDec(), 0, inf.RoundUp)
215+
big := tmp.UnscaledBig()
216+
str = humanize.BigIBytes(big) + "/" + humanize.BigBytes(big)
217+
str = strings.ReplaceAll(str, " ", "")
218+
} else {
219+
str = q.AsDec().String()
220+
}
221+
}
222+
values = append(values, str)
223+
}
224+
tw.Append(append([]string{row.name}, values...))
225+
}
226+
tw.SetHeader([]string{"Description", "Total", "Mean", "Median"})
227+
tw.Render()
228+
229+
return nil
230+
}
231+
232+
func (t table) sumQuantities(column func(i int) *resource.Quantity) *resource.Quantity {
233+
var sum resource.Quantity
234+
for i := range t {
235+
v := column(i)
236+
if v != nil {
237+
sum.Add(*v)
238+
}
239+
}
240+
return &sum
241+
}
242+
243+
func (t table) meanQuantities(column func(i int) *resource.Quantity) *resource.Quantity {
244+
sum := t.sumQuantities(column)
245+
dec := sum.AsDec()
246+
tmp := inf.Dec{}
247+
tmp.QuoRound(dec, inf.NewDec(int64(len(t)), 0), dec.Scale(), inf.RoundDown)
248+
249+
return resource.NewDecimalQuantity(tmp, resource.DecimalSI)
250+
}
251+
252+
func (t table) medianQuantities(column func(i int) *resource.Quantity) *resource.Quantity {
253+
var values []*resource.Quantity
254+
255+
// Collect all values and sort them.
256+
for i := range t {
257+
v := column(i)
258+
if v != nil {
259+
values = append(values, v)
260+
}
261+
}
262+
sort.Slice(values, func(i, j int) bool {
263+
b := compareQuantities(values[i], values[j])
264+
switch b {
265+
case -1:
266+
return true
267+
default:
268+
return false
269+
}
270+
})
271+
// No math is needed if there are no numbers.
272+
// For even numbers we add the two middle values
273+
// and divide by two.
274+
// For odd numbers we just use the middle value.
275+
l := len(values)
276+
if l == 0 {
277+
return nil
278+
} else if l%2 == 0 {
279+
q := values[l/2-1]
280+
q.Add(*(values[l/2+1]))
281+
tmp := inf.Dec{}
282+
tmp.QuoRound(q.AsDec(), inf.NewDec(2, 0), 0, inf.RoundDown)
283+
284+
return resource.NewDecimalQuantity(tmp, resource.DecimalSI)
285+
}
286+
return values[l/2]
287+
}
288+
289+
// newKubectlTableWriter returns a new table writer that writes
290+
// to w and print according to the Kubectl output format.
291+
func newKubectlTableWriter(w io.Writer) *tablewriter.Table {
292+
tw := tablewriter.NewWriter(w)
293+
294+
tw.SetAutoWrapText(false)
295+
tw.SetAutoFormatHeaders(true)
296+
tw.SetNoWhiteSpace(true)
297+
tw.SetHeaderLine(false)
298+
tw.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
299+
tw.SetAlignment(tablewriter.ALIGN_LEFT)
300+
tw.SetCenterSeparator("")
301+
tw.SetColumnSeparator("")
302+
tw.SetRowSeparator("")
303+
tw.SetTablePadding(" ")
304+
tw.SetBorder(false)
305+
306+
return tw
188307
}
189308

190309
// multiTableSorter implements the sort.Sort interface

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/wI2L/kubectl-vpa-recommendation
33
go 1.17
44

55
require (
6+
github.com/dustin/go-humanize v1.0.0
67
github.com/muesli/termenv v0.9.0
78
github.com/olekukonko/tablewriter v0.0.5
89
github.com/spf13/cobra v1.3.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,8 @@ github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8
151151
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
152152
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
153153
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
154+
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
155+
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
154156
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc=
155157
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
156158
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=

0 commit comments

Comments
 (0)