Skip to content

Commit 372c855

Browse files
fix(controller): carry healthCheckExprs and waitStrategy to existing releases (#5087)
## What this PR does This PR syncs `healthCheckExprs` and `waitStrategy` from ApplicationDefinition to HelmReleases that already exist. cozystack-api writes both fields only when it builds a release, and the definition reconciler copied chartRef, valuesFrom, disable-wait setting and labels but not these two, so release created before its definition got expressions keeps waiting on objects the definition already exempts. Most visible case is vm-disk. Every disk created before the DataVolume `current: "true"` expression was added still waits on its DataVolume, so after platform upgrade an existing disk with import still in progress fails the upgrade after 10 minutes with `timeout waiting for: [DataVolume/<namespace>/vm-disk-<name> status: 'InProgress']`, then retries forever and holds one of 5 workers of its helm-controller shard. Reconciler now derives both fields same way the API does (`config.ResolveWaitStrategy`) and syncs them in both directions, so existing release ends up same as freshly written one. On current definitions only vm-disk (DataVolume) and postgres (KEDA ScaledObject) set expressions, both `current: "true"`, so this can only relax waits. Releases of other kinds have nothing to sync and are not rewritten. HelmReleases rendered by another chart (tenant modules like Etcd, Ingress, Monitoring) carry Flux origin label `helm.toolkit.fluxcd.io/name` and are skipped, their gate comes from their own chart. Their chartRef is still synced as before. Failed update was logged and dropped before, and nothing requeues the definition for hours after that. Now errors are returned, so a conflict is retried. Unit tests cover both sync directions, `waitStrategy: legacy` without expressions, no rewrite of in-sync release (nil vs empty slice too), chart-rendered and other-kind releases left alone, and update failure returned as error. Fixes #4409 ### Downstream repositories - [x] No downstream repository is affected by this change - [ ] [cozystack/website](https://github.com/cozystack/website) - follow-up: - [ ] [cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack) - follow-up: - [ ] [cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack) - follow-up: - [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up: - [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up: - [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) - follow-up: - [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) - follow-up: - [ ] [cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server) - follow-up: - [ ] [cozystack/external-apps-example](https://github.com/cozystack/external-apps-example) - follow-up: - [ ] [cozystack/examples](https://github.com/cozystack/examples) - follow-up: - [ ] [cozystack/community](https://github.com/cozystack/community) - follow-up: ### Release note ```release-note fix(controller): existing HelmReleases now get `healthCheckExprs` and `waitStrategy` from their ApplicationDefinition. vm-disk and postgres releases created before these expressions were added stop waiting on DataVolume and KEDA ScaledObject during upgrades. ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **Bug Fixes** * Helm releases now synchronize health-check expressions and wait strategies configured in their application definition, including when those values are removed or no expressions are specified. * Releases with chart-rendered health-check settings retain those settings, and releases outside the matching application kind are unaffected. * Update failures are now reported during reconciliation, while updates to other releases continue. Reconciliation can retry after a failed update. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2 parents 1ac83ae + 8d8179f commit 372c855

2 files changed

Lines changed: 332 additions & 3 deletions

File tree

‎internal/controller/applicationdefinition_helmreconciler.go‎

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package controller
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67

78
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
@@ -10,6 +11,7 @@ import (
1011
helmv2 "github.com/fluxcd/helm-controller/api/v2"
1112

1213
corev1 "k8s.io/api/core/v1"
14+
"k8s.io/apimachinery/pkg/api/equality"
1315
"k8s.io/apimachinery/pkg/runtime"
1416
"k8s.io/apimachinery/pkg/types"
1517
"k8s.io/client-go/tools/record"
@@ -133,16 +135,20 @@ func (r *ApplicationDefinitionHelmReconciler) updateHelmReleasesForAppDef(ctx co
133135

134136
logger.V(4).Info("Found HelmReleases to update", "appDef", appDef.Name, "kind", applicationKind, "count", len(hrList.Items))
135137

136-
// Update each HelmRelease
138+
// Update each HelmRelease. Only definition events and the manager's
139+
// hours-long resync enqueue this reconciler, so a release whose update
140+
// failed (a conflict with helm-controller writing it, say) would stay out
141+
// of sync until then unless the failure is returned and retried.
142+
var errs []error
137143
for i := range hrList.Items {
138144
hr := &hrList.Items[i]
139145
if err := r.updateHelmReleaseChart(ctx, hr, appDef); err != nil {
140146
logger.Error(err, "failed to update HelmRelease", "name", hr.Name, "namespace", hr.Namespace)
141-
continue
147+
errs = append(errs, err)
142148
}
143149
}
144150

145-
return nil
151+
return errors.Join(errs...)
146152
}
147153

148154
// expectedValuesFrom returns the expected valuesFrom configuration for HelmReleases
@@ -250,6 +256,31 @@ func (r *ApplicationDefinitionHelmReconciler) updateHelmReleaseChart(ctx context
250256
}
251257
}
252258

259+
// Check and update the health checks the wait applies. cozystack-api writes
260+
// healthCheckExprs and the wait strategy only when it builds a release, so
261+
// a release created before the definition exempted an object would keep
262+
// waiting on it at every upgrade, and fail for good on one that never
263+
// settles. Both are derived the way the API derives them, in both
264+
// directions, so the release matches what a fresh write would produce. A
265+
// release another chart renders carries Flux's origin label and takes its
266+
// gate from that chart.
267+
if _, rendered := hr.Labels["helm.toolkit.fluxcd.io/name"]; !rendered {
268+
exprs := appDef.Spec.Release.HealthCheckExprs
269+
// Semantic equality treats nil and empty as equal, so a release
270+
// already in sync is not rewritten.
271+
if !equality.Semantic.DeepEqual(hrCopy.Spec.HealthCheckExprs, exprs) {
272+
logger.V(4).Info("Updating HelmRelease healthCheckExprs", "name", hr.Name, "namespace", hr.Namespace)
273+
hrCopy.Spec.HealthCheckExprs = exprs
274+
updated = true
275+
}
276+
waitStrategy := config.ResolveWaitStrategy(appDef.Spec.Release.WaitStrategy, len(exprs) > 0)
277+
if !equality.Semantic.DeepEqual(hrCopy.Spec.WaitStrategy, waitStrategy) {
278+
logger.V(4).Info("Updating HelmRelease waitStrategy", "name", hr.Name, "namespace", hr.Namespace, "waitStrategy", waitStrategy)
279+
hrCopy.Spec.WaitStrategy = waitStrategy
280+
updated = true
281+
}
282+
}
283+
253284
// Check and update labels from ApplicationDefinition
254285
if len(appDef.Spec.Release.Labels) > 0 {
255286
if hrCopy.Labels == nil {

‎internal/controller/applicationdefinition_helmreconciler_test.go‎

Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,24 @@ package controller
22

33
import (
44
"context"
5+
"errors"
6+
"reflect"
57
"strings"
68
"testing"
79
"time"
810

911
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
1012
"github.com/cozystack/cozystack/pkg/config"
1113
helmv2 "github.com/fluxcd/helm-controller/api/v2"
14+
"github.com/fluxcd/pkg/apis/kustomize"
15+
apierrors "k8s.io/apimachinery/pkg/api/errors"
1216
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1317
"k8s.io/apimachinery/pkg/runtime"
1418
"k8s.io/apimachinery/pkg/types"
1519
"k8s.io/client-go/tools/record"
20+
"sigs.k8s.io/controller-runtime/pkg/client"
1621
"sigs.k8s.io/controller-runtime/pkg/client/fake"
22+
"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
1723
"sigs.k8s.io/controller-runtime/pkg/reconcile"
1824
)
1925

@@ -661,3 +667,295 @@ func TestAppDefHelm_SameKindRequests(t *testing.T) {
661667
t.Fatalf("requests = %v (%d), want harbor and tap-harbor once each", names, len(reqs))
662668
}
663669
}
670+
671+
// dataVolumeCurrent is the expression the vm-disk definition carries, so that a
672+
// disk CDI is still importing does not hold its release's wait.
673+
var dataVolumeCurrent = kustomize.CustomHealthCheck{
674+
APIVersion: "cdi.kubevirt.io/v1beta1",
675+
Kind: "DataVolume",
676+
HealthCheckExpressions: kustomize.HealthCheckExpressions{Current: "true"},
677+
}
678+
679+
// vmDiskRelease builds a VMDisk HelmRelease the way cozystack-api wrote it
680+
// before the definition declared any expression: chartRef and valuesFrom in
681+
// sync, no healthCheckExprs, no waitStrategy.
682+
func vmDiskRelease() *helmv2.HelmRelease {
683+
return &helmv2.HelmRelease{
684+
ObjectMeta: metav1.ObjectMeta{
685+
Name: "vm-disk-data",
686+
Namespace: "tenant-foo",
687+
Labels: map[string]string{
688+
"apps.cozystack.io/application.kind": "VMDisk",
689+
"apps.cozystack.io/application.group": "apps.cozystack.io",
690+
},
691+
},
692+
Spec: helmv2.HelmReleaseSpec{
693+
ChartRef: &helmv2.CrossNamespaceSourceReference{
694+
Kind: "ExternalArtifact",
695+
Name: "cozystack-vm-disk-application-kubevirt-vm-disk",
696+
Namespace: "cozy-system",
697+
},
698+
ValuesFrom: expectedValuesFrom(),
699+
},
700+
}
701+
}
702+
703+
// vmDiskAppDef builds the VMDisk ApplicationDefinition with the given wait
704+
// strategy and expressions.
705+
func vmDiskAppDef(waitStrategy string, exprs ...kustomize.CustomHealthCheck) *cozyv1alpha1.ApplicationDefinition {
706+
return &cozyv1alpha1.ApplicationDefinition{
707+
ObjectMeta: metav1.ObjectMeta{Name: "vm-disk"},
708+
Spec: cozyv1alpha1.ApplicationDefinitionSpec{
709+
Application: cozyv1alpha1.ApplicationDefinitionApplication{Kind: "VMDisk"},
710+
Release: cozyv1alpha1.ApplicationDefinitionRelease{
711+
ChartRef: &helmv2.CrossNamespaceSourceReference{
712+
Kind: "ExternalArtifact",
713+
Name: "cozystack-vm-disk-application-kubevirt-vm-disk",
714+
Namespace: "cozy-system",
715+
},
716+
WaitStrategy: waitStrategy,
717+
HealthCheckExprs: exprs,
718+
},
719+
},
720+
}
721+
}
722+
723+
// TestAppDefHelm_HealthCheckExprsApplied pins the drift behind #4409:
724+
// cozystack-api writes healthCheckExprs only when it builds a release, so a
725+
// disk created before the definition exempted its DataVolume kept waiting on
726+
// it at every upgrade, and one CDI never finished importing failed its upgrade
727+
// for good. The expressions must land, with the poller strategy that is the
728+
// only one evaluating them.
729+
func TestAppDefHelm_HealthCheckExprsApplied(t *testing.T) {
730+
got := reconcileVMInstance(t, vmDiskAppDef("", dataVolumeCurrent), vmDiskRelease())
731+
732+
if want := []kustomize.CustomHealthCheck{dataVolumeCurrent}; !reflect.DeepEqual(got.Spec.HealthCheckExprs, want) {
733+
t.Fatalf("expected HealthCheckExprs %+v, got %+v", want, got.Spec.HealthCheckExprs)
734+
}
735+
if got.Spec.WaitStrategy == nil || got.Spec.WaitStrategy.Name != helmv2.WaitStrategyPoller {
736+
t.Fatalf("expected WaitStrategy poller, got %+v", got.Spec.WaitStrategy)
737+
}
738+
}
739+
740+
// TestAppDefHelm_HealthCheckExprsRemoved pins the other direction: the API
741+
// rebuilds the whole spec on every write, so a release left carrying what the
742+
// definition dropped would differ from one written a moment later.
743+
func TestAppDefHelm_HealthCheckExprsRemoved(t *testing.T) {
744+
hr := vmDiskRelease()
745+
hr.Spec.HealthCheckExprs = []kustomize.CustomHealthCheck{dataVolumeCurrent}
746+
hr.Spec.WaitStrategy = &helmv2.WaitStrategy{Name: helmv2.WaitStrategyPoller}
747+
748+
got := reconcileVMInstance(t, vmDiskAppDef(""), hr)
749+
750+
if len(got.Spec.HealthCheckExprs) != 0 {
751+
t.Fatalf("expected HealthCheckExprs removed, got %+v", got.Spec.HealthCheckExprs)
752+
}
753+
if got.Spec.WaitStrategy != nil {
754+
t.Fatalf("expected WaitStrategy removed, got %+v", got.Spec.WaitStrategy)
755+
}
756+
}
757+
758+
// TestAppDefHelm_WaitStrategyWithoutExprsApplied pins that an explicit strategy
759+
// is carried on its own, not only as the default the expressions imply.
760+
func TestAppDefHelm_WaitStrategyWithoutExprsApplied(t *testing.T) {
761+
got := reconcileVMInstance(t, vmDiskAppDef("legacy"), vmDiskRelease())
762+
763+
if got.Spec.WaitStrategy == nil || got.Spec.WaitStrategy.Name != helmv2.WaitStrategyLegacy {
764+
t.Fatalf("expected WaitStrategy legacy, got %+v", got.Spec.WaitStrategy)
765+
}
766+
if len(got.Spec.HealthCheckExprs) != 0 {
767+
t.Fatalf("expected no HealthCheckExprs, got %+v", got.Spec.HealthCheckExprs)
768+
}
769+
}
770+
771+
// TestAppDefHelm_HealthCheckExprsInSyncSkipsUpdate pins idempotence, like
772+
// TestAppDefHelm_DisableWaitInSyncSkipsUpdate. A definition read with
773+
// healthCheckExprs: [] decodes to an empty slice, while a release built without
774+
// expressions carries none at all; comparing the two as different would
775+
// rewrite every release of the kind on every reconcile. The fake client drops
776+
// an empty slice on its JSON round trip, so the objects are handed to
777+
// updateHelmReleaseChart directly.
778+
func TestAppDefHelm_HealthCheckExprsInSyncSkipsUpdate(t *testing.T) {
779+
cases := []struct {
780+
name string
781+
defExprs []kustomize.CustomHealthCheck
782+
hrExprs []kustomize.CustomHealthCheck
783+
hrStrategy *helmv2.WaitStrategy
784+
}{
785+
{
786+
name: "expressions and poller already set",
787+
defExprs: []kustomize.CustomHealthCheck{dataVolumeCurrent},
788+
hrExprs: []kustomize.CustomHealthCheck{dataVolumeCurrent},
789+
hrStrategy: &helmv2.WaitStrategy{Name: helmv2.WaitStrategyPoller},
790+
},
791+
{
792+
name: "empty on the definition, absent on the release",
793+
defExprs: []kustomize.CustomHealthCheck{},
794+
},
795+
{
796+
name: "absent on the definition, empty on the release",
797+
hrExprs: []kustomize.CustomHealthCheck{},
798+
},
799+
}
800+
for _, tc := range cases {
801+
t.Run(tc.name, func(t *testing.T) {
802+
stored := vmDiskRelease()
803+
stored.Spec.HealthCheckExprs = tc.hrExprs
804+
stored.Spec.WaitStrategy = tc.hrStrategy
805+
806+
scheme := newAppDefHelmScheme(t)
807+
fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(stored).Build()
808+
809+
before := &helmv2.HelmRelease{}
810+
if err := fakeClient.Get(context.TODO(), types.NamespacedName{Name: stored.Name, Namespace: stored.Namespace}, before); err != nil {
811+
t.Fatalf("get HR: %v", err)
812+
}
813+
hr := before.DeepCopy()
814+
hr.Spec.HealthCheckExprs = tc.hrExprs
815+
816+
r := &ApplicationDefinitionHelmReconciler{Client: fakeClient, Scheme: scheme}
817+
if err := r.updateHelmReleaseChart(context.TODO(), hr, vmDiskAppDef("", tc.defExprs...)); err != nil {
818+
t.Fatalf("unexpected error: %v", err)
819+
}
820+
821+
after := &helmv2.HelmRelease{}
822+
if err := fakeClient.Get(context.TODO(), types.NamespacedName{Name: stored.Name, Namespace: stored.Namespace}, after); err != nil {
823+
t.Fatalf("get HR after update: %v", err)
824+
}
825+
if after.ResourceVersion != before.ResourceVersion {
826+
t.Fatalf("release rewritten with nothing to change: %q -> %q", before.ResourceVersion, after.ResourceVersion)
827+
}
828+
})
829+
}
830+
}
831+
832+
// TestAppDefHelm_HealthCheckExprsLeaveRenderedReleaseAlone pins the scope of
833+
// the sync. The tenant chart renders an Etcd HelmRelease carrying the same kind
834+
// labels as one the API builds, and it sets that release's wait gate itself;
835+
// rewriting it from the definition would strip the chart's gate until the
836+
// parent's next upgrade put it back. Its chartRef still follows the definition.
837+
func TestAppDefHelm_HealthCheckExprsLeaveRenderedReleaseAlone(t *testing.T) {
838+
chartGate := kustomize.CustomHealthCheck{
839+
APIVersion: "etcd-operator.cozystack.io/v1alpha2",
840+
Kind: "EtcdCluster",
841+
HealthCheckExpressions: kustomize.HealthCheckExpressions{Current: "status.readyMembers == spec.replicas"},
842+
}
843+
hr := &helmv2.HelmRelease{
844+
ObjectMeta: metav1.ObjectMeta{Name: "etcd", Namespace: "tenant-foo", Labels: map[string]string{
845+
"apps.cozystack.io/application.kind": "Etcd",
846+
"apps.cozystack.io/application.group": "apps.cozystack.io",
847+
"helm.toolkit.fluxcd.io/name": "tenant-foo",
848+
}},
849+
Spec: helmv2.HelmReleaseSpec{
850+
ChartRef: &helmv2.CrossNamespaceSourceReference{Kind: "ExternalArtifact", Name: "stale", Namespace: "cozy-system"},
851+
ValuesFrom: expectedValuesFrom(),
852+
HealthCheckExprs: []kustomize.CustomHealthCheck{chartGate},
853+
WaitStrategy: &helmv2.WaitStrategy{Name: helmv2.WaitStrategyPoller},
854+
},
855+
}
856+
appDef := &cozyv1alpha1.ApplicationDefinition{
857+
ObjectMeta: metav1.ObjectMeta{Name: "etcd"},
858+
Spec: cozyv1alpha1.ApplicationDefinitionSpec{
859+
Application: cozyv1alpha1.ApplicationDefinitionApplication{Kind: "Etcd"},
860+
Release: cozyv1alpha1.ApplicationDefinitionRelease{
861+
ChartRef: &helmv2.CrossNamespaceSourceReference{Kind: "ExternalArtifact", Name: "cozystack-etcd-application-default-etcd", Namespace: "cozy-system"},
862+
},
863+
},
864+
}
865+
866+
got := reconcileVMInstance(t, appDef, hr)
867+
868+
if got.Spec.ChartRef == nil || got.Spec.ChartRef.Name != "cozystack-etcd-application-default-etcd" {
869+
t.Fatalf("expected ChartRef synced from the definition, got %+v", got.Spec.ChartRef)
870+
}
871+
if want := []kustomize.CustomHealthCheck{chartGate}; !reflect.DeepEqual(got.Spec.HealthCheckExprs, want) {
872+
t.Fatalf("expected the chart's HealthCheckExprs %+v kept, got %+v", want, got.Spec.HealthCheckExprs)
873+
}
874+
if got.Spec.WaitStrategy == nil || got.Spec.WaitStrategy.Name != helmv2.WaitStrategyPoller {
875+
t.Fatalf("expected the chart's WaitStrategy poller kept, got %+v", got.Spec.WaitStrategy)
876+
}
877+
}
878+
879+
// TestAppDefHelm_HealthCheckExprsScopedToKind pins the kind label in the
880+
// selector: a release of another kind must not pick up the VMDisk expressions.
881+
func TestAppDefHelm_HealthCheckExprsScopedToKind(t *testing.T) {
882+
disk := vmDiskRelease()
883+
other := vmDiskRelease()
884+
other.Name = "harbor-registry"
885+
other.Labels["apps.cozystack.io/application.kind"] = "Harbor"
886+
887+
scheme := newAppDefHelmScheme(t)
888+
appDef := vmDiskAppDef("", dataVolumeCurrent)
889+
fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(appDef, disk, other).Build()
890+
891+
r := &ApplicationDefinitionHelmReconciler{Client: fakeClient, Scheme: scheme}
892+
if _, err := r.Reconcile(context.TODO(), reconcile.Request{
893+
NamespacedName: types.NamespacedName{Name: appDef.Name},
894+
}); err != nil {
895+
t.Fatalf("unexpected error: %v", err)
896+
}
897+
898+
gotDisk := &helmv2.HelmRelease{}
899+
if err := fakeClient.Get(context.TODO(), types.NamespacedName{Name: disk.Name, Namespace: disk.Namespace}, gotDisk); err != nil {
900+
t.Fatalf("get VMDisk HR: %v", err)
901+
}
902+
if len(gotDisk.Spec.HealthCheckExprs) != 1 {
903+
t.Fatalf("expected VMDisk HealthCheckExprs synced, got %+v", gotDisk.Spec.HealthCheckExprs)
904+
}
905+
906+
gotOther := &helmv2.HelmRelease{}
907+
if err := fakeClient.Get(context.TODO(), types.NamespacedName{Name: other.Name, Namespace: other.Namespace}, gotOther); err != nil {
908+
t.Fatalf("get Harbor HR: %v", err)
909+
}
910+
if len(gotOther.Spec.HealthCheckExprs) != 0 {
911+
t.Fatalf("expected Harbor HealthCheckExprs untouched, got %+v", gotOther.Spec.HealthCheckExprs)
912+
}
913+
if gotOther.Spec.WaitStrategy != nil {
914+
t.Fatalf("expected Harbor WaitStrategy untouched, got %+v", gotOther.Spec.WaitStrategy)
915+
}
916+
}
917+
918+
// TestAppDefHelm_UpdateFailureRequeues pins that a failed write is returned
919+
// rather than logged and dropped: nothing else re-enqueues the definition for
920+
// hours, so a release that lost a conflict to helm-controller would keep its
921+
// old wait until then. The releases after it are still written.
922+
func TestAppDefHelm_UpdateFailureRequeues(t *testing.T) {
923+
first := vmDiskRelease()
924+
second := vmDiskRelease()
925+
second.Name = "vm-disk-logs"
926+
927+
failed := ""
928+
scheme := newAppDefHelmScheme(t)
929+
appDef := vmDiskAppDef("", dataVolumeCurrent)
930+
fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(appDef, first, second).
931+
WithInterceptorFuncs(interceptor.Funcs{
932+
Update: func(ctx context.Context, cl client.WithWatch, obj client.Object, opts ...client.UpdateOption) error {
933+
// Fail whichever release is written first, so the assertion
934+
// below does not depend on the order List returns them in.
935+
if failed == "" {
936+
failed = obj.GetName()
937+
return apierrors.NewConflict(helmv2.GroupVersion.WithResource("helmreleases").GroupResource(), obj.GetName(), errors.New("the object has been modified"))
938+
}
939+
return cl.Update(ctx, obj, opts...)
940+
},
941+
}).Build()
942+
943+
r := &ApplicationDefinitionHelmReconciler{Client: fakeClient, Scheme: scheme}
944+
if _, err := r.Reconcile(context.TODO(), reconcile.Request{
945+
NamespacedName: types.NamespacedName{Name: appDef.Name},
946+
}); !apierrors.IsConflict(err) {
947+
t.Fatalf("expected the conflict returned so the definition is requeued, got %v", err)
948+
}
949+
950+
written := first.Name
951+
if failed == first.Name {
952+
written = second.Name
953+
}
954+
got := &helmv2.HelmRelease{}
955+
if err := fakeClient.Get(context.TODO(), types.NamespacedName{Name: written, Namespace: "tenant-foo"}, got); err != nil {
956+
t.Fatalf("get HR: %v", err)
957+
}
958+
if len(got.Spec.HealthCheckExprs) != 1 {
959+
t.Fatalf("expected %s synced past the failed %s, got %+v", written, failed, got.Spec.HealthCheckExprs)
960+
}
961+
}

0 commit comments

Comments
 (0)