diff --git a/.generated_docs b/.generated_docs index de64706d09..c2fae3285d 100644 --- a/.generated_docs +++ b/.generated_docs @@ -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 diff --git a/docs/man/man1/.files_generated b/docs/man/man1/.files_generated index a7620fa822..89e38e687c 100644 --- a/docs/man/man1/.files_generated +++ b/docs/man/man1/.files_generated @@ -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 diff --git a/docs/man/man1/kubectl-top-node.1 b/docs/man/man1/kubectl-top-node.1 new file mode 100644 index 0000000000..b6fd7a0f98 --- /dev/null +++ b/docs/man/man1/kubectl-top-node.1 @@ -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. diff --git a/docs/man/man1/kubectl-top-pod.1 b/docs/man/man1/kubectl-top-pod.1 new file mode 100644 index 0000000000..b6fd7a0f98 --- /dev/null +++ b/docs/man/man1/kubectl-top-pod.1 @@ -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. diff --git a/docs/man/man1/kubectl-top.1 b/docs/man/man1/kubectl-top.1 new file mode 100644 index 0000000000..b6fd7a0f98 --- /dev/null +++ b/docs/man/man1/kubectl-top.1 @@ -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. diff --git a/docs/user-guide/kubectl/kubectl_top-node.md b/docs/user-guide/kubectl/kubectl_top-node.md new file mode 100644 index 0000000000..fdbc418392 --- /dev/null +++ b/docs/user-guide/kubectl/kubectl_top-node.md @@ -0,0 +1,36 @@ + + + + +WARNING +WARNING +WARNING +WARNING +WARNING + +

PLEASE NOTE: This document applies to the HEAD of the source tree

+ +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). + +-- + + + + + +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. + + +[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/user-guide/kubectl/kubectl_top-node.md?pixel)]() + diff --git a/docs/user-guide/kubectl/kubectl_top-pod.md b/docs/user-guide/kubectl/kubectl_top-pod.md new file mode 100644 index 0000000000..ef6fd4a548 --- /dev/null +++ b/docs/user-guide/kubectl/kubectl_top-pod.md @@ -0,0 +1,36 @@ + + + + +WARNING +WARNING +WARNING +WARNING +WARNING + +

PLEASE NOTE: This document applies to the HEAD of the source tree

+ +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). + +-- + + + + + +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. + + +[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/user-guide/kubectl/kubectl_top-pod.md?pixel)]() + diff --git a/docs/user-guide/kubectl/kubectl_top.md b/docs/user-guide/kubectl/kubectl_top.md new file mode 100644 index 0000000000..da020909b0 --- /dev/null +++ b/docs/user-guide/kubectl/kubectl_top.md @@ -0,0 +1,36 @@ + + + + +WARNING +WARNING +WARNING +WARNING +WARNING + +

PLEASE NOTE: This document applies to the HEAD of the source tree

+ +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). + +-- + + + + + +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. + + +[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/user-guide/kubectl/kubectl_top.md?pixel)]() + diff --git a/docs/user-guide/kubectl/kubectl_top_node.md b/docs/user-guide/kubectl/kubectl_top_node.md new file mode 100644 index 0000000000..dd57d7d7c2 --- /dev/null +++ b/docs/user-guide/kubectl/kubectl_top_node.md @@ -0,0 +1,36 @@ + + + + +WARNING +WARNING +WARNING +WARNING +WARNING + +

PLEASE NOTE: This document applies to the HEAD of the source tree

+ +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). + +-- + + + + + +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. + + +[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/user-guide/kubectl/kubectl_top_node.md?pixel)]() + diff --git a/docs/user-guide/kubectl/kubectl_top_pod.md b/docs/user-guide/kubectl/kubectl_top_pod.md new file mode 100644 index 0000000000..6c888e2196 --- /dev/null +++ b/docs/user-guide/kubectl/kubectl_top_pod.md @@ -0,0 +1,36 @@ + + + + +WARNING +WARNING +WARNING +WARNING +WARNING + +

PLEASE NOTE: This document applies to the HEAD of the source tree

+ +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). + +-- + + + + + +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. + + +[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/user-guide/kubectl/kubectl_top_pod.md?pixel)]() + diff --git a/docs/yaml/kubectl/kubectl_top-node.yaml b/docs/yaml/kubectl/kubectl_top-node.yaml new file mode 100644 index 0000000000..b6fd7a0f98 --- /dev/null +++ b/docs/yaml/kubectl/kubectl_top-node.yaml @@ -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. diff --git a/docs/yaml/kubectl/kubectl_top-pod.yaml b/docs/yaml/kubectl/kubectl_top-pod.yaml new file mode 100644 index 0000000000..b6fd7a0f98 --- /dev/null +++ b/docs/yaml/kubectl/kubectl_top-pod.yaml @@ -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. diff --git a/docs/yaml/kubectl/kubectl_top.yaml b/docs/yaml/kubectl/kubectl_top.yaml new file mode 100644 index 0000000000..b6fd7a0f98 --- /dev/null +++ b/docs/yaml/kubectl/kubectl_top.yaml @@ -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. diff --git a/pkg/kubectl/cmd/cmd.go b/pkg/kubectl/cmd/cmd.go index fa28f8de2b..b362ff5d0f 100644 --- a/pkg/kubectl/cmd/cmd.go +++ b/pkg/kubectl/cmd/cmd.go @@ -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{} diff --git a/pkg/kubectl/cmd/top.go b/pkg/kubectl/cmd/top.go new file mode 100644 index 0000000000..7d1f3dfdd7 --- /dev/null +++ b/pkg/kubectl/cmd/top.go @@ -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() +} diff --git a/pkg/kubectl/cmd/top_node.go b/pkg/kubectl/cmd/top_node.go new file mode 100644 index 0000000000..f02a529bae --- /dev/null +++ b/pkg/kubectl/cmd/top_node.go @@ -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) +} diff --git a/pkg/kubectl/cmd/top_node_test.go b/pkg/kubectl/cmd/top_node_test.go new file mode 100644 index 0000000000..e89368f83a --- /dev/null +++ b/pkg/kubectl/cmd/top_node_test.go @@ -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) + } + } +} diff --git a/pkg/kubectl/cmd/top_pod.go b/pkg/kubectl/cmd/top_pod.go new file mode 100644 index 0000000000..eff06c39cc --- /dev/null +++ b/pkg/kubectl/cmd/top_pod.go @@ -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) +} diff --git a/pkg/kubectl/cmd/top_pod_test.go b/pkg/kubectl/cmd/top_pod_test.go new file mode 100644 index 0000000000..fe9d5da658 --- /dev/null +++ b/pkg/kubectl/cmd/top_pod_test.go @@ -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) + } + } +} diff --git a/pkg/kubectl/cmd/top_test.go b/pkg/kubectl/cmd/top_test.go new file mode 100644 index 0000000000..6d6a12716a --- /dev/null +++ b/pkg/kubectl/cmd/top_test.go @@ -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), + }, + }, + }, + }, + } +} diff --git a/pkg/kubectl/metricsutil/metrics_client.go b/pkg/kubectl/metricsutil/metrics_client.go new file mode 100644 index 0000000000..2eadfef19d --- /dev/null +++ b/pkg/kubectl/metricsutil/metrics_client.go @@ -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() +} diff --git a/pkg/kubectl/metricsutil/metrics_printer.go b/pkg/kubectl/metricsutil/metrics_printer.go new file mode 100644 index 0000000000..6825d8b476 --- /dev/null +++ b/pkg/kubectl/metricsutil/metrics_printer.go @@ -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()) + } +}