diff --git a/pkg/api/helpers.go b/pkg/api/helpers.go index d846bbc0b7315..621ce4ed80da0 100644 --- a/pkg/api/helpers.go +++ b/pkg/api/helpers.go @@ -76,3 +76,11 @@ var standardResources = util.NewStringSet( func IsStandardResourceName(str string) bool { return standardResources.Has(str) } + +// NewDeleteOptions returns a DeleteOptions indicating the resource should +// be deleted within the specified grace period. Use zero to indicate +// immediate deletion. If you would prefer to use the default grace period, +// use &api.DeleteOptions{} directly. +func NewDeleteOptions(grace int64) *DeleteOptions { + return &DeleteOptions{GracePeriodSeconds: &grace} +} diff --git a/pkg/api/register.go b/pkg/api/register.go index 19744af7fb611..81d01572076e7 100644 --- a/pkg/api/register.go +++ b/pkg/api/register.go @@ -52,6 +52,7 @@ func init() { &NamespaceList{}, &Secret{}, &SecretList{}, + &DeleteOptions{}, ) // Legacy names are supported Scheme.AddKnownTypeWithName("", "Minion", &Node{}) @@ -85,3 +86,4 @@ func (*Namespace) IsAnAPIObject() {} func (*NamespaceList) IsAnAPIObject() {} func (*Secret) IsAnAPIObject() {} func (*SecretList) IsAnAPIObject() {} +func (*DeleteOptions) IsAnAPIObject() {} diff --git a/pkg/api/rest/delete.go b/pkg/api/rest/delete.go new file mode 100644 index 0000000000000..a636e65e29861 --- /dev/null +++ b/pkg/api/rest/delete.go @@ -0,0 +1,51 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 rest + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" +) + +// RESTDeleteStrategy defines deletion behavior on an object that follows Kubernetes +// API conventions. +type RESTDeleteStrategy interface { + runtime.ObjectTyper + + // CheckGracefulDelete should return true if the object can be gracefully deleted and set + // any default values on the DeleteOptions. + CheckGracefulDelete(obj runtime.Object, options *api.DeleteOptions) bool +} + +// BeforeDelete tests whether the object can be gracefully deleted. If graceful is set the object +// should be gracefully deleted, if gracefulPending is set the object has already been gracefully deleted +// (and the provided grace period is longer than the time to deletion), and an error is returned if the +// condition cannot be checked or the gracePeriodSeconds is invalid. The options argument may be updated with +// default values if graceful is true. +func BeforeDelete(strategy RESTDeleteStrategy, ctx api.Context, obj runtime.Object, options *api.DeleteOptions) (graceful, gracefulPending bool, err error) { + if strategy == nil { + return false, false, nil + } + _, _, kerr := objectMetaAndKind(strategy, obj) + if kerr != nil { + return false, false, kerr + } + if !strategy.CheckGracefulDelete(obj, options) { + return false, false, nil + } + return true, false, nil +} diff --git a/pkg/api/rest/resttest/resttest.go b/pkg/api/rest/resttest/resttest.go index 1b83ef5cd8ace..0690bfc079553 100644 --- a/pkg/api/rest/resttest/resttest.go +++ b/pkg/api/rest/resttest/resttest.go @@ -196,3 +196,63 @@ func (t *Tester) TestCreateRejectsNamespace(valid runtime.Object) { t.Errorf("Expected 'Controller.Namespace does not match the provided context' error, got '%v'", err.Error()) } } + +func (t *Tester) TestDeleteGraceful(createFn func() runtime.Object, expectedGrace int64, wasGracefulFn func() bool) { + t.TestDeleteGracefulHasDefault(createFn(), expectedGrace, wasGracefulFn) + t.TestDeleteGracefulUsesZeroOnNil(createFn(), 0) +} + +func (t *Tester) TestDeleteNoGraceful(createFn func() runtime.Object, wasGracefulFn func() bool) { + existing := createFn() + objectMeta, err := api.ObjectMetaFor(existing) + if err != nil { + t.Fatalf("object does not have ObjectMeta: %v\n%#v", err, existing) + } + + ctx := api.WithNamespace(api.NewContext(), objectMeta.Namespace) + _, err = t.storage.(apiserver.RESTGracefulDeleter).Delete(ctx, objectMeta.Name, api.NewDeleteOptions(10)) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if _, err := t.storage.(apiserver.RESTGetter).Get(ctx, objectMeta.Name); !errors.IsNotFound(err) { + t.Errorf("unexpected error, object should not exist: %v", err) + } + if wasGracefulFn() { + t.Errorf("resource should not support graceful delete") + } +} + +func (t *Tester) TestDeleteGracefulHasDefault(existing runtime.Object, expectedGrace int64, wasGracefulFn func() bool) { + objectMeta, err := api.ObjectMetaFor(existing) + if err != nil { + t.Fatalf("object does not have ObjectMeta: %v\n%#v", err, existing) + } + + ctx := api.WithNamespace(api.NewContext(), objectMeta.Namespace) + _, err = t.storage.(apiserver.RESTGracefulDeleter).Delete(ctx, objectMeta.Name, &api.DeleteOptions{}) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if _, err := t.storage.(apiserver.RESTGetter).Get(ctx, objectMeta.Name); err != nil { + t.Errorf("unexpected error, object should exist: %v", err) + } + if !wasGracefulFn() { + t.Errorf("did not gracefully delete resource") + } +} + +func (t *Tester) TestDeleteGracefulUsesZeroOnNil(existing runtime.Object, expectedGrace int64) { + objectMeta, err := api.ObjectMetaFor(existing) + if err != nil { + t.Fatalf("object does not have ObjectMeta: %v\n%#v", err, existing) + } + + ctx := api.WithNamespace(api.NewContext(), objectMeta.Namespace) + _, err = t.storage.(apiserver.RESTGracefulDeleter).Delete(ctx, objectMeta.Name, nil) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if _, err := t.storage.(apiserver.RESTGetter).Get(ctx, objectMeta.Name); !errors.IsNotFound(err) { + t.Errorf("unexpected error, object should exist: %v", err) + } +} diff --git a/pkg/api/types.go b/pkg/api/types.go index 183c3c6c410f0..ca8011fde23ff 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -120,6 +120,17 @@ type ObjectMeta struct { // Clients may not set this value. It is represented in RFC3339 form and is in UTC. CreationTimestamp util.Time `json:"creationTimestamp,omitempty"` + // DeletionTimestamp is the time after which this resource will be deleted. This + // field is set by the server when a graceful deletion is requested by the user, and is not + // directly settable by a client. The resource will be deleted (no longer visible from + // resource lists, and not reachable by name) after the time in this field. Once set, this + // value may not be unset or be set further into the future, although it may be shortened + // or the resource may be deleted prior to this time. For example, a user may request that + // a pod is deleted in 30 seconds. The Kubelet will react by sending a graceful termination + // signal to the containers in the pod. Once the resource is deleted in the API, the Kubelet + // will send a hard termination signal to the container. + DeletionTimestamp *util.Time `json:"deletionTimestamp,omitempty"` + // Labels are key value pairs that may be used to scope and select individual resources. // Label keys are of the form: // label-key ::= prefixed-name | name @@ -995,6 +1006,16 @@ type Binding struct { Target ObjectReference `json:"target"` } +// DeleteOptions may be provided when deleting an API object +type DeleteOptions struct { + TypeMeta `json:",inline"` + + // Optional duration in seconds before the object should be deleted. Value must be non-negative integer. + // The value zero indicates delete immediately. If this value is nil, the default grace period for the + // specified type will be used. + GracePeriodSeconds *int64 `json:"gracePeriodSeconds"` +} + // Status is a return value for calls that don't return other objects. // TODO: this could go in apiserver, but I'm including it here so clients needn't // import both. diff --git a/pkg/api/v1beta1/conversion.go b/pkg/api/v1beta1/conversion.go index 5a0437960d20d..99a54d5bdb67a 100644 --- a/pkg/api/v1beta1/conversion.go +++ b/pkg/api/v1beta1/conversion.go @@ -84,6 +84,7 @@ func init() { out.GenerateName = in.GenerateName out.UID = in.UID out.CreationTimestamp = in.CreationTimestamp + out.DeletionTimestamp = in.DeletionTimestamp out.SelfLink = in.SelfLink if len(in.ResourceVersion) > 0 { v, err := strconv.ParseUint(in.ResourceVersion, 10, 64) @@ -100,6 +101,7 @@ func init() { out.GenerateName = in.GenerateName out.UID = in.UID out.CreationTimestamp = in.CreationTimestamp + out.DeletionTimestamp = in.DeletionTimestamp out.SelfLink = in.SelfLink if in.ResourceVersion != 0 { out.ResourceVersion = strconv.FormatUint(in.ResourceVersion, 10) diff --git a/pkg/api/v1beta1/conversion_test.go b/pkg/api/v1beta1/conversion_test.go index df0009b41982e..b0dec254d7f6d 100644 --- a/pkg/api/v1beta1/conversion_test.go +++ b/pkg/api/v1beta1/conversion_test.go @@ -29,6 +29,17 @@ import ( var Convert = newer.Scheme.Convert +func TestEmptyObjectConversion(t *testing.T) { + s, err := current.Codec.Encode(¤t.LimitRange{}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + // DeletionTimestamp is not included, while CreationTimestamp is (would always be set) + if string(s) != `{"kind":"LimitRange","creationTimestamp":null,"apiVersion":"v1beta1","spec":{"limits":null}}` { + t.Errorf("unexpected empty object: %s", string(s)) + } +} + func TestNodeConversion(t *testing.T) { version, kind, err := newer.Scheme.ObjectVersionAndKind(¤t.Minion{}) if err != nil { diff --git a/pkg/api/v1beta1/register.go b/pkg/api/v1beta1/register.go index 54e1203e4ae72..5765cbf7554b4 100644 --- a/pkg/api/v1beta1/register.go +++ b/pkg/api/v1beta1/register.go @@ -59,6 +59,7 @@ func init() { &NamespaceList{}, &Secret{}, &SecretList{}, + &DeleteOptions{}, ) // Future names are supported api.Scheme.AddKnownTypeWithName("v1beta1", "Node", &Minion{}) @@ -92,3 +93,4 @@ func (*Namespace) IsAnAPIObject() {} func (*NamespaceList) IsAnAPIObject() {} func (*Secret) IsAnAPIObject() {} func (*SecretList) IsAnAPIObject() {} +func (*DeleteOptions) IsAnAPIObject() {} diff --git a/pkg/api/v1beta1/types.go b/pkg/api/v1beta1/types.go index dc7674b91e893..3adb770db036e 100644 --- a/pkg/api/v1beta1/types.go +++ b/pkg/api/v1beta1/types.go @@ -356,6 +356,17 @@ type TypeMeta struct { APIVersion string `json:"apiVersion,omitempty" description:"version of the schema the object should have"` Namespace string `json:"namespace,omitempty" description:"namespace to which the object belongs; must be a DNS_SUBDOMAIN; 'default' by default; cannot be updated"` + // DeletionTimestamp is the time after which this resource will be deleted. This + // field is set by the server when a graceful deletion is requested by the user, and is not + // directly settable by a client. The resource will be deleted (no longer visible from + // resource lists, and not reachable by name) after the time in this field. Once set, this + // value may not be unset or be set further into the future, although it may be shortened + // or the resource may be deleted prior to this time. For example, a user may request that + // a pod is deleted in 30 seconds. The Kubelet will react by sending a graceful termination + // signal to the containers in the pod. Once the resource is deleted in the API, the Kubelet + // will send a hard termination signal to the container. + DeletionTimestamp *util.Time `json:"deletionTimestamp,omitempty" description:"RFC 3339 date and time at which the object will be deleted; populated by the system when a graceful deletion is requested, read-only; if not set, graceful deletion of the object has not been requested"` + // GenerateName indicates that the name should be made unique by the server prior to persisting // it. A non-empty value for the field indicates the name will be made unique (and the name // returned to the client will be different than the name passed). The value of this field will @@ -813,6 +824,16 @@ type Binding struct { Host string `json:"host" description:"host to which to bind the specified pod"` } +// DeleteOptions may be provided when deleting an API object +type DeleteOptions struct { + TypeMeta `json:",inline"` + + // Optional duration in seconds before the object should be deleted. Value must be non-negative integer. + // The value zero indicates delete immediately. If this value is nil, the default grace period for the + // specified type will be used. + GracePeriodSeconds *int64 `json:"gracePeriodSeconds" description:"the duration in seconds to wait before deleting this object; defaults to a per object value if not specified; zero means delete immediately"` +} + // Status is a return value for calls that don't return other objects. // TODO: this could go in apiserver, but I'm including it here so clients needn't // import both. diff --git a/pkg/api/v1beta2/conversion.go b/pkg/api/v1beta2/conversion.go index a315cefacd627..c25e13ffd52a8 100644 --- a/pkg/api/v1beta2/conversion.go +++ b/pkg/api/v1beta2/conversion.go @@ -84,6 +84,7 @@ func init() { out.GenerateName = in.GenerateName out.UID = in.UID out.CreationTimestamp = in.CreationTimestamp + out.DeletionTimestamp = in.DeletionTimestamp out.SelfLink = in.SelfLink if len(in.ResourceVersion) > 0 { v, err := strconv.ParseUint(in.ResourceVersion, 10, 64) @@ -100,6 +101,7 @@ func init() { out.GenerateName = in.GenerateName out.UID = in.UID out.CreationTimestamp = in.CreationTimestamp + out.DeletionTimestamp = in.DeletionTimestamp out.SelfLink = in.SelfLink if in.ResourceVersion != 0 { out.ResourceVersion = strconv.FormatUint(in.ResourceVersion, 10) diff --git a/pkg/api/v1beta2/register.go b/pkg/api/v1beta2/register.go index 7536e44afa49b..caf553af06a59 100644 --- a/pkg/api/v1beta2/register.go +++ b/pkg/api/v1beta2/register.go @@ -59,6 +59,7 @@ func init() { &NamespaceList{}, &Secret{}, &SecretList{}, + &DeleteOptions{}, ) // Future names are supported api.Scheme.AddKnownTypeWithName("v1beta2", "Node", &Minion{}) @@ -92,3 +93,4 @@ func (*Namespace) IsAnAPIObject() {} func (*NamespaceList) IsAnAPIObject() {} func (*Secret) IsAnAPIObject() {} func (*SecretList) IsAnAPIObject() {} +func (*DeleteOptions) IsAnAPIObject() {} diff --git a/pkg/api/v1beta2/types.go b/pkg/api/v1beta2/types.go index db46c1023c2f3..c5076b6b101d9 100644 --- a/pkg/api/v1beta2/types.go +++ b/pkg/api/v1beta2/types.go @@ -351,6 +351,17 @@ type TypeMeta struct { APIVersion string `json:"apiVersion,omitempty" description:"version of the schema the object should have"` Namespace string `json:"namespace,omitempty" description:"namespace to which the object belongs; must be a DNS_SUBDOMAIN; 'default' by default; cannot be updated"` + // DeletionTimestamp is the time after which this resource will be deleted. This + // field is set by the server when a graceful deletion is requested by the user, and is not + // directly settable by a client. The resource will be deleted (no longer visible from + // resource lists, and not reachable by name) after the time in this field. Once set, this + // value may not be unset or be set further into the future, although it may be shortened + // or the resource may be deleted prior to this time. For example, a user may request that + // a pod is deleted in 30 seconds. The Kubelet will react by sending a graceful termination + // signal to the containers in the pod. Once the resource is deleted in the API, the Kubelet + // will send a hard termination signal to the container. + DeletionTimestamp *util.Time `json:"deletionTimestamp,omitempty" description:"RFC 3339 date and time at which the object will be deleted; populated by the system when a graceful deletion is requested, read-only; if not set, graceful deletion of the object has not been requested"` + // GenerateName indicates that the name should be made unique by the server prior to persisting // it. A non-empty value for the field indicates the name will be made unique (and the name // returned to the client will be different than the name passed). The value of this field will @@ -831,6 +842,16 @@ type Binding struct { Host string `json:"host" description:"host to which to bind the specified pod"` } +// DeleteOptions may be provided when deleting an API object +type DeleteOptions struct { + TypeMeta `json:",inline"` + + // Optional duration in seconds before the object should be deleted. Value must be non-negative integer. + // The value zero indicates delete immediately. If this value is nil, the default grace period for the + // specified type will be used. + GracePeriodSeconds *int64 `json:"gracePeriodSeconds" description:"the duration in seconds to wait before deleting this object; defaults to a per object value if not specified; zero means delete immediately"` +} + // Status is a return value for calls that don't return other objects. // TODO: this could go in apiserver, but I'm including it here so clients needn't // import both. diff --git a/pkg/api/v1beta3/register.go b/pkg/api/v1beta3/register.go index 09997bc985f04..ef0ad07c5d4e9 100644 --- a/pkg/api/v1beta3/register.go +++ b/pkg/api/v1beta3/register.go @@ -53,6 +53,7 @@ func init() { &NamespaceList{}, &Secret{}, &SecretList{}, + &DeleteOptions{}, ) // Legacy names are supported api.Scheme.AddKnownTypeWithName("v1beta3", "Minion", &Node{}) @@ -86,3 +87,4 @@ func (*Namespace) IsAnAPIObject() {} func (*NamespaceList) IsAnAPIObject() {} func (*Secret) IsAnAPIObject() {} func (*SecretList) IsAnAPIObject() {} +func (*DeleteOptions) IsAnAPIObject() {} diff --git a/pkg/api/v1beta3/types.go b/pkg/api/v1beta3/types.go index a7e0d8d24e9c0..2b47b73a27a54 100644 --- a/pkg/api/v1beta3/types.go +++ b/pkg/api/v1beta3/types.go @@ -120,6 +120,17 @@ type ObjectMeta struct { // Clients may not set this value. It is represented in RFC3339 form and is in UTC. CreationTimestamp util.Time `json:"creationTimestamp,omitempty" description:"RFC 3339 date and time at which the object was created; populated by the system, read-only; null for lists"` + // DeletionTimestamp is the time after which this resource will be deleted. This + // field is set by the server when a graceful deletion is requested by the user, and is not + // directly settable by a client. The resource will be deleted (no longer visible from + // resource lists, and not reachable by name) after the time in this field. Once set, this + // value may not be unset or be set further into the future, although it may be shortened + // or the resource may be deleted prior to this time. For example, a user may request that + // a pod is deleted in 30 seconds. The Kubelet will react by sending a graceful termination + // signal to the containers in the pod. Once the resource is deleted in the API, the Kubelet + // will send a hard termination signal to the container. + DeletionTimestamp *util.Time `json:"deletionTimestamp,omitempty" description:"RFC 3339 date and time at which the object will be deleted; populated by the system when a graceful deletion is requested, read-only; if not set, graceful deletion of the object has not been requested"` + // Labels are key value pairs that may be used to scope and select individual resources. // TODO: replace map[string]string with labels.LabelSet type Labels map[string]string `json:"labels,omitempty" description:"map of string keys and values that can be used to organize and categorize objects; may match selectors of replication controllers and services"` @@ -982,6 +993,16 @@ type Binding struct { Target ObjectReference `json:"target" description:"an object to bind to"` } +// DeleteOptions may be provided when deleting an API object +type DeleteOptions struct { + TypeMeta `json:",inline"` + + // Optional duration in seconds before the object should be deleted. Value must be non-negative integer. + // The value zero indicates delete immediately. If this value is nil, the default grace period for the + // specified type will be used. + GracePeriodSeconds *int64 `json:"gracePeriodSeconds" description:"the duration in seconds to wait before deleting this object; defaults to a per object value if not specified; zero means delete immediately"` +} + // Status is a return value for calls that don't return other objects. type Status struct { TypeMeta `json:",inline"` diff --git a/pkg/apiserver/api_installer.go b/pkg/apiserver/api_installer.go index ab098b03f51f2..0cf52dd58bb44 100644 --- a/pkg/apiserver/api_installer.go +++ b/pkg/apiserver/api_installer.go @@ -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. diff --git a/pkg/apiserver/apiserver_test.go b/pkg/apiserver/apiserver_test.go index af378a8d5c192..1a9718c5de556 100644 --- a/pkg/apiserver/apiserver_test.go +++ b/pkg/apiserver/apiserver_test.go @@ -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{} diff --git a/pkg/apiserver/interfaces.go b/pkg/apiserver/interfaces.go index e6c556a3526a6..450481bdb9e7e 100644 --- a/pkg/apiserver/interfaces.go +++ b/pkg/apiserver/interfaces.go @@ -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) diff --git a/pkg/apiserver/resthandler.go b/pkg/apiserver/resthandler.go index 4ce223680dde4..37a289183b5e6 100644 --- a/pkg/apiserver/resthandler.go +++ b/pkg/apiserver/resthandler.go @@ -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) diff --git a/pkg/registry/event/rest.go b/pkg/registry/event/rest.go index 098d31efb6e9c..18d2a669c933e 100644 --- a/pkg/registry/event/rest.go +++ b/pkg/registry/event/rest.go @@ -97,7 +97,7 @@ func (rs *REST) Delete(ctx api.Context, name string) (runtime.Object, error) { if !ok { return nil, fmt.Errorf("invalid object type") } - return rs.registry.Delete(ctx, name) + return rs.registry.Delete(ctx, name, nil) } func (rs *REST) Get(ctx api.Context, id string) (runtime.Object, error) { diff --git a/pkg/registry/generic/etcd/etcd.go b/pkg/registry/generic/etcd/etcd.go index 8a585162039b4..b852fe159135b 100644 --- a/pkg/registry/generic/etcd/etcd.go +++ b/pkg/registry/generic/etcd/etcd.go @@ -81,19 +81,21 @@ type Etcd struct { // storage of the value in etcd is not appropriate, since they cannot // be watched. Decorator rest.ObjectFunc - // Allows extended behavior during creation + // Allows extended behavior during creation, required CreateStrategy rest.RESTCreateStrategy // On create of an object, attempt to run a further operation. AfterCreate rest.ObjectFunc - // Allows extended behavior during updates + // Allows extended behavior during updates, required UpdateStrategy rest.RESTUpdateStrategy // On update of an object, attempt to run a further operation. AfterUpdate rest.ObjectFunc + // Allows extended behavior during updates, optional + DeleteStrategy rest.RESTDeleteStrategy + // On deletion of an object, attempt to run a further operation. + AfterDelete rest.ObjectFunc // If true, return the object that was deleted. Otherwise, return a generic // success status response. ReturnDeletedObject bool - // On deletion of an object, attempt to run a further operation. - AfterDelete rest.ObjectFunc // Used for all etcd access functions Helper tools.EtcdHelper @@ -333,8 +335,7 @@ func (e *Etcd) Get(ctx api.Context, name string) (runtime.Object, error) { if err != nil { return nil, err } - err = e.Helper.ExtractObj(key, obj, false) - if err != nil { + if err := e.Helper.ExtractObj(key, obj, false); err != nil { return nil, etcderr.InterpretGetError(err, e.EndpointName, name) } if e.Decorator != nil { @@ -346,26 +347,56 @@ func (e *Etcd) Get(ctx api.Context, name string) (runtime.Object, error) { } // Delete removes the item from etcd. -func (e *Etcd) Delete(ctx api.Context, name string) (runtime.Object, error) { +func (e *Etcd) Delete(ctx api.Context, name string, options *api.DeleteOptions) (runtime.Object, error) { key, err := e.KeyFunc(ctx, name) if err != nil { return nil, err } + obj := e.NewFunc() - if err := e.Helper.DeleteObj(key, obj); err != nil { + if err := e.Helper.ExtractObj(key, obj, false); err != nil { return nil, etcderr.InterpretDeleteError(err, e.EndpointName, name) } - if e.AfterDelete != nil { - if err := e.AfterDelete(obj); err != nil { - return nil, err + + // support older consumers of delete by treating "nil" as delete immediately + if options == nil { + options = api.NewDeleteOptions(0) + } + graceful, pendingGraceful, err := rest.BeforeDelete(e.DeleteStrategy, ctx, obj, options) + if err != nil { + return nil, err + } + if pendingGraceful { + return e.finalizeDelete(obj, false) + } + if graceful && *options.GracePeriodSeconds != 0 { + out := e.NewFunc() + if err := e.Helper.SetObj(key, obj, out, uint64(*options.GracePeriodSeconds)); err != nil { + return nil, etcderr.InterpretUpdateError(err, e.EndpointName, name) } + return e.finalizeDelete(out, true) } - if e.Decorator != nil { - if err := e.Decorator(obj); err != nil { + + // delete immediately, or no graceful deletion supported + out := e.NewFunc() + if err := e.Helper.DeleteObj(key, out); err != nil { + return nil, etcderr.InterpretDeleteError(err, e.EndpointName, name) + } + return e.finalizeDelete(out, true) +} + +func (e *Etcd) finalizeDelete(obj runtime.Object, runHooks bool) (runtime.Object, error) { + if runHooks && e.AfterDelete != nil { + if err := e.AfterDelete(obj); err != nil { return nil, err } } if e.ReturnDeletedObject { + if e.Decorator != nil { + if err := e.Decorator(obj); err != nil { + return nil, err + } + } return obj, nil } return &api.Status{Status: api.StatusSuccess}, nil diff --git a/pkg/registry/generic/etcd/etcd_test.go b/pkg/registry/generic/etcd/etcd_test.go index 339280b288d39..b2a972e39a147 100644 --- a/pkg/registry/generic/etcd/etcd_test.go +++ b/pkg/registry/generic/etcd/etcd_test.go @@ -631,7 +631,7 @@ func TestEtcdDelete(t *testing.T) { for name, item := range table { fakeClient, registry := NewTestGenericEtcdRegistry(t) fakeClient.Data[path] = item.existing - obj, err := registry.Delete(api.NewContext(), key) + obj, err := registry.Delete(api.NewContext(), key, nil) if !item.errOK(err) { t.Errorf("%v: unexpected error: %v (%#v)", name, err, obj) } diff --git a/pkg/registry/generic/registry.go b/pkg/registry/generic/registry.go index 89a17d9e713cd..66de3060afcce 100644 --- a/pkg/registry/generic/registry.go +++ b/pkg/registry/generic/registry.go @@ -80,7 +80,7 @@ type Registry interface { CreateWithName(ctx api.Context, id string, obj runtime.Object) error UpdateWithName(ctx api.Context, id string, obj runtime.Object) error Get(ctx api.Context, id string) (runtime.Object, error) - Delete(ctx api.Context, id string) (runtime.Object, error) + Delete(ctx api.Context, id string, options *api.DeleteOptions) (runtime.Object, error) WatchPredicate(ctx api.Context, m Matcher, resourceVersion string) (watch.Interface, error) } diff --git a/pkg/registry/limitrange/rest.go b/pkg/registry/limitrange/rest.go index a76fc6b130c6f..4a5bb0ab70573 100644 --- a/pkg/registry/limitrange/rest.go +++ b/pkg/registry/limitrange/rest.go @@ -115,7 +115,7 @@ func (rs *REST) Delete(ctx api.Context, name string) (runtime.Object, error) { if !ok { return nil, fmt.Errorf("invalid object type") } - return rs.registry.Delete(ctx, name) + return rs.registry.Delete(ctx, name, nil) } // Get gets a LimitRange with the specified name diff --git a/pkg/registry/namespace/etcd/etcd_test.go b/pkg/registry/namespace/etcd/etcd_test.go index c029c2a6a9f7a..60f770736dbd2 100644 --- a/pkg/registry/namespace/etcd/etcd_test.go +++ b/pkg/registry/namespace/etcd/etcd_test.go @@ -312,7 +312,7 @@ func TestDeleteNamespace(t *testing.T) { }, } storage := NewREST(helper) - _, err := storage.Delete(api.NewDefaultContext(), "foo") + _, err := storage.Delete(api.NewDefaultContext(), "foo", nil) if err != nil { t.Fatalf("unexpected error: %v", err) } diff --git a/pkg/registry/namespace/registry.go b/pkg/registry/namespace/registry.go index 3add8a3bc2707..9098c418be965 100644 --- a/pkg/registry/namespace/registry.go +++ b/pkg/registry/namespace/registry.go @@ -44,7 +44,7 @@ type Registry interface { // Storage is an interface for a standard REST Storage backend // TODO: move me somewhere common type Storage interface { - apiserver.RESTDeleter + apiserver.RESTGracefulDeleter apiserver.RESTLister apiserver.RESTGetter apiserver.ResourceWatcher @@ -95,6 +95,6 @@ func (s *storage) UpdateNamespace(ctx api.Context, namespace *api.Namespace) err } func (s *storage) DeleteNamespace(ctx api.Context, namespaceID string) error { - _, err := s.Delete(ctx, namespaceID) + _, err := s.Delete(ctx, namespaceID, nil) return err } diff --git a/pkg/registry/pod/etcd/etcd.go b/pkg/registry/pod/etcd/etcd.go index a1f0861d219e8..d2b56bc591edb 100644 --- a/pkg/registry/pod/etcd/etcd.go +++ b/pkg/registry/pod/etcd/etcd.go @@ -65,6 +65,7 @@ func NewREST(h tools.EtcdHelper) (*REST, *BindingREST, *StatusREST) { store.CreateStrategy = pod.Strategy store.UpdateStrategy = pod.Strategy store.AfterUpdate = bindings.AfterUpdate + store.DeleteStrategy = pod.Strategy store.ReturnDeletedObject = true store.AfterDelete = bindings.AfterDelete diff --git a/pkg/registry/pod/etcd/etcd_test.go b/pkg/registry/pod/etcd/etcd_test.go index 694eb865b71b8..a54ed295c0943 100644 --- a/pkg/registry/pod/etcd/etcd_test.go +++ b/pkg/registry/pod/etcd/etcd_test.go @@ -130,6 +130,34 @@ func TestCreate(t *testing.T) { ) } +func TestDelete(t *testing.T) { + fakeEtcdClient, helper := newHelper(t) + storage, _, _ := NewREST(helper) + cache := &fakeCache{statusToReturn: &api.PodStatus{}} + storage = storage.WithPodStatus(cache) + test := resttest.New(t, storage, fakeEtcdClient.SetError) + + createFn := func() runtime.Object { + pod := validChangedPod() + fakeEtcdClient.Data["/registry/pods/default/foo"] = tools.EtcdResponseWithError{ + R: &etcd.Response{ + Node: &etcd.Node{ + Value: runtime.EncodeOrDie(latest.Codec, pod), + ModifiedIndex: 1, + }, + }, + } + return pod + } + gracefulSetFn := func() bool { + if fakeEtcdClient.Data["/registry/pods/default/foo"].R.Node == nil { + return false + } + return fakeEtcdClient.Data["/registry/pods/default/foo"].R.Node.TTL == 30 + } + test.TestDeleteNoGraceful(createFn, gracefulSetFn) +} + func expectPod(t *testing.T, out runtime.Object) (*api.Pod, bool) { pod, ok := out.(*api.Pod) if !ok || pod == nil { @@ -688,7 +716,7 @@ func TestDeletePod(t *testing.T) { cache := &fakeCache{statusToReturn: &api.PodStatus{}} storage = storage.WithPodStatus(cache) - result, err := storage.Delete(api.NewDefaultContext(), "foo") + result, err := storage.Delete(api.NewDefaultContext(), "foo", nil) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -1265,7 +1293,7 @@ func TestEtcdDeletePod(t *testing.T) { ObjectMeta: api.ObjectMeta{Name: "foo"}, Status: api.PodStatus{Host: "machine"}, }), 0) - _, err := registry.Delete(ctx, "foo") + _, err := registry.Delete(ctx, "foo", api.NewDeleteOptions(0)) if err != nil { t.Errorf("unexpected error: %v", err) } @@ -1286,7 +1314,7 @@ func TestEtcdDeletePodMultipleContainers(t *testing.T) { ObjectMeta: api.ObjectMeta{Name: "foo"}, Status: api.PodStatus{Host: "machine"}, }), 0) - _, err := registry.Delete(ctx, "foo") + _, err := registry.Delete(ctx, "foo", api.NewDeleteOptions(0)) if err != nil { t.Errorf("unexpected error: %v", err) } diff --git a/pkg/registry/pod/registry.go b/pkg/registry/pod/registry.go index ea45b49ad203a..338be13aeff08 100644 --- a/pkg/registry/pod/registry.go +++ b/pkg/registry/pod/registry.go @@ -44,7 +44,7 @@ type Registry interface { // Storage is an interface for a standard REST Storage backend // TODO: move me somewhere common type Storage interface { - apiserver.RESTDeleter + apiserver.RESTGracefulDeleter apiserver.RESTLister apiserver.RESTGetter apiserver.ResourceWatcher @@ -95,6 +95,6 @@ func (s *storage) UpdatePod(ctx api.Context, pod *api.Pod) error { } func (s *storage) DeletePod(ctx api.Context, podID string) error { - _, err := s.Delete(ctx, podID) + _, err := s.Delete(ctx, podID, nil) return err } diff --git a/pkg/registry/pod/rest.go b/pkg/registry/pod/rest.go index 6d7d6354f0c33..6af1a3f76aa98 100644 --- a/pkg/registry/pod/rest.go +++ b/pkg/registry/pod/rest.go @@ -72,6 +72,11 @@ func (podStrategy) ValidateUpdate(obj, old runtime.Object) errors.ValidationErro return validation.ValidatePodUpdate(obj.(*api.Pod), old.(*api.Pod)) } +// CheckGracefulDelete allows a pod to be gracefully deleted. +func (podStrategy) CheckGracefulDelete(obj runtime.Object, options *api.DeleteOptions) bool { + return false +} + type podStatusStrategy struct { podStrategy } diff --git a/pkg/registry/registrytest/generic.go b/pkg/registry/registrytest/generic.go index 924724b7f7687..19ff9d80d5968 100644 --- a/pkg/registry/registrytest/generic.go +++ b/pkg/registry/registrytest/generic.go @@ -84,7 +84,7 @@ func (r *GenericRegistry) UpdateWithName(ctx api.Context, id string, obj runtime return r.Err } -func (r *GenericRegistry) Delete(ctx api.Context, id string) (runtime.Object, error) { +func (r *GenericRegistry) Delete(ctx api.Context, id string, options *api.DeleteOptions) (runtime.Object, error) { r.Lock() defer r.Unlock() r.Broadcaster.Action(watch.Deleted, r.Object) diff --git a/pkg/registry/resourcequota/etcd/etcd.go b/pkg/registry/resourcequota/etcd/etcd.go index 5e71cdc328a5a..5dac87430e187 100644 --- a/pkg/registry/resourcequota/etcd/etcd.go +++ b/pkg/registry/resourcequota/etcd/etcd.go @@ -25,12 +25,11 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/resourcequota" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" - "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" ) // rest implements a RESTStorage for resourcequotas against etcd type REST struct { - store *etcdgeneric.Etcd + *etcdgeneric.Etcd } // NewREST returns a RESTStorage object that will work against ResourceQuota objects. @@ -63,47 +62,7 @@ func NewREST(h tools.EtcdHelper) (*REST, *StatusREST) { statusStore := *store statusStore.UpdateStrategy = resourcequota.StatusStrategy - return &REST{store: store}, &StatusREST{store: &statusStore} -} - -// New returns a new object -func (r *REST) New() runtime.Object { - return r.store.NewFunc() -} - -// NewList returns a new list object -func (r *REST) NewList() runtime.Object { - return r.store.NewListFunc() -} - -// List obtains a list of resourcequotas with labels that match selector. -func (r *REST) List(ctx api.Context, label labels.Selector, field fields.Selector) (runtime.Object, error) { - return r.store.List(ctx, label, field) -} - -// Watch begins watching for new, changed, or deleted resourcequotas. -func (r *REST) Watch(ctx api.Context, label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) { - return r.store.Watch(ctx, label, field, resourceVersion) -} - -// Get gets a specific resourcequota specified by its ID. -func (r *REST) Get(ctx api.Context, name string) (runtime.Object, error) { - return r.store.Get(ctx, name) -} - -// Create creates a resourcequota based on a specification. -func (r *REST) Create(ctx api.Context, obj runtime.Object) (runtime.Object, error) { - return r.store.Create(ctx, obj) -} - -// Update changes a resourcequota specification. -func (r *REST) Update(ctx api.Context, obj runtime.Object) (runtime.Object, bool, error) { - return r.store.Update(ctx, obj) -} - -// Delete deletes an existing resourcequota specified by its name. -func (r *REST) Delete(ctx api.Context, name string) (runtime.Object, error) { - return r.store.Delete(ctx, name) + return &REST{store}, &StatusREST{store: &statusStore} } // StatusREST implements the REST endpoint for changing the status of a resourcequota. diff --git a/pkg/registry/resourcequota/etcd/etcd_test.go b/pkg/registry/resourcequota/etcd/etcd_test.go index a8a4002c05084..74764831b1bb8 100644 --- a/pkg/registry/resourcequota/etcd/etcd_test.go +++ b/pkg/registry/resourcequota/etcd/etcd_test.go @@ -340,7 +340,7 @@ func TestDeleteResourceQuota(t *testing.T) { }, } storage, _ := NewREST(helper) - _, err := storage.Delete(api.NewDefaultContext(), "foo") + _, err := storage.Delete(api.NewDefaultContext(), "foo", nil) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -353,8 +353,8 @@ func TestEtcdGetDifferentNamespace(t *testing.T) { ctx1 := api.NewDefaultContext() ctx2 := api.WithNamespace(api.NewContext(), "other") - key1, _ := registry.store.KeyFunc(ctx1, "foo") - key2, _ := registry.store.KeyFunc(ctx2, "foo") + key1, _ := registry.KeyFunc(ctx1, "foo") + key2, _ := registry.KeyFunc(ctx2, "foo") fakeClient.Set(key1, runtime.EncodeOrDie(latest.Codec, &api.ResourceQuota{ObjectMeta: api.ObjectMeta{Namespace: "default", Name: "foo"}}), 0) fakeClient.Set(key2, runtime.EncodeOrDie(latest.Codec, &api.ResourceQuota{ObjectMeta: api.ObjectMeta{Namespace: "other", Name: "foo"}}), 0) @@ -388,7 +388,7 @@ func TestEtcdGetDifferentNamespace(t *testing.T) { func TestEtcdGet(t *testing.T) { registry, _, fakeClient, _ := newStorage(t) ctx := api.NewDefaultContext() - key, _ := registry.store.KeyFunc(ctx, "foo") + key, _ := registry.KeyFunc(ctx, "foo") fakeClient.Set(key, runtime.EncodeOrDie(latest.Codec, &api.ResourceQuota{ObjectMeta: api.ObjectMeta{Name: "foo"}}), 0) obj, err := registry.Get(ctx, "foo") if err != nil { @@ -403,7 +403,7 @@ func TestEtcdGet(t *testing.T) { func TestEtcdGetNotFound(t *testing.T) { registry, _, fakeClient, _ := newStorage(t) ctx := api.NewDefaultContext() - key, _ := registry.store.KeyFunc(ctx, "foo") + key, _ := registry.KeyFunc(ctx, "foo") fakeClient.Data[key] = tools.EtcdResponseWithError{ R: &etcd.Response{ Node: nil, @@ -431,7 +431,7 @@ func TestEtcdCreateFailsWithoutNamespace(t *testing.T) { func TestEtcdCreateAlreadyExisting(t *testing.T) { registry, _, fakeClient, _ := newStorage(t) ctx := api.NewDefaultContext() - key, _ := registry.store.KeyFunc(ctx, "foo") + key, _ := registry.KeyFunc(ctx, "foo") fakeClient.Data[key] = tools.EtcdResponseWithError{ R: &etcd.Response{ Node: &etcd.Node{ @@ -451,7 +451,7 @@ func TestEtcdUpdateStatus(t *testing.T) { ctx := api.NewDefaultContext() fakeClient.TestIndex = true - key, _ := registry.store.KeyFunc(ctx, "foo") + key, _ := registry.KeyFunc(ctx, "foo") resourcequotaStart := validNewResourceQuota() fakeClient.Set(key, runtime.EncodeOrDie(latest.Codec, resourcequotaStart), 1) @@ -502,7 +502,7 @@ func TestEtcdUpdateStatus(t *testing.T) { func TestEtcdEmptyList(t *testing.T) { registry, _, fakeClient, _ := newStorage(t) ctx := api.NewDefaultContext() - key := registry.store.KeyRootFunc(ctx) + key := registry.KeyRootFunc(ctx) fakeClient.Data[key] = tools.EtcdResponseWithError{ R: &etcd.Response{ Node: &etcd.Node{ @@ -525,7 +525,7 @@ func TestEtcdEmptyList(t *testing.T) { func TestEtcdListNotFound(t *testing.T) { registry, _, fakeClient, _ := newStorage(t) ctx := api.NewDefaultContext() - key := registry.store.KeyRootFunc(ctx) + key := registry.KeyRootFunc(ctx) fakeClient.Data[key] = tools.EtcdResponseWithError{ R: &etcd.Response{}, E: tools.EtcdErrorNotFound, @@ -543,7 +543,7 @@ func TestEtcdListNotFound(t *testing.T) { func TestEtcdList(t *testing.T) { registry, _, fakeClient, _ := newStorage(t) ctx := api.NewDefaultContext() - key := registry.store.KeyRootFunc(ctx) + key := registry.KeyRootFunc(ctx) fakeClient.Data[key] = tools.EtcdResponseWithError{ R: &etcd.Response{ Node: &etcd.Node{ diff --git a/pkg/registry/resourcequota/registry.go b/pkg/registry/resourcequota/registry.go index 8ab0f38812b03..86135c961fdbd 100644 --- a/pkg/registry/resourcequota/registry.go +++ b/pkg/registry/resourcequota/registry.go @@ -44,7 +44,7 @@ type Registry interface { // Storage is an interface for a standard REST Storage backend // TODO: move me somewhere common type Storage interface { - apiserver.RESTDeleter + apiserver.RESTGracefulDeleter apiserver.RESTLister apiserver.RESTGetter apiserver.ResourceWatcher @@ -95,6 +95,6 @@ func (s *storage) UpdateResourceQuota(ctx api.Context, pod *api.ResourceQuota) e } func (s *storage) DeleteResourceQuota(ctx api.Context, podID string) error { - _, err := s.Delete(ctx, podID) + _, err := s.Delete(ctx, podID, nil) return err } diff --git a/pkg/registry/secret/rest.go b/pkg/registry/secret/rest.go index a9b7daca5f3e3..39d5317c56b9a 100644 --- a/pkg/registry/secret/rest.go +++ b/pkg/registry/secret/rest.go @@ -118,7 +118,7 @@ func (rs *REST) Delete(ctx api.Context, name string) (runtime.Object, error) { return nil, fmt.Errorf("invalid object type") } - return rs.registry.Delete(ctx, name) + return rs.registry.Delete(ctx, name, nil) } // Get gets a Secret with the specified name diff --git a/pkg/tools/etcd_object.go b/pkg/tools/etcd_object.go index a50adb290b68d..ef809c3e8640e 100644 --- a/pkg/tools/etcd_object.go +++ b/pkg/tools/etcd_object.go @@ -21,6 +21,8 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + "github.com/coreos/go-etcd/etcd" ) @@ -34,6 +36,9 @@ func (a APIObjectVersioner) UpdateObject(obj runtime.Object, node *etcd.Node) er if err != nil { return err } + if ttl := node.Expiration; ttl != nil { + objectMeta.DeletionTimestamp = &util.Time{*node.Expiration} + } version := node.ModifiedIndex versionString := "" if version != 0 { diff --git a/pkg/tools/etcd_object_test.go b/pkg/tools/etcd_object_test.go new file mode 100644 index 0000000000000..b0604ac0470d6 --- /dev/null +++ b/pkg/tools/etcd_object_test.go @@ -0,0 +1,51 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 tools + +import ( + "testing" + "time" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + "github.com/coreos/go-etcd/etcd" +) + +func TestObjectVersioner(t *testing.T) { + v := APIObjectVersioner{} + if ver, err := v.ObjectResourceVersion(&TestResource{ObjectMeta: api.ObjectMeta{ResourceVersion: "5"}}); err != nil || ver != 5 { + t.Errorf("unexpected version: %d %v", ver, err) + } + if ver, err := v.ObjectResourceVersion(&TestResource{ObjectMeta: api.ObjectMeta{ResourceVersion: "a"}}); err == nil || ver != 0 { + t.Errorf("unexpected version: %d %v", ver, err) + } + obj := &TestResource{ObjectMeta: api.ObjectMeta{ResourceVersion: "a"}} + if err := v.UpdateObject(obj, &etcd.Node{ModifiedIndex: 5}); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if obj.ResourceVersion != "5" || obj.DeletionTimestamp != nil { + t.Errorf("unexpected resource version: %#v", obj) + } + now := util.Time{time.Now()} + obj = &TestResource{ObjectMeta: api.ObjectMeta{ResourceVersion: "a"}} + if err := v.UpdateObject(obj, &etcd.Node{ModifiedIndex: 5, Expiration: &now.Time}); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if obj.ResourceVersion != "5" || *obj.DeletionTimestamp != now { + t.Errorf("unexpected resource version: %#v", obj) + } +} diff --git a/pkg/util/time.go b/pkg/util/time.go index 84d02ac8dc3bd..ecf6d28e067fa 100644 --- a/pkg/util/time.go +++ b/pkg/util/time.go @@ -46,6 +46,14 @@ func Now() Time { return Time{time.Now()} } +// IsZero returns true if the value is nil or time is zero. +func (t *Time) IsZero() bool { + if t == nil { + return true + } + return t.Time.IsZero() +} + // Before reports whether the time instant t is before u. func (t Time) Before(u Time) bool { return t.Time.Before(u.Time) @@ -94,6 +102,9 @@ func (t Time) MarshalJSON() ([]byte, error) { // Fuzz satisfies fuzz.Interface. func (t *Time) Fuzz(c fuzz.Continue) { + if t == nil { + return + } // Allow for about 1000 years of randomness. Leave off nanoseconds // because JSON doesn't represent them so they can't round-trip // properly. diff --git a/test/integration/auth_test.go b/test/integration/auth_test.go index 69fdfddb7e360..916cb7be7844d 100644 --- a/test/integration/auth_test.go +++ b/test/integration/auth_test.go @@ -169,6 +169,14 @@ var aEndpoints string = ` } ` +var deleteNow string = ` +{ + "kind": "DeleteOptions", + "apiVersion": "v1beta1", + "gracePeriod": 0%s +} +` + // To ensure that a POST completes before a dependent GET, set a timeout. var timeoutFlag = "?timeout=60s" @@ -203,7 +211,7 @@ func getTestRequests() []struct { {"GET", "/api/v1beta1/pods", "", code200}, {"GET", "/api/v1beta1/pods/a", "", code200}, {"PATCH", "/api/v1beta1/pods/a", "{%v}", code200}, - {"DELETE", "/api/v1beta1/pods/a" + timeoutFlag, "", code200}, + {"DELETE", "/api/v1beta1/pods/a" + timeoutFlag, deleteNow, code200}, // Non-standard methods (not expected to work, // but expected to pass/fail authorization prior to diff --git a/test/integration/client_test.go b/test/integration/client_test.go index 3f5a372db619f..282d721748509 100644 --- a/test/integration/client_test.go +++ b/test/integration/client_test.go @@ -55,6 +55,7 @@ func TestClient(t *testing.T) { EnableLogsSupport: false, EnableProfiling: true, EnableUISupport: false, + EnableV1Beta3: true, APIPrefix: "/api", Authorizer: apiserver.NewAlwaysAllowAuthorizer(), AdmissionControl: admit.NewAlwaysAdmit(), @@ -63,6 +64,7 @@ func TestClient(t *testing.T) { testCases := []string{ "v1beta1", "v1beta2", + "v1beta3", } for _, apiVersion := range testCases { ns := api.NamespaceDefault