Skip to content

Commit 8ef484b

Browse files
committed
chore: add helm applier unit tests
1 parent f0e25c1 commit 8ef484b

File tree

1 file changed

+315
-0
lines changed

1 file changed

+315
-0
lines changed

internal/applier/helm_test.go

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

0 commit comments

Comments
 (0)