diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 2c2dd0ee..94deaed2 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -572,7 +572,7 @@ }, { "ImportPath": "k8s.io/apimachinery", - "Rev": "48159c651603" + "Rev": "1aec6bc431a9" }, { "ImportPath": "k8s.io/cli-runtime", diff --git a/go.mod b/go.mod index da3492fe..506e4aae 100644 --- a/go.mod +++ b/go.mod @@ -35,7 +35,7 @@ require ( golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 gopkg.in/yaml.v2 v2.2.8 k8s.io/api v0.0.0-20200320042356-1fc28ea2498c - k8s.io/apimachinery v0.0.0-20200320122144-48159c651603 + k8s.io/apimachinery v0.0.0-20200324202305-1aec6bc431a9 k8s.io/cli-runtime v0.0.0-20200323050020-c5a29d532eb1 k8s.io/client-go v0.0.0-20200323042902-70eb4849511b k8s.io/component-base v0.0.0-20200323043823-bedba4b6402d @@ -52,7 +52,7 @@ replace ( golang.org/x/sys => golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a // pinned to release-branch.go1.13 golang.org/x/tools => golang.org/x/tools v0.0.0-20190821162956-65e3620a7ae7 // pinned to release-branch.go1.13 k8s.io/api => k8s.io/api v0.0.0-20200320042356-1fc28ea2498c - k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20200320122144-48159c651603 + k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20200324202305-1aec6bc431a9 k8s.io/cli-runtime => k8s.io/cli-runtime v0.0.0-20200323050020-c5a29d532eb1 k8s.io/client-go => k8s.io/client-go v0.0.0-20200323042902-70eb4849511b k8s.io/code-generator => k8s.io/code-generator v0.0.0-20200319201949-6bb2b634cece diff --git a/go.sum b/go.sum index 5224e8f6..29a8c9f8 100644 --- a/go.sum +++ b/go.sum @@ -315,7 +315,7 @@ gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= k8s.io/api v0.0.0-20200320042356-1fc28ea2498c/go.mod h1:5nMyHS4bWX496fulniJ+Sws3P6GLvaP43GadMObLf58= -k8s.io/apimachinery v0.0.0-20200320122144-48159c651603/go.mod h1:yKN3QjQfKl8UdUL9RQ+/1VkR7nIUs7w02zC5CXhD+G0= +k8s.io/apimachinery v0.0.0-20200324202305-1aec6bc431a9/go.mod h1:yKN3QjQfKl8UdUL9RQ+/1VkR7nIUs7w02zC5CXhD+G0= k8s.io/cli-runtime v0.0.0-20200323050020-c5a29d532eb1/go.mod h1:wTCA9N28mT3TbLT9ZNny22D5bBGkRwgZr+dha/ewwb8= k8s.io/client-go v0.0.0-20200323042902-70eb4849511b/go.mod h1:64IdCZeusi2RxFpEc1R4cLo9mtiRealqqsl6ysIAFeY= k8s.io/code-generator v0.0.0-20200319201949-6bb2b634cece/go.mod h1:HgVCDA66DadRZIO1Ym1MX49lfUdXKOzJ8HA7GQVQTMI= diff --git a/pkg/cmd/top/top_node.go b/pkg/cmd/top/top_node.go index 35d30bd7..24dae38c 100644 --- a/pkg/cmd/top/top_node.go +++ b/pkg/cmd/top/top_node.go @@ -44,8 +44,6 @@ type TopNodeOptions struct { SortBy string NoHeaders bool NodeClient corev1client.CoreV1Interface - HeapsterOptions HeapsterTopOptions - Client *metricsutil.HeapsterMetricsClient Printer *metricsutil.TopCmdPrinter DiscoveryClient discovery.DiscoveryInterface MetricsClient metricsclientset.Interface @@ -53,31 +51,15 @@ type TopNodeOptions struct { genericclioptions.IOStreams } -type HeapsterTopOptions struct { - Namespace string - Service string - Scheme string - Port string -} - -func (o *HeapsterTopOptions) Bind(flags *pflag.FlagSet) { - if len(o.Namespace) == 0 { - o.Namespace = metricsutil.DefaultHeapsterNamespace - } - if len(o.Service) == 0 { - o.Service = metricsutil.DefaultHeapsterService - } - if len(o.Scheme) == 0 { - o.Scheme = metricsutil.DefaultHeapsterScheme - } - if len(o.Port) == 0 { - o.Port = metricsutil.DefaultHeapsterPort - } - - flags.StringVar(&o.Namespace, "heapster-namespace", o.Namespace, "Namespace Heapster service is located in") - flags.StringVar(&o.Service, "heapster-service", o.Service, "Name of Heapster service") - flags.StringVar(&o.Scheme, "heapster-scheme", o.Scheme, "Scheme (http or https) to connect to Heapster as") - flags.StringVar(&o.Port, "heapster-port", o.Port, "Port name in service to use") +func heapsterTopOptions(flags *pflag.FlagSet) { + flags.String("heapster-namespace", "kube-system", "Namespace Heapster service is located in") + flags.MarkDeprecated("heapster-namespace", "This flag is currently no-op and will be deleted.") + flags.String("heapster-service", "heapster", "Name of Heapster service") + flags.MarkDeprecated("heapster-service", "This flag is currently no-op and will be deleted.") + flags.String("heapster-scheme", "http", "Scheme (http or https) to connect to Heapster as") + flags.MarkDeprecated("heapster-scheme", "This flag is currently no-op and will be deleted.") + flags.String("heapster-port", "", "Port name in service to use") + flags.MarkDeprecated("heapster-port", "This flag is currently no-op and will be deleted.") } var ( @@ -117,8 +99,8 @@ func NewCmdTopNode(f cmdutil.Factory, o *TopNodeOptions, streams genericclioptio cmd.Flags().StringVarP(&o.Selector, "selector", "l", o.Selector, "Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2)") cmd.Flags().StringVar(&o.SortBy, "sort-by", o.Selector, "If non-empty, sort nodes list using specified field. The field can be either 'cpu' or 'memory'.") cmd.Flags().BoolVar(&o.NoHeaders, "no-headers", o.NoHeaders, "If present, print output without headers") + heapsterTopOptions(cmd.Flags()) - o.HeapsterOptions.Bind(cmd.Flags()) return cmd } @@ -146,7 +128,6 @@ func (o *TopNodeOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args [] } o.NodeClient = clientset.CoreV1() - o.Client = metricsutil.NewHeapsterMetricsClient(clientset.CoreV1(), o.HeapsterOptions.Namespace, o.HeapsterOptions.Scheme, o.HeapsterOptions.Service, o.HeapsterOptions.Port) o.Printer = metricsutil.NewTopCmdPrinter(o.Out) return nil @@ -181,17 +162,13 @@ func (o TopNodeOptions) RunTopNode() error { metricsAPIAvailable := SupportedMetricsAPIVersionAvailable(apiGroups) - var metrics *metricsapi.NodeMetricsList - if metricsAPIAvailable { - metrics, err = getNodeMetricsFromMetricsAPI(o.MetricsClient, o.ResourceName, selector) - if err != nil { - return err - } - } else { - metrics, err = o.Client.GetNodeMetrics(o.ResourceName, selector.String()) - if err != nil { - return err - } + if !metricsAPIAvailable { + return errors.New("Metrics API not available") + } + + metrics, err := getNodeMetricsFromMetricsAPI(o.MetricsClient, o.ResourceName, selector) + if err != nil { + return err } if len(metrics.Items) == 0 { diff --git a/pkg/cmd/top/top_node_test.go b/pkg/cmd/top/top_node_test.go index 746eb59c..dca44b83 100644 --- a/pkg/cmd/top/top_node_test.go +++ b/pkg/cmd/top/top_node_test.go @@ -25,8 +25,6 @@ import ( "strings" "testing" - "net/url" - "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/genericclioptions" @@ -34,7 +32,6 @@ import ( core "k8s.io/client-go/testing" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" "k8s.io/kubectl/pkg/scheme" - metricsv1alpha1api "k8s.io/metrics/pkg/apis/metrics/v1alpha1" metricsv1beta1api "k8s.io/metrics/pkg/apis/metrics/v1beta1" metricsfake "k8s.io/metrics/pkg/client/clientset/versioned/fake" ) @@ -44,388 +41,7 @@ const ( apiVersion = "v1" ) -func TestTopNodeAllMetrics(t *testing.T) { - cmdtesting.InitTestErrorHandler(t) - metrics, nodes := testNodeV1alpha1MetricsData() - expectedMetricsPath := fmt.Sprintf("%s/%s/nodes", baseMetricsAddress, metricsAPIVersion) - expectedNodePath := fmt.Sprintf("/%s/%s/nodes", apiPrefix, apiVersion) - - tf := cmdtesting.NewTestFactory().WithNamespace("test") - defer tf.Cleanup() - - codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) - ns := scheme.Codecs.WithoutConversion() - - 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 == "/api": - return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: ioutil.NopCloser(bytes.NewReader([]byte(apibody)))}, nil - case p == "/apis": - return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: ioutil.NopCloser(bytes.NewReader([]byte(apisbody)))}, nil - case p == expectedMetricsPath && m == "GET": - body, err := marshallBody(metrics) - if err != nil { - t.Errorf("unexpected error: %v", err) - } - return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil - case p == expectedNodePath && m == "GET": - return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, nodes)}, nil - default: - t.Fatalf("unexpected request: %#v\nGot URL: %#v\nExpected path: %#v", req, req.URL, expectedMetricsPath) - return nil, nil - } - }), - } - tf.ClientConfigVal = cmdtesting.DefaultClientConfig() - streams, _, buf, _ := genericclioptions.NewTestIOStreams() - - cmd := NewCmdTopNode(tf, nil, streams) - cmd.Flags().Set("no-headers", "true") - cmd.Run(cmd, []string{}) - - // Check the presence of node names in the output. - result := buf.String() - for _, m := range metrics.Items { - if !strings.Contains(result, m.Name) { - t.Errorf("missing metrics for %s: \n%s", m.Name, result) - } - } - if strings.Contains(result, "MEMORY") { - t.Errorf("should not print headers with --no-headers option set:\n%s\n", result) - } -} - -func TestTopNodeAllMetricsCustomDefaults(t *testing.T) { - customBaseHeapsterServiceAddress := "/api/v1/namespaces/custom-namespace/services/https:custom-heapster-service:/proxy" - customBaseMetricsAddress := customBaseHeapsterServiceAddress + "/apis/metrics" - - cmdtesting.InitTestErrorHandler(t) - metrics, nodes := testNodeV1alpha1MetricsData() - expectedMetricsPath := fmt.Sprintf("%s/%s/nodes", customBaseMetricsAddress, metricsAPIVersion) - expectedNodePath := fmt.Sprintf("/%s/%s/nodes", apiPrefix, apiVersion) - - tf := cmdtesting.NewTestFactory().WithNamespace("test") - defer tf.Cleanup() - - codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) - ns := scheme.Codecs.WithoutConversion() - - 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 == "/api": - return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: ioutil.NopCloser(bytes.NewReader([]byte(apibody)))}, nil - case p == "/apis": - return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: ioutil.NopCloser(bytes.NewReader([]byte(apisbody)))}, nil - case p == expectedMetricsPath && m == "GET": - body, err := marshallBody(metrics) - if err != nil { - t.Errorf("unexpected error: %v", err) - } - return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil - case p == expectedNodePath && m == "GET": - return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, nodes)}, nil - default: - t.Fatalf("unexpected request: %#v\nGot URL: %#v\nExpected path: %#v", req, req.URL, expectedMetricsPath) - return nil, nil - } - }), - } - tf.ClientConfigVal = cmdtesting.DefaultClientConfig() - streams, _, buf, _ := genericclioptions.NewTestIOStreams() - - opts := &TopNodeOptions{ - HeapsterOptions: HeapsterTopOptions{ - Namespace: "custom-namespace", - Scheme: "https", - Service: "custom-heapster-service", - }, - IOStreams: streams, - } - cmd := NewCmdTopNode(tf, opts, streams) - cmd.Run(cmd, []string{}) - - // Check the presence of node names in the output. - result := buf.String() - for _, m := range metrics.Items { - if !strings.Contains(result, m.Name) { - t.Errorf("missing metrics for %s: \n%s", m.Name, result) - } - } -} - -func TestTopNodeWithNameMetrics(t *testing.T) { - cmdtesting.InitTestErrorHandler(t) - metrics, nodes := testNodeV1alpha1MetricsData() - expectedMetrics := metrics.Items[0] - expectedNode := nodes.Items[0] - nonExpectedMetrics := metricsv1alpha1api.NodeMetricsList{ - ListMeta: metrics.ListMeta, - Items: metrics.Items[1:], - } - expectedPath := fmt.Sprintf("%s/%s/nodes/%s", baseMetricsAddress, metricsAPIVersion, expectedMetrics.Name) - expectedNodePath := fmt.Sprintf("/%s/%s/nodes/%s", apiPrefix, apiVersion, expectedMetrics.Name) - - tf := cmdtesting.NewTestFactory().WithNamespace("test") - defer tf.Cleanup() - - codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) - ns := scheme.Codecs.WithoutConversion() - - 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 == "/api": - return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: ioutil.NopCloser(bytes.NewReader([]byte(apibody)))}, nil - case p == "/apis": - return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: ioutil.NopCloser(bytes.NewReader([]byte(apisbody)))}, nil - case p == expectedPath && m == "GET": - body, err := marshallBody(expectedMetrics) - if err != nil { - t.Errorf("unexpected error: %v", err) - } - return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil - case p == expectedNodePath && m == "GET": - return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &expectedNode)}, nil - default: - t.Fatalf("unexpected request: %#v\nGot URL: %#v\nExpected path: %#v", req, req.URL, expectedPath) - return nil, nil - } - }), - } - tf.ClientConfigVal = cmdtesting.DefaultClientConfig() - streams, _, buf, _ := genericclioptions.NewTestIOStreams() - - cmd := NewCmdTopNode(tf, nil, streams) - 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.Items { - if strings.Contains(result, m.Name) { - t.Errorf("unexpected metrics for %s: \n%s", m.Name, result) - } - } -} - -func TestTopNodeWithLabelSelectorMetrics(t *testing.T) { - cmdtesting.InitTestErrorHandler(t) - metrics, nodes := testNodeV1alpha1MetricsData() - expectedMetrics := metricsv1alpha1api.NodeMetricsList{ - ListMeta: metrics.ListMeta, - Items: metrics.Items[0:1], - } - expectedNodes := v1.NodeList{ - ListMeta: nodes.ListMeta, - Items: nodes.Items[0:1], - } - nonExpectedMetrics := metricsv1alpha1api.NodeMetricsList{ - ListMeta: metrics.ListMeta, - Items: metrics.Items[1:], - } - label := "key=value" - expectedPath := fmt.Sprintf("%s/%s/nodes", baseMetricsAddress, metricsAPIVersion) - expectedQuery := fmt.Sprintf("labelSelector=%s", url.QueryEscape(label)) - expectedNodePath := fmt.Sprintf("/%s/%s/nodes", apiPrefix, apiVersion) - - tf := cmdtesting.NewTestFactory().WithNamespace("test") - defer tf.Cleanup() - - codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) - ns := scheme.Codecs.WithoutConversion() - - 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 == "/api": - return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: ioutil.NopCloser(bytes.NewReader([]byte(apibody)))}, nil - case p == "/apis": - return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: ioutil.NopCloser(bytes.NewReader([]byte(apisbody)))}, nil - case p == expectedPath && m == "GET" && q == expectedQuery: - body, err := marshallBody(expectedMetrics) - if err != nil { - t.Errorf("unexpected error: %v", err) - } - return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil - case p == expectedNodePath && m == "GET": - return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &expectedNodes)}, nil - default: - t.Fatalf("unexpected request: %#v\nGot URL: %#v\nExpected path: %#v", req, req.URL, expectedPath) - return nil, nil - } - }), - } - tf.ClientConfigVal = cmdtesting.DefaultClientConfig() - streams, _, buf, _ := genericclioptions.NewTestIOStreams() - - cmd := NewCmdTopNode(tf, nil, streams) - 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.Items { - if !strings.Contains(result, m.Name) { - t.Errorf("missing metrics for %s: \n%s", m.Name, result) - } - } - for _, m := range nonExpectedMetrics.Items { - if strings.Contains(result, m.Name) { - t.Errorf("unexpected metrics for %s: \n%s", m.Name, result) - } - } -} - -func TestTopNodeWithSortByCpuMetrics(t *testing.T) { - cmdtesting.InitTestErrorHandler(t) - metrics, nodes := testNodeV1beta1MetricsData() - expectedMetrics := metricsv1beta1api.NodeMetricsList{ - ListMeta: metrics.ListMeta, - Items: metrics.Items[:], - } - - expectedMetricsPath := fmt.Sprintf("%s/%s/nodes", baseMetricsAddress, metricsAPIVersion) - expectedNodePath := fmt.Sprintf("/%s/%s/nodes", apiPrefix, apiVersion) - expectedNodes := []string{"node2", "node3", "node1"} - - tf := cmdtesting.NewTestFactory().WithNamespace("test") - defer tf.Cleanup() - - codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) - ns := scheme.Codecs - - 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 == "/api": - return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: ioutil.NopCloser(bytes.NewReader([]byte(apibody)))}, nil - case p == "/apis": - return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: ioutil.NopCloser(bytes.NewReader([]byte(apisbody)))}, nil - case p == expectedMetricsPath && m == "GET": - body, err := marshallBody(expectedMetrics) - if err != nil { - t.Errorf("unexpected error: %v", err) - } - return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: body}, nil - case p == expectedNodePath && m == "GET": - return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, nodes)}, nil - default: - t.Fatalf("unexpected request: %#v\nGot URL: %#v\nExpected path: %#v", req, req.URL, expectedMetricsPath) - return nil, nil - } - }), - } - tf.ClientConfigVal = cmdtesting.DefaultClientConfig() - streams, _, buf, _ := genericclioptions.NewTestIOStreams() - - cmd := NewCmdTopNode(tf, nil, streams) - cmd.Flags().Set("sort-by", "cpu") - - cmd.Run(cmd, []string{}) - - // Check the presence of node names in the output. - result := buf.String() - - for _, m := range expectedMetrics.Items { - if !strings.Contains(result, m.Name) { - t.Errorf("missing metrics for %s: \n%s", m.Name, result) - } - } - - resultLines := strings.Split(result, "\n") - resultNodes := make([]string, len(resultLines)-2) // don't process first (header) and last (empty) line - - for i, line := range resultLines[1 : len(resultLines)-1] { // don't process first (header) and last (empty) line - lineFirstColumn := strings.Split(line, " ")[0] - resultNodes[i] = lineFirstColumn - } - - if !reflect.DeepEqual(resultNodes, expectedNodes) { - t.Errorf("kinds not matching:\n\texpectedKinds: %v\n\tgotKinds: %v\n", expectedNodes, resultNodes) - } - -} - -func TestTopNodeWithSortByMemoryMetrics(t *testing.T) { - cmdtesting.InitTestErrorHandler(t) - metrics, nodes := testNodeV1beta1MetricsData() - expectedMetrics := metricsv1beta1api.NodeMetricsList{ - ListMeta: metrics.ListMeta, - Items: metrics.Items[:], - } - expectedMetricsPath := fmt.Sprintf("%s/%s/nodes", baseMetricsAddress, metricsAPIVersion) - expectedNodePath := fmt.Sprintf("/%s/%s/nodes", apiPrefix, apiVersion) - expectedNodes := []string{"node2", "node3", "node1"} - - tf := cmdtesting.NewTestFactory().WithNamespace("test") - defer tf.Cleanup() - - codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) - ns := scheme.Codecs - - 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 == "/api": - return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: ioutil.NopCloser(bytes.NewReader([]byte(apibody)))}, nil - case p == "/apis": - return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: ioutil.NopCloser(bytes.NewReader([]byte(apisbody)))}, nil - case p == expectedMetricsPath && m == "GET": - body, err := marshallBody(expectedMetrics) - if err != nil { - t.Errorf("unexpected error: %v", err) - } - return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: body}, nil - case p == expectedNodePath && m == "GET": - return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, nodes)}, nil - default: - t.Fatalf("unexpected request: %#v\nGot URL: %#v\nExpected path: %#v", req, req.URL, expectedMetricsPath) - return nil, nil - } - }), - } - tf.ClientConfigVal = cmdtesting.DefaultClientConfig() - streams, _, buf, _ := genericclioptions.NewTestIOStreams() - - cmd := NewCmdTopNode(tf, nil, streams) - cmd.Flags().Set("sort-by", "memory") - - cmd.Run(cmd, []string{}) - - // Check the presence of node names in the output. - result := buf.String() - - for _, m := range expectedMetrics.Items { - if !strings.Contains(result, m.Name) { - t.Errorf("missing metrics for %s: \n%s", m.Name, result) - } - } - - resultLines := strings.Split(result, "\n") - resultNodes := make([]string, len(resultLines)-2) // don't process first (header) and last (empty) line - - for i, line := range resultLines[1 : len(resultLines)-1] { // don't process first (header) and last (empty) line - lineFirstColumn := strings.Split(line, " ")[0] - resultNodes[i] = lineFirstColumn - } - - if !reflect.DeepEqual(resultNodes, expectedNodes) { - t.Errorf("kinds not matching:\n\texpectedKinds: %v\n\tgotKinds: %v\n", expectedNodes, resultNodes) - } - -} - -func TestTopNodeAllMetricsFromMetricsServer(t *testing.T) { +func TestTopNodeAllMetricsFrom(t *testing.T) { cmdtesting.InitTestErrorHandler(t) expectedMetrics, nodes := testNodeV1beta1MetricsData() expectedNodePath := fmt.Sprintf("/%s/%s/nodes", apiPrefix, apiVersion) @@ -486,7 +102,7 @@ func TestTopNodeAllMetricsFromMetricsServer(t *testing.T) { } } -func TestTopNodeWithNameMetricsFromMetricsServer(t *testing.T) { +func TestTopNodeWithNameMetricsFrom(t *testing.T) { cmdtesting.InitTestErrorHandler(t) metrics, nodes := testNodeV1beta1MetricsData() expectedMetrics := metrics.Items[0] @@ -556,7 +172,7 @@ func TestTopNodeWithNameMetricsFromMetricsServer(t *testing.T) { } } -func TestTopNodeWithLabelSelectorMetricsFromMetricsServer(t *testing.T) { +func TestTopNodeWithLabelSelectorMetricsFrom(t *testing.T) { cmdtesting.InitTestErrorHandler(t) metrics, nodes := testNodeV1beta1MetricsData() expectedMetrics := &metricsv1beta1api.NodeMetricsList{ @@ -637,7 +253,7 @@ func TestTopNodeWithLabelSelectorMetricsFromMetricsServer(t *testing.T) { } } -func TestTopNodeWithSortByCpuMetricsFromMetricsServer(t *testing.T) { +func TestTopNodeWithSortByCpuMetricsFrom(t *testing.T) { cmdtesting.InitTestErrorHandler(t) metrics, nodes := testNodeV1beta1MetricsData() expectedMetrics := &metricsv1beta1api.NodeMetricsList{ @@ -723,7 +339,7 @@ func TestTopNodeWithSortByCpuMetricsFromMetricsServer(t *testing.T) { } -func TestTopNodeWithSortByMemoryMetricsFromMetricsServer(t *testing.T) { +func TestTopNodeWithSortByMemoryMetricsFrom(t *testing.T) { cmdtesting.InitTestErrorHandler(t) metrics, nodes := testNodeV1beta1MetricsData() expectedMetrics := &metricsv1beta1api.NodeMetricsList{ diff --git a/pkg/cmd/top/top_pod.go b/pkg/cmd/top/top_pod.go index e56e5fe9..a1886efd 100644 --- a/pkg/cmd/top/top_pod.go +++ b/pkg/cmd/top/top_pod.go @@ -49,8 +49,6 @@ type TopPodOptions struct { PrintContainers bool NoHeaders bool PodClient corev1client.PodsGetter - HeapsterOptions HeapsterTopOptions - Client *metricsutil.HeapsterMetricsClient Printer *metricsutil.TopCmdPrinter DiscoveryClient discovery.DiscoveryInterface MetricsClient metricsclientset.Interface @@ -108,7 +106,6 @@ func NewCmdTopPod(f cmdutil.Factory, o *TopPodOptions, streams genericclioptions cmd.Flags().BoolVar(&o.PrintContainers, "containers", o.PrintContainers, "If present, print usage of containers within a pod.") cmd.Flags().BoolVarP(&o.AllNamespaces, "all-namespaces", "A", o.AllNamespaces, "If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace.") cmd.Flags().BoolVar(&o.NoHeaders, "no-headers", o.NoHeaders, "If present, print output without headers.") - o.HeapsterOptions.Bind(cmd.Flags()) return cmd } @@ -140,7 +137,6 @@ func (o *TopPodOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []s } o.PodClient = clientset.CoreV1() - o.Client = metricsutil.NewHeapsterMetricsClient(clientset.CoreV1(), o.HeapsterOptions.Namespace, o.HeapsterOptions.Scheme, o.HeapsterOptions.Service, o.HeapsterOptions.Port) o.Printer = metricsutil.NewTopCmdPrinter(o.Out) return nil @@ -175,17 +171,12 @@ func (o TopPodOptions) RunTopPod() error { metricsAPIAvailable := SupportedMetricsAPIVersionAvailable(apiGroups) - var metrics *metricsapi.PodMetricsList - if metricsAPIAvailable { - metrics, err = getMetricsFromMetricsAPI(o.MetricsClient, o.Namespace, o.ResourceName, o.AllNamespaces, selector) - if err != nil { - return err - } - } else { - metrics, err = o.Client.GetPodMetrics(o.Namespace, o.ResourceName, o.AllNamespaces, selector) - if err != nil { - return err - } + if !metricsAPIAvailable { + return errors.New("Metrics API not available") + } + metrics, err := getMetricsFromMetricsAPI(o.MetricsClient, o.Namespace, o.ResourceName, o.AllNamespaces, selector) + if err != nil { + return err } // TODO: Refactor this once Heapster becomes the API server. diff --git a/pkg/cmd/top/top_pod_test.go b/pkg/cmd/top/top_pod_test.go index a51caad6..05671c5e 100644 --- a/pkg/cmd/top/top_pod_test.go +++ b/pkg/cmd/top/top_pod_test.go @@ -26,15 +26,11 @@ import ( "testing" "time" - "github.com/googleapis/gnostic/OpenAPIv2" - "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - apiversion "k8s.io/apimachinery/pkg/version" "k8s.io/cli-runtime/pkg/genericclioptions" - restclient "k8s.io/client-go/rest" "k8s.io/client-go/rest/fake" core "k8s.io/client-go/testing" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" @@ -46,9 +42,7 @@ import ( ) const ( - topPathPrefix = baseMetricsAddress + "/" + metricsAPIVersion - topMetricsAPIPathPrefix = "/apis/metrics.k8s.io/v1beta1" - apibody = `{ + apibody = `{ "kind": "APIVersions", "versions": [ "v1" @@ -60,12 +54,6 @@ const ( } ] }` - // This is not the full output one would usually get, just a trimmed down version. - apisbody = `{ - "kind": "APIGroupList", - "apiVersion": "v1", - "groups": [{}] -}` apisbodyWithMetrics = `{ "kind": "APIGroupList", @@ -90,191 +78,12 @@ const ( ) func TestTopPod(t *testing.T) { - testNS := "testns" - testCases := []struct { - name string - flags map[string]string - args []string - expectedPath string - expectedQuery string - expectedPods []string - namespaces []string - containers bool - listsNamespaces bool - }{ - { - name: "all namespaces", - flags: map[string]string{"all-namespaces": "true"}, - expectedPath: topPathPrefix + "/pods", - namespaces: []string{testNS, "secondtestns", "thirdtestns"}, - listsNamespaces: true, - }, - { - name: "all in namespace", - expectedPath: topPathPrefix + "/namespaces/" + testNS + "/pods", - namespaces: []string{testNS, testNS}, - }, - { - name: "pod with name", - args: []string{"pod1"}, - expectedPath: topPathPrefix + "/namespaces/" + testNS + "/pods/pod1", - namespaces: []string{testNS}, - }, - { - name: "pod with label selector", - flags: map[string]string{"selector": "key=value"}, - expectedPath: topPathPrefix + "/namespaces/" + testNS + "/pods", - expectedQuery: "labelSelector=" + url.QueryEscape("key=value"), - namespaces: []string{testNS, testNS}, - }, - { - name: "pod with container metrics", - flags: map[string]string{"containers": "true"}, - args: []string{"pod1"}, - expectedPath: topPathPrefix + "/namespaces/" + testNS + "/pods/pod1", - namespaces: []string{testNS}, - containers: true, - }, - { - name: "no-headers set", - flags: map[string]string{"containers": "true", "no-headers": "true"}, - args: []string{"pod1"}, - expectedPath: topPathPrefix + "/namespaces/" + testNS + "/pods/pod1", - namespaces: []string{testNS}, - containers: true, - }, - { - name: "pod with label sort by cpu", - flags: map[string]string{"sort-by": "cpu"}, - expectedPath: topPathPrefix + "/namespaces/" + testNS + "/pods", - expectedPods: []string{"pod2", "pod3", "pod1"}, - namespaces: []string{testNS, testNS, testNS}, - }, - { - name: "pod with label sort by memory", - flags: map[string]string{"sort-by": "memory"}, - expectedPath: topPathPrefix + "/namespaces/" + testNS + "/pods", - expectedPods: []string{"pod2", "pod3", "pod1"}, - namespaces: []string{testNS, testNS, testNS}, - }, - } - cmdtesting.InitTestErrorHandler(t) - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - t.Logf("Running test case: %s", testCase.name) - metricsList := testPodMetricsData() - var expectedMetrics []metricsv1alpha1api.PodMetrics - var expectedContainerNames, nonExpectedMetricsNames []string - for n, m := range metricsList { - if n < len(testCase.namespaces) { - m.Namespace = testCase.namespaces[n] - expectedMetrics = append(expectedMetrics, m) - for _, c := range m.Containers { - expectedContainerNames = append(expectedContainerNames, c.Name) - } - } else { - nonExpectedMetricsNames = append(nonExpectedMetricsNames, m.Name) - } - } - - var response interface{} - if len(expectedMetrics) == 1 { - response = expectedMetrics[0] - } else { - response = metricsv1alpha1api.PodMetricsList{ - ListMeta: metav1.ListMeta{ - ResourceVersion: "2", - }, - Items: expectedMetrics, - } - } - - tf := cmdtesting.NewTestFactory().WithNamespace(testNS) - defer tf.Cleanup() - - ns := scheme.Codecs.WithoutConversion() - - 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 == "/api": - return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: ioutil.NopCloser(bytes.NewReader([]byte(apibody)))}, nil - case p == "/apis": - return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: ioutil.NopCloser(bytes.NewReader([]byte(apisbody)))}, nil - case p == testCase.expectedPath && m == "GET" && (testCase.expectedQuery == "" || q == testCase.expectedQuery): - body, err := marshallBody(response) - if err != nil { - t.Errorf("%s: unexpected error: %v", testCase.name, err) - } - return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil - default: - t.Fatalf("%s: unexpected request: %#v\nGot URL: %#v\nExpected path: %#v\nExpected query: %#v", - testCase.name, req, req.URL, testCase.expectedPath, testCase.expectedQuery) - return nil, nil - } - }), - } - tf.ClientConfigVal = cmdtesting.DefaultClientConfig() - streams, _, buf, _ := genericclioptions.NewTestIOStreams() - - cmd := NewCmdTopPod(tf, nil, streams) - for name, value := range testCase.flags { - cmd.Flags().Set(name, value) - } - cmd.Run(cmd, testCase.args) - - // Check the presence of pod names&namespaces/container names in the output. - result := buf.String() - - if testCase.containers { - for _, containerName := range expectedContainerNames { - if !strings.Contains(result, containerName) { - t.Errorf("%s: missing metrics for container %s: \n%s", testCase.name, containerName, result) - } - } - } - for _, m := range expectedMetrics { - if !strings.Contains(result, m.Name) { - t.Errorf("%s: missing metrics for %s: \n%s", testCase.name, m.Name, result) - } - if testCase.listsNamespaces && !strings.Contains(result, m.Namespace) { - t.Errorf("%s: missing metrics for %s/%s: \n%s", testCase.name, m.Namespace, m.Name, result) - } - } - for _, name := range nonExpectedMetricsNames { - if strings.Contains(result, name) { - t.Errorf("%s: unexpected metrics for %s: \n%s", testCase.name, name, result) - } - } - if cmdutil.GetFlagBool(cmd, "no-headers") && strings.Contains(result, "MEMORY") { - t.Errorf("%s: unexpected headers with no-headers option set: \n%s", testCase.name, result) - } - if cmdutil.GetFlagString(cmd, "sort-by") == "cpu" || cmdutil.GetFlagString(cmd, "sort-by") == "memory" { - resultLines := strings.Split(result, "\n") - resultPods := make([]string, len(resultLines)-2) // don't process first (header) and last (empty) line - - for i, line := range resultLines[1 : len(resultLines)-1] { // don't process first (header) and last (empty) line - lineFirstColumn := strings.Split(line, " ")[0] - resultPods[i] = lineFirstColumn - } - - if !reflect.DeepEqual(testCase.expectedPods, resultPods) { - t.Errorf("kinds not matching:\n\texpectedKinds: %v\n\tgotKinds: %v\n", testCase.expectedPods, resultPods) - } - } - }) - } -} - -func TestTopPodWithMetricsServer(t *testing.T) { testNS := "testns" testCases := []struct { name string namespace string options *TopPodOptions args []string - expectedPath string expectedQuery string expectedPods []string namespaces []string @@ -284,47 +93,40 @@ func TestTopPodWithMetricsServer(t *testing.T) { { name: "all namespaces", options: &TopPodOptions{AllNamespaces: true}, - expectedPath: topMetricsAPIPathPrefix + "/pods", namespaces: []string{testNS, "secondtestns", "thirdtestns"}, listsNamespaces: true, }, { - name: "all in namespace", - expectedPath: topMetricsAPIPathPrefix + "/namespaces/" + testNS + "/pods", - namespaces: []string{testNS, testNS}, + name: "all in namespace", + namespaces: []string{testNS, testNS}, }, { - name: "pod with name", - args: []string{"pod1"}, - expectedPath: topMetricsAPIPathPrefix + "/namespaces/" + testNS + "/pods/pod1", - namespaces: []string{testNS}, + name: "pod with name", + args: []string{"pod1"}, + namespaces: []string{testNS}, }, { name: "pod with label selector", options: &TopPodOptions{Selector: "key=value"}, - expectedPath: topMetricsAPIPathPrefix + "/namespaces/" + testNS + "/pods", expectedQuery: "labelSelector=" + url.QueryEscape("key=value"), namespaces: []string{testNS, testNS}, }, { - name: "pod with container metrics", - options: &TopPodOptions{PrintContainers: true}, - args: []string{"pod1"}, - expectedPath: topMetricsAPIPathPrefix + "/namespaces/" + testNS + "/pods/pod1", - namespaces: []string{testNS}, - containers: true, + name: "pod with container metrics", + options: &TopPodOptions{PrintContainers: true}, + args: []string{"pod1"}, + namespaces: []string{testNS}, + containers: true, }, { name: "pod with label sort by cpu", options: &TopPodOptions{SortBy: "cpu"}, - expectedPath: topPathPrefix + "/namespaces/" + testNS + "/pods", expectedPods: []string{"pod2", "pod3", "pod1"}, namespaces: []string{testNS, testNS, testNS}, }, { name: "pod with label sort by memory", options: &TopPodOptions{SortBy: "memory"}, - expectedPath: topPathPrefix + "/namespaces/" + testNS + "/pods", expectedPods: []string{"pod2", "pod3", "pod1"}, namespaces: []string{testNS, testNS, testNS}, }, @@ -457,21 +259,18 @@ func TestTopPodNoResourcesFound(t *testing.T) { namespace string expectedOutput string expectedErr string - expectedPath string }{ { name: "all namespaces", options: &TopPodOptions{AllNamespaces: true}, expectedOutput: "", expectedErr: "No resources found\n", - expectedPath: topMetricsAPIPathPrefix + "/pods", }, { name: "all in namespace", namespace: testNS, expectedOutput: "", expectedErr: "No resources found in " + testNS + " namespace.\n", - expectedPath: topMetricsAPIPathPrefix + "/namespaces/" + testNS + "/pods", }, } cmdtesting.InitTestErrorHandler(t) @@ -551,237 +350,6 @@ func TestTopPodNoResourcesFound(t *testing.T) { } } -type fakeDiscovery struct{} - -// ServerGroups returns the supported groups, with information like supported versions and the -// preferred version. -func (d *fakeDiscovery) ServerGroups() (*metav1.APIGroupList, error) { - return nil, nil -} - -// ServerResourcesForGroupVersion returns the supported resources for a group and version. -func (d *fakeDiscovery) ServerResourcesForGroupVersion(groupVersion string) (*metav1.APIResourceList, error) { - return nil, nil -} - -// ServerResources returns the supported resources for all groups and versions. -// Deprecated: use ServerGroupsAndResources instead. -func (d *fakeDiscovery) ServerResources() ([]*metav1.APIResourceList, error) { - return nil, nil -} - -// ServerGroupsAndResources returns the supported groups and resources for all groups and versions. -func (d *fakeDiscovery) ServerGroupsAndResources() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) { - return nil, nil, nil -} - -// ServerPreferredResources returns the supported resources with the version preferred by the -// server. -func (d *fakeDiscovery) ServerPreferredResources() ([]*metav1.APIResourceList, error) { - return nil, nil -} - -// ServerPreferredNamespacedResources returns the supported namespaced resources with the -// version preferred by the server. -func (d *fakeDiscovery) ServerPreferredNamespacedResources() ([]*metav1.APIResourceList, error) { - return nil, nil -} - -// ServerVersion retrieves and parses the server's version (git version). -func (d *fakeDiscovery) ServerVersion() (*apiversion.Info, error) { - return nil, nil -} - -// OpenAPISchema retrieves and parses the swagger API schema the server supports. -func (d *fakeDiscovery) OpenAPISchema() (*openapi_v2.Document, error) { - return nil, nil -} - -// RESTClient returns a RESTClient that is used to communicate -// with API server by this client implementation. -func (d *fakeDiscovery) RESTClient() restclient.Interface { - return nil -} - -func TestTopPodCustomDefaults(t *testing.T) { - customBaseHeapsterServiceAddress := "/api/v1/namespaces/custom-namespace/services/https:custom-heapster-service:/proxy" - customBaseMetricsAddress := customBaseHeapsterServiceAddress + "/apis/metrics" - customTopPathPrefix := customBaseMetricsAddress + "/" + metricsAPIVersion - - testNS := "custom-namespace" - testCases := []struct { - name string - flags map[string]string - args []string - expectedPath string - expectedQuery string - expectedPods []string - namespaces []string - containers bool - listsNamespaces bool - }{ - { - name: "all namespaces", - flags: map[string]string{"all-namespaces": "true"}, - expectedPath: customTopPathPrefix + "/pods", - namespaces: []string{testNS, "secondtestns", "thirdtestns"}, - listsNamespaces: true, - }, - { - name: "all in namespace", - expectedPath: customTopPathPrefix + "/namespaces/" + testNS + "/pods", - namespaces: []string{testNS, testNS}, - }, - { - name: "pod with name", - args: []string{"pod1"}, - expectedPath: customTopPathPrefix + "/namespaces/" + testNS + "/pods/pod1", - namespaces: []string{testNS}, - }, - { - name: "pod with label selector", - flags: map[string]string{"selector": "key=value"}, - expectedPath: customTopPathPrefix + "/namespaces/" + testNS + "/pods", - expectedQuery: "labelSelector=" + url.QueryEscape("key=value"), - namespaces: []string{testNS, testNS}, - }, - { - name: "pod with container metrics", - flags: map[string]string{"containers": "true"}, - args: []string{"pod1"}, - expectedPath: customTopPathPrefix + "/namespaces/" + testNS + "/pods/pod1", - namespaces: []string{testNS}, - containers: true, - }, - { - name: "pod with label sort by cpu", - flags: map[string]string{"sort-by": "cpu"}, - expectedPath: customTopPathPrefix + "/namespaces/" + testNS + "/pods", - expectedPods: []string{"pod2", "pod3", "pod1"}, - namespaces: []string{testNS, testNS, testNS}, - }, - { - name: "pod with label sort by memory", - flags: map[string]string{"sort-by": "memory"}, - expectedPath: customTopPathPrefix + "/namespaces/" + testNS + "/pods", - expectedPods: []string{"pod2", "pod3", "pod1"}, - namespaces: []string{testNS, testNS, testNS}, - }, - } - cmdtesting.InitTestErrorHandler(t) - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - t.Logf("Running test case: %s", testCase.name) - metricsList := testPodMetricsData() - var expectedMetrics []metricsv1alpha1api.PodMetrics - var expectedContainerNames, nonExpectedMetricsNames []string - for n, m := range metricsList { - if n < len(testCase.namespaces) { - m.Namespace = testCase.namespaces[n] - expectedMetrics = append(expectedMetrics, m) - for _, c := range m.Containers { - expectedContainerNames = append(expectedContainerNames, c.Name) - } - } else { - nonExpectedMetricsNames = append(nonExpectedMetricsNames, m.Name) - } - } - - var response interface{} - if len(expectedMetrics) == 1 { - response = expectedMetrics[0] - } else { - response = metricsv1alpha1api.PodMetricsList{ - ListMeta: metav1.ListMeta{ - ResourceVersion: "2", - }, - Items: expectedMetrics, - } - } - - tf := cmdtesting.NewTestFactory().WithNamespace(testNS) - defer tf.Cleanup() - - ns := scheme.Codecs.WithoutConversion() - - 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 == "/api": - return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: ioutil.NopCloser(bytes.NewReader([]byte(apibody)))}, nil - case p == "/apis": - return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: ioutil.NopCloser(bytes.NewReader([]byte(apisbody)))}, nil - case p == testCase.expectedPath && m == "GET" && (testCase.expectedQuery == "" || q == testCase.expectedQuery): - body, err := marshallBody(response) - if err != nil { - t.Errorf("%s: unexpected error: %v", testCase.name, err) - } - return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil - default: - t.Fatalf("%s: unexpected request: %#v\nGot URL: %#v\nExpected path: %#v\nExpected query: %#v", - testCase.name, req, req.URL, testCase.expectedPath, testCase.expectedQuery) - return nil, nil - } - }), - } - tf.ClientConfigVal = cmdtesting.DefaultClientConfig() - streams, _, buf, _ := genericclioptions.NewTestIOStreams() - - opts := &TopPodOptions{ - HeapsterOptions: HeapsterTopOptions{ - Namespace: "custom-namespace", - Scheme: "https", - Service: "custom-heapster-service", - }, - DiscoveryClient: &fakeDiscovery{}, - IOStreams: streams, - } - cmd := NewCmdTopPod(tf, opts, streams) - for name, value := range testCase.flags { - cmd.Flags().Set(name, value) - } - cmd.Run(cmd, testCase.args) - - // Check the presence of pod names&namespaces/container names in the output. - result := buf.String() - if testCase.containers { - for _, containerName := range expectedContainerNames { - if !strings.Contains(result, containerName) { - t.Errorf("%s: missing metrics for container %s: \n%s", testCase.name, containerName, result) - } - } - } - for _, m := range expectedMetrics { - if !strings.Contains(result, m.Name) { - t.Errorf("%s: missing metrics for %s: \n%s", testCase.name, m.Name, result) - } - if testCase.listsNamespaces && !strings.Contains(result, m.Namespace) { - t.Errorf("%s: missing metrics for %s/%s: \n%s", testCase.name, m.Namespace, m.Name, result) - } - } - for _, name := range nonExpectedMetricsNames { - if strings.Contains(result, name) { - t.Errorf("%s: unexpected metrics for %s: \n%s", testCase.name, name, result) - } - } - if cmdutil.GetFlagString(cmd, "sort-by") == "cpu" || cmdutil.GetFlagString(cmd, "sort-by") == "memory" { - resultLines := strings.Split(result, "\n") - resultPods := make([]string, len(resultLines)-2) // don't process first (header) and last (empty) line - - for i, line := range resultLines[1 : len(resultLines)-1] { // don't process first (header) and last (empty) line - lineFirstColumn := strings.Split(line, " ")[0] - resultPods[i] = lineFirstColumn - } - - if !reflect.DeepEqual(testCase.expectedPods, resultPods) { - t.Errorf("kinds not matching:\n\texpectedKinds: %v\n\tgotKinds: %v\n", testCase.expectedPods, resultPods) - } - } - }) - } -} - func testV1beta1PodMetricsData() []metricsv1beta1api.PodMetrics { return []metricsv1beta1api.PodMetrics{ { @@ -852,74 +420,3 @@ func testV1beta1PodMetricsData() []metricsv1beta1api.PodMetrics { }, } } - -func testPodMetricsData() []metricsv1alpha1api.PodMetrics { - return []metricsv1alpha1api.PodMetrics{ - { - ObjectMeta: metav1.ObjectMeta{Name: "pod1", Namespace: "test", ResourceVersion: "10"}, - Window: metav1.Duration{Duration: time.Minute}, - Containers: []metricsv1alpha1api.ContainerMetrics{ - { - Name: "container1-1", - Usage: v1.ResourceList{ - v1.ResourceCPU: *resource.NewMilliQuantity(1, resource.DecimalSI), - v1.ResourceMemory: *resource.NewQuantity(2*(1024*1024), resource.DecimalSI), - v1.ResourceStorage: *resource.NewQuantity(3*(1024*1024), resource.DecimalSI), - }, - }, - { - Name: "container1-2", - Usage: v1.ResourceList{ - v1.ResourceCPU: *resource.NewMilliQuantity(4, resource.DecimalSI), - v1.ResourceMemory: *resource.NewQuantity(5*(1024*1024), resource.DecimalSI), - v1.ResourceStorage: *resource.NewQuantity(6*(1024*1024), resource.DecimalSI), - }, - }, - }, - }, - { - ObjectMeta: metav1.ObjectMeta{Name: "pod2", Namespace: "test", ResourceVersion: "11"}, - Window: metav1.Duration{Duration: time.Minute}, - Containers: []metricsv1alpha1api.ContainerMetrics{ - { - Name: "container2-1", - Usage: v1.ResourceList{ - v1.ResourceCPU: *resource.NewMilliQuantity(7, resource.DecimalSI), - v1.ResourceMemory: *resource.NewQuantity(8*(1024*1024), resource.DecimalSI), - v1.ResourceStorage: *resource.NewQuantity(9*(1024*1024), resource.DecimalSI), - }, - }, - { - Name: "container2-2", - Usage: v1.ResourceList{ - v1.ResourceCPU: *resource.NewMilliQuantity(10, resource.DecimalSI), - v1.ResourceMemory: *resource.NewQuantity(11*(1024*1024), resource.DecimalSI), - v1.ResourceStorage: *resource.NewQuantity(12*(1024*1024), resource.DecimalSI), - }, - }, - { - Name: "container2-3", - Usage: v1.ResourceList{ - v1.ResourceCPU: *resource.NewMilliQuantity(13, resource.DecimalSI), - v1.ResourceMemory: *resource.NewQuantity(14*(1024*1024), resource.DecimalSI), - v1.ResourceStorage: *resource.NewQuantity(15*(1024*1024), resource.DecimalSI), - }, - }, - }, - }, - { - ObjectMeta: metav1.ObjectMeta{Name: "pod3", Namespace: "test", ResourceVersion: "12"}, - Window: metav1.Duration{Duration: time.Minute}, - Containers: []metricsv1alpha1api.ContainerMetrics{ - { - Name: "container3-1", - Usage: v1.ResourceList{ - v1.ResourceCPU: *resource.NewMilliQuantity(7, resource.DecimalSI), - v1.ResourceMemory: *resource.NewQuantity(8*(1024*1024), resource.DecimalSI), - v1.ResourceStorage: *resource.NewQuantity(9*(1024*1024), resource.DecimalSI), - }, - }, - }, - }, - } -} diff --git a/pkg/cmd/top/top_test.go b/pkg/cmd/top/top_test.go index 0fbbd76f..4f8a03f5 100644 --- a/pkg/cmd/top/top_test.go +++ b/pkg/cmd/top/top_test.go @@ -30,17 +30,9 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/cli-runtime/pkg/genericclioptions" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" - metricsv1alpha1api "k8s.io/metrics/pkg/apis/metrics/v1alpha1" metricsv1beta1api "k8s.io/metrics/pkg/apis/metrics/v1beta1" ) -const ( - baseHeapsterServiceAddress = "/api/v1/namespaces/kube-system/services/http:heapster:/proxy" - baseMetricsAddress = baseHeapsterServiceAddress + "/apis/metrics" - baseMetricsServerAddress = "/apis/metrics.k8s.io/v1beta1" - metricsAPIVersion = "v1alpha1" -) - func TestTopSubcommandsExist(t *testing.T) { cmdtesting.InitTestErrorHandler(t) @@ -61,62 +53,6 @@ func marshallBody(metrics interface{}) (io.ReadCloser, error) { return ioutil.NopCloser(bytes.NewReader(result)), nil } -func testNodeV1alpha1MetricsData() (*metricsv1alpha1api.NodeMetricsList, *v1.NodeList) { - metrics := &metricsv1alpha1api.NodeMetricsList{ - ListMeta: metav1.ListMeta{ - ResourceVersion: "1", - }, - Items: []metricsv1alpha1api.NodeMetrics{ - { - ObjectMeta: metav1.ObjectMeta{Name: "node1", ResourceVersion: "10"}, - Window: metav1.Duration{Duration: time.Minute}, - Usage: v1.ResourceList{ - v1.ResourceCPU: *resource.NewMilliQuantity(1, resource.DecimalSI), - v1.ResourceMemory: *resource.NewQuantity(2*(1024*1024), resource.DecimalSI), - v1.ResourceStorage: *resource.NewQuantity(3*(1024*1024), resource.DecimalSI), - }, - }, - { - ObjectMeta: metav1.ObjectMeta{Name: "node2", ResourceVersion: "11"}, - Window: metav1.Duration{Duration: time.Minute}, - Usage: v1.ResourceList{ - v1.ResourceCPU: *resource.NewMilliQuantity(5, resource.DecimalSI), - v1.ResourceMemory: *resource.NewQuantity(6*(1024*1024), resource.DecimalSI), - v1.ResourceStorage: *resource.NewQuantity(7*(1024*1024), resource.DecimalSI), - }, - }, - }, - } - nodes := &v1.NodeList{ - ListMeta: metav1.ListMeta{ - ResourceVersion: "15", - }, - Items: []v1.Node{ - { - ObjectMeta: metav1.ObjectMeta{Name: "node1", ResourceVersion: "10"}, - Status: v1.NodeStatus{ - Allocatable: v1.ResourceList{ - v1.ResourceCPU: *resource.NewMilliQuantity(10, resource.DecimalSI), - v1.ResourceMemory: *resource.NewQuantity(20*(1024*1024), resource.DecimalSI), - v1.ResourceStorage: *resource.NewQuantity(30*(1024*1024), resource.DecimalSI), - }, - }, - }, - { - ObjectMeta: metav1.ObjectMeta{Name: "node2", ResourceVersion: "11"}, - Status: v1.NodeStatus{ - Allocatable: v1.ResourceList{ - v1.ResourceCPU: *resource.NewMilliQuantity(50, resource.DecimalSI), - v1.ResourceMemory: *resource.NewQuantity(60*(1024*1024), resource.DecimalSI), - v1.ResourceStorage: *resource.NewQuantity(70*(1024*1024), resource.DecimalSI), - }, - }, - }, - }, - } - return metrics, nodes -} - func testNodeV1beta1MetricsData() (*metricsv1beta1api.NodeMetricsList, *v1.NodeList) { metrics := &metricsv1beta1api.NodeMetricsList{ ListMeta: metav1.ListMeta{ diff --git a/pkg/metricsutil/metrics_client.go b/pkg/metricsutil/metrics_client.go deleted file mode 100644 index 55a596ed..00000000 --- a/pkg/metricsutil/metrics_client.go +++ /dev/null @@ -1,171 +0,0 @@ -/* -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 ( - "context" - "encoding/json" - "errors" - "fmt" - - "k8s.io/apimachinery/pkg/api/validation" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/runtime/schema" - corev1client "k8s.io/client-go/kubernetes/typed/core/v1" - metricsapi "k8s.io/metrics/pkg/apis/metrics" - metricsv1alpha1api "k8s.io/metrics/pkg/apis/metrics/v1alpha1" -) - -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 = schema.GroupVersion{Group: "metrics", Version: "v1alpha1"} -) - -type HeapsterMetricsClient struct { - SVCClient corev1client.ServicesGetter - HeapsterNamespace string - HeapsterScheme string - HeapsterService string - HeapsterPort string -} - -func NewHeapsterMetricsClient(svcClient corev1client.ServicesGetter, namespace, scheme, service, port string) *HeapsterMetricsClient { - return &HeapsterMetricsClient{ - SVCClient: svcClient, - HeapsterNamespace: namespace, - HeapsterScheme: scheme, - HeapsterService: service, - HeapsterPort: port, - } -} - -func podMetricsURL(namespace string, name string) (string, error) { - if namespace == metav1.NamespaceAll { - return fmt.Sprintf("%s/pods", metricsRoot), nil - } - 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.NameIsDNSSubdomain(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.NameIsDNSSubdomain(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) (*metricsapi.NodeMetricsList, error) { - params := map[string]string{"labelSelector": selector} - path, err := nodeMetricsURL(nodeName) - if err != nil { - return nil, err - } - resultRaw, err := GetHeapsterMetrics(cli, path, params) - if err != nil { - return nil, err - } - versionedMetrics := metricsv1alpha1api.NodeMetricsList{} - if len(nodeName) == 0 { - err = json.Unmarshal(resultRaw, &versionedMetrics) - if err != nil { - return nil, fmt.Errorf("failed to unmarshall heapster response: %v", err) - } - } else { - var singleMetric metricsv1alpha1api.NodeMetrics - err = json.Unmarshal(resultRaw, &singleMetric) - if err != nil { - return nil, fmt.Errorf("failed to unmarshall heapster response: %v", err) - } - versionedMetrics.Items = []metricsv1alpha1api.NodeMetrics{singleMetric} - } - metrics := &metricsapi.NodeMetricsList{} - err = metricsv1alpha1api.Convert_v1alpha1_NodeMetricsList_To_metrics_NodeMetricsList(&versionedMetrics, metrics, nil) - if err != nil { - return nil, err - } - return metrics, nil -} - -func (cli *HeapsterMetricsClient) GetPodMetrics(namespace string, podName string, allNamespaces bool, selector labels.Selector) (*metricsapi.PodMetricsList, error) { - if allNamespaces { - namespace = metav1.NamespaceAll - } - path, err := podMetricsURL(namespace, podName) - if err != nil { - return nil, err - } - - params := map[string]string{"labelSelector": selector.String()} - versionedMetrics := metricsv1alpha1api.PodMetricsList{} - - resultRaw, err := GetHeapsterMetrics(cli, path, params) - if err != nil { - return nil, err - } - if len(podName) == 0 { - err = json.Unmarshal(resultRaw, &versionedMetrics) - if err != nil { - return nil, fmt.Errorf("failed to unmarshall heapster response: %v", err) - } - } else { - var singleMetric metricsv1alpha1api.PodMetrics - err = json.Unmarshal(resultRaw, &singleMetric) - if err != nil { - return nil, fmt.Errorf("failed to unmarshall heapster response: %v", err) - } - versionedMetrics.Items = []metricsv1alpha1api.PodMetrics{singleMetric} - } - metrics := &metricsapi.PodMetricsList{} - err = metricsv1alpha1api.Convert_v1alpha1_PodMetricsList_To_metrics_PodMetricsList(&versionedMetrics, metrics, nil) - if err != nil { - return nil, err - } - return metrics, nil -} - -func GetHeapsterMetrics(cli *HeapsterMetricsClient, path string, params map[string]string) ([]byte, error) { - return cli.SVCClient.Services(cli.HeapsterNamespace). - ProxyGet(cli.HeapsterScheme, cli.HeapsterService, cli.HeapsterPort, path, params). - DoRaw(context.TODO()) -}