• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

SAP / sap-btp-service-operator / 21958694827

12 Feb 2026 06:13PM UTC coverage: 78.515% (+0.1%) from 78.394%
21958694827

Pull #610

github

kerenlahav
lint
Pull Request #610: BUG fix - OOM crash when cluster contains a lot of secrets

25 of 29 new or added lines in 2 files covered. (86.21%)

4 existing lines in 1 file now uncovered.

2803 of 3570 relevant lines covered (78.52%)

0.88 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

51.0
/internal/utils/controller_util.go
1
package utils
2

3
import (
4
        "context"
5
        "encoding/json"
6
        "errors"
7
        "fmt"
8
        "net/http"
9
        "strings"
10
        "time"
11

12
        "github.com/SAP/sap-btp-service-operator/internal/utils/logutils"
13
        corev1 "k8s.io/api/core/v1"
14

15
        "github.com/SAP/sap-btp-service-operator/api/common"
16
        "github.com/SAP/sap-btp-service-operator/client/sm"
17
        smClientTypes "github.com/SAP/sap-btp-service-operator/client/sm/types"
18
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
19
        apimachinerytypes "k8s.io/apimachinery/pkg/types"
20
        ctrl "sigs.k8s.io/controller-runtime"
21
        "sigs.k8s.io/controller-runtime/pkg/client"
22
        "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
23

24
        "k8s.io/apimachinery/pkg/util/rand"
25

26
        v1 "k8s.io/api/authentication/v1"
27
)
28

29
const (
30
        TEXT    format = "text"
31
        JSON    format = "json"
32
        UNKNOWN format = "unknown"
33
)
34

35
type SecretMetadataProperty struct {
36
        Name      string `json:"name"`
37
        Container bool   `json:"container,omitempty"`
38
        Format    string `json:"format"`
39
}
40

41
type format string
42

43
func RemoveFinalizer(ctx context.Context, k8sClient client.Client, object client.Object, finalizerName string) error {
×
44
        log := logutils.GetLogger(ctx)
×
45
        if controllerutil.RemoveFinalizer(object, finalizerName) {
×
46
                log.Info(fmt.Sprintf("removing finalizer %s from resource %s named '%s' in namespace '%s'", finalizerName, object.GetObjectKind(), object.GetName(), object.GetNamespace()))
×
47
                return k8sClient.Update(ctx, object)
×
48
        }
×
49
        return nil
×
50
}
51

52
func UpdateStatus(ctx context.Context, k8sClient client.Client, object common.SAPBTPResource) error {
1✔
53
        log := logutils.GetLogger(ctx)
1✔
54
        log.Info(fmt.Sprintf("updating %s status", object.GetObjectKind().GroupVersionKind().Kind))
1✔
55
        object.SetObservedGeneration(getLastObservedGen(object))
1✔
56
        return k8sClient.Status().Update(ctx, object)
1✔
57
}
1✔
58

59
func NormalizeCredentials(credentialsJSON json.RawMessage) (map[string][]byte, []SecretMetadataProperty, error) {
1✔
60
        var credentialsMap map[string]interface{}
1✔
61
        err := json.Unmarshal(credentialsJSON, &credentialsMap)
1✔
62
        if err != nil {
1✔
63
                return nil, nil, err
×
64
        }
×
65

66
        normalized := make(map[string][]byte)
1✔
67
        metadata := make([]SecretMetadataProperty, 0)
1✔
68
        for propertyName, value := range credentialsMap {
2✔
69
                keyString := strings.Replace(propertyName, " ", "_", -1)
1✔
70
                normalizedValue, typpe, err := serialize(value)
1✔
71
                if err != nil {
1✔
72
                        return nil, nil, err
×
73
                }
×
74
                metadata = append(metadata, SecretMetadataProperty{
1✔
75
                        Name:   keyString,
1✔
76
                        Format: string(typpe),
1✔
77
                })
1✔
78
                normalized[keyString] = normalizedValue
1✔
79
        }
80
        return normalized, metadata, nil
1✔
81
}
82

83
func BuildUserInfo(ctx context.Context, userInfo *v1.UserInfo) string {
1✔
84
        log := logutils.GetLogger(ctx)
1✔
85
        if userInfo == nil {
2✔
86
                return ""
1✔
87
        }
1✔
88
        userInfoStr, err := json.Marshal(userInfo)
1✔
89
        if err != nil {
1✔
90
                log.Error(err, "failed to prepare user info")
×
91
                return ""
×
92
        }
×
93

94
        return string(userInfoStr)
1✔
95
}
96

97
func SliceContains(slice []string, i string) bool {
1✔
98
        for _, s := range slice {
2✔
99
                if s == i {
2✔
100
                        return true
1✔
101
                }
1✔
102
        }
103

104
        return false
1✔
105
}
106

107
func RandStringRunes(n int) string {
×
108
        var letterRunes = []rune("abcdefghijklmnopqrstuvwxyz1234567890")
×
109
        b := make([]rune, n)
×
110
        for i := range b {
×
111
                b[i] = letterRunes[rand.Intn(len(letterRunes))]
×
112
        }
×
113
        return string(b)
×
114
}
115

