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

SAP / sap-btp-service-operator / 20265349919

16 Dec 2025 10:51AM UTC coverage: 78.323% (-0.2%) from 78.477%
20265349919

Pull #587

github

kerenlahav
Merge remote-tracking branch 'origin/customcert' into customcert
Pull Request #587: Add custom CA certificate support

36 of 48 new or added lines in 8 files covered. (75.0%)

2 existing lines in 1 file now uncovered.

2728 of 3483 relevant lines covered (78.32%)

0.88 hits per line

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

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

UNCOV
43
func RemoveFinalizer(ctx context.Context, k8sClient client.Client, object client.Object, finalizerName string) error {
×
NEW
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

UNCOV
116
func HandleServiceManagerError(ctx context.Context, k8sClient client.Client, resource common.SAPBTPResource, operationType smClientTypes.OperationCategory, err error) (ctrl.Result, error) {
×
NEW
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) {
×
NEW
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
        if secret.Annotations == nil {
2✔
181
                secret.Annotations = make(map[string]string)
1✔
182
        }
1✔
183
        if len(secret.Annotations[common.WatchSecretAnnotation+string(instanceUID)]) == 0 {
2✔
184
                log.Info(fmt.Sprintf("adding secret watch for secret %s", secret.Name))
1✔
185
                secret.Annotations[common.WatchSecretAnnotation+instanceUID] = "true"
1✔
186
                controllerutil.AddFinalizer(secret, common.FinalizerName)
1✔
187
                return k8sClient.Update(ctx, secret)
1✔
188
        }
1✔
189

190
        return nil
×
191
}
192

193
func RemoveWatchForSecret(ctx context.Context, k8sClient client.Client, secretKey apimachinerytypes.NamespacedName, instanceUID string) error {
×
194
        secret := &corev1.Secret{}
×
195
        if err := k8sClient.Get(ctx, secretKey, secret); err != nil {
×
196
                return client.IgnoreNotFound(err)
×
197
        }
×
198

199
        delete(secret.Annotations, common.WatchSecretAnnotation+instanceUID)
×
200
        if !IsSecretWatched(secret.Annotations) {
×
201
                controllerutil.RemoveFinalizer(secret, common.FinalizerName)
×
202
        }
×
203
        return k8sClient.Update(ctx, secret)
×
204
}
205

206
func IsSecretWatched(secretAnnotations map[string]string) bool {
×
207
        for key := range secretAnnotations {
×
208
                if strings.HasPrefix(key, common.WatchSecretAnnotation) {
×
209
                        return true
×
210
                }
×
211
        }
212
        return false
×
213
}
214

215
func GetLabelKeyForInstanceSecret(secretName string) string {
×
216
        return common.InstanceSecretRefLabel + secretName
×
217
}
×
218

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

×
222
        errMsg := err.Error()
×
223
        if smError, ok := err.(*sm.ServiceManagerError); ok {
×
224
                log.Info(fmt.Sprintf("SM returned error with status code %d", smError.StatusCode))
×
225
                errMsg = smError.Error()
×
226

×
227
                if smError.StatusCode == http.StatusTooManyRequests {
×
228
                        return handleRateLimitError(ctx, k8sClient, object, common.Unknown, smError)
×
229
                } else if reason == common.ShareFailed &&
×
230
                        (smError.StatusCode == http.StatusBadRequest || smError.StatusCode == http.StatusInternalServerError) {
×
231
                        /* non-transient error may occur only when sharing
×
232
                           SM return 400 when plan is not sharable
×
233
                           SM returns 500 when TOGGLES_ENABLE_INSTANCE_SHARE_FROM_OPERATOR feature toggle is off */
×
234
                        reason = common.ShareNotSupported
×
235
                }
×
236
        }
237

238
        SetSharedCondition(object, status, reason, errMsg)
×
239
        if updateErr := UpdateStatus(ctx, k8sClient, object); updateErr != nil {
×
240
                log.Error(updateErr, "failed to update instance status")
×
241
                return ctrl.Result{}, updateErr
×
242
        }
×
243

244
        return ctrl.Result{}, err
×
245
}
246

247
func handleRateLimitError(ctx context.Context, sClient client.Client, resource common.SAPBTPResource, operationType smClientTypes.OperationCategory, smError *sm.ServiceManagerError) (ctrl.Result, error) {
1✔
248
        log := logutils.GetLogger(ctx)
1✔
249
        SetInProgressConditions(ctx, operationType, "", resource, false)
1✔
250
        if updateErr := UpdateStatus(ctx, sClient, resource); updateErr != nil {
1✔
251
                log.Info("failed to update status after rate limit error")
×
252
                return ctrl.Result{}, updateErr
×
253
        }
×
254

255
        retryAfterStr := smError.ResponseHeaders.Get("Retry-After")
1✔
256
        if len(retryAfterStr) > 0 {
2✔
257
                log.Info(fmt.Sprintf("SM returned 429 with Retry-After: %s, requeueing after it...", retryAfterStr))
1✔
258
                retryAfter, err := time.Parse(time.DateTime, retryAfterStr[:len(time.DateTime)]) // format 2024-11-11 14:59:33 +0000 UTC
1✔
259
                if err != nil {
1✔
260
                        log.Error(err, "failed to parse Retry-After header, using default requeue time")
×
261
                } else {
1✔
262
                        timeToRequeue := time.Until(retryAfter)
1✔
263
                        log.Info(fmt.Sprintf("requeueing after %d minutes, %d seconds", int(timeToRequeue.Minutes()), int(timeToRequeue.Seconds())%60))
1✔
264
                        return ctrl.Result{RequeueAfter: timeToRequeue}, nil
1✔
265
                }
1✔
266
        }
267

268
        return ctrl.Result{Requeue: true}, nil
×
269
}
270

271
func serialize(value interface{}) ([]byte, format, error) {
1✔
272
        if byteArrayVal, ok := value.([]byte); ok {
1✔
273
                return byteArrayVal, JSON, nil
×
274
        }
×
275
        if strVal, ok := value.(string); ok {
2✔
276
                return []byte(strVal), TEXT, nil
1✔
277
        }
1✔
278
        data, err := json.Marshal(value)
1✔
279
        if err != nil {
1✔
280
                return nil, UNKNOWN, err
×
281
        }
×
282
        return data, JSON, nil
1✔
283
}
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