From 1d32897eed070d318aaf63a47a871e7cc4a57ba2 Mon Sep 17 00:00:00 2001 From: nikhiljindal Date: Mon, 6 Feb 2017 17:22:37 -0800 Subject: [PATCH 1/4] Updating the registry to return whether the resource was immediately deleted --- federation/registry/cluster/registry.go | 2 +- pkg/master/thirdparty/thirdparty.go | 2 +- .../certificates/certificates/registry.go | 2 +- pkg/registry/core/configmap/registry.go | 3 +-- pkg/registry/core/endpoint/registry.go | 2 +- pkg/registry/core/namespace/registry.go | 2 +- .../core/namespace/storage/storage.go | 14 +++++------ pkg/registry/core/node/registry.go | 2 +- pkg/registry/core/pod/storage/eviction.go | 2 +- .../core/replicationcontroller/registry.go | 2 +- pkg/registry/core/secret/registry.go | 2 +- pkg/registry/core/service/registry.go | 2 +- pkg/registry/core/serviceaccount/registry.go | 2 +- .../extensions/deployment/registry.go | 2 +- .../extensions/replicaset/registry.go | 2 +- .../thirdpartyresourcedata/registry.go | 2 +- pkg/registry/rbac/clusterrole/registry.go | 2 +- .../rbac/clusterrolebinding/registry.go | 2 +- pkg/registry/rbac/role/registry.go | 2 +- pkg/registry/rbac/rolebinding/registry.go | 2 +- .../apiserver/pkg/endpoints/handlers/rest.go | 3 ++- .../pkg/registry/generic/registry/store.go | 25 +++++++++++-------- .../apiserver/pkg/registry/rest/rest.go | 9 ++++--- 23 files changed, 48 insertions(+), 42 deletions(-) diff --git a/federation/registry/cluster/registry.go b/federation/registry/cluster/registry.go index ee1e972db5d..0cb919ea09b 100644 --- a/federation/registry/cluster/registry.go +++ b/federation/registry/cluster/registry.go @@ -78,6 +78,6 @@ func (s *storage) UpdateCluster(ctx genericapirequest.Context, cluster *federati } func (s *storage) DeleteCluster(ctx genericapirequest.Context, name string) error { - _, err := s.Delete(ctx, name, nil) + _, _, err := s.Delete(ctx, name, nil) return err } diff --git a/pkg/master/thirdparty/thirdparty.go b/pkg/master/thirdparty/thirdparty.go index d9a4f19c6c6..6c614238e02 100644 --- a/pkg/master/thirdparty/thirdparty.go +++ b/pkg/master/thirdparty/thirdparty.go @@ -175,7 +175,7 @@ func (m *ThirdPartyResourceServer) removeAllThirdPartyResources(registry *thirdp } for ix := range list.Items { item := &list.Items[ix] - if _, err := registry.Delete(ctx, item.Name, nil); err != nil { + if _, _, err := registry.Delete(ctx, item.Name, nil); err != nil { return err } } diff --git a/pkg/registry/certificates/certificates/registry.go b/pkg/registry/certificates/certificates/registry.go index 1c78c71b582..05d3bb63a59 100644 --- a/pkg/registry/certificates/certificates/registry.go +++ b/pkg/registry/certificates/certificates/registry.go @@ -79,6 +79,6 @@ func (s *storage) GetCSR(ctx genericapirequest.Context, name string, options *me } func (s *storage) DeleteCSR(ctx genericapirequest.Context, name string) error { - _, err := s.Delete(ctx, name, nil) + _, _, err := s.Delete(ctx, name, nil) return err } diff --git a/pkg/registry/core/configmap/registry.go b/pkg/registry/core/configmap/registry.go index 581d40a9798..1d4c9c6f623 100644 --- a/pkg/registry/core/configmap/registry.go +++ b/pkg/registry/core/configmap/registry.go @@ -87,7 +87,6 @@ func (s *storage) UpdateConfigMap(ctx genericapirequest.Context, cfg *api.Config } func (s *storage) DeleteConfigMap(ctx genericapirequest.Context, name string) error { - _, err := s.Delete(ctx, name, nil) - + _, _, err := s.Delete(ctx, name, nil) return err } diff --git a/pkg/registry/core/endpoint/registry.go b/pkg/registry/core/endpoint/registry.go index 86d969e9b0e..8ac57b2a9e0 100644 --- a/pkg/registry/core/endpoint/registry.go +++ b/pkg/registry/core/endpoint/registry.go @@ -71,6 +71,6 @@ func (s *storage) UpdateEndpoints(ctx genericapirequest.Context, endpoints *api. } func (s *storage) DeleteEndpoints(ctx genericapirequest.Context, name string) error { - _, err := s.Delete(ctx, name, nil) + _, _, err := s.Delete(ctx, name, nil) return err } diff --git a/pkg/registry/core/namespace/registry.go b/pkg/registry/core/namespace/registry.go index 5cb6fcdf157..dc5d422df55 100644 --- a/pkg/registry/core/namespace/registry.go +++ b/pkg/registry/core/namespace/registry.go @@ -77,6 +77,6 @@ func (s *storage) UpdateNamespace(ctx genericapirequest.Context, namespace *api. } func (s *storage) DeleteNamespace(ctx genericapirequest.Context, namespaceID string) error { - _, err := s.Delete(ctx, namespaceID, nil) + _, _, err := s.Delete(ctx, namespaceID, nil) return err } diff --git a/pkg/registry/core/namespace/storage/storage.go b/pkg/registry/core/namespace/storage/storage.go index 2d6cdb2a5d6..de7508ecfe9 100644 --- a/pkg/registry/core/namespace/storage/storage.go +++ b/pkg/registry/core/namespace/storage/storage.go @@ -82,10 +82,10 @@ func NewREST(optsGetter generic.RESTOptionsGetter) (*REST, *StatusREST, *Finaliz } // Delete enforces life-cycle rules for namespace termination -func (r *REST) Delete(ctx genericapirequest.Context, name string, options *metav1.DeleteOptions) (runtime.Object, error) { +func (r *REST) Delete(ctx genericapirequest.Context, name string, options *metav1.DeleteOptions) (runtime.Object, bool, error) { nsObj, err := r.Get(ctx, name, &metav1.GetOptions{}) if err != nil { - return nil, err + return nil, false, err } namespace := nsObj.(*api.Namespace) @@ -105,7 +105,7 @@ func (r *REST) Delete(ctx genericapirequest.Context, name string, options *metav name, fmt.Errorf("Precondition failed: UID in precondition: %v, UID in object meta: %v", *options.Preconditions.UID, namespace.UID), ) - return nil, err + return nil, false, err } // upon first request to delete, we switch the phase to start namespace termination @@ -113,7 +113,7 @@ func (r *REST) Delete(ctx genericapirequest.Context, name string, options *metav if namespace.DeletionTimestamp.IsZero() { key, err := r.Store.KeyFunc(ctx, name) if err != nil { - return nil, err + return nil, false, err } preconditions := storage.Preconditions{UID: options.Preconditions.UID} @@ -159,16 +159,16 @@ func (r *REST) Delete(ctx genericapirequest.Context, name string, options *metav if _, ok := err.(*apierrors.StatusError); !ok { err = apierrors.NewInternalError(err) } - return nil, err + return nil, false, err } - return out, nil + return out, false, nil } // prior to final deletion, we must ensure that finalizers is empty if len(namespace.Spec.Finalizers) != 0 { err = apierrors.NewConflict(api.Resource("namespaces"), namespace.Name, fmt.Errorf("The system is ensuring all content is removed from this namespace. Upon completion, this namespace will automatically be purged by the system.")) - return nil, err + return nil, false, err } return r.Store.Delete(ctx, name, options) } diff --git a/pkg/registry/core/node/registry.go b/pkg/registry/core/node/registry.go index a6505f37d4f..2fab505dbbe 100644 --- a/pkg/registry/core/node/registry.go +++ b/pkg/registry/core/node/registry.go @@ -78,6 +78,6 @@ func (s *storage) GetNode(ctx genericapirequest.Context, name string, options *m } func (s *storage) DeleteNode(ctx genericapirequest.Context, name string) error { - _, err := s.Delete(ctx, name, nil) + _, _, err := s.Delete(ctx, name, nil) return err } diff --git a/pkg/registry/core/pod/storage/eviction.go b/pkg/registry/core/pod/storage/eviction.go index 52bc1e10a73..214680cce0a 100644 --- a/pkg/registry/core/pod/storage/eviction.go +++ b/pkg/registry/core/pod/storage/eviction.go @@ -136,7 +136,7 @@ func (r *EvictionREST) Create(ctx genericapirequest.Context, obj runtime.Object) // At this point there was either no PDB or we succeded in decrementing // Try the delete - _, err = r.store.Delete(ctx, eviction.Name, eviction.DeleteOptions) + _, _, err = r.store.Delete(ctx, eviction.Name, eviction.DeleteOptions) if err != nil { return nil, err } diff --git a/pkg/registry/core/replicationcontroller/registry.go b/pkg/registry/core/replicationcontroller/registry.go index dbb07524ece..02eff75fe7f 100644 --- a/pkg/registry/core/replicationcontroller/registry.go +++ b/pkg/registry/core/replicationcontroller/registry.go @@ -90,6 +90,6 @@ func (s *storage) UpdateController(ctx genericapirequest.Context, controller *ap } func (s *storage) DeleteController(ctx genericapirequest.Context, controllerID string) error { - _, err := s.Delete(ctx, controllerID, nil) + _, _, err := s.Delete(ctx, controllerID, nil) return err } diff --git a/pkg/registry/core/secret/registry.go b/pkg/registry/core/secret/registry.go index bdf529a39bd..21ecbc6d09f 100644 --- a/pkg/registry/core/secret/registry.go +++ b/pkg/registry/core/secret/registry.go @@ -77,6 +77,6 @@ func (s *storage) UpdateSecret(ctx genericapirequest.Context, secret *api.Secret } func (s *storage) DeleteSecret(ctx genericapirequest.Context, name string) error { - _, err := s.Delete(ctx, name, nil) + _, _, err := s.Delete(ctx, name, nil) return err } diff --git a/pkg/registry/core/service/registry.go b/pkg/registry/core/service/registry.go index 6f966c5f035..e37afb50074 100644 --- a/pkg/registry/core/service/registry.go +++ b/pkg/registry/core/service/registry.go @@ -74,7 +74,7 @@ func (s *storage) GetService(ctx genericapirequest.Context, name string, options } func (s *storage) DeleteService(ctx genericapirequest.Context, name string) error { - _, err := s.Delete(ctx, name, nil) + _, _, err := s.Delete(ctx, name, nil) return err } diff --git a/pkg/registry/core/serviceaccount/registry.go b/pkg/registry/core/serviceaccount/registry.go index 8246e1d3fa0..0a733d5a808 100644 --- a/pkg/registry/core/serviceaccount/registry.go +++ b/pkg/registry/core/serviceaccount/registry.go @@ -77,6 +77,6 @@ func (s *storage) UpdateServiceAccount(ctx genericapirequest.Context, serviceAcc } func (s *storage) DeleteServiceAccount(ctx genericapirequest.Context, name string) error { - _, err := s.Delete(ctx, name, nil) + _, _, err := s.Delete(ctx, name, nil) return err } diff --git a/pkg/registry/extensions/deployment/registry.go b/pkg/registry/extensions/deployment/registry.go index fb5160c5094..45d95224000 100644 --- a/pkg/registry/extensions/deployment/registry.go +++ b/pkg/registry/extensions/deployment/registry.go @@ -82,6 +82,6 @@ func (s *storage) UpdateDeployment(ctx genericapirequest.Context, deployment *ex } func (s *storage) DeleteDeployment(ctx genericapirequest.Context, deploymentID string) error { - _, err := s.Delete(ctx, deploymentID, nil) + _, _, err := s.Delete(ctx, deploymentID, nil) return err } diff --git a/pkg/registry/extensions/replicaset/registry.go b/pkg/registry/extensions/replicaset/registry.go index 781bfcb10df..e33cbd075b3 100644 --- a/pkg/registry/extensions/replicaset/registry.go +++ b/pkg/registry/extensions/replicaset/registry.go @@ -91,6 +91,6 @@ func (s *storage) UpdateReplicaSet(ctx genericapirequest.Context, replicaSet *ex } func (s *storage) DeleteReplicaSet(ctx genericapirequest.Context, replicaSetID string) error { - _, err := s.Delete(ctx, replicaSetID, nil) + _, _, err := s.Delete(ctx, replicaSetID, nil) return err } diff --git a/pkg/registry/extensions/thirdpartyresourcedata/registry.go b/pkg/registry/extensions/thirdpartyresourcedata/registry.go index b2ea5c1b812..c844d0a5d2e 100644 --- a/pkg/registry/extensions/thirdpartyresourcedata/registry.go +++ b/pkg/registry/extensions/thirdpartyresourcedata/registry.go @@ -78,6 +78,6 @@ func (s *storage) UpdateThirdPartyResourceData(ctx genericapirequest.Context, Th } func (s *storage) DeleteThirdPartyResourceData(ctx genericapirequest.Context, name string) error { - _, err := s.Delete(ctx, name, nil) + _, _, err := s.Delete(ctx, name, nil) return err } diff --git a/pkg/registry/rbac/clusterrole/registry.go b/pkg/registry/rbac/clusterrole/registry.go index 4c3cf731950..df6b5863878 100644 --- a/pkg/registry/rbac/clusterrole/registry.go +++ b/pkg/registry/rbac/clusterrole/registry.go @@ -79,7 +79,7 @@ func (s *storage) GetClusterRole(ctx genericapirequest.Context, name string, opt } func (s *storage) DeleteClusterRole(ctx genericapirequest.Context, name string) error { - _, err := s.Delete(ctx, name, nil) + _, _, err := s.Delete(ctx, name, nil) return err } diff --git a/pkg/registry/rbac/clusterrolebinding/registry.go b/pkg/registry/rbac/clusterrolebinding/registry.go index c07f384e840..1764f4a7386 100644 --- a/pkg/registry/rbac/clusterrolebinding/registry.go +++ b/pkg/registry/rbac/clusterrolebinding/registry.go @@ -79,7 +79,7 @@ func (s *storage) GetClusterRoleBinding(ctx genericapirequest.Context, name stri } func (s *storage) DeleteClusterRoleBinding(ctx genericapirequest.Context, name string) error { - _, err := s.Delete(ctx, name, nil) + _, _, err := s.Delete(ctx, name, nil) return err } diff --git a/pkg/registry/rbac/role/registry.go b/pkg/registry/rbac/role/registry.go index 4457aa46c95..9c3ea883b48 100644 --- a/pkg/registry/rbac/role/registry.go +++ b/pkg/registry/rbac/role/registry.go @@ -79,7 +79,7 @@ func (s *storage) GetRole(ctx genericapirequest.Context, name string, options *m } func (s *storage) DeleteRole(ctx genericapirequest.Context, name string) error { - _, err := s.Delete(ctx, name, nil) + _, _, err := s.Delete(ctx, name, nil) return err } diff --git a/pkg/registry/rbac/rolebinding/registry.go b/pkg/registry/rbac/rolebinding/registry.go index f826198d37f..c468cb71668 100644 --- a/pkg/registry/rbac/rolebinding/registry.go +++ b/pkg/registry/rbac/rolebinding/registry.go @@ -80,7 +80,7 @@ func (s *storage) GetRoleBinding(ctx genericapirequest.Context, name string, opt } func (s *storage) DeleteRoleBinding(ctx genericapirequest.Context, name string) error { - _, err := s.Delete(ctx, name, nil) + _, _, err := s.Delete(ctx, name, nil) return err } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go index 280729c6261..ac954e154f8 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go @@ -858,7 +858,8 @@ func DeleteResource(r rest.GracefulDeleter, allowsOptions bool, scope RequestSco trace.Step("About do delete object from database") result, err := finishRequest(timeout, func() (runtime.Object, error) { - return r.Delete(ctx, name, options) + obj, _, err := r.Delete(ctx, name, options) + return obj, err }) if err != nil { scope.err(err, res.ResponseWriter, req.Request) diff --git a/staging/src/k8s.io/apiserver/pkg/registry/generic/registry/store.go b/staging/src/k8s.io/apiserver/pkg/registry/generic/registry/store.go index 0f3e0fa5113..5977613ed45 100644 --- a/staging/src/k8s.io/apiserver/pkg/registry/generic/registry/store.go +++ b/staging/src/k8s.io/apiserver/pkg/registry/generic/registry/store.go @@ -758,15 +758,15 @@ func (e *Store) updateForGracefulDeletionAndFinalizers(ctx genericapirequest.Con } // Delete removes the item from storage. -func (e *Store) Delete(ctx genericapirequest.Context, name string, options *metav1.DeleteOptions) (runtime.Object, error) { +func (e *Store) Delete(ctx genericapirequest.Context, name string, options *metav1.DeleteOptions) (runtime.Object, bool, error) { key, err := e.KeyFunc(ctx, name) if err != nil { - return nil, err + return nil, false, err } obj := e.NewFunc() if err := e.Storage.Get(ctx, key, "", obj, false); err != nil { - return nil, storeerr.InterpretDeleteError(err, e.QualifiedResource, name) + return nil, false, storeerr.InterpretDeleteError(err, e.QualifiedResource, name) } // support older consumers of delete by treating "nil" as delete immediately if options == nil { @@ -778,16 +778,17 @@ func (e *Store) Delete(ctx genericapirequest.Context, name string, options *meta } graceful, pendingGraceful, err := rest.BeforeDelete(e.DeleteStrategy, ctx, obj, options) if err != nil { - return nil, err + return nil, false, err } // this means finalizers cannot be updated via DeleteOptions if a deletion is already pending if pendingGraceful { - return e.finalizeDelete(obj, false) + out, err := e.finalizeDelete(obj, false) + return out, false, err } // check if obj has pending finalizers accessor, err := meta.Accessor(obj) if err != nil { - return nil, kubeerr.NewInternalError(err) + return nil, false, kubeerr.NewInternalError(err) } pendingFinalizers := len(accessor.GetFinalizers()) != 0 var ignoreNotFound bool @@ -810,7 +811,7 @@ func (e *Store) Delete(ctx genericapirequest.Context, name string, options *meta } // !deleteImmediately covers all cases where err != nil. We keep both to be future-proof. if !deleteImmediately || err != nil { - return out, err + return out, false, err } // delete immediately, or no graceful deletion supported @@ -822,11 +823,13 @@ func (e *Store) Delete(ctx genericapirequest.Context, name string, options *meta if storage.IsNotFound(err) && ignoreNotFound && lastExisting != nil { // The lastExisting object may not be the last state of the object // before its deletion, but it's the best approximation. - return e.finalizeDelete(lastExisting, true) + out, err := e.finalizeDelete(lastExisting, true) + return out, true, err } - return nil, storeerr.InterpretDeleteError(err, e.QualifiedResource, name) + return nil, false, storeerr.InterpretDeleteError(err, e.QualifiedResource, name) } - return e.finalizeDelete(out, true) + out, err = e.finalizeDelete(out, true) + return out, true, err } // DeleteCollection removes all items returned by List with a given ListOptions from storage. @@ -890,7 +893,7 @@ func (e *Store) DeleteCollection(ctx genericapirequest.Context, options *metav1. errs <- err return } - if _, err := e.Delete(ctx, accessor.GetName(), options); err != nil && !kubeerr.IsNotFound(err) { + if _, _, err := e.Delete(ctx, accessor.GetName(), options); err != nil && !kubeerr.IsNotFound(err) { glog.V(4).Infof("Delete %s in DeleteCollection failed: %v", accessor.GetName(), err) errs <- err return diff --git a/staging/src/k8s.io/apiserver/pkg/registry/rest/rest.go b/staging/src/k8s.io/apiserver/pkg/registry/rest/rest.go index 9af285cecdd..cc6613f9d6c 100644 --- a/staging/src/k8s.io/apiserver/pkg/registry/rest/rest.go +++ b/staging/src/k8s.io/apiserver/pkg/registry/rest/rest.go @@ -136,7 +136,9 @@ type GracefulDeleter interface { // returned error value err when the specified resource is not found. // Delete *may* return the object that was deleted, or a status object indicating additional // information about deletion. - Delete(ctx genericapirequest.Context, name string, options *metav1.DeleteOptions) (runtime.Object, error) + // It also returns a boolean which is set to true if the resource was instantly + // deleted or false if it will be deleted asynchronously. + Delete(ctx genericapirequest.Context, name string, options *metav1.DeleteOptions) (runtime.Object, bool, error) } // GracefulDeleteAdapter adapts the Deleter interface to GracefulDeleter @@ -145,8 +147,9 @@ type GracefulDeleteAdapter struct { } // Delete implements RESTGracefulDeleter in terms of Deleter -func (w GracefulDeleteAdapter) Delete(ctx genericapirequest.Context, name string, options *metav1.DeleteOptions) (runtime.Object, error) { - return w.Deleter.Delete(ctx, name) +func (w GracefulDeleteAdapter) Delete(ctx genericapirequest.Context, name string, options *metav1.DeleteOptions) (runtime.Object, bool, error) { + obj, err := w.Deleter.Delete(ctx, name) + return obj, true, err } // CollectionDeleter is an object that can delete a collection From 4ee81eb037794500b08d7e882a327208e6dc2b56 Mon Sep 17 00:00:00 2001 From: nikhiljindal Date: Tue, 7 Feb 2017 18:58:16 -0800 Subject: [PATCH 2/4] Updating tests for deleted boolean from Delete() --- .../core/namespace/storage/storage_test.go | 4 +- pkg/registry/core/pod/storage/storage_test.go | 4 +- pkg/registry/registrytest/etcd.go | 2 +- .../apiserver/pkg/endpoints/apiserver_test.go | 9 +-- .../registry/generic/registry/store_test.go | 21 +++++-- .../pkg/registry/rest/resttest/resttest.go | 58 ++++++++++++++----- 6 files changed, 69 insertions(+), 29 deletions(-) diff --git a/pkg/registry/core/namespace/storage/storage_test.go b/pkg/registry/core/namespace/storage/storage_test.go index a6c763d7497..f91fb03a276 100644 --- a/pkg/registry/core/namespace/storage/storage_test.go +++ b/pkg/registry/core/namespace/storage/storage_test.go @@ -157,7 +157,7 @@ func TestDeleteNamespaceWithIncompleteFinalizers(t *testing.T) { if err := storage.Storage.Create(ctx, key, namespace, nil, 0); err != nil { t.Fatalf("unexpected error: %v", err) } - if _, err := storage.Delete(ctx, "foo", nil); err == nil { + if _, _, err := storage.Delete(ctx, "foo", nil); err == nil { t.Errorf("unexpected error: %v", err) } } @@ -182,7 +182,7 @@ func TestDeleteNamespaceWithCompleteFinalizers(t *testing.T) { if err := storage.Storage.Create(ctx, key, namespace, nil, 0); err != nil { t.Fatalf("unexpected error: %v", err) } - if _, err := storage.Delete(ctx, "foo", nil); err != nil { + if _, _, err := storage.Delete(ctx, "foo", nil); err != nil { t.Errorf("unexpected error: %v", err) } } diff --git a/pkg/registry/core/pod/storage/storage_test.go b/pkg/registry/core/pod/storage/storage_test.go index 9c0a27ac2e5..093fbfbfdcc 100644 --- a/pkg/registry/core/pod/storage/storage_test.go +++ b/pkg/registry/core/pod/storage/storage_test.go @@ -176,7 +176,7 @@ func TestIgnoreDeleteNotFound(t *testing.T) { defer registry.Store.DestroyFunc() // should fail if pod A is not created yet. - _, err := registry.Delete(testContext, pod.Name, nil) + _, _, err := registry.Delete(testContext, pod.Name, nil) if !errors.IsNotFound(err) { t.Errorf("Unexpected error: %v", err) } @@ -191,7 +191,7 @@ func TestIgnoreDeleteNotFound(t *testing.T) { // registry shouldn't get any error since we ignore the NotFound error. zero := int64(0) opt := &metav1.DeleteOptions{GracePeriodSeconds: &zero} - obj, err := registry.Delete(testContext, pod.Name, opt) + obj, _, err := registry.Delete(testContext, pod.Name, opt) if err != nil { t.Fatalf("Unexpected error: %v", err) } diff --git a/pkg/registry/registrytest/etcd.go b/pkg/registry/registrytest/etcd.go index f4e31d9ba3c..13abeac2baa 100644 --- a/pkg/registry/registrytest/etcd.go +++ b/pkg/registry/registrytest/etcd.go @@ -221,7 +221,7 @@ func (t *Tester) emitObject(obj runtime.Object, action string) error { if err != nil { return err } - _, err = t.storage.Delete(ctx, accessor.GetName(), nil) + _, _, err = t.storage.Delete(ctx, accessor.GetName(), nil) default: err = fmt.Errorf("unexpected action: %v", action) } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go index 55571975068..397f0c43230 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go @@ -457,19 +457,19 @@ func (storage *SimpleRESTStorage) checkContext(ctx request.Context) { storage.actualNamespace, storage.namespacePresent = request.NamespaceFrom(ctx) } -func (storage *SimpleRESTStorage) Delete(ctx request.Context, id string, options *metav1.DeleteOptions) (runtime.Object, error) { +func (storage *SimpleRESTStorage) Delete(ctx request.Context, id string, options *metav1.DeleteOptions) (runtime.Object, bool, error) { storage.checkContext(ctx) storage.deleted = id storage.deleteOptions = options if err := storage.errors["delete"]; err != nil { - return nil, err + return nil, false, err } var obj runtime.Object = &metav1.Status{Status: metav1.StatusSuccess} var err error if storage.injectedFunction != nil { obj, err = storage.injectedFunction(&genericapitesting.Simple{ObjectMeta: metav1.ObjectMeta{Name: id}}) } - return obj, err + return obj, true, err } func (storage *SimpleRESTStorage) New() runtime.Object { @@ -605,7 +605,8 @@ type LegacyRESTStorage struct { } func (storage LegacyRESTStorage) Delete(ctx request.Context, id string) (runtime.Object, error) { - return storage.SimpleRESTStorage.Delete(ctx, id, nil) + obj, _, err := storage.SimpleRESTStorage.Delete(ctx, id, nil) + return obj, err } type MetadataRESTStorage struct { diff --git a/staging/src/k8s.io/apiserver/pkg/registry/generic/registry/store_test.go b/staging/src/k8s.io/apiserver/pkg/registry/generic/registry/store_test.go index 005917cff45..5fa68c67776 100644 --- a/staging/src/k8s.io/apiserver/pkg/registry/generic/registry/store_test.go +++ b/staging/src/k8s.io/apiserver/pkg/registry/generic/registry/store_test.go @@ -335,7 +335,7 @@ func TestStoreCreate(t *testing.T) { // now delete pod with graceful period set delOpts := &metav1.DeleteOptions{GracePeriodSeconds: &gracefulPeriod} - _, err = registry.Delete(testContext, podA.Name, delOpts) + _, _, err = registry.Delete(testContext, podA.Name, delOpts) if err != nil { t.Fatalf("Failed to delete pod gracefully. Unexpected error: %v", err) } @@ -609,7 +609,7 @@ func TestStoreDelete(t *testing.T) { defer destroyFunc() // test failure condition - _, err := registry.Delete(testContext, podA.Name, nil) + _, _, err := registry.Delete(testContext, podA.Name, nil) if !errors.IsNotFound(err) { t.Errorf("Unexpected error: %v", err) } @@ -621,10 +621,13 @@ func TestStoreDelete(t *testing.T) { } // delete object - _, err = registry.Delete(testContext, podA.Name, nil) + _, wasDeleted, err := registry.Delete(testContext, podA.Name, nil) if err != nil { t.Errorf("Unexpected error: %v", err) } + if !wasDeleted { + t.Errorf("unexpected, pod %s should have been deleted immediately", podA.Name) + } // try to get a item which should be deleted _, err = registry.Get(testContext, podA.Name, &metav1.GetOptions{}) @@ -690,10 +693,13 @@ func TestGracefulStoreHandleFinalizers(t *testing.T) { } // delete the pod with grace period=0, the pod should still exist because it has a finalizer - _, err = registry.Delete(testContext, podWithFinalizer.Name, metav1.NewDeleteOptions(0)) + _, wasDeleted, err := registry.Delete(testContext, podWithFinalizer.Name, metav1.NewDeleteOptions(0)) if err != nil { t.Fatalf("Unexpected error: %v", err) } + if wasDeleted { + t.Errorf("unexpected, pod %s should not have been deleted immediately", podWithFinalizer.Name) + } _, err = registry.Get(testContext, podWithFinalizer.Name, &metav1.GetOptions{}) if err != nil { t.Fatalf("Unexpected error: %v", err) @@ -747,10 +753,13 @@ func TestNonGracefulStoreHandleFinalizers(t *testing.T) { } // delete object with nil delete options doesn't delete the object - _, err = registry.Delete(testContext, podWithFinalizer.Name, nil) + _, wasDeleted, err := registry.Delete(testContext, podWithFinalizer.Name, nil) if err != nil { t.Errorf("Unexpected error: %v", err) } + if wasDeleted { + t.Errorf("unexpected, pod %s should not have been deleted immediately", podWithFinalizer.Name) + } // the object should still exist obj, err := registry.Get(testContext, podWithFinalizer.Name, &metav1.GetOptions{}) @@ -1043,7 +1052,7 @@ func TestStoreDeleteWithOrphanDependents(t *testing.T) { if err != nil { t.Fatalf("Unexpected error: %v", err) } - _, err = registry.Delete(testContext, tc.pod.Name, tc.options) + _, _, err = registry.Delete(testContext, tc.pod.Name, tc.options) if err != nil { t.Fatalf("Unexpected error: %v", err) } diff --git a/staging/src/k8s.io/apiserver/pkg/registry/rest/resttest/resttest.go b/staging/src/k8s.io/apiserver/pkg/registry/rest/resttest/resttest.go index 8fdb59886c4..3d680e4dc66 100644 --- a/staging/src/k8s.io/apiserver/pkg/registry/rest/resttest/resttest.go +++ b/staging/src/k8s.io/apiserver/pkg/registry/rest/resttest/resttest.go @@ -236,7 +236,7 @@ func (t *Tester) delete(ctx genericapirequest.Context, obj runtime.Object) error if !ok { return fmt.Errorf("Expected deleting storage, got %v", t.storage) } - _, err = deleter.Delete(ctx, objectMeta.Name, nil) + _, _, err = deleter.Delete(ctx, objectMeta.Name, nil) return err } @@ -765,10 +765,13 @@ func (t *Tester) testDeleteNoGraceful(obj runtime.Object, createFn CreateFunc, g t.Errorf("unexpected error: %v", err) } objectMeta := t.getObjectMetaOrFail(foo) - obj, err := t.storage.(rest.GracefulDeleter).Delete(ctx, objectMeta.Name, metav1.NewDeleteOptions(10)) + obj, wasDeleted, err := t.storage.(rest.GracefulDeleter).Delete(ctx, objectMeta.Name, metav1.NewDeleteOptions(10)) if err != nil { t.Errorf("unexpected error: %v", err) } + if !wasDeleted { + t.Errorf("unexpected, object %s should have been deleted immediately", objectMeta.Name) + } if !t.returnDeletedObject { if status, ok := obj.(*metav1.Status); !ok { t.Errorf("expected status of delete, got %v", status) @@ -786,7 +789,7 @@ func (t *Tester) testDeleteNoGraceful(obj runtime.Object, createFn CreateFunc, g func (t *Tester) testDeleteNonExist(obj runtime.Object) { objectMeta := t.getObjectMetaOrFail(obj) - _, err := t.storage.(rest.GracefulDeleter).Delete(t.TestContext(), objectMeta.Name, nil) + _, _, err := t.storage.(rest.GracefulDeleter).Delete(t.TestContext(), objectMeta.Name, nil) if err == nil || !errors.IsNotFound(err) { t.Errorf("unexpected error: %v", err) } @@ -805,12 +808,12 @@ func (t *Tester) testDeleteWithUID(obj runtime.Object, createFn CreateFunc, getF if err := createFn(ctx, foo); err != nil { t.Errorf("unexpected error: %v", err) } - obj, err := t.storage.(rest.GracefulDeleter).Delete(ctx, objectMeta.Name, metav1.NewPreconditionDeleteOptions("UID1111")) + obj, _, err := t.storage.(rest.GracefulDeleter).Delete(ctx, objectMeta.Name, metav1.NewPreconditionDeleteOptions("UID1111")) if err == nil || !errors.IsConflict(err) { t.Errorf("unexpected error: %v", err) } - obj, err = t.storage.(rest.GracefulDeleter).Delete(ctx, objectMeta.Name, metav1.NewPreconditionDeleteOptions("UID0000")) + obj, _, err = t.storage.(rest.GracefulDeleter).Delete(ctx, objectMeta.Name, metav1.NewPreconditionDeleteOptions("UID0000")) if err != nil { t.Errorf("unexpected error: %v", err) } @@ -842,10 +845,13 @@ func (t *Tester) testDeleteGracefulHasDefault(obj runtime.Object, createFn Creat } objectMeta := t.getObjectMetaOrFail(foo) generation := objectMeta.Generation - _, err := t.storage.(rest.GracefulDeleter).Delete(ctx, objectMeta.Name, &metav1.DeleteOptions{}) + _, wasDeleted, err := t.storage.(rest.GracefulDeleter).Delete(ctx, objectMeta.Name, &metav1.DeleteOptions{}) if err != nil { t.Errorf("unexpected error: %v", err) } + if wasDeleted { + t.Errorf("unexpected, object %s should not have been deleted immediately", objectMeta.Name) + } if _, err := getFn(ctx, foo); err != nil { t.Fatalf("did not gracefully delete resource: %v", err) } @@ -873,10 +879,13 @@ func (t *Tester) testDeleteGracefulWithValue(obj runtime.Object, createFn Create } objectMeta := t.getObjectMetaOrFail(foo) generation := objectMeta.Generation - _, err := t.storage.(rest.GracefulDeleter).Delete(ctx, objectMeta.Name, metav1.NewDeleteOptions(expectedGrace+2)) + _, wasDeleted, err := t.storage.(rest.GracefulDeleter).Delete(ctx, objectMeta.Name, metav1.NewDeleteOptions(expectedGrace+2)) if err != nil { t.Errorf("unexpected error: %v", err) } + if wasDeleted { + t.Errorf("unexpected, object %s should not have been deleted immediately", objectMeta.Name) + } if _, err := getFn(ctx, foo); err != nil { t.Fatalf("did not gracefully delete resource: %v", err) } @@ -904,19 +913,25 @@ func (t *Tester) testDeleteGracefulExtend(obj runtime.Object, createFn CreateFun } objectMeta := t.getObjectMetaOrFail(foo) generation := objectMeta.Generation - _, err := t.storage.(rest.GracefulDeleter).Delete(ctx, objectMeta.Name, metav1.NewDeleteOptions(expectedGrace)) + _, wasDeleted, err := t.storage.(rest.GracefulDeleter).Delete(ctx, objectMeta.Name, metav1.NewDeleteOptions(expectedGrace)) if err != nil { t.Errorf("unexpected error: %v", err) } + if wasDeleted { + t.Errorf("unexpected, object %s should not have been deleted immediately", objectMeta.Name) + } if _, err := getFn(ctx, foo); err != nil { t.Fatalf("did not gracefully delete resource: %v", err) } // second delete duration is ignored - _, err = t.storage.(rest.GracefulDeleter).Delete(ctx, objectMeta.Name, metav1.NewDeleteOptions(expectedGrace+2)) + _, wasDeleted, err = t.storage.(rest.GracefulDeleter).Delete(ctx, objectMeta.Name, metav1.NewDeleteOptions(expectedGrace+2)) if err != nil { t.Errorf("unexpected error: %v", err) } + if wasDeleted { + t.Errorf("unexpected, object %s should not have been deleted immediately", objectMeta.Name) + } object, err := t.storage.(rest.Getter).Get(ctx, objectMeta.Name, &metav1.GetOptions{}) if err != nil { t.Errorf("unexpected error, object should exist: %v", err) @@ -940,19 +955,25 @@ func (t *Tester) testDeleteGracefulImmediate(obj runtime.Object, createFn Create } objectMeta := t.getObjectMetaOrFail(foo) generation := objectMeta.Generation - _, err := t.storage.(rest.GracefulDeleter).Delete(ctx, objectMeta.Name, metav1.NewDeleteOptions(expectedGrace)) + _, wasDeleted, err := t.storage.(rest.GracefulDeleter).Delete(ctx, objectMeta.Name, metav1.NewDeleteOptions(expectedGrace)) if err != nil { t.Errorf("unexpected error: %v", err) } + if wasDeleted { + t.Errorf("unexpected, object %s should not have been deleted immediately", objectMeta.Name) + } if _, err := getFn(ctx, foo); err != nil { t.Fatalf("did not gracefully delete resource: %v", err) } // second delete is immediate, resource is deleted - out, err := t.storage.(rest.GracefulDeleter).Delete(ctx, objectMeta.Name, metav1.NewDeleteOptions(0)) + out, wasDeleted, err := t.storage.(rest.GracefulDeleter).Delete(ctx, objectMeta.Name, metav1.NewDeleteOptions(0)) if err != nil { t.Errorf("unexpected error: %v", err) } + if wasDeleted != true { + t.Errorf("unexpected, object %s should have been deleted immediately", objectMeta.Name) + } _, err = t.storage.(rest.Getter).Get(ctx, objectMeta.Name, &metav1.GetOptions{}) if !errors.IsNotFound(err) { t.Errorf("unexpected error, object should be deleted immediately: %v", err) @@ -976,10 +997,13 @@ func (t *Tester) testDeleteGracefulUsesZeroOnNil(obj runtime.Object, createFn Cr t.Errorf("unexpected error: %v", err) } objectMeta := t.getObjectMetaOrFail(foo) - _, err := t.storage.(rest.GracefulDeleter).Delete(ctx, objectMeta.Name, nil) + _, wasDeleted, err := t.storage.(rest.GracefulDeleter).Delete(ctx, objectMeta.Name, nil) if err != nil { t.Errorf("unexpected error: %v", err) } + if !wasDeleted { + t.Errorf("unexpected, object %s should have been deleted immediately", objectMeta.Name) + } if _, err := t.storage.(rest.Getter).Get(ctx, objectMeta.Name, &metav1.GetOptions{}); !errors.IsNotFound(err) { t.Errorf("unexpected error, object should not exist: %v", err) } @@ -999,10 +1023,13 @@ func (t *Tester) testDeleteGracefulShorten(obj runtime.Object, createFn CreateFu bigGrace = 2 * expectedGrace } objectMeta := t.getObjectMetaOrFail(foo) - _, err := t.storage.(rest.GracefulDeleter).Delete(ctx, objectMeta.Name, metav1.NewDeleteOptions(bigGrace)) + _, wasDeleted, err := t.storage.(rest.GracefulDeleter).Delete(ctx, objectMeta.Name, metav1.NewDeleteOptions(bigGrace)) if err != nil { t.Errorf("unexpected error: %v", err) } + if wasDeleted { + t.Errorf("unexpected, object %s should not have been deleted immediately", objectMeta.Name) + } object, err := getFn(ctx, foo) if err != nil { t.Fatalf("did not gracefully delete resource: %v", err) @@ -1011,10 +1038,13 @@ func (t *Tester) testDeleteGracefulShorten(obj runtime.Object, createFn CreateFu deletionTimestamp := *objectMeta.DeletionTimestamp // second delete duration is ignored - _, err = t.storage.(rest.GracefulDeleter).Delete(ctx, objectMeta.Name, metav1.NewDeleteOptions(expectedGrace)) + _, wasDeleted, err = t.storage.(rest.GracefulDeleter).Delete(ctx, objectMeta.Name, metav1.NewDeleteOptions(expectedGrace)) if err != nil { t.Errorf("unexpected error: %v", err) } + if wasDeleted { + t.Errorf("unexpected, object %s should not have been deleted immediately", objectMeta.Name) + } object, err = t.storage.(rest.Getter).Get(ctx, objectMeta.Name, &metav1.GetOptions{}) if err != nil { t.Errorf("unexpected error, object should exist: %v", err) From 2dc404fcb77826b74f4f6039db5a412f31c1d7ac Mon Sep 17 00:00:00 2001 From: nikhiljindal Date: Wed, 8 Feb 2017 15:25:53 -0800 Subject: [PATCH 3/4] Update REST Handler to return 202 for cascading deletion --- .../apiserver/pkg/endpoints/handlers/rest.go | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go index ac954e154f8..a129b0c667a 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go @@ -857,8 +857,10 @@ func DeleteResource(r rest.GracefulDeleter, allowsOptions bool, scope RequestSco } trace.Step("About do delete object from database") + wasDeleted := true result, err := finishRequest(timeout, func() (runtime.Object, error) { - obj, _, err := r.Delete(ctx, name, options) + obj, deleted, err := r.Delete(ctx, name, options) + wasDeleted = deleted return obj, err }) if err != nil { @@ -867,12 +869,22 @@ func DeleteResource(r rest.GracefulDeleter, allowsOptions bool, scope RequestSco } trace.Step("Object deleted from database") + status := http.StatusOK + // Return http.StatusAccepted if the resource was not deleted immediately and + // user requested cascading deletion by setting OrphanDependents=false. + // Note: We want to do this always if resource was not deleted immediately, but + // that will break existing clients. + // Other cases where resource is not instantly deleted are: namespace deletion + // and pod graceful deletion. + if !wasDeleted && options.OrphanDependents != nil && *options.OrphanDependents == false { + status = http.StatusAccepted + } // if the rest.Deleter returns a nil object, fill out a status. Callers may return a valid // object with the response. if result == nil { result = &metav1.Status{ Status: metav1.StatusSuccess, - Code: http.StatusOK, + Code: int32(status), Details: &metav1.StatusDetails{ Name: name, Kind: scope.Kind.Kind, @@ -887,7 +899,7 @@ func DeleteResource(r rest.GracefulDeleter, allowsOptions bool, scope RequestSco } } } - responsewriters.WriteObject(http.StatusOK, scope.Kind.GroupVersion(), scope.Serializer, result, w, req.Request) + responsewriters.WriteObject(status, scope.Kind.GroupVersion(), scope.Serializer, result, w, req.Request) } } From 4895aeef3759251369e75de87cffa3df7d166911 Mon Sep 17 00:00:00 2001 From: nikhiljindal Date: Thu, 23 Feb 2017 16:28:10 -0800 Subject: [PATCH 4/4] Adding an integration test for testing 202 status code from apiserver --- test/integration/apiserver/apiserver_test.go | 158 +++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 test/integration/apiserver/apiserver_test.go diff --git a/test/integration/apiserver/apiserver_test.go b/test/integration/apiserver/apiserver_test.go new file mode 100644 index 00000000000..065e12e796a --- /dev/null +++ b/test/integration/apiserver/apiserver_test.go @@ -0,0 +1,158 @@ +// +build integration,!no-etcd + +/* +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 apiserver + +import ( + "bytes" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + + "github.com/golang/glog" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + restclient "k8s.io/client-go/rest" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/testapi" + "k8s.io/kubernetes/pkg/api/v1" + "k8s.io/kubernetes/pkg/apis/extensions/v1beta1" + "k8s.io/kubernetes/pkg/client/clientset_generated/clientset" + "k8s.io/kubernetes/test/integration/framework" +) + +func setup(t *testing.T) (*httptest.Server, clientset.Interface) { + masterConfig := framework.NewIntegrationTestMasterConfig() + masterConfig.EnableCoreControllers = false + _, s := framework.RunAMaster(masterConfig) + + clientSet, err := clientset.NewForConfig(&restclient.Config{Host: s.URL}) + if err != nil { + t.Fatalf("Error in create clientset: %v", err) + } + return s, clientSet +} + +func verifyStatusCode(t *testing.T, verb, URL, body string, expectedStatusCode int) { + // We dont use the typed Go client to send this request to be able to verify the response status code. + bodyBytes := bytes.NewReader([]byte(body)) + req, err := http.NewRequest(verb, URL, bodyBytes) + if err != nil { + t.Fatalf("unexpected error: %v in sending req with verb: %s, URL: %s and body: %s", err, verb, URL, body) + } + transport := http.DefaultTransport + glog.Infof("Sending request: %v", req) + resp, err := transport.RoundTrip(req) + if err != nil { + t.Fatalf("unexpected error: %v in req: %v", err, req) + } + defer resp.Body.Close() + b, _ := ioutil.ReadAll(resp.Body) + if resp.StatusCode != expectedStatusCode { + t.Errorf("Expected status %v, but got %v", expectedStatusCode, resp.StatusCode) + t.Errorf("Body: %v", string(b)) + } +} + +func path(resource, namespace, name string) string { + return testapi.Extensions.ResourcePath(resource, namespace, name) +} + +func newRS(namespace string) *v1beta1.ReplicaSet { + return &v1beta1.ReplicaSet{ + TypeMeta: metav1.TypeMeta{ + Kind: "ReplicaSet", + APIVersion: "extensions/v1beta1", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + GenerateName: "apiserver-test", + }, + Spec: v1beta1.ReplicaSetSpec{ + Template: v1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"name": "test"}, + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: "fake-name", + Image: "fakeimage", + }, + }, + }, + }, + }, + } +} + +var cascDel string = ` +{ + "kind": "DeleteOptions", + "apiVersion": "` + api.Registry.GroupOrDie(api.GroupName).GroupVersion.String() + `", + "orphanDependents": false +} +` + +// Tests that the apiserver returns 202 status code as expected. +func Test202StatusCode(t *testing.T) { + s, clientSet := setup(t) + defer s.Close() + + ns := framework.CreateTestingNamespace("status-code", s, t) + defer framework.DeleteTestingNamespace(ns, s, t) + + rsClient := clientSet.Extensions().ReplicaSets(ns.Name) + + // 1. Create the resource without any finalizer and then delete it without setting DeleteOptions. + // Verify that server returns 200 in this case. + rs, err := rsClient.Create(newRS(ns.Name)) + if err != nil { + t.Fatalf("Failed to create rs: %v", err) + } + verifyStatusCode(t, "DELETE", s.URL+path("replicasets", ns.Name, rs.Name), "", 200) + + // 2. Create the resource with a finalizer so that the resource is not immediately deleted and then delete it without setting DeleteOptions. + // Verify that the apiserver still returns 200 since DeleteOptions.OrphanDependents is not set. + rs = newRS(ns.Name) + rs.ObjectMeta.Finalizers = []string{"kube.io/dummy-finalizer"} + rs, err = rsClient.Create(rs) + if err != nil { + t.Fatalf("Failed to create rs: %v", err) + } + verifyStatusCode(t, "DELETE", s.URL+path("replicasets", ns.Name, rs.Name), "", 200) + + // 3. Create the resource and then delete it with DeleteOptions.OrphanDependents=false. + // Verify that the server still returns 200 since the resource is immediately deleted. + rs = newRS(ns.Name) + rs, err = rsClient.Create(rs) + if err != nil { + t.Fatalf("Failed to create rs: %v", err) + } + verifyStatusCode(t, "DELETE", s.URL+path("replicasets", ns.Name, rs.Name), cascDel, 200) + + // 4. Create the resource with a finalizer so that the resource is not immediately deleted and then delete it with DeleteOptions.OrphanDependents=false. + // Verify that the server returns 202 in this case. + rs = newRS(ns.Name) + rs.ObjectMeta.Finalizers = []string{"kube.io/dummy-finalizer"} + rs, err = rsClient.Create(rs) + if err != nil { + t.Fatalf("Failed to create rs: %v", err) + } + verifyStatusCode(t, "DELETE", s.URL+path("replicasets", ns.Name, rs.Name), cascDel, 202) +}