116
func HandleServiceManagerError(ctx context.Context, k8sClient client.Client, resource common.SAPBTPResource, operationType smClientTypes.OperationCategory, err error) (ctrl.Result, error) {
×
117
        log := logutils.GetLogger(ctx)
×
118
        var smError *sm.ServiceManagerError
×
119
        if ok := errors.As(err, &smError); ok {
×
120
                if smError.StatusCode == http.StatusTooManyRequests {
×
121
                        log.Info(fmt.Sprintf("SM returned 429 (%s), requeueing...", smError.Error()))
×
122
                        return handleRateLimitError(ctx, k8sClient, resource, operationType, smError)
×
123
                }
×
124
        }
125

126
        return HandleOperationFailure(ctx, k8sClient, resource, operationType, err)
×
127
}
128

129
func HandleCredRotationError(ctx context.Context, k8sClient client.Client, binding common.SAPBTPResource, err error) (ctrl.Result, error) {
×
130
        log := logutils.GetLogger(ctx)
×
131
        var smError *sm.ServiceManagerError
×
132
        if ok := errors.As(err, &smError); ok {
×
133
                if smError.StatusCode == http.StatusTooManyRequests {
×
134
                        log.Info(fmt.Sprintf("SM returned 429 (%s), requeueing...", smError.Error()))
×
135
                        return handleRateLimitError(ctx, k8sClient, binding, common.Unknown, smError)
×
136
                }
×
137
                log.Info(fmt.Sprintf("SM returned error: %s", smError.Error()))
×
138
        }
139

140
        log.Info("updating cred rotation condition with error", err)
×
141
        SetCredRotationInProgressConditions(common.CredPreparing, err.Error(), binding)
×
142
        return ctrl.Result{}, UpdateStatus(ctx, k8sClient, binding)
×
143
}
144

145
// ParseNamespacedName converts a "namespace/name" string to a types.NamespacedName object.
146
func ParseNamespacedName(input string) (apimachinerytypes.NamespacedName, error) {
1✔
147
        parts := strings.SplitN(input, "/", 2)
1✔
148
        if len(parts) != 2 {
2✔
149
                return apimachinerytypes.NamespacedName{}, fmt.Errorf("invalid format: expected 'namespace/name', got '%s'", input)
1✔
150
        }
1✔
151
        return apimachinerytypes.NamespacedName{Namespace: parts[0], Name: parts[1]}, nil
1✔
152
}
153

154
func IsMarkedForDeletion(object metav1.ObjectMeta) bool {
×
155
        return !object.DeletionTimestamp.IsZero()
×
156
}
×
157

158
func RemoveAnnotations(ctx context.Context, k8sClient client.Client, object common.SAPBTPResource, keys ...string) error {
1✔
159
        log := logutils.GetLogger(ctx)
1✔
160
        annotations := object.GetAnnotations()
1✔
161
        shouldUpdate := false
1✔
162
        if annotations != nil {
2✔
163
                for _, key := range keys {
2✔
164
                        if _, ok := annotations[key]; ok {
2✔
165
                                log.Info(fmt.Sprintf("deleting annotation with key %s", key))
1✔
166
                                delete(annotations, key)
1✔
167
                                shouldUpdate = true
1✔
168
                        }
1✔
169
                }
170
                if shouldUpdate {
2✔
171
                        object.SetAnnotations(annotations)
1✔
172
                        return k8sClient.Update(ctx, object)
1✔
173
                }
1✔
174
        }
175
        return nil
1✔
176
}
177

178
func AddWatchForSecretIfNeeded(ctx context.Context, k8sClient client.Client, secret *corev1.Secret, instanceUID string) error {
1✔
179
        log := logutils.GetLogger(ctx)
1✔
180
        updateRequired := false
1✔
181
        if secret.Annotations == nil {
2✔
182
                secret.Annotations = make(map[string]string)
1✔
183
        }
1✔
184
        if len(secret.Annotations[common.WatchSecretAnnotation+string(instanceUID)]) == 0 {
2✔
185
                log.Info(fmt.Sprintf("adding secret watch annotation for instance %s on secret %s", instanceUID, secret.Name))
1✔
186
                secret.Annotations[common.WatchSecretAnnotation+instanceUID] = "true"
1✔
187
                updateRequired = true
1✔
188
        }
1✔
189
        if secret.Labels == nil {
2✔
190
                secret.Labels = make(map[string]string)
1✔
191
        }
1✔
192
        if secret.Labels[common.WatchSecretLabel] != "true" {
2✔
193
                log.Info(fmt.Sprintf("adding watch label for secret %s", secret.Name))
1✔
194
                secret.Labels[common.WatchSecretLabel] = "true"
1✔
195
                controllerutil.AddFinalizer(secret, common.FinalizerName)
1✔
196
                updateRequired = true
1✔
197
        }
1✔
198
        if updateRequired {
2✔
199
                return k8sClient.Update(ctx, secret)
1✔
200
        }
1✔
201

202
        return nil
×
203
}
204

