Skip to content

Commit 9725289

Browse files
kaovilaiclaude
andcommitted
Fix unnecessary secret updates and logging in STS flow
The operator was repeatedly logging "Secret already exists, updating" and "Following standardized STS workflow, secret created successfully" even when the secret content hadn't changed. This was happening because the CloudStorage controller calls STSStandardizedFlow() on every reconciliation, which always attempted to create the secret first, then caught the AlreadyExists error and performed an update. Changed the approach to: - First check if the secret exists - Compare existing data with desired data - Only update when there are actual differences - Skip updates and avoid logging when content is identical - Changed CloudStorage controller to use Debug level and more accurate message when STS secret is available (not necessarily created) This eliminates unnecessary API calls to the Kubernetes cluster and reduces noise in the operator logs. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent d9617c6 commit 9725289

File tree

3 files changed

+120
-42
lines changed

3 files changed

+120
-42
lines changed

internal/controller/cloudstorage_controller.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,8 @@ func (b CloudStorageReconciler) Reconcile(ctx context.Context, req ctrl.Request)
132132
return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
133133
}
134134
if secretName != "" {
135-
// Secret was created successfully by STSStandardizedFlow
136-
logger.Info(fmt.Sprintf("Following standardized STS workflow, secret %s created successfully", secretName))
135+
// Secret exists after STSStandardizedFlow (may have been created, updated, or unchanged)
136+
logger.V(1).Info(fmt.Sprintf("Following standardized STS workflow, secret %s is available", secretName))
137137
}
138138
// Now continue with bucket creation as secret exists and we are good to go !!!
139139
if ok, err = clnt.Exists(); !ok && err == nil {

pkg/credentials/stsflow/stsflow.go

Lines changed: 67 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ func CreateOrUpdateSTSSecretWithClients(setupLog logr.Logger, secretName string,
216216
func CreateOrUpdateSTSSecretWithClientsAndWait(setupLog logr.Logger, secretName string, credStringData map[string]string, secretNS string, clientInstance client.Client, clientset kubernetes.Interface, waitForSecret bool) error {
217217
// Create a secret with the appropriate credentials format for STS/WIF authentication
218218
// Secret format follows standard patterns used by cloud providers
219-
secret := corev1.Secret{
219+
desiredSecret := corev1.Secret{
220220
ObjectMeta: metav1.ObjectMeta{
221221
Name: secretName,
222222
Namespace: secretNS,
@@ -226,48 +226,88 @@ func CreateOrUpdateSTSSecretWithClientsAndWait(setupLog logr.Logger, secretName
226226
},
227227
StringData: credStringData,
228228
}
229+
230+
// First, try to get the existing secret
231+
existingSecret := corev1.Secret{}
232+
err := clientInstance.Get(context.Background(), types.NamespacedName{Name: secretName, Namespace: secretNS}, &existingSecret)
233+
229234
verb := "created"
230-
if err := clientInstance.Create(context.Background(), &secret); err != nil {
231-
if errors.IsAlreadyExists(err) {
232-
verb = "updated"
233-
setupLog.Info("Secret already exists, updating")
234-
fromCluster := corev1.Secret{}
235-
err = clientInstance.Get(context.Background(), types.NamespacedName{Name: secret.Name, Namespace: secret.Namespace}, &fromCluster)
236-
if err != nil {
237-
setupLog.Error(err, "unable to get existing secret resource")
235+
if err != nil {
236+
if errors.IsNotFound(err) {
237+
// Secret doesn't exist, create it
238+
if err := clientInstance.Create(context.Background(), &desiredSecret); err != nil {
239+
setupLog.Error(err, "unable to create secret resource")
238240
return err
239241
}
240-
// update StringData - preserve existing Data that's not being replaced
241-
// This is safe because STS credentials are only updated during install/reconfiguration,
242-
// and any BSL-specific patches (like region) should be preserved
243-
updatedFromCluster := fromCluster.DeepCopy()
242+
} else {
243+
// Some other error occurred while getting the secret
244+
setupLog.Error(err, "unable to get secret resource")
245+
return err
246+
}
247+
} else {
248+
// Secret exists, check if update is needed
249+
needsUpdate := false
250+
251+
// Check if labels need updating
252+
if existingSecret.Labels == nil || existingSecret.Labels["oadp.openshift.io/secret-type"] != "sts-credentials" {
253+
needsUpdate = true
254+
}
255+
256+
// Check if data needs updating
257+
// Convert existing Data to string for comparison
258+
existingData := make(map[string]string)
259+
for key, value := range existingSecret.Data {
260+
existingData[key] = string(value)
261+
}
262+
263+
// Compare each key in credStringData
264+
for key, desiredValue := range credStringData {
265+
if existingValue, exists := existingData[key]; !exists || existingValue != desiredValue {
266+
needsUpdate = true
267+
break
268+
}
269+
}
270+
271+
if needsUpdate {
272+
verb = "updated"
273+
setupLog.Info("Secret content differs, updating")
274+
275+
// Update the secret
276+
updatedSecret := existingSecret.DeepCopy()
277+
244278
// Initialize StringData if not present
245-
if updatedFromCluster.StringData == nil {
246-
updatedFromCluster.StringData = make(map[string]string)
279+
if updatedSecret.StringData == nil {
280+
updatedSecret.StringData = make(map[string]string)
247281
}
282+
248283
// Update only the new StringData fields, preserving existing Data
249-
for key, value := range secret.StringData {
250-
updatedFromCluster.StringData[key] = value
284+
for key, value := range credStringData {
285+
updatedSecret.StringData[key] = value
251286
}
287+
252288
// Ensure labels are set
253-
if updatedFromCluster.Labels == nil {
254-
updatedFromCluster.Labels = make(map[string]string)
289+
if updatedSecret.Labels == nil {
290+
updatedSecret.Labels = make(map[string]string)
255291
}
256-
updatedFromCluster.Labels["oadp.openshift.io/secret-type"] = "sts-credentials"
257-
if err := clientInstance.Patch(context.Background(), updatedFromCluster, client.MergeFrom(&fromCluster)); err != nil {
292+
updatedSecret.Labels["oadp.openshift.io/secret-type"] = "sts-credentials"
293+
294+
if err := clientInstance.Patch(context.Background(), updatedSecret, client.MergeFrom(&existingSecret)); err != nil {
258295
setupLog.Error(err, fmt.Sprintf("unable to update secret resource: %v", err))
259296
return err
260297
}
261298
} else {
262-
setupLog.Error(err, "unable to create secret resource")
263-
return err
299+
// No update needed
300+
verb = "unchanged"
264301
}
265302
}
266-
setupLog.Info("Secret " + secret.Name + " " + verb + " successfully")
267303

268-
if waitForSecret {
269-
// Wait for the Secret to be available
270-
setupLog.Info(fmt.Sprintf("Waiting for %s Secret to be available", secret.Name))
304+
if verb != "unchanged" {
305+
setupLog.Info("Secret " + desiredSecret.Name + " " + verb + " successfully")
306+
}
307+
308+
if waitForSecret && verb == "created" {
309+
// Wait for the Secret to be available (only needed for newly created secrets)
310+
setupLog.Info(fmt.Sprintf("Waiting for %s Secret to be available", desiredSecret.Name))
271311
_, err := WaitForSecret(clientset, secretNS, secretName)
272312
if err != nil {
273313
setupLog.Error(err, "error waiting for credentials Secret")

pkg/credentials/stsflow/stsflow_test.go

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -284,18 +284,11 @@ func TestCreateOrUpdateSTSSecret_ErrorScenarios(t *testing.T) {
284284
testSecretName := "test-secret"
285285
testLogger := zap.New(zap.UseDevMode(true))
286286

287-
t.Run("Get error during update", func(t *testing.T) {
288-
// Create a client that returns an error on Get
289-
fakeClient := &mockErrorClient{
290-
Client: fake.NewClientBuilder().
291-
WithRuntimeObjects(&corev1.Secret{
292-
ObjectMeta: metav1.ObjectMeta{
293-
Name: testSecretName,
294-
Namespace: testNamespace,
295-
},
296-
}).
297-
Build(),
298-
getError: true,
287+
t.Run("Get error (non-NotFound) during initial check", func(t *testing.T) {
288+
// Create a client that returns a non-NotFound error on Get
289+
// This simulates a real error (not just secret not existing)
290+
fakeClient := &mockErrorClientGenericGetError{
291+
Client: fake.NewClientBuilder().Build(),
299292
}
300293
fakeClientset := k8sfake.NewSimpleClientset()
301294

@@ -304,7 +297,7 @@ func TestCreateOrUpdateSTSSecret_ErrorScenarios(t *testing.T) {
304297
}, testNamespace, fakeClient, fakeClientset, false)
305298

306299
assert.Error(t, err)
307-
assert.Contains(t, err.Error(), "not found")
300+
assert.Contains(t, err.Error(), "unable to get secret resource")
308301
})
309302

310303
t.Run("Patch error during update", func(t *testing.T) {
@@ -329,6 +322,42 @@ func TestCreateOrUpdateSTSSecret_ErrorScenarios(t *testing.T) {
329322
assert.Error(t, err)
330323
assert.Contains(t, err.Error(), "patch error")
331324
})
325+
326+
t.Run("No update when content is identical", func(t *testing.T) {
327+
// Create a secret with the same data we'll try to update with
328+
existingSecret := &corev1.Secret{
329+
ObjectMeta: metav1.ObjectMeta{
330+
Name: testSecretName,
331+
Namespace: testNamespace,
332+
Labels: map[string]string{
333+
"oadp.openshift.io/secret-type": "sts-credentials",
334+
},
335+
},
336+
Data: map[string][]byte{
337+
"key": []byte("value"),
338+
},
339+
}
340+
fakeClient := fake.NewClientBuilder().
341+
WithRuntimeObjects(existingSecret).
342+
Build()
343+
fakeClientset := k8sfake.NewSimpleClientset()
344+
345+
// Try to update with the same content
346+
err := CreateOrUpdateSTSSecretWithClientsAndWait(testLogger, testSecretName, map[string]string{
347+
"key": "value",
348+
}, testNamespace, fakeClient, fakeClientset, false)
349+
350+
assert.NoError(t, err)
351+
// Verify the secret wasn't modified
352+
secretResult := &corev1.Secret{}
353+
err = fakeClient.Get(context.Background(), client.ObjectKey{
354+
Name: testSecretName,
355+
Namespace: testNamespace,
356+
}, secretResult)
357+
assert.NoError(t, err)
358+
// The Data should remain unchanged (no StringData should be set)
359+
assert.Equal(t, []byte("value"), secretResult.Data["key"])
360+
})
332361
}
333362

334363
// Mock client that can simulate errors
@@ -357,6 +386,15 @@ func (m *mockErrorClient) Patch(ctx context.Context, obj client.Object, patch cl
357386
return m.Client.Patch(ctx, obj, patch, opts...)
358387
}
359388

389+
// New mock client that returns a generic error on Get (not NotFound)
390+
type mockErrorClientGenericGetError struct {
391+
client.Client
392+
}
393+
394+
func (m *mockErrorClientGenericGetError) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
395+
return errors.NewServiceUnavailable("unable to get secret resource")
396+
}
397+
360398
func TestSTSStandardizedFlow(t *testing.T) {
361399
// Save original env values
362400
originalWatchNS := os.Getenv("WATCH_NAMESPACE")

0 commit comments

Comments
 (0)