diff --git a/test/extended/apiserver/apply.go b/test/extended/apiserver/apply.go new file mode 100644 index 000000000000..79cb7c39e7d9 --- /dev/null +++ b/test/extended/apiserver/apply.go @@ -0,0 +1,174 @@ +package apiserver + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + g "github.com/onsi/ginkgo" + o "github.com/onsi/gomega" + + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + discocache "k8s.io/client-go/discovery/cached" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/restmapper" + "k8s.io/kubernetes/test/e2e/framework" + + exetcd "github.com/openshift/origin/test/extended/etcd" + exutil "github.com/openshift/origin/test/extended/util" +) + +var _ = g.Describe("[sig-api-machinery][Feature:ServerSideApply] Server-Side Apply", func() { + var ( + // mapper is used to translate the gvr provided by etcd + // storage data to the gvk required to create correct resource + // yaml. + mapper *restmapper.DeferredDiscoveryRESTMapper + ) + + defer g.GinkgoRecover() + + // Defer project creation to tests that require it by calling + // NewCLIWithoutNamespace instead of NewCLI. + oc := exutil.NewCLIWithoutNamespace("server-side-apply") + + g.BeforeEach(func() { + // Only init once per worker + if mapper != nil { + return + } + kubeClient, err := kubernetes.NewForConfig(oc.AdminConfig()) + o.Expect(err).NotTo(o.HaveOccurred()) + mapper = restmapper.NewDeferredDiscoveryRESTMapper(discocache.NewMemCacheClient(kubeClient.Discovery())) + }) + storageData := exetcd.OpenshiftEtcdStorageData + for key := range storageData { + gvr := key + data := storageData[gvr] + + // Apply for core types is already well-tested, so skip + // openshift types that are just aliases. + aliasToCoreType := data.ExpectedGVK != nil + if aliasToCoreType { + continue + } + + g.It(fmt.Sprintf("should work for %s", gvr), func() { + // Create at most one namespace only if needed. + var testNamespace string + + for _, prerequisite := range data.Prerequisites { + // The etcd storage test for oauthclientauthorizations needs to + // manually create a service account secret but that is not + // necessary (or possible) when interacting with an apiserver. + // The service account secret will be created by the controller + // manager. + if gvr.Resource == "oauthclientauthorizations" && prerequisite.GvrData.Resource == "secrets" { + continue + } + resourceClient, unstructuredObj, namespace := createResource(oc, mapper, prerequisite.GvrData, prerequisite.Stub, testNamespace) + testNamespace = namespace + defer deleteResource(resourceClient, unstructuredObj.GetName()) + } + + resourceClient, unstructuredObj, namespace := createResource(oc, mapper, gvr, data.Stub, testNamespace) + testNamespace = namespace + defer deleteResource(resourceClient, unstructuredObj.GetName()) + + serializedObj, err := json.Marshal(unstructuredObj.Object) + o.Expect(err).NotTo(o.HaveOccurred()) + + g.By(fmt.Sprintf("updating the %s via apply", unstructuredObj.GetKind())) + obj, err := resourceClient.Patch( + context.Background(), + unstructuredObj.GetName(), + types.ApplyPatchType, + serializedObj, + metav1.PatchOptions{ + FieldManager: "apply_test", + }, + ) + o.Expect(err).NotTo(o.HaveOccurred()) + + g.By(fmt.Sprintf("checking that the field managers are as expected")) + accessor, err := meta.Accessor(obj) + o.Expect(err).NotTo(o.HaveOccurred()) + managedFields := accessor.GetManagedFields() + o.Expect(managedFields).NotTo(o.BeNil()) + if !findManager(managedFields, "create_test") { + g.Fail(fmt.Sprintf("Couldn't find create_test: %v", managedFields)) + } + if !findManager(managedFields, "apply_test") { + g.Fail(fmt.Sprintf("Couldn't find apply_test: %v", managedFields)) + } + }) + } +}) + +func findManager(managedFields []metav1.ManagedFieldsEntry, manager string) bool { + for _, entry := range managedFields { + if entry.Manager == manager { + return true + } + } + return false +} + +func deleteResource(resourceClient dynamic.ResourceInterface, name string) { + err := resourceClient.Delete(context.Background(), name, metav1.DeleteOptions{}) + if err != nil { + framework.Logf("Unexpected error deleting resource: %v", err) + } +} + +func createResource( + oc *exutil.CLI, + mapper *restmapper.DeferredDiscoveryRESTMapper, + gvr schema.GroupVersionResource, + stub, testNamespace string) ( + dynamic.ResourceInterface, *unstructured.Unstructured, string) { + + // Discover the gvk from the gvr + gvk, err := mapper.KindFor(gvr) + o.Expect(err).NotTo(o.HaveOccurred()) + + // Supply a value for namespace if the scope requires + mapping, err := mapper.RESTMapping(gvk.GroupKind()) + o.Expect(err).NotTo(o.HaveOccurred()) + namespace := "" + if mapping.Scope.Name() == meta.RESTScopeNameNamespace && len(testNamespace) == 0 { + // Create the namespace on demand + namespace = oc.SetupProject() + testNamespace = namespace + } + + // Ensure that any stub embedding the etcd test namespace + // is updated to use local test namespace instead. + if len(testNamespace) > 0 { + stub = strings.Replace(stub, exetcd.TestNamespace, testNamespace, -1) + } + + // Create unstructured object from stub + unstructuredObj := unstructured.Unstructured{} + err = json.Unmarshal([]byte(stub), &unstructuredObj.Object) + o.Expect(err).NotTo(o.HaveOccurred()) + unstructuredObj.SetGroupVersionKind(gvk) + + dynamicClient, err := dynamic.NewForConfig(oc.AdminConfig()) + o.Expect(err).NotTo(o.HaveOccurred()) + resourceClient := dynamicClient.Resource(gvr).Namespace(namespace) + + g.By(fmt.Sprintf("creating a %s", gvk.Kind)) + _, err = resourceClient.Create(context.Background(), &unstructuredObj, metav1.CreateOptions{ + FieldManager: "create_test", + }) + o.Expect(err).NotTo(o.HaveOccurred()) + + return resourceClient, &unstructuredObj, testNamespace +} diff --git a/test/extended/etcd/etcd_storage_path.go b/test/extended/etcd/etcd_storage_path.go index f6861e2acf78..d9c3004d9643 100644 --- a/test/extended/etcd/etcd_storage_path.go +++ b/test/extended/etcd/etcd_storage_path.go @@ -36,8 +36,8 @@ import ( ) // Etcd data for all persisted OpenShift objects. -var openshiftEtcdStorageData = map[schema.GroupVersionResource]etcddata.StorageData{ - // github.com/openshift/openshift-apiserver/pkg/authorization/apis/authorization/v1 +var OpenshiftEtcdStorageData = map[schema.GroupVersionResource]etcddata.StorageData{ + // github.com/openshift/api/authorization/v1 gvr("authorization.openshift.io", "v1", "roles"): { Stub: `{"metadata": {"name": "r1b1o2"}, "rules": [{"verbs": ["create"], "apiGroups": ["authorization.k8s.io"], "resources": ["selfsubjectaccessreviews"]}]}`, ExpectedEtcdPath: "kubernetes.io/roles/etcdstoragepathtestnamespace/r1b1o2", @@ -60,7 +60,7 @@ var openshiftEtcdStorageData = map[schema.GroupVersionResource]etcddata.StorageD }, // -- - // github.com/openshift/openshift-apiserver/pkg/build/apis/build/v1 + // github.com/openshift/api/build/v1 gvr("build.openshift.io", "v1", "builds"): { Stub: `{"metadata": {"name": "build1g"}, "spec": {"source": {"dockerfile": "Dockerfile1"}, "strategy": {"dockerStrategy": {"noCache": true}}}}`, ExpectedEtcdPath: "openshift.io/builds/etcdstoragepathtestnamespace/build1g", @@ -71,14 +71,14 @@ var openshiftEtcdStorageData = map[schema.GroupVersionResource]etcddata.StorageD }, // -- - // github.com/openshift/openshift-apiserver/pkg/apps/apis/apps/v1 + // github.com/openshift/api/apps/v1 gvr("apps.openshift.io", "v1", "deploymentconfigs"): { Stub: `{"metadata": {"name": "dc1g"}, "spec": {"selector": {"d": "c"}, "template": {"metadata": {"labels": {"d": "c"}}, "spec": {"containers": [{"image": "fedora:latest", "name": "container2"}]}}}}`, ExpectedEtcdPath: "openshift.io/deploymentconfigs/etcdstoragepathtestnamespace/dc1g", }, // -- - // github.com/openshift/openshift-apiserver/pkg/image/apis/image/v1 + // github.com/openshift/api/image/v1 gvr("image.openshift.io", "v1", "imagestreams"): { Stub: `{"metadata": {"name": "is1g"}, "spec": {"dockerImageRepository": "docker"}}`, ExpectedEtcdPath: "openshift.io/imagestreams/etcdstoragepathtestnamespace/is1g", @@ -89,7 +89,7 @@ var openshiftEtcdStorageData = map[schema.GroupVersionResource]etcddata.StorageD }, // -- - // github.com/openshift/openshift-apiserver/pkg/oauth/apis/oauth/v1 + // github.com/openshift/api/oauth/v1 gvr("oauth.openshift.io", "v1", "oauthclientauthorizations"): { Stub: `{"clientName": "system:serviceaccount:etcdstoragepathtestnamespace:clientg", "metadata": {"name": "user:system:serviceaccount:etcdstoragepathtestnamespace:clientg"}, "scopes": ["user:info"], "userName": "user", "userUID": "cannot be empty"}`, ExpectedEtcdPath: "openshift.io/oauth/clientauthorizations/user:system:serviceaccount:etcdstoragepathtestnamespace:clientg", @@ -130,7 +130,7 @@ var openshiftEtcdStorageData = map[schema.GroupVersionResource]etcddata.StorageD }, // -- - // github.com/openshift/openshift-apiserver/pkg/project/apis/project/v1 + // github.com/openshift/api/project/v1 gvr("project.openshift.io", "v1", "projects"): { Stub: `{"metadata": {"name": "namespace2g"}, "spec": {"finalizers": ["kubernetes", "openshift.io/origin"]}}`, ExpectedEtcdPath: "kubernetes.io/namespaces/namespace2g", @@ -138,21 +138,21 @@ var openshiftEtcdStorageData = map[schema.GroupVersionResource]etcddata.StorageD }, // -- - // github.com/openshift/openshift-apiserver/pkg/route/apis/route/v1 + // github.com/openshift/api/route/v1 gvr("route.openshift.io", "v1", "routes"): { Stub: `{"metadata": {"name": "route1g"}, "spec": {"host": "hostname1", "to": {"name": "service1"}}}`, ExpectedEtcdPath: "openshift.io/routes/etcdstoragepathtestnamespace/route1g", }, // -- - // github.com/openshift/openshift-apiserver/pkg/security/apis/security/v1 + // github.com/openshift/api/security/v1 gvr("security.openshift.io", "v1", "rangeallocations"): { - Stub: `{"metadata": {"name": "scc2"}}`, + Stub: `{"metadata": {"name": "scc2"}, "range": "", "data": ""}`, ExpectedEtcdPath: "openshift.io/rangeallocations/scc2", }, // -- - // github.com/openshift/openshift-apiserver/pkg/template/apis/template/v1 + // github.com/openshift/api/template/v1 gvr("template.openshift.io", "v1", "templates"): { Stub: `{"message": "Jenkins template", "metadata": {"name": "template1g"}}`, ExpectedEtcdPath: "openshift.io/templates/etcdstoragepathtestnamespace/template1g", @@ -167,7 +167,7 @@ var openshiftEtcdStorageData = map[schema.GroupVersionResource]etcddata.StorageD }, // -- - // github.com/openshift/openshift-apiserver/pkg/user/apis/user/v1 + // github.com/openshift/api/user/v1 gvr("user.openshift.io", "v1", "groups"): { Stub: `{"metadata": {"name": "groupg"}, "users": ["user1", "user2"]}`, ExpectedEtcdPath: "openshift.io/groups/groupg", @@ -197,7 +197,7 @@ var kindWhiteList = sets.NewString( ) // namespace used for all tests, do not change this -const testNamespace = "etcdstoragepathtestnamespace" +const TestNamespace = "etcdstoragepathtestnamespace" type helperT struct { g.GinkgoTInterface @@ -259,16 +259,16 @@ func testEtcd3StoragePath(t g.GinkgoTInterface, kubeConfig *restclient.Config, e client := &allClient{dynamicClient: dynamic.NewForConfigOrDie(kubeConfig)} - if _, err := kubeClient.CoreV1().Namespaces().Create(context.Background(), &kapiv1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: testNamespace}}, metav1.CreateOptions{}); err != nil { + if _, err := kubeClient.CoreV1().Namespaces().Create(context.Background(), &kapiv1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: TestNamespace}}, metav1.CreateOptions{}); err != nil { t.Fatalf("error creating test namespace: %#v", err) } defer func() { - if err := kubeClient.CoreV1().Namespaces().Delete(context.Background(), testNamespace, metav1.DeleteOptions{}); err != nil { + if err := kubeClient.CoreV1().Namespaces().Delete(context.Background(), TestNamespace, metav1.DeleteOptions{}); err != nil { t.Fatalf("error deleting test namespace: %#v", err) } }() - if err := exutil.WaitForServiceAccount(kubeClient.CoreV1().ServiceAccounts(testNamespace), "default"); err != nil { + if err := exutil.WaitForServiceAccount(kubeClient.CoreV1().ServiceAccounts(TestNamespace), "default"); err != nil { t.Fatalf("error waiting for the default service account: %v", err) } @@ -346,7 +346,7 @@ func testEtcd3StoragePath(t g.GinkgoTInterface, kubeConfig *restclient.Config, e } // add openshift specific data - for gvr, data := range openshiftEtcdStorageData { + for gvr, data := range OpenshiftEtcdStorageData { if _, ok := etcdStorageData[gvr]; ok { t.Errorf("%s exists in both Kube and OpenShift ETCD data, data=%#v", gvr.String(), data) } @@ -424,13 +424,13 @@ func testEtcd3StoragePath(t g.GinkgoTInterface, kubeConfig *restclient.Config, e } }() - if err := client.createPrerequisites(mapper, testNamespace, testData.Prerequisites, all); err != nil { + if err := client.createPrerequisites(mapper, TestNamespace, testData.Prerequisites, all); err != nil { t.Errorf("failed to create prerequisites for %v: %#v", gvk, err) return } if shouldCreate { // do not try to create items with no stub - if err := client.create(testData.Stub, testNamespace, mapping, all); err != nil { + if err := client.create(testData.Stub, TestNamespace, mapping, all); err != nil { t.Errorf("failed to create stub for %v: %#v", gvk, err) return } diff --git a/test/extended/util/annotate/generated/zz_generated.annotations.go b/test/extended/util/annotate/generated/zz_generated.annotations.go index 25de0d5e6230..392b71f36824 100644 --- a/test/extended/util/annotate/generated/zz_generated.annotations.go +++ b/test/extended/util/annotate/generated/zz_generated.annotations.go @@ -543,6 +543,40 @@ var annotations = map[string]string{ "[Top Level] [sig-api-machinery][Feature:ClusterResourceQuota] Cluster resource quota should control resource limits across namespaces": "should control resource limits across namespaces [Suite:openshift/conformance/parallel]", + "[Top Level] [sig-api-machinery][Feature:ServerSideApply] Server-Side Apply should work for apps.openshift.io/v1, Resource=deploymentconfigs": "should work for apps.openshift.io/v1, Resource=deploymentconfigs [Suite:openshift/conformance/parallel]", + + "[Top Level] [sig-api-machinery][Feature:ServerSideApply] Server-Side Apply should work for build.openshift.io/v1, Resource=buildconfigs": "should work for build.openshift.io/v1, Resource=buildconfigs [Suite:openshift/conformance/parallel]", + + "[Top Level] [sig-api-machinery][Feature:ServerSideApply] Server-Side Apply should work for build.openshift.io/v1, Resource=builds": "should work for build.openshift.io/v1, Resource=builds [Suite:openshift/conformance/parallel]", + + "[Top Level] [sig-api-machinery][Feature:ServerSideApply] Server-Side Apply should work for image.openshift.io/v1, Resource=images": "should work for image.openshift.io/v1, Resource=images [Suite:openshift/conformance/parallel]", + + "[Top Level] [sig-api-machinery][Feature:ServerSideApply] Server-Side Apply should work for image.openshift.io/v1, Resource=imagestreams": "should work for image.openshift.io/v1, Resource=imagestreams [Suite:openshift/conformance/parallel]", + + "[Top Level] [sig-api-machinery][Feature:ServerSideApply] Server-Side Apply should work for oauth.openshift.io/v1, Resource=oauthaccesstokens": "should work for oauth.openshift.io/v1, Resource=oauthaccesstokens [Suite:openshift/conformance/parallel]", + + "[Top Level] [sig-api-machinery][Feature:ServerSideApply] Server-Side Apply should work for oauth.openshift.io/v1, Resource=oauthauthorizetokens": "should work for oauth.openshift.io/v1, Resource=oauthauthorizetokens [Suite:openshift/conformance/parallel]", + + "[Top Level] [sig-api-machinery][Feature:ServerSideApply] Server-Side Apply should work for oauth.openshift.io/v1, Resource=oauthclientauthorizations": "should work for oauth.openshift.io/v1, Resource=oauthclientauthorizations [Suite:openshift/conformance/parallel]", + + "[Top Level] [sig-api-machinery][Feature:ServerSideApply] Server-Side Apply should work for oauth.openshift.io/v1, Resource=oauthclients": "should work for oauth.openshift.io/v1, Resource=oauthclients [Suite:openshift/conformance/parallel]", + + "[Top Level] [sig-api-machinery][Feature:ServerSideApply] Server-Side Apply should work for route.openshift.io/v1, Resource=routes": "should work for route.openshift.io/v1, Resource=routes [Suite:openshift/conformance/parallel]", + + "[Top Level] [sig-api-machinery][Feature:ServerSideApply] Server-Side Apply should work for security.openshift.io/v1, Resource=rangeallocations": "should work for security.openshift.io/v1, Resource=rangeallocations [Suite:openshift/conformance/parallel]", + + "[Top Level] [sig-api-machinery][Feature:ServerSideApply] Server-Side Apply should work for template.openshift.io/v1, Resource=brokertemplateinstances": "should work for template.openshift.io/v1, Resource=brokertemplateinstances [Suite:openshift/conformance/parallel]", + + "[Top Level] [sig-api-machinery][Feature:ServerSideApply] Server-Side Apply should work for template.openshift.io/v1, Resource=templateinstances": "should work for template.openshift.io/v1, Resource=templateinstances [Suite:openshift/conformance/parallel]", + + "[Top Level] [sig-api-machinery][Feature:ServerSideApply] Server-Side Apply should work for template.openshift.io/v1, Resource=templates": "should work for template.openshift.io/v1, Resource=templates [Suite:openshift/conformance/parallel]", + + "[Top Level] [sig-api-machinery][Feature:ServerSideApply] Server-Side Apply should work for user.openshift.io/v1, Resource=groups": "should work for user.openshift.io/v1, Resource=groups [Suite:openshift/conformance/parallel]", + + "[Top Level] [sig-api-machinery][Feature:ServerSideApply] Server-Side Apply should work for user.openshift.io/v1, Resource=identities": "should work for user.openshift.io/v1, Resource=identities [Suite:openshift/conformance/parallel]", + + "[Top Level] [sig-api-machinery][Feature:ServerSideApply] Server-Side Apply should work for user.openshift.io/v1, Resource=users": "should work for user.openshift.io/v1, Resource=users [Suite:openshift/conformance/parallel]", + "[Top Level] [sig-apps] CronJob should delete failed finished jobs with limit of one job": "should delete failed finished jobs with limit of one job [Suite:openshift/conformance/parallel] [Suite:k8s]", "[Top Level] [sig-apps] CronJob should delete successful finished jobs with limit of one successful job": "should delete successful finished jobs with limit of one successful job [Suite:openshift/conformance/parallel] [Suite:k8s]", diff --git a/test/extended/util/client.go b/test/extended/util/client.go index 30f092a0a5cf..5308c91b1d93 100644 --- a/test/extended/util/client.go +++ b/test/extended/util/client.go @@ -108,7 +108,7 @@ func NewCLIWithFramework(kubeFramework *framework.Framework) *CLI { func NewCLI(project string) *CLI { cli := NewCLIWithoutNamespace(project) // create our own project - g.BeforeEach(cli.SetupProject) + g.BeforeEach(func() { _ = cli.SetupProject() }) return cli } @@ -198,7 +198,8 @@ func (c CLI) WithoutNamespace() *CLI { // SetupProject creates a new project and assign a random user to the project. // All resources will be then created within this project. -func (c *CLI) SetupProject() { +// Returns the name of the new project. +func (c *CLI) SetupProject() string { requiresTestStart() newNamespace := names.SimpleNameGenerator.GenerateName(fmt.Sprintf("e2e-test-%s-", c.kubeFramework.BaseName)) c.SetNamespace(newNamespace).ChangeUser(fmt.Sprintf("%s-user", newNamespace)) @@ -279,6 +280,7 @@ func (c *CLI) SetupProject() { WaitForNamespaceSCCAnnotations(c.ProjectClient().ProjectV1(), newNamespace) framework.Logf("Project %q has been fully provisioned.", newNamespace) + return newNamespace } func (c *CLI) setupSelfProvisionerRoleBinding() error {