Skip to content

Commit 5970407

Browse files
committed
Preserve ServiceAccount annotations during CSV update
Fixes #3607 When a ClusterServiceVersion (CSV) is updated, the EnsureServiceAccount function now preserves existing annotations on the ServiceAccount. This prevents the loss of important annotations that may have been added by users or other controllers. The fix merges existing annotations from the old ServiceAccount with annotations from the new ServiceAccount object, giving precedence to the new annotations in case of conflicts. Signed-off-by: Tiger Kaovilai <[email protected]> Add unit tests for ServiceAccount annotation preservation Added comprehensive unit tests for the EnsureServiceAccount function to verify that annotations are properly preserved during updates. The tests cover: - Creating new service accounts - Preserving existing annotations during updates - Handling annotation conflicts (new annotations take precedence) - Preserving secrets during updates - Error handling scenarios Signed-off-by: Tiger Kaovilai <[email protected]>
1 parent 3c2163e commit 5970407

File tree

2 files changed

+247
-1
lines changed

2 files changed

+247
-1
lines changed

pkg/controller/operators/catalog/step_ensurer.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ func (o *StepEnsurer) EnsureServiceAccount(namespace string, sa *corev1.ServiceA
151151
return
152152
}
153153

154-
// Carrying secrets through the service account update.
154+
// Carrying secrets and annotations through the service account update.
155155
preSa, getErr := o.kubeClient.KubernetesInterface().CoreV1().ServiceAccounts(namespace).Get(context.TODO(),
156156
sa.Name,
157157
metav1.GetOptions{})
@@ -162,6 +162,16 @@ func (o *StepEnsurer) EnsureServiceAccount(namespace string, sa *corev1.ServiceA
162162
sa.Secrets = preSa.Secrets
163163
sa.OwnerReferences = mergedOwnerReferences(preSa.OwnerReferences, sa.OwnerReferences)
164164

165+
// Merge annotations, giving precedence to the new ones.
166+
if sa.Annotations == nil {
167+
sa.Annotations = make(map[string]string)
168+
}
169+
for k, v := range preSa.Annotations {
170+
if _, ok := sa.Annotations[k]; !ok {
171+
sa.Annotations[k] = v
172+
}
173+
}
174+
165175
sa.SetNamespace(namespace)
166176

167177
// Use DeepDerivative to check if new SA is the same as the old SA. If no field is changed, we skip the update call.

pkg/controller/operators/catalog/step_ensurer_test.go

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,21 @@ package catalog
33
import (
44
"testing"
55

6+
"github.com/golang/mock/gomock"
67
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
79

10+
corev1 "k8s.io/api/core/v1"
11+
apierrors "k8s.io/apimachinery/pkg/api/errors"
812
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13+
"k8s.io/apimachinery/pkg/runtime"
14+
k8sfake "k8s.io/client-go/kubernetes/fake"
15+
fakedynamic "k8s.io/client-go/dynamic/fake"
16+
k8stesting "k8s.io/client-go/testing"
17+
18+
"github.com/operator-framework/api/pkg/operators/v1alpha1"
19+
"github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned/fake"
20+
"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient/operatorclientmocks"
921
)
1022

1123
func TestMergedOwnerReferences(t *testing.T) {
@@ -188,3 +200,227 @@ func TestMergedOwnerReferences(t *testing.T) {
188200
})
189201
}
190202
}
203+
204+
func TestEnsureServiceAccount(t *testing.T) {
205+
namespace := "test-namespace"
206+
saName := "test-sa"
207+
208+
tests := []struct {
209+
name string
210+
existingServiceAccount *corev1.ServiceAccount
211+
newServiceAccount *corev1.ServiceAccount
212+
expectedAnnotations map[string]string
213+
expectedStatus v1alpha1.StepStatus
214+
expectError bool
215+
createError error
216+
getError error
217+
updateError error
218+
}{
219+
{
220+
name: "create new service account",
221+
newServiceAccount: &corev1.ServiceAccount{
222+
ObjectMeta: metav1.ObjectMeta{
223+
Name: saName,
224+
Namespace: namespace,
225+
Annotations: map[string]string{
226+
"new-annotation": "new-value",
227+
},
228+
},
229+
},
230+
expectedAnnotations: map[string]string{
231+
"new-annotation": "new-value",
232+
},
233+
expectedStatus: v1alpha1.StepStatusCreated,
234+
},
235+
{
236+
name: "update existing service account - preserve existing annotations",
237+
existingServiceAccount: &corev1.ServiceAccount{
238+
ObjectMeta: metav1.ObjectMeta{
239+
Name: saName,
240+
Namespace: namespace,
241+
Annotations: map[string]string{
242+
"existing-annotation": "existing-value",
243+
"override-annotation": "old-value",
244+
},
245+
},
246+
Secrets: []corev1.ObjectReference{
247+
{Name: "existing-secret"},
248+
},
249+
},
250+
newServiceAccount: &corev1.ServiceAccount{
251+
ObjectMeta: metav1.ObjectMeta{
252+
Name: saName,
253+
Namespace: namespace,
254+
Annotations: map[string]string{
255+
"new-annotation": "new-value",
256+
"override-annotation": "new-value",
257+
},
258+
},
259+
},
260+
expectedAnnotations: map[string]string{
261+
"existing-annotation": "existing-value",
262+
"new-annotation": "new-value",
263+
"override-annotation": "new-value",
264+
},
265+
expectedStatus: v1alpha1.StepStatusPresent,
266+
createError: apierrors.NewAlreadyExists(corev1.Resource("serviceaccounts"), saName),
267+
},
268+
{
269+
name: "update existing service account - no annotations on new SA",
270+
existingServiceAccount: &corev1.ServiceAccount{
271+
ObjectMeta: metav1.ObjectMeta{
272+
Name: saName,
273+
Namespace: namespace,
274+
Annotations: map[string]string{
275+
"existing-annotation": "existing-value",
276+
},
277+
},
278+
},
279+
newServiceAccount: &corev1.ServiceAccount{
280+
ObjectMeta: metav1.ObjectMeta{
281+
Name: saName,
282+
Namespace: namespace,
283+
},
284+
},
285+
expectedAnnotations: map[string]string{
286+
"existing-annotation": "existing-value",
287+
},
288+
expectedStatus: v1alpha1.StepStatusPresent,
289+
createError: apierrors.NewAlreadyExists(corev1.Resource("serviceaccounts"), saName),
290+
},
291+
{
292+
name: "update existing service account - preserve secrets",
293+
existingServiceAccount: &corev1.ServiceAccount{
294+
ObjectMeta: metav1.ObjectMeta{
295+
Name: saName,
296+
Namespace: namespace,
297+
},
298+
Secrets: []corev1.ObjectReference{
299+
{Name: "secret-1"},
300+
{Name: "secret-2"},
301+
},
302+
},
303+
newServiceAccount: &corev1.ServiceAccount{
304+
ObjectMeta: metav1.ObjectMeta{
305+
Name: saName,
306+
Namespace: namespace,
307+
},
308+
},
309+
expectedAnnotations: map[string]string{},
310+
expectedStatus: v1alpha1.StepStatusPresent,
311+
createError: apierrors.NewAlreadyExists(corev1.Resource("serviceaccounts"), saName),
312+
},
313+
{
314+
name: "create error - not already exists",
315+
newServiceAccount: &corev1.ServiceAccount{
316+
ObjectMeta: metav1.ObjectMeta{
317+
Name: saName,
318+
Namespace: namespace,
319+
},
320+
},
321+
createError: apierrors.NewInternalError(assert.AnError),
322+
expectError: true,
323+
},
324+
{
325+
name: "update error - get existing fails",
326+
existingServiceAccount: &corev1.ServiceAccount{
327+
ObjectMeta: metav1.ObjectMeta{
328+
Name: saName,
329+
Namespace: namespace,
330+
},
331+
},
332+
newServiceAccount: &corev1.ServiceAccount{
333+
ObjectMeta: metav1.ObjectMeta{
334+
Name: saName,
335+
Namespace: namespace,
336+
},
337+
},
338+
createError: apierrors.NewAlreadyExists(corev1.Resource("serviceaccounts"), saName),
339+
getError: apierrors.NewInternalError(assert.AnError),
340+
expectError: true,
341+
},
342+
}
343+
344+
for _, tc := range tests {
345+
t.Run(tc.name, func(t *testing.T) {
346+
ctrl := gomock.NewController(t)
347+
defer ctrl.Finish()
348+
349+
// Create mock client
350+
mockClient := operatorclientmocks.NewMockClientInterface(ctrl)
351+
352+
// Create fake kubernetes client
353+
var objects []runtime.Object
354+
if tc.existingServiceAccount != nil {
355+
objects = append(objects, tc.existingServiceAccount)
356+
}
357+
358+
fakeClient := k8sfake.NewSimpleClientset(objects...)
359+
360+
// Setup expectations
361+
mockClient.EXPECT().KubernetesInterface().Return(fakeClient).AnyTimes()
362+
363+
// Mock the create call
364+
if tc.createError != nil {
365+
// We need to intercept the create call and return the error
366+
fakeClient.PrependReactor("create", "serviceaccounts", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
367+
return true, nil, tc.createError
368+
})
369+
}
370+
371+
// Mock the get call if needed
372+
if tc.getError != nil {
373+
fakeClient.PrependReactor("get", "serviceaccounts", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
374+
return true, nil, tc.getError
375+
})
376+
}
377+
378+
// Mock UpdateServiceAccount if the test expects an update
379+
if tc.createError != nil && apierrors.IsAlreadyExists(tc.createError) && tc.getError == nil {
380+
// Calculate expected SA after merge
381+
expectedSA := tc.newServiceAccount.DeepCopy()
382+
if tc.existingServiceAccount != nil {
383+
expectedSA.Secrets = tc.existingServiceAccount.Secrets
384+
// Merge annotations
385+
if expectedSA.Annotations == nil {
386+
expectedSA.Annotations = make(map[string]string)
387+
}
388+
for k, v := range tc.existingServiceAccount.Annotations {
389+
if _, ok := expectedSA.Annotations[k]; !ok {
390+
expectedSA.Annotations[k] = v
391+
}
392+
}
393+
}
394+
expectedSA.SetNamespace(namespace)
395+
396+
mockClient.EXPECT().UpdateServiceAccount(gomock.Any()).DoAndReturn(func(sa *corev1.ServiceAccount) (*corev1.ServiceAccount, error) {
397+
// Verify the merged service account has the expected annotations
398+
assert.Equal(t, tc.expectedAnnotations, sa.Annotations)
399+
// Verify secrets were preserved if they existed
400+
if tc.existingServiceAccount != nil && len(tc.existingServiceAccount.Secrets) > 0 {
401+
assert.Equal(t, tc.existingServiceAccount.Secrets, sa.Secrets)
402+
}
403+
return sa, tc.updateError
404+
}).MaxTimes(1)
405+
}
406+
407+
// Create StepEnsurer
408+
ensurer := &StepEnsurer{
409+
kubeClient: mockClient,
410+
crClient: fake.NewSimpleClientset(),
411+
dynamicClient: fakedynamic.NewSimpleDynamicClient(runtime.NewScheme()),
412+
}
413+
414+
// Execute EnsureServiceAccount
415+
status, err := ensurer.EnsureServiceAccount(namespace, tc.newServiceAccount)
416+
417+
// Verify results
418+
if tc.expectError {
419+
require.Error(t, err)
420+
} else {
421+
require.NoError(t, err)
422+
assert.Equal(t, tc.expectedStatus, status)
423+
}
424+
})
425+
}
426+
}

0 commit comments

Comments
 (0)