mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-29 06:27:05 +00:00
Merge pull request #30838 from caesarxuchao/per-resource-orphan-behavior
Automatic merge from submit-queue [GarbageCollector] Allow per-resource default garbage collection behavior What's the bug: When deleting an RC with `deleteOptions.OrphanDependents==nil`, garbage collector is supposed to treat it as `deleteOptions.OrphanDependents==true", and orphan the pods created by it. But the apiserver is not doing that. What's in the pr: Allow each resource to specify the default garbage collection behavior in the registry. For example, RC registry's default GC behavior is Orphan, and Pod registry's default GC behavior is CascadingDeletion.
This commit is contained in:
commit
f297ea966e
@ -32,6 +32,20 @@ type RESTDeleteStrategy interface {
|
||||
runtime.ObjectTyper
|
||||
}
|
||||
|
||||
type GarbageCollectionPolicy string
|
||||
|
||||
const (
|
||||
DeleteDependents GarbageCollectionPolicy = "DeleteDependents"
|
||||
OrphanDependents GarbageCollectionPolicy = "OrphanDependents"
|
||||
)
|
||||
|
||||
// GarbageCollectionDeleteStrategy must be implemented by the registry that wants to
|
||||
// orphan dependents by default.
|
||||
type GarbageCollectionDeleteStrategy interface {
|
||||
// DefaultGarbageCollectionPolicy returns the default garbage collection behavior.
|
||||
DefaultGarbageCollectionPolicy() GarbageCollectionPolicy
|
||||
}
|
||||
|
||||
// RESTGracefulDeleteStrategy must be implemented by the registry that supports
|
||||
// graceful deletion.
|
||||
type RESTGracefulDeleteStrategy interface {
|
||||
|
@ -24,6 +24,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/api/rest"
|
||||
"k8s.io/kubernetes/pkg/api/validation"
|
||||
"k8s.io/kubernetes/pkg/fields"
|
||||
"k8s.io/kubernetes/pkg/labels"
|
||||
@ -41,6 +42,12 @@ type rcStrategy struct {
|
||||
// Strategy is the default logic that applies when creating and updating Replication Controller objects.
|
||||
var Strategy = rcStrategy{api.Scheme, api.SimpleNameGenerator}
|
||||
|
||||
// DefaultGarbageCollectionPolicy returns Orphan because that was the default
|
||||
// behavior before the server-side garbage collection was implemented.
|
||||
func (rcStrategy) DefaultGarbageCollectionPolicy() rest.GarbageCollectionPolicy {
|
||||
return rest.OrphanDependents
|
||||
}
|
||||
|
||||
// NamespaceScoped returns true because all Replication Controllers need to be within a namespace.
|
||||
func (rcStrategy) NamespaceScoped() bool {
|
||||
return true
|
||||
|
@ -445,29 +445,49 @@ var (
|
||||
errEmptiedFinalizers = fmt.Errorf("emptied finalizers")
|
||||
)
|
||||
|
||||
// return if we need to update the finalizers of the object, and the desired list of finalizers
|
||||
func shouldUpdateFinalizers(accessor meta.Object, options *api.DeleteOptions) (shouldUpdate bool, newFinalizers []string) {
|
||||
if options == nil || options.OrphanDependents == nil {
|
||||
return false, accessor.GetFinalizers()
|
||||
// shouldUpdateFinalizers returns if we need to update the finalizers of the
|
||||
// object, and the desired list of finalizers.
|
||||
// When deciding whether to add the OrphanDependent finalizer, factors in the
|
||||
// order of highest to lowest priority are: options.OrphanDependents, existing
|
||||
// finalizers of the object, e.DeleteStrategy.DefaultGarbageCollectionPolicy.
|
||||
func shouldUpdateFinalizers(e *Store, accessor meta.Object, options *api.DeleteOptions) (shouldUpdate bool, newFinalizers []string) {
|
||||
shouldOrphan := false
|
||||
// Get default orphan policy from this REST object type
|
||||
if gcStrategy, ok := e.DeleteStrategy.(rest.GarbageCollectionDeleteStrategy); ok {
|
||||
if gcStrategy.DefaultGarbageCollectionPolicy() == rest.OrphanDependents {
|
||||
shouldOrphan = true
|
||||
}
|
||||
}
|
||||
shouldOrphan := *options.OrphanDependents
|
||||
alreadyOrphan := false
|
||||
// If a finalizer is set in the object, it overrides the default
|
||||
hasOrphanFinalizer := false
|
||||
finalizers := accessor.GetFinalizers()
|
||||
newFinalizers = make([]string, 0, len(finalizers))
|
||||
for _, f := range finalizers {
|
||||
if f == api.FinalizerOrphan {
|
||||
alreadyOrphan = true
|
||||
if !shouldOrphan {
|
||||
shouldOrphan = true
|
||||
hasOrphanFinalizer = true
|
||||
break
|
||||
}
|
||||
// TODO: update this when we add a finalizer indicating a preference for the other behavior
|
||||
}
|
||||
// If an explicit policy was set at deletion time, that overrides both
|
||||
if options != nil && options.OrphanDependents != nil {
|
||||
shouldOrphan = *options.OrphanDependents
|
||||
}
|
||||
if shouldOrphan && !hasOrphanFinalizer {
|
||||
finalizers = append(finalizers, api.FinalizerOrphan)
|
||||
return true, finalizers
|
||||
}
|
||||
if !shouldOrphan && hasOrphanFinalizer {
|
||||
var newFinalizers []string
|
||||
for _, f := range finalizers {
|
||||
if f == api.FinalizerOrphan {
|
||||
continue
|
||||
}
|
||||
newFinalizers = append(newFinalizers, f)
|
||||
}
|
||||
newFinalizers = append(newFinalizers, f)
|
||||
return true, newFinalizers
|
||||
}
|
||||
if shouldOrphan && !alreadyOrphan {
|
||||
newFinalizers = append(newFinalizers, api.FinalizerOrphan)
|
||||
}
|
||||
shouldUpdate = shouldOrphan != alreadyOrphan
|
||||
return shouldUpdate, newFinalizers
|
||||
return false, finalizers
|
||||
}
|
||||
|
||||
// markAsDeleting sets the obj's DeletionGracePeriodSeconds to 0, and sets the
|
||||
@ -560,7 +580,7 @@ func (e *Store) updateForGracefulDeletionAndFinalizers(ctx api.Context, name, ke
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
shouldUpdate, newFinalizers := shouldUpdateFinalizers(existingAccessor, options)
|
||||
shouldUpdate, newFinalizers := shouldUpdateFinalizers(e, existingAccessor, options)
|
||||
if shouldUpdate {
|
||||
existingAccessor.SetFinalizers(newFinalizers)
|
||||
}
|
||||
@ -654,7 +674,7 @@ func (e *Store) Delete(ctx api.Context, name string, options *api.DeleteOptions)
|
||||
err, ignoreNotFound, deleteImmediately, out, lastExisting = e.updateForGracefulDeletion(ctx, name, key, options, preconditions, obj)
|
||||
}
|
||||
} else {
|
||||
shouldUpdateFinalizers, _ := shouldUpdateFinalizers(accessor, options)
|
||||
shouldUpdateFinalizers, _ := shouldUpdateFinalizers(e, accessor, options)
|
||||
// TODO: remove the check, because we support no-op updates now.
|
||||
if graceful || pendingFinalizers || shouldUpdateFinalizers {
|
||||
err, ignoreNotFound, deleteImmediately, out, lastExisting = e.updateForGracefulDeletionAndFinalizers(ctx, name, key, options, preconditions, obj)
|
||||
|
@ -46,6 +46,14 @@ import (
|
||||
"k8s.io/kubernetes/pkg/util/wait"
|
||||
)
|
||||
|
||||
type testOrphanDeleteStrategy struct {
|
||||
*testRESTStrategy
|
||||
}
|
||||
|
||||
func (t *testOrphanDeleteStrategy) DefaultGarbageCollectionPolicy() rest.GarbageCollectionPolicy {
|
||||
return rest.OrphanDependents
|
||||
}
|
||||
|
||||
type testRESTStrategy struct {
|
||||
runtime.ObjectTyper
|
||||
api.NameGenerator
|
||||
@ -680,9 +688,16 @@ func TestStoreDeleteWithOrphanDependents(t *testing.T) {
|
||||
nonOrphanOptions := &api.DeleteOptions{OrphanDependents: &falseVar}
|
||||
nilOrphanOptions := &api.DeleteOptions{}
|
||||
|
||||
// defaultDeleteStrategy doesn't implement rest.GarbageCollectionDeleteStrategy.
|
||||
defaultDeleteStrategy := &testRESTStrategy{api.Scheme, api.SimpleNameGenerator, true, false, true}
|
||||
// orphanDeleteStrategy indicates the default garbage collection policy is
|
||||
// to orphan dependentes.
|
||||
orphanDeleteStrategy := &testOrphanDeleteStrategy{defaultDeleteStrategy}
|
||||
|
||||
testcases := []struct {
|
||||
pod *api.Pod
|
||||
options *api.DeleteOptions
|
||||
strategy rest.RESTDeleteStrategy
|
||||
expectNotFound bool
|
||||
updatedFinalizers []string
|
||||
}{
|
||||
@ -690,99 +705,179 @@ func TestStoreDeleteWithOrphanDependents(t *testing.T) {
|
||||
{
|
||||
podWithOrphanFinalizer("pod1"),
|
||||
orphanOptions,
|
||||
defaultDeleteStrategy,
|
||||
false,
|
||||
[]string{"foo.com/x", api.FinalizerOrphan, "bar.com/y"},
|
||||
},
|
||||
{
|
||||
podWithOtherFinalizers("pod2"),
|
||||
orphanOptions,
|
||||
defaultDeleteStrategy,
|
||||
false,
|
||||
[]string{"foo.com/x", "bar.com/y", api.FinalizerOrphan},
|
||||
},
|
||||
{
|
||||
podWithNoFinalizer("pod3"),
|
||||
orphanOptions,
|
||||
defaultDeleteStrategy,
|
||||
false,
|
||||
[]string{api.FinalizerOrphan},
|
||||
},
|
||||
{
|
||||
podWithOnlyOrphanFinalizer("pod4"),
|
||||
orphanOptions,
|
||||
defaultDeleteStrategy,
|
||||
false,
|
||||
[]string{api.FinalizerOrphan},
|
||||
},
|
||||
// cases run with DeleteOptions.OrphanDedependents=false
|
||||
// these cases all have oprhanDeleteStrategy, which should be ignored
|
||||
// because DeleteOptions has the highest priority.
|
||||
{
|
||||
podWithOrphanFinalizer("pod5"),
|
||||
nonOrphanOptions,
|
||||
orphanDeleteStrategy,
|
||||
false,
|
||||
[]string{"foo.com/x", "bar.com/y"},
|
||||
},
|
||||
{
|
||||
podWithOtherFinalizers("pod6"),
|
||||
nonOrphanOptions,
|
||||
orphanDeleteStrategy,
|
||||
false,
|
||||
[]string{"foo.com/x", "bar.com/y"},
|
||||
},
|
||||
{
|
||||
podWithNoFinalizer("pod7"),
|
||||
nonOrphanOptions,
|
||||
orphanDeleteStrategy,
|
||||
true,
|
||||
[]string{},
|
||||
},
|
||||
{
|
||||
podWithOnlyOrphanFinalizer("pod8"),
|
||||
nonOrphanOptions,
|
||||
orphanDeleteStrategy,
|
||||
true,
|
||||
[]string{},
|
||||
},
|
||||
// cases run with nil DeleteOptions, the finalizers are not updated.
|
||||
// cases run with nil DeleteOptions.OrphanDependents. If the object
|
||||
// already has the orphan finalizer, then the DeleteStrategy should be
|
||||
// ignored. Otherwise the DeleteStrategy decides whether to add the
|
||||
// orphan finalizer.
|
||||
{
|
||||
podWithOrphanFinalizer("pod9"),
|
||||
nil,
|
||||
nilOrphanOptions,
|
||||
defaultDeleteStrategy,
|
||||
false,
|
||||
[]string{"foo.com/x", api.FinalizerOrphan, "bar.com/y"},
|
||||
},
|
||||
{
|
||||
podWithOtherFinalizers("pod10"),
|
||||
nil,
|
||||
podWithOrphanFinalizer("pod10"),
|
||||
nilOrphanOptions,
|
||||
orphanDeleteStrategy,
|
||||
false,
|
||||
[]string{"foo.com/x", api.FinalizerOrphan, "bar.com/y"},
|
||||
},
|
||||
{
|
||||
podWithOtherFinalizers("pod11"),
|
||||
nilOrphanOptions,
|
||||
defaultDeleteStrategy,
|
||||
false,
|
||||
[]string{"foo.com/x", "bar.com/y"},
|
||||
},
|
||||
{
|
||||
podWithNoFinalizer("pod11"),
|
||||
nil,
|
||||
podWithOtherFinalizers("pod12"),
|
||||
nilOrphanOptions,
|
||||
orphanDeleteStrategy,
|
||||
false,
|
||||
[]string{"foo.com/x", "bar.com/y", api.FinalizerOrphan},
|
||||
},
|
||||
{
|
||||
podWithNoFinalizer("pod13"),
|
||||
nilOrphanOptions,
|
||||
defaultDeleteStrategy,
|
||||
true,
|
||||
[]string{},
|
||||
},
|
||||
{
|
||||
podWithOnlyOrphanFinalizer("pod12"),
|
||||
nil,
|
||||
podWithNoFinalizer("pod14"),
|
||||
nilOrphanOptions,
|
||||
orphanDeleteStrategy,
|
||||
false,
|
||||
[]string{api.FinalizerOrphan},
|
||||
},
|
||||
// cases run with non-nil DeleteOptions, but nil OrphanDependents, it's treated as OrphanDependents=true
|
||||
{
|
||||
podWithOrphanFinalizer("pod13"),
|
||||
podWithOnlyOrphanFinalizer("pod15"),
|
||||
nilOrphanOptions,
|
||||
defaultDeleteStrategy,
|
||||
false,
|
||||
[]string{"foo.com/x", api.FinalizerOrphan, "bar.com/y"},
|
||||
},
|
||||
{
|
||||
podWithOtherFinalizers("pod14"),
|
||||
nilOrphanOptions,
|
||||
false,
|
||||
[]string{"foo.com/x", "bar.com/y"},
|
||||
},
|
||||
{
|
||||
podWithNoFinalizer("pod15"),
|
||||
nilOrphanOptions,
|
||||
true,
|
||||
[]string{},
|
||||
[]string{api.FinalizerOrphan},
|
||||
},
|
||||
{
|
||||
podWithOnlyOrphanFinalizer("pod16"),
|
||||
nilOrphanOptions,
|
||||
orphanDeleteStrategy,
|
||||
false,
|
||||
[]string{api.FinalizerOrphan},
|
||||
},
|
||||
|
||||
// cases run with nil DeleteOptions should have exact same behavior.
|
||||
// They should be exactly the same as above cases where
|
||||
// DeleteOptions.OrphanDependents is nil.
|
||||
{
|
||||
podWithOrphanFinalizer("pod17"),
|
||||
nil,
|
||||
defaultDeleteStrategy,
|
||||
false,
|
||||
[]string{"foo.com/x", api.FinalizerOrphan, "bar.com/y"},
|
||||
},
|
||||
{
|
||||
podWithOrphanFinalizer("pod18"),
|
||||
nil,
|
||||
orphanDeleteStrategy,
|
||||
false,
|
||||
[]string{"foo.com/x", api.FinalizerOrphan, "bar.com/y"},
|
||||
},
|
||||
{
|
||||
podWithOtherFinalizers("pod19"),
|
||||
nil,
|
||||
defaultDeleteStrategy,
|
||||
false,
|
||||
[]string{"foo.com/x", "bar.com/y"},
|
||||
},
|
||||
{
|
||||
podWithOtherFinalizers("pod20"),
|
||||
nil,
|
||||
orphanDeleteStrategy,
|
||||
false,
|
||||
[]string{"foo.com/x", "bar.com/y", api.FinalizerOrphan},
|
||||
},
|
||||
{
|
||||
podWithNoFinalizer("pod21"),
|
||||
nil,
|
||||
defaultDeleteStrategy,
|
||||
true,
|
||||
[]string{},
|
||||
},
|
||||
{
|
||||
podWithNoFinalizer("pod22"),
|
||||
nil,
|
||||
orphanDeleteStrategy,
|
||||
false,
|
||||
[]string{api.FinalizerOrphan},
|
||||
},
|
||||
{
|
||||
podWithOnlyOrphanFinalizer("pod23"),
|
||||
nil,
|
||||
defaultDeleteStrategy,
|
||||
false,
|
||||
[]string{api.FinalizerOrphan},
|
||||
},
|
||||
{
|
||||
podWithOnlyOrphanFinalizer("pod24"),
|
||||
nil,
|
||||
orphanDeleteStrategy,
|
||||
false,
|
||||
[]string{api.FinalizerOrphan},
|
||||
},
|
||||
@ -793,6 +888,7 @@ func TestStoreDeleteWithOrphanDependents(t *testing.T) {
|
||||
defer server.Terminate(t)
|
||||
|
||||
for _, tc := range testcases {
|
||||
registry.DeleteStrategy = tc.strategy
|
||||
// create pod
|
||||
_, err := registry.Create(testContext, tc.pod)
|
||||
if err != nil {
|
||||
|
@ -24,6 +24,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/api/rest"
|
||||
"k8s.io/kubernetes/pkg/apis/extensions"
|
||||
"k8s.io/kubernetes/pkg/apis/extensions/validation"
|
||||
"k8s.io/kubernetes/pkg/fields"
|
||||
@ -42,6 +43,12 @@ type rsStrategy struct {
|
||||
// Strategy is the default logic that applies when creating and updating ReplicaSet objects.
|
||||
var Strategy = rsStrategy{api.Scheme, api.SimpleNameGenerator}
|
||||
|
||||
// DefaultGarbageCollectionPolicy returns Orphan because that's the default
|
||||
// behavior before the server-side garbage collection is implemented.
|
||||
func (rsStrategy) DefaultGarbageCollectionPolicy() rest.GarbageCollectionPolicy {
|
||||
return rest.OrphanDependents
|
||||
}
|
||||
|
||||
// NamespaceScoped returns true because all ReplicaSets need to be within a namespace.
|
||||
func (rsStrategy) NamespaceScoped() bool {
|
||||
return true
|
||||
|
@ -168,7 +168,7 @@ var _ = framework.KubeDescribe("Garbage collector", func() {
|
||||
gatherMetrics(f)
|
||||
})
|
||||
|
||||
It("[Feature:GarbageCollector] should orphan pods created by rc", func() {
|
||||
It("[Feature:GarbageCollector] should orphan pods created by rc if delete options say so", func() {
|
||||
clientSet := f.Clientset_1_3
|
||||
rcClient := clientSet.Core().ReplicationControllers(f.Namespace.Name)
|
||||
podClient := clientSet.Core().Pods(f.Namespace.Name)
|
||||
@ -214,4 +214,51 @@ var _ = framework.KubeDescribe("Garbage collector", func() {
|
||||
}
|
||||
gatherMetrics(f)
|
||||
})
|
||||
|
||||
It("[Feature:GarbageCollector] should orphan pods created by rc if deleteOptions.OrphanDependents is nil", func() {
|
||||
clientSet := f.Clientset_1_3
|
||||
rcClient := clientSet.Core().ReplicationControllers(f.Namespace.Name)
|
||||
podClient := clientSet.Core().Pods(f.Namespace.Name)
|
||||
rcName := "simpletest.rc"
|
||||
rc := newOwnerRC(f, rcName)
|
||||
By("create the rc")
|
||||
rc, err := rcClient.Create(rc)
|
||||
if err != nil {
|
||||
framework.Failf("Failed to create replication controller: %v", err)
|
||||
}
|
||||
// wait for rc to create some pods
|
||||
if err := wait.Poll(5*time.Second, 30*time.Second, func() (bool, error) {
|
||||
rc, err := rcClient.Get(rc.Name)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("Failed to get rc: %v", err)
|
||||
}
|
||||
if rc.Status.Replicas == *rc.Spec.Replicas {
|
||||
return true, nil
|
||||
} else {
|
||||
return false, nil
|
||||
}
|
||||
}); err != nil {
|
||||
framework.Failf("failed to wait for the rc.Status.Replicas to reach rc.Spec.Replicas: %v", err)
|
||||
}
|
||||
By("delete the rc")
|
||||
deleteOptions := &api.DeleteOptions{}
|
||||
deleteOptions.Preconditions = api.NewUIDPreconditions(string(rc.UID))
|
||||
if err := rcClient.Delete(rc.ObjectMeta.Name, deleteOptions); err != nil {
|
||||
framework.Failf("failed to delete the rc: %v", err)
|
||||
}
|
||||
By("wait for 30 seconds to see if the garbage collector mistakenly deletes the pods")
|
||||
if err := wait.Poll(5*time.Second, 30*time.Second, func() (bool, error) {
|
||||
pods, err := podClient.List(api.ListOptions{})
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("Failed to list pods: %v", err)
|
||||
}
|
||||
if e, a := int(*(rc.Spec.Replicas)), len(pods.Items); e != a {
|
||||
return false, fmt.Errorf("expect %d pods, got %d pods", e, a)
|
||||
}
|
||||
return false, nil
|
||||
}); err != nil && err != wait.ErrWaitTimeout {
|
||||
framework.Failf("%v", err)
|
||||
}
|
||||
gatherMetrics(f)
|
||||
})
|
||||
})
|
||||
|
@ -209,7 +209,7 @@ func TestCascadingDeletion(t *testing.T) {
|
||||
go gc.Run(5, stopCh)
|
||||
defer close(stopCh)
|
||||
// delete one of the replication controller
|
||||
if err := rcClient.Delete(toBeDeletedRCName, nil); err != nil {
|
||||
if err := rcClient.Delete(toBeDeletedRCName, getNonOrphanOptions()); err != nil {
|
||||
t.Fatalf("failed to delete replication controller: %v", err)
|
||||
}
|
||||
|
||||
@ -374,7 +374,7 @@ func TestStressingCascadingDeletion(t *testing.T) {
|
||||
wg.Add(collections * 4)
|
||||
rcUIDs := make(chan types.UID, collections*4)
|
||||
for i := 0; i < collections; i++ {
|
||||
// rc is created with empty finalizers, deleted with nil delete options, pods will be deleted
|
||||
// rc is created with empty finalizers, deleted with nil delete options, pods will remain.
|
||||
go setupRCsPods(t, gc, clientSet, "collection1-"+strconv.Itoa(i), ns.Name, []string{}, nil, &wg, rcUIDs)
|
||||
// rc is created with the orphan finalizer, deleted with nil options, pods will remain.
|
||||
go setupRCsPods(t, gc, clientSet, "collection2-"+strconv.Itoa(i), ns.Name, []string{api.FinalizerOrphan}, nil, &wg, rcUIDs)
|
||||
@ -397,7 +397,7 @@ func TestStressingCascadingDeletion(t *testing.T) {
|
||||
if err := wait.Poll(5*time.Second, 30*time.Second, func() (bool, error) {
|
||||
podsInEachCollection := 3
|
||||
// see the comments on the calls to setupRCsPods for details
|
||||
remainingGroups := 2
|
||||
remainingGroups := 3
|
||||
return verifyRemainingObjects(t, clientSet, ns.Name, 0, collections*podsInEachCollection*remainingGroups)
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
@ -411,7 +411,7 @@ func TestStressingCascadingDeletion(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, pod := range pods.Items {
|
||||
if !strings.Contains(pod.ObjectMeta.Name, "collection2-") && !strings.Contains(pod.ObjectMeta.Name, "collection4-") {
|
||||
if !strings.Contains(pod.ObjectMeta.Name, "collection1-") && !strings.Contains(pod.ObjectMeta.Name, "collection2-") && !strings.Contains(pod.ObjectMeta.Name, "collection4-") {
|
||||
t.Errorf("got unexpected remaining pod: %#v", pod)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user