diff --git a/api/v1/kustomization_types.go b/api/v1/kustomization_types.go
index ea6c92ae..22b6b330 100644
--- a/api/v1/kustomization_types.go
+++ b/api/v1/kustomization_types.go
@@ -33,6 +33,10 @@ const (
MergeValue = "Merge"
IfNotPresentValue = "IfNotPresent"
IgnoreValue = "Ignore"
+
+ DeletionPolicyMirrorPrune = "MirrorPrune"
+ DeletionPolicyDelete = "Delete"
+ DeletionPolicyOrphan = "Orphan"
)
// KustomizationSpec defines the configuration to calculate the desired state
@@ -95,6 +99,14 @@ type KustomizationSpec struct {
// +required
Prune bool `json:"prune"`
+ // DeletionPolicy can be used to control garbage collection when this
+ // Kustomization is deleted. Valid values are ('MirrorPrune', 'Delete',
+ // 'Orphan'). 'MirrorPrune' mirrors the Prune field (orphan if false,
+ // delete if true). Defaults to 'MirrorPrune'.
+ // +kubebuilder:validation:Enum=MirrorPrune;Delete;Orphan
+ // +optional
+ DeletionPolicy string `json:"deletionPolicy,omitempty"`
+
// A list of resources to be included in the health assessment.
// +optional
HealthChecks []meta.NamespacedObjectKindReference `json:"healthChecks,omitempty"`
@@ -287,6 +299,14 @@ func (in Kustomization) GetRequeueAfter() time.Duration {
return in.Spec.Interval.Duration
}
+// GetDeletionPolicy returns the deletion policy and default value if not specified.
+func (in Kustomization) GetDeletionPolicy() string {
+ if in.Spec.DeletionPolicy == "" {
+ return DeletionPolicyMirrorPrune
+ }
+ return in.Spec.DeletionPolicy
+}
+
// GetDependsOn returns the list of dependencies across-namespaces.
func (in Kustomization) GetDependsOn() []meta.NamespacedObjectReference {
return in.Spec.DependsOn
diff --git a/config/crd/bases/kustomize.toolkit.fluxcd.io_kustomizations.yaml b/config/crd/bases/kustomize.toolkit.fluxcd.io_kustomizations.yaml
index bbeb11ba..9fa9f991 100644
--- a/config/crd/bases/kustomize.toolkit.fluxcd.io_kustomizations.yaml
+++ b/config/crd/bases/kustomize.toolkit.fluxcd.io_kustomizations.yaml
@@ -98,6 +98,17 @@ spec:
required:
- provider
type: object
+ deletionPolicy:
+ description: |-
+ DeletionPolicy can be used to control garbage collection when this
+ Kustomization is deleted. Valid values are ('MirrorPrune', 'Delete',
+ 'Orphan'). 'MirrorPrune' mirrors the Prune field (orphan if false,
+ delete if true). Defaults to 'MirrorPrune'.
+ enum:
+ - MirrorPrune
+ - Delete
+ - Orphan
+ type: string
dependsOn:
description: |-
DependsOn may contain a meta.NamespacedObjectReference slice
diff --git a/docs/api/v1/kustomize.md b/docs/api/v1/kustomize.md
index 7a2281ad..9973006f 100644
--- a/docs/api/v1/kustomize.md
+++ b/docs/api/v1/kustomize.md
@@ -208,6 +208,21 @@ bool
+deletionPolicy
+
+string
+
+ |
+
+(Optional)
+ DeletionPolicy can be used to control garbage collection when this
+Kustomization is deleted. Valid values are (‘MirrorPrune’, ‘Delete’,
+‘Orphan’). ‘MirrorPrune’ mirrors the Prune field (orphan if false,
+delete if true). Defaults to ‘MirrorPrune’.
+ |
+
+
+
healthChecks
@@ -716,6 +731,21 @@ bool
|
+deletionPolicy
+
+string
+
+ |
+
+(Optional)
+ DeletionPolicy can be used to control garbage collection when this
+Kustomization is deleted. Valid values are (‘MirrorPrune’, ‘Delete’,
+‘Orphan’). ‘MirrorPrune’ mirrors the Prune field (orphan if false,
+delete if true). Defaults to ‘MirrorPrune’.
+ |
+
+
+
healthChecks
diff --git a/docs/spec/v1/kustomizations.md b/docs/spec/v1/kustomizations.md
index 77c1e01e..a9cfa72d 100644
--- a/docs/spec/v1/kustomizations.md
+++ b/docs/spec/v1/kustomizations.md
@@ -169,6 +169,39 @@ kustomize.toolkit.fluxcd.io/prune: disabled
For details on how the controller tracks Kubernetes objects and determines what
to garbage collect, see [`.status.inventory`](#inventory).
+### Deletion policy
+
+`.spec.deletionPolicy` is an optional field that allows control over
+garbage collection when a Kustomization object is deleted. The default behavior
+is to mirror the configuration of [`.spec.prune`](#prune).
+
+Valid values:
+
+- `MirrorPrune` (default) - The managed resources will be deleted if `prune` is
+ `true` and orphaned if `false`.
+- `Delete` - Ensure the managed resources are deleted before the Kustomization
+ is deleted.
+- `Orphan` - Leave the managed resources when the Kustomization is deleted.
+
+For special cases when the managed resources are removed by other means (e.g.
+the deletion of the namespace specified with
+[`.spec.targetNamespace`](#target-namespace)), you can set the deletion policy
+to `Orphan`:
+
+```yaml
+---
+apiVersion: kustomize.toolkit.fluxcd.io/v1
+kind: Kustomization
+metadata:
+ name: app
+ namespace: default
+spec:
+ # ...omitted for brevity
+ targetNamespace: app-namespace
+ prune: true
+ deletionPolicy: Orphan
+```
+
### Interval
`.spec.interval` is a required field that specifies the interval at which the
diff --git a/internal/controller/kustomization_controller.go b/internal/controller/kustomization_controller.go
index 9df5966f..bd2ef9a1 100644
--- a/internal/controller/kustomization_controller.go
+++ b/internal/controller/kustomization_controller.go
@@ -956,10 +956,17 @@ func (r *KustomizationReconciler) prune(ctx context.Context,
return false, nil
}
+func finalizerShouldDeleteResources(obj *kustomizev1.Kustomization) bool {
+ if obj.GetDeletionPolicy() == kustomizev1.DeletionPolicyMirrorPrune {
+ return obj.Spec.Prune
+ }
+ return obj.Spec.DeletionPolicy == kustomizev1.DeletionPolicyDelete
+}
+
func (r *KustomizationReconciler) finalize(ctx context.Context,
obj *kustomizev1.Kustomization) (ctrl.Result, error) {
log := ctrl.LoggerFrom(ctx)
- if obj.Spec.Prune &&
+ if finalizerShouldDeleteResources(obj) &&
!obj.Spec.Suspend &&
obj.Status.Inventory != nil &&
obj.Status.Inventory.Entries != nil {
diff --git a/internal/controller/kustomization_deletion_policy_test.go b/internal/controller/kustomization_deletion_policy_test.go
new file mode 100644
index 00000000..f5493602
--- /dev/null
+++ b/internal/controller/kustomization_deletion_policy_test.go
@@ -0,0 +1,164 @@
+/*
+Copyright 2024 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package controller
+
+import (
+ "context"
+ "fmt"
+ "testing"
+ "time"
+
+ "github.com/fluxcd/pkg/apis/meta"
+ "github.com/fluxcd/pkg/testserver"
+ sourcev1 "github.com/fluxcd/source-controller/api/v1"
+ . "github.com/onsi/gomega"
+ corev1 "k8s.io/api/core/v1"
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/types"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+
+ kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
+)
+
+func TestKustomizationReconciler_DeletionPolicyDelete(t *testing.T) {
+ tests := []struct {
+ name string
+ prune bool
+ deletionPolicy string
+ wantDelete bool
+ }{
+ {
+ name: "should delete when deletionPolicy overrides pruning disabled",
+ prune: false,
+ deletionPolicy: kustomizev1.DeletionPolicyDelete,
+ wantDelete: true,
+ },
+ {
+ name: "should delete when deletionPolicy mirrors prune and pruning enabled",
+ prune: true,
+ deletionPolicy: kustomizev1.DeletionPolicyMirrorPrune,
+ wantDelete: true,
+ },
+ {
+ name: "should orphan when deletionPolicy overrides pruning enabled",
+ prune: true,
+ deletionPolicy: kustomizev1.DeletionPolicyOrphan,
+ wantDelete: false,
+ },
+ {
+ name: "should orphan when deletionPolicy mirrors prune and pruning disabled",
+ prune: false,
+ deletionPolicy: kustomizev1.DeletionPolicyMirrorPrune,
+ wantDelete: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ g := NewWithT(t)
+ id := "gc-" + randStringRunes(5)
+ revision := "v1.0.0"
+
+ err := createNamespace(id)
+ g.Expect(err).NotTo(HaveOccurred(), "failed to create test namespace")
+
+ err = createKubeConfigSecret(id)
+ g.Expect(err).NotTo(HaveOccurred(), "failed to create kubeconfig secret")
+
+ manifests := func(name string, data string) []testserver.File {
+ return []testserver.File{
+ {
+ Name: "config.yaml",
+ Body: fmt.Sprintf(`---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: %[1]s
+data:
+ key: "%[2]s"
+`, name, data),
+ },
+ }
+ }
+
+ artifact, err := testServer.ArtifactFromFiles(manifests(id, id))
+ g.Expect(err).NotTo(HaveOccurred())
+
+ repositoryName := types.NamespacedName{
+ Name: fmt.Sprintf("gc-%s", randStringRunes(5)),
+ Namespace: id,
+ }
+
+ err = applyGitRepository(repositoryName, artifact, revision)
+ g.Expect(err).NotTo(HaveOccurred())
+
+ kustomizationKey := types.NamespacedName{
+ Name: fmt.Sprintf("gc-%s", randStringRunes(5)),
+ Namespace: id,
+ }
+ kustomization := &kustomizev1.Kustomization{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: kustomizationKey.Name,
+ Namespace: kustomizationKey.Namespace,
+ },
+ Spec: kustomizev1.KustomizationSpec{
+ Interval: metav1.Duration{Duration: reconciliationInterval},
+ Path: "./",
+ KubeConfig: &meta.KubeConfigReference{
+ SecretRef: meta.SecretKeyReference{
+ Name: "kubeconfig",
+ },
+ },
+ SourceRef: kustomizev1.CrossNamespaceSourceReference{
+ Name: repositoryName.Name,
+ Namespace: repositoryName.Namespace,
+ Kind: sourcev1.GitRepositoryKind,
+ },
+ TargetNamespace: id,
+ Prune: tt.prune,
+ DeletionPolicy: tt.deletionPolicy,
+ },
+ }
+
+ g.Expect(k8sClient.Create(context.Background(), kustomization)).To(Succeed())
+
+ resultK := &kustomizev1.Kustomization{}
+ resultConfig := &corev1.ConfigMap{}
+
+ g.Eventually(func() bool {
+ _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK)
+ return resultK.Status.LastAppliedRevision == revision
+ }, timeout, time.Second).Should(BeTrue())
+
+ g.Expect(k8sClient.Get(context.Background(), types.NamespacedName{Name: id, Namespace: id}, resultConfig)).Should(Succeed())
+
+ g.Expect(k8sClient.Delete(context.Background(), kustomization)).To(Succeed())
+ g.Eventually(func() bool {
+ err = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), kustomization)
+ return apierrors.IsNotFound(err)
+ }, timeout, time.Second).Should(BeTrue())
+
+ if tt.wantDelete {
+ err = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(resultConfig), resultConfig)
+ g.Expect(apierrors.IsNotFound(err)).To(BeTrue())
+ } else {
+ g.Expect(k8sClient.Get(context.Background(), client.ObjectKeyFromObject(resultConfig), resultConfig)).Should(Succeed())
+ }
+
+ })
+ }
+}
|