Unverified Commit c082ace1 authored by Kubernetes Prow Robot's avatar Kubernetes Prow Robot Committed by GitHub

Merge pull request #76161 from liggitt/kubectl-watch-table

use server-side printing in `kubectl get -w`
parents 8f8d9e2c 53e55d31
......@@ -23,6 +23,7 @@ go_library(
"get_flags.go",
"humanreadable_flags.go",
"sorter.go",
"table_printer.go",
],
importpath = "k8s.io/kubernetes/pkg/kubectl/cmd/get",
visibility = ["//visibility:public"],
......@@ -40,6 +41,7 @@ go_library(
"//staging/src/k8s.io/api/core/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/api/meta:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/internalversion:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1beta1:go_default_library",
......
......@@ -351,6 +351,44 @@ foo 0/0 0 <unknown> <none>
}
}
func TestGetEmptyTable(t *testing.T) {
tf := cmdtesting.NewTestFactory().WithNamespace("test")
defer tf.Cleanup()
emptyTable := ioutil.NopCloser(bytes.NewBufferString(`{
"kind":"Table",
"apiVersion":"meta.k8s.io/v1beta1",
"metadata":{
"selfLink":"/api/v1/namespaces/default/pods",
"resourceVersion":"346"
},
"columnDefinitions":[
{"name":"Name","type":"string","format":"name","description":"the name","priority":0}
],
"rows":[]
}`))
tf.UnstructuredClient = &fake.RESTClient{
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
Resp: &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: emptyTable},
}
streams, _, buf, errbuf := genericclioptions.NewTestIOStreams()
cmd := NewCmdGet("kubectl", tf, streams)
cmd.SetOutput(buf)
cmd.Run(cmd, []string{"pods"})
expected := ``
if e, a := expected, buf.String(); e != a {
t.Errorf("expected\n%v\ngot\n%v", e, a)
}
expectedErr := `No resources found.
`
if e, a := expectedErr, errbuf.String(); e != a {
t.Errorf("expectedErr\n%v\ngot\n%v", e, a)
}
}
func TestGetObjectIgnoreNotFound(t *testing.T) {
cmdtesting.InitTestErrorHandler(t)
......@@ -464,9 +502,49 @@ c 0/0 0 <unknown>
}
}
func TestGetSortedObjectsUnstructuredTable(t *testing.T) {
unstructuredMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(sortTestTableData()[0])
if err != nil {
t.Fatal(err)
}
unstructuredBytes, err := encjson.MarshalIndent(unstructuredMap, "", " ")
if err != nil {
t.Fatal(err)
}
// t.Log(string(unstructuredBytes))
body := ioutil.NopCloser(bytes.NewReader(unstructuredBytes))
tf := cmdtesting.NewTestFactory().WithNamespace("test")
defer tf.Cleanup()
tf.UnstructuredClient = &fake.RESTClient{
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
Resp: &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: body},
}
tf.ClientConfigVal = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: &corev1.SchemeGroupVersion}}
streams, _, buf, _ := genericclioptions.NewTestIOStreams()
cmd := NewCmdGet("kubectl", tf, streams)
cmd.SetOutput(buf)
// sorting with metedata.name
cmd.Flags().Set("sort-by", ".metadata.name")
cmd.Run(cmd, []string{"pods"})
expected := `NAME CUSTOM
a custom-a
b custom-b
c custom-c
`
if e, a := expected, buf.String(); e != a {
t.Errorf("expected\n%v\ngot\n%v", e, a)
}
}
func sortTestData() []runtime.Object {
return []runtime.Object{
&corev1.Pod{
TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Pod"},
ObjectMeta: metav1.ObjectMeta{Name: "c", Namespace: "test", ResourceVersion: "10"},
Spec: corev1.PodSpec{
RestartPolicy: corev1.RestartPolicyAlways,
......@@ -477,6 +555,7 @@ func sortTestData() []runtime.Object {
},
},
&corev1.Pod{
TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Pod"},
ObjectMeta: metav1.ObjectMeta{Name: "b", Namespace: "test", ResourceVersion: "11"},
Spec: corev1.PodSpec{
RestartPolicy: corev1.RestartPolicyAlways,
......@@ -487,6 +566,7 @@ func sortTestData() []runtime.Object {
},
},
&corev1.Pod{
TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Pod"},
ObjectMeta: metav1.ObjectMeta{Name: "a", Namespace: "test", ResourceVersion: "9"},
Spec: corev1.PodSpec{
RestartPolicy: corev1.RestartPolicyAlways,
......@@ -502,11 +582,17 @@ func sortTestData() []runtime.Object {
func sortTestTableData() []runtime.Object {
return []runtime.Object{
&metav1beta1.Table{
TypeMeta: metav1.TypeMeta{Kind: "Table"},
TypeMeta: metav1.TypeMeta{APIVersion: "meta.k8s.io/v1beta1", Kind: "Table"},
ColumnDefinitions: []metav1beta1.TableColumnDefinition{
{Name: "NAME", Type: "string", Format: "name"},
{Name: "CUSTOM", Type: "string", Format: ""},
},
Rows: []metav1beta1.TableRow{
{
Cells: []interface{}{"c", "custom-c"},
Object: runtime.RawExtension{
Object: &corev1.Pod{
TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Pod"},
ObjectMeta: metav1.ObjectMeta{Name: "c", Namespace: "test", ResourceVersion: "10"},
Spec: corev1.PodSpec{
RestartPolicy: corev1.RestartPolicyAlways,
......@@ -519,8 +605,10 @@ func sortTestTableData() []runtime.Object {
},
},
{
Cells: []interface{}{"b", "custom-b"},
Object: runtime.RawExtension{
Object: &corev1.Pod{
TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Pod"},
ObjectMeta: metav1.ObjectMeta{Name: "b", Namespace: "test", ResourceVersion: "11"},
Spec: corev1.PodSpec{
RestartPolicy: corev1.RestartPolicyAlways,
......@@ -533,8 +621,10 @@ func sortTestTableData() []runtime.Object {
},
},
{
Cells: []interface{}{"a", "custom-a"},
Object: runtime.RawExtension{
Object: &corev1.Pod{
TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Pod"},
ObjectMeta: metav1.ObjectMeta{Name: "a", Namespace: "test", ResourceVersion: "9"},
Spec: corev1.PodSpec{
RestartPolicy: corev1.RestartPolicyAlways,
......@@ -1313,6 +1403,113 @@ foo 0/0 0 <unknown>
}
}
func TestWatchResourceTable(t *testing.T) {
columns := []metav1beta1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name", Description: "the name", Priority: 0},
{Name: "Active", Type: "boolean", Description: "active", Priority: 0},
}
listTable := &metav1beta1.Table{
TypeMeta: metav1.TypeMeta{APIVersion: "meta.k8s.io/v1beta1", Kind: "Table"},
ColumnDefinitions: columns,
Rows: []metav1beta1.TableRow{
{
Cells: []interface{}{"a", true},
Object: runtime.RawExtension{
Object: &corev1.Pod{
TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Pod"},
ObjectMeta: metav1.ObjectMeta{Name: "a", Namespace: "test", ResourceVersion: "10"},
},
},
},
{
Cells: []interface{}{"b", true},
Object: runtime.RawExtension{
Object: &corev1.Pod{
TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Pod"},
ObjectMeta: metav1.ObjectMeta{Name: "b", Namespace: "test", ResourceVersion: "20"},
},
},
},
},
}
events := []watch.Event{
{
Type: watch.Added,
Object: &metav1beta1.Table{
TypeMeta: metav1.TypeMeta{APIVersion: "meta.k8s.io/v1beta1", Kind: "Table"},
ColumnDefinitions: columns, // first event includes the columns
Rows: []metav1beta1.TableRow{{
Cells: []interface{}{"a", false},
Object: runtime.RawExtension{
Object: &corev1.Pod{
TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Pod"},
ObjectMeta: metav1.ObjectMeta{Name: "a", Namespace: "test", ResourceVersion: "30"},
},
},
}},
},
},
{
Type: watch.Deleted,
Object: &metav1beta1.Table{
ColumnDefinitions: []metav1beta1.TableColumnDefinition{},
Rows: []metav1beta1.TableRow{{
Cells: []interface{}{"b", false},
Object: runtime.RawExtension{
Object: &corev1.Pod{
TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Pod"},
ObjectMeta: metav1.ObjectMeta{Name: "b", Namespace: "test", ResourceVersion: "40"},
},
},
}},
},
},
}
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":
if req.URL.Query().Get("watch") != "true" && req.URL.Query().Get("fieldSelector") == "" {
return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, listTable)}, nil
}
if req.URL.Query().Get("watch") == "true" && req.URL.Query().Get("fieldSelector") == "" {
return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: watchBody(codec, events)}, 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"})
expected := `NAME ACTIVE
a true
b true
a false
b false
`
if e, a := expected, buf.String(); e != a {
t.Errorf("expected\n%v\ngot\n%v", e, a)
}
}
func TestWatchResourceIdentifiedByFile(t *testing.T) {
pods, events := watchTestData()
......@@ -1448,7 +1645,9 @@ func watchBody(codec runtime.Codec, events []watch.Event) io.ReadCloser {
buf := bytes.NewBuffer([]byte{})
enc := restclientwatch.NewEncoder(streaming.NewEncoder(buf, codec), codec)
for i := range events {
enc.Encode(&events[i])
if err := enc.Encode(&events[i]); err != nil {
panic(err)
}
}
return json.Framer.NewFrameReader(ioutil.NopCloser(buf))
}
......@@ -46,13 +46,27 @@ type SortingPrinter struct {
}
func (s *SortingPrinter) PrintObj(obj runtime.Object, out io.Writer) error {
if !meta.IsListType(obj) {
return s.Delegate.PrintObj(obj, out)
if table, isTable := obj.(*metav1beta1.Table); isTable && len(table.Rows) > 1 {
parsedField, err := RelaxedJSONPathExpression(s.SortField)
if err != nil {
parsedField = s.SortField
}
if sorter, err := NewTableSorter(table, parsedField); err != nil {
return err
} else if err := sorter.Sort(); err != nil {
return err
}
return s.Delegate.PrintObj(table, out)
}
if err := s.sortObj(obj); err != nil {
return err
if meta.IsListType(obj) {
if err := s.sortObj(obj); err != nil {
return err
}
return s.Delegate.PrintObj(obj, out)
}
return s.Delegate.PrintObj(obj, out)
}
......
/*
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 (
"fmt"
"io"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/cli-runtime/pkg/printers"
"k8s.io/klog"
)
// TablePrinter decodes table objects into typed objects before delegating to another printer.
// Non-table types are simply passed through
type TablePrinter struct {
Delegate printers.ResourcePrinter
}
func (t *TablePrinter) PrintObj(obj runtime.Object, writer io.Writer) error {
table, err := decodeIntoTable(obj)
if err == nil {
return t.Delegate.PrintObj(table, writer)
}
// if we are unable to decode server response into a v1beta1.Table,
// fallback to client-side printing with whatever info the server returned.
klog.V(2).Infof("Unable to decode server response into a Table. Falling back to hardcoded types: %v", err)
return t.Delegate.PrintObj(obj, writer)
}
func decodeIntoTable(obj runtime.Object) (runtime.Object, error) {
if obj.GetObjectKind().GroupVersionKind().Group != metav1beta1.GroupName {
return nil, fmt.Errorf("attempt to decode non-Table object into a v1beta1.Table")
}
if obj.GetObjectKind().GroupVersionKind().Kind != "Table" {
return nil, fmt.Errorf("attempt to decode non-Table object into a v1beta1.Table")
}
unstr, ok := obj.(*unstructured.Unstructured)
if !ok {
return nil, fmt.Errorf("attempt to decode non-Unstructured object")
}
table := &metav1beta1.Table{}
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(unstr.Object, table); err != nil {
return nil, err
}
for i := range table.Rows {
row := &table.Rows[i]
if row.Object.Raw == nil || row.Object.Object != nil {
continue
}
converted, err := runtime.Decode(unstructured.UnstructuredJSONScheme, row.Object.Raw)
if err != nil {
return nil, err
}
row.Object.Object = converted
}
return table, nil
}
......@@ -38,6 +38,7 @@ go_library(
"//staging/src/k8s.io/api/storage/v1beta1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
"//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:go_default_library",
......
......@@ -45,6 +45,7 @@ import (
storagev1 "k8s.io/api/storage/v1"
storagev1beta1 "k8s.io/api/storage/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
"k8s.io/apimachinery/pkg/runtime/schema"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/client-go/kubernetes/scheme"
......@@ -56,6 +57,7 @@ import (
func init() {
// Register external types for Scheme
metav1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"})
utilruntime.Must(metav1beta1.AddToScheme(Scheme))
utilruntime.Must(scheme.AddToScheme(Scheme))
utilruntime.Must(Scheme.SetVersionPriority(corev1.SchemeGroupVersion))
......
......@@ -63,6 +63,7 @@ type HumanReadablePrinter struct {
defaultHandler *handlerEntry
options PrintOptions
lastType interface{}
lastColumns []metav1beta1.TableColumnDefinition
skipTabWriter bool
encoder runtime.Encoder
decoder runtime.Decoder
......@@ -289,10 +290,26 @@ func (h *HumanReadablePrinter) PrintObj(obj runtime.Object, output io.Writer) er
// display tables following the rules of options
if table, ok := obj.(*metav1beta1.Table); ok {
if err := DecorateTable(table, h.options); err != nil {
// 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) {
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 {
// If this table has column definitions, remember them for future use.
h.lastColumns = table.ColumnDefinitions
}
if err := DecorateTable(table, localOptions); err != nil {
return err
}
return PrintTable(table, output, h.options)
return PrintTable(table, output, localOptions)
}
// check if the object is unstructured. If so, let's attempt to convert it to a type we can understand before
......
......@@ -39,6 +39,12 @@ var scheme = runtime.NewScheme()
var ParameterCodec = runtime.NewParameterCodec(scheme)
func init() {
if err := AddToScheme(scheme); err != nil {
panic(err)
}
}
func AddToScheme(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion,
&Table{},
&TableOptions{},
......@@ -46,11 +52,9 @@ func init() {
&PartialObjectMetadataList{},
)
if err := scheme.AddConversionFuncs(
return scheme.AddConversionFuncs(
Convert_Slice_string_To_v1beta1_IncludeObjectPolicy,
); err != nil {
panic(err)
}
)
// register manually. This usually goes through the SchemeBuilder, which we cannot use here.
//scheme.AddGeneratedDeepCopyFuncs(GetGeneratedDeepCopyFuncs()...)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment