diff --git a/test/e2e/kubectl/BUILD b/test/e2e/kubectl/BUILD index 9931e1f2d4d..f9c20465f7c 100644 --- a/test/e2e/kubectl/BUILD +++ b/test/e2e/kubectl/BUILD @@ -19,15 +19,19 @@ go_library( "//staging/src/k8s.io/api/rbac/v1:go_default_library", "//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/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/api/resource: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/labels:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/net:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/rand:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/uuid:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library", "//staging/src/k8s.io/apiserver/pkg/authentication/serviceaccount:go_default_library", "//staging/src/k8s.io/apiserver/pkg/registry/generic/registry:go_default_library", + "//staging/src/k8s.io/client-go/dynamic:go_default_library", "//staging/src/k8s.io/client-go/kubernetes:go_default_library", "//staging/src/k8s.io/kubectl/pkg/polymorphichelpers:go_default_library", "//test/e2e/common:go_default_library", @@ -39,6 +43,7 @@ go_library( "//test/e2e/framework/service:go_default_library", "//test/e2e/framework/testfiles:go_default_library", "//test/e2e/scheduling:go_default_library", + "//test/integration/etcd:go_default_library", "//test/utils:go_default_library", "//test/utils/crd:go_default_library", "//test/utils/image:go_default_library", diff --git a/test/e2e/kubectl/kubectl.go b/test/e2e/kubectl/kubectl.go index 7f7244184aa..23e12b57503 100644 --- a/test/e2e/kubectl/kubectl.go +++ b/test/e2e/kubectl/kubectl.go @@ -46,15 +46,19 @@ import ( rbacv1 "k8s.io/api/rbac/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apierrs "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime/schema" utilnet "k8s.io/apimachinery/pkg/util/net" + "k8s.io/apimachinery/pkg/util/rand" "k8s.io/apimachinery/pkg/util/uuid" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/apiserver/pkg/authentication/serviceaccount" genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" + "k8s.io/client-go/dynamic" clientset "k8s.io/client-go/kubernetes" "k8s.io/kubectl/pkg/polymorphichelpers" "k8s.io/kubernetes/pkg/controller" @@ -67,6 +71,7 @@ import ( e2eservice "k8s.io/kubernetes/test/e2e/framework/service" "k8s.io/kubernetes/test/e2e/framework/testfiles" "k8s.io/kubernetes/test/e2e/scheduling" + "k8s.io/kubernetes/test/integration/etcd" testutils "k8s.io/kubernetes/test/utils" "k8s.io/kubernetes/test/utils/crd" imageutils "k8s.io/kubernetes/test/utils/image" @@ -410,6 +415,82 @@ var _ = SIGDescribe("Kubectl client", func() { }) }) + ginkgo.Describe("kubectl get output", func() { + ginkgo.It("should contain custom columns for each resource", func() { + randString := rand.String(10) + + ignoredResources := map[string]bool{ + // ignored for intentionally using standard fields. + // This assumption is based on a lack of TableColumnDefinition + // in pkg/printers/internalversion/printers.go + "ClusterRole": true, + "LimitRange": true, + "PodPreset": true, + + // ignored temporarily while waiting for bug fix. + "ComponentStatus": true, + "ClusterRoleBinding": true, + "ResourceQuota": true, + + // ignored because no test data exists. + // Do not add anything to this list, instead add fixtures in + // the test/integration/etcd package. + "BackendConfig": true, + "NodeMetrics": true, + "PodMetrics": true, + "ScalingPolicy": true, + "VolumeSnapshotClass": true, + "VolumeSnapshotContent": true, + "VolumeSnapshot": true, + + // A CRD created by other e2e tests without any test data. + "Noxu": true, + } + + apiGroups, err := c.Discovery().ServerPreferredResources() + framework.ExpectNoError(err) + + testableResources := etcd.GetEtcdStorageDataForNamespace(f.Namespace.Name) + + for _, group := range apiGroups { + for _, resource := range group.APIResources { + if !verbsContain(resource.Verbs, "get") || ignoredResources[resource.Kind] || strings.HasPrefix(resource.Name, "e2e-test") { + continue + } + + // compute gvr + gv, err := schema.ParseGroupVersion(group.GroupVersion) + framework.ExpectNoError(err) + gvr := gv.WithResource(resource.Name) + + // assert test data exists + testData := testableResources[gvr] + gomega.ExpectWithOffset(1, testData).ToNot(gomega.BeZero(), "No test data available for %s", gvr) + + // create test resource + mapping := &meta.RESTMapping{ + Resource: gvr, + GroupVersionKind: gv.WithKind(resource.Kind), + } + + if resource.Namespaced { + mapping.Scope = meta.RESTScopeNamespace + } else { + mapping.Scope = meta.RESTScopeRoot + } + + client, obj, err := etcd.JSONToUnstructured(testData.Stub, f.Namespace.Name, mapping, f.DynamicClient) + framework.ExpectNoError(err) + if resource.Kind != "APIService" && resource.Kind != "CustomResourceDefinition" { + obj.SetName(obj.GetName() + randString) + } + + createObjValidateOutputAndCleanup(client, obj, resource) + } + } + }) + }) + ginkgo.Describe("Simple pod", func() { var podYaml string ginkgo.BeforeEach(func() { @@ -2482,3 +2563,43 @@ waitLoop: // Reaching here means that one of more checks failed multiple times. Assuming its not a race condition, something is broken. framework.Failf("Timed out after %v seconds waiting for %s pods to reach valid state", framework.PodStartTimeout.Seconds(), testname) } + +// verbsContain returns true if the provided list of verbs contain the provided +// verb string. +func verbsContain(verbs metav1.Verbs, str string) bool { + for _, v := range verbs { + if v == str { + return true + } + } + return false +} + +// deleteObj deletes an Object with the provided client and name. +func deleteObj(client dynamic.ResourceInterface, name string) { + err := client.Delete(name, &metav1.DeleteOptions{}) + framework.ExpectNoError(err) +} + +// createObjValidateOutputAndCleanup creates an object using the provided client +// and then verifies that the kubectl get output provides custom columns. Once +// the test has completed, it deletes the object. +func createObjValidateOutputAndCleanup(client dynamic.ResourceInterface, obj *unstructured.Unstructured, resource metav1.APIResource) { + _, err := client.Create(obj, metav1.CreateOptions{}) + framework.ExpectNoError(err) + defer deleteObj(client, obj.GetName()) + + // get test resource + output := framework.RunKubectlOrDie("get", resource.Name, "--all-namespaces") + if output == "" { + framework.Failf("No stdout from kubectl get for %s (likely need to define test resources)", resource.Name) + } + + splitOutput := strings.SplitN(output, "\n", 2) + fields := strings.Fields(splitOutput[0]) + if resource.Namespaced { + framework.ExpectNotEqual(fields, []string{"NAMESPACE", "NAME", "CREATED", "AT"}, fmt.Sprintf("expected non-default fields for namespaced resource: %s", resource.Name)) + } else { + framework.ExpectNotEqual(fields, []string{"NAME", "AGE"}, fmt.Sprintf("expected non-default fields for resource: %s", resource.Name)) + } +}