Skip to content

Commit

Permalink
Merge pull request #1314 from erikgb/finalizer-tuning
Browse files Browse the repository at this point in the history
Allow control of finalization garbage collection with `.spec.deletionPolicy`
  • Loading branch information
stefanprodan authored Dec 19, 2024
2 parents a87337c + c38ebab commit c41cb82
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 c41cb82

Please sign in to comment.