Implement kubectl top command with subcommands.

pull/6/head
mksalawa 2016-08-11 17:24:22 +02:00
parent 2fb7cae2be
commit 5df9fe684d
22 changed files with 1387 additions and 0 deletions

View File

@ -68,6 +68,9 @@ docs/man/man1/kubectl-set-image.1
docs/man/man1/kubectl-set.1
docs/man/man1/kubectl-stop.1
docs/man/man1/kubectl-taint.1
docs/man/man1/kubectl-top-node.1
docs/man/man1/kubectl-top-pod.1
docs/man/man1/kubectl-top.1
docs/man/man1/kubectl-uncordon.1
docs/man/man1/kubectl-version.1
docs/man/man1/kubectl.1
@ -133,6 +136,9 @@ docs/user-guide/kubectl/kubectl_scale.md
docs/user-guide/kubectl/kubectl_set.md
docs/user-guide/kubectl/kubectl_set_image.md
docs/user-guide/kubectl/kubectl_taint.md
docs/user-guide/kubectl/kubectl_top.md
docs/user-guide/kubectl/kubectl_top_node.md
docs/user-guide/kubectl/kubectl_top_pod.md
docs/user-guide/kubectl/kubectl_uncordon.md
docs/user-guide/kubectl/kubectl_version.md
docs/yaml/kubectl/kubectl.yaml
@ -169,5 +175,6 @@ docs/yaml/kubectl/kubectl_scale.yaml
docs/yaml/kubectl/kubectl_set.yaml
docs/yaml/kubectl/kubectl_stop.yaml
docs/yaml/kubectl/kubectl_taint.yaml
docs/yaml/kubectl/kubectl_top.yaml
docs/yaml/kubectl/kubectl_uncordon.yaml
docs/yaml/kubectl/kubectl_version.yaml

View File

@ -31,5 +31,6 @@ kubectl-rolling-update.1
kubectl-run.1
kubectl-scale.1
kubectl-stop.1
kubectl-top.1
kubectl-version.1
kubectl.1

View File

@ -0,0 +1,3 @@
This file is autogenerated, but we've stopped checking such files into the
repository to reduce the need for rebases. Please run hack/generate-docs.sh to
populate this file.

View File

@ -0,0 +1,3 @@
This file is autogenerated, but we've stopped checking such files into the
repository to reduce the need for rebases. Please run hack/generate-docs.sh to
populate this file.

View File

@ -0,0 +1,3 @@
This file is autogenerated, but we've stopped checking such files into the
repository to reduce the need for rebases. Please run hack/generate-docs.sh to
populate this file.

View File

@ -0,0 +1,36 @@
<!-- BEGIN MUNGE: UNVERSIONED_WARNING -->
<!-- BEGIN STRIP_FOR_RELEASE -->
<img src="http://kubernetes.io/kubernetes/img/warning.png" alt="WARNING"
width="25" height="25">
<img src="http://kubernetes.io/kubernetes/img/warning.png" alt="WARNING"
width="25" height="25">
<img src="http://kubernetes.io/kubernetes/img/warning.png" alt="WARNING"
width="25" height="25">
<img src="http://kubernetes.io/kubernetes/img/warning.png" alt="WARNING"
width="25" height="25">
<img src="http://kubernetes.io/kubernetes/img/warning.png" alt="WARNING"
width="25" height="25">
<h2>PLEASE NOTE: This document applies to the HEAD of the source tree</h2>
If you are using a released version of Kubernetes, you should
refer to the docs that go with that version.
Documentation for other releases can be found at
[releases.k8s.io](http://releases.k8s.io).
</strong>
--
<!-- END STRIP_FOR_RELEASE -->
<!-- END MUNGE: UNVERSIONED_WARNING -->
This file is autogenerated, but we've stopped checking such files into the
repository to reduce the need for rebases. Please run hack/generate-docs.sh to
populate this file.
<!-- BEGIN MUNGE: GENERATED_ANALYTICS -->
[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/user-guide/kubectl/kubectl_top-node.md?pixel)]()
<!-- END MUNGE: GENERATED_ANALYTICS -->

View File

@ -0,0 +1,36 @@
<!-- BEGIN MUNGE: UNVERSIONED_WARNING -->
<!-- BEGIN STRIP_FOR_RELEASE -->
<img src="http://kubernetes.io/kubernetes/img/warning.png" alt="WARNING"
width="25" height="25">
<img src="http://kubernetes.io/kubernetes/img/warning.png" alt="WARNING"
width="25" height="25">
<img src="http://kubernetes.io/kubernetes/img/warning.png" alt="WARNING"
width="25" height="25">
<img src="http://kubernetes.io/kubernetes/img/warning.png" alt="WARNING"
width="25" height="25">
<img src="http://kubernetes.io/kubernetes/img/warning.png" alt="WARNING"
width="25" height="25">
<h2>PLEASE NOTE: This document applies to the HEAD of the source tree</h2>
If you are using a released version of Kubernetes, you should
refer to the docs that go with that version.
Documentation for other releases can be found at
[releases.k8s.io](http://releases.k8s.io).
</strong>
--
<!-- END STRIP_FOR_RELEASE -->
<!-- END MUNGE: UNVERSIONED_WARNING -->
This file is autogenerated, but we've stopped checking such files into the
repository to reduce the need for rebases. Please run hack/generate-docs.sh to
populate this file.
<!-- BEGIN MUNGE: GENERATED_ANALYTICS -->
[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/user-guide/kubectl/kubectl_top-pod.md?pixel)]()
<!-- END MUNGE: GENERATED_ANALYTICS -->

View File

