Skip to content

Commit c637838

Browse files
authored
Merge pull request #411 from fluxcd/skip-gc-for-ownerReference
Skip garbage collection of objects with owner references
2 parents 1032b6b + 16c451b commit c637838

File tree

4 files changed

+189
-4
lines changed

4 files changed

+189
-4
lines changed

controllers/kustomization_controller_gc_test.go

Lines changed: 158 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ var _ = Describe("KustomizationReconciler", func() {
112112
Expect(k8sClient.Delete(context.Background(), namespace)).To(Succeed())
113113
})
114114

115-
It("garbage collects deleted manifests", func() {
115+
It("collects deleted manifests", func() {
116116
configMapManifest := func(name string) string {
117117
return fmt.Sprintf(`---
118118
apiVersion: v1
@@ -176,6 +176,163 @@ data:
176176
Expect(apierrors.IsNotFound(err)).To(BeTrue())
177177
})
178178

179+
It("skips objects with blockOwnerDeletion=true", func() {
180+
configMapManifest := func(name string) string {
181+
return fmt.Sprintf(`---
182+
apiVersion: v1
183+
kind: ConfigMap
184+
metadata:
185+
name: %[1]s
186+
data:
187+
value: %[1]s
188+
`, name)
189+
}
190+
manifest := testserver.File{Name: "configmap.yaml", Body: configMapManifest("first")}
191+
artifact, err := artifactServer.ArtifactFromFiles([]testserver.File{manifest})
192+
Expect(err).ToNot(HaveOccurred())
193+
artifactURL, err := artifactServer.URLForFile(artifact)
194+
Expect(err).ToNot(HaveOccurred())
195+
196+
gitRepo.Status.Artifact.URL = artifactURL
197+
gitRepo.Status.Artifact.Revision = "first"
198+
199+
Expect(k8sClient.Create(context.Background(), gitRepo)).To(Succeed())
200+
Expect(k8sClient.Status().Update(context.Background(), gitRepo)).To(Succeed())
201+
Expect(k8sClient.Create(context.Background(), kustomization)).To(Succeed())
202+
203+
var got kustomizev1.Kustomization
204+
Eventually(func() bool {
205+
_ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), &got)
206+
c := apimeta.FindStatusCondition(got.Status.Conditions, meta.ReadyCondition)
207+
return c != nil && c.Reason == meta.ReconciliationSucceededReason
208+
}, timeout, time.Second).Should(BeTrue())
209+
210+
var configMap corev1.ConfigMap
211+
Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: "first", Namespace: namespace.Name}, &configMap)).To(Succeed())
212+
213+
owner := &corev1.ServiceAccount{
214+
ObjectMeta: metav1.ObjectMeta{
215+
Name: "test",
216+
Namespace: namespace.Name,
217+
},
218+
}
219+
Expect(k8sClient.Create(context.Background(), owner)).To(Succeed())
220+
221+
sa := &corev1.ServiceAccount{}
222+
objName := types.NamespacedName{Name: "test", Namespace: namespace.Name}
223+
Expect(k8sClient.Get(context.Background(), objName, sa)).To(Succeed())
224+
225+
blockOwnerDeletion := true
226+
owned := &corev1.ConfigMap{
227+
TypeMeta: metav1.TypeMeta{},
228+
ObjectMeta: metav1.ObjectMeta{
229+
Name: "test",
230+
Namespace: namespace.Name,
231+
Labels: configMap.GetLabels(),
232+
Annotations: configMap.GetAnnotations(),
233+
OwnerReferences: []metav1.OwnerReference{
234+
{
235+
APIVersion: "v1",
236+
Kind: "ServiceAccount",
237+
Name: sa.Name,
238+
UID: sa.UID,
239+
Controller: &blockOwnerDeletion,
240+
BlockOwnerDeletion: &blockOwnerDeletion,
241+
},
242+
},
243+
},
244+
}
245+
Expect(k8sClient.Create(context.Background(), owned)).To(Succeed())
246+
247+
Expect(k8sClient.Delete(context.Background(), kustomization)).To(Succeed())
248+
Eventually(func() bool {
249+
err = k8sClient.Get(context.Background(), client.ObjectKey{Name: kustomization.Name, Namespace: namespace.Name}, kustomization)
250+
return apierrors.IsNotFound(err)
251+
}, timeout, time.Second).Should(BeTrue())
252+
253+
cf := &corev1.ConfigMap{}
254+
Expect(k8sClient.Get(context.Background(), objName, cf)).To(Succeed())
255+
})
256+
257+
It("deletes objects with blockOwnerDeletion=false", func() {
258+
configMapManifest := func(name string) string {
259+
return fmt.Sprintf(`---
260+
apiVersion: v1
261+
kind: ConfigMap
262+
metadata:
263+
name: %[1]s
264+
data:
265+
value: %[1]s
266+
`, name)
267+
}
268+
manifest := testserver.File{Name: "configmap.yaml", Body: configMapManifest("first")}
269+
artifact, err := artifactServer.ArtifactFromFiles([]testserver.File{manifest})
270+
Expect(err).ToNot(HaveOccurred())
271+
artifactURL, err := artifactServer.URLForFile(artifact)
272+
Expect(err).ToNot(HaveOccurred())
273+
274+
gitRepo.Status.Artifact.URL = artifactURL
275+
gitRepo.Status.Artifact.Revision = "first"
276+
277+
Expect(k8sClient.Create(context.Background(), gitRepo)).To(Succeed())
278+
Expect(k8sClient.Status().Update(context.Background(), gitRepo)).To(Succeed())
279+
Expect(k8sClient.Create(context.Background(), kustomization)).To(Succeed())
280+
281+
var got kustomizev1.Kustomization
282+
Eventually(func() bool {
283+
_ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), &got)
284+
c := apimeta.FindStatusCondition(got.Status.Conditions, meta.ReadyCondition)
285+
return c != nil && c.Reason == meta.ReconciliationSucceededReason
286+
}, timeout, time.Second).Should(BeTrue())
287+
288+
var configMap corev1.ConfigMap
289+
Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: "first", Namespace: namespace.Name}, &configMap)).To(Succeed())
290+
291+
owner := &corev1.ServiceAccount{
292+
ObjectMeta: metav1.ObjectMeta{
293+
Name: "test",
294+
Namespace: namespace.Name,
295+
},
296+
}
297+
Expect(k8sClient.Create(context.Background(), owner)).To(Succeed())
298+
299+
sa := &corev1.ServiceAccount{}
300+
objName := types.NamespacedName{Name: "test", Namespace: namespace.Name}
301+
Expect(k8sClient.Get(context.Background(), objName, sa)).To(Succeed())
302+
303+
blockOwnerDeletion := false
304+
owned := &corev1.ConfigMap{
305+
TypeMeta: metav1.TypeMeta{},
306+
ObjectMeta: metav1.ObjectMeta{
307+
Name: "test",
308+
Namespace: namespace.Name,
309+
Labels: configMap.GetLabels(),
310+
Annotations: configMap.GetAnnotations(),
311+
OwnerReferences: []metav1.OwnerReference{
312+
{
313+
APIVersion: "v1",
314+
Kind: "ServiceAccount",
315+
Name: sa.Name,
316+
UID: sa.UID,
317+
Controller: &blockOwnerDeletion,
318+
BlockOwnerDeletion: &blockOwnerDeletion,
319+
},
320+
},
321+
},
322+
}
323+
Expect(k8sClient.Create(context.Background(), owned)).To(Succeed())
324+
325+
Expect(k8sClient.Delete(context.Background(), kustomization)).To(Succeed())
326+
Eventually(func() bool {
327+
err = k8sClient.Get(context.Background(), client.ObjectKey{Name: kustomization.Name, Namespace: namespace.Name}, kustomization)
328+
return apierrors.IsNotFound(err)
329+
}, timeout, time.Second).Should(BeTrue())
330+
331+
cf := &corev1.ConfigMap{}
332+
err = k8sClient.Get(context.Background(), objName, cf)
333+
Expect(apierrors.IsNotFound(err)).To(BeTrue())
334+
})
335+
179336
It("skips deleted manifests labeled with prune disabled", func() {
180337
configMapManifest := func(name string, skip string) string {
181338
return fmt.Sprintf(`---

controllers/kustomization_gc.go

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,17 @@ func (kgc *KustomizeGarbageCollector) Prune(timeout time.Duration, name string,
7171
if err == nil {
7272
for _, item := range ulist.Items {
7373
id := fmt.Sprintf("%s/%s/%s", item.GetKind(), item.GetNamespace(), item.GetName())
74+
7475
if kgc.shouldSkip(item) {
7576
kgc.log.V(1).Info(fmt.Sprintf("gc is disabled for '%s'", id))
7677
continue
7778
}
7879

80+
if kgc.hasBlockOwnerDeletion(item) {
81+
kgc.log.V(1).Info(fmt.Sprintf("gc is disabled for '%s' due to 'ownerReference.blockOwnerDeletion=true'", id))
82+
continue
83+
}
84+
7985
if kgc.isStale(item) && item.GetDeletionTimestamp().IsZero() {
8086
err = kgc.Delete(ctx, &item)
8187
if err != nil {
@@ -113,6 +119,11 @@ func (kgc *KustomizeGarbageCollector) Prune(timeout time.Duration, name string,
113119
continue
114120
}
115121

122+
if kgc.hasBlockOwnerDeletion(item) {
123+
kgc.log.V(1).Info(fmt.Sprintf("gc is disabled for '%s' due to 'ownerReference.blockOwnerDeletion=true'", id))
124+
continue
125+
}
126+
116127
if kgc.isStale(item) && item.GetDeletionTimestamp().IsZero() {
117128
err = kgc.Delete(ctx, &item)
118129
if err != nil {
@@ -142,13 +153,25 @@ func (kgc *KustomizeGarbageCollector) isStale(obj unstructured.Unstructured) boo
142153
itemAnnotationChecksum := obj.GetAnnotations()[fmt.Sprintf("%s/checksum", kustomizev1.GroupVersion.Group)]
143154

144155
switch kgc.newChecksum {
156+
// when the Kustomization is deleted the new checksum is set to string empty making all objects stale
145157
case "":
146158
return true
159+
// skip GC if the new checksum matches the object checksum
147160
case itemAnnotationChecksum:
148161
return false
149-
default:
150-
return true
151162
}
163+
164+
// skip GC if the checksum annotation is missing from the object
165+
return itemAnnotationChecksum != ""
166+
}
167+
168+
func (kgc *KustomizeGarbageCollector) hasBlockOwnerDeletion(obj unstructured.Unstructured) bool {
169+
for _, ownerReference := range obj.GetOwnerReferences() {
170+
if bod := ownerReference.BlockOwnerDeletion; bod != nil && *bod == true {
171+
return true
172+
}
173+
}
174+
return false
152175
}
153176

154177
func (kgc *KustomizeGarbageCollector) shouldSkip(obj unstructured.Unstructured) bool {

controllers/suite_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,9 @@ var _ = BeforeSuite(func(done Done) {
113113
Expect(err).ToNot(HaveOccurred())
114114
}()
115115

116-
k8sClient = k8sManager.GetClient()
116+
// client with caching disabled
117+
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
118+
Expect(err).ToNot(HaveOccurred())
117119
Expect(k8sClient).ToNot(BeNil())
118120

119121
close(done)

docs/spec/v1beta1/kustomization.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,9 @@ labeling or annotating them with:
390390
kustomize.toolkit.fluxcd.io/prune: disabled
391391
```
392392

393+
Note that Kubernetes objects generated by other controllers that have `ownerReference.blockOwnerDeletion=true`
394+
are skipped from garbage collection.
395+
393396
## Health assessment
394397

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

0 commit comments

Comments
 (0)