Skip to content

Commit 957fc1b

Browse files
perdasilvaPer Goncalves da Silva
and
Per Goncalves da Silva
authored
mask helm conflict errors (#1016)
Signed-off-by: Per Goncalves da Silva <[email protected]> Co-authored-by: Per Goncalves da Silva <[email protected]>
1 parent 369bcce commit 957fc1b

File tree

5 files changed

+341
-1
lines changed

5 files changed

+341
-1
lines changed

cmd/manager/main.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import (
4141
helmclient "github.com/operator-framework/helm-operator-plugins/pkg/client"
4242

4343
ocv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1"
44+
"github.com/operator-framework/operator-controller/internal/action"
4445
"github.com/operator-framework/operator-controller/internal/catalogmetadata/cache"
4546
catalogclient "github.com/operator-framework/operator-controller/internal/catalogmetadata/client"
4647
"github.com/operator-framework/operator-controller/internal/controllers"
@@ -184,9 +185,10 @@ func main() {
184185
os.Exit(1)
185186
}
186187

187-
acg, err := helmclient.NewActionClientGetter(cfgGetter,
188+
acg, err := action.NewWrappedActionClientGetter(cfgGetter,
188189
helmclient.WithFailureRollbacks(false),
189190
)
191+
190192
if err != nil {
191193
setupLog.Error(err, "unable to create helm client")
192194
os.Exit(1)

internal/action/error/errors.go

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package error
2+
3+
import (
4+
"fmt"
5+
"regexp"
6+
)
7+
8+
var (
9+
installConflictErrorPattern = regexp.MustCompile(`Unable to continue with install: (\w+) "(.*)" in namespace "(.*)" exists and cannot be imported into the current release.*`)
10+
)
11+
12+
type Olmv1Err struct {
13+
originalErr error
14+
message string
15+
}
16+
17+
func (o Olmv1Err) Error() string {
18+
return o.message
19+
}
20+
21+
func (o Olmv1Err) Cause() error {
22+
return o.originalErr
23+
}
24+
25+
func newOlmv1Err(originalErr error, message string) error {
26+
return &Olmv1Err{
27+
originalErr: originalErr,
28+
message: message,
29+
}
30+
}
31+
32+
func AsOlmErr(originalErr error) error {
33+
if originalErr == nil {
34+
return nil
35+
}
36+
37+
for _, exec := range rules {
38+
if err := exec(originalErr); err != nil {
39+
return err
40+
}
41+
}
42+
43+
// let's mark any unmatched errors as unknown
44+
return defaultErrTranslator(originalErr)
45+
}
46+
47+
// rule is a function that translates an error into a more specific error
48+
// typically to hide internal implementation details
49+
// in: helm error
50+
// out: nil -> no translation | !nil -> translated error
51+
type rule func(originalErr error) error
52+
53+
// rules is a list of rules for error translation
54+
var rules = []rule{
55+
helmInstallConflictErr,
56+
}
57+
58+
// installConflictErrorTranslator
59+
func helmInstallConflictErr(originalErr error) error {
60+
matches := installConflictErrorPattern.FindStringSubmatch(originalErr.Error())
61+
if len(matches) != 4 {
62+
// there was no match
63+
return nil
64+
}
65+
kind := matches[1]
66+
name := matches[2]
67+
namespace := matches[3]
68+
return newOlmv1Err(originalErr, fmt.Sprintf("%s '%s' already exists in namespace '%s' and cannot be managed by operator-controller", kind, name, namespace))
69+
}
70+
71+
func defaultErrTranslator(originalErr error) error {
72+
return originalErr
73+
}

internal/action/error/errors_test.go

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package error
2+
3+
import (
4+
"errors"
5+
"testing"
6+
)
7+
8+
func TestAsOlmErr(t *testing.T) {
9+
tests := []struct {
10+
name string
11+
err error
12+
expected error
13+
}{
14+
{
15+
name: "Install conflict error (match)",
16+
err: errors.New("Unable to continue with install: Deployment \"my-deploy\" in namespace \"my-namespace\" exists and cannot be imported into the current release"),
17+
expected: errors.New("Deployment 'my-deploy' already exists in namespace 'my-namespace' and cannot be managed by operator-controller"),
18+
},
19+
{
20+
name: "Install conflict error (no match)",
21+
err: errors.New("Unable to continue with install: because of something"),
22+
expected: errors.New("Unable to continue with install: because of something"),
23+
},
24+
{
25+
name: "Unknown error",
26+
err: errors.New("some unknown error"),
27+
expected: errors.New("some unknown error"),
28+
},
29+
{
30+
name: "Nil error",
31+
err: nil,
32+
expected: nil,
33+
},
34+
}
35+
36+
for _, tt := range tests {
37+
t.Run(tt.name, func(t *testing.T) {
38+
result := AsOlmErr(tt.err)
39+
if result != nil && result.Error() != tt.expected.Error() {
40+
t.Errorf("Expected: %v, got: %v", tt.expected, result)
41+
}
42+
})
43+
}
44+
}

internal/action/helm.go

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package action
2+
3+
import (
4+
"context"
5+
6+
"helm.sh/helm/v3/pkg/chart"
7+
"helm.sh/helm/v3/pkg/release"
8+
"sigs.k8s.io/controller-runtime/pkg/client"
9+
10+
actionclient "github.com/operator-framework/helm-operator-plugins/pkg/client"
11+
12+
olmv1error "github.com/operator-framework/operator-controller/internal/action/error"
13+
)
14+
15+
type ActionClientGetter struct {
16+
actionclient.ActionClientGetter
17+
}
18+
19+
func (a ActionClientGetter) ActionClientFor(ctx context.Context, obj client.Object) (actionclient.ActionInterface, error) {
20+
ac, err := a.ActionClientGetter.ActionClientFor(ctx, obj)
21+
if err != nil {
22+
return nil, err
23+
}
24+
return &ActionClient{
25+
ActionInterface: ac,
26+
actionClientErrorTranslator: olmv1error.AsOlmErr,
27+
}, nil
28+
}
29+
30+
func NewWrappedActionClientGetter(acg actionclient.ActionConfigGetter, opts ...actionclient.ActionClientGetterOption) (actionclient.ActionClientGetter, error) {
31+
ag, err := actionclient.NewActionClientGetter(acg, opts...)
32+
if err != nil {
33+
return nil, err
34+
}
35+
return &ActionClientGetter{
36+
ActionClientGetter: ag,
37+
}, nil
38+
}
39+
40+
type ActionClientErrorTranslator func(err error) error
41+
42+
type ActionClient struct {
43+
actionclient.ActionInterface
44+
actionClientErrorTranslator ActionClientErrorTranslator
45+
}
46+
47+
func NewWrappedActionClient(ca actionclient.ActionInterface, errTranslator ActionClientErrorTranslator) actionclient.ActionInterface {
48+
return &ActionClient{
49+
ActionInterface: ca,
50+
actionClientErrorTranslator: errTranslator,
51+
}
52+
}
53+
54+
func (a ActionClient) Install(name, namespace string, chrt *chart.Chart, vals map[string]interface{}, opts ...actionclient.InstallOption) (*release.Release, error) {
55+
rel, err := a.ActionInterface.Install(name, namespace, chrt, vals, opts...)
56+
err = a.actionClientErrorTranslator(err)
57+
return rel, err
58+
}
59+
60+
func (a ActionClient) Upgrade(name, namespace string, chrt *chart.Chart, vals map[string]interface{}, opts ...actionclient.UpgradeOption) (*release.Release, error) {
61+
rel, err := a.ActionInterface.Upgrade(name, namespace, chrt, vals, opts...)
62+
err = a.actionClientErrorTranslator(err)
63+
return rel, err
64+
}
65+
66+
func (a ActionClient) Uninstall(name string, opts ...actionclient.UninstallOption) (*release.UninstallReleaseResponse, error) {
67+
resp, err := a.ActionInterface.Uninstall(name, opts...)
68+
err = a.actionClientErrorTranslator(err)
69+
return resp, err
70+
}
71+
72+
func (a ActionClient) Get(name string, opts ...actionclient.GetOption) (*release.Release, error) {
73+
resp, err := a.ActionInterface.Get(name, opts...)
74+
err = a.actionClientErrorTranslator(err)
75+
return resp, err
76+
}
77+
78+
func (a ActionClient) Reconcile(rel *release.Release) error {
79+
return a.actionClientErrorTranslator(a.ActionInterface.Reconcile(rel))
80+
}

internal/action/helm_test.go

+141
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package action
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/mock"
11+
"helm.sh/helm/v3/pkg/chart"
12+
"helm.sh/helm/v3/pkg/release"
13+
"sigs.k8s.io/controller-runtime/pkg/client"
14+
15+
actionclient "github.com/operator-framework/helm-operator-plugins/pkg/client"
16+
)
17+
18+
var _ actionclient.ActionInterface = &mockActionClient{}
19+
20+
type mockActionClient struct {
21+
mock.Mock
22+
}
23+
24+
func (m *mockActionClient) Get(name string, opts ...actionclient.GetOption) (*release.Release, error) {
25+
args := m.Called(name, opts)
26+
if args.Get(0) == nil {
27+
return nil, args.Error(1)
28+
}
29+
return args.Get(0).(*release.Release), args.Error(1)
30+
}
31+
32+
func (m *mockActionClient) Install(name, namespace string, chrt *chart.Chart, vals map[string]interface{}, opts ...actionclient.InstallOption) (*release.Release, error) {
33+
args := m.Called(name, namespace, chrt, vals, opts)
34+
if args.Get(0) == nil {
35+
return nil, args.Error(1)
36+
}
37+
return args.Get(0).(*release.Release), args.Error(1)
38+
}
39+
40+
func (m *mockActionClient) Upgrade(name, namespace string, chrt *chart.Chart, vals map[string]interface{}, opts ...actionclient.UpgradeOption) (*release.Release, error) {
41+
args := m.Called(name, namespace, chrt, vals, opts)
42+
if args.Get(0) == nil {
43+
return nil, args.Error(1)
44+
}
45+
return args.Get(0).(*release.Release), args.Error(1)
46+
}
47+
48+
func (m *mockActionClient) Uninstall(name string, opts ...actionclient.UninstallOption) (*release.UninstallReleaseResponse, error) {
49+
args := m.Called(name, opts)
50+
if args.Get(0) == nil {
51+
return nil, args.Error(1)
52+
}
53+
return args.Get(0).(*release.UninstallReleaseResponse), args.Error(1)
54+
}
55+
56+
func (m *mockActionClient) Reconcile(rel *release.Release) error {
57+
args := m.Called(rel)
58+
return args.Error(0)
59+
}
60+
61+
var _ actionclient.ActionClientGetter = &mockActionClientGetter{}
62+
63+
type mockActionClientGetter struct {
64+
mock.Mock
65+
}
66+
67+
func (m *mockActionClientGetter) ActionClientFor(ctx context.Context, obj client.Object) (actionclient.ActionInterface, error) {
68+
args := m.Called(ctx, obj)
69+
if args.Get(0) == nil {
70+
return nil, args.Error(1)
71+
}
72+
return args.Get(0).(actionclient.ActionInterface), args.Error(1)
73+
}
74+
75+
func TestActionClientErrorTranslation(t *testing.T) {
76+
originalError := fmt.Errorf("some error")
77+
expectedErr := fmt.Errorf("something other error")
78+
errTranslator := func(originalErr error) error {
79+
return expectedErr
80+
}
81+
82+
ac := new(mockActionClient)
83+
ac.On("Get", mock.Anything, mock.Anything).Return(nil, originalError)
84+
ac.On("Install", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, originalError)
85+
ac.On("Uninstall", mock.Anything, mock.Anything).Return(nil, originalError)
86+
ac.On("Upgrade", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, originalError)
87+
ac.On("Reconcile", mock.Anything, mock.Anything).Return(originalError)
88+
89+
wrappedAc := NewWrappedActionClient(ac, errTranslator)
90+
91+
// Get
92+
_, err := wrappedAc.Get("something")
93+
assert.Equal(t, expectedErr, err, "expected Get() to return translated error")
94+
95+
// Install
96+
_, err = wrappedAc.Install("something", "somethingelse", nil, nil)
97+
assert.Equal(t, expectedErr, err, "expected Install() to return translated error")
98+
99+
// Uninstall
100+
_, err = wrappedAc.Uninstall("something")
101+
assert.Equal(t, expectedErr, err, "expected Uninstall() to return translated error")
102+
103+
// Upgrade
104+
_, err = wrappedAc.Upgrade("something", "somethingelse", nil, nil)
105+
assert.Equal(t, expectedErr, err, "expected Upgrade() to return translated error")
106+
107+
// Reconcile
108+
err = wrappedAc.Reconcile(nil)
109+
assert.Equal(t, expectedErr, err, "expected Reconcile() to return translated error")
110+
}
111+
112+
func TestActionClientFor(t *testing.T) {
113+
// Create a mock for the ActionClientGetter
114+
mockActionClientGetter := new(mockActionClientGetter)
115+
mockActionInterface := new(mockActionClient)
116+
testError := errors.New("test error")
117+
118+
// Set up expectations for the mock
119+
mockActionClientGetter.On("ActionClientFor", mock.Anything, mock.Anything).Return(mockActionInterface, nil).Once()
120+
mockActionClientGetter.On("ActionClientFor", mock.Anything, mock.Anything).Return(nil, testError).Once()
121+
122+
// Create an instance of ActionClientGetter with the mock
123+
acg := ActionClientGetter{
124+
ActionClientGetter: mockActionClientGetter,
125+
}
126+
127+
// Define a test context and object
128+
ctx := context.Background()
129+
var obj client.Object // Replace with an actual client.Object implementation
130+
131+
// Test the successful case
132+
actionClient, err := acg.ActionClientFor(ctx, obj)
133+
assert.NoError(t, err)
134+
assert.NotNil(t, actionClient)
135+
assert.IsType(t, &ActionClient{}, actionClient)
136+
137+
// Test the error case
138+
actionClient, err = acg.ActionClientFor(ctx, obj)
139+
assert.Error(t, err)
140+
assert.Nil(t, actionClient)
141+
}

0 commit comments

Comments
 (0)