diff --git a/pkg/kubectl/cmd/get/BUILD b/pkg/kubectl/cmd/get/BUILD index 58b82a922d0..c62cf65b3c6 100644 --- a/pkg/kubectl/cmd/get/BUILD +++ b/pkg/kubectl/cmd/get/BUILD @@ -22,6 +22,7 @@ go_library( "get.go", "get_flags.go", "humanreadable_flags.go", + "skip_printer.go", "sorter.go", "table_printer.go", ], @@ -60,6 +61,7 @@ go_library( "//vendor/github.com/spf13/cobra:go_default_library", "//vendor/k8s.io/klog:go_default_library", "//vendor/k8s.io/utils/integer:go_default_library", + "//vendor/k8s.io/utils/pointer:go_default_library", "//vendor/vbom.ml/util/sortorder:go_default_library", ], ) @@ -97,7 +99,6 @@ go_test( "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1beta1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", - "//staging/src/k8s.io/apimachinery/pkg/runtime/serializer/json:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime/serializer/streaming:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/watch:go_default_library", diff --git a/pkg/kubectl/cmd/get/get.go b/pkg/kubectl/cmd/get/get.go index 5f39a5adbd4..d92937a0eff 100644 --- a/pkg/kubectl/cmd/get/get.go +++ b/pkg/kubectl/cmd/get/get.go @@ -49,12 +49,13 @@ import ( "k8s.io/kubernetes/pkg/api/legacyscheme" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" "k8s.io/kubernetes/pkg/kubectl/util/i18n" + utilpointer "k8s.io/utils/pointer" ) // GetOptions contains the input to the get command. type GetOptions struct { PrintFlags *PrintFlags - ToPrinter func(*meta.RESTMapping, bool, bool) (printers.ResourcePrinterFunc, error) + ToPrinter func(*meta.RESTMapping, *bool, bool, bool) (printers.ResourcePrinterFunc, error) IsHumanReadablePrinter bool PrintWithOpenAPICols bool @@ -230,7 +231,7 @@ func (o *GetOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []stri o.IsHumanReadablePrinter = true } - o.ToPrinter = func(mapping *meta.RESTMapping, withNamespace bool, withKind bool) (printers.ResourcePrinterFunc, error) { + o.ToPrinter = func(mapping *meta.RESTMapping, outputObjects *bool, withNamespace bool, withKind bool) (printers.ResourcePrinterFunc, error) { // make a new copy of current flags / opts before mutating printFlags := o.PrintFlags.Copy() @@ -257,6 +258,9 @@ func (o *GetOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []stri if o.Sort { printer = &SortingPrinter{Delegate: printer, SortField: sortBy} } + if outputObjects != nil { + printer = &skipPrinter{delegate: printer, output: outputObjects} + } if o.ServerPrint { printer = &TablePrinter{Delegate: printer} } @@ -539,7 +543,7 @@ func (o *GetOptions) Run(f cmdutil.Factory, cmd *cobra.Command, args []string) e fmt.Fprintln(o.ErrOut) } - printer, err = o.ToPrinter(mapping, printWithNamespace, printWithKind) + printer, err = o.ToPrinter(mapping, nil, printWithNamespace, printWithKind) if err != nil { if !errs.Has(err.Error()) { errs.Insert(err.Error()) @@ -635,7 +639,8 @@ func (o *GetOptions) watch(f cmdutil.Factory, cmd *cobra.Command, args []string) info := infos[0] mapping := info.ResourceMapping() - printer, err := o.ToPrinter(mapping, o.AllNamespaces, false) + outputObjects := utilpointer.BoolPtr(!o.WatchOnly) + printer, err := o.ToPrinter(mapping, outputObjects, o.AllNamespaces, false) if err != nil { return err } @@ -664,25 +669,29 @@ func (o *GetOptions) watch(f cmdutil.Factory, cmd *cobra.Command, args []string) tableGK := metainternal.SchemeGroupVersion.WithKind("Table").GroupKind() // print the current object - if !o.WatchOnly { - var objsToPrint []runtime.Object - - if isList { - objsToPrint, _ = meta.ExtractList(obj) - } else { - objsToPrint = append(objsToPrint, obj) + var objsToPrint []runtime.Object + if isList { + objsToPrint, _ = meta.ExtractList(obj) + } else { + objsToPrint = append(objsToPrint, obj) + } + for _, objToPrint := range objsToPrint { + if o.IsHumanReadablePrinter && objToPrint.GetObjectKind().GroupVersionKind().GroupKind() != tableGK { + // printing anything other than tables always takes the internal version, but the watch event uses externals + internalGV := mapping.GroupVersionKind.GroupKind().WithVersion(runtime.APIVersionInternal).GroupVersion() + objToPrint = attemptToConvertToInternal(objToPrint, legacyscheme.Scheme, internalGV) } - for _, objToPrint := range objsToPrint { - if o.IsHumanReadablePrinter && objToPrint.GetObjectKind().GroupVersionKind().GroupKind() != tableGK { - // printing anything other than tables always takes the internal version, but the watch event uses externals - internalGV := mapping.GroupVersionKind.GroupKind().WithVersion(runtime.APIVersionInternal).GroupVersion() - objToPrint = attemptToConvertToInternal(objToPrint, legacyscheme.Scheme, internalGV) - } - if err := printer.PrintObj(objToPrint, writer); err != nil { - return fmt.Errorf("unable to output the provided object: %v", err) - } + if err := printer.PrintObj(objToPrint, writer); err != nil { + return fmt.Errorf("unable to output the provided object: %v", err) } - writer.Flush() + } + writer.Flush() + if isList { + // we can start outputting objects now, watches started from lists don't emit synthetic added events + *outputObjects = true + } else { + // suppress output, since watches started for individual items emit a synthetic ADDED event first + *outputObjects = false } // print watched changes @@ -691,18 +700,11 @@ func (o *GetOptions) watch(f cmdutil.Factory, cmd *cobra.Command, args []string) return err } - first := true ctx, cancel := context.WithCancel(context.Background()) defer cancel() intr := interrupt.New(nil, cancel) intr.Run(func() error { _, err := watchtools.UntilWithoutRetry(ctx, w, func(e watch.Event) (bool, error) { - if !isList && first { - // drop the initial watch event in the single resource case - first = false - return false, nil - } - // printing always takes the internal version, but the watch event uses externals // TODO fix printing to use server-side or be version agnostic objToPrint := e.Object @@ -714,6 +716,8 @@ func (o *GetOptions) watch(f cmdutil.Factory, cmd *cobra.Command, args []string) return false, err } writer.Flush() + // after processing at least one event, start outputting objects + *outputObjects = true return false, nil }) return err @@ -750,7 +754,7 @@ func (o *GetOptions) printGeneric(r *resource.Result) error { return utilerrors.Reduce(utilerrors.Flatten(utilerrors.NewAggregate(errs))) } - printer, err := o.ToPrinter(nil, false, false) + printer, err := o.ToPrinter(nil, nil, false, false) if err != nil { return err } diff --git a/pkg/kubectl/cmd/get/get_test.go b/pkg/kubectl/cmd/get/get_test.go index 2c2e62dc505..456e8fd307e 100644 --- a/pkg/kubectl/cmd/get/get_test.go +++ b/pkg/kubectl/cmd/get/get_test.go @@ -38,7 +38,6 @@ import ( metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/runtime/serializer/json" "k8s.io/apimachinery/pkg/runtime/serializer/streaming" "k8s.io/apimachinery/pkg/util/diff" "k8s.io/apimachinery/pkg/watch" @@ -244,6 +243,31 @@ foo 0/0 0 } } +func TestGetTableObjects(t *testing.T) { + pods, _, _ := cmdtesting.TestData() + + tf := cmdtesting.NewTestFactory().WithNamespace("test") + defer tf.Cleanup() + codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) + + tf.UnstructuredClient = &fake.RESTClient{ + NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, + Resp: &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: podTableObjBody(codec, pods.Items[0])}, + } + + streams, _, buf, _ := genericclioptions.NewTestIOStreams() + cmd := NewCmdGet("kubectl", tf, streams) + cmd.SetOutput(buf) + cmd.Run(cmd, []string{"pods", "foo"}) + + expected := `NAME READY STATUS RESTARTS AGE +foo 0/0 0 +` + if e, a := expected, buf.String(); e != a { + t.Errorf("expected\n%v\ngot\n%v", e, a) + } +} + func TestGetObjectsShowKind(t *testing.T) { pods, _, _ := cmdtesting.TestData() @@ -270,6 +294,32 @@ pod/foo 0/0 0 } } +func TestGetTableObjectsShowKind(t *testing.T) { + pods, _, _ := cmdtesting.TestData() + + tf := cmdtesting.NewTestFactory().WithNamespace("test") + defer tf.Cleanup() + codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) + + tf.UnstructuredClient = &fake.RESTClient{ + NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, + Resp: &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: podTableObjBody(codec, pods.Items[0])}, + } + + streams, _, buf, _ := genericclioptions.NewTestIOStreams() + cmd := NewCmdGet("kubectl", tf, streams) + cmd.SetOutput(buf) + cmd.Flags().Set("show-kind", "true") + cmd.Run(cmd, []string{"pods", "foo"}) + + expected := `NAME READY STATUS RESTARTS AGE +pod/foo 0/0 0 +` + if e, a := expected, buf.String(); e != a { + t.Errorf("expected\n%v\ngot\n%v", e, a) + } +} + func TestGetMultipleResourceTypesShowKinds(t *testing.T) { pods, svcs, _ := cmdtesting.TestData() @@ -325,6 +375,61 @@ service/baz ClusterIP } } +func TestGetMultipleTableResourceTypesShowKinds(t *testing.T) { + pods, svcs, _ := cmdtesting.TestData() + + 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: podTableObjBody(codec, pods.Items...)}, nil + case p == "/namespaces/test/replicationcontrollers" && m == "GET": + return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &corev1.ReplicationControllerList{})}, nil + case p == "/namespaces/test/services" && m == "GET": + return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: serviceTableObjBody(codec, svcs.Items...)}, nil + case p == "/namespaces/test/statefulsets" && m == "GET": + return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &appsv1.StatefulSetList{})}, nil + case p == "/namespaces/test/horizontalpodautoscalers" && m == "GET": + return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &autoscalingv1.HorizontalPodAutoscalerList{})}, nil + case p == "/namespaces/test/jobs" && m == "GET": + return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &batchv1.JobList{})}, nil + case p == "/namespaces/test/cronjobs" && m == "GET": + return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &batchv1beta1.CronJobList{})}, nil + case p == "/namespaces/test/daemonsets" && m == "GET": + return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &appsv1.DaemonSetList{})}, nil + case p == "/namespaces/test/deployments" && m == "GET": + return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &extensionsv1beta1.DeploymentList{})}, nil + case p == "/namespaces/test/replicasets" && m == "GET": + return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &extensionsv1beta1.ReplicaSetList{})}, nil + + default: + t.Fatalf("request url: %#v,and request: %#v", req.URL, req) + return nil, nil + } + }), + } + + streams, _, buf, _ := genericclioptions.NewTestIOStreams() + cmd := NewCmdGet("kubectl", tf, streams) + cmd.SetOutput(buf) + cmd.Run(cmd, []string{"all"}) + + 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) + } +} + func TestGetObjectsShowLabels(t *testing.T) { pods, _, _ := cmdtesting.TestData() @@ -351,6 +456,32 @@ foo 0/0 0 } } +func TestGetTableObjectsShowLabels(t *testing.T) { + pods, _, _ := cmdtesting.TestData() + + tf := cmdtesting.NewTestFactory().WithNamespace("test") + defer tf.Cleanup() + codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) + + tf.UnstructuredClient = &fake.RESTClient{ + NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, + Resp: &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: podTableObjBody(codec, pods.Items[0])}, + } + + streams, _, buf, _ := genericclioptions.NewTestIOStreams() + cmd := NewCmdGet("kubectl", tf, streams) + cmd.SetOutput(buf) + cmd.Flags().Set("show-labels", "true") + cmd.Run(cmd, []string{"pods", "foo"}) + + expected := `NAME READY STATUS RESTARTS AGE LABELS +foo 0/0 0 +` + if e, a := expected, buf.String(); e != a { + t.Errorf("expected\n%v\ngot\n%v", e, a) + } +} + func TestGetEmptyTable(t *testing.T) { tf := cmdtesting.NewTestFactory().WithNamespace("test") defer tf.Cleanup() @@ -769,6 +900,32 @@ foo 0/0 0 } } +func TestGetTableObjectsIdentifiedByFile(t *testing.T) { + pods, _, _ := cmdtesting.TestData() + + tf := cmdtesting.NewTestFactory().WithNamespace("test") + defer tf.Cleanup() + codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) + + tf.UnstructuredClient = &fake.RESTClient{ + NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, + Resp: &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: podTableObjBody(codec, pods.Items[0])}, + } + + streams, _, buf, _ := genericclioptions.NewTestIOStreams() + cmd := NewCmdGet("kubectl", tf, streams) + cmd.SetOutput(buf) + cmd.Flags().Set("filename", "../../../../test/e2e/testing-manifests/statefulset/cassandra/controller.yaml") + cmd.Run(cmd, []string{}) + + expected := `NAME READY STATUS RESTARTS AGE +foo 0/0 0 +` + if e, a := expected, buf.String(); e != a { + t.Errorf("expected\n%v\ngot\n%v", e, a) + } +} + func TestGetListObjects(t *testing.T) { pods, _, _ := cmdtesting.TestData() @@ -795,6 +952,32 @@ bar 0/0 0 } } +func TestGetListTableObjects(t *testing.T) { + pods, _, _ := cmdtesting.TestData() + + tf := cmdtesting.NewTestFactory().WithNamespace("test") + defer tf.Cleanup() + codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) + + tf.UnstructuredClient = &fake.RESTClient{ + NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, + Resp: &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: podTableObjBody(codec, pods.Items...)}, + } + + streams, _, buf, _ := genericclioptions.NewTestIOStreams() + cmd := NewCmdGet("kubectl", tf, streams) + cmd.SetOutput(buf) + cmd.Run(cmd, []string{"pods"}) + + expected := `NAME READY STATUS RESTARTS AGE +foo 0/0 0 +bar 0/0 0 +` + if e, a := expected, buf.String(); e != a { + t.Errorf("expected\n%v\ngot\n%v", e, a) + } +} + func TestGetListComponentStatus(t *testing.T) { statuses := testComponentStatusData() @@ -922,6 +1105,44 @@ service/baz ClusterIP } } +func TestGetMultipleTypeTableObjects(t *testing.T) { + pods, svc, _ := cmdtesting.TestData() + + 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 req.URL.Path { + case "/namespaces/test/pods": + return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: podTableObjBody(codec, pods.Items...)}, nil + case "/namespaces/test/services": + return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: serviceTableObjBody(codec, svc.Items...)}, nil + default: + t.Fatalf("request url: %#v,and request: %#v", req.URL, req) + return nil, nil + } + }), + } + + streams, _, buf, _ := genericclioptions.NewTestIOStreams() + cmd := NewCmdGet("kubectl", tf, streams) + cmd.SetOutput(buf) + cmd.Run(cmd, []string{"pods,services"}) + + 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) + } +} + func TestGetMultipleTypeObjectsAsList(t *testing.T) { pods, svc, _ := cmdtesting.TestData() @@ -1066,6 +1287,49 @@ service/baz ClusterIP } } +func TestGetMultipleTypeTableObjectsWithLabelSelector(t *testing.T) { + pods, svc, _ := cmdtesting.TestData() + + 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) { + if req.URL.Query().Get(metav1.LabelSelectorQueryParam("v1")) != "a=b" { + t.Fatalf("request url: %#v,and request: %#v", req.URL, req) + } + switch req.URL.Path { + case "/namespaces/test/pods": + return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: podTableObjBody(codec, pods.Items...)}, nil + case "/namespaces/test/services": + return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: serviceTableObjBody(codec, svc.Items...)}, nil + default: + t.Fatalf("request url: %#v,and request: %#v", req.URL, req) + return nil, nil + } + }), + } + + streams, _, buf, _ := genericclioptions.NewTestIOStreams() + cmd := NewCmdGet("kubectl", tf, streams) + cmd.SetOutput(buf) + + cmd.Flags().Set("selector", "a=b") + cmd.Run(cmd, []string{"pods,services"}) + + 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) + } +} + func TestGetMultipleTypeObjectsWithFieldSelector(t *testing.T) { pods, svc, _ := cmdtesting.TestData() @@ -1109,6 +1373,49 @@ service/baz ClusterIP } } +func TestGetMultipleTypeTableObjectsWithFieldSelector(t *testing.T) { + pods, svc, _ := cmdtesting.TestData() + + 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) { + if req.URL.Query().Get(metav1.FieldSelectorQueryParam("v1")) != "a=b" { + t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) + } + switch req.URL.Path { + case "/namespaces/test/pods": + return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: podTableObjBody(codec, pods.Items...)}, nil + case "/namespaces/test/services": + return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: serviceTableObjBody(codec, svc.Items...)}, nil + default: + t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) + return nil, nil + } + }), + } + + streams, _, buf, _ := genericclioptions.NewTestIOStreams() + cmd := NewCmdGet("kubectl", tf, streams) + cmd.SetOutput(buf) + + cmd.Flags().Set("field-selector", "a=b") + cmd.Run(cmd, []string{"pods,services"}) + + 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) + } +} + func TestGetMultipleTypeObjectsWithDirectReference(t *testing.T) { _, svc, _ := cmdtesting.TestData() node := &corev1.Node{ @@ -1152,6 +1459,49 @@ node/foo Unknown } } +func TestGetMultipleTypeTableObjectsWithDirectReference(t *testing.T) { + _, svc, _ := cmdtesting.TestData() + node := &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + } + + 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 req.URL.Path { + case "/nodes/foo": + return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: nodeTableObjBody(codec, *node)}, nil + case "/namespaces/test/services/bar": + return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: serviceTableObjBody(codec, svc.Items[0])}, nil + default: + t.Fatalf("request url: %#v,and request: %#v", req.URL, req) + return nil, nil + } + }), + } + + streams, _, buf, _ := genericclioptions.NewTestIOStreams() + cmd := NewCmdGet("kubectl", tf, streams) + cmd.SetOutput(buf) + + cmd.Run(cmd, []string{"services/bar", "node/foo"}) + + expected := `NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +service/baz ClusterIP +NAME STATUS ROLES AGE VERSION +node/foo Unknown +` + if e, a := expected, buf.String(); e != a { + t.Errorf("expected\n%v\ngot\n%v", e, a) + } +} + func watchTestData() ([]corev1.Pod, []watch.Event) { pods := []corev1.Pod{ { @@ -1309,6 +1659,57 @@ foo 0/0 0 } } +func TestWatchTableLabelSelector(t *testing.T) { + pods, events := watchTestData() + + tf := cmdtesting.NewTestFactory().WithNamespace("test") + defer tf.Cleanup() + codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) + + podList := &corev1.PodList{ + Items: pods, + ListMeta: metav1.ListMeta{ + ResourceVersion: "10", + }, + } + tf.UnstructuredClient = &fake.RESTClient{ + NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, + Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { + if req.URL.Query().Get(metav1.LabelSelectorQueryParam("v1")) != "a=b" { + t.Fatalf("request url: %#v,and request: %#v", req.URL, req) + } + switch req.URL.Path { + case "/namespaces/test/pods": + if req.URL.Query().Get("watch") == "true" { + return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: podTableWatchBody(codec, events[2:])}, nil + } + return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: podTableObjBody(codec, podList.Items...)}, nil + default: + t.Fatalf("request url: %#v,and request: %#v", req.URL, req) + return nil, nil + } + }), + } + + streams, _, buf, _ := genericclioptions.NewTestIOStreams() + cmd := NewCmdGet("kubectl", tf, streams) + cmd.SetOutput(buf) + + cmd.Flags().Set("watch", "true") + cmd.Flags().Set("selector", "a=b") + cmd.Run(cmd, []string{"pods"}) + + expected := `NAME READY STATUS RESTARTS AGE +bar 0/0 0 +foo 0/0 0 +foo 0/0 0 +foo 0/0 0 +` + if e, a := expected, buf.String(); e != a { + t.Errorf("expected\n%v\ngot\n%v", e, a) + } +} + func TestWatchFieldSelector(t *testing.T) { pods, events := watchTestData() @@ -1360,6 +1761,57 @@ foo 0/0 0 } } +func TestWatchTableFieldSelector(t *testing.T) { + pods, events := watchTestData() + + tf := cmdtesting.NewTestFactory().WithNamespace("test") + defer tf.Cleanup() + codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) + + podList := &corev1.PodList{ + Items: pods, + ListMeta: metav1.ListMeta{ + ResourceVersion: "10", + }, + } + tf.UnstructuredClient = &fake.RESTClient{ + NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, + Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { + if req.URL.Query().Get(metav1.FieldSelectorQueryParam("v1")) != "a=b" { + t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) + } + switch req.URL.Path { + case "/namespaces/test/pods": + if req.URL.Query().Get("watch") == "true" { + return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: podTableWatchBody(codec, events[2:])}, nil + } + return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: podTableObjBody(codec, podList.Items...)}, nil + default: + t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) + return nil, nil + } + }), + } + + streams, _, buf, _ := genericclioptions.NewTestIOStreams() + cmd := NewCmdGet("kubectl", tf, streams) + cmd.SetOutput(buf) + + cmd.Flags().Set("watch", "true") + cmd.Flags().Set("field-selector", "a=b") + cmd.Run(cmd, []string{"pods"}) + + expected := `NAME READY STATUS RESTARTS AGE +bar 0/0 0 +foo 0/0 0 +foo 0/0 0 +foo 0/0 0 +` + if e, a := expected, buf.String(); e != a { + t.Errorf("expected\n%v\ngot\n%v", e, a) + } +} + func TestWatchResource(t *testing.T) { pods, events := watchTestData() @@ -1403,6 +1855,49 @@ foo 0/0 0 } } +func TestWatchTableResource(t *testing.T) { + pods, events := watchTestData() + + 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 req.URL.Path { + case "/namespaces/test/pods/foo": + return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: podTableObjBody(codec, pods[1])}, nil + case "/namespaces/test/pods": + if req.URL.Query().Get("watch") == "true" && req.URL.Query().Get("fieldSelector") == "metadata.name=foo" { + return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: podTableWatchBody(codec, events[1:])}, nil + } + t.Fatalf("request url: %#v,and request: %#v", req.URL, req) + return nil, nil + default: + t.Fatalf("request url: %#v,and request: %#v", req.URL, req) + return nil, nil + } + }), + } + + streams, _, buf, _ := genericclioptions.NewTestIOStreams() + cmd := NewCmdGet("kubectl", tf, streams) + cmd.SetOutput(buf) + + cmd.Flags().Set("watch", "true") + cmd.Run(cmd, []string{"pods", "foo"}) + + expected := `NAME READY STATUS RESTARTS AGE +foo 0/0 0 +foo 0/0 0 +foo 0/0 0 +` + if e, a := expected, buf.String(); e != a { + t.Errorf("expected\n%v\ngot\n%v", e, a) + } +} + func TestWatchResourceTable(t *testing.T) { columns := []metav1beta1.TableColumnDefinition{ {Name: "Name", Type: "string", Format: "name", Description: "the name", Priority: 0}, @@ -1596,6 +2091,48 @@ foo 0/0 0 } } +func TestWatchOnlyTableResource(t *testing.T) { + pods, events := watchTestData() + + 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 req.URL.Path { + case "/namespaces/test/pods/foo": + return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: podTableObjBody(codec, pods[1])}, nil + case "/namespaces/test/pods": + if req.URL.Query().Get("watch") == "true" && req.URL.Query().Get("fieldSelector") == "metadata.name=foo" { + return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: podTableWatchBody(codec, events[1:])}, nil + } + t.Fatalf("request url: %#v,and request: %#v", req.URL, req) + return nil, nil + default: + t.Fatalf("request url: %#v,and request: %#v", req.URL, req) + return nil, nil + } + }), + } + + streams, _, buf, _ := genericclioptions.NewTestIOStreams() + cmd := NewCmdGet("kubectl", tf, streams) + cmd.SetOutput(buf) + + cmd.Flags().Set("watch-only", "true") + cmd.Run(cmd, []string{"pods", "foo"}) + + expected := `NAME READY STATUS RESTARTS AGE +foo 0/0 0 +foo 0/0 0 +` + if e, a := expected, buf.String(); e != a { + t.Errorf("expected\n%v\ngot\n%v", e, a) + } +} + func TestWatchOnlyList(t *testing.T) { pods, events := watchTestData() @@ -1641,6 +2178,51 @@ foo 0/0 0 } } +func TestWatchOnlyTableList(t *testing.T) { + pods, events := watchTestData() + + tf := cmdtesting.NewTestFactory().WithNamespace("test") + defer tf.Cleanup() + codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) + + podList := &corev1.PodList{ + Items: pods, + ListMeta: metav1.ListMeta{ + ResourceVersion: "10", + }, + } + tf.UnstructuredClient = &fake.RESTClient{ + NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, + Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { + switch req.URL.Path { + case "/namespaces/test/pods": + if req.URL.Query().Get("watch") == "true" { + return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: podTableWatchBody(codec, events[2:])}, nil + } + return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: podTableObjBody(codec, podList.Items...)}, nil + default: + t.Fatalf("request url: %#v,and request: %#v", req.URL, req) + return nil, nil + } + }), + } + + streams, _, buf, _ := genericclioptions.NewTestIOStreams() + cmd := NewCmdGet("kubectl", tf, streams) + cmd.SetOutput(buf) + + cmd.Flags().Set("watch-only", "true") + cmd.Run(cmd, []string{"pods"}) + + expected := `NAME READY STATUS RESTARTS AGE +foo 0/0 0 +foo 0/0 0 +` + if e, a := expected, buf.String(); e != a { + t.Errorf("expected\n%v\ngot\n%v", e, a) + } +} + func watchBody(codec runtime.Codec, events []watch.Event) io.ReadCloser { buf := bytes.NewBuffer([]byte{}) enc := restclientwatch.NewEncoder(streaming.NewEncoder(buf, codec), codec) @@ -1649,5 +2231,101 @@ func watchBody(codec runtime.Codec, events []watch.Event) io.ReadCloser { panic(err) } } - return json.Framer.NewFrameReader(ioutil.NopCloser(buf)) + return ioutil.NopCloser(buf) +} + +var podColumns = []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name"}, + {Name: "Ready", Type: "string", Format: ""}, + {Name: "Status", Type: "string", Format: ""}, + {Name: "Restarts", Type: "integer", Format: ""}, + {Name: "Age", Type: "string", Format: ""}, + {Name: "IP", Type: "string", Format: "", Priority: 1}, + {Name: "Node", Type: "string", Format: "", Priority: 1}, + {Name: "Nominated Node", Type: "string", Format: "", Priority: 1}, + {Name: "Readiness Gates", Type: "string", Format: "", Priority: 1}, +} + +// build a meta table response from a pod list +func podTableObjBody(codec runtime.Codec, pods ...corev1.Pod) io.ReadCloser { + table := &metav1.Table{ + ColumnDefinitions: podColumns, + } + for i := range pods { + b := bytes.NewBuffer(nil) + codec.Encode(&pods[i], b) + table.Rows = append(table.Rows, metav1.TableRow{ + Object: runtime.RawExtension{Raw: b.Bytes()}, + Cells: []interface{}{pods[i].Name, "0/0", "", int64(0), "", "", "", "", ""}, + }) + } + return cmdtesting.ObjBody(codec, table) +} + +// build meta table watch events from pod watch events +func podTableWatchBody(codec runtime.Codec, events []watch.Event) io.ReadCloser { + tableEvents := []watch.Event{} + for i, e := range events { + b := bytes.NewBuffer(nil) + codec.Encode(e.Object, b) + var columns []metav1.TableColumnDefinition + if i == 0 { + columns = podColumns + } + tableEvents = append(tableEvents, watch.Event{ + Type: e.Type, + Object: &metav1.Table{ + ColumnDefinitions: columns, + Rows: []metav1.TableRow{{ + Object: runtime.RawExtension{Raw: b.Bytes()}, + Cells: []interface{}{e.Object.(*corev1.Pod).Name, "0/0", "", int64(0), "", "", "", "", ""}, + }}}, + }) + } + return watchBody(codec, tableEvents) +} + +// build a meta table response from a service list +func serviceTableObjBody(codec runtime.Codec, services ...corev1.Service) io.ReadCloser { + table := &metav1.Table{ + ColumnDefinitions: []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name"}, + {Name: "Type", Type: "string", Format: ""}, + {Name: "Cluster-IP", Type: "string", Format: ""}, + {Name: "External-IP", Type: "string", Format: ""}, + {Name: "Port(s)", Type: "string", Format: ""}, + {Name: "Age", Type: "string", Format: ""}, + }, + } + for i := range services { + b := bytes.NewBuffer(nil) + codec.Encode(&services[i], b) + table.Rows = append(table.Rows, metav1.TableRow{ + Object: runtime.RawExtension{Raw: b.Bytes()}, + Cells: []interface{}{services[i].Name, "ClusterIP", "", "", "", ""}, + }) + } + return cmdtesting.ObjBody(codec, table) +} + +// build a meta table response from a node list +func nodeTableObjBody(codec runtime.Codec, nodes ...corev1.Node) io.ReadCloser { + table := &metav1.Table{ + ColumnDefinitions: []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name"}, + {Name: "Status", Type: "string", Format: ""}, + {Name: "Roles", Type: "string", Format: ""}, + {Name: "Age", Type: "string", Format: ""}, + {Name: "Version", Type: "string", Format: ""}, + }, + } + for i := range nodes { + b := bytes.NewBuffer(nil) + codec.Encode(&nodes[i], b) + table.Rows = append(table.Rows, metav1.TableRow{ + Object: runtime.RawExtension{Raw: b.Bytes()}, + Cells: []interface{}{nodes[i].Name, "Unknown", "", "", ""}, + }) + } + return cmdtesting.ObjBody(codec, table) } diff --git a/pkg/kubectl/cmd/get/humanreadable_flags_test.go b/pkg/kubectl/cmd/get/humanreadable_flags_test.go index cc547df42ae..ce90d18daac 100644 --- a/pkg/kubectl/cmd/get/humanreadable_flags_test.go +++ b/pkg/kubectl/cmd/get/humanreadable_flags_test.go @@ -18,26 +18,58 @@ package get import ( "bytes" + "fmt" "regexp" "strings" "testing" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/cli-runtime/pkg/genericclioptions" api "k8s.io/kubernetes/pkg/apis/core" ) func TestHumanReadablePrinterSupportsExpectedOptions(t *testing.T) { - testObject := &api.Pod{ObjectMeta: metav1.ObjectMeta{ - Name: "foo", - Labels: map[string]string{ - "l1": "value", + testTable := &metav1.Table{ + ColumnDefinitions: []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name"}, + {Name: "Ready", Type: "string", Format: ""}, + {Name: "Status", Type: "string", Format: ""}, + {Name: "Restarts", Type: "integer", Format: ""}, + {Name: "Age", Type: "string", Format: ""}, + {Name: "IP", Type: "string", Format: "", Priority: 1}, + {Name: "Node", Type: "string", Format: "", Priority: 1}, + {Name: "Nominated Node", Type: "string", Format: "", Priority: 1}, + {Name: "Readiness Gates", Type: "string", Format: "", Priority: 1}, }, - }} + Rows: []metav1.TableRow{{ + Object: runtime.RawExtension{ + Object: &corev1.Pod{ + TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Pod"}, + ObjectMeta: metav1.ObjectMeta{Name: "foo", Labels: map[string]string{"l1": "value"}}, + }, + }, + Cells: []interface{}{"foo", "0/0", "", int64(0), "", "", "", "", ""}, + }}, + } + testPod := &api.Pod{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Pod", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Labels: map[string]string{ + "l1": "value", + }, + }, + } testCases := []struct { name string + testObject runtime.Object showKind bool showLabels bool @@ -56,53 +88,104 @@ func TestHumanReadablePrinterSupportsExpectedOptions(t *testing.T) { }{ { name: "empty output format matches a humanreadable printer", + testObject: testPod.DeepCopy(), + expectedOutput: "NAME\\ +READY\\ +STATUS\\ +RESTARTS\\ +AGE\nfoo\\ +0/0\\ +0\\ +\n", + }, + { + name: "empty output format matches a humanreadable printer", + testObject: testTable.DeepCopy(), expectedOutput: "NAME\\ +READY\\ +STATUS\\ +RESTARTS\\ +AGE\nfoo\\ +0/0\\ +0\\ +\n", }, { name: "\"wide\" output format prints", + testObject: testPod.DeepCopy(), + outputFormat: "wide", + expectedOutput: "NAME\\ +READY\\ +STATUS\\ +RESTARTS\\ +AGE\\ +IP\\ +NODE\\ +NOMINATED NODE\\ +READINESS GATES\nfoo\\ +0/0\\ +0\\ +\\ +\\ +\\ +\\ +\n", + }, + { + name: "\"wide\" output format prints", + testObject: testTable.DeepCopy(), outputFormat: "wide", expectedOutput: "NAME\\ +READY\\ +STATUS\\ +RESTARTS\\ +AGE\\ +IP\\ +NODE\\ +NOMINATED NODE\\ +READINESS GATES\nfoo\\ +0/0\\ +0\\ +\\ +\\ +\\ +\\ +\n", }, { name: "no-headers prints output with no headers", + testObject: testPod.DeepCopy(), + noHeaders: true, + expectedOutput: "foo\\ +0/0\\ +0\\ +\n", + }, + { + name: "no-headers prints output with no headers", + testObject: testTable.DeepCopy(), noHeaders: true, expectedOutput: "foo\\ +0/0\\ +0\\ +\n", }, { name: "no-headers and a \"wide\" output format prints output with no headers and additional columns", + testObject: testPod.DeepCopy(), + outputFormat: "wide", + noHeaders: true, + expectedOutput: "foo\\ +0/0\\ +0\\ +\\ +\\ +\\ +\\ +\n", + }, + { + name: "no-headers and a \"wide\" output format prints output with no headers and additional columns", + testObject: testTable.DeepCopy(), outputFormat: "wide", noHeaders: true, expectedOutput: "foo\\ +0/0\\ +0\\ +\\ +\\ +\\ +\\ +\n", }, { name: "show-kind displays the resource's kind, even when printing a single type of resource", + testObject: testPod.DeepCopy(), + showKind: true, + expectedOutput: "NAME\\ +READY\\ +STATUS\\ +RESTARTS\\ +AGE\npod/foo\\ +0/0\\ +0\\ +\n", + }, + { + name: "show-kind displays the resource's kind, even when printing a single type of resource", + testObject: testTable.DeepCopy(), showKind: true, expectedOutput: "NAME\\ +READY\\ +STATUS\\ +RESTARTS\\ +AGE\npod/foo\\ +0/0\\ +0\\ +\n", }, { name: "label-columns prints specified label values in new column", + testObject: testPod.DeepCopy(), + columnLabels: []string{"l1"}, + expectedOutput: "NAME\\ +READY\\ +STATUS\\ +RESTARTS\\ +AGE\\ +L1\nfoo\\ +0/0\\ +0\\ +\\ +value\n", + }, + { + name: "label-columns prints specified label values in new column", + testObject: testTable.DeepCopy(), columnLabels: []string{"l1"}, expectedOutput: "NAME\\ +READY\\ +STATUS\\ +RESTARTS\\ +AGE\\ +L1\nfoo\\ +0/0\\ +0\\ +\\ +value\n", }, { name: "withNamespace displays an additional NAMESPACE column", + testObject: testPod.DeepCopy(), + withNamespace: true, + expectedOutput: "NAMESPACE\\ +NAME\\ +READY\\ +STATUS\\ +RESTARTS\\ +AGE\n\\ +foo\\ +0/0\\ +0\\ +\n", + }, + { + name: "withNamespace displays an additional NAMESPACE column", + testObject: testTable.DeepCopy(), withNamespace: true, expectedOutput: "NAMESPACE\\ +NAME\\ +READY\\ +STATUS\\ +RESTARTS\\ +AGE\n\\ +foo\\ +0/0\\ +0\\ +\n", }, { name: "no printer is matched on an invalid outputFormat", + testObject: testPod.DeepCopy(), outputFormat: "invalid", expectNoMatch: true, }, { name: "printer should not match on any other format supported by another printer", + testObject: testPod.DeepCopy(), outputFormat: "go-template", expectNoMatch: true, }, } for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { + t.Run(fmt.Sprintf("%s %T", tc.name, tc.testObject), func(t *testing.T) { printFlags := HumanPrintFlags{ ShowKind: &tc.showKind, ShowLabels: &tc.showLabels, @@ -139,7 +222,7 @@ func TestHumanReadablePrinterSupportsExpectedOptions(t *testing.T) { } out := bytes.NewBuffer([]byte{}) - err = p.PrintObj(testObject, out) + err = p.PrintObj(tc.testObject, out) if err != nil { t.Errorf("unexpected error: %v", err) } @@ -149,7 +232,7 @@ func TestHumanReadablePrinterSupportsExpectedOptions(t *testing.T) { t.Errorf("unexpected error: %v", err) } if !match { - t.Errorf("unexpected output: expecting %q, got %q", tc.expectedOutput, out.String()) + t.Errorf("unexpected output: expecting\n%s\ngot\n%s", tc.expectedOutput, out.String()) } }) } diff --git a/pkg/kubectl/cmd/get/skip_printer.go b/pkg/kubectl/cmd/get/skip_printer.go new file mode 100644 index 00000000000..f02883cb1da --- /dev/null +++ b/pkg/kubectl/cmd/get/skip_printer.go @@ -0,0 +1,48 @@ +/* +Copyright 2019 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 get + +import ( + "io" + + metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/cli-runtime/pkg/printers" +) + +// skipPrinter allows conditionally suppressing object output via the output field. +// table objects are suppressed by setting their Rows to nil (allowing column definitions to propagate to the delegate). +// non-table objects are suppressed by not calling the delegate at all. +type skipPrinter struct { + delegate printers.ResourcePrinter + output *bool +} + +func (p *skipPrinter) PrintObj(obj runtime.Object, writer io.Writer) error { + if *p.output { + return p.delegate.PrintObj(obj, writer) + } + + table, isTable := obj.(*metav1beta1.Table) + if !isTable { + return nil + } + + table = table.DeepCopy() + table.Rows = nil + return p.delegate.PrintObj(table, writer) +} diff --git a/pkg/printers/tablegenerator.go b/pkg/printers/tablegenerator.go index a9d62c45a61..d6bf97caf70 100644 --- a/pkg/printers/tablegenerator.go +++ b/pkg/printers/tablegenerator.go @@ -48,10 +48,11 @@ type handlerEntry struct { // will only be printed if the object type changes. This makes it useful for printing items // received from watches. type HumanReadablePrinter struct { - handlerMap map[reflect.Type]*handlerEntry - options PrintOptions - lastType interface{} - lastColumns []metav1beta1.TableColumnDefinition + handlerMap map[reflect.Type]*handlerEntry + options PrintOptions + lastType interface{} + lastColumns []metav1beta1.TableColumnDefinition + printedHeaders bool } var _ TableGenerator = &HumanReadablePrinter{} diff --git a/pkg/printers/tableprinter.go b/pkg/printers/tableprinter.go index eeea57762ba..2e0ec209193 100644 --- a/pkg/printers/tableprinter.go +++ b/pkg/printers/tableprinter.go @@ -80,18 +80,22 @@ func (h *HumanReadablePrinter) PrintObj(obj runtime.Object, output io.Writer) er if table, ok := obj.(*metav1beta1.Table); ok { // Do not print headers if this table has no column definitions, or they are the same as the last ones we printed localOptions := h.options - if len(table.ColumnDefinitions) == 0 || reflect.DeepEqual(table.ColumnDefinitions, h.lastColumns) { + if h.printedHeaders && (len(table.ColumnDefinitions) == 0 || reflect.DeepEqual(table.ColumnDefinitions, h.lastColumns)) { localOptions.NoHeaders = true } if len(table.ColumnDefinitions) == 0 { // If this table has no column definitions, use the columns from the last table we printed for decoration and layout. // This is done when receiving tables in watch events to save bandwidth. - localOptions.NoHeaders = true table.ColumnDefinitions = h.lastColumns - } else { + } else if !reflect.DeepEqual(table.ColumnDefinitions, h.lastColumns) { // If this table has column definitions, remember them for future use. h.lastColumns = table.ColumnDefinitions + h.printedHeaders = false + } + + if len(table.Rows) > 0 { + h.printedHeaders = true } if err := decorateTable(table, localOptions); err != nil {