Skip to content

Commit 6809614

Browse files
authored
fix: nil desiredState for not found releases in applier (#1539)
Co-authored-by: Artur Zych <[email protected]>
1 parent 332397b commit 6809614

File tree

2 files changed

+325
-7
lines changed

2 files changed

+325
-7
lines changed

internal/applier/helm.go

+4-7
Original file line numberDiff line numberDiff line change
@@ -153,13 +153,6 @@ func (h *Helm) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.ClusterExte
153153

154154
func (h *Helm) getReleaseState(cl helmclient.ActionInterface, ext *ocv1.ClusterExtension, chrt *chart.Chart, values chartutil.Values, post postrender.PostRenderer) (*release.Release, *release.Release, string, error) {
155155
currentRelease, err := cl.Get(ext.GetName())
156-
if err != nil && !errors.Is(err, driver.ErrReleaseNotFound) {
157-
return nil, nil, StateError, err
158-
}
159-
if errors.Is(err, driver.ErrReleaseNotFound) {
160-
return nil, nil, StateNeedsInstall, nil
161-
}
162-
163156
if errors.Is(err, driver.ErrReleaseNotFound) {
164157
desiredRelease, err := cl.Install(ext.GetName(), ext.Spec.Namespace, chrt, values, func(i *action.Install) error {
165158
i.DryRun = true
@@ -171,6 +164,10 @@ func (h *Helm) getReleaseState(cl helmclient.ActionInterface, ext *ocv1.ClusterE
171164
}
172165
return nil, desiredRelease, StateNeedsInstall, nil
173166
}
167+
if err != nil {
168+
return nil, nil, StateError, err
169+
}
170+
174171
desiredRelease, err := cl.Upgrade(ext.GetName(), ext.Spec.Namespace, chrt, values, func(upgrade *action.Upgrade) error {
175172
upgrade.MaxHistory = maxHelmReleaseHistory
176173
upgrade.DryRun = true

internal/applier/helm_test.go

+321
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
package applier_test
2+
3+
import (
4+
"context"
5+
"errors"
6+
"os"
7+
"testing"
8+
"testing/fstest"
9+
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
"helm.sh/helm/v3/pkg/action"
13+
"helm.sh/helm/v3/pkg/chart"
14+
"helm.sh/helm/v3/pkg/release"
15+
"helm.sh/helm/v3/pkg/storage/driver"
16+
"sigs.k8s.io/controller-runtime/pkg/client"
17+
18+
helmclient "github.com/operator-framework/helm-operator-plugins/pkg/client"
19+
20+
v1 "github.com/operator-framework/operator-controller/api/v1"
21+
"github.com/operator-framework/operator-controller/internal/applier"
22+
)
23+
24+
type mockPreflight struct {
25+
installErr error
26+
upgradeErr error
27+
}
28+
29+
func (mp *mockPreflight) Install(context.Context, *release.Release) error {
30+
return mp.installErr
31+
}
32+
33+
func (mp *mockPreflight) Upgrade(context.Context, *release.Release) error {
34+
return mp.upgradeErr
35+
}
36+
37+
type mockActionGetter struct {
38+
actionClientForErr error
39+
getClientErr error
40+
installErr error
41+
dryRunInstallErr error
42+
upgradeErr error
43+
dryRunUpgradeErr error
44+
reconcileErr error
45+
desiredRel *release.Release
46+
currentRel *release.Release
47+
}
48+
49+
func (mag *mockActionGetter) ActionClientFor(ctx context.Context, obj client.Object) (helmclient.ActionInterface, error) {
50+
return mag, mag.actionClientForErr
51+
}
52+
53+
func (mag *mockActionGetter) Get(name string, opts ...helmclient.GetOption) (*release.Release, error) {
54+
return mag.currentRel, mag.getClientErr
55+
}
56+
57+
func (mag *mockActionGetter) History(name string, opts ...helmclient.HistoryOption) ([]*release.Release, error) {
58+
return nil, mag.getClientErr
59+
}
60+
61+
func (mag *mockActionGetter) Install(name, namespace string, chrt *chart.Chart, vals map[string]interface{}, opts ...helmclient.InstallOption) (*release.Release, error) {
62+
i := action.Install{}
63+
for _, opt := range opts {
64+
if err := opt(&i); err != nil {
65+
return nil, err
66+
}
67+
}
68+
if i.DryRun {
69+
return mag.desiredRel, mag.dryRunInstallErr
70+
}
71+
return mag.desiredRel, mag.installErr
72+
}
73+
74+
func (mag *mockActionGetter) Upgrade(name, namespace string, chrt *chart.Chart, vals map[string]interface{}, opts ...helmclient.UpgradeOption) (*release.Release, error) {
75+
i := action.Upgrade{}
76+
for _, opt := range opts {
77+
if err := opt(&i); err != nil {
78+
return nil, err
79+
}
80+
}
81+
if i.DryRun {
82+
return mag.desiredRel, mag.dryRunUpgradeErr
83+
}
84+
return mag.desiredRel, mag.upgradeErr
85+
}
86+
87+
func (mag *mockActionGetter) Uninstall(name string, opts ...helmclient.UninstallOption) (*release.UninstallReleaseResponse, error) {
88+
return nil, nil
89+
}
90+
91+
func (mag *mockActionGetter) Reconcile(rel *release.Release) error {
92+
return mag.reconcileErr
93+
}
94+
95+
var (
96+
// required for unmockable call to convert.RegistryV1ToHelmChart
97+
validFS = fstest.MapFS{
98+
"metadata/annotations.yaml": &fstest.MapFile{Data: []byte("{}")},
99+
"manifests": &fstest.MapFile{Data: []byte(`apiVersion: operators.operatorframework.io/v1alpha1
100+
kind: ClusterServiceVersion
101+
metadata:
102+
name: test.v1.0.0
103+
annotations:
104+
olm.properties: '[{"type":"from-csv-annotations-key", "value":"from-csv-annotations-value"}]'
105+
spec:
106+
installModes:
107+
- type: AllNamespaces
108+
supported: true`)},
109+
}
110+
111+
// required for unmockable call to util.ManifestObjects
112+
validManifest = `apiVersion: v1
113+
kind: Service
114+
metadata:
115+
name: service-a
116+
namespace: ns-a
117+
spec:
118+
clusterIP: None
119+
---
120+
apiVersion: v1
121+
kind: Service
122+
metadata:
123+
name: service-b
124+
namespace: ns-b
125+
spec:
126+
clusterIP: 0.0.0.0`
127+
128+
testCE = &v1.ClusterExtension{}
129+
testObjectLabels = map[string]string{"object": "label"}
130+
testStorageLabels = map[string]string{"storage": "label"}
131+
)
132+
133+
func TestApply_Base(t *testing.T) {
134+
t.Run("fails converting content FS to helm chart", func(t *testing.T) {
135+
helmApplier := applier.Helm{}
136+
137+
objs, state, err := helmApplier.Apply(context.TODO(), os.DirFS("/"), testCE, testObjectLabels, testStorageLabels)
138+
require.Error(t, err)
139+
require.Nil(t, objs)
140+
require.Empty(t, state)
141+
})
142+
143+
t.Run("fails trying to obtain an action client", func(t *testing.T) {
144+
mockAcg := &mockActionGetter{actionClientForErr: errors.New("failed getting action client")}
145+
helmApplier := applier.Helm{ActionClientGetter: mockAcg}
146+
147+
objs, state, err := helmApplier.Apply(context.TODO(), validFS, testCE, testObjectLabels, testStorageLabels)
148+
require.Error(t, err)
149+
require.ErrorContains(t, err, "getting action client")
150+
require.Nil(t, objs)
151+
require.Empty(t, state)
152+
})
153+
154+
t.Run("fails getting current release and !driver.ErrReleaseNotFound", func(t *testing.T) {
155+
mockAcg := &mockActionGetter{getClientErr: errors.New("failed getting current release")}
156+
helmApplier := applier.Helm{ActionClientGetter: mockAcg}
157+
158+
objs, state, err := helmApplier.Apply(context.TODO(), validFS, testCE, testObjectLabels, testStorageLabels)
159+
require.Error(t, err)
160+
require.ErrorContains(t, err, "getting current release")
161+
require.Nil(t, objs)
162+
require.Empty(t, state)
163+
})
164+
}
165+
166+
func TestApply_Installation(t *testing.T) {
167+
t.Run("fails during dry-run installation", func(t *testing.T) {
168+
mockAcg := &mockActionGetter{
169+
getClientErr: driver.ErrReleaseNotFound,
170+
dryRunInstallErr: errors.New("failed attempting to dry-run install chart"),
171+
}
172+
helmApplier := applier.Helm{ActionClientGetter: mockAcg}
173+
174+
objs, state, err := helmApplier.Apply(context.TODO(), validFS, testCE, testObjectLabels, testStorageLabels)
175+
require.Error(t, err)
176+
require.ErrorContains(t, err, "attempting to dry-run install chart")
177+
require.Nil(t, objs)
178+
require.Empty(t, state)
179+
})
180+
181+
t.Run("fails during pre-flight installation", func(t *testing.T) {
182+
mockAcg := &mockActionGetter{
183+
getClientErr: driver.ErrReleaseNotFound,
184+
installErr: errors.New("failed installing chart"),
185+
}
186+
mockPf := &mockPreflight{installErr: errors.New("failed during install pre-flight check")}
187+
helmApplier := applier.Helm{ActionClientGetter: mockAcg, Preflights: []applier.Preflight{mockPf}}
188+
189+
objs, state, err := helmApplier.Apply(context.TODO(), validFS, testCE, testObjectLabels, testStorageLabels)
190+
require.Error(t, err)
191+
require.ErrorContains(t, err, "install pre-flight check")
192+
require.Equal(t, applier.StateNeedsInstall, state)
193+
require.Nil(t, objs)
194+
})
195+
196+
t.Run("fails during installation", func(t *testing.T) {
197+
mockAcg := &mockActionGetter{
198+
getClientErr: driver.ErrReleaseNotFound,
199+
installErr: errors.New("failed installing chart"),
200+
}
201+
helmApplier := applier.Helm{ActionClientGetter: mockAcg}
202+
203+
objs, state, err := helmApplier.Apply(context.TODO(), validFS, testCE, testObjectLabels, testStorageLabels)
204+
require.Error(t, err)
205+
require.ErrorContains(t, err, "installing chart")
206+
require.Equal(t, applier.StateNeedsInstall, state)
207+
require.Nil(t, objs)
208+
})
209+
210+
t.Run("successful installation", func(t *testing.T) {
211+
mockAcg := &mockActionGetter{
212+
getClientErr: driver.ErrReleaseNotFound,
213+
desiredRel: &release.Release{
214+
Info: &release.Info{Status: release.StatusDeployed},
215+
Manifest: validManifest,
216+
},
217+
}
218+
helmApplier := applier.Helm{ActionClientGetter: mockAcg}
219+
220+
objs, state, err := helmApplier.Apply(context.TODO(), validFS, testCE, testObjectLabels, testStorageLabels)
221+
require.NoError(t, err)
222+
require.Equal(t, applier.StateNeedsInstall, state)
223+
require.NotNil(t, objs)
224+
assert.Equal(t, "service-a", objs[0].GetName())
225+
assert.Equal(t, "service-b", objs[1].GetName())
226+
})
227+
}
228+
229+
func TestApply_Upgrade(t *testing.T) {
230+
testCurrentRelease := &release.Release{
231+
Info: &release.Info{Status: release.StatusDeployed},
232+
}
233+
234+
t.Run("fails during dry-run upgrade", func(t *testing.T) {
235+
mockAcg := &mockActionGetter{
236+
dryRunUpgradeErr: errors.New("failed attempting to dry-run upgrade chart"),
237+
}
238+
helmApplier := applier.Helm{ActionClientGetter: mockAcg}
239+
240+
objs, state, err := helmApplier.Apply(context.TODO(), validFS, testCE, testObjectLabels, testStorageLabels)
241+
require.Error(t, err)
242+
require.ErrorContains(t, err, "attempting to dry-run upgrade chart")
243+
require.Nil(t, objs)
244+
require.Empty(t, state)
245+
})
246+
247+
t.Run("fails during pre-flight upgrade", func(t *testing.T) {
248+
testDesiredRelease := *testCurrentRelease
249+
testDesiredRelease.Manifest = "do-not-match-current"
250+
251+
mockAcg := &mockActionGetter{
252+
upgradeErr: errors.New("failed upgrading chart"),
253+
currentRel: testCurrentRelease,
254+
desiredRel: &testDesiredRelease,
255+
}
256+
mockPf := &mockPreflight{upgradeErr: errors.New("failed during upgrade pre-flight check")}
257+
helmApplier := applier.Helm{ActionClientGetter: mockAcg, Preflights: []applier.Preflight{mockPf}}
258+
259+
objs, state, err := helmApplier.Apply(context.TODO(), validFS, testCE, testObjectLabels, testStorageLabels)
260+
require.Error(t, err)
261+
require.ErrorContains(t, err, "upgrade pre-flight check")
262+
require.Equal(t, applier.StateNeedsUpgrade, state)
263+
require.Nil(t, objs)
264+
})
265+
266+
t.Run("fails during upgrade", func(t *testing.T) {
267+
testDesiredRelease := *testCurrentRelease
268+
testDesiredRelease.Manifest = "do-not-match-current"
269+
270+
mockAcg := &mockActionGetter{
271+
upgradeErr: errors.New("failed upgrading chart"),
272+
currentRel: testCurrentRelease,
273+
desiredRel: &testDesiredRelease,
274+
}
275+
mockPf := &mockPreflight{}
276+
helmApplier := applier.Helm{ActionClientGetter: mockAcg, Preflights: []applier.Preflight{mockPf}}
277+
278+
objs, state, err := helmApplier.Apply(context.TODO(), validFS, testCE, testObjectLabels, testStorageLabels)
279+
require.Error(t, err)
280+
require.ErrorContains(t, err, "upgrading chart")
281+
require.Equal(t, applier.StateNeedsUpgrade, state)
282+
require.Nil(t, objs)
283+
})
284+
285+
t.Run("fails during upgrade reconcile (StateUnchanged)", func(t *testing.T) {
286+
// make sure desired and current are the same this time
287+
testDesiredRelease := *testCurrentRelease
288+
289+
mockAcg := &mockActionGetter{
290+
reconcileErr: errors.New("failed reconciling charts"),
291+
currentRel: testCurrentRelease,
292+
desiredRel: &testDesiredRelease,
293+
}
294+
mockPf := &mockPreflight{}
295+
helmApplier := applier.Helm{ActionClientGetter: mockAcg, Preflights: []applier.Preflight{mockPf}}
296+
297+
objs, state, err := helmApplier.Apply(context.TODO(), validFS, testCE, testObjectLabels, testStorageLabels)
298+
require.Error(t, err)
299+
require.ErrorContains(t, err, "reconciling charts")
300+
require.Equal(t, applier.StateUnchanged, state)
301+
require.Nil(t, objs)
302+
})
303+
304+
t.Run("successful upgrade", func(t *testing.T) {
305+
testDesiredRelease := *testCurrentRelease
306+
testDesiredRelease.Manifest = validManifest
307+
308+
mockAcg := &mockActionGetter{
309+
currentRel: testCurrentRelease,
310+
desiredRel: &testDesiredRelease,
311+
}
312+
helmApplier := applier.Helm{ActionClientGetter: mockAcg}
313+
314+
objs, state, err := helmApplier.Apply(context.TODO(), validFS, testCE, testObjectLabels, testStorageLabels)
315+
require.NoError(t, err)
316+
require.Equal(t, applier.StateNeedsUpgrade, state)
317+
require.NotNil(t, objs)
318+
assert.Equal(t, "service-a", objs[0].GetName())
319+
assert.Equal(t, "service-b", objs[1].GetName())
320+
})
321+
}

0 commit comments

Comments
 (0)