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

SAP / sap-btp-service-operator / 22057017061

16 Feb 2026 09:23AM UTC coverage: 78.407% (+0.01%) from 78.394%
22057017061

Pull #610

github

kerenlahav
fix test and bump go
Pull Request #610: BUG fix - OOM crash when cluster contains a lot of secrets

45 of 64 new or added lines in 3 files covered. (70.31%)

7 existing lines in 3 files now uncovered.

2825 of 3603 relevant lines covered (78.41%)

0.88 hits per line

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

50.5
/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
        authv1 "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 *authv1.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)
×
NEW
212
        existInstanceAnnotation := false
×
NEW
213
        for key := range secret.Annotations {
×
NEW
214
                if strings.HasPrefix(key, common.WatchSecretAnnotation) {
×
NEW
215
                        existInstanceAnnotation = true
×
NEW
216
                        break
×
217
                }
218
        }
NEW
219
        if !existInstanceAnnotation {
×
NEW
220
                delete(secret.Labels, common.WatchSecretLabel)
×
221
                controllerutil.RemoveFinalizer(secret, common.FinalizerName)
×
222
        }
×
223
        return k8sClient.Update(ctx, secret)
×
224
}
225

NEW
226
func IsSecretWatched(secret client.Object) bool {
×
NEW
227
        return secret.GetLabels() != nil && secret.GetLabels()[common.WatchSecretLabel] == "true"
×
UNCOV
228
}
×
229

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

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

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

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

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

259
        return ctrl.Result{}, err
×
260
}
261

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

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

282
        return ctrl.Result{}, smError
×
283
}
284

285
func serialize(value interface{}) ([]byte, format, error) {
1✔
286
        if byteArrayVal, ok := value.([]byte); ok {
1✔
287
                return byteArrayVal, JSON, nil
×
288
        }
×
289
        if strVal, ok := value.(string); ok {
2✔
290
                return []byte(strVal), TEXT, nil
1✔
291
        }
1✔
292
        data, err := json.Marshal(value)
1✔
293
        if err != nil {
1✔
294
                return nil, UNKNOWN, err
×
295
        }
×
296
        return data, JSON, nil
1✔
297
}
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