Merge pull request #87498 from serathius/drop-heapster

Remove heapster support for kubectl

Kubernetes-commit: d3c3907e10e4a9812a3faea00bbd3b4e61e82ac0
This commit is contained in:
Kubernetes Publisher 2020-03-24 12:37:04 -07:00
commit 09dae15fbc
9 changed files with 43 additions and 1197 deletions

2
Godeps/Godeps.json generated
View File

@ -572,7 +572,7 @@
},
{
"ImportPath": "k8s.io/apimachinery",
"Rev": "48159c651603"
"Rev": "1aec6bc431a9"
},
{
"ImportPath": "k8s.io/cli-runtime",

4
go.mod
View File

@ -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

2
go.sum
View File

@ -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=

View File

@ -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 {

View File

@ -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{

View File

@ -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.

View File

@ -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),
},
},
},
},
}
}

View File

@ -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{

View File

@ -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())
}