diff --git a/test/e2e/common/BUILD b/test/e2e/common/BUILD index d6a23af7e60..babd63cf9a3 100644 --- a/test/e2e/common/BUILD +++ b/test/e2e/common/BUILD @@ -52,6 +52,7 @@ go_library( "//pkg/kubelet/runtimeclass/testing:go_default_library", "//staging/src/k8s.io/api/coordination/v1:go_default_library", "//staging/src/k8s.io/api/core/v1:go_default_library", + "//staging/src/k8s.io/api/node/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/api/equality:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/api/resource:go_default_library", diff --git a/test/e2e/common/runtimeclass.go b/test/e2e/common/runtimeclass.go index 490c0a6a8bf..3b1bc6682b9 100644 --- a/test/e2e/common/runtimeclass.go +++ b/test/e2e/common/runtimeclass.go @@ -22,10 +22,13 @@ import ( "time" v1 "k8s.io/api/core/v1" + nodev1 "k8s.io/api/node/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" + types "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/apimachinery/pkg/watch" "k8s.io/kubernetes/pkg/kubelet/events" runtimeclasstest "k8s.io/kubernetes/pkg/kubelet/runtimeclass/testing" "k8s.io/kubernetes/test/e2e/framework" @@ -84,6 +87,169 @@ var _ = ginkgo.Describe("[sig-node] RuntimeClass", func() { expectPodRejection(f, e2enode.NewRuntimeClassPod(rcName)) }) + + /* + Release: v1.20 + Testname: RuntimeClass API + Description: + The node.k8s.io API group MUST exist in the /apis discovery document. + The node.k8s.io/v1 API group/version MUST exist in the /apis/mode.k8s.io discovery document. + The runtimeclasses resource MUST exist in the /apis/node.k8s.io/v1 discovery document. + The runtimeclasses resource must support create, get, list, watch, update, patch, delete, and deletecollection. + */ + ginkgo.It(" [NodeConformance] should support RuntimeClasses API operations", func() { + // Setup + rcVersion := "v1" + rcClient := f.ClientSet.NodeV1().RuntimeClasses() + + // This is a conformance test that must configure opaque handlers to validate CRUD operations. + // Test should not use any existing handler like gVisor or runc + // + // All CRUD operations in this test are limited to the objects with the label test=f.UniqueName + rc := runtimeclasstest.NewRuntimeClass(f.UniqueName+"-handler", f.UniqueName+"-conformance-runtime-class") + rc.SetLabels(map[string]string{"test": f.UniqueName}) + rc2 := runtimeclasstest.NewRuntimeClass(f.UniqueName+"-handler2", f.UniqueName+"-conformance-runtime-class2") + rc2.SetLabels(map[string]string{"test": f.UniqueName}) + rc3 := runtimeclasstest.NewRuntimeClass(f.UniqueName+"-handler3", f.UniqueName+"-conformance-runtime-class3") + rc3.SetLabels(map[string]string{"test": f.UniqueName}) + + // Discovery + + ginkgo.By("getting /apis") + { + discoveryGroups, err := f.ClientSet.Discovery().ServerGroups() + framework.ExpectNoError(err) + found := false + for _, group := range discoveryGroups.Groups { + if group.Name == nodev1.GroupName { + for _, version := range group.Versions { + if version.Version == rcVersion { + found = true + break + } + } + } + } + framework.ExpectEqual(found, true, fmt.Sprintf("expected RuntimeClass API group/version, got %#v", discoveryGroups.Groups)) + } + + ginkgo.By("getting /apis/node.k8s.io") + { + group := &metav1.APIGroup{} + err := f.ClientSet.Discovery().RESTClient().Get().AbsPath("/apis/node.k8s.io").Do(context.TODO()).Into(group) + framework.ExpectNoError(err) + found := false + for _, version := range group.Versions { + if version.Version == rcVersion { + found = true + break + } + } + framework.ExpectEqual(found, true, fmt.Sprintf("expected RuntimeClass API version, got %#v", group.Versions)) + } + + ginkgo.By("getting /apis/node.k8s.io/" + rcVersion) + { + resources, err := f.ClientSet.Discovery().ServerResourcesForGroupVersion(nodev1.SchemeGroupVersion.String()) + framework.ExpectNoError(err) + found := false + for _, resource := range resources.APIResources { + switch resource.Name { + case "runtimeclasses": + found = true + } + } + framework.ExpectEqual(found, true, fmt.Sprintf("expected runtimeclasses, got %#v", resources.APIResources)) + } + + // Main resource create/read/update/watch operations + + ginkgo.By("creating") + createdRC, err := rcClient.Create(context.TODO(), rc, metav1.CreateOptions{}) + framework.ExpectNoError(err) + _, err = rcClient.Create(context.TODO(), rc, metav1.CreateOptions{}) + framework.ExpectEqual(apierrors.IsAlreadyExists(err), true, fmt.Sprintf("expected 409, got %#v", err)) + _, err = rcClient.Create(context.TODO(), rc2, metav1.CreateOptions{}) + framework.ExpectNoError(err) + + ginkgo.By("watching") + framework.Logf("starting watch") + rcWatch, err := rcClient.Watch(context.TODO(), metav1.ListOptions{LabelSelector: "test=" + f.UniqueName}) + framework.ExpectNoError(err) + + // added for a watch + _, err = rcClient.Create(context.TODO(), rc3, metav1.CreateOptions{}) + framework.ExpectNoError(err) + + ginkgo.By("getting") + gottenRC, err := rcClient.Get(context.TODO(), rc.Name, metav1.GetOptions{}) + framework.ExpectNoError(err) + framework.ExpectEqual(gottenRC.UID, createdRC.UID) + + ginkgo.By("listing") + rcs, err := rcClient.List(context.TODO(), metav1.ListOptions{LabelSelector: "test=" + f.UniqueName}) + framework.ExpectNoError(err) + framework.ExpectEqual(len(rcs.Items), 3, "filtered list should have 3 items") + + ginkgo.By("patching") + patchedRC, err := rcClient.Patch(context.TODO(), createdRC.Name, types.MergePatchType, []byte(`{"metadata":{"annotations":{"patched":"true"}}}`), metav1.PatchOptions{}) + framework.ExpectNoError(err) + framework.ExpectEqual(patchedRC.Annotations["patched"], "true", "patched object should have the applied annotation") + + ginkgo.By("updating") + csrToUpdate := patchedRC.DeepCopy() + csrToUpdate.Annotations["updated"] = "true" + updatedRC, err := rcClient.Update(context.TODO(), csrToUpdate, metav1.UpdateOptions{}) + framework.ExpectNoError(err) + framework.ExpectEqual(updatedRC.Annotations["updated"], "true", "updated object should have the applied annotation") + + framework.Logf("waiting for watch events with expected annotations") + for sawAdded, sawPatched, sawUpdated := false, false, false; !sawAdded && !sawPatched && !sawUpdated; { + select { + case evt, ok := <-rcWatch.ResultChan(): + framework.ExpectEqual(ok, true, "watch channel should not close") + if evt.Type == watch.Modified { + watchedRC, isRC := evt.Object.(*nodev1.RuntimeClass) + framework.ExpectEqual(isRC, true, fmt.Sprintf("expected RC, got %T", evt.Object)) + if watchedRC.Annotations["patched"] == "true" { + framework.Logf("saw patched annotations") + sawPatched = true + } else if watchedRC.Annotations["updated"] == "true" { + framework.Logf("saw updated annotations") + sawUpdated = true + } else { + framework.Logf("missing expected annotations, waiting: %#v", watchedRC.Annotations) + } + } else if evt.Type == watch.Added { + _, isRC := evt.Object.(*nodev1.RuntimeClass) + framework.ExpectEqual(isRC, true, fmt.Sprintf("expected RC, got %T", evt.Object)) + sawAdded = true + } + + case <-time.After(wait.ForeverTestTimeout): + framework.Fail("timed out waiting for watch event") + } + } + rcWatch.Stop() + + // main resource delete operations + + ginkgo.By("deleting") + err = rcClient.Delete(context.TODO(), createdRC.Name, metav1.DeleteOptions{}) + framework.ExpectNoError(err) + _, err = rcClient.Get(context.TODO(), createdRC.Name, metav1.GetOptions{}) + framework.ExpectEqual(apierrors.IsNotFound(err), true, fmt.Sprintf("expected 404, got %#v", err)) + rcs, err = rcClient.List(context.TODO(), metav1.ListOptions{LabelSelector: "test=" + f.UniqueName}) + framework.ExpectNoError(err) + framework.ExpectEqual(len(rcs.Items), 2, "filtered list should have 2 items") + + ginkgo.By("deleting a collection") + err = rcClient.DeleteCollection(context.TODO(), metav1.DeleteOptions{}, metav1.ListOptions{LabelSelector: "test=" + f.UniqueName}) + framework.ExpectNoError(err) + rcs, err = rcClient.List(context.TODO(), metav1.ListOptions{LabelSelector: "test=" + f.UniqueName}) + framework.ExpectNoError(err) + framework.ExpectEqual(len(rcs.Items), 0, "filtered list should have 0 items") + }) }) // createRuntimeClass generates a RuntimeClass with the desired handler and a "namespaced" name,