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

SAP / sap-btp-service-operator / 26096664055

19 May 2026 12:18PM UTC coverage: 77.784% (+0.08%) from 77.708%
26096664055

Pull #635

github

kerenlahav
correlation id
Pull Request #635: Async retry with backoff

121 of 156 new or added lines in 5 files covered. (77.56%)

35 existing lines in 3 files now uncovered.

2906 of 3736 relevant lines covered (77.78%)

0.88 hits per line

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

49.76
/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

NEW
116
func HandleServiceManagerError(ctx context.Context, k8sClient client.Client, resource common.SAPBTPResource, operationType smClientTypes.OperationCategory, err error, updateStatus bool) (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

NEW
126
        if updateStatus {
×
NEW
127
                return HandleOperationFailure(ctx, k8sClient, resource, operationType, err)
×
NEW
128
        }
×
NEW
129
        return ctrl.Result{}, err
×
130
}
131

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

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

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

157
func IsMarkedForDeletion(object metav1.ObjectMeta) bool {
×
158
        return !object.DeletionTimestamp.IsZero()
×
159
}
×
160

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

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

205
        return nil
×
206
}
207

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

214
        delete(secret.Annotations, common.WatchSecretAnnotation+instanceUID)
×
215
        existInstanceAnnotation := false
×
216
        for key := range secret.Annotations {
×
217
                if strings.HasPrefix(key, common.WatchSecretAnnotation) {
×
218
                        existInstanceAnnotation = true
×
219
                        break
×
220
                }
221
        }
222
        if !existInstanceAnnotation {
×
223
                delete(secret.Labels, common.WatchSecretLabel)
×
224
                controllerutil.RemoveFinalizer(secret, common.FinalizerName)
×
225
        }
×
226
        return k8sClient.Update(ctx, secret)
×
227
}
228

229
func IsSecretWatched(secret client.Object) bool {
×
230
        return secret.GetLabels() != nil && secret.GetLabels()[common.WatchSecretLabel] == "true"
×
231
}
×
232

233
func GetLabelKeyForInstanceSecret(secretName string) string {
×
234
        return common.InstanceSecretRefLabel + secretName
×
235
}
×
236

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

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

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

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

262
        return ctrl.Result{}, err
×
263
}
264

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

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

285
        return ctrl.Result{}, smError
×
286
}
287

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