205
func RemoveWatchForSecret(ctx context.Context, k8sClient client.Client, secretKey apimachinerytypes.NamespacedName, instanceUID string) error {
×
206
        secret := &corev1.Secret{}
×
207
        if err := k8sClient.Get(ctx, secretKey, secret); err != nil {
×
208
                return client.IgnoreNotFound(err)
×
209
        }
×
210

211
        delete(secret.Annotations, common.WatchSecretAnnotation+instanceUID)
×
212
        if !IsSecretWatched(secret.Annotations) {
×
NEW
213
                delete(secret.Labels, common.WatchSecretLabel)
×
214
                controllerutil.RemoveFinalizer(secret, common.FinalizerName)
×
215
        }
×
216
        return k8sClient.Update(ctx, secret)
×
217
}
218

219
func IsSecretWatched(secretAnnotations map[string]string) bool {
×
220
        for key := range secretAnnotations {
×
221
                if strings.HasPrefix(key, common.WatchSecretAnnotation) {
×
222
                        return true
×
223
                }
×
224
        }
225
        return false
×
226
}
227

228
func GetLabelKeyForInstanceSecret(secretName string) string {
×
229
        return common.InstanceSecretRefLabel + secretName
×
230
}
×
231

232
func HandleInstanceSharingError(ctx context.Context, k8sClient client.Client, object common.SAPBTPResource, status metav1.ConditionStatus, reason string, err error) (ctrl.Result, error) {
×
233
        log := logutils.GetLogger(ctx)
×
234

×
235
        errMsg := err.Error()
×
236
        if smError, ok := err.(*sm.ServiceManagerError); ok {
×
237
                log.Info(fmt.Sprintf("SM returned error with status code %d", smError.StatusCode))
×
238
                errMsg = smError.Error()
×
239

×
240
                if smError.StatusCode == http.StatusTooManyRequests {
×
241
                        return handleRateLimitError(ctx, k8sClient, object, common.Unknown, smError)
×
242
                } else if reason == common.ShareFailed &&
×
243
                        (smError.StatusCode == http.StatusBadRequest || smError.StatusCode == http.StatusInternalServerError) {
×
244
                        /* non-transient error may occur only when sharing
×
245
                           SM return 400 when plan is not sharable
×
246
                           SM returns 500 when TOGGLES_ENABLE_INSTANCE_SHARE_FROM_OPERATOR feature toggle is off */
×
247
                        reason = common.ShareNotSupported
×
248
                }
×
249
        }
250

251
        SetSharedCondition(object, status, reason, errMsg)
×
252
        if updateErr := UpdateStatus(ctx, k8sClient, object); updateErr != nil {
×
253
                log.Error(updateErr, "failed to update instance status")
×
254
                return ctrl.Result{}, updateErr
×
255
        }
×
256

257
        return ctrl.Result{}, err
×
258
}
259

260
func handleRateLimitError(ctx context.Context, sClient client.Client, resource common.SAPBTPResource, operationType smClientTypes.OperationCategory, smError *sm.ServiceManagerError) (ctrl.Result, error) {
1✔
261
        log := logutils.GetLogger(ctx)
1✔
262
        SetInProgressConditions(ctx, operationType, "", resource, false)
1✔
263
        if updateErr := UpdateStatus(ctx, sClient, resource); updateErr != nil {
1✔
264
                log.Info("failed to update status after rate limit error")
×
265
                return ctrl.Result{}, updateErr
×
266
        }
×
267

268
        retryAfterStr := smError.ResponseHeaders.Get("Retry-After")
1✔
269
        if len(retryAfterStr) > 0 {
2✔
270
                log.Info(fmt.Sprintf("SM returned 429 with Retry-After: %s, requeueing after it...", retryAfterStr))
1✔
271
                if retryAfter, err := time.Parse(time.DateTime, retryAfterStr[:len(time.DateTime)]); err != nil { // format 2024-11-11 14:59:33 +0000 UTC
1✔
272
                        log.Error(err, "failed to parse Retry-After header, using default requeue time")
×
273
                } else {
1✔
274
                        timeToRequeue := time.Until(retryAfter)
1✔
275
                        log.Info(fmt.Sprintf("requeueing after %d minutes, %d seconds", int(timeToRequeue.Minutes()), int(timeToRequeue.Seconds())%60))
1✔
276
                        return ctrl.Result{RequeueAfter: timeToRequeue}, nil
1✔
277
                }
1✔
278
        }
279

280
        return ctrl.Result{}, smError
×
281
}
282

283
func serialize(value interface{}) ([]byte, format, error) {
1✔
284
        if byteArrayVal, ok := value.([]byte); ok {
1✔
285
                return byteArrayVal, JSON, nil
×
286
        }
×
287
        if strVal, ok := value.(string); ok {
2✔
288
                return []byte(strVal), TEXT, nil
1✔
289
        }
1✔
290
        data, err := json.Marshal(value)
1✔
291
        if err != nil {
1✔
292
                return nil, UNKNOWN, err
×
293
        }
×
294
        return data, JSON, nil
1✔
295
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc