Skip to content

Commit 72bc87d

Browse files
fix(plugindefinitions): cleanup orphaned Flux HelmChart resources (#2061)
* fix(plugindefinition): cleanup orphaned Flux HelmChart resources on version bump (#2047) When the HelmChart version in a PluginDefinition spec is updated, the controller now lists all HelmChart resources owned (controller-reference) by the PluginDefinition and deletes any whose name no longer matches the current FluxHelmChartResourceName (i.e. <pluginDef.Name>-<version>). This prevents accumulation of orphaned HelmChart resources that were previously left behind whenever the chart version was bumped. The cleanup is performed in the EnsureCreated reconcile path after the new HelmChart has been created/updated, so it only runs when the PluginDefinition is active. Adds a controller integration test that: - creates a PluginDefinition with chart version 1.0.0 - bumps the version to 2.0.0 - asserts the new HelmChart (v2.0.0) is created - asserts the orphaned HelmChart (v1.0.0) is deleted Fixes: #2047 Signed-off-by: Edrilan Berisha <edi91@gmx.net> * fix(plugindefinition): address PR review comments (#2047) - Ignore NotFound error when deleting orphaned HelmChart (avoids hard reconciliation failure if a chart is deleted concurrently) - Compare owner UIDs directly (types.UID) instead of via string() conversions for clarity - Wire deleteOrphanedHelmCharts into ClusterPluginDefinitionReconciler so both PluginDefinition and ClusterPluginDefinition clean up orphans - Tests now derive expected HelmChart names via FluxHelmChartResourceName() instead of hard-coding the name+version concatenation convention - Added ClusterPluginDefinition orphan cleanup integration test mirroring the PluginDefinition test Signed-off-by: Edrilan Berisha <edi91@gmx.net> * chore: add sign-off to commits Signed-off-by: Edrilan Berisha <edi91@gmx.net> Signed-off-by: Edrilan Berisha <edi91@gmx.net> * fix(plugindefinition): address additional review comments on PR #2047 - Add LabelKeyPluginDefinition label to HelmChart during CreateOrUpdate so the orphan-cleanup list call can use a label selector instead of scanning all HelmCharts in the namespace on every reconcile (scalability). - Switch deleteOrphanedHelmCharts to use client.MatchingLabels with greenhouseapis.LabelKeyPluginDefinition for an efficient server-side filter. - Replace clientutil.GetOwnerReference + GetObjectKind().GroupVersionKind().Kind with metav1.GetControllerOf() which is the idiomatic, reliable way to get the controller owner reference (GetObjectKind() can return an empty Kind when an object is constructed directly rather than decoded from the API). - Remove now-unused 'clientutil' import. Signed-off-by: Edrilan Berisha <edi91@gmx.net> Signed-off-by: Edrilan Berisha <edi91@gmx.net> * fix(plugindefinition): apply review feedback and rebase on upstream main (#2047) Rebase on top of upstream commit 4d2da4b which was merged by abhijith-darshan after the PR was opened. - Incorporate LabelKeyOwnedBy propagation from upstream PR #2062: when the PluginDefinition has an 'owned-by' label it is propagated to the managed HelmChart. - Add deleteOrphanedHelmCharts() with: - client.MatchingLabels{LabelKeyPluginDefinition: name} for efficient server-side filtering (abhijith-darshan request) - metav1.GetControllerOf() for reliable owner reference lookup - apierrors.IsNotFound guard for concurrent deletion safety - lowercase log message (NIT by abhijith-darshan) Signed-off-by: Edrilan Berisha <edi91@gmx.net> Signed-off-by: Edrilan Berisha <edi91@gmx.net> * fix(plugindefinition): run make fmt to fix lint/formatting errors Run 'make fmt' (goimports + gofmt) to fix import ordering and formatting issues flagged by the CI linter. Signed-off-by: Edrilan Berisha <edi91@gmx.net> --------- Signed-off-by: Edrilan Berisha <edi91@gmx.net> Co-authored-by: Abhijith Ravindra <137736216+abhijith-darshan@users.noreply.github.com>
1 parent 58d11ce commit 72bc87d

6 files changed

Lines changed: 156 additions & 3 deletions

File tree

api/meta/v1alpha1/zz_generated.deepcopy.go

Lines changed: 0 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/controller/plugindefinition/cluster_plugindefinition_controller.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,11 @@ func (r *ClusterPluginDefinitionReconciler) EnsureCreated(ctx context.Context, o
101101
if err != nil {
102102
return ctrl.Result{}, lifecycle.Failed, err
103103
}
104+
105+
if err := h.deleteOrphanedHelmCharts(ctx); err != nil {
106+
return ctrl.Result{}, lifecycle.Failed, err
107+
}
108+
104109
h.setHelmChartReadyCondition(ctx, helmChart)
105110

106111
return ctrl.Result{}, lifecycle.Success, nil

internal/controller/plugindefinition/plugindefinition_controller.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ func (r *PluginDefinitionReconciler) EnsureCreated(ctx context.Context, obj life
104104
return ctrl.Result{}, lifecycle.Failed, err
105105
}
106106

107+
if err := h.deleteOrphanedHelmCharts(ctx); err != nil {
108+
return ctrl.Result{}, lifecycle.Failed, err
109+
}
110+
107111
h.setHelmChartReadyCondition(ctx, helmChart)
108112

109113
return ctrl.Result{}, lifecycle.Success, nil

internal/controller/plugindefinition/plugindefinition_controller_test.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,109 @@ var _ = Describe("PluginDefinition controller", func() {
204204
})
205205
})
206206

207+
Context("When updating a PluginDefinition's HelmChart version", Ordered, func() {
208+
It("should delete orphaned HelmChart resources when the chart version is bumped", func() {
209+
By("creating a PluginDefinition with chart version 1.0.0")
210+
pluginDef := test.NewPluginDefinition(test.Ctx, "orphan-test-plugin", test.TestNamespace,
211+
test.WithPluginDefinitionVersion(PluginDefinitionVersion),
212+
test.WithPluginDefinitionHelmChart(&greenhousev1alpha1.HelmChartReference{
213+
Name: HelmChart,
214+
Repository: HelmRepo,
215+
Version: "1.0.0",
216+
}),
217+
)
218+
err := test.K8sClient.Create(test.Ctx, pluginDef)
219+
Expect(err).ToNot(HaveOccurred(), "there should be no error creating the PluginDefinition")
220+
221+
By("waiting for the initial HelmChart (v1.0.0) to be created")
222+
// Use FluxHelmChartResourceName() rather than hard-coding the name+version
223+
// convention so that the test stays aligned with the production logic.
224+
initialHelmChart := &sourcev1.HelmChart{}
225+
initialHelmChart.SetName(pluginDef.FluxHelmChartResourceName())
226+
initialHelmChart.SetNamespace(pluginDef.GetNamespace())
227+
Eventually(func(g Gomega) {
228+
err := test.K8sClient.Get(test.Ctx, cl.ObjectKeyFromObject(initialHelmChart), initialHelmChart)
229+
g.Expect(err).ToNot(HaveOccurred(), "initial HelmChart v1.0.0 should be created")
230+
}).Should(Succeed(), "the initial HelmChart should be created")
231+
232+
By("updating the PluginDefinition to chart version 2.0.0")
233+
Eventually(func(g Gomega) {
234+
err := test.K8sClient.Get(test.Ctx, cl.ObjectKeyFromObject(pluginDef), pluginDef)
235+
g.Expect(err).ToNot(HaveOccurred())
236+
pluginDef.Spec.HelmChart.Version = "2.0.0"
237+
err = test.K8sClient.Update(test.Ctx, pluginDef)
238+
g.Expect(err).ToNot(HaveOccurred(), "should be able to update the PluginDefinition version")
239+
}).Should(Succeed())
240+
241+
By("verifying the new HelmChart (v2.0.0) is created")
242+
// Derive the new expected name via FluxHelmChartResourceName() after the version was bumped.
243+
newHelmChart := &sourcev1.HelmChart{}
244+
newHelmChart.SetName(pluginDef.FluxHelmChartResourceName())
245+
newHelmChart.SetNamespace(pluginDef.GetNamespace())
246+
Eventually(func(g Gomega) {
247+
err := test.K8sClient.Get(test.Ctx, cl.ObjectKeyFromObject(newHelmChart), newHelmChart)
248+
g.Expect(err).ToNot(HaveOccurred(), "new HelmChart v2.0.0 should be created")
249+
}).Should(Succeed(), "the new HelmChart v2.0.0 should be created")
250+
251+
By("verifying the orphaned HelmChart (v1.0.0) is deleted")
252+
Eventually(func(g Gomega) {
253+
err := test.K8sClient.Get(test.Ctx, cl.ObjectKeyFromObject(initialHelmChart), initialHelmChart)
254+
g.Expect(err).To(HaveOccurred(), "orphaned HelmChart v1.0.0 should be deleted")
255+
g.Expect(cl.IgnoreNotFound(err)).To(Succeed(), "the error should be a NotFound error")
256+
}).Should(Succeed(), "the orphaned HelmChart v1.0.0 should be deleted after the version bump")
257+
})
258+
})
259+
260+
Context("When updating a ClusterPluginDefinition's HelmChart version", Ordered, func() {
261+
It("should delete orphaned HelmChart resources when the chart version is bumped", func() {
262+
By("creating a ClusterPluginDefinition with chart version 1.0.0")
263+
clusterDef := test.NewClusterPluginDefinition(test.Ctx, "orphan-cluster-test-plugin",
264+
test.WithVersion(PluginDefinitionVersion),
265+
test.WithHelmChart(&greenhousev1alpha1.HelmChartReference{
266+
Name: HelmChart,
267+
Repository: HelmRepo,
268+
Version: "1.0.0",
269+
}),
270+
)
271+
err := test.K8sClient.Create(test.Ctx, clusterDef)
272+
Expect(err).ToNot(HaveOccurred(), "there should be no error creating the ClusterPluginDefinition")
273+
274+
By("waiting for the initial HelmChart (v1.0.0) to be created")
275+
initialHelmChart := &sourcev1.HelmChart{}
276+
initialHelmChart.SetName(clusterDef.FluxHelmChartResourceName())
277+
initialHelmChart.SetNamespace(flux.HelmRepositoryDefaultNamespace)
278+
Eventually(func(g Gomega) {
279+
err := test.K8sClient.Get(test.Ctx, cl.ObjectKeyFromObject(initialHelmChart), initialHelmChart)
280+
g.Expect(err).ToNot(HaveOccurred(), "initial HelmChart v1.0.0 should be created")
281+
}).Should(Succeed(), "the initial ClusterPluginDefinition HelmChart should be created")
282+
283+
By("updating the ClusterPluginDefinition to chart version 2.0.0")
284+
Eventually(func(g Gomega) {
285+
err := test.K8sClient.Get(test.Ctx, cl.ObjectKeyFromObject(clusterDef), clusterDef)
286+
g.Expect(err).ToNot(HaveOccurred())
287+
clusterDef.Spec.HelmChart.Version = "2.0.0"
288+
err = test.K8sClient.Update(test.Ctx, clusterDef)
289+
g.Expect(err).ToNot(HaveOccurred(), "should be able to update the ClusterPluginDefinition version")
290+
}).Should(Succeed())
291+
292+
By("verifying the new HelmChart (v2.0.0) is created")
293+
newHelmChart := &sourcev1.HelmChart{}
294+
newHelmChart.SetName(clusterDef.FluxHelmChartResourceName())
295+
newHelmChart.SetNamespace(flux.HelmRepositoryDefaultNamespace)
296+
Eventually(func(g Gomega) {
297+
err := test.K8sClient.Get(test.Ctx, cl.ObjectKeyFromObject(newHelmChart), newHelmChart)
298+
g.Expect(err).ToNot(HaveOccurred(), "new HelmChart v2.0.0 should be created")
299+
}).Should(Succeed(), "the new ClusterPluginDefinition HelmChart v2.0.0 should be created")
300+
301+
By("verifying the orphaned HelmChart (v1.0.0) is deleted")
302+
Eventually(func(g Gomega) {
303+
err := test.K8sClient.Get(test.Ctx, cl.ObjectKeyFromObject(initialHelmChart), initialHelmChart)
304+
g.Expect(err).To(HaveOccurred(), "orphaned HelmChart v1.0.0 should be deleted")
305+
g.Expect(cl.IgnoreNotFound(err)).To(Succeed(), "the error should be a NotFound error")
306+
}).Should(Succeed(), "the orphaned ClusterPluginDefinition HelmChart v1.0.0 should be deleted after the version bump")
307+
})
308+
})
309+
207310
Context("When creating a ClusterPluginDefinition", Ordered, func() {
208311
It("should successfully create a HelmRepository from ClusterPluginDefinition", func() {
209312
By("creating a ClusterPluginDefinition")

internal/controller/plugindefinition/utils.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
fluxmeta "github.com/fluxcd/pkg/apis/meta"
1414
sourcev1 "github.com/fluxcd/source-controller/api/v1"
1515
corev1 "k8s.io/api/core/v1"
16+
apierrors "k8s.io/apimachinery/pkg/api/errors"
1617
"k8s.io/apimachinery/pkg/api/meta"
1718
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1819
"k8s.io/client-go/tools/events"
@@ -188,6 +189,48 @@ func (h *helmer) createUpdateHelmChart(ctx context.Context, helmRepo *sourcev1.H
188189
return helmChart, nil
189190
}
190191

192+
// deleteOrphanedHelmCharts lists HelmChart resources labelled with the owner PluginDefinition
193+
// name and deletes every one whose name differs from the currently desired FluxHelmChartResourceName.
194+
// Using a label selector avoids scanning all HelmCharts in the namespace on every reconcile.
195+
// The controller-reference UID check provides a secondary guard to prevent touching charts that
196+
// were labelled identically by an unrelated controller.
197+
func (h *helmer) deleteOrphanedHelmCharts(ctx context.Context) error {
198+
currentName := h.pluginDef.FluxHelmChartResourceName()
199+
logger := log.FromContext(ctx)
200+
201+
helmChartList := &sourcev1.HelmChartList{}
202+
if err := h.k8sClient.List(ctx, helmChartList,
203+
client.InNamespace(h.namespaceName),
204+
client.MatchingLabels{greenhouseapis.LabelKeyPluginDefinition: h.pluginDef.GetName()},
205+
); err != nil {
206+
return fmt.Errorf("failed to list HelmCharts in namespace %s: %w", h.namespaceName, err)
207+
}
208+
209+
for i := range helmChartList.Items {
210+
chart := &helmChartList.Items[i]
211+
// Use metav1.GetControllerOf to reliably retrieve the controller owner reference
212+
// (avoids relying on GetObjectKind().GroupVersionKind().Kind which can be empty).
213+
controllerRef := metav1.GetControllerOf(chart)
214+
if controllerRef == nil || controllerRef.UID != h.pluginDef.GetUID() {
215+
continue
216+
}
217+
// Keep the current (desired) HelmChart; delete everything else.
218+
if chart.Name == currentName {
219+
continue
220+
}
221+
logger.Info("deleting orphaned HelmChart", "namespace", chart.Namespace, "name", chart.Name)
222+
if err := h.k8sClient.Delete(ctx, chart); err != nil {
223+
// Ignore NotFound — the chart may have been deleted concurrently.
224+
if apierrors.IsNotFound(err) {
225+
continue
226+
}
227+
return fmt.Errorf("failed to delete orphaned HelmChart %s/%s: %w", chart.Namespace, chart.Name, err)
228+
}
229+
h.recorder.Eventf(h.pluginDef, chart, corev1.EventTypeNormal, "Deleted", "reconciling (Cluster-)PluginDefinition", "Deleted orphaned HelmChart %s", chart.Name)
230+
}
231+
return nil
232+
}
233+
191234
// ensureChartReplication triggers replication for the Helm chart OCI artifact to the configured mirror registry.
192235
func (h *helmer) ensureChartReplication(ctx context.Context) error {
193236
if !h.ociMirroringEnabled {

0 commit comments

Comments
 (0)