@ -0,0 +1,36 @@
<!-- BEGIN MUNGE: UNVERSIONED_WARNING -->
<!-- BEGIN STRIP_FOR_RELEASE -->
<img src="http://kubernetes.io/kubernetes/img/warning.png" alt="WARNING"
width="25" height="25">
<img src="http://kubernetes.io/kubernetes/img/warning.png" alt="WARNING"
width="25" height="25">
<img src="http://kubernetes.io/kubernetes/img/warning.png" alt="WARNING"
width="25" height="25">
<img src="http://kubernetes.io/kubernetes/img/warning.png" alt="WARNING"
width="25" height="25">
<img src="http://kubernetes.io/kubernetes/img/warning.png" alt="WARNING"
width="25" height="25">
<h2>PLEASE NOTE: This document applies to the HEAD of the source tree</h2>
If you are using a released version of Kubernetes, you should
refer to the docs that go with that version.
Documentation for other releases can be found at
[releases.k8s.io](http://releases.k8s.io).
</strong>
--
<!-- END STRIP_FOR_RELEASE -->
<!-- END MUNGE: UNVERSIONED_WARNING -->
This file is autogenerated, but we've stopped checking such files into the
repository to reduce the need for rebases. Please run hack/generate-docs.sh to
populate this file.
<!-- BEGIN MUNGE: GENERATED_ANALYTICS -->
[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/user-guide/kubectl/kubectl_top.md?pixel)]()
<!-- END MUNGE: GENERATED_ANALYTICS -->

View File

@ -0,0 +1,36 @@
<!-- BEGIN MUNGE: UNVERSIONED_WARNING -->
<!-- BEGIN STRIP_FOR_RELEASE -->
<img src="http://kubernetes.io/kubernetes/img/warning.png" alt="WARNING"
width="25" height="25">
<img src="http://kubernetes.io/kubernetes/img/warning.png" alt="WARNING"
width="25" height="25">
<img src="http://kubernetes.io/kubernetes/img/warning.png" alt="WARNING"
width="25" height="25">
<img src="http://kubernetes.io/kubernetes/img/warning.png" alt="WARNING"
width="25" height="25">
<img src="http://kubernetes.io/kubernetes/img/warning.png" alt="WARNING"
width="25" height="25">
<h2>PLEASE NOTE: This document applies to the HEAD of the source tree</h2>
If you are using a released version of Kubernetes, you should
refer to the docs that go with that version.
Documentation for other releases can be found at
[releases.k8s.io](http://releases.k8s.io).
</strong>
--
<!-- END STRIP_FOR_RELEASE -->
<!-- END MUNGE: UNVERSIONED_WARNING -->
This file is autogenerated, but we've stopped checking such files into the
repository to reduce the need for rebases. Please run hack/generate-docs.sh to
populate this file.
<!-- BEGIN MUNGE: GENERATED_ANALYTICS -->
[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/user-guide/kubectl/kubectl_top_node.md?pixel)]()
<!-- END MUNGE: GENERATED_ANALYTICS -->

View File

@ -0,0 +1,36 @@
<!-- BEGIN MUNGE: UNVERSIONED_WARNING -->
<!-- BEGIN STRIP_FOR_RELEASE -->
<img src="http://kubernetes.io/kubernetes/img/warning.png" alt="WARNING"
width="25" height="25">
<img src="http://kubernetes.io/kubernetes/img/warning.png" alt="WARNING"
width="25" height="25">
<img src="http://kubernetes.io/kubernetes/img/warning.png" alt="WARNING"
width="25" height="25">
<img src="http://kubernetes.io/kubernetes/img/warning.png" alt="WARNING"
width="25" height="25">
<img src="http://kubernetes.io/kubernetes/img/warning.png" alt="WARNING"
width="25" height="25">
<h2>PLEASE NOTE: This document applies to the HEAD of the source tree</h2>
If you are using a released version of Kubernetes, you should
refer to the docs that go with that version.
Documentation for other releases can be found at
[releases.k8s.io](http://releases.k8s.io).
</strong>
--
<!-- END STRIP_FOR_RELEASE -->
<!-- END MUNGE: UNVERSIONED_WARNING -->
This file is autogenerated, but we've stopped checking such files into the
repository to reduce the need for rebases. Please run hack/generate-docs.sh to
populate this file.
<!-- BEGIN MUNGE: GENERATED_ANALYTICS -->
[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/user-guide/kubectl/kubectl_top_pod.md?pixel)]()
<!-- END MUNGE: GENERATED_ANALYTICS -->

View File

@ -0,0 +1,3 @@
This file is autogenerated, but we've stopped checking such files into the
repository to reduce the need for rebases. Please run hack/generate-docs.sh to
populate this file.

View File

@ -0,0 +1,3 @@
This file is autogenerated, but we've stopped checking such files into the
repository to reduce the need for rebases. Please run hack/generate-docs.sh to
populate this file.

View File

@ -0,0 +1,3 @@
This file is autogenerated, but we've stopped checking such files into the
repository to reduce the need for rebases. Please run hack/generate-docs.sh to
populate this file.

View File

