Merge pull request #82189 from deads2k/ns-resources

add conditions for remaining object totals during ns termination
This commit is contained in:
Kubernetes Prow Robot 2019-09-20 01:33:00 -07:00 committed by GitHub
commit 259d6bf608
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 241 additions and 32 deletions

View File

@ -410,33 +410,43 @@ func (d *namespacedResourcesDeleter) deleteEachItem(gvr schema.GroupVersionResou
return nil
}
type gvrDeletionMetadata struct {
// finalizerEstimateSeconds is an estimate of how much longer to wait. zero means that no estimate has made and does not
// mean that all content has been removed.
finalizerEstimateSeconds int64
// numRemaining is how many instances of the gvr remain
numRemaining int
// finalizersToNumRemaining maps finalizers to how many resources are stuck on them
finalizersToNumRemaining map[string]int
}
// deleteAllContentForGroupVersionResource will use the dynamic client to delete each resource identified in gvr.
// It returns an estimate of the time remaining before the remaining resources are deleted.
// If estimate > 0, not all resources are guaranteed to be gone.
func (d *namespacedResourcesDeleter) deleteAllContentForGroupVersionResource(
gvr schema.GroupVersionResource, namespace string,
namespaceDeletedAt metav1.Time) (int64, error) {
namespaceDeletedAt metav1.Time) (gvrDeletionMetadata, error) {
klog.V(5).Infof("namespace controller - deleteAllContentForGroupVersionResource - namespace: %s, gvr: %v", namespace, gvr)
// estimate how long it will take for the resource to be deleted (needed for objects that support graceful delete)
estimate, err := d.estimateGracefulTermination(gvr, namespace, namespaceDeletedAt)
if err != nil {
klog.V(5).Infof("namespace controller - deleteAllContentForGroupVersionResource - unable to estimate - namespace: %s, gvr: %v, err: %v", namespace, gvr, err)
return estimate, err
return gvrDeletionMetadata{}, err
}
klog.V(5).Infof("namespace controller - deleteAllContentForGroupVersionResource - estimate - namespace: %s, gvr: %v, estimate: %v", namespace, gvr, estimate)
// first try to delete the entire collection
deleteCollectionSupported, err := d.deleteCollection(gvr, namespace)
if err != nil {
return estimate, err
return gvrDeletionMetadata{finalizerEstimateSeconds: estimate}, err
}
// delete collection was not supported, so we list and delete each item...
if !deleteCollectionSupported {
err = d.deleteEachItem(gvr, namespace)
if err != nil {
return estimate, err
return gvrDeletionMetadata{finalizerEstimateSeconds: estimate}, err
}
}
@ -446,24 +456,56 @@ func (d *namespacedResourcesDeleter) deleteAllContentForGroupVersionResource(
unstructuredList, listSupported, err := d.listCollection(gvr, namespace)
if err != nil {
klog.V(5).Infof("namespace controller - deleteAllContentForGroupVersionResource - error verifying no items in namespace: %s, gvr: %v, err: %v", namespace, gvr, err)
return estimate, err
return gvrDeletionMetadata{finalizerEstimateSeconds: estimate}, err
}
if !listSupported {
return estimate, nil
return gvrDeletionMetadata{finalizerEstimateSeconds: estimate}, nil
}
klog.V(5).Infof("namespace controller - deleteAllContentForGroupVersionResource - items remaining - namespace: %s, gvr: %v, items: %v", namespace, gvr, len(unstructuredList.Items))
if len(unstructuredList.Items) != 0 && estimate == int64(0) {
// if any item has a finalizer, we treat that as a normal condition, and use a default estimation to allow for GC to complete.
for _, item := range unstructuredList.Items {
if len(item.GetFinalizers()) > 0 {
klog.V(5).Infof("namespace controller - deleteAllContentForGroupVersionResource - items remaining with finalizers - namespace: %s, gvr: %v, finalizers: %v", namespace, gvr, item.GetFinalizers())
return finalizerEstimateSeconds, nil
}
}
// nothing reported a finalizer, so something was unexpected as it should have been deleted.
return estimate, fmt.Errorf("unexpected items still remain in namespace: %s for gvr: %v", namespace, gvr)
if len(unstructuredList.Items) == 0 {
// we're done
return gvrDeletionMetadata{finalizerEstimateSeconds: 0, numRemaining: 0}, nil
}
return estimate, nil
// use the list to find the finalizers
finalizersToNumRemaining := map[string]int{}
for _, item := range unstructuredList.Items {
for _, finalizer := range item.GetFinalizers() {
finalizersToNumRemaining[finalizer] = finalizersToNumRemaining[finalizer] + 1
}
}
if estimate != int64(0) {
klog.V(5).Infof("namespace controller - deleteAllContentForGroupVersionResource - estimate is present - namespace: %s, gvr: %v, finalizers: %v", namespace, gvr, finalizersToNumRemaining)
return gvrDeletionMetadata{
finalizerEstimateSeconds: estimate,
numRemaining: len(unstructuredList.Items),
finalizersToNumRemaining: finalizersToNumRemaining,
}, nil
}
// if any item has a finalizer, we treat that as a normal condition, and use a default estimation to allow for GC to complete.
if len(finalizersToNumRemaining) > 0 {
klog.V(5).Infof("namespace controller - deleteAllContentForGroupVersionResource - items remaining with finalizers - namespace: %s, gvr: %v, finalizers: %v", namespace, gvr, finalizersToNumRemaining)
return gvrDeletionMetadata{
finalizerEstimateSeconds: finalizerEstimateSeconds,
numRemaining: len(unstructuredList.Items),
finalizersToNumRemaining: finalizersToNumRemaining,
}, nil
}
// nothing reported a finalizer, so something was unexpected as it should have been deleted.
return gvrDeletionMetadata{
finalizerEstimateSeconds: estimate,
numRemaining: len(unstructuredList.Items),
}, fmt.Errorf("unexpected items still remain in namespace: %s for gvr: %v", namespace, gvr)
}
type allGVRDeletionMetadata struct {
// gvrToNumRemaining is how many instances of the gvr remain
gvrToNumRemaining map[schema.GroupVersionResource]int
// finalizersToNumRemaining maps finalizers to how many resources are stuck on them
finalizersToNumRemaining map[string]int
}
// deleteAllContent will use the dynamic client to delete each resource identified in groupVersionResources.
@ -491,18 +533,33 @@ func (d *namespacedResourcesDeleter) deleteAllContent(ns *v1.Namespace) (int64,
errs = append(errs, err)
conditionUpdater.ProcessGroupVersionErr(err)
}
numRemainingTotals := allGVRDeletionMetadata{
gvrToNumRemaining: map[schema.GroupVersionResource]int{},
finalizersToNumRemaining: map[string]int{},
}
for gvr := range groupVersionResources {
gvrEstimate, err := d.deleteAllContentForGroupVersionResource(gvr, namespace, namespaceDeletedAt)
gvrDeletionMetadata, err := d.deleteAllContentForGroupVersionResource(gvr, namespace, namespaceDeletedAt)
if err != nil {
// If there is an error, hold on to it but proceed with all the remaining
// groupVersionResources.
errs = append(errs, err)
conditionUpdater.ProcessDeleteContentErr(err)
}
if gvrEstimate > estimate {
estimate = gvrEstimate
if gvrDeletionMetadata.finalizerEstimateSeconds > estimate {
estimate = gvrDeletionMetadata.finalizerEstimateSeconds
}
if gvrDeletionMetadata.numRemaining > 0 {
numRemainingTotals.gvrToNumRemaining[gvr] = gvrDeletionMetadata.numRemaining
for finalizer, numRemaining := range gvrDeletionMetadata.finalizersToNumRemaining {
if numRemaining == 0 {
continue
}
numRemainingTotals.finalizersToNumRemaining[finalizer] = numRemainingTotals.finalizersToNumRemaining[finalizer] + numRemaining
}
}
}
conditionUpdater.ProcessContentTotals(numRemainingTotals)
// we always want to update the conditions because if we have set a condition to "it worked" after it was previously, "it didn't work",
// we need to reflect that information. Recall that additional finalizers can be set on namespaces, so this finalizer may clear itself and
@ -529,7 +586,7 @@ func (d *namespacedResourcesDeleter) estimateGracefulTermination(gvr schema.Grou
estimate, err = d.estimateGracefulTerminationForPods(ns)
}
if err != nil {
return estimate, err
return 0, err
}
// determine if the estimate is greater than the deletion timestamp
duration := time.Since(namespaceDeletedAt.Time)
@ -546,11 +603,11 @@ func (d *namespacedResourcesDeleter) estimateGracefulTerminationForPods(ns strin
estimate := int64(0)
podsGetter := d.podsGetter
if podsGetter == nil || reflect.ValueOf(podsGetter).IsNil() {
return estimate, fmt.Errorf("unexpected: podsGetter is nil. Cannot estimate grace period seconds for pods")
return 0, fmt.Errorf("unexpected: podsGetter is nil. Cannot estimate grace period seconds for pods")
}
items, err := podsGetter.Pods(ns).List(metav1.ListOptions{})
if err != nil {
return estimate, err
return 0, err
}
for i := range items.Items {
pod := items.Items[i]

View File

@ -48,16 +48,22 @@ var (
v1.NamespaceDeletionDiscoveryFailure,
v1.NamespaceDeletionGVParsingFailure,
v1.NamespaceDeletionContentFailure,
v1.NamespaceContentRemaining,
v1.NamespaceFinalizersRemaining,
}
okMessages = map[v1.NamespaceConditionType]string{
v1.NamespaceDeletionDiscoveryFailure: "All resources successfully discovered",
v1.NamespaceDeletionGVParsingFailure: "All legacy kube types successfully parsed",
v1.NamespaceDeletionContentFailure: "All content successfully deleted",
v1.NamespaceDeletionContentFailure: "All content successfully deleted, may be waiting on finalization",
v1.NamespaceContentRemaining: "All content successfully removed",
v1.NamespaceFinalizersRemaining: "All content-preserving finalizers finished",
}
okReasons = map[v1.NamespaceConditionType]string{
v1.NamespaceDeletionDiscoveryFailure: "ResourcesDiscovered",
v1.NamespaceDeletionGVParsingFailure: "ParsedGroupVersions",
v1.NamespaceDeletionContentFailure: "ContentDeleted",
v1.NamespaceContentRemaining: "ContentRemoved",
v1.NamespaceFinalizersRemaining: "ContentHasNoFinalizers",
}
)
@ -92,6 +98,47 @@ func (u *namespaceConditionUpdater) ProcessDiscoverResourcesErr(err error) {
}
// ProcessContentTotals may create conditions for NamespaceContentRemaining and NamespaceFinalizersRemaining.
func (u *namespaceConditionUpdater) ProcessContentTotals(contentTotals allGVRDeletionMetadata) {
if len(contentTotals.gvrToNumRemaining) != 0 {
remainingResources := []string{}
for gvr, numRemaining := range contentTotals.gvrToNumRemaining {
if numRemaining == 0 {
continue
}
remainingResources = append(remainingResources, fmt.Sprintf("%s.%s has %d resource instances", gvr.Resource, gvr.Group, numRemaining))
}
// sort for stable updates
sort.Strings(remainingResources)
u.newConditions = append(u.newConditions, v1.NamespaceCondition{
Type: v1.NamespaceContentRemaining,
Status: v1.ConditionTrue,
LastTransitionTime: metav1.Now(),
Reason: "SomeResourcesRemain",
Message: fmt.Sprintf("Some resources are remaining: %s", strings.Join(remainingResources, ", ")),
})
}
if len(contentTotals.finalizersToNumRemaining) != 0 {
remainingByFinalizer := []string{}
for finalizer, numRemaining := range contentTotals.finalizersToNumRemaining {
if numRemaining == 0 {
continue
}
remainingByFinalizer = append(remainingByFinalizer, fmt.Sprintf("%s in %d resource instances", finalizer, numRemaining))
}
// sort for stable updates
sort.Strings(remainingByFinalizer)
u.newConditions = append(u.newConditions, v1.NamespaceCondition{
Type: v1.NamespaceFinalizersRemaining,
Status: v1.ConditionTrue,
LastTransitionTime: metav1.Now(),
Reason: "SomeFinalizersRemain",
Message: fmt.Sprintf("Some content in the namespace has finalizers remaining: %s", strings.Join(remainingByFinalizer, ", ")),
})
}
}
// ProcessDeleteContentErr creates error condition from multiple delete content errors.
func (u *namespaceConditionUpdater) ProcessDeleteContentErr(err error) {
u.deleteContentErrors = append(u.deleteContentErrors, err)

View File

@ -21,6 +21,7 @@ import (
"testing"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
)
func TestUpdateConditions(t *testing.T) {
@ -46,6 +47,8 @@ func TestUpdateConditions(t *testing.T) {
*newSuccessfulCondition(v1.NamespaceDeletionDiscoveryFailure),
*newSuccessfulCondition(v1.NamespaceDeletionGVParsingFailure),
*newSuccessfulCondition(v1.NamespaceDeletionContentFailure),
*newSuccessfulCondition(v1.NamespaceContentRemaining),
*newSuccessfulCondition(v1.NamespaceFinalizersRemaining),
},
},
{
@ -61,6 +64,8 @@ func TestUpdateConditions(t *testing.T) {
*newSuccessfulCondition(v1.NamespaceDeletionDiscoveryFailure),
*newSuccessfulCondition(v1.NamespaceDeletionGVParsingFailure),
*newSuccessfulCondition(v1.NamespaceDeletionContentFailure),
*newSuccessfulCondition(v1.NamespaceContentRemaining),
*newSuccessfulCondition(v1.NamespaceFinalizersRemaining),
},
},
{
@ -77,6 +82,8 @@ func TestUpdateConditions(t *testing.T) {
*newSuccessfulCondition(v1.NamespaceDeletionGVParsingFailure),
*newSuccessfulCondition(v1.NamespaceDeletionDiscoveryFailure),
*newSuccessfulCondition(v1.NamespaceDeletionContentFailure),
*newSuccessfulCondition(v1.NamespaceContentRemaining),
*newSuccessfulCondition(v1.NamespaceFinalizersRemaining),
},
},
{
@ -95,6 +102,8 @@ func TestUpdateConditions(t *testing.T) {
{Type: v1.NamespaceDeletionGVParsingFailure, Status: v1.ConditionTrue, Reason: "foo", Message: "bar"},
*newSuccessfulCondition(v1.NamespaceDeletionDiscoveryFailure),
*newSuccessfulCondition(v1.NamespaceDeletionContentFailure),
*newSuccessfulCondition(v1.NamespaceContentRemaining),
*newSuccessfulCondition(v1.NamespaceFinalizersRemaining),
},
},
{
@ -112,6 +121,8 @@ func TestUpdateConditions(t *testing.T) {
*newSuccessfulCondition(v1.NamespaceDeletionDiscoveryFailure),
{Type: v1.NamespaceDeletionGVParsingFailure, Status: v1.ConditionTrue, Reason: "foo", Message: "bar"},
*newSuccessfulCondition(v1.NamespaceDeletionContentFailure),
*newSuccessfulCondition(v1.NamespaceContentRemaining),
*newSuccessfulCondition(v1.NamespaceFinalizersRemaining),
},
},
}
@ -135,3 +146,89 @@ func TestUpdateConditions(t *testing.T) {
})
}
}
func TestProcessContentTotals(t *testing.T) {
tests := []struct {
name string
contentTotals allGVRDeletionMetadata
expecteds []v1.NamespaceCondition
}{
{
name: "nothing",
contentTotals: allGVRDeletionMetadata{
gvrToNumRemaining: map[schema.GroupVersionResource]int{},
finalizersToNumRemaining: map[string]int{},
},
expecteds: []v1.NamespaceCondition{},
},
{
name: "just remaining",
contentTotals: allGVRDeletionMetadata{
gvrToNumRemaining: map[schema.GroupVersionResource]int{
{Group: "apps.k8s.io", Resource: "daemonsets"}: 5,
{Group: "apps.k8s.io", Resource: "deployments"}: 5,
},
finalizersToNumRemaining: map[string]int{},
},
expecteds: []v1.NamespaceCondition{
{Type: v1.NamespaceContentRemaining, Status: v1.ConditionTrue, Reason: "SomeResourcesRemain", Message: `Some resources are remaining: daemonsets.apps.k8s.io has 5 resource instances, deployments.apps.k8s.io has 5 resource instances`},
},
},
{
name: "just finalizers", // this shouldn't happen
contentTotals: allGVRDeletionMetadata{
gvrToNumRemaining: map[schema.GroupVersionResource]int{},
finalizersToNumRemaining: map[string]int{
"service-catalog": 6,
"kubedb": 5,
},
},
expecteds: []v1.NamespaceCondition{
{Type: v1.NamespaceFinalizersRemaining, Status: v1.ConditionTrue, Reason: "SomeFinalizersRemain", Message: `Some content in the namespace has finalizers remaining: kubedb in 5 resource instances, service-catalog in 6 resource instances`},
},
},
{
name: "both",
contentTotals: allGVRDeletionMetadata{
gvrToNumRemaining: map[schema.GroupVersionResource]int{
{Group: "apps.k8s.io", Resource: "daemonsets"}: 5,
{Group: "apps.k8s.io", Resource: "deployments"}: 5,
},
finalizersToNumRemaining: map[string]int{
"service-catalog": 6,
"kubedb": 5,
},
},
expecteds: []v1.NamespaceCondition{
{Type: v1.NamespaceContentRemaining, Status: v1.ConditionTrue, Reason: "SomeResourcesRemain", Message: `Some resources are remaining: daemonsets.apps.k8s.io has 5 resource instances, deployments.apps.k8s.io has 5 resource instances`},
{Type: v1.NamespaceFinalizersRemaining, Status: v1.ConditionTrue, Reason: "SomeFinalizersRemain", Message: `Some content in the namespace has finalizers remaining: kubedb in 5 resource instances, service-catalog in 6 resource instances`},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
u := namespaceConditionUpdater{}
u.ProcessContentTotals(test.contentTotals)
actuals := u.newConditions
if len(actuals) != len(test.expecteds) {
t.Fatal(actuals)
}
for i := range actuals {
actual := actuals[i]
expected := test.expecteds[i]
expected.LastTransitionTime = actual.LastTransitionTime
if !reflect.DeepEqual(expected, actual) {
t.Error(actual)
}
}
})
}
}

View File

@ -4670,6 +4670,10 @@ const (
NamespaceDeletionContentFailure NamespaceConditionType = "NamespaceDeletionContentFailure"
// NamespaceDeletionGVParsingFailure contains information about namespace deleter errors parsing GV for legacy types.
NamespaceDeletionGVParsingFailure NamespaceConditionType = "NamespaceDeletionGroupVersionParsingFailure"
// NamespaceContentRemaining contains information about resources remaining in a namespace.
NamespaceContentRemaining NamespaceConditionType = "NamespaceContentRemaining"
// NamespaceFinalizersRemaining contains information about which finalizers are on resources remaining in a namespace.
NamespaceFinalizersRemaining NamespaceConditionType = "NamespaceFinalizersRemaining"
)
// NamespaceCondition contains details about state of namespace.

View File

@ -87,23 +87,27 @@ func TestNamespaceCondition(t *testing.T) {
return false, err
}
foundContentCondition := false
foundFinalizerCondition := false
conditionsFound := 0
for _, condition := range curr.Status.Conditions {
if condition.Type == corev1.NamespaceDeletionGVParsingFailure && condition.Message == `All legacy kube types successfully parsed` {
foundContentCondition = true
conditionsFound++
}
if condition.Type == corev1.NamespaceDeletionDiscoveryFailure && condition.Message == `All resources successfully discovered` {
foundFinalizerCondition = true
conditionsFound++
}
if condition.Type == corev1.NamespaceDeletionContentFailure && condition.Message == `All content successfully deleted` {
foundFinalizerCondition = true
if condition.Type == corev1.NamespaceDeletionContentFailure && condition.Message == `All content successfully deleted, may be waiting on finalization` {
conditionsFound++
}
if condition.Type == corev1.NamespaceContentRemaining && condition.Message == `Some resources are remaining: deployments.apps has 1 resource instances` {
conditionsFound++
}
if condition.Type == corev1.NamespaceFinalizersRemaining && condition.Message == `Some content in the namespace has finalizers remaining: custom.io/finalizer in 1 resource instances` {
conditionsFound++
}
}
t.Log(spew.Sdump(curr))
return foundContentCondition && foundFinalizerCondition, nil
return conditionsFound == 5, nil
})
if err != nil {
t.Fatal(err)