Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an e2e test for server side get #46881

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 6 additions & 3 deletions staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go
Expand Up @@ -437,6 +437,10 @@ func (storage *SimpleRESTStorage) Export(ctx request.Context, name string, opts
return obj, storage.errors["export"]
}

func (storage *SimpleRESTStorage) ConvertToTable(ctx request.Context, obj runtime.Object, tableOptions runtime.Object) (*metav1alpha1.Table, error) {
return rest.NewDefaultTableConvertor(schema.GroupResource{Resource: "simple"}).ConvertToTable(ctx, obj, tableOptions)
}

func (storage *SimpleRESTStorage) List(ctx request.Context, options *metainternalversion.ListOptions) (runtime.Object, error) {
storage.checkContext(ctx)
result := &genericapitesting.SimpleList{
Expand Down Expand Up @@ -1653,12 +1657,11 @@ func TestGetTable(t *testing.T) {
expected: &metav1alpha1.Table{
TypeMeta: metav1.TypeMeta{Kind: "Table", APIVersion: "meta.k8s.io/v1alpha1"},
ColumnDefinitions: []metav1alpha1.TableColumnDefinition{
{Name: "Namespace", Type: "string", Description: metaDoc["namespace"]},
{Name: "Name", Type: "string", Description: metaDoc["name"]},
{Name: "Created At", Type: "date", Description: metaDoc["creationTimestamp"]},
},
Rows: []metav1alpha1.TableRow{
{Cells: []interface{}{"ns1", "foo1", now.Time.UTC().Format(time.RFC3339)}, Object: runtime.RawExtension{Raw: encodedBody}},
{Cells: []interface{}{"foo1", now.Time.UTC().Format(time.RFC3339)}, Object: runtime.RawExtension{Raw: encodedBody}},
},
},
},
Expand All @@ -1683,7 +1686,7 @@ func TestGetTable(t *testing.T) {
continue
}
if resp.StatusCode != http.StatusOK {
t.Fatal(err)
t.Errorf("%d: unexpected response: %#v", resp)
}
var itemOut metav1alpha1.Table
if _, err = extractBody(resp, &itemOut); err != nil {
Expand Down
5 changes: 1 addition & 4 deletions staging/src/k8s.io/apiserver/pkg/endpoints/installer.go
Expand Up @@ -374,10 +374,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
shortNames = shortNamesProvider.ShortNames()
}

tableProvider, ok := storage.(rest.TableConvertor)
if !ok {
tableProvider = rest.DefaultTableConvertor
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we no longer defaulting?

Copy link
Contributor

@deads2k deads2k Jun 6, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a panic or something here so that people who aren't using the generic store will get at least some notification that things won't work right. Method supports an error, so use that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're allowed not to support server side get - this change returns a 406 if that happens. This ensures that swagger matches what the storage supports.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're allowed not to support server side get - this change returns a 406 if that happens. This ensures that swagger matches what the storage supports.

Hmmm.. We'll want some sort of verify or we'll get weird problems eventually.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm.. We'll want some sort of verify or we'll get weird problems eventually.

Can be a followup in 1.8.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a verify in the e2e test

}
tableProvider, _ := storage.(rest.TableConvertor)

var apiResource metav1.APIResource
// Get the list of actions for the given scope.
Expand Down
Expand Up @@ -1351,5 +1351,5 @@ func (e *Store) ConvertToTable(ctx genericapirequest.Context, object runtime.Obj
if e.TableConvertor != nil {
return e.TableConvertor.ConvertToTable(ctx, object, tableOptions)
}
return rest.DefaultTableConvertor.ConvertToTable(ctx, object, tableOptions)
return rest.NewDefaultTableConvertor(e.QualifiedResource).ConvertToTable(ctx, object, tableOptions)
}
73 changes: 28 additions & 45 deletions staging/src/k8s.io/apiserver/pkg/registry/rest/table.go
Expand Up @@ -17,31 +17,38 @@ limitations under the License.
package rest

import (
"fmt"
"net/http"
"time"

"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
metav1alpha1 "k8s.io/apimachinery/pkg/apis/meta/v1alpha1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
)

var DefaultTableConvertor TableConvertor = defaultTableConvertor{}
type defaultTableConvertor struct {
qualifiedResource schema.GroupResource
}

type defaultTableConvertor struct{}
// NewDefaultTableConvertor creates a default convertor for the provided resource.
func NewDefaultTableConvertor(resource schema.GroupResource) TableConvertor {
return defaultTableConvertor{qualifiedResource: resource}
}

var swaggerMetadataDescriptions = metav1.ObjectMeta{}.SwaggerDoc()

func (defaultTableConvertor) ConvertToTable(ctx genericapirequest.Context, object runtime.Object, tableOptions runtime.Object) (*metav1alpha1.Table, error) {
func (c defaultTableConvertor) ConvertToTable(ctx genericapirequest.Context, object runtime.Object, tableOptions runtime.Object) (*metav1alpha1.Table, error) {
var table metav1alpha1.Table
fn := func(obj runtime.Object) error {
m, err := meta.Accessor(obj)
if err != nil {
// TODO: skip objects we don't recognize
return nil
return errNotAcceptable{resource: c.qualifiedResource}
}
table.Rows = append(table.Rows, metav1alpha1.TableRow{
Cells: []interface{}{m.GetClusterName(), m.GetNamespace(), m.GetName(), m.GetCreationTimestamp().Time.UTC().Format(time.RFC3339)},
Cells: []interface{}{m.GetName(), m.GetCreationTimestamp().Time.UTC().Format(time.RFC3339)},
Object: runtime.RawExtension{Object: obj},
})
return nil
Expand All @@ -57,50 +64,26 @@ func (defaultTableConvertor) ConvertToTable(ctx genericapirequest.Context, objec
}
}
table.ColumnDefinitions = []metav1alpha1.TableColumnDefinition{
{Name: "Cluster Name", Type: "string", Description: swaggerMetadataDescriptions["clusterName"]},
{Name: "Namespace", Type: "string", Description: swaggerMetadataDescriptions["namespace"]},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most resources are namespaced. This seems like a bad default.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Namespace column is handled on the client.

{Name: "Name", Type: "string", Description: swaggerMetadataDescriptions["name"]},
{Name: "Created At", Type: "date", Description: swaggerMetadataDescriptions["creationTimestamp"]},
}
// trim the left two columns if completely empty
if trimColumn(0, &table) {
trimColumn(0, &table)
} else {
trimColumn(1, &table)
}
return &table, nil
}

func trimColumn(column int, table *metav1alpha1.Table) bool {
for _, item := range table.Rows {
switch t := item.Cells[column].(type) {
case string:
if len(t) > 0 {
return false
}
case interface{}:
if t == nil {
return false
}
}
}
if column == 0 {
table.ColumnDefinitions = table.ColumnDefinitions[1:]
} else {
for j := column; j < len(table.ColumnDefinitions); j++ {
table.ColumnDefinitions[j] = table.ColumnDefinitions[j+1]
}
}
for i := range table.Rows {
cells := table.Rows[i].Cells
if column == 0 {
table.Rows[i].Cells = cells[1:]
continue
}
for j := column; j < len(cells); j++ {
cells[j] = cells[j+1]
}
table.Rows[i].Cells = cells[:len(cells)-1]
// errNotAcceptable indicates the resource doesn't support Table conversion
type errNotAcceptable struct {
resource schema.GroupResource
}

func (e errNotAcceptable) Error() string {
return fmt.Sprintf("the resource %s does not support being converted to a Table", e.resource)
}

func (e errNotAcceptable) Status() metav1.Status {
return metav1.Status{
Status: metav1.StatusFailure,
Code: http.StatusNotAcceptable,
Reason: metav1.StatusReason("NotAcceptable"),
Message: e.Error(),
}
return true
}
2 changes: 2 additions & 0 deletions test/e2e/BUILD
Expand Up @@ -21,6 +21,7 @@ go_test(
"//pkg/api/v1:go_default_library",
"//pkg/client/clientset_generated/clientset:go_default_library",
"//pkg/metrics:go_default_library",
"//test/e2e/api:go_default_library",
"//test/e2e/autoscaling:go_default_library",
"//test/e2e/cluster-logging:go_default_library",
"//test/e2e/extension:go_default_library",
Expand Down Expand Up @@ -232,6 +233,7 @@ filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//test/e2e/api:all-srcs",
"//test/e2e/autoscaling:all-srcs",
"//test/e2e/chaosmonkey:all-srcs",
"//test/e2e/cluster-logging:all-srcs",
Expand Down
37 changes: 37 additions & 0 deletions test/e2e/api/BUILD
@@ -0,0 +1,37 @@
package(default_visibility = ["//visibility:public"])

licenses(["notice"])

load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)

go_library(
name = "go_default_library",
srcs = ["table_conversion.go"],
tags = ["automanaged"],
deps = [
"//pkg/api/v1:go_default_library",
"//pkg/printers:go_default_library",
"//test/e2e/framework:go_default_library",
"//vendor/github.com/onsi/ginkgo:go_default_library",
"//vendor/github.com/onsi/gomega:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1alpha1:go_default_library",
],
)

filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)

filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)
122 changes: 122 additions & 0 deletions test/e2e/api/table_conversion.go
@@ -0,0 +1,122 @@
/*
Copyright 2017 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 extension

import (
"bytes"
"fmt"
"text/tabwriter"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"

"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
metav1alpha1 "k8s.io/apimachinery/pkg/apis/meta/v1alpha1"
"k8s.io/kubernetes/pkg/api/v1"
"k8s.io/kubernetes/pkg/printers"
"k8s.io/kubernetes/test/e2e/framework"
)

var _ = framework.KubeDescribe("Servers with support for Table transformation", func() {
f := framework.NewDefaultFramework("tables")

It("should return pod details", func() {
ns := f.Namespace.Name
c := f.ClientSet

podName := "pod-1"
framework.Logf("Creating pod %s", podName)

_, err := c.Core().Pods(ns).Create(newPod(podName))
Expect(err).NotTo(HaveOccurred())

table := &metav1alpha1.Table{}
err = c.Core().RESTClient().Get().Resource("pods").Namespace(ns).Name(podName).SetHeader("Accept", "application/json;as=Table;v=v1alpha1;g=meta.k8s.io").Do().Into(table)
Expect(err).NotTo(HaveOccurred())
framework.Logf("Table: %#v", table)

Expect(len(table.ColumnDefinitions)).To(BeNumerically(">", 2))
Expect(len(table.Rows)).To(Equal(1))
Expect(len(table.Rows[0].Cells)).To(Equal(len(table.ColumnDefinitions)))
Expect(table.ColumnDefinitions[0].Name).To(Equal("Name"))
Expect(table.Rows[0].Cells[0]).To(Equal(podName))

out := printTable(table)
Expect(out).To(MatchRegexp("^NAME\\s"))
Expect(out).To(MatchRegexp("\npod-1\\s"))
framework.Logf("Table:\n%s", out)
})

It("should return generic metadata details across all namespaces for nodes", func() {
c := f.ClientSet

table := &metav1alpha1.Table{}
err := c.Core().RESTClient().Get().Resource("nodes").SetHeader("Accept", "application/json;as=Table;v=v1alpha1;g=meta.k8s.io").Do().Into(table)
Expect(err).NotTo(HaveOccurred())
framework.Logf("Table: %#v", table)

Expect(len(table.ColumnDefinitions)).To(BeNumerically(">=", 2))
Expect(len(table.Rows)).To(BeNumerically(">=", 1))
Expect(len(table.Rows[0].Cells)).To(Equal(len(table.ColumnDefinitions)))
Expect(table.ColumnDefinitions[0].Name).To(Equal("Name"))

out := printTable(table)
Expect(out).To(MatchRegexp("^NAME\\s"))
framework.Logf("Table:\n%s", out)
})

It("should return a 406 for a backend which does not implement metadata", func() {
c := f.ClientSet

table := &metav1alpha1.Table{}
err := c.Core().RESTClient().Get().Resource("services").SetHeader("Accept", "application/json;as=Table;v=v1alpha1;g=meta.k8s.io").Do().Into(table)
Expect(err).To(HaveOccurred())
Expect(err.(errors.APIStatus).Status().Code).To(Equal(int32(406)))
})
})

func printTable(table *metav1alpha1.Table) string {
buf := &bytes.Buffer{}
tw := tabwriter.NewWriter(buf, 5, 8, 1, ' ', 0)
err := printers.PrintTable(table, tw, printers.PrintOptions{})
Expect(err).NotTo(HaveOccurred())
tw.Flush()
return buf.String()
}

func newPod(podName string) *v1.Pod {
containerName := fmt.Sprintf("%s-container", podName)
port := 8080
pod := &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: podName,
},
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: containerName,
Image: "gcr.io/google_containers/porter:4524579c0eb935c056c8e75563b4e1eda31587e0",
Env: []v1.EnvVar{{Name: fmt.Sprintf("SERVE_PORT_%d", port), Value: "foo"}},
Ports: []v1.ContainerPort{{ContainerPort: int32(port)}},
},
},
RestartPolicy: v1.RestartPolicyNever,
},
}
return pod
}
1 change: 1 addition & 0 deletions test/e2e/e2e_test.go
Expand Up @@ -19,6 +19,7 @@ package e2e
import (
"testing"

_ "k8s.io/kubernetes/test/e2e/api"
_ "k8s.io/kubernetes/test/e2e/autoscaling"
_ "k8s.io/kubernetes/test/e2e/cluster-logging"
_ "k8s.io/kubernetes/test/e2e/extension"
Expand Down