Commit d2a62fd4 authored by Clayton Coleman's avatar Clayton Coleman

Table printers and server generation should always copy ListMeta

Tables should be a mapping from lists, so if the incoming object has these add them to the table. Allows paging over server side tables. Add tests on the generic creater and on the resttest compatibility.
parent 52492e68
...@@ -525,6 +525,16 @@ func (h *HumanReadablePrinter) PrintTable(obj runtime.Object, options PrintOptio ...@@ -525,6 +525,16 @@ func (h *HumanReadablePrinter) PrintTable(obj runtime.Object, options PrintOptio
ColumnDefinitions: columns, ColumnDefinitions: columns,
Rows: results[0].Interface().([]metav1alpha1.TableRow), Rows: results[0].Interface().([]metav1alpha1.TableRow),
} }
if m, err := meta.ListAccessor(obj); err == nil {
table.ResourceVersion = m.GetResourceVersion()
table.SelfLink = m.GetSelfLink()
table.Continue = m.GetContinue()
} else {
if m, err := meta.CommonAccessor(obj); err == nil {
table.ResourceVersion = m.GetResourceVersion()
table.SelfLink = m.GetSelfLink()
}
}
if err := DecorateTable(table, options); err != nil { if err := DecorateTable(table, options); err != nil {
return nil, err return nil, err
} }
......
...@@ -89,7 +89,7 @@ func ListAccessor(obj interface{}) (List, error) { ...@@ -89,7 +89,7 @@ func ListAccessor(obj interface{}) (List, error) {
} }
return nil, errNotList return nil, errNotList
default: default:
panic(fmt.Errorf("%T does not implement the List interface", obj)) return nil, errNotList
} }
} }
......
...@@ -441,6 +441,10 @@ func (storage *SimpleRESTStorage) ConvertToTable(ctx request.Context, obj runtim ...@@ -441,6 +441,10 @@ func (storage *SimpleRESTStorage) ConvertToTable(ctx request.Context, obj runtim
func (storage *SimpleRESTStorage) List(ctx request.Context, options *metainternalversion.ListOptions) (runtime.Object, error) { func (storage *SimpleRESTStorage) List(ctx request.Context, options *metainternalversion.ListOptions) (runtime.Object, error) {
storage.checkContext(ctx) storage.checkContext(ctx)
result := &genericapitesting.SimpleList{ result := &genericapitesting.SimpleList{
ListMeta: metav1.ListMeta{
ResourceVersion: "10",
SelfLink: "/test/link",
},
Items: storage.list, Items: storage.list,
} }
storage.requestedLabelSelector = labels.Everything() storage.requestedLabelSelector = labels.Everything()
...@@ -1832,24 +1836,10 @@ func TestGetPretty(t *testing.T) { ...@@ -1832,24 +1836,10 @@ func TestGetPretty(t *testing.T) {
func TestGetTable(t *testing.T) { func TestGetTable(t *testing.T) {
now := metav1.Now() now := metav1.Now()
storage := map[string]rest.Storage{}
obj := genericapitesting.Simple{ obj := genericapitesting.Simple{
ObjectMeta: metav1.ObjectMeta{Name: "foo1", Namespace: "ns1", CreationTimestamp: now, UID: types.UID("abcdef0123")}, ObjectMeta: metav1.ObjectMeta{Name: "foo1", Namespace: "ns1", ResourceVersion: "10", SelfLink: "/blah", CreationTimestamp: now, UID: types.UID("abcdef0123")},
Other: "foo", Other: "foo",
} }
simpleStorage := SimpleRESTStorage{
item: obj,
}
selfLinker := &setTestSelfLinker{
t: t,
expectedSet: "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + "/namespaces/default/simple/id",
name: "id",
namespace: "default",
}
storage["simple"] = &simpleStorage
handler := handleLinker(storage, selfLinker)
server := httptest.NewServer(handler)
defer server.Close()
m, err := meta.Accessor(&obj) m, err := meta.Accessor(&obj)
if err != nil { if err != nil {
...@@ -1872,15 +1862,34 @@ func TestGetTable(t *testing.T) { ...@@ -1872,15 +1862,34 @@ func TestGetTable(t *testing.T) {
pretty bool pretty bool
expected *metav1alpha1.Table expected *metav1alpha1.Table
statusCode int statusCode int
item bool
}{ }{
{ {
accept: runtime.ContentTypeJSON + ";as=Table;v=v1;g=meta.k8s.io", accept: runtime.ContentTypeJSON + ";as=Table;v=v1;g=meta.k8s.io",
statusCode: http.StatusNotAcceptable, statusCode: http.StatusNotAcceptable,
}, },
{ {
item: true,
accept: runtime.ContentTypeJSON + ";as=Table;v=v1alpha1;g=meta.k8s.io",
expected: &metav1alpha1.Table{
TypeMeta: metav1.TypeMeta{Kind: "Table", APIVersion: "meta.k8s.io/v1alpha1"},
ListMeta: metav1.ListMeta{ResourceVersion: "10", SelfLink: "/blah"},
ColumnDefinitions: []metav1alpha1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name", Description: metaDoc["name"]},
{Name: "Created At", Type: "date", Description: metaDoc["creationTimestamp"]},
},
Rows: []metav1alpha1.TableRow{
{Cells: []interface{}{"foo1", now.Time.UTC().Format(time.RFC3339)}, Object: runtime.RawExtension{Raw: encodedBody}},
},
},
},
{
item: true,
accept: runtime.ContentTypeJSON + ";as=Table;v=v1alpha1;g=meta.k8s.io", accept: runtime.ContentTypeJSON + ";as=Table;v=v1alpha1;g=meta.k8s.io",
params: url.Values{"includeObject": []string{"Metadata"}},
expected: &metav1alpha1.Table{ expected: &metav1alpha1.Table{
TypeMeta: metav1.TypeMeta{Kind: "Table", APIVersion: "meta.k8s.io/v1alpha1"}, TypeMeta: metav1.TypeMeta{Kind: "Table", APIVersion: "meta.k8s.io/v1alpha1"},
ListMeta: metav1.ListMeta{ResourceVersion: "10", SelfLink: "/blah"},
ColumnDefinitions: []metav1alpha1.TableColumnDefinition{ ColumnDefinitions: []metav1alpha1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name", Description: metaDoc["name"]}, {Name: "Name", Type: "string", Format: "name", Description: metaDoc["name"]},
{Name: "Created At", Type: "date", Description: metaDoc["creationTimestamp"]}, {Name: "Created At", Type: "date", Description: metaDoc["creationTimestamp"]},
...@@ -1895,6 +1904,7 @@ func TestGetTable(t *testing.T) { ...@@ -1895,6 +1904,7 @@ func TestGetTable(t *testing.T) {
params: url.Values{"includeObject": []string{"Metadata"}}, params: url.Values{"includeObject": []string{"Metadata"}},
expected: &metav1alpha1.Table{ expected: &metav1alpha1.Table{
TypeMeta: metav1.TypeMeta{Kind: "Table", APIVersion: "meta.k8s.io/v1alpha1"}, TypeMeta: metav1.TypeMeta{Kind: "Table", APIVersion: "meta.k8s.io/v1alpha1"},
ListMeta: metav1.ListMeta{ResourceVersion: "10", SelfLink: "/test/link"},
ColumnDefinitions: []metav1alpha1.TableColumnDefinition{ ColumnDefinitions: []metav1alpha1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name", Description: metaDoc["name"]}, {Name: "Name", Type: "string", Format: "name", Description: metaDoc["name"]},
{Name: "Created At", Type: "date", Description: metaDoc["creationTimestamp"]}, {Name: "Created At", Type: "date", Description: metaDoc["creationTimestamp"]},
...@@ -1906,45 +1916,69 @@ func TestGetTable(t *testing.T) { ...@@ -1906,45 +1916,69 @@ func TestGetTable(t *testing.T) {
}, },
} }
for i, test := range tests { for i, test := range tests {
u, err := url.Parse(server.URL + "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + "/namespaces/default/simple/id") t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
if err != nil { storage := map[string]rest.Storage{}
t.Fatal(err) simpleStorage := SimpleRESTStorage{
} item: obj,
u.RawQuery = test.params.Encode() list: []genericapitesting.Simple{obj},
req := &http.Request{Method: "GET", URL: u} }
req.Header = http.Header{} selfLinker := &setTestSelfLinker{
req.Header.Set("Accept", test.accept) t: t,
resp, err := http.DefaultClient.Do(req) expectedSet: "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + "/namespaces/default/simple",
if err != nil { namespace: "default",
t.Fatal(err) }
} if test.item {
if test.statusCode != 0 { selfLinker.expectedSet += "/id"
if resp.StatusCode != test.statusCode { selfLinker.name = "id"
}
storage["simple"] = &simpleStorage
handler := handleLinker(storage, selfLinker)
server := httptest.NewServer(handler)
defer server.Close()
var id string
if test.item {
id = "/id"
}
u, err := url.Parse(server.URL + "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + "/namespaces/default/simple" + id)
if err != nil {
t.Fatal(err)
}
u.RawQuery = test.params.Encode()
req := &http.Request{Method: "GET", URL: u}
req.Header = http.Header{}
req.Header.Set("Accept", test.accept)
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
if test.statusCode != 0 {
if resp.StatusCode != test.statusCode {
t.Errorf("%d: unexpected response: %#v", i, resp)
}
obj, _, err := extractBodyObject(resp, unstructured.UnstructuredJSONScheme)
if err != nil {
t.Fatalf("%d: unexpected body read error: %v", err)
}
gvk := schema.GroupVersionKind{Version: "v1", Kind: "Status"}
if obj.GetObjectKind().GroupVersionKind() != gvk {
t.Fatalf("%d: unexpected error body: %#v", obj)
}
return
}
if resp.StatusCode != http.StatusOK {
t.Errorf("%d: unexpected response: %#v", i, resp) t.Errorf("%d: unexpected response: %#v", i, resp)
} }
obj, _, err := extractBodyObject(resp, unstructured.UnstructuredJSONScheme) var itemOut metav1alpha1.Table
body, err := extractBody(resp, &itemOut)
if err != nil { if err != nil {
t.Errorf("%d: unexpected body read error: %v", err) t.Fatal(err)
continue
} }
gvk := schema.GroupVersionKind{Version: "v1", Kind: "Status"} if !reflect.DeepEqual(test.expected, &itemOut) {
if obj.GetObjectKind().GroupVersionKind() != gvk { t.Log(body)
t.Errorf("%d: unexpected error body: %#v", obj) t.Errorf("%d: did not match: %s", i, diff.ObjectReflectDiff(test.expected, &itemOut))
} }
continue })
}
if resp.StatusCode != http.StatusOK {
t.Errorf("%d: unexpected response: %#v", i, resp)
}
var itemOut metav1alpha1.Table
body, err := extractBody(resp, &itemOut)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(test.expected, &itemOut) {
t.Log(body)
t.Errorf("%d: did not match: %s", i, diff.ObjectReflectDiff(test.expected, &itemOut))
}
} }
} }
......
...@@ -1291,10 +1291,21 @@ func (t *Tester) testListTableConversion(obj runtime.Object, assignFn AssignFunc ...@@ -1291,10 +1291,21 @@ func (t *Tester) testListTableConversion(obj runtime.Object, assignFn AssignFunc
t.Errorf("expected: %#v, got: %#v", objs, items) t.Errorf("expected: %#v, got: %#v", objs, items)
} }
m, err := meta.ListAccessor(listObj)
if err != nil {
t.Fatalf("list should support ListMeta %T: %v", listObj, err)
}
m.SetContinue("continuetoken")
m.SetResourceVersion("11")
m.SetSelfLink("/list/link")
table, err := t.storage.(rest.TableConvertor).ConvertToTable(ctx, listObj, nil) table, err := t.storage.(rest.TableConvertor).ConvertToTable(ctx, listObj, nil)
if err != nil { if err != nil {
t.Errorf("unexpected error: %v", err) t.Errorf("unexpected error: %v", err)
} }
if table.ResourceVersion != "11" || table.SelfLink != "/list/link" || table.Continue != "continuetoken" {
t.Errorf("printer lost list meta: %#v", table.ListMeta)
}
if len(table.Rows) != len(items) { if len(table.Rows) != len(items) {
t.Errorf("unexpected number of rows: %v", len(table.Rows)) t.Errorf("unexpected number of rows: %v", len(table.Rows))
} }
......
...@@ -63,6 +63,16 @@ func (c defaultTableConvertor) ConvertToTable(ctx genericapirequest.Context, obj ...@@ -63,6 +63,16 @@ func (c defaultTableConvertor) ConvertToTable(ctx genericapirequest.Context, obj
return nil, err return nil, err
} }
} }
if m, err := meta.ListAccessor(object); err == nil {
table.ResourceVersion = m.GetResourceVersion()
table.SelfLink = m.GetSelfLink()
table.Continue = m.GetContinue()
} else {
if m, err := meta.CommonAccessor(object); err == nil {
table.ResourceVersion = m.GetResourceVersion()
table.SelfLink = m.GetSelfLink()
}
}
table.ColumnDefinitions = []metav1alpha1.TableColumnDefinition{ table.ColumnDefinitions = []metav1alpha1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name", Description: swaggerMetadataDescriptions["name"]}, {Name: "Name", Type: "string", Format: "name", Description: swaggerMetadataDescriptions["name"]},
{Name: "Created At", Type: "date", Description: swaggerMetadataDescriptions["creationTimestamp"]}, {Name: "Created At", Type: "date", Description: swaggerMetadataDescriptions["creationTimestamp"]},
......
...@@ -28,6 +28,7 @@ import ( ...@@ -28,6 +28,7 @@ import (
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
metav1alpha1 "k8s.io/apimachinery/pkg/apis/meta/v1alpha1" metav1alpha1 "k8s.io/apimachinery/pkg/apis/meta/v1alpha1"
"k8s.io/client-go/util/workqueue"
"k8s.io/kubernetes/pkg/printers" "k8s.io/kubernetes/pkg/printers"
"k8s.io/kubernetes/test/e2e/framework" "k8s.io/kubernetes/test/e2e/framework"
imageutils "k8s.io/kubernetes/test/utils/image" imageutils "k8s.io/kubernetes/test/utils/image"
...@@ -63,6 +64,56 @@ var _ = SIGDescribe("Servers with support for Table transformation", func() { ...@@ -63,6 +64,56 @@ var _ = SIGDescribe("Servers with support for Table transformation", func() {
framework.Logf("Table:\n%s", out) framework.Logf("Table:\n%s", out)
}) })
It("should return chunks of table results for list calls", func() {
ns := f.Namespace.Name
c := f.ClientSet
client := c.CoreV1().PodTemplates(ns)
By("creating a large number of resources")
workqueue.Parallelize(5, 20, func(i int) {
for tries := 3; tries >= 0; tries-- {
_, err := client.Create(&v1.PodTemplate{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("template-%04d", i),
},
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
{Name: "test", Image: "test2"},
},
},
},
})
if err == nil {
return
}
framework.Logf("Got an error creating template %d: %v", i, err)
}
Fail("Unable to create template %d, exiting", i)
})
pagedTable := &metav1alpha1.Table{}
err := c.CoreV1().RESTClient().Get().Namespace(ns).Resource("podtemplates").
VersionedParams(&metav1.ListOptions{Limit: 2}, metav1.ParameterCodec).
SetHeader("Accept", "application/json;as=Table;v=v1alpha1;g=meta.k8s.io").
Do().Into(pagedTable)
Expect(err).NotTo(HaveOccurred())
Expect(len(pagedTable.Rows)).To(Equal(2))
Expect(pagedTable.ResourceVersion).ToNot(Equal(""))
Expect(pagedTable.SelfLink).ToNot(Equal(""))
Expect(pagedTable.Continue).ToNot(Equal(""))
Expect(pagedTable.Rows[0].Cells[0]).To(Equal("template-0000"))
Expect(pagedTable.Rows[1].Cells[0]).To(Equal("template-0001"))
err = c.CoreV1().RESTClient().Get().Namespace(ns).Resource("podtemplates").
VersionedParams(&metav1.ListOptions{Continue: pagedTable.Continue}, metav1.ParameterCodec).
SetHeader("Accept", "application/json;as=Table;v=v1alpha1;g=meta.k8s.io").
Do().Into(pagedTable)
Expect(err).NotTo(HaveOccurred())
Expect(len(pagedTable.Rows)).To(BeNumerically(">", 0))
Expect(pagedTable.Rows[0].Cells[0]).To(Equal("template-0002"))
})
It("should return generic metadata details across all namespaces for nodes", func() { It("should return generic metadata details across all namespaces for nodes", func() {
c := f.ClientSet c := f.ClientSet
...@@ -75,6 +126,8 @@ var _ = SIGDescribe("Servers with support for Table transformation", func() { ...@@ -75,6 +126,8 @@ var _ = SIGDescribe("Servers with support for Table transformation", func() {
Expect(len(table.Rows)).To(BeNumerically(">=", 1)) Expect(len(table.Rows)).To(BeNumerically(">=", 1))
Expect(len(table.Rows[0].Cells)).To(Equal(len(table.ColumnDefinitions))) Expect(len(table.Rows[0].Cells)).To(Equal(len(table.ColumnDefinitions)))
Expect(table.ColumnDefinitions[0].Name).To(Equal("Name")) Expect(table.ColumnDefinitions[0].Name).To(Equal("Name"))
Expect(table.ResourceVersion).ToNot(Equal(""))
Expect(table.SelfLink).ToNot(Equal(""))
out := printTable(table) out := printTable(table)
Expect(out).To(MatchRegexp("^NAME\\s")) Expect(out).To(MatchRegexp("^NAME\\s"))
......
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