Skip to content

Commit

Permalink
feat: introducing a karmada top command
Browse files Browse the repository at this point in the history
Signed-off-by: chaunceyjiang <chaunceyjiang@gmail.com>
  • Loading branch information
chaunceyjiang committed Jun 2, 2023
1 parent dff7a5b commit a4e738a
Show file tree
Hide file tree
Showing 15 changed files with 1,464 additions and 0 deletions.
2 changes: 2 additions & 0 deletions pkg/karmadactl/karmadactl.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/karmada-io/karmada/pkg/karmadactl/register"
"github.com/karmada-io/karmada/pkg/karmadactl/taint"
"github.com/karmada-io/karmada/pkg/karmadactl/token"
"github.com/karmada-io/karmada/pkg/karmadactl/top"
"github.com/karmada-io/karmada/pkg/karmadactl/unjoin"
"github.com/karmada-io/karmada/pkg/karmadactl/util"
"github.com/karmada-io/karmada/pkg/version/sharedcommand"
Expand Down Expand Up @@ -106,6 +107,7 @@ func NewKarmadaCtlCommand(cmdUse, parentCommand string) *cobra.Command {
Commands: []*cobra.Command{
apply.NewCmdApply(f, parentCommand, ioStreams),
promote.NewCmdPromote(f, parentCommand),
top.NewCmdTop(f, parentCommand, ioStreams),
},
},
}
Expand Down
207 changes: 207 additions & 0 deletions pkg/karmadactl/top/metrics_printer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
package top

import (
"fmt"
"io"
"sort"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/cli-runtime/pkg/printers"
"k8s.io/kubectl/pkg/metricsutil"
metricsapi "k8s.io/metrics/pkg/apis/metrics"
)

var (
MeasuredResources = []corev1.ResourceName{
corev1.ResourceCPU,
corev1.ResourceMemory,
}
PodColumns = []string{"NAME", "CPU(cores)", "MEMORY(bytes)"}
ClusterNameColumn = "CLUSTER"
NamespaceColumn = "NAMESPACE"
PodColumn = "POD"
)

type ResourceMetricsInfo struct {
Cluster string
Name string
Metrics corev1.ResourceList
Available corev1.ResourceList
}

type TopCmdPrinter struct {
out io.Writer
}

func NewTopCmdPrinter(out io.Writer) *TopCmdPrinter {
return &TopCmdPrinter{out: out}
}

func (printer *TopCmdPrinter) PrintPodMetrics(metrics []metricsapi.PodMetrics, printContainers, withNamespace, noHeaders bool, sortBy string, sum bool) error {
if len(metrics) == 0 {
return nil
}
w := printers.GetNewTabWriter(printer.out)
defer w.Flush()

columnWidth := len(PodColumns)
columnWidth++
if !noHeaders {
printValue(w, ClusterNameColumn)
if withNamespace {
printValue(w, NamespaceColumn)
columnWidth++
}
if printContainers {
printValue(w, PodColumn)
columnWidth++
}
printColumnNames(w, PodColumns)
}

sort.Sort(metricsutil.NewPodMetricsSorter(metrics, withNamespace, sortBy))

for i := range metrics {
if printContainers {
sort.Sort(metricsutil.NewContainerMetricsSorter(metrics[i].Containers, sortBy))
printSinglePodContainerMetrics(w, &metrics[i], withNamespace)
} else {
printSinglePodMetrics(w, &metrics[i], withNamespace)
}
}

if sum {
adder := NewResourceAdder(MeasuredResources)
for i := range metrics {
adder.AddPodMetrics(&metrics[i])
}
printPodResourcesSum(w, adder.total, columnWidth)
}

return nil
}

func printColumnNames(out io.Writer, names []string) {
for _, name := range names {
printValue(out, name)
}
fmt.Fprint(out, "\n")
}

func printSinglePodMetrics(out io.Writer, m *metricsapi.PodMetrics, withNamespace bool) {
podMetrics := getPodMetrics(m)
printValue(out, m.Annotations["resource.karmada.io/query-from-cluster"])
if withNamespace {
printValue(out, m.Namespace)
}
printMetricsLine(out, &ResourceMetricsInfo{
Name: m.Name,
Metrics: podMetrics,
Available: corev1.ResourceList{},
})
}

func printSinglePodContainerMetrics(out io.Writer, m *metricsapi.PodMetrics, withNamespace bool) {
for _, c := range m.Containers {
printValue(out, m.Annotations["resource.karmada.io/query-from-cluster"])
if withNamespace {
printValue(out, m.Namespace)
}
printValue(out, m.Name)
printMetricsLine(out, &ResourceMetricsInfo{
Name: c.Name,
Metrics: c.Usage,
Available: corev1.ResourceList{},
})
}
}

func getPodMetrics(m *metricsapi.PodMetrics) corev1.ResourceList {
podMetrics := make(corev1.ResourceList)
for _, res := range MeasuredResources {
podMetrics[res], _ = resource.ParseQuantity("0")
}

for _, c := range m.Containers {
for _, res := range MeasuredResources {
quantity := podMetrics[res]
quantity.Add(c.Usage[res])
podMetrics[res] = quantity
}
}
return podMetrics
}

func printMetricsLine(out io.Writer, metrics *ResourceMetricsInfo) {
printValue(out, metrics.Name)
printAllResourceUsages(out, metrics)
fmt.Fprint(out, "\n")
}

func printValue(out io.Writer, value interface{}) {
fmt.Fprintf(out, "%v\t", value)
}

func printAllResourceUsages(out io.Writer, metrics *ResourceMetricsInfo) {
for _, res := range MeasuredResources {
quantity := metrics.Metrics[res]
printSingleResourceUsage(out, res, quantity)
fmt.Fprint(out, "\t")
if available, found := metrics.Available[res]; found {
fraction := float64(quantity.MilliValue()) / float64(available.MilliValue()) * 100
fmt.Fprintf(out, "%d%%\t", int64(fraction))
}
}
}

func printSingleResourceUsage(out io.Writer, resourceType corev1.ResourceName, quantity resource.Quantity) {
switch resourceType {
case corev1.ResourceCPU:
fmt.Fprintf(out, "%vm", quantity.MilliValue())
case corev1.ResourceMemory:
fmt.Fprintf(out, "%vMi", quantity.Value()/(1024*1024))
default:
fmt.Fprintf(out, "%v", quantity.Value())
}
}

func printPodResourcesSum(out io.Writer, total corev1.ResourceList, columnWidth int) {
for i := 0; i < columnWidth-2; i++ {
printValue(out, "")
}
printValue(out, "________")
printValue(out, "________")
fmt.Fprintf(out, "\n")
for i := 0; i < columnWidth-3; i++ {
printValue(out, "")
}
printMetricsLine(out, &ResourceMetricsInfo{
Name: "",
Metrics: total,
Available: corev1.ResourceList{},
})
}

type ResourceAdder struct {
resources []corev1.ResourceName
total corev1.ResourceList
}

func NewResourceAdder(resources []corev1.ResourceName) *ResourceAdder {
return &ResourceAdder{
resources: resources,
total: make(corev1.ResourceList),
}
}

// AddPodMetrics adds each pod metric to the total
func (adder *ResourceAdder) AddPodMetrics(m *metricsapi.PodMetrics) {
for _, c := range m.Containers {
for _, res := range adder.resources {
total := adder.total[res]
total.Add(c.Usage[res])
adder.total[res] = total
}
}
}
58 changes: 58 additions & 0 deletions pkg/karmadactl/top/top.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package top

import (
"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/cli-runtime/pkg/genericclioptions"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/util/templates"
metricsapi "k8s.io/metrics/pkg/apis/metrics"
)

const (
sortByCPU = "cpu"
sortByMemory = "memory"
)

var (
supportedMetricsAPIVersions = []string{
"v1beta1",
}
topLong = templates.LongDesc(`
Display Resource (CPU/Memory) usage of member clusters.
The top command allows you to see the resource consumption for nodes or pods of member clusters.
This command requires karmada-metrics-adapter to be correctly configured and working on the Karmada control plane and
Metrics Server to be correctly configured and working on the member clusters . `)
)

func NewCmdTop(f cmdutil.Factory, parentCommand string, streams genericclioptions.IOStreams) *cobra.Command {
cmd := &cobra.Command{
Use: "top",
Short: "Display resource (CPU/memory) usage of member clusters",
Long: topLong,
Run: cmdutil.DefaultSubCommandRun(streams.ErrOut),
}

// create subcommands
cmd.AddCommand(NewCmdTopPod(f, nil, streams))

return cmd
}

func SupportedMetricsAPIVersionAvailable(discoveredAPIGroups *metav1.APIGroupList) bool {
for _, discoveredAPIGroup := range discoveredAPIGroups.Groups {
if discoveredAPIGroup.Name != metricsapi.GroupName {
continue
}
for _, version := range discoveredAPIGroup.Versions {
for _, supportedVersion := range supportedMetricsAPIVersions {
if version.Version == supportedVersion {
return true
}
}
}
}
return false
}
Loading

0 comments on commit a4e738a

Please sign in to comment.