Skip to content

Commit 80af83d

Browse files
authored
Merge pull request #316 from dmvolod/issue-315
🐛 HelmChartProxy is removed even when the release cannot be uninstalled
2 parents dfce3f6 + 7779d4b commit 80af83d

File tree

3 files changed

+149
-83
lines changed

3 files changed

+149
-83
lines changed

controllers/controllers_test.go

Lines changed: 114 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@ limitations under the License.
1717
package controllers_test
1818

1919
import (
20-
"time"
21-
2220
. "github.com/onsi/ginkgo/v2"
2321
. "github.com/onsi/gomega"
2422
helmrelease "helm.sh/helm/v3/pkg/release"
@@ -32,51 +30,62 @@ import (
3230
"sigs.k8s.io/controller-runtime/pkg/client"
3331
)
3432

35-
var (
36-
testNamespace = "test-namespace"
37-
newVersion = "new-version"
33+
const (
34+
testNamespace1 = "test-namespace1"
35+
testNamespace2 = "test-namespace2"
36+
newVersion = "new-version"
37+
releaseFailedMessage = "unable to remove helm release"
38+
)
3839

39-
defaultProxy = &addonsv1alpha1.HelmChartProxy{
40-
TypeMeta: metav1.TypeMeta{
41-
APIVersion: addonsv1alpha1.GroupVersion.String(),
42-
Kind: "HelmChartProxy",
43-
},
44-
ObjectMeta: metav1.ObjectMeta{
45-
Name: "test-hcp",
46-
Namespace: testNamespace,
47-
},
48-
Spec: addonsv1alpha1.HelmChartProxySpec{
49-
ClusterSelector: metav1.LabelSelector{
50-
MatchLabels: map[string]string{
51-
"test-label": "test-value",
40+
var (
41+
namespaces = []string{testNamespace1, testNamespace2}
42+
failedHelmUninstall bool
43+
44+
newProxy = func(namespace string) *addonsv1alpha1.HelmChartProxy {
45+
return &addonsv1alpha1.HelmChartProxy{
46+
TypeMeta: metav1.TypeMeta{
47+
APIVersion: addonsv1alpha1.GroupVersion.String(),
48+
Kind: "HelmChartProxy",
49+
},
50+
ObjectMeta: metav1.ObjectMeta{
51+
Name: "test-hcp",
52+
Namespace: namespace,
53+
},
54+
Spec: addonsv1alpha1.HelmChartProxySpec{
55+
ClusterSelector: metav1.LabelSelector{
56+
MatchLabels: map[string]string{
57+
"test-label": "test-value",
58+
},
5259
},
60+
ReleaseName: "test-release-name",
61+
ChartName: "test-chart-name",
62+
RepoURL: "https://test-repo-url",
63+
ReleaseNamespace: "test-release-namespace",
64+
Version: "test-version",
65+
ValuesTemplate: "apiServerPort: {{ .Cluster.spec.clusterNetwork.apiServerPort }}",
5366
},
54-
ReleaseName: "test-release-name",
55-
ChartName: "test-chart-name",
56-
RepoURL: "https://test-repo-url",
57-
ReleaseNamespace: "test-release-namespace",
58-
Version: "test-version",
59-
ValuesTemplate: "apiServerPort: {{ .Cluster.spec.clusterNetwork.apiServerPort }}",
60-
},
67+
}
6168
}
6269

63-
cluster1 = &clusterv1.Cluster{
64-
TypeMeta: metav1.TypeMeta{
65-
APIVersion: clusterv1.GroupVersion.String(),
66-
Kind: "Cluster",
67-
},
68-
ObjectMeta: metav1.ObjectMeta{
69-
Name: "test-cluster-1",
70-
Namespace: testNamespace,
71-
Labels: map[string]string{
72-
"test-label": "test-value",
70+
newCluster = func(namespace string) *clusterv1.Cluster {
71+
return &clusterv1.Cluster{
72+
TypeMeta: metav1.TypeMeta{
73+
APIVersion: clusterv1.GroupVersion.String(),
74+
Kind: "Cluster",
7375
},
74-
},
75-
Spec: clusterv1.ClusterSpec{
76-
ClusterNetwork: &clusterv1.ClusterNetwork{
77-
APIServerPort: ptr.To(int32(1234)),
76+
ObjectMeta: metav1.ObjectMeta{
77+
Name: "test-cluster-1",
78+
Namespace: namespace,
79+
Labels: map[string]string{
80+
"test-label": "test-value",
81+
},
7882
},
79-
},
83+
Spec: clusterv1.ClusterSpec{
84+
ClusterNetwork: &clusterv1.ClusterNetwork{
85+
APIServerPort: ptr.To(int32(1234)),
86+
},
87+
},
88+
}
8089
}
8190

8291
helmReleaseDeployed = &helmrelease.Release{
@@ -135,62 +144,91 @@ var _ = Describe("Testing HelmChartProxy and HelmReleaseProxy reconcile", func()
135144
return condition != nil && condition(hrpList.Items)
136145
}, timeout, interval).Should(BeTrue())
137146
}
138-
)
139147

140-
It("HelmChartProxy and HelmReleaseProxy lifecycle test", func() {
141-
cluster := cluster1.DeepCopy()
142-
err := k8sClient.Create(ctx, cluster)
143-
Expect(err).ToNot(HaveOccurred())
144-
err = k8sClient.Create(ctx, newKubeconfigSecretForCluster(cluster))
145-
Expect(err).ToNot(HaveOccurred())
148+
install = func(cluster *clusterv1.Cluster, proxy *addonsv1alpha1.HelmChartProxy) {
149+
err := k8sClient.Create(ctx, cluster)
150+
Expect(err).ToNot(HaveOccurred())
151+
err = k8sClient.Create(ctx, newKubeconfigSecretForCluster(cluster))
152+
Expect(err).ToNot(HaveOccurred())
146153

147-
patch := client.MergeFrom(cluster.DeepCopy())
148-
cluster.Status.Conditions = clusterv1.Conditions{
149-
{
150-
Type: clusterv1.ControlPlaneInitializedCondition,
151-
Status: corev1.ConditionTrue,
152-
LastTransitionTime: metav1.NewTime(time.Now()),
153-
},
154+
patch := client.MergeFrom(cluster.DeepCopy())
155+
conditions.MarkTrue(cluster, clusterv1.ControlPlaneInitializedCondition)
156+
err = k8sClient.Status().Patch(ctx, cluster, patch)
157+
Expect(err).ToNot(HaveOccurred())
158+
159+
err = k8sClient.Create(ctx, proxy)
160+
Expect(err).ToNot(HaveOccurred())
161+
162+
waitForHelmChartProxyCondition(client.ObjectKeyFromObject(proxy), func(helmChartProxy *addonsv1alpha1.HelmChartProxy) bool {
163+
return conditions.IsTrue(helmChartProxy, clusterv1.ReadyCondition)
164+
})
165+
166+
waitForHelmReleaseProxyCondition(client.ObjectKeyFromObject(proxy), func(helmReleaseProxyList []addonsv1alpha1.HelmReleaseProxy) bool {
167+
return len(helmReleaseProxyList) == 1 && conditions.IsTrue(&helmReleaseProxyList[0], clusterv1.ReadyCondition)
168+
})
154169
}
155-
err = k8sClient.Status().Patch(ctx, cluster, patch)
156-
Expect(err).ToNot(HaveOccurred())
157170

158-
err = k8sClient.Create(ctx, defaultProxy)
159-
Expect(err).ToNot(HaveOccurred())
171+
deleteAndWaitHelmChartProxy = func(proxy *addonsv1alpha1.HelmChartProxy) {
172+
err := k8sClient.Delete(ctx, proxy)
173+
Expect(err).ToNot(HaveOccurred())
160174

161-
waitForHelmChartProxyCondition(client.ObjectKeyFromObject(defaultProxy), func(helmChartProxy *addonsv1alpha1.HelmChartProxy) bool {
162-
return conditions.IsTrue(helmChartProxy, clusterv1.ReadyCondition)
163-
})
175+
Eventually(func() bool {
176+
if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(proxy), &addonsv1alpha1.HelmChartProxy{}); client.IgnoreNotFound(err) != nil {
177+
return false
178+
}
164179

165-
waitForHelmReleaseProxyCondition(client.ObjectKeyFromObject(defaultProxy), func(helmReleaseProxyList []addonsv1alpha1.HelmReleaseProxy) bool {
166-
return len(helmReleaseProxyList) == 1 && conditions.IsTrue(&helmReleaseProxyList[0], clusterv1.ReadyCondition)
167-
})
180+
return true
181+
}, timeout, interval).Should(BeTrue())
182+
183+
waitForHelmReleaseProxyCondition(client.ObjectKeyFromObject(proxy), func(helmReleaseProxyList []addonsv1alpha1.HelmReleaseProxy) bool {
184+
return len(helmReleaseProxyList) == 0
185+
})
186+
}
187+
)
188+
189+
It("HelmChartProxy and HelmReleaseProxy lifecycle happy path test", func() {
190+
cluster := newCluster(testNamespace1)
191+
helmChartProxy := newProxy(testNamespace1)
192+
install(cluster, helmChartProxy)
168193

169-
hcp := &addonsv1alpha1.HelmChartProxy{}
170-
err = k8sClient.Get(ctx, client.ObjectKeyFromObject(defaultProxy), hcp)
194+
err := k8sClient.Get(ctx, client.ObjectKeyFromObject(helmChartProxy), helmChartProxy)
171195
Expect(err).ToNot(HaveOccurred())
172-
patch = client.MergeFrom(hcp.DeepCopy())
173-
hcp.Spec.Version = newVersion
174-
err = k8sClient.Patch(ctx, hcp, patch)
196+
patch := client.MergeFrom(helmChartProxy.DeepCopy())
197+
helmChartProxy.Spec.Version = newVersion
198+
err = k8sClient.Patch(ctx, helmChartProxy, patch)
175199
Expect(err).ToNot(HaveOccurred())
176200

177-
waitForHelmReleaseProxyCondition(client.ObjectKeyFromObject(defaultProxy), func(helmReleaseProxyList []addonsv1alpha1.HelmReleaseProxy) bool {
201+
waitForHelmReleaseProxyCondition(client.ObjectKeyFromObject(helmChartProxy), func(helmReleaseProxyList []addonsv1alpha1.HelmReleaseProxy) bool {
178202
return len(helmReleaseProxyList) == 1 && conditions.IsTrue(&helmReleaseProxyList[0], clusterv1.ReadyCondition) && helmReleaseProxyList[0].Spec.Version == "new-version"
179203
})
180204

181-
err = k8sClient.Delete(ctx, hcp)
205+
deleteAndWaitHelmChartProxy(helmChartProxy)
206+
})
207+
208+
It("HelmChartProxy and HelmReleaseProxy test with failed Release uninstall", func() {
209+
cluster := newCluster(testNamespace2)
210+
helmChartProxy := newProxy(testNamespace2)
211+
failedHelmUninstall = true
212+
install(cluster, helmChartProxy)
213+
214+
err := k8sClient.Delete(ctx, helmChartProxy)
182215
Expect(err).ToNot(HaveOccurred())
183216

184-
Eventually(func() bool {
185-
if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(hcp), hcp); client.IgnoreNotFound(err) != nil {
217+
Consistently(func() bool {
218+
if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(helmChartProxy), helmChartProxy); err != nil {
186219
return false
187220
}
188221

189222
return true
190223
}, timeout, interval).Should(BeTrue())
191224

192-
waitForHelmReleaseProxyCondition(client.ObjectKeyFromObject(defaultProxy), func(helmReleaseProxyList []addonsv1alpha1.HelmReleaseProxy) bool {
193-
return len(helmReleaseProxyList) == 0
194-
})
225+
readyCondition := conditions.Get(helmChartProxy, clusterv1.ReadyCondition)
226+
Expect(readyCondition).NotTo(BeNil())
227+
Expect(readyCondition.Status).To(Equal(corev1.ConditionFalse))
228+
Expect(readyCondition.Message).To(Equal(releaseFailedMessage))
229+
230+
By("Making HelmChartProxy uninstallable")
231+
failedHelmUninstall = false
232+
deleteAndWaitHelmChartProxy(helmChartProxy)
195233
})
196234
})

controllers/helmchartproxy/helmchartproxy_controller.go

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ func (r *HelmChartProxyReconciler) Reconcile(ctx context.Context, req ctrl.Reque
148148
// The object is being deleted
149149
if controllerutil.ContainsFinalizer(helmChartProxy, addonsv1alpha1.HelmChartProxyFinalizer) {
150150
// our finalizer is present, so lets handle any external dependency
151-
if err := r.reconcileDelete(ctx, helmChartProxy, releaseList.Items); err != nil {
151+
if result, err := r.reconcileDelete(ctx, helmChartProxy, releaseList.Items); err != nil || !result.IsZero() {
152152
// if fail to delete the external dependency here, return with error
153153
// so that it can be retried
154154
return ctrl.Result{}, err
@@ -213,22 +213,40 @@ func (r *HelmChartProxyReconciler) reconcileNormal(ctx context.Context, helmChar
213213
}
214214

215215
// reconcileDelete handles the deletion of a HelmChartProxy. It takes a list of HelmReleaseProxies to uninstall the Helm chart from all selected Clusters.
216-
func (r *HelmChartProxyReconciler) reconcileDelete(ctx context.Context, helmChartProxy *addonsv1alpha1.HelmChartProxy, releases []addonsv1alpha1.HelmReleaseProxy) error {
216+
func (r *HelmChartProxyReconciler) reconcileDelete(ctx context.Context, helmChartProxy *addonsv1alpha1.HelmChartProxy, releases []addonsv1alpha1.HelmReleaseProxy) (ctrl.Result, error) {
217217
log := ctrl.LoggerFrom(ctx)
218+
getters := make([]conditions.Getter, 0)
218219

219220
log.V(2).Info("Deleting all HelmReleaseProxies as part of HelmChartProxy deletion", "helmChartProxy", helmChartProxy.Name)
220-
221221
for i := range releases {
222222
release := releases[i]
223223

224224
log.V(2).Info("Deleting release", "releaseName", release.Name, "cluster", release.Spec.ClusterRef.Name)
225225
if err := r.deleteHelmReleaseProxy(ctx, &release); err != nil {
226226
// TODO: will this fail if clusterRef is nil
227-
return errors.Wrapf(err, "failed to delete release %s from cluster %s", release.Name, release.Spec.ClusterRef.Name)
227+
return ctrl.Result{}, errors.Wrapf(err, "failed to delete release %s from cluster %s", release.Name, release.Spec.ClusterRef.Name)
228+
}
229+
230+
log.V(2).Info("Validating release deletion", "releaseName", release.Name)
231+
if err := r.Get(ctx, client.ObjectKeyFromObject(&release), &release); err != nil {
232+
if apierrors.IsNotFound(err) {
233+
continue
234+
}
235+
236+
return ctrl.Result{}, errors.Wrapf(err, "failed to get HelmReleaseProxy %s", release.Name)
228237
}
238+
239+
log.V(2).Info("The release has not been deleted yet, waiting for it to be removed", "releaseName", release.Name)
240+
getters = append(getters, &release)
229241
}
230242

231-
return nil
243+
if len(getters) > 0 {
244+
conditions.SetAggregate(helmChartProxy, addonsv1alpha1.HelmReleaseProxiesReadyCondition, getters, conditions.AddSourceRef(), conditions.WithStepCounterIf(false))
245+
246+
return ctrl.Result{Requeue: true}, nil
247+
}
248+
249+
return ctrl.Result{}, nil
232250
}
233251

234252
// listClustersWithLabels returns a list of Clusters that match the given label selector.

controllers/suite_test.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package controllers_test
1818

1919
import (
2020
"context"
21+
"errors"
2122
"flag"
2223
"fmt"
2324
"path/filepath"
@@ -114,7 +115,9 @@ var _ = BeforeSuite(func() {
114115
Expect(err).NotTo(HaveOccurred())
115116
Expect(k8sClient).NotTo(BeNil())
116117

117-
Expect(k8sClient.Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: testNamespace}})).NotTo(HaveOccurred())
118+
for _, namespace := range namespaces {
119+
Expect(k8sClient.Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}})).NotTo(HaveOccurred())
120+
}
118121

119122
k8sManager, err = ctrl.NewManager(cfg, ctrl.Options{
120123
Scheme: scheme.Scheme,
@@ -129,8 +132,15 @@ var _ = BeforeSuite(func() {
129132
helmClient = mocks.NewMockClient(gomock.NewController(&TestReporter{}))
130133

131134
helmClient.EXPECT().InstallOrUpgradeHelmRelease(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(helmReleaseDeployed, nil).AnyTimes()
132-
helmClient.EXPECT().UninstallHelmRelease(gomock.Any(), gomock.Any(), gomock.Any()).Return(&helmRelease.UninstallReleaseResponse{}, nil).AnyTimes()
133135
helmClient.EXPECT().GetHelmRelease(gomock.Any(), gomock.Any(), gomock.Any()).Return(&helmRelease.Release{}, nil).AnyTimes()
136+
helmClient.EXPECT().UninstallHelmRelease(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(_, _, _ any) (*helmRelease.UninstallReleaseResponse, error) {
137+
if failedHelmUninstall {
138+
return nil, errors.New(releaseFailedMessage)
139+
}
140+
141+
return &helmRelease.UninstallReleaseResponse{}, nil
142+
},
143+
).AnyTimes()
134144

135145
err = (&helmchartproxy.HelmChartProxyReconciler{
136146
Client: k8sManager.GetClient(),

0 commit comments

Comments
 (0)