@ -274,6 +274,8 @@ Find more information at https://github.com/kubernetes/kubernetes.`,
cmds.AddCommand(NewCmdConvert(f, out))
cmds.AddCommand(NewCmdCompletion(f, out))
cmds.AddCommand(NewCmdTop(f, out))
if cmds.Flag("namespace") != nil {
if cmds.Flag("namespace").Annotations == nil {
cmds.Flag("namespace").Annotations = map[string][]string{}

60
pkg/kubectl/cmd/top.go Normal file
View File

@ -0,0 +1,60 @@
/*
Copyright 2016 The Kubernetes Authors.
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 cmd
import (
"io"
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
"github.com/renstrom/dedent"
"github.com/spf13/cobra"
)
// TopOptions contains all the options for running the top cli command.
type TopOptions struct{}
var (
topLong = dedent.Dedent(`
Display Resource (CPU/Memory/Storage) usage.
The top command allows you to see the resource consumption for nodes or pods.`)
)
func NewCmdTop(f *cmdutil.Factory, out io.Writer) *cobra.Command {
options := &TopOptions{}
cmd := &cobra.Command{
Use: "top",
Short: "Display Resource (CPU/Memory/Storage) usage",
Long: topLong,
Run: func(cmd *cobra.Command, args []string) {
if err := options.RunTop(f, cmd, args, out); err != nil {
cmdutil.CheckErr(err)
}
},
}
// create subcommands
cmd.AddCommand(NewCmdTopNode(f, out))
cmd.AddCommand(NewCmdTopPod(f, out))
return cmd
}
func (o TopOptions) RunTop(f *cmdutil.Factory, cmd *cobra.Command, args []string, out io.Writer) error {
return cmd.Help()
}

107
pkg/kubectl/cmd/top_node.go Normal file
View File

@ -0,0 +1,107 @@
/*
Copyright 2016 The Kubernetes Authors.
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 cmd
import (
"errors"
"io"
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
"k8s.io/kubernetes/pkg/kubectl/metricsutil"
"github.com/renstrom/dedent"
"github.com/spf13/cobra"
)
// TopNodeOptions contains all the options for running the top-node cli command.
type TopNodeOptions struct {
ResourceName string
Selector string
Client *metricsutil.HeapsterMetricsClient
Printer *metricsutil.TopCmdPrinter
}
var (
topNodeLong = dedent.Dedent(`
Display Resource (CPU/Memory/Storage) usage of nodes.
The top-node command allows you to see the resource consumption of nodes.`)
topNodeExample = dedent.Dedent(`
# Show metrics for all nodes
kubectl top node
# Show metrics for a given node
kubectl top node NODE_NAME`)
)
func NewCmdTopNode(f *cmdutil.Factory, out io.Writer) *cobra.Command {
options := &TopNodeOptions{}
cmd := &cobra.Command{
Use: "node [NAME | -l label]",
Short: "Display Resource (CPU/Memory/Storage) usage of nodes",
Long: topNodeLong,
Example: topNodeExample,
Run: func(cmd *cobra.Command, args []string) {
if err := options.Complete(f, cmd, args, out); err != nil {
cmdutil.CheckErr(err)
}
if err := options.Validate(); err != nil {
cmdutil.CheckErr(cmdutil.UsageError(cmd, err.Error()))
}
if err := options.RunTopNode(); err != nil {
cmdutil.CheckErr(err)
}
},
Aliases: []string{"nodes"},
}
cmd.Flags().StringVarP(&options.Selector, "selector", "l", "", "Selector (label query) to filter on")
return cmd
}
func (o *TopNodeOptions) Complete(f *cmdutil.Factory, cmd *cobra.Command, args []string, out io.Writer) error {
var err error
if len(args) == 1 {
o.ResourceName = args[0]
} else if len(args) > 1 {
return cmdutil.UsageError(cmd, cmd.Use)
}
cli, err := f.Client()
if err != nil {
return err
}
o.Client = metricsutil.DefaultHeapsterMetricsClient(cli)
o.Printer = metricsutil.NewTopCmdPrinter(out)
return nil
}
func (o *TopNodeOptions) Validate() error {
if len(o.ResourceName) > 0 && len(o.Selector) > 0 {
return errors.New("only one of NAME or --selector can be provided")
}
return nil
}
func (o TopNodeOptions) RunTopNode() error {
metrics, err := o.Client.GetNodeMetrics(o.ResourceName, o.Selector)
if err != nil {
return err
}
return o.Printer.PrintNodeMetrics(metrics)
}

View File

@ -0,0 +1,162 @@
/*
Copyright 2016 The Kubernetes Authors.
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 cmd
import (
"bytes"
"fmt"
"net/http"
"strings"
"testing"
"k8s.io/kubernetes/pkg/api/unversioned"
"k8s.io/kubernetes/pkg/client/restclient"
"k8s.io/kubernetes/pkg/client/unversioned/fake"
"net/url"
)
func TestTopNodeAllMetrics(t *testing.T) {
initTestErrorHandler(t)
metrics := testNodeMetricsData()
expectedPath := fmt.Sprintf("%s/%s/nodes", baseMetricsAddress, metricsApiVersion)
f, tf, _, ns := NewAPIFactory()
tf.Printer = &testPrinter{}
tf.Client = &fake.RESTClient{
NegotiatedSerializer: ns,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
switch p, m := req.URL.Path, req.Method; {
case p == expectedPath && m == "GET":
body, err := marshallBody(metrics)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: body}, nil
default:
t.Fatalf("unexpected request: %#v\nGot URL: %#v\nExpected path: %#v", req, req.URL, expectedPath)
return nil, nil
}
}),
}
tf.Namespace = "test"
tf.ClientConfig = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: &unversioned.GroupVersion{Version: "v1"}}}
buf := bytes.NewBuffer([]byte{})
cmd := NewCmdTopNode(f, buf)
cmd.Run(cmd, []string{})
// Check the presence of node names in the output.
result := buf.String()
for _, m := range metrics {
if !strings.Contains(result, m.Name) {
t.Errorf("missing metrics for %s: \n%s", m.Name, result)
}
}
}
func TestTopNodeWithNameMetrics(t *testing.T) {
initTestErrorHandler(t)
metrics := testNodeMetricsData()
expectedMetrics := metrics[0]
nonExpectedMetrics := metrics[1:]
expectedPath := fmt.Sprintf("%s/%s/nodes/%s", baseMetricsAddress, metricsApiVersion, expectedMetrics.Name)
f, tf, _, ns := NewAPIFactory()
tf.Printer = &testPrinter{}
tf.Client = &fake.RESTClient{
NegotiatedSerializer: ns,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
switch p, m := req.URL.Path, req.Method; {
case p == expectedPath && m == "GET":
body, err := marshallBody(expectedMetrics)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: body}, nil
default:
t.Fatalf("unexpected request: %#v\nGot URL: %#v\nExpected path: %#v", req, req.URL, expectedPath)
return nil, nil
}
}),
}
tf.Namespace = "test"
tf.ClientConfig = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: &unversioned.GroupVersion{Version: "v1"}}}
buf := bytes.NewBuffer([]byte{})
cmd := NewCmdTopNode(f, buf)
cmd.Run(cmd, []string{expectedMetrics.Name})
// Check the presence of node names in the output.
result := buf.String()
if !strings.Contains(result, expectedMetrics.Name) {
t.Errorf("missing metrics for %s: \n%s", expectedMetrics.Name, result)
}
for _, m := range nonExpectedMetrics {
if strings.Contains(result, m.Name) {
t.Errorf("unexpected metrics for %s: \n%s", m.Name, result)
}
}
}
func TestTopNodeWithLabelSelectorMetrics(t *testing.T) {
initTestErrorHandler(t)
metrics := testNodeMetricsData()
expectedMetrics := metrics[0:1]
nonExpectedMetrics := metrics[1:]
label := "key=value"
expectedPath := fmt.Sprintf("%s/%s/nodes", baseMetricsAddress, metricsApiVersion)
expectedQuery := fmt.Sprintf("labelSelector=%s", url.QueryEscape(label))
f, tf, _, ns := NewAPIFactory()
tf.Printer = &testPrinter{}
tf.Client = &fake.RESTClient{
NegotiatedSerializer: ns,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
switch p, m, q := req.URL.Path, req.Method, req.URL.RawQuery; {
case p == expectedPath && m == "GET" && q == expectedQuery:
body, err := marshallBody(expectedMetrics)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: body}, nil
default:
t.Fatalf("unexpected request: %#v\nGot URL: %#v\nExpected path: %#v", req, req.URL, expectedPath)
return nil, nil
}
}),
}
tf.Namespace = "test"
tf.ClientConfig = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: &unversioned.GroupVersion{Version: "v1"}}}
buf := bytes.NewBuffer([]byte{})
cmd := NewCmdTopNode(f, buf)
cmd.Flags().Set("selector", label)
cmd.Run(cmd, []string{})
// Check the presence of node names in the output.
result := buf.String()
for _, m := range expectedMetrics {
if !strings.Contains(result, m.Name) {
t.Errorf("missing metrics for %s: \n%s", m.Name, result)
}
}
for _, m := range nonExpectedMetrics {
if strings.Contains(result, m.Name) {
t.Errorf("unexpected metrics for %s: \n%s", m.Name, result)
}
}
}

121
pkg/kubectl/cmd/top_pod.go Normal file
View File

@ -0,0 +1,121 @@
/*
Copyright 2016 The Kubernetes Authors.
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 cmd
import (
"errors"
"io"
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
"k8s.io/kubernetes/pkg/kubectl/metricsutil"
"github.com/renstrom/dedent"
"github.com/spf13/cobra"
)
// TopPodOptions contains all the options for running the top-pod cli command.
type TopPodOptions struct {
ResourceName string
AllNamespaces bool
PrintContainers bool
Selector string
Namespace string
Client *metricsutil.HeapsterMetricsClient
Printer *metricsutil.TopCmdPrinter
}
var (
topPodLong = dedent.Dedent(`
Display Resource (CPU/Memory/Storage) usage of pods.
The 'top pod' command allows you to see the resource consumption of pods.`)
topPodExample = dedent.Dedent(`
# Show metrics for all pods in the default namespace
kubectl top pod
# Show metrics for all pods in the given namespace
kubectl top pod --namespace=NAMESPACE
# Show metrics for a given pod and its containers
kubectl top pod POD_NAME --containers
# Show metrics for the pods defined by label name=myLabel
kubectl top pod -l name=myLabel`)
)
func NewCmdTopPod(f *cmdutil.Factory, out io.Writer) *cobra.Command {
options := &TopPodOptions{}
cmd := &cobra.Command{
Use: "pod [NAME | -l label]",
Short: "Display Resource (CPU/Memory/Storage) usage of pods",
Long: topPodLong,
Example: topPodExample,
Run: func(cmd *cobra.Command, args []string) {
if err := options.Complete(f, cmd, args, out); err != nil {
cmdutil.CheckErr(err)
}
if err := options.RunTopPod(); err != nil {
cmdutil.CheckErr(err)
}
},
Aliases: []string{"pods"},
}
cmd.Flags().StringVarP(&options.Selector, "selector", "l", "", "Selector (label query) to filter on")
cmd.Flags().BoolVar(&options.PrintContainers, "containers", false, "If present, print usage of containers within a pod.")
cmd.Flags().BoolVar(&options.AllNamespaces, "all-namespaces", false, "If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace.")
return cmd
}
// Complete completes all the required options for top.
func (o *TopPodOptions) Complete(f *cmdutil.Factory, cmd *cobra.Command, args []string, out io.Writer) error {
var err error
if len(args) == 1 {
o.ResourceName = args[0]
} else if len(args) > 1 {
return cmdutil.UsageError(cmd, cmd.Use)
}
o.Namespace, _, err = f.DefaultNamespace()
if err != nil {
return err
}
cli, err := f.Client()
if err != nil {
return err
}
o.Client = metricsutil.DefaultHeapsterMetricsClient(cli)
o.Printer = metricsutil.NewTopCmdPrinter(out)
return nil
}
func (o *TopPodOptions) Validate() error {
if len(o.ResourceName) > 0 && len(o.Selector) > 0 {
return errors.New("only one of NAME or --selector can be provided")
}
return nil
}
// RunTop implements all the necessary functionality for top.
func (o TopPodOptions) RunTopPod() error {
metrics, err := o.Client.GetPodMetrics(o.Namespace, o.ResourceName, o.AllNamespaces, o.Selector)
if err != nil {
return err
}
return o.Printer.PrintPodMetrics(metrics, o.PrintContainers, o.AllNamespaces)
}

View File

@ -0,0 +1,233 @@
/*
Copyright 2016 The Kubernetes Authors.
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 cmd
import (
"bytes"
"fmt"
"net/http"
"strings"
"testing"
"k8s.io/kubernetes/pkg/api/unversioned"
"k8s.io/kubernetes/pkg/client/restclient"
"k8s.io/kubernetes/pkg/client/unversioned/fake"
"net/url"
)
func TestTopPodAllInNamespaceMetrics(t *testing.T) {
initTestErrorHandler(t)
// TODO(magorzata): refactor to pods/ path after updating heapster version
metrics := testPodMetricsData()
testNamespace := "testnamespace"
nonTestNamespace := "anothernamespace"
expectedMetrics := metrics[0:2]
for _, m := range expectedMetrics {
m.Namespace = testNamespace
}
nonExpectedMetrics := metrics[2:]
for _, m := range expectedMetrics {
m.Namespace = nonTestNamespace
}
expectedPath := fmt.Sprintf("%s/%s/namespaces/%s/pods", baseMetricsAddress, metricsApiVersion, testNamespace)
f, tf, _, ns := NewAPIFactory()
tf.Printer = &testPrinter{}
tf.Client = &fake.RESTClient{
NegotiatedSerializer: ns,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
switch p, m := req.URL.Path, req.Method; {
case p == expectedPath && m == "GET":
body, err := marshallBody(expectedMetrics)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: body}, nil
default:
t.Fatalf("unexpected request: %#v\nGot URL: %#v\nExpected path: %#v", req, req.URL, expectedPath)
return nil, nil
}
}),
}
tf.Namespace = testNamespace
tf.ClientConfig = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: &unversioned.GroupVersion{Version: "v1"}}}
buf := bytes.NewBuffer([]byte{})
cmd := NewCmdTopPod(f, buf)
cmd.Run(cmd, []string{})
// Check the presence of pod names in the output.
result := buf.String()
for _, m := range expectedMetrics {
if !strings.Contains(result, m.Name) {
t.Errorf("missing metrics for %s: \n%s", m.Name, result)
}
}
for _, m := range nonExpectedMetrics {
if strings.Contains(result, m.Name) {
t.Errorf("unexpected metrics for %s: \n%s", m.Name, result)
}
}
}
func TestTopPodWithNameMetrics(t *testing.T) {
initTestErrorHandler(t)
metrics := testPodMetricsData()
expectedMetrics := metrics[0]
nonExpectedMetrics := metrics[1:]
testNamespace := "testnamespace"
expectedMetrics.Namespace = testNamespace
expectedPath := fmt.Sprintf("%s/%s/namespaces/%s/pods/%s", baseMetricsAddress, metricsApiVersion, testNamespace, expectedMetrics.Name)
f, tf, _, ns := NewAPIFactory()
tf.Printer = &testPrinter{}
tf.Client = &fake.RESTClient{
NegotiatedSerializer: ns,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
switch p, m := req.URL.Path, req.Method; {
case p == expectedPath && m == "GET":
body, err := marshallBody(expectedMetrics)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: body}, nil
default:
t.Fatalf("unexpected request: %#v\nGot URL: %#v\nExpected path: %#v", req, req.URL, expectedPath)
return nil, nil
}
}),
}
tf.Namespace = testNamespace
tf.ClientConfig = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: &unversioned.GroupVersion{Version: "v1"}}}
buf := bytes.NewBuffer([]byte{})
cmd := NewCmdTopPod(f, buf)
cmd.Run(cmd, []string{expectedMetrics.Name})
// Check the presence of pod names in the output.
result := buf.String()
if !strings.Contains(result, expectedMetrics.Name) {
t.Errorf("missing metrics for %s: \n%s", expectedMetrics.Name, result)
}
for _, m := range nonExpectedMetrics {
if strings.Contains(result, m.Name) {
t.Errorf("unexpected metrics for %s: \n%s", m.Name, result)
}
}
}
func TestTopPodWithLabelSelectorMetrics(t *testing.T) {
initTestErrorHandler(t)
metrics := testPodMetricsData()
expectedMetrics := metrics[0:2]
nonExpectedMetrics := metrics[2:]
label := "key=value"
testNamespace := "testnamespace"
expectedPath := fmt.Sprintf("%s/%s/namespaces/%s/pods", baseMetricsAddress, metricsApiVersion, testNamespace)
expectedQuery := fmt.Sprintf("labelSelector=%s", url.QueryEscape(label))
f, tf, _, ns := NewAPIFactory()
tf.Printer = &testPrinter{}
tf.Client = &fake.RESTClient{
NegotiatedSerializer: ns,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
switch p, m, q := req.URL.Path, req.Method, req.URL.RawQuery; {
case p == expectedPath && m == "GET" && q == expectedQuery:
body, err := marshallBody(expectedMetrics)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: body}, nil
default:
t.Fatalf("unexpected request: %#v\nGot URL: %#v\nExpected path: %#v", req, req.URL, expectedPath)
return nil, nil
}
}),
}
tf.Namespace = testNamespace
tf.ClientConfig = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: &unversioned.GroupVersion{Version: "v1"}}}
buf := bytes.NewBuffer([]byte{})
cmd := NewCmdTopPod(f, buf)
cmd.Flags().Set("selector", label)
cmd.Run(cmd, []string{})
// Check the presence of pod names in the output.
result := buf.String()
for _, m := range expectedMetrics {
if !strings.Contains(result, m.Name) {
t.Errorf("missing metrics for %s: \n%s", m.Name, result)
}
}
for _, m := range nonExpectedMetrics {
if strings.Contains(result, m.Name) {
t.Errorf("unexpected metrics for %s: \n%s", m.Name, result)
}
}
}
func TestTopPodWithContainersMetrics(t *testing.T) {
initTestErrorHandler(t)
metrics := testPodMetricsData()
expectedMetrics := metrics[0]
nonExpectedMetrics := metrics[1:]
testNamespace := "testnamespace"
expectedMetrics.Namespace = testNamespace
expectedPath := fmt.Sprintf("%s/%s/namespaces/%s/pods/%s", baseMetricsAddress, metricsApiVersion, testNamespace, expectedMetrics.Name)
f, tf, _, ns := NewAPIFactory()
tf.Printer = &testPrinter{}
tf.Client = &fake.RESTClient{
NegotiatedSerializer: ns,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
switch p, m := req.URL.Path, req.Method; {
case p == expectedPath && m == "GET":
body, err := marshallBody(expectedMetrics)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: body}, nil
default:
t.Fatalf("unexpected request: %#v\nGot URL: %#v\nExpected path: %#v", req, req.URL, expectedPath)
return nil, nil
}
}),
}
tf.Namespace = testNamespace
tf.ClientConfig = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: &unversioned.GroupVersion{Version: "v1"}}}
buf := bytes.NewBuffer([]byte{})
cmd := NewCmdTopPod(f, buf)
cmd.Flags().Set("containers", "true")
cmd.Run(cmd, []string{expectedMetrics.Name})
// Check the presence of pod names in the output.
result := buf.String()
if !strings.Contains(result, expectedMetrics.Name) {
t.Errorf("missing metrics for %s: \n%s", expectedMetrics.Name, result)
}
for _, m := range expectedMetrics.Containers {
if !strings.Contains(result, m.Name) {
t.Errorf("missing metrics for container %s: \n%s", m.Name, result)
}
}
for _, m := range nonExpectedMetrics {
if strings.Contains(result, m.Name) {
t.Errorf("unexpected metrics for %s: \n%s", m.Name, result)
}
}
}

151
pkg/kubectl/cmd/top_test.go Normal file
View File

@ -0,0 +1,151 @@
/*
Copyright 2016 The Kubernetes Authors.
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 cmd
import (
"bytes"
"encoding/json"
"io"
"io/ioutil"
"time"
metrics_api "k8s.io/heapster/metrics/apis/metrics/v1alpha1"
"k8s.io/kubernetes/pkg/api/resource"
"k8s.io/kubernetes/pkg/api/unversioned"
api "k8s.io/kubernetes/pkg/api/v1"
"testing"
)
const (
baseHeapsterServiceAddress = "/api/v1/namespaces/kube-system/services/http:heapster:"
baseMetricsAddress = baseHeapsterServiceAddress + "/proxy/apis/metrics"
metricsApiVersion = "v1alpha1"
)
func TestTopSubcommandsExist(t *testing.T) {
initTestErrorHandler(t)
f, _, _, _ := NewAPIFactory()
buf := bytes.NewBuffer([]byte{})
cmd := NewCmdTop(f, buf)
if !cmd.HasSubCommands() {
t.Error("top command should have subcommands")
}
}
func marshallBody(metrics interface{}) (io.ReadCloser, error) {
result, err := json.Marshal(metrics)
if err != nil {
return nil, err
}
return ioutil.NopCloser(bytes.NewReader(result)), nil
}
func testNodeMetricsData() []metrics_api.NodeMetrics {
return []metrics_api.NodeMetrics{
{
ObjectMeta: api.ObjectMeta{Name: "node1", ResourceVersion: "10"},
Window: unversioned.Duration{Duration: time.Minute},
Usage: api.ResourceList{
api.ResourceCPU: *resource.NewMilliQuantity(1, resource.DecimalSI),
api.ResourceMemory: *resource.NewQuantity(2*(1024*1024), resource.DecimalSI),
api.ResourceStorage: *resource.NewQuantity(3*(1024*1024), resource.DecimalSI),
},
},
{
ObjectMeta: api.ObjectMeta{Name: "node2", ResourceVersion: "11"},
Window: unversioned.Duration{Duration: time.Minute},
Usage: api.ResourceList{
api.ResourceCPU: *resource.NewMilliQuantity(5, resource.DecimalSI),
api.ResourceMemory: *resource.NewQuantity(6*(1024*1024), resource.DecimalSI),
api.ResourceStorage: *resource.NewQuantity(7*(1024*1024), resource.DecimalSI),
},
},
}
}
func testPodMetricsData() []metrics_api.PodMetrics {
return []metrics_api.PodMetrics{
{
ObjectMeta: api.ObjectMeta{Name: "pod1", Namespace: "test", ResourceVersion: "10"},
Window: unversioned.Duration{Duration: time.Minute},
Containers: []metrics_api.ContainerMetrics{
{
Name: "container1-1",
Usage: api.ResourceList{
api.ResourceCPU: *resource.NewMilliQuantity(1, resource.DecimalSI),
api.ResourceMemory: *resource.NewQuantity(2*(1024*1024), resource.DecimalSI),
api.ResourceStorage: *resource.NewQuantity(3*(1024*1024), resource.DecimalSI),
},
},
{
Name: "container1-2",
Usage: api.ResourceList{
api.ResourceCPU: *resource.NewMilliQuantity(4, resource.DecimalSI),
api.ResourceMemory: *resource.NewQuantity(5*(1024*1024), resource.DecimalSI),
api.ResourceStorage: *resource.NewQuantity(6*(1024*1024), resource.DecimalSI),
},
},
},
},
{
ObjectMeta: api.ObjectMeta{Name: "pod2", Namespace: "test", ResourceVersion: "11"},
Window: unversioned.Duration{Duration: time.Minute},
Containers: []metrics_api.ContainerMetrics{
{
Name: "container2-1",
Usage: api.ResourceList{
api.ResourceCPU: *resource.NewMilliQuantity(7, resource.DecimalSI),
api.ResourceMemory: *resource.NewQuantity(8*(1024*1024), resource.DecimalSI),
api.ResourceStorage: *resource.NewQuantity(9*(1024*1024), resource.DecimalSI),
},
},
{
Name: "container2-2",
Usage: api.ResourceList{
api.ResourceCPU: *resource.NewMilliQuantity(10, resource.DecimalSI),
api.ResourceMemory: *resource.NewQuantity(11*(1024*1024), resource.DecimalSI),
api.ResourceStorage: *resource.NewQuantity(12*(1024*1024), resource.DecimalSI),
},
},
{
Name: "container2-3",
Usage: api.ResourceList{
api.ResourceCPU: *resource.NewMilliQuantity(13, resource.DecimalSI),
api.ResourceMemory: *resource.NewQuantity(14*(1024*1024), resource.DecimalSI),
api.ResourceStorage: *resource.NewQuantity(15*(1024*1024), resource.DecimalSI),
},
},
},
},
{
ObjectMeta: api.ObjectMeta{Name: "pod3", Namespace: "test", ResourceVersion: "12"},
Window: unversioned.Duration{Duration: time.Minute},
Containers: []metrics_api.ContainerMetrics{
{
Name: "container3-1",
Usage: api.ResourceList{
api.ResourceCPU: *resource.NewMilliQuantity(7, resource.DecimalSI),
api.ResourceMemory: *resource.NewQuantity(8*(1024*1024), resource.DecimalSI),
api.ResourceStorage: *resource.NewQuantity(9*(1024*1024), resource.DecimalSI),
},
},
},
},
}
}

View File

@ -0,0 +1,173 @@
/*
Copyright 2016 The Kubernetes Authors.
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 metricsutil
import (
"encoding/json"
"errors"
"fmt"
metrics_api "k8s.io/heapster/metrics/apis/metrics/v1alpha1"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/unversioned"
"k8s.io/kubernetes/pkg/api/validation"
client "k8s.io/kubernetes/pkg/client/unversioned"
)
const (
DefaultHeapsterNamespace = "kube-system"
DefaultHeapsterScheme = "http"
DefaultHeapsterService = "heapster"
DefaultHeapsterPort = "" // use the first exposed port on the service
)
var (
prefix = "/apis"
groupVersion = fmt.Sprintf("%s/%s", metricsGv.Group, metricsGv.Version)
MetricsRoot = fmt.Sprintf("%s/%s", prefix, groupVersion)
// TODO: get this from metrics api once it's finished
metricsGv = unversioned.GroupVersion{Group: "metrics", Version: "v1alpha1"}
)
type HeapsterMetricsClient struct {
Client *client.Client
HeapsterNamespace string
HeapsterScheme string
HeapsterService string
HeapsterPort string
}
func NewHeapsterMetricsClient(client *client.Client, namespace, scheme, service, port string) *HeapsterMetricsClient {
return &HeapsterMetricsClient{
Client: client,
HeapsterNamespace: namespace,
HeapsterScheme: scheme,
HeapsterService: service,
HeapsterPort: port,
}
}
func DefaultHeapsterMetricsClient(client *client.Client) *HeapsterMetricsClient {
return NewHeapsterMetricsClient(client, DefaultHeapsterNamespace, DefaultHeapsterScheme, DefaultHeapsterService, DefaultHeapsterPort)
}
func PodMetricsUrl(namespace string, name string) (string, error) {
errs := validation.ValidateNamespaceName(namespace, false)
if len(errs) > 0 {
message := fmt.Sprintf("invalid namespace: %s - %v", namespace, errs)
return "", errors.New(message)
}
if len(name) > 0 {
errs = validation.ValidatePodName(name, false)
if len(errs) > 0 {
message := fmt.Sprintf("invalid pod name: %s - %v", name, errs)
return "", errors.New(message)
}
}
return fmt.Sprintf("%s/namespaces/%s/pods/%s", MetricsRoot, namespace, name), nil
}
func NodeMetricsUrl(name string) (string, error) {
if len(name) > 0 {
errs := validation.ValidateNodeName(name, false)
if len(errs) > 0 {
message := fmt.Sprintf("invalid node name: %s - %v", name, errs)
return "", errors.New(message)
}
}
return fmt.Sprintf("%s/nodes/%s", MetricsRoot, name), nil
}
func (cli *HeapsterMetricsClient) GetNodeMetrics(nodeName string, selector string) ([]metrics_api.NodeMetrics, error) {
params := map[string]string{"labelSelector": selector}
path, err := NodeMetricsUrl(nodeName)
if err != nil {
return []metrics_api.NodeMetrics{}, err
}
resultRaw, err := GetHeapsterMetrics(cli, path, params)
if err != nil {
return []metrics_api.NodeMetrics{}, err
}
metrics := make([]metrics_api.NodeMetrics, 0)
if len(nodeName) == 0 {
err = json.Unmarshal(resultRaw, &metrics)
if err != nil {
return []metrics_api.NodeMetrics{}, fmt.Errorf("failed to unmarshall heapster response: %v", err)
}
} else {
var singleMetric metrics_api.NodeMetrics
err = json.Unmarshal(resultRaw, &singleMetric)
if err != nil {
return []metrics_api.NodeMetrics{}, fmt.Errorf("failed to unmarshall heapster response: %v", err)
}
metrics = append(metrics, singleMetric)
}
return metrics, nil
}
func (cli *HeapsterMetricsClient) GetPodMetrics(namespace string, podName string, allNamespaces bool, selector string) ([]metrics_api.PodMetrics, error) {
// TODO: extend Master Metrics API with getting pods from all namespaces
// instead of aggregating the results here
namespaces := make([]string, 0)
if allNamespaces {
list, err := cli.Client.Namespaces().List(api.ListOptions{})
if err != nil {
return []metrics_api.PodMetrics{}, err
}
for _, ns := range list.Items {
namespaces = append(namespaces, ns.Name)
}
} else {
namespaces = append(namespaces, namespace)
}
params := map[string]string{"labelSelector": selector}
allMetrics := make([]metrics_api.PodMetrics, 0)
for _, ns := range namespaces {
path, err := PodMetricsUrl(ns, podName)
if err != nil {
return []metrics_api.PodMetrics{}, err
}
resultRaw, err := GetHeapsterMetrics(cli, path, params)
if err != nil {
return []metrics_api.PodMetrics{}, err
}
if len(podName) == 0 {
metrics := make([]metrics_api.PodMetrics, 0)
err = json.Unmarshal(resultRaw, &metrics)
if err != nil {
return []metrics_api.PodMetrics{}, fmt.Errorf("failed to unmarshall heapster response: %v", err)
}
allMetrics = append(allMetrics, metrics...)
} else {
var singleMetric metrics_api.PodMetrics
err = json.Unmarshal(resultRaw, &singleMetric)
if err != nil {
return []metrics_api.PodMetrics{}, fmt.Errorf("failed to unmarshall heapster response: %v", err)
}
allMetrics = append(allMetrics, singleMetric)
}
}
return allMetrics, nil
}
func GetHeapsterMetrics(cli *HeapsterMetricsClient, path string, params map[string]string) ([]byte, error) {
return cli.Client.Services(cli.HeapsterNamespace).
ProxyGet(cli.HeapsterScheme, cli.HeapsterService, cli.HeapsterPort, path, params).
DoRaw()
}

View File

@ -0,0 +1,172 @@
/*
Copyright 2016 The Kubernetes Authors.
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 metricsutil
import (
"fmt"
"io"
"time"
metrics_api "k8s.io/heapster/metrics/apis/metrics/v1alpha1"
"k8s.io/kubernetes/pkg/api/resource"
"k8s.io/kubernetes/pkg/api/v1"
"k8s.io/kubernetes/pkg/kubectl"
)
var (
MeasuredResources = []v1.ResourceName{
v1.ResourceCPU,
v1.ResourceMemory,
v1.ResourceStorage,
}
NodeColumns = []string{"NAME", "CPU (cores)", "MEMORY (bytes)", "STORAGE (bytes)", "TIMESTAMP"}
PodColumns = []string{"NAME", "CPU (cores)", "MEMORY (bytes)", "STORAGE (bytes)", "TIMESTAMP"}
NamespaceColumn = "NAMESPACE"
PodColumn = "POD"
)
type ResourceMetricsInfo struct {
Name string
Metrics v1.ResourceList
Timestamp string
}
type TopCmdPrinter struct {
out io.Writer
}
func NewTopCmdPrinter(out io.Writer) *TopCmdPrinter {
return &TopCmdPrinter{out: out}
}
func (printer *TopCmdPrinter) PrintNodeMetrics(metrics []metrics_api.NodeMetrics) error {
if len(metrics) == 0 {
return nil
}
w := kubectl.GetNewTabWriter(printer.out)
defer w.Flush()
printColumnNames(w, NodeColumns)
for _, m := range metrics {
printMetricsLine(w, &ResourceMetricsInfo{
Name: m.Name,
Metrics: m.Usage,
Timestamp: m.Timestamp.Time.Format(time.RFC1123Z),
})
}
return nil
}
func (printer *TopCmdPrinter) PrintPodMetrics(metrics []metrics_api.PodMetrics, printContainers bool, withNamespace bool) error {
if len(metrics) == 0 {
return nil
}
w := kubectl.GetNewTabWriter(printer.out)
defer w.Flush()
if withNamespace {
printValue(w, NamespaceColumn)
}
if printContainers {
printValue(w, PodColumn)
}
printColumnNames(w, PodColumns)
for _, m := range metrics {
printSinglePodMetrics(w, &m, printContainers, withNamespace)
}
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 *metrics_api.PodMetrics, printContainersOnly bool, withNamespace bool) {
containers := make(map[string]v1.ResourceList)
podMetrics := make(v1.ResourceList)
for _, res := range MeasuredResources {
podMetrics[res], _ = resource.ParseQuantity("0")
}
for _, c := range m.Containers {
containers[c.Name] = c.Usage
if !printContainersOnly {
for _, res := range MeasuredResources {
quantity := podMetrics[res]
quantity.Add(c.Usage[res])
podMetrics[res] = quantity
}
}
}
if printContainersOnly {
for contName := range containers {
if withNamespace {
printValue(out, m.Namespace)
}
printValue(out, m.Name)
printMetricsLine(out, &ResourceMetricsInfo{
Name: contName,
Metrics: containers[contName],
Timestamp: m.Timestamp.Time.Format(time.RFC1123Z),
})
}
} else {
if withNamespace {
printValue(out, m.Namespace)
}
printMetricsLine(out, &ResourceMetricsInfo{
Name: m.Name,
Metrics: podMetrics,
Timestamp: m.Timestamp.Time.Format(time.RFC1123Z),
})
}
}
func printMetricsLine(out io.Writer, metrics *ResourceMetricsInfo) {
printValue(out, metrics.Name)
printAllResourceUsages(out, metrics.Metrics)
printValue(out, metrics.Timestamp)
fmt.Fprint(out, "\n")
}
func printValue(out io.Writer, value interface{}) {
fmt.Fprintf(out, "%v\t", value)
}
func printAllResourceUsages(out io.Writer, usage v1.ResourceList) {
for _, res := range MeasuredResources {
quantity := usage[res]
printSingleResourceUsage(out, res, quantity)
fmt.Fprint(out, "\t")
}
}
func printSingleResourceUsage(out io.Writer, resourceType v1.ResourceName, quantity resource.Quantity) {
switch resourceType {
case v1.ResourceCPU:
fmt.Fprintf(out, "%vm", quantity.MilliValue())
case v1.ResourceMemory:
fmt.Fprintf(out, "%vMi", quantity.Value()/(1024*1024))
case v1.ResourceStorage:
// TODO: Change it after storage metrics collection is finished.
fmt.Fprint(out, "-")
default:
fmt.Fprintf(out, "%v", quantity.Value())
}
}