Skip to content

Commit

Permalink
Allow control of finalization garbage collection
Browse files Browse the repository at this point in the history
Signed-off-by: Erik Godding Boye <[email protected]>
Co-authored-by: Stefan Prodan <[email protected]>
Co-authored-by: Amund Tenstad <[email protected]>
  • Loading branch information
3 people committed Dec 18, 2024
1 parent a87337c commit c38ebab
Show file tree
Hide file tree
Showing 6 changed files with 266 additions and 1 deletion.
20 changes: 20 additions & 0 deletions api/v1/kustomization_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"`
Expand Down Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions config/crd/bases/kustomize.toolkit.fluxcd.io_kustomizations.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions docs/api/v1/kustomize.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,21 @@ bool
</tr>
<tr>
<td>
<code>deletionPolicy</code><br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>DeletionPolicy can be used to control garbage collection when this
Kustomization is deleted. Valid values are (&lsquo;MirrorPrune&rsquo;, &lsquo;Delete&rsquo;,
&lsquo;Orphan&rsquo;). &lsquo;MirrorPrune&rsquo; mirrors the Prune field (orphan if false,
delete if true). Defaults to &lsquo;MirrorPrune&rsquo;.</p>
</td>
</tr>
<tr>
<td>
<code>healthChecks</code><br>
<em>
<a href="https://godoc.org/github.com/fluxcd/pkg/apis/meta#NamespacedObjectKindReference">
Expand Down Expand Up @@ -716,6 +731,21 @@ bool
</tr>
<tr>
<td>
<code>deletionPolicy</code><br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>DeletionPolicy can be used to control garbage collection when this
Kustomization is deleted. Valid values are (&lsquo;MirrorPrune&rsquo;, &lsquo;Delete&rsquo;,
&lsquo;Orphan&rsquo;). &lsquo;MirrorPrune&rsquo; mirrors the Prune field (orphan if false,
delete if true). Defaults to &lsquo;MirrorPrune&rsquo;.</p>
</td>
</tr>
<tr>
<td>
<code>healthChecks</code><br>
<em>
<a href="https://godoc.org/github.com/fluxcd/pkg/apis/meta#NamespacedObjectKindReference">
Expand Down
33 changes: 33 additions & 0 deletions docs/spec/v1/kustomizations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 8 additions & 1 deletion internal/controller/kustomization_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
164 changes: 164 additions & 0 deletions internal/controller/kustomization_deletion_policy_test.go
Original file line number Diff line number Diff line change
@@ -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())
}

})
}
}

0 comments on commit c38ebab

Please sign in to comment.