mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-02 00:07:50 +00:00
Graceful deletion of resources
This commit adds support to core resources to enable deferred deletion of resources. Clients may optionally specify a time period after which resources must be deleted via an object sent with their DELETE. That object may define an optional grace period in seconds, or allow the default "preferred" value for a resource to be used. Once the object is marked as pending deletion, the deletionTimestamp field will be set and an etcd TTL will be in place. Clients should assume resources that have deletionTimestamp set will be deleted at some point in the future. Other changes will come later to enable graceful deletion on a per resource basis.
This commit is contained in:
parent
6f6485909e
commit
428d2263e5
@ -76,3 +76,11 @@ var standardResources = util.NewStringSet(
|
|||||||
func IsStandardResourceName(str string) bool {
|
func IsStandardResourceName(str string) bool {
|
||||||
return standardResources.Has(str)
|
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{},
|
&NamespaceList{},
|
||||||
&Secret{},
|
&Secret{},
|
||||||
&SecretList{},
|
&SecretList{},
|
||||||
|
&DeleteOptions{},
|
||||||
)
|
)
|
||||||
// Legacy names are supported
|
// Legacy names are supported
|
||||||
Scheme.AddKnownTypeWithName("", "Minion", &Node{})
|
Scheme.AddKnownTypeWithName("", "Minion", &Node{})
|
||||||
@ -85,3 +86,4 @@ func (*Namespace) IsAnAPIObject() {}
|
|||||||
func (*NamespaceList) IsAnAPIObject() {}
|
func (*NamespaceList) IsAnAPIObject() {}
|
||||||
func (*Secret) IsAnAPIObject() {}
|
func (*Secret) IsAnAPIObject() {}
|
||||||
func (*SecretList) 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())
|
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.
|
// Clients may not set this value. It is represented in RFC3339 form and is in UTC.
|
||||||
CreationTimestamp util.Time `json:"creationTimestamp,omitempty"`
|
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.
|
// Labels are key value pairs that may be used to scope and select individual resources.
|
||||||
// Label keys are of the form:
|
// Label keys are of the form:
|
||||||
// label-key ::= prefixed-name | name
|
// label-key ::= prefixed-name | name
|
||||||
@ -995,6 +1006,16 @@ type Binding struct {
|
|||||||
Target ObjectReference `json:"target"`
|
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.
|
// 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
|
// TODO: this could go in apiserver, but I'm including it here so clients needn't
|
||||||
// import both.
|
// import both.
|
||||||
|
@ -84,6 +84,7 @@ func init() {
|
|||||||
out.GenerateName = in.GenerateName
|
out.GenerateName = in.GenerateName
|
||||||
out.UID = in.UID
|
out.UID = in.UID
|
||||||
out.CreationTimestamp = in.CreationTimestamp
|
out.CreationTimestamp = in.CreationTimestamp
|
||||||
|
out.DeletionTimestamp = in.DeletionTimestamp
|
||||||
out.SelfLink = in.SelfLink
|
out.SelfLink = in.SelfLink
|
||||||
if len(in.ResourceVersion) > 0 {
|
if len(in.ResourceVersion) > 0 {
|
||||||
v, err := strconv.ParseUint(in.ResourceVersion, 10, 64)
|
v, err := strconv.ParseUint(in.ResourceVersion, 10, 64)
|
||||||
@ -100,6 +101,7 @@ func init() {
|
|||||||
out.GenerateName = in.GenerateName
|
out.GenerateName = in.GenerateName
|
||||||
out.UID = in.UID
|
out.UID = in.UID
|
||||||
out.CreationTimestamp = in.CreationTimestamp
|
out.CreationTimestamp = in.CreationTimestamp
|
||||||
|
out.DeletionTimestamp = in.DeletionTimestamp
|
||||||
out.SelfLink = in.SelfLink
|
out.SelfLink = in.SelfLink
|
||||||
if in.ResourceVersion != 0 {
|
if in.ResourceVersion != 0 {
|
||||||
out.ResourceVersion = strconv.FormatUint(in.ResourceVersion, 10)
|
out.ResourceVersion = strconv.FormatUint(in.ResourceVersion, 10)
|
||||||
|
@ -29,6 +29,17 @@ import (
|
|||||||
|
|
||||||
var Convert = newer.Scheme.Convert
|
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) {
|
func TestNodeConversion(t *testing.T) {
|
||||||
version, kind, err := newer.Scheme.ObjectVersionAndKind(¤t.Minion{})
|
version, kind, err := newer.Scheme.ObjectVersionAndKind(¤t.Minion{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -59,6 +59,7 @@ func init() {
|
|||||||
&NamespaceList{},
|
&NamespaceList{},
|
||||||
&Secret{},
|
&Secret{},
|
||||||
&SecretList{},
|
&SecretList{},
|
||||||
|
&DeleteOptions{},
|
||||||
)
|
)
|
||||||
// Future names are supported
|
// Future names are supported
|
||||||
api.Scheme.AddKnownTypeWithName("v1beta1", "Node", &Minion{})
|
api.Scheme.AddKnownTypeWithName("v1beta1", "Node", &Minion{})
|
||||||
@ -92,3 +93,4 @@ func (*Namespace) IsAnAPIObject() {}
|
|||||||
func (*NamespaceList) IsAnAPIObject() {}
|
func (*NamespaceList) IsAnAPIObject() {}
|
||||||
func (*Secret) IsAnAPIObject() {}
|
func (*Secret) IsAnAPIObject() {}
|
||||||
func (*SecretList) 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"`
|
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"`
|
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
|
// 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
|
// 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
|
// 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"`
|
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.
|
// 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
|
// TODO: this could go in apiserver, but I'm including it here so clients needn't
|
||||||
// import both.
|
// import both.
|
||||||
|
@ -84,6 +84,7 @@ func init() {
|
|||||||
out.GenerateName = in.GenerateName
|
out.GenerateName = in.GenerateName
|
||||||
out.UID = in.UID
|
out.UID = in.UID
|
||||||
out.CreationTimestamp = in.CreationTimestamp
|
out.CreationTimestamp = in.CreationTimestamp
|
||||||
|
out.DeletionTimestamp = in.DeletionTimestamp
|
||||||
out.SelfLink = in.SelfLink
|
out.SelfLink = in.SelfLink
|
||||||
if len(in.ResourceVersion) > 0 {
|
if len(in.ResourceVersion) > 0 {
|
||||||
v, err := strconv.ParseUint(in.ResourceVersion, 10, 64)
|
v, err := strconv.ParseUint(in.ResourceVersion, 10, 64)
|
||||||
@ -100,6 +101,7 @@ func init() {
|
|||||||
out.GenerateName = in.GenerateName
|
out.GenerateName = in.GenerateName
|
||||||
out.UID = in.UID
|
out.UID = in.UID
|
||||||
out.CreationTimestamp = in.CreationTimestamp
|
out.CreationTimestamp = in.CreationTimestamp
|
||||||
|
out.DeletionTimestamp = in.DeletionTimestamp
|
||||||
out.SelfLink = in.SelfLink
|
out.SelfLink = in.SelfLink
|
||||||
if in.ResourceVersion != 0 {
|
if in.ResourceVersion != 0 {
|
||||||
out.ResourceVersion = strconv.FormatUint(in.ResourceVersion, 10)
|
out.ResourceVersion = strconv.FormatUint(in.ResourceVersion, 10)
|
||||||
|
@ -59,6 +59,7 @@ func init() {
|
|||||||
&NamespaceList{},
|
&NamespaceList{},
|
||||||
&Secret{},
|
&Secret{},
|
||||||
&SecretList{},
|
&SecretList{},
|
||||||
|
&DeleteOptions{},
|
||||||
)
|
)
|
||||||
// Future names are supported
|
// Future names are supported
|
||||||
api.Scheme.AddKnownTypeWithName("v1beta2", "Node", &Minion{})
|
api.Scheme.AddKnownTypeWithName("v1beta2", "Node", &Minion{})
|
||||||
@ -92,3 +93,4 @@ func (*Namespace) IsAnAPIObject() {}
|
|||||||
func (*NamespaceList) IsAnAPIObject() {}
|
func (*NamespaceList) IsAnAPIObject() {}
|
||||||
func (*Secret) IsAnAPIObject() {}
|
func (*Secret) IsAnAPIObject() {}
|
||||||
func (*SecretList) 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"`
|
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"`
|
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
|
// 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
|
// 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
|
// 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"`
|
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.
|
// 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
|
// TODO: this could go in apiserver, but I'm including it here so clients needn't
|
||||||
// import both.
|
// import both.
|
||||||
|
@ -53,6 +53,7 @@ func init() {
|
|||||||
&NamespaceList{},
|
&NamespaceList{},
|
||||||
&Secret{},
|
&Secret{},
|
||||||
&SecretList{},
|
&SecretList{},
|
||||||
|
&DeleteOptions{},
|
||||||
)
|
)
|
||||||
// Legacy names are supported
|
// Legacy names are supported
|
||||||
api.Scheme.AddKnownTypeWithName("v1beta3", "Minion", &Node{})
|
api.Scheme.AddKnownTypeWithName("v1beta3", "Minion", &Node{})
|
||||||
@ -86,3 +87,4 @@ func (*Namespace) IsAnAPIObject() {}
|
|||||||
func (*NamespaceList) IsAnAPIObject() {}
|
func (*NamespaceList) IsAnAPIObject() {}
|
||||||
func (*Secret) IsAnAPIObject() {}
|
func (*Secret) IsAnAPIObject() {}
|
||||||
func (*SecretList) 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.
|
// 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"`
|
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.
|
// Labels are key value pairs that may be used to scope and select individual resources.
|
||||||
// TODO: replace map[string]string with labels.LabelSet type
|
// 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"`
|
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"`
|
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.
|
// Status is a return value for calls that don't return other objects.
|
||||||
type Status struct {
|
type Status struct {
|
||||||
TypeMeta `json:",inline"`
|
TypeMeta `json:",inline"`
|
||||||
|
@ -141,11 +141,25 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage RESTStorage
|
|||||||
lister, isLister := storage.(RESTLister)
|
lister, isLister := storage.(RESTLister)
|
||||||
getter, isGetter := storage.(RESTGetter)
|
getter, isGetter := storage.(RESTGetter)
|
||||||
deleter, isDeleter := storage.(RESTDeleter)
|
deleter, isDeleter := storage.(RESTDeleter)
|
||||||
|
gracefulDeleter, isGracefulDeleter := storage.(RESTGracefulDeleter)
|
||||||
updater, isUpdater := storage.(RESTUpdater)
|
updater, isUpdater := storage.(RESTUpdater)
|
||||||
patcher, isPatcher := storage.(RESTPatcher)
|
patcher, isPatcher := storage.(RESTPatcher)
|
||||||
_, isWatcher := storage.(ResourceWatcher)
|
_, isWatcher := storage.(ResourceWatcher)
|
||||||
_, isRedirector := storage.(Redirector)
|
_, 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
|
var ctxFn ContextFunc
|
||||||
ctxFn = func(req *restful.Request) api.Context {
|
ctxFn = func(req *restful.Request) api.Context {
|
||||||
if ctx, ok := context.Get(req.Request); ok {
|
if ctx, ok := context.Get(req.Request); ok {
|
||||||
@ -314,10 +328,13 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage RESTStorage
|
|||||||
addParams(route, action.Params)
|
addParams(route, action.Params)
|
||||||
ws.Route(route)
|
ws.Route(route)
|
||||||
case "DELETE": // Delete a resource.
|
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).
|
Filter(m).
|
||||||
Doc("delete a " + kind).
|
Doc("delete a " + kind).
|
||||||
Operation("delete" + kind)
|
Operation("delete" + kind)
|
||||||
|
if isGracefulDeleter {
|
||||||
|
route.Reads(versionedDeleterObject)
|
||||||
|
}
|
||||||
addParams(route, action.Params)
|
addParams(route, action.Params)
|
||||||
ws.Route(route)
|
ws.Route(route)
|
||||||
case "WATCH": // Watch a resource.
|
case "WATCH": // Watch a resource.
|
||||||
|
@ -97,8 +97,7 @@ func init() {
|
|||||||
&api.Status{})
|
&api.Status{})
|
||||||
// "version" version
|
// "version" version
|
||||||
// TODO: Use versioned api objects?
|
// TODO: Use versioned api objects?
|
||||||
api.Scheme.AddKnownTypes(testVersion, &Simple{}, &SimpleList{},
|
api.Scheme.AddKnownTypes(testVersion, &Simple{}, &SimpleList{}, &api.DeleteOptions{}, &api.Status{})
|
||||||
&api.Status{})
|
|
||||||
|
|
||||||
nsMapper := newMapper()
|
nsMapper := newMapper()
|
||||||
legacyNsMapper := newMapper()
|
legacyNsMapper := newMapper()
|
||||||
@ -204,13 +203,16 @@ func TestSimpleSetupRight(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SimpleRESTStorage struct {
|
type SimpleRESTStorage struct {
|
||||||
errors map[string]error
|
errors map[string]error
|
||||||
list []Simple
|
list []Simple
|
||||||
item Simple
|
item Simple
|
||||||
deleted string
|
|
||||||
updated *Simple
|
updated *Simple
|
||||||
created *Simple
|
created *Simple
|
||||||
|
|
||||||
|
deleted string
|
||||||
|
deleteOptions *api.DeleteOptions
|
||||||
|
|
||||||
actualNamespace string
|
actualNamespace string
|
||||||
namespacePresent bool
|
namespacePresent bool
|
||||||
|
|
||||||
@ -248,9 +250,10 @@ func (storage *SimpleRESTStorage) checkContext(ctx api.Context) {
|
|||||||
storage.actualNamespace, storage.namespacePresent = api.NamespaceFrom(ctx)
|
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.checkContext(ctx)
|
||||||
storage.deleted = id
|
storage.deleted = id
|
||||||
|
storage.deleteOptions = options
|
||||||
if err := storage.errors["delete"]; err != nil {
|
if err := storage.errors["delete"]; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -325,6 +328,14 @@ func (storage *SimpleRESTStorage) ResourceLocation(ctx api.Context, id string) (
|
|||||||
return storage.resourceLocation, nil
|
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) {
|
func extractBody(response *http.Response, object runtime.Object) (string, error) {
|
||||||
defer response.Body.Close()
|
defer response.Body.Close()
|
||||||
body, err := ioutil.ReadAll(response.Body)
|
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) {
|
func TestDeleteInvokesAdmissionControl(t *testing.T) {
|
||||||
storage := map[string]RESTStorage{}
|
storage := map[string]RESTStorage{}
|
||||||
simpleStorage := SimpleRESTStorage{}
|
simpleStorage := SimpleRESTStorage{}
|
||||||
|
@ -58,6 +58,27 @@ type RESTDeleter interface {
|
|||||||
Delete(ctx api.Context, id string) (runtime.Object, error)
|
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 {
|
type RESTCreater interface {
|
||||||
// New returns an empty object that can be used with Create after request data has been put into it.
|
// 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)
|
// 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
|
// 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) {
|
return func(req *restful.Request, res *restful.Response) {
|
||||||
w := res.ResponseWriter
|
w := res.ResponseWriter
|
||||||
|
|
||||||
@ -347,6 +347,21 @@ func DeleteResource(r RESTDeleter, ctxFn ContextFunc, namer ScopeNamer, codec ru
|
|||||||
ctx = api.WithNamespace(ctx, namespace)
|
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"))
|
err = admit.Admit(admission.NewAttributesRecord(nil, namespace, resource, "DELETE"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorJSON(err, codec, w)
|
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) {
|
result, err := finishRequest(timeout, func() (runtime.Object, error) {
|
||||||
return r.Delete(ctx, name)
|
return r.Delete(ctx, name, options)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorJSON(err, codec, w)
|
errorJSON(err, codec, w)
|
||||||
|
@ -97,7 +97,7 @@ func (rs *REST) Delete(ctx api.Context, name string) (runtime.Object, error) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("invalid object type")
|
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) {
|
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
|
// storage of the value in etcd is not appropriate, since they cannot
|
||||||
// be watched.
|
// be watched.
|
||||||
Decorator rest.ObjectFunc
|
Decorator rest.ObjectFunc
|
||||||
// Allows extended behavior during creation
|
// Allows extended behavior during creation, required
|
||||||
CreateStrategy rest.RESTCreateStrategy
|
CreateStrategy rest.RESTCreateStrategy
|
||||||
// On create of an object, attempt to run a further operation.
|
// On create of an object, attempt to run a further operation.
|
||||||
AfterCreate rest.ObjectFunc
|
AfterCreate rest.ObjectFunc
|
||||||
// Allows extended behavior during updates
|
// Allows extended behavior during updates, required
|
||||||
UpdateStrategy rest.RESTUpdateStrategy
|
UpdateStrategy rest.RESTUpdateStrategy
|
||||||
// On update of an object, attempt to run a further operation.
|
// On update of an object, attempt to run a further operation.
|
||||||
AfterUpdate rest.ObjectFunc
|
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
|
// If true, return the object that was deleted. Otherwise, return a generic
|
||||||
// success status response.
|
// success status response.
|
||||||
ReturnDeletedObject bool
|
ReturnDeletedObject bool
|
||||||
// On deletion of an object, attempt to run a further operation.
|
|
||||||
AfterDelete rest.ObjectFunc
|
|
||||||
|
|
||||||
// Used for all etcd access functions
|
// Used for all etcd access functions
|
||||||
Helper tools.EtcdHelper
|
Helper tools.EtcdHelper
|
||||||
@ -333,8 +335,7 @@ func (e *Etcd) Get(ctx api.Context, name string) (runtime.Object, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
err = e.Helper.ExtractObj(key, obj, false)
|
if err := e.Helper.ExtractObj(key, obj, false); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, etcderr.InterpretGetError(err, e.EndpointName, name)
|
return nil, etcderr.InterpretGetError(err, e.EndpointName, name)
|
||||||
}
|
}
|
||||||
if e.Decorator != nil {
|
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.
|
// 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)
|
key, err := e.KeyFunc(ctx, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
obj := e.NewFunc()
|
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)
|
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 {
|
if err := e.AfterDelete(obj); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if e.Decorator != nil {
|
|
||||||
if err := e.Decorator(obj); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if e.ReturnDeletedObject {
|
if e.ReturnDeletedObject {
|
||||||
|
if e.Decorator != nil {
|
||||||
|
if err := e.Decorator(obj); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
return obj, nil
|
return obj, nil
|
||||||
}
|
}
|
||||||
return &api.Status{Status: api.StatusSuccess}, nil
|
return &api.Status{Status: api.StatusSuccess}, nil
|
||||||
|
@ -631,7 +631,7 @@ func TestEtcdDelete(t *testing.T) {
|
|||||||
for name, item := range table {
|
for name, item := range table {
|
||||||
fakeClient, registry := NewTestGenericEtcdRegistry(t)
|
fakeClient, registry := NewTestGenericEtcdRegistry(t)
|
||||||
fakeClient.Data[path] = item.existing
|
fakeClient.Data[path] = item.existing
|
||||||
obj, err := registry.Delete(api.NewContext(), key)
|
obj, err := registry.Delete(api.NewContext(), key, nil)
|
||||||
if !item.errOK(err) {
|
if !item.errOK(err) {
|
||||||
t.Errorf("%v: unexpected error: %v (%#v)", name, err, obj)
|
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
|
CreateWithName(ctx api.Context, id string, obj runtime.Object) error
|
||||||
UpdateWithName(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)
|
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)
|
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 {
|
if !ok {
|
||||||
return nil, fmt.Errorf("invalid object type")
|
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
|
// Get gets a LimitRange with the specified name
|
||||||
|
@ -312,7 +312,7 @@ func TestDeleteNamespace(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
storage := NewREST(helper)
|
storage := NewREST(helper)
|
||||||
_, err := storage.Delete(api.NewDefaultContext(), "foo")
|
_, err := storage.Delete(api.NewDefaultContext(), "foo", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error: %v", err)
|
t.Fatalf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,7 @@ type Registry interface {
|
|||||||
// Storage is an interface for a standard REST Storage backend
|
// Storage is an interface for a standard REST Storage backend
|
||||||
// TODO: move me somewhere common
|
// TODO: move me somewhere common
|
||||||
type Storage interface {
|
type Storage interface {
|
||||||
apiserver.RESTDeleter
|
apiserver.RESTGracefulDeleter
|
||||||
apiserver.RESTLister
|
apiserver.RESTLister
|
||||||
apiserver.RESTGetter
|
apiserver.RESTGetter
|
||||||
apiserver.ResourceWatcher
|
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 {
|
func (s *storage) DeleteNamespace(ctx api.Context, namespaceID string) error {
|
||||||
_, err := s.Delete(ctx, namespaceID)
|
_, err := s.Delete(ctx, namespaceID, nil)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -65,6 +65,7 @@ func NewREST(h tools.EtcdHelper) (*REST, *BindingREST, *StatusREST) {
|
|||||||
store.CreateStrategy = pod.Strategy
|
store.CreateStrategy = pod.Strategy
|
||||||
store.UpdateStrategy = pod.Strategy
|
store.UpdateStrategy = pod.Strategy
|
||||||
store.AfterUpdate = bindings.AfterUpdate
|
store.AfterUpdate = bindings.AfterUpdate
|
||||||
|
store.DeleteStrategy = pod.Strategy
|
||||||
store.ReturnDeletedObject = true
|
store.ReturnDeletedObject = true
|
||||||
store.AfterDelete = bindings.AfterDelete
|
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) {
|
func expectPod(t *testing.T, out runtime.Object) (*api.Pod, bool) {
|
||||||
pod, ok := out.(*api.Pod)
|
pod, ok := out.(*api.Pod)
|
||||||
if !ok || pod == nil {
|
if !ok || pod == nil {
|
||||||
@ -688,7 +716,7 @@ func TestDeletePod(t *testing.T) {
|
|||||||
cache := &fakeCache{statusToReturn: &api.PodStatus{}}
|
cache := &fakeCache{statusToReturn: &api.PodStatus{}}
|
||||||
storage = storage.WithPodStatus(cache)
|
storage = storage.WithPodStatus(cache)
|
||||||
|
|
||||||
result, err := storage.Delete(api.NewDefaultContext(), "foo")
|
result, err := storage.Delete(api.NewDefaultContext(), "foo", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error: %v", err)
|
t.Fatalf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
@ -1265,7 +1293,7 @@ func TestEtcdDeletePod(t *testing.T) {
|
|||||||
ObjectMeta: api.ObjectMeta{Name: "foo"},
|
ObjectMeta: api.ObjectMeta{Name: "foo"},
|
||||||
Status: api.PodStatus{Host: "machine"},
|
Status: api.PodStatus{Host: "machine"},
|
||||||
}), 0)
|
}), 0)
|
||||||
_, err := registry.Delete(ctx, "foo")
|
_, err := registry.Delete(ctx, "foo", api.NewDeleteOptions(0))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unexpected error: %v", err)
|
t.Errorf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
@ -1286,7 +1314,7 @@ func TestEtcdDeletePodMultipleContainers(t *testing.T) {
|
|||||||
ObjectMeta: api.ObjectMeta{Name: "foo"},
|
ObjectMeta: api.ObjectMeta{Name: "foo"},
|
||||||
Status: api.PodStatus{Host: "machine"},
|
Status: api.PodStatus{Host: "machine"},
|
||||||
}), 0)
|
}), 0)
|
||||||
_, err := registry.Delete(ctx, "foo")
|
_, err := registry.Delete(ctx, "foo", api.NewDeleteOptions(0))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unexpected error: %v", err)
|
t.Errorf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,7 @@ type Registry interface {
|
|||||||
// Storage is an interface for a standard REST Storage backend
|
// Storage is an interface for a standard REST Storage backend
|
||||||
// TODO: move me somewhere common
|
// TODO: move me somewhere common
|
||||||
type Storage interface {
|
type Storage interface {
|
||||||
apiserver.RESTDeleter
|
apiserver.RESTGracefulDeleter
|
||||||
apiserver.RESTLister
|
apiserver.RESTLister
|
||||||
apiserver.RESTGetter
|
apiserver.RESTGetter
|
||||||
apiserver.ResourceWatcher
|
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 {
|
func (s *storage) DeletePod(ctx api.Context, podID string) error {
|
||||||
_, err := s.Delete(ctx, podID)
|
_, err := s.Delete(ctx, podID, nil)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -72,6 +72,11 @@ func (podStrategy) ValidateUpdate(obj, old runtime.Object) errors.ValidationErro
|
|||||||
return validation.ValidatePodUpdate(obj.(*api.Pod), old.(*api.Pod))
|
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 {
|
type podStatusStrategy struct {
|
||||||
podStrategy
|
podStrategy
|
||||||
}
|
}
|
||||||
|
@ -84,7 +84,7 @@ func (r *GenericRegistry) UpdateWithName(ctx api.Context, id string, obj runtime
|
|||||||
return r.Err
|
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()
|
r.Lock()
|
||||||
defer r.Unlock()
|
defer r.Unlock()
|
||||||
r.Broadcaster.Action(watch.Deleted, r.Object)
|
r.Broadcaster.Action(watch.Deleted, r.Object)
|
||||||
|
@ -25,12 +25,11 @@ import (
|
|||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/resourcequota"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/resourcequota"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/tools"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/tools"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// rest implements a RESTStorage for resourcequotas against etcd
|
// rest implements a RESTStorage for resourcequotas against etcd
|
||||||
type REST struct {
|
type REST struct {
|
||||||
store *etcdgeneric.Etcd
|
*etcdgeneric.Etcd
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewREST returns a RESTStorage object that will work against ResourceQuota objects.
|
// 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 := *store
|
||||||
statusStore.UpdateStrategy = resourcequota.StatusStrategy
|
statusStore.UpdateStrategy = resourcequota.StatusStrategy
|
||||||
|
|
||||||
return &REST{store: store}, &StatusREST{store: &statusStore}
|
return &REST{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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// StatusREST implements the REST endpoint for changing the status of a resourcequota.
|
// StatusREST implements the REST endpoint for changing the status of a resourcequota.
|
||||||
|
@ -340,7 +340,7 @@ func TestDeleteResourceQuota(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
storage, _ := NewREST(helper)
|
storage, _ := NewREST(helper)
|
||||||
_, err := storage.Delete(api.NewDefaultContext(), "foo")
|
_, err := storage.Delete(api.NewDefaultContext(), "foo", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error: %v", err)
|
t.Fatalf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
@ -353,8 +353,8 @@ func TestEtcdGetDifferentNamespace(t *testing.T) {
|
|||||||
ctx1 := api.NewDefaultContext()
|
ctx1 := api.NewDefaultContext()
|
||||||
ctx2 := api.WithNamespace(api.NewContext(), "other")
|
ctx2 := api.WithNamespace(api.NewContext(), "other")
|
||||||
|
|
||||||
key1, _ := registry.store.KeyFunc(ctx1, "foo")
|
key1, _ := registry.KeyFunc(ctx1, "foo")
|
||||||
key2, _ := registry.store.KeyFunc(ctx2, "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(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)
|
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) {
|
func TestEtcdGet(t *testing.T) {
|
||||||
registry, _, fakeClient, _ := newStorage(t)
|
registry, _, fakeClient, _ := newStorage(t)
|
||||||
ctx := api.NewDefaultContext()
|
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)
|
fakeClient.Set(key, runtime.EncodeOrDie(latest.Codec, &api.ResourceQuota{ObjectMeta: api.ObjectMeta{Name: "foo"}}), 0)
|
||||||
obj, err := registry.Get(ctx, "foo")
|
obj, err := registry.Get(ctx, "foo")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -403,7 +403,7 @@ func TestEtcdGet(t *testing.T) {
|
|||||||
func TestEtcdGetNotFound(t *testing.T) {
|
func TestEtcdGetNotFound(t *testing.T) {
|
||||||
registry, _, fakeClient, _ := newStorage(t)
|
registry, _, fakeClient, _ := newStorage(t)
|
||||||
ctx := api.NewDefaultContext()
|
ctx := api.NewDefaultContext()
|
||||||
key, _ := registry.store.KeyFunc(ctx, "foo")
|
key, _ := registry.KeyFunc(ctx, "foo")
|
||||||
fakeClient.Data[key] = tools.EtcdResponseWithError{
|
fakeClient.Data[key] = tools.EtcdResponseWithError{
|
||||||
R: &etcd.Response{
|
R: &etcd.Response{
|
||||||
Node: nil,
|
Node: nil,
|
||||||
@ -431,7 +431,7 @@ func TestEtcdCreateFailsWithoutNamespace(t *testing.T) {
|
|||||||
func TestEtcdCreateAlreadyExisting(t *testing.T) {
|
func TestEtcdCreateAlreadyExisting(t *testing.T) {
|
||||||
registry, _, fakeClient, _ := newStorage(t)
|
registry, _, fakeClient, _ := newStorage(t)
|
||||||
ctx := api.NewDefaultContext()
|
ctx := api.NewDefaultContext()
|
||||||
key, _ := registry.store.KeyFunc(ctx, "foo")
|
key, _ := registry.KeyFunc(ctx, "foo")
|
||||||
fakeClient.Data[key] = tools.EtcdResponseWithError{
|
fakeClient.Data[key] = tools.EtcdResponseWithError{
|
||||||
R: &etcd.Response{
|
R: &etcd.Response{
|
||||||
Node: &etcd.Node{
|
Node: &etcd.Node{
|
||||||
@ -451,7 +451,7 @@ func TestEtcdUpdateStatus(t *testing.T) {
|
|||||||
ctx := api.NewDefaultContext()
|
ctx := api.NewDefaultContext()
|
||||||
fakeClient.TestIndex = true
|
fakeClient.TestIndex = true
|
||||||
|
|
||||||
key, _ := registry.store.KeyFunc(ctx, "foo")
|
key, _ := registry.KeyFunc(ctx, "foo")
|
||||||
resourcequotaStart := validNewResourceQuota()
|
resourcequotaStart := validNewResourceQuota()
|
||||||
fakeClient.Set(key, runtime.EncodeOrDie(latest.Codec, resourcequotaStart), 1)
|
fakeClient.Set(key, runtime.EncodeOrDie(latest.Codec, resourcequotaStart), 1)
|
||||||
|
|
||||||
@ -502,7 +502,7 @@ func TestEtcdUpdateStatus(t *testing.T) {
|
|||||||
func TestEtcdEmptyList(t *testing.T) {
|
func TestEtcdEmptyList(t *testing.T) {
|
||||||
registry, _, fakeClient, _ := newStorage(t)
|
registry, _, fakeClient, _ := newStorage(t)
|
||||||
ctx := api.NewDefaultContext()
|
ctx := api.NewDefaultContext()
|
||||||
key := registry.store.KeyRootFunc(ctx)
|
key := registry.KeyRootFunc(ctx)
|
||||||
fakeClient.Data[key] = tools.EtcdResponseWithError{
|
fakeClient.Data[key] = tools.EtcdResponseWithError{
|
||||||
R: &etcd.Response{
|
R: &etcd.Response{
|
||||||
Node: &etcd.Node{
|
Node: &etcd.Node{
|
||||||
@ -525,7 +525,7 @@ func TestEtcdEmptyList(t *testing.T) {
|
|||||||
func TestEtcdListNotFound(t *testing.T) {
|
func TestEtcdListNotFound(t *testing.T) {
|
||||||
registry, _, fakeClient, _ := newStorage(t)
|
registry, _, fakeClient, _ := newStorage(t)
|
||||||
ctx := api.NewDefaultContext()
|
ctx := api.NewDefaultContext()
|
||||||
key := registry.store.KeyRootFunc(ctx)
|
key := registry.KeyRootFunc(ctx)
|
||||||
fakeClient.Data[key] = tools.EtcdResponseWithError{
|
fakeClient.Data[key] = tools.EtcdResponseWithError{
|
||||||
R: &etcd.Response{},
|
R: &etcd.Response{},
|
||||||
E: tools.EtcdErrorNotFound,
|
E: tools.EtcdErrorNotFound,
|
||||||
@ -543,7 +543,7 @@ func TestEtcdListNotFound(t *testing.T) {
|
|||||||
func TestEtcdList(t *testing.T) {
|
func TestEtcdList(t *testing.T) {
|
||||||
registry, _, fakeClient, _ := newStorage(t)
|
registry, _, fakeClient, _ := newStorage(t)
|
||||||
ctx := api.NewDefaultContext()
|
ctx := api.NewDefaultContext()
|
||||||
key := registry.store.KeyRootFunc(ctx)
|
key := registry.KeyRootFunc(ctx)
|
||||||
fakeClient.Data[key] = tools.EtcdResponseWithError{
|
fakeClient.Data[key] = tools.EtcdResponseWithError{
|
||||||
R: &etcd.Response{
|
R: &etcd.Response{
|
||||||
Node: &etcd.Node{
|
Node: &etcd.Node{
|
||||||
|
@ -44,7 +44,7 @@ type Registry interface {
|
|||||||
// Storage is an interface for a standard REST Storage backend
|
// Storage is an interface for a standard REST Storage backend
|
||||||
// TODO: move me somewhere common
|
// TODO: move me somewhere common
|
||||||
type Storage interface {
|
type Storage interface {
|
||||||
apiserver.RESTDeleter
|
apiserver.RESTGracefulDeleter
|
||||||
apiserver.RESTLister
|
apiserver.RESTLister
|
||||||
apiserver.RESTGetter
|
apiserver.RESTGetter
|
||||||
apiserver.ResourceWatcher
|
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 {
|
func (s *storage) DeleteResourceQuota(ctx api.Context, podID string) error {
|
||||||
_, err := s.Delete(ctx, podID)
|
_, err := s.Delete(ctx, podID, nil)
|
||||||
return err
|
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 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
|
// Get gets a Secret with the specified name
|
||||||
|
@ -21,6 +21,8 @@ import (
|
|||||||
|
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||||
|
|
||||||
"github.com/coreos/go-etcd/etcd"
|
"github.com/coreos/go-etcd/etcd"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -34,6 +36,9 @@ func (a APIObjectVersioner) UpdateObject(obj runtime.Object, node *etcd.Node) er
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if ttl := node.Expiration; ttl != nil {
|
||||||
|
objectMeta.DeletionTimestamp = &util.Time{*node.Expiration}
|
||||||
|
}
|
||||||
version := node.ModifiedIndex
|
version := node.ModifiedIndex
|
||||||
versionString := ""
|
versionString := ""
|
||||||
if version != 0 {
|
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()}
|
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.
|
// Before reports whether the time instant t is before u.
|
||||||
func (t Time) Before(u Time) bool {
|
func (t Time) Before(u Time) bool {
|
||||||
return t.Time.Before(u.Time)
|
return t.Time.Before(u.Time)
|
||||||
@ -94,6 +102,9 @@ func (t Time) MarshalJSON() ([]byte, error) {
|
|||||||
|
|
||||||
// Fuzz satisfies fuzz.Interface.
|
// Fuzz satisfies fuzz.Interface.
|
||||||
func (t *Time) Fuzz(c fuzz.Continue) {
|
func (t *Time) Fuzz(c fuzz.Continue) {
|
||||||
|
if t == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
// Allow for about 1000 years of randomness. Leave off nanoseconds
|
// Allow for about 1000 years of randomness. Leave off nanoseconds
|
||||||
// because JSON doesn't represent them so they can't round-trip
|
// because JSON doesn't represent them so they can't round-trip
|
||||||
// properly.
|
// 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.
|
// To ensure that a POST completes before a dependent GET, set a timeout.
|
||||||
var timeoutFlag = "?timeout=60s"
|
var timeoutFlag = "?timeout=60s"
|
||||||
|
|
||||||
@ -203,7 +211,7 @@ func getTestRequests() []struct {
|
|||||||
{"GET", "/api/v1beta1/pods", "", code200},
|
{"GET", "/api/v1beta1/pods", "", code200},
|
||||||
{"GET", "/api/v1beta1/pods/a", "", code200},
|
{"GET", "/api/v1beta1/pods/a", "", code200},
|
||||||
{"PATCH", "/api/v1beta1/pods/a", "{%v}", 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,
|
// Non-standard methods (not expected to work,
|
||||||
// but expected to pass/fail authorization prior to
|
// but expected to pass/fail authorization prior to
|
||||||
|
@ -55,6 +55,7 @@ func TestClient(t *testing.T) {
|
|||||||
EnableLogsSupport: false,
|
EnableLogsSupport: false,
|
||||||
EnableProfiling: true,
|
EnableProfiling: true,
|
||||||
EnableUISupport: false,
|
EnableUISupport: false,
|
||||||
|
EnableV1Beta3: true,
|
||||||
APIPrefix: "/api",
|
APIPrefix: "/api",
|
||||||
Authorizer: apiserver.NewAlwaysAllowAuthorizer(),
|
Authorizer: apiserver.NewAlwaysAllowAuthorizer(),
|
||||||
AdmissionControl: admit.NewAlwaysAdmit(),
|
AdmissionControl: admit.NewAlwaysAdmit(),
|
||||||
@ -63,6 +64,7 @@ func TestClient(t *testing.T) {
|
|||||||
testCases := []string{
|
testCases := []string{
|
||||||
"v1beta1",
|
"v1beta1",
|
||||||
"v1beta2",
|
"v1beta2",
|
||||||
|
"v1beta3",
|
||||||
}
|
}
|
||||||
for _, apiVersion := range testCases {
|
for _, apiVersion := range testCases {
|
||||||
ns := api.NamespaceDefault
|
ns := api.NamespaceDefault
|
||||||
|
Loading…
Reference in New Issue
Block a user