Skip to content

Commit c38ebab

Browse files
erikgbstefanprodantenstad
committed
Allow control of finalization garbage collection
Signed-off-by: Erik Godding Boye <[email protected]> Co-authored-by: Stefan Prodan <[email protected]> Co-authored-by: Amund Tenstad <[email protected]>
1 parent a87337c commit c38ebab

File tree

6 files changed

+266
-1
lines changed

6 files changed

+266
-1
lines changed

api/v1/kustomization_types.go

+20
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ const (
3333
MergeValue = "Merge"
3434
IfNotPresentValue = "IfNotPresent"
3535
IgnoreValue = "Ignore"
36+
37+
DeletionPolicyMirrorPrune = "MirrorPrune"
38+
DeletionPolicyDelete = "Delete"
39+
DeletionPolicyOrphan = "Orphan"
3640
)
3741

3842
// KustomizationSpec defines the configuration to calculate the desired state
@@ -95,6 +99,14 @@ type KustomizationSpec struct {
9599
// +required
96100
Prune bool `json:"prune"`
97101

102+
// DeletionPolicy can be used to control garbage collection when this
103+
// Kustomization is deleted. Valid values are ('MirrorPrune', 'Delete',
104+
// 'Orphan'). 'MirrorPrune' mirrors the Prune field (orphan if false,
105+
// delete if true). Defaults to 'MirrorPrune'.
106+
// +kubebuilder:validation:Enum=MirrorPrune;Delete;Orphan
107+
// +optional
108+
DeletionPolicy string `json:"deletionPolicy,omitempty"`
109+
98110
// A list of resources to be included in the health assessment.
99111
// +optional
100112
HealthChecks []meta.NamespacedObjectKindReference `json:"healthChecks,omitempty"`
@@ -287,6 +299,14 @@ func (in Kustomization) GetRequeueAfter() time.Duration {
287299
return in.Spec.Interval.Duration
288300
}
289301

302+
// GetDeletionPolicy returns the deletion policy and default value if not specified.
303+
func (in Kustomization) GetDeletionPolicy() string {
304+
if in.Spec.DeletionPolicy == "" {
305+
return DeletionPolicyMirrorPrune
306+
}
307+
return in.Spec.DeletionPolicy
308+
}
309+
290310
// GetDependsOn returns the list of dependencies across-namespaces.
291311
func (in Kustomization) GetDependsOn() []meta.NamespacedObjectReference {
292312
return in.Spec.DependsOn

config/crd/bases/kustomize.toolkit.fluxcd.io_kustomizations.yaml

+11
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,17 @@ spec:
9898
required:
9999
- provider
100100
type: object
101+
deletionPolicy:
102+
description: |-
103+
DeletionPolicy can be used to control garbage collection when this
104+
Kustomization is deleted. Valid values are ('MirrorPrune', 'Delete',
105+
'Orphan'). 'MirrorPrune' mirrors the Prune field (orphan if false,
106+
delete if true). Defaults to 'MirrorPrune'.
107+
enum:
108+
- MirrorPrune
109+
- Delete
110+
- Orphan
111+
type: string
101112
dependsOn:
102113
description: |-
103114
DependsOn may contain a meta.NamespacedObjectReference slice

docs/api/v1/kustomize.md

+30
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,21 @@ bool
208208
</tr>
209209
<tr>
210210
<td>
211+
<code>deletionPolicy</code><br>
212+
<em>
213+
string
214+
</em>
215+
</td>
216+
<td>
217+
<em>(Optional)</em>
218+
<p>DeletionPolicy can be used to control garbage collection when this
219+
Kustomization is deleted. Valid values are (&lsquo;MirrorPrune&rsquo;, &lsquo;Delete&rsquo;,
220+
&lsquo;Orphan&rsquo;). &lsquo;MirrorPrune&rsquo; mirrors the Prune field (orphan if false,
221+
delete if true). Defaults to &lsquo;MirrorPrune&rsquo;.</p>
222+
</td>
223+
</tr>
224+
<tr>
225+
<td>
211226
<code>healthChecks</code><br>
212227
<em>
213228
<a href="https://godoc.org/github.com/fluxcd/pkg/apis/meta#NamespacedObjectKindReference">
@@ -716,6 +731,21 @@ bool
716731
</tr>
717732
<tr>
718733
<td>
734+
<code>deletionPolicy</code><br>
735+
<em>
736+
string
737+
</em>
738+
</td>
739+
<td>
740+
<em>(Optional)</em>
741+
<p>DeletionPolicy can be used to control garbage collection when this
742+
Kustomization is deleted. Valid values are (&lsquo;MirrorPrune&rsquo;, &lsquo;Delete&rsquo;,
743+
&lsquo;Orphan&rsquo;). &lsquo;MirrorPrune&rsquo; mirrors the Prune field (orphan if false,
744+
delete if true). Defaults to &lsquo;MirrorPrune&rsquo;.</p>
745+
</td>
746+
</tr>
747+
<tr>
748+
<td>
719749
<code>healthChecks</code><br>
720750
<em>
721751
<a href="https://godoc.org/github.com/fluxcd/pkg/apis/meta#NamespacedObjectKindReference">

docs/spec/v1/kustomizations.md

+33
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,39 @@ kustomize.toolkit.fluxcd.io/prune: disabled
169169
For details on how the controller tracks Kubernetes objects and determines what
170170
to garbage collect, see [`.status.inventory`](#inventory).
171171

172+
### Deletion policy
173+
174+
`.spec.deletionPolicy` is an optional field that allows control over
175+
garbage collection when a Kustomization object is deleted. The default behavior
176+
is to mirror the configuration of [`.spec.prune`](#prune).
177+
178+
Valid values:
179+
180+
- `MirrorPrune` (default) - The managed resources will be deleted if `prune` is
181+
`true` and orphaned if `false`.
182+
- `Delete` - Ensure the managed resources are deleted before the Kustomization
183+
is deleted.
184+
- `Orphan` - Leave the managed resources when the Kustomization is deleted.
185+
186+
For special cases when the managed resources are removed by other means (e.g.
187+
the deletion of the namespace specified with
188+
[`.spec.targetNamespace`](#target-namespace)), you can set the deletion policy
189+
to `Orphan`:
190+
191+
```yaml
192+
---
193+
apiVersion: kustomize.toolkit.fluxcd.io/v1
194+
kind: Kustomization
195+
metadata:
196+
name: app
197+
namespace: default
198+
spec:
199+
# ...omitted for brevity
200+
targetNamespace: app-namespace
201+
prune: true
202+
deletionPolicy: Orphan
203+
```
204+
172205
### Interval
173206

174207
`.spec.interval` is a required field that specifies the interval at which the

internal/controller/kustomization_controller.go

+8-1
Original file line numberDiff line numberDiff line change
@@ -956,10 +956,17 @@ func (r *KustomizationReconciler) prune(ctx context.Context,
956956
return false, nil
957957
}
958958

959+
func finalizerShouldDeleteResources(obj *kustomizev1.Kustomization) bool {
960+
if obj.GetDeletionPolicy() == kustomizev1.DeletionPolicyMirrorPrune {
961+
return obj.Spec.Prune
962+
}
963+
return obj.Spec.DeletionPolicy == kustomizev1.DeletionPolicyDelete
964+
}
965+
959966
func (r *KustomizationReconciler) finalize(ctx context.Context,
960967
obj *kustomizev1.Kustomization) (ctrl.Result, error) {
961968
log := ctrl.LoggerFrom(ctx)
962-
if obj.Spec.Prune &&
969+
if finalizerShouldDeleteResources(obj) &&
963970
!obj.Spec.Suspend &&
964971
obj.Status.Inventory != nil &&
965972
obj.Status.Inventory.Entries != nil {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/*
2+
Copyright 2024 The Flux authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package controller
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"testing"
23+
"time"
24+
25+
"github.com/fluxcd/pkg/apis/meta"
26+
"github.com/fluxcd/pkg/testserver"
27+
sourcev1 "github.com/fluxcd/source-controller/api/v1"
28+
. "github.com/onsi/gomega"
29+
corev1 "k8s.io/api/core/v1"
30+
apierrors "k8s.io/apimachinery/pkg/api/errors"
31+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
32+
"k8s.io/apimachinery/pkg/types"
33+
"sigs.k8s.io/controller-runtime/pkg/client"
34+
35+
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
36+
)
37+
38+
func TestKustomizationReconciler_DeletionPolicyDelete(t *testing.T) {
39+
tests := []struct {
40+
name string
41+
prune bool
42+
deletionPolicy string
43+
wantDelete bool
44+
}{
45+
{
46+
name: "should delete when deletionPolicy overrides pruning disabled",
47+
prune: false,
48+
deletionPolicy: kustomizev1.DeletionPolicyDelete,
49+
wantDelete: true,
50+
},
51+
{
52+
name: "should delete when deletionPolicy mirrors prune and pruning enabled",
53+
prune: true,
54+
deletionPolicy: kustomizev1.DeletionPolicyMirrorPrune,
55+
wantDelete: true,
56+
},
57+
{
58+
name: "should orphan when deletionPolicy overrides pruning enabled",
59+
prune: true,
60+
deletionPolicy: kustomizev1.DeletionPolicyOrphan,
61+
wantDelete: false,
62+
},
63+
{
64+
name: "should orphan when deletionPolicy mirrors prune and pruning disabled",
65+
prune: false,
66+
deletionPolicy: kustomizev1.DeletionPolicyMirrorPrune,
67+
wantDelete: false,
68+
},
69+
}
70+
for _, tt := range tests {
71+
t.Run(tt.name, func(t *testing.T) {
72+
g := NewWithT(t)
73+
id := "gc-" + randStringRunes(5)
74+
revision := "v1.0.0"
75+
76+
err := createNamespace(id)
77+
g.Expect(err).NotTo(HaveOccurred(), "failed to create test namespace")
78+
79+
err = createKubeConfigSecret(id)
80+
g.Expect(err).NotTo(HaveOccurred(), "failed to create kubeconfig secret")
81+
82+
manifests := func(name string, data string) []testserver.File {
83+
return []testserver.File{
84+
{
85+
Name: "config.yaml",
86+
Body: fmt.Sprintf(`---
87+
apiVersion: v1
88+
kind: ConfigMap
89+
metadata:
90+
name: %[1]s
91+
data:
92+
key: "%[2]s"
93+
`, name, data),
94+
},
95+
}
96+
}
97+
98+
artifact, err := testServer.ArtifactFromFiles(manifests(id, id))
99+
g.Expect(err).NotTo(HaveOccurred())
100+
101+
repositoryName := types.NamespacedName{
102+
Name: fmt.Sprintf("gc-%s", randStringRunes(5)),
103+
Namespace: id,
104+
}
105+
106+
err = applyGitRepository(repositoryName, artifact, revision)
107+
g.Expect(err).NotTo(HaveOccurred())
108+
109+
kustomizationKey := types.NamespacedName{
110+
Name: fmt.Sprintf("gc-%s", randStringRunes(5)),
111+
Namespace: id,
112+
}
113+
kustomization := &kustomizev1.Kustomization{
114+
ObjectMeta: metav1.ObjectMeta{
115+
Name: kustomizationKey.Name,
116+
Namespace: kustomizationKey.Namespace,
117+
},
118+
Spec: kustomizev1.KustomizationSpec{
119+
Interval: metav1.Duration{Duration: reconciliationInterval},
120+
Path: "./",
121+
KubeConfig: &meta.KubeConfigReference{
122+
SecretRef: meta.SecretKeyReference{
123+
Name: "kubeconfig",
124+
},
125+
},
126+
SourceRef: kustomizev1.CrossNamespaceSourceReference{
127+
Name: repositoryName.Name,
128+
Namespace: repositoryName.Namespace,
129+
Kind: sourcev1.GitRepositoryKind,
130+
},
131+
TargetNamespace: id,
132+
Prune: tt.prune,
133+
DeletionPolicy: tt.deletionPolicy,
134+
},
135+
}
136+
137+
g.Expect(k8sClient.Create(context.Background(), kustomization)).To(Succeed())
138+
139+
resultK := &kustomizev1.Kustomization{}
140+
resultConfig := &corev1.ConfigMap{}
141+
142+
g.Eventually(func() bool {
143+
_ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK)
144+
return resultK.Status.LastAppliedRevision == revision
145+
}, timeout, time.Second).Should(BeTrue())
146+
147+
g.Expect(k8sClient.Get(context.Background(), types.NamespacedName{Name: id, Namespace: id}, resultConfig)).Should(Succeed())
148+
149+
g.Expect(k8sClient.Delete(context.Background(), kustomization)).To(Succeed())
150+
g.Eventually(func() bool {
151+
err = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), kustomization)
152+
return apierrors.IsNotFound(err)
153+
}, timeout, time.Second).Should(BeTrue())
154+
155+
if tt.wantDelete {
156+
err = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(resultConfig), resultConfig)
157+
g.Expect(apierrors.IsNotFound(err)).To(BeTrue())
158+
} else {
159+
g.Expect(k8sClient.Get(context.Background(), client.ObjectKeyFromObject(resultConfig), resultConfig)).Should(Succeed())
160+
}
161+
162+
})
163+
}
164+
}

0 commit comments

Comments
 (0)