Graceful deletion of resources

This commit adds support to core resources to enable deferred deletion
of resources.  Clients may optionally specify a time period after which
resources must be deleted via an object sent with their DELETE. That
object may define an optional grace period in seconds, or allow the
default "preferred" value for a resource to be used. Once the object
is marked as pending deletion, the deletionTimestamp field will be set
and an etcd TTL will be in place.

Clients should assume resources that have deletionTimestamp set will
be deleted at some point in the future.  Other changes will come later
to enable graceful deletion on a per resource basis.
This commit is contained in:
Clayton Coleman
2015-03-04 22:34:31 -05:00
parent 6f6485909e
commit 428d2263e5
39 changed files with 581 additions and 94 deletions

View File

@@ -141,11 +141,25 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage RESTStorage
lister, isLister := storage.(RESTLister)
getter, isGetter := storage.(RESTGetter)
deleter, isDeleter := storage.(RESTDeleter)
gracefulDeleter, isGracefulDeleter := storage.(RESTGracefulDeleter)
updater, isUpdater := storage.(RESTUpdater)
patcher, isPatcher := storage.(RESTPatcher)
_, isWatcher := storage.(ResourceWatcher)
_, isRedirector := storage.(Redirector)
var versionedDeleterObject runtime.Object
switch {
case isGracefulDeleter:
object, err := a.group.Creater.New(a.group.Version, "DeleteOptions")
if err != nil {
return err
}
versionedDeleterObject = object
isDeleter = true
case isDeleter:
gracefulDeleter = GracefulDeleteAdapter{deleter}
}
var ctxFn ContextFunc
ctxFn = func(req *restful.Request) api.Context {
if ctx, ok := context.Get(req.Request); ok {
@@ -314,10 +328,13 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage RESTStorage
addParams(route, action.Params)
ws.Route(route)
case "DELETE": // Delete a resource.
route := ws.DELETE(action.Path).To(DeleteResource(deleter, ctxFn, action.Namer, mapping.Codec, resource, kind, admit)).
route := ws.DELETE(action.Path).To(DeleteResource(gracefulDeleter, isGracefulDeleter, ctxFn, action.Namer, mapping.Codec, resource, kind, admit)).
Filter(m).
Doc("delete a " + kind).
Operation("delete" + kind)
if isGracefulDeleter {
route.Reads(versionedDeleterObject)
}
addParams(route, action.Params)
ws.Route(route)
case "WATCH": // Watch a resource.

View File

@@ -97,8 +97,7 @@ func init() {
&api.Status{})
// "version" version
// TODO: Use versioned api objects?
api.Scheme.AddKnownTypes(testVersion, &Simple{}, &SimpleList{},
&api.Status{})
api.Scheme.AddKnownTypes(testVersion, &Simple{}, &SimpleList{}, &api.DeleteOptions{}, &api.Status{})
nsMapper := newMapper()
legacyNsMapper := newMapper()
@@ -204,13 +203,16 @@ func TestSimpleSetupRight(t *testing.T) {
}
type SimpleRESTStorage struct {
errors map[string]error
list []Simple
item Simple
deleted string
errors map[string]error
list []Simple
item Simple
updated *Simple
created *Simple
deleted string
deleteOptions *api.DeleteOptions
actualNamespace string
namespacePresent bool
@@ -248,9 +250,10 @@ func (storage *SimpleRESTStorage) checkContext(ctx api.Context) {
storage.actualNamespace, storage.namespacePresent = api.NamespaceFrom(ctx)
}
func (storage *SimpleRESTStorage) Delete(ctx api.Context, id string) (runtime.Object, error) {
func (storage *SimpleRESTStorage) Delete(ctx api.Context, id string, options *api.DeleteOptions) (runtime.Object, error) {
storage.checkContext(ctx)
storage.deleted = id
storage.deleteOptions = options
if err := storage.errors["delete"]; err != nil {
return nil, err
}
@@ -325,6 +328,14 @@ func (storage *SimpleRESTStorage) ResourceLocation(ctx api.Context, id string) (
return storage.resourceLocation, nil
}
type LegacyRESTStorage struct {
*SimpleRESTStorage
}
func (storage LegacyRESTStorage) Delete(ctx api.Context, id string) (runtime.Object, error) {
return storage.SimpleRESTStorage.Delete(ctx, id, nil)
}
func extractBody(response *http.Response, object runtime.Object) (string, error) {
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
@@ -785,6 +796,102 @@ func TestDelete(t *testing.T) {
}
}
func TestDeleteWithOptions(t *testing.T) {
storage := map[string]RESTStorage{}
simpleStorage := SimpleRESTStorage{}
ID := "id"
storage["simple"] = &simpleStorage
handler := handle(storage)
server := httptest.NewServer(handler)
defer server.Close()
grace := int64(300)
item := &api.DeleteOptions{
GracePeriodSeconds: &grace,
}
body, err := codec.Encode(item)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
client := http.Client{}
request, err := http.NewRequest("DELETE", server.URL+"/api/version/simple/"+ID, bytes.NewReader(body))
res, err := client.Do(request)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if res.StatusCode != http.StatusOK {
t.Errorf("unexpected response: %s %#v", request.URL, res)
s, _ := ioutil.ReadAll(res.Body)
t.Logf(string(s))
}
if simpleStorage.deleted != ID {
t.Errorf("Unexpected delete: %s, expected %s", simpleStorage.deleted, ID)
}
if !api.Semantic.DeepEqual(simpleStorage.deleteOptions, item) {
t.Errorf("unexpected delete options: %s", util.ObjectDiff(simpleStorage.deleteOptions, item))
}
}
func TestLegacyDelete(t *testing.T) {
storage := map[string]RESTStorage{}
simpleStorage := SimpleRESTStorage{}
ID := "id"
storage["simple"] = LegacyRESTStorage{&simpleStorage}
var _ RESTDeleter = storage["simple"].(LegacyRESTStorage)
handler := handle(storage)
server := httptest.NewServer(handler)
defer server.Close()
client := http.Client{}
request, err := http.NewRequest("DELETE", server.URL+"/api/version/simple/"+ID, nil)
res, err := client.Do(request)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if res.StatusCode != http.StatusOK {
t.Errorf("unexpected response: %#v", res)
}
if simpleStorage.deleted != ID {
t.Errorf("Unexpected delete: %s, expected %s", simpleStorage.deleted, ID)
}
if simpleStorage.deleteOptions != nil {
t.Errorf("unexpected delete options: %#v", simpleStorage.deleteOptions)
}
}
func TestLegacyDeleteIgnoresOptions(t *testing.T) {
storage := map[string]RESTStorage{}
simpleStorage := SimpleRESTStorage{}
ID := "id"
storage["simple"] = LegacyRESTStorage{&simpleStorage}
handler := handle(storage)
server := httptest.NewServer(handler)
defer server.Close()
item := api.NewDeleteOptions(300)
body, err := codec.Encode(item)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
client := http.Client{}
request, err := http.NewRequest("DELETE", server.URL+"/api/version/simple/"+ID, bytes.NewReader(body))
res, err := client.Do(request)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if res.StatusCode != http.StatusOK {
t.Errorf("unexpected response: %#v", res)
}
if simpleStorage.deleted != ID {
t.Errorf("Unexpected delete: %s, expected %s", simpleStorage.deleted, ID)
}
if simpleStorage.deleteOptions != nil {
t.Errorf("unexpected delete options: %#v", simpleStorage.deleteOptions)
}
}
func TestDeleteInvokesAdmissionControl(t *testing.T) {
storage := map[string]RESTStorage{}
simpleStorage := SimpleRESTStorage{}

View File

@@ -58,6 +58,27 @@ type RESTDeleter interface {
Delete(ctx api.Context, id string) (runtime.Object, error)
}
type RESTGracefulDeleter interface {
// Delete finds a resource in the storage and deletes it.
// If options are provided, the resource will attempt to honor them or return an invalid
// request error.
// Although it can return an arbitrary error value, IsNotFound(err) is true for the
// 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 api.Context, id string, options *api.DeleteOptions) (runtime.Object, error)
}
// GracefulDeleteAdapter adapts the RESTDeleter interface to RESTGracefulDeleter
type GracefulDeleteAdapter struct {
RESTDeleter
}
// Delete implements RESTGracefulDeleter in terms of RESTDeleter
func (w GracefulDeleteAdapter) Delete(ctx api.Context, id string, options *api.DeleteOptions) (runtime.Object, error) {
return w.RESTDeleter.Delete(ctx, id)
}
type RESTCreater interface {
// New returns an empty object that can be used with Create after request data has been put into it.
// This object must be a pointer type for use with Codec.DecodeInto([]byte, runtime.Object)

View File

@@ -330,7 +330,7 @@ func UpdateResource(r RESTUpdater, ctxFn ContextFunc, namer ScopeNamer, codec ru
}
// DeleteResource returns a function that will handle a resource deletion
func DeleteResource(r RESTDeleter, ctxFn ContextFunc, namer ScopeNamer, codec runtime.Codec, resource, kind string, admit admission.Interface) restful.RouteFunction {
func DeleteResource(r RESTGracefulDeleter, checkBody bool, ctxFn ContextFunc, namer ScopeNamer, codec runtime.Codec, resource, kind string, admit admission.Interface) restful.RouteFunction {
return func(req *restful.Request, res *restful.Response) {
w := res.ResponseWriter
@@ -347,6 +347,21 @@ func DeleteResource(r RESTDeleter, ctxFn ContextFunc, namer ScopeNamer, codec ru
ctx = api.WithNamespace(ctx, namespace)
}
options := &api.DeleteOptions{}
if checkBody {
body, err := readBody(req.Request)
if err != nil {
errorJSON(err, codec, w)
return
}
if len(body) > 0 {
if err := codec.DecodeInto(body, options); err != nil {
errorJSON(err, codec, w)
return
}
}
}
err = admit.Admit(admission.NewAttributesRecord(nil, namespace, resource, "DELETE"))
if err != nil {
errorJSON(err, codec, w)
@@ -354,7 +369,7 @@ func DeleteResource(r RESTDeleter, ctxFn ContextFunc, namer ScopeNamer, codec ru
}
result, err := finishRequest(timeout, func() (runtime.Object, error) {
return r.Delete(ctx, name)
return r.Delete(ctx, name, options)
})
if err != nil {
errorJSON(err, codec, w)