mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-09-17 07:03:31 +00:00
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:
@@ -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.
|
||||
|
@@ -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{}
|
||||
|
@@ -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)
|
||||
|
@@ -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)
|
||||
|
Reference in New Issue
Block a user