diff --git a/pkg/kubectl/cmd/get/get.go b/pkg/kubectl/cmd/get/get.go index 352f63ae353..36c09df826b 100644 --- a/pkg/kubectl/cmd/get/get.go +++ b/pkg/kubectl/cmd/get/get.go @@ -522,6 +522,9 @@ func (o *GetOptions) Run(f cmdutil.Factory, cmd *cobra.Command, args []string) e // track if we write any output trackingWriter := &trackingWriterWrapper{Delegate: o.Out} + // remember how much we've written + written := 0 + w := utilprinters.GetNewTabWriter(trackingWriter) for ix := range objs { var mapping *meta.RESTMapping @@ -544,11 +547,14 @@ func (o *GetOptions) Run(f cmdutil.Factory, cmd *cobra.Command, args []string) e w.Flush() w.SetRememberedWidths(nil) - // TODO: this doesn't belong here // add linebreak between resource groups (if there is more than one) // skip linebreak above first resource group if lastMapping != nil && !o.NoHeaders { - fmt.Fprintln(o.ErrOut) + // If we've written output since the last time we started a new set of headers, write an empty line to separate object types + if written != trackingWriter.Written { + fmt.Fprintln(w) + written = trackingWriter.Written + } } printer, err = o.ToPrinter(mapping, nil, printWithNamespace, printWithKind) diff --git a/pkg/kubectl/cmd/get/get_test.go b/pkg/kubectl/cmd/get/get_test.go index f906bbb458c..c2b5d0bec99 100644 --- a/pkg/kubectl/cmd/get/get_test.go +++ b/pkg/kubectl/cmd/get/get_test.go @@ -359,7 +359,7 @@ func TestGetMultipleResourceTypesShowKinds(t *testing.T) { }), } - streams, _, buf, _ := genericclioptions.NewTestIOStreams() + streams, _, buf, bufErr := genericclioptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOutput(buf) cmd.Run(cmd, []string{"all"}) @@ -367,12 +367,18 @@ func TestGetMultipleResourceTypesShowKinds(t *testing.T) { expected := `NAME AGE pod/foo pod/bar + NAME AGE service/baz ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } + + // The error out should be empty + if e, a := "", bufErr.String(); e != a { + t.Errorf("expected\n%v\ngot\n%v", e, a) + } } func TestGetMultipleTableResourceTypesShowKinds(t *testing.T) { @@ -414,7 +420,7 @@ func TestGetMultipleTableResourceTypesShowKinds(t *testing.T) { }), } - streams, _, buf, _ := genericclioptions.NewTestIOStreams() + streams, _, buf, bufErr := genericclioptions.NewTestIOStreams() cmd := NewCmdGet("kubectl", tf, streams) cmd.SetOutput(buf) cmd.Run(cmd, []string{"all"}) @@ -422,12 +428,71 @@ func TestGetMultipleTableResourceTypesShowKinds(t *testing.T) { expected := `NAME READY STATUS RESTARTS AGE pod/foo 0/0 0 pod/bar 0/0 0 + NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/baz ClusterIP ` if e, a := expected, buf.String(); e != a { t.Errorf("expected\n%v\ngot\n%v", e, a) } + + // The error out should be empty + if e, a := "", bufErr.String(); e != a { + t.Errorf("expected\n%v\ngot\n%v", e, a) + } +} + +func TestNoBlankLinesForGetAll(t *testing.T) { + tf := cmdtesting.NewTestFactory().WithNamespace("test") + defer tf.Cleanup() + + codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) + tf.UnstructuredClient = &fake.RESTClient{ + NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, + Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { + switch p, m := req.URL.Path, req.Method; { + case p == "/namespaces/test/pods" && m == "GET": + return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: emptyTableObjBody(codec)}, nil + case p == "/namespaces/test/replicationcontrollers" && m == "GET": + return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: emptyTableObjBody(codec)}, nil + case p == "/namespaces/test/services" && m == "GET": + return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: emptyTableObjBody(codec)}, nil + case p == "/namespaces/test/statefulsets" && m == "GET": + return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: emptyTableObjBody(codec)}, nil + case p == "/namespaces/test/horizontalpodautoscalers" && m == "GET": + return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: emptyTableObjBody(codec)}, nil + case p == "/namespaces/test/jobs" && m == "GET": + return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: emptyTableObjBody(codec)}, nil + case p == "/namespaces/test/cronjobs" && m == "GET": + return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: emptyTableObjBody(codec)}, nil + case p == "/namespaces/test/daemonsets" && m == "GET": + return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: emptyTableObjBody(codec)}, nil + case p == "/namespaces/test/deployments" && m == "GET": + return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: emptyTableObjBody(codec)}, nil + case p == "/namespaces/test/replicasets" && m == "GET": + return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: emptyTableObjBody(codec)}, nil + + default: + t.Fatalf("request url: %#v,and request: %#v", req.URL, req) + return nil, nil + } + }), + } + + streams, _, buf, errbuf := genericclioptions.NewTestIOStreams() + cmd := NewCmdGet("kubectl", tf, streams) + cmd.SetOutput(buf) + cmd.Run(cmd, []string{"all"}) + + expected := `` + if e, a := expected, buf.String(); e != a { + t.Errorf("expected\n%v\ngot\n%v", e, a) + } + expectedErr := `No resources found in test namespace. +` + if e, a := expectedErr, errbuf.String(); e != a { + t.Errorf("expectedErr\n%v\ngot\n%v", e, a) + } } func TestGetObjectsShowLabels(t *testing.T) { @@ -1097,6 +1162,7 @@ func TestGetMultipleTypeObjects(t *testing.T) { expected := `NAME AGE pod/foo pod/bar + NAME AGE service/baz ` @@ -1135,6 +1201,7 @@ func TestGetMultipleTypeTableObjects(t *testing.T) { expected := `NAME READY STATUS RESTARTS AGE pod/foo 0/0 0 pod/bar 0/0 0 + NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/baz ClusterIP ` @@ -1279,6 +1346,7 @@ func TestGetMultipleTypeObjectsWithLabelSelector(t *testing.T) { expected := `NAME AGE pod/foo pod/bar + NAME AGE service/baz ` @@ -1322,6 +1390,7 @@ func TestGetMultipleTypeTableObjectsWithLabelSelector(t *testing.T) { expected := `NAME READY STATUS RESTARTS AGE pod/foo 0/0 0 pod/bar 0/0 0 + NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/baz ClusterIP ` @@ -1365,6 +1434,7 @@ func TestGetMultipleTypeObjectsWithFieldSelector(t *testing.T) { expected := `NAME AGE pod/foo pod/bar + NAME AGE service/baz ` @@ -1408,6 +1478,7 @@ func TestGetMultipleTypeTableObjectsWithFieldSelector(t *testing.T) { expected := `NAME READY STATUS RESTARTS AGE pod/foo 0/0 0 pod/bar 0/0 0 + NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/baz ClusterIP ` @@ -1451,6 +1522,7 @@ func TestGetMultipleTypeObjectsWithDirectReference(t *testing.T) { expected := `NAME AGE service/baz + NAME AGE node/foo ` @@ -1494,6 +1566,7 @@ func TestGetMultipleTypeTableObjectsWithDirectReference(t *testing.T) { expected := `NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/baz ClusterIP + NAME STATUS ROLES AGE VERSION node/foo Unknown ` @@ -2590,3 +2663,11 @@ func nodeTableObjBody(codec runtime.Codec, nodes ...corev1.Node) io.ReadCloser { } return cmdtesting.ObjBody(codec, table) } + +// build an empty table response +func emptyTableObjBody(codec runtime.Codec) io.ReadCloser { + table := &metav1.Table{ + ColumnDefinitions: podColumns, + } + return cmdtesting.ObjBody(codec, table) +} diff --git a/pkg/kubectl/cmd/testing/util.go b/pkg/kubectl/cmd/testing/util.go index 076820b0b0b..4e9c969433a 100644 --- a/pkg/kubectl/cmd/testing/util.go +++ b/pkg/kubectl/cmd/testing/util.go @@ -126,6 +126,30 @@ func TestData() (*corev1.PodList, *corev1.ServiceList, *corev1.ReplicationContro return pods, svc, rc } +// EmptyTestData returns no pod, service, or replication controller +func EmptyTestData() (*corev1.PodList, *corev1.ServiceList, *corev1.ReplicationControllerList) { + pods := &corev1.PodList{ + ListMeta: metav1.ListMeta{ + ResourceVersion: "15", + }, + Items: []corev1.Pod{}, + } + svc := &corev1.ServiceList{ + ListMeta: metav1.ListMeta{ + ResourceVersion: "16", + }, + Items: []corev1.Service{}, + } + + rc := &corev1.ReplicationControllerList{ + ListMeta: metav1.ListMeta{ + ResourceVersion: "17", + }, + Items: []corev1.ReplicationController{}, + } + return pods, svc, rc +} + func GenResponseWithJsonEncodedBody(bodyStruct interface{}) (*http.Response, error) { jsonBytes, err := json.Marshal(bodyStruct) if err != nil {