Skip to content

Commit

Permalink
Merge pull request #411 from fluxcd/skip-gc-for-ownerReference
Browse files Browse the repository at this point in the history
Skip garbage collection of objects with owner references
  • Loading branch information
stefanprodan authored Aug 26, 2021
2 parents 1032b6b + 16c451b commit c637838
Show file tree
Hide file tree
Showing 4 changed files with 189 additions and 4 deletions.
159 changes: 158 additions & 1 deletion controllers/kustomization_controller_gc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ var _ = Describe("KustomizationReconciler", func() {
Expect(k8sClient.Delete(context.Background(), namespace)).To(Succeed())
})

It("garbage collects deleted manifests", func() {
It("collects deleted manifests", func() {
configMapManifest := func(name string) string {
return fmt.Sprintf(`---
apiVersion: v1
Expand Down Expand Up @@ -176,6 +176,163 @@ data:
Expect(apierrors.IsNotFound(err)).To(BeTrue())
})

It("skips objects with blockOwnerDeletion=true", func() {
configMapManifest := func(name string) string {
return fmt.Sprintf(`---
apiVersion: v1
kind: ConfigMap
metadata:
name: %[1]s
data:
value: %[1]s
`, name)
}
manifest := testserver.File{Name: "configmap.yaml", Body: configMapManifest("first")}
artifact, err := artifactServer.ArtifactFromFiles([]testserver.File{manifest})
Expect(err).ToNot(HaveOccurred())
artifactURL, err := artifactServer.URLForFile(artifact)
Expect(err).ToNot(HaveOccurred())

gitRepo.Status.Artifact.URL = artifactURL
gitRepo.Status.Artifact.Revision = "first"

Expect(k8sClient.Create(context.Background(), gitRepo)).To(Succeed())
Expect(k8sClient.Status().Update(context.Background(), gitRepo)).To(Succeed())
Expect(k8sClient.Create(context.Background(), kustomization)).To(Succeed())

var got kustomizev1.Kustomization
Eventually(func() bool {
_ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), &got)
c := apimeta.FindStatusCondition(got.Status.Conditions, meta.ReadyCondition)
return c != nil && c.Reason == meta.ReconciliationSucceededReason
}, timeout, time.Second).Should(BeTrue())

var configMap corev1.ConfigMap
Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: "first", Namespace: namespace.Name}, &configMap)).To(Succeed())

owner := &corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: namespace.Name,
},
}
Expect(k8sClient.Create(context.Background(), owner)).To(Succeed())

sa := &corev1.ServiceAccount{}
objName := types.NamespacedName{Name: "test", Namespace: namespace.Name}
Expect(k8sClient.Get(context.Background(), objName, sa)).To(Succeed())

blockOwnerDeletion := true
owned := &corev1.ConfigMap{
TypeMeta: metav1.TypeMeta{},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: namespace.Name,
Labels: configMap.GetLabels(),
Annotations: configMap.GetAnnotations(),
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: "v1",
Kind: "ServiceAccount",
Name: sa.Name,
UID: sa.UID,
Controller: &blockOwnerDeletion,
BlockOwnerDeletion: &blockOwnerDeletion,
},
},
},
}
Expect(k8sClient.Create(context.Background(), owned)).To(Succeed())

Expect(k8sClient.Delete(context.Background(), kustomization)).To(Succeed())
Eventually(func() bool {
err = k8sClient.Get(context.Background(), client.ObjectKey{Name: kustomization.Name, Namespace: namespace.Name}, kustomization)
return apierrors.IsNotFound(err)
}, timeout, time.Second).Should(BeTrue())

cf := &corev1.ConfigMap{}
Expect(k8sClient.Get(context.Background(), objName, cf)).To(Succeed())
})

It("deletes objects with blockOwnerDeletion=false", func() {
configMapManifest := func(name string) string {
return fmt.Sprintf(`---
apiVersion: v1
kind: ConfigMap
metadata:
name: %[1]s
data:
value: %[1]s
`, name)
}
manifest := testserver.File{Name: "configmap.yaml", Body: configMapManifest("first")}
artifact, err := artifactServer.ArtifactFromFiles([]testserver.File{manifest})
Expect(err).ToNot(HaveOccurred())
artifactURL, err := artifactServer.URLForFile(artifact)
Expect(err).ToNot(HaveOccurred())

gitRepo.Status.Artifact.URL = artifactURL
gitRepo.Status.Artifact.Revision = "first"

Expect(k8sClient.Create(context.Background(), gitRepo)).To(Succeed())
Expect(k8sClient.Status().Update(context.Background(), gitRepo)).To(Succeed())
Expect(k8sClient.Create(context.Background(), kustomization)).To(Succeed())

var got kustomizev1.Kustomization
Eventually(func() bool {
_ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), &got)
c := apimeta.FindStatusCondition(got.Status.Conditions, meta.ReadyCondition)
return c != nil && c.Reason == meta.ReconciliationSucceededReason
}, timeout, time.Second).Should(BeTrue())

var configMap corev1.ConfigMap
Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: "first", Namespace: namespace.Name}, &configMap)).To(Succeed())

owner := &corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: namespace.Name,
},
}
Expect(k8sClient.Create(context.Background(), owner)).To(Succeed())

sa := &corev1.ServiceAccount{}
objName := types.NamespacedName{Name: "test", Namespace: namespace.Name}
Expect(k8sClient.Get(context.Background(), objName, sa)).To(Succeed())

blockOwnerDeletion := false
owned := &corev1.ConfigMap{
TypeMeta: metav1.TypeMeta{},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: namespace.Name,
Labels: configMap.GetLabels(),
Annotations: configMap.GetAnnotations(),
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: "v1",
Kind: "ServiceAccount",
Name: sa.Name,
UID: sa.UID,
Controller: &blockOwnerDeletion,
BlockOwnerDeletion: &blockOwnerDeletion,
},
},
},
}
Expect(k8sClient.Create(context.Background(), owned)).To(Succeed())

Expect(k8sClient.Delete(context.Background(), kustomization)).To(Succeed())
Eventually(func() bool {
err = k8sClient.Get(context.Background(), client.ObjectKey{Name: kustomization.Name, Namespace: namespace.Name}, kustomization)
return apierrors.IsNotFound(err)
}, timeout, time.Second).Should(BeTrue())

cf := &corev1.ConfigMap{}
err = k8sClient.Get(context.Background(), objName, cf)
Expect(apierrors.IsNotFound(err)).To(BeTrue())
})

It("skips deleted manifests labeled with prune disabled", func() {
configMapManifest := func(name string, skip string) string {
return fmt.Sprintf(`---
Expand Down
27 changes: 25 additions & 2 deletions controllers/kustomization_gc.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,17 @@ func (kgc *KustomizeGarbageCollector) Prune(timeout time.Duration, name string,
if err == nil {
for _, item := range ulist.Items {
id := fmt.Sprintf("%s/%s/%s", item.GetKind(), item.GetNamespace(), item.GetName())

if kgc.shouldSkip(item) {
kgc.log.V(1).Info(fmt.Sprintf("gc is disabled for '%s'", id))
continue
}

if kgc.hasBlockOwnerDeletion(item) {
kgc.log.V(1).Info(fmt.Sprintf("gc is disabled for '%s' due to 'ownerReference.blockOwnerDeletion=true'", id))
continue
}

if kgc.isStale(item) && item.GetDeletionTimestamp().IsZero() {
err = kgc.Delete(ctx, &item)
if err != nil {
Expand Down Expand Up @@ -113,6 +119,11 @@ func (kgc *KustomizeGarbageCollector) Prune(timeout time.Duration, name string,
continue
}

if kgc.hasBlockOwnerDeletion(item) {
kgc.log.V(1).Info(fmt.Sprintf("gc is disabled for '%s' due to 'ownerReference.blockOwnerDeletion=true'", id))
continue
}

if kgc.isStale(item) && item.GetDeletionTimestamp().IsZero() {
err = kgc.Delete(ctx, &item)
if err != nil {
Expand Down Expand Up @@ -142,13 +153,25 @@ func (kgc *KustomizeGarbageCollector) isStale(obj unstructured.Unstructured) boo
itemAnnotationChecksum := obj.GetAnnotations()[fmt.Sprintf("%s/checksum", kustomizev1.GroupVersion.Group)]

switch kgc.newChecksum {
// when the Kustomization is deleted the new checksum is set to string empty making all objects stale
case "":
return true
// skip GC if the new checksum matches the object checksum
case itemAnnotationChecksum:
return false
default:
return true
}

// skip GC if the checksum annotation is missing from the object
return itemAnnotationChecksum != ""
}

func (kgc *KustomizeGarbageCollector) hasBlockOwnerDeletion(obj unstructured.Unstructured) bool {
for _, ownerReference := range obj.GetOwnerReferences() {
if bod := ownerReference.BlockOwnerDeletion; bod != nil && *bod == true {
return true
}
}
return false
}

func (kgc *KustomizeGarbageCollector) shouldSkip(obj unstructured.Unstructured) bool {
Expand Down
4 changes: 3 additions & 1 deletion controllers/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,9 @@ var _ = BeforeSuite(func(done Done) {
Expect(err).ToNot(HaveOccurred())
}()

k8sClient = k8sManager.GetClient()
// client with caching disabled
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
Expect(err).ToNot(HaveOccurred())
Expect(k8sClient).ToNot(BeNil())

close(done)
Expand Down
3 changes: 3 additions & 0 deletions docs/spec/v1beta1/kustomization.md
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,9 @@ labeling or annotating them with:
kustomize.toolkit.fluxcd.io/prune: disabled
```

Note that Kubernetes objects generated by other controllers that have `ownerReference.blockOwnerDeletion=true`
are skipped from garbage collection.

## Health assessment

A Kustomization can contain a series of health checks used to determine the
Expand Down

0 comments on commit c637838

Please sign in to comment.