mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-31 15:25:57 +00:00
Merge pull request #5085 from smarterclayton/allow_graceful_delete
Support graceful deletion of resources
This commit is contained in:
commit
699dc96c7b
@ -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}
|
||||
}
|
||||
|
@ -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() {}
|
||||
|
51
pkg/api/rest/delete.go
Normal file
51
pkg/api/rest/delete.go
Normal file
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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() {}
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
@ -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() {}
|
||||
|
@ -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.
|
||||
|
@ -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() {}
|
||||
|
@ -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"`
|
||||
|
@ -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)
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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.Decorator != nil {
|
||||
if err := e.Decorator(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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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{
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
51
pkg/tools/etcd_object_test.go
Normal file
51
pkg/tools/etcd_object_test.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user