• 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

82.69
/controllers/servicebinding_controller.go
1
/*
2

3

4
Licensed under the Apache License, Version 2.0 (the "License");
5
you may not use this file except in compliance with the License.
6
You may obtain a copy of the License at
7

8
    http://www.apache.org/licenses/LICENSE-2.0
9

10
Unless required by applicable law or agreed to in writing, software
11
distributed under the License is distributed on an "AS IS" BASIS,
12
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
See the License for the specific language governing permissions and
14
limitations under the License.
15
*/
16

17
package controllers
18

19
import (
20
        "context"
21
        "encoding/json"
22
        "net/http"
23
        "strings"
24
        "time"
25

26
        commonutils "github.com/SAP/sap-btp-service-operator/api/common/utils"
27
        "github.com/SAP/sap-btp-service-operator/internal/utils/logutils"
28
        "sigs.k8s.io/controller-runtime/pkg/reconcile"
29

30
        "github.com/pkg/errors"
31

32
        "github.com/SAP/sap-btp-service-operator/api/common"
33
        "github.com/SAP/sap-btp-service-operator/internal/config"
34
        "github.com/SAP/sap-btp-service-operator/internal/utils"
35
        "github.com/go-logr/logr"
36
        "k8s.io/apimachinery/pkg/runtime"
37
        "k8s.io/client-go/tools/events"
38

39
        "fmt"
40

41
        "k8s.io/client-go/util/workqueue"
42
        "sigs.k8s.io/controller-runtime/pkg/controller"
43

44
        v1 "github.com/SAP/sap-btp-service-operator/api/v1"
45

46
        "k8s.io/apimachinery/pkg/api/meta"
47
        "k8s.io/apimachinery/pkg/runtime/schema"
48

49
        "github.com/google/uuid"
50

51
        "github.com/SAP/sap-btp-service-operator/client/sm"
52

53
        smClientTypes "github.com/SAP/sap-btp-service-operator/client/sm/types"
54

55
        corev1 "k8s.io/api/core/v1"
56
        apierrors "k8s.io/apimachinery/pkg/api/errors"
57
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
58
        "k8s.io/apimachinery/pkg/types"
59
        ctrl "sigs.k8s.io/controller-runtime"
60
        "sigs.k8s.io/controller-runtime/pkg/client"
61
        "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
62
)
63

64
const (
65
        secretNameTakenErrorFormat    = "the specified secret name '%s' is already taken. Choose another name and try again"
66
        secretAlreadyOwnedErrorFormat = "secret %s belongs to another binding %s, choose a different name"
67
)
68

69
// ServiceBindingReconciler reconciles a ServiceBinding object
70
type ServiceBindingReconciler struct {
71
        client.Client
72
        Log         logr.Logger
73
        Scheme      *runtime.Scheme
74
        GetSMClient func(ctx context.Context, instance *v1.ServiceInstance) (sm.Client, error)
75
        Config      config.Config
76
        Recorder    events.EventRecorder
77
        Retries     *utils.RetryStore
78
}
79

80
// +kubebuilder:rbac:groups=services.cloud.sap.com,resources=servicebindings,verbs=get;list;watch;create;update;patch;delete
81
// +kubebuilder:rbac:groups=services.cloud.sap.com,resources=servicebindings/status,verbs=get;update;patch
82
// +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch;create;update;patch;delete
83
// +kubebuilder:rbac:groups=core,resources=events,verbs=get;list;watch;create;update;patch;delete
84
// +kubebuilder:rbac:groups=coordination.k8s.io,resources=leases,verbs=get;list;create;update
85

86
func (r *ServiceBindingReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
1✔
87
        correlationID := uuid.New().String()
1✔
88
        retry := r.Retries.Get(req.NamespacedName)
1✔
89
        if retry != nil {
2✔
90
                correlationID = retry.CorrelationID
1✔
91
        }
1✔
92
        log := r.Log.WithValues("servicebinding", req.NamespacedName).WithValues("correlation_id", correlationID, req.Name, req.Namespace)
1✔
93
        if retry != nil && time.Now().Before(retry.NextRetry) {
2✔
94
                remaining := time.Until(retry.NextRetry)
1✔
95
                log.Info(fmt.Sprintf("skipping binding reconcile due to backoff. attempts=%d retryIn=%s", retry.Attempts, remaining))
1✔
96
                return ctrl.Result{RequeueAfter: remaining}, nil
1✔
97
        }
1✔
98
        ctx = context.WithValue(ctx, logutils.LogKey, log)
1✔
99
        ctx = context.WithValue(ctx, logutils.CorrelationIDKey, correlationID)
1✔
100

1✔
101
        serviceBinding := &v1.ServiceBinding{}
1✔
102
        if err := r.Client.Get(ctx, req.NamespacedName, serviceBinding); err != nil {
2✔
103
                if !apierrors.IsNotFound(err) {
1✔
104
                        log.Error(err, "unable to fetch ServiceBinding")
×
105
                }
×
106
                return ctrl.Result{}, client.IgnoreNotFound(err)
1✔
107
        }
108

109
        log.Info(fmt.Sprintf("*** staring reconcile of ServiceBinding %s/%s ***", serviceBinding.Namespace, serviceBinding.Name))
1✔
110

1✔
111
        serviceBinding = serviceBinding.DeepCopy()
1✔
112
        log.Info(fmt.Sprintf("Current generation is %v and observed is %v", serviceBinding.Generation, common.GetObservedGeneration(serviceBinding)))
1✔
113

1✔
114
        if len(serviceBinding.GetConditions()) == 0 {
2✔
115
                if err := utils.InitConditions(ctx, r.Client, serviceBinding); err != nil {
1✔
116
                        return ctrl.Result{}, err
×
117
                }
×
118
        }
119

120
        serviceInstance, instanceErr := r.getServiceInstanceForBinding(ctx, serviceBinding)
1✔
121
        if instanceErr != nil {
2✔
122
                if !apierrors.IsNotFound(instanceErr) {
1✔
123
                        log.Error(instanceErr, "failed to get service instance for binding")
×
124
                        return ctrl.Result{}, instanceErr
×
125
                }
×
126
                if !utils.IsMarkedForDeletion(serviceBinding.ObjectMeta) {
2✔
127
                        //instance is not found and binding is not marked for deletion
1✔
128
                        return r.handleInstanceForBindingNotFound(ctx, serviceBinding)
1✔
129
                }
1✔
130
                if len(serviceBinding.Status.BindingID) == 0 {
2✔
131
                        log.Info("service instance not found, binding is marked for deletion and has no binding id, removing finalizer if exists")
1✔
132
                        if err := r.deleteBindingSecret(ctx, serviceBinding); err != nil {
1✔
133
                                return ctrl.Result{}, err
×
134
                        }
×
135
                        return ctrl.Result{}, utils.RemoveFinalizer(ctx, r.Client, serviceBinding, common.FinalizerName)
1✔
136
                }
137
        }
138

139
        smClient, err := r.GetSMClient(ctx, serviceInstance)
1✔
140
        if err != nil {
1✔
141
                return utils.HandleOperationFailure(ctx, r.Client, serviceBinding, common.Unknown, err)
×
142
        }
×
143

144
        // poll only if delete sm operation is in progress or there is create/update ongoing operation and instance is not marked for deletion
145
        // if marked for deletion we should trigger the sm delete and ignore the current operation url
146
        if len(serviceBinding.Status.OperationURL) > 0 &&
1✔
147
                (serviceBinding.Status.OperationType == smClientTypes.DELETE || !utils.IsMarkedForDeletion(serviceBinding.ObjectMeta)) {
2✔
148
                return r.poll(ctx, smClient, serviceBinding)
1✔
149
        }
1✔
150

151
        if utils.IsMarkedForDeletion(serviceBinding.ObjectMeta) {
2✔
152
                return r.delete(ctx, smClient, serviceBinding)
1✔
153
        }
1✔
154

155
        if len(serviceBinding.Status.BindingID) > 0 {
2✔
156
                if bindingExist, err := isBindingExistInSM(smClient, serviceInstance, serviceBinding.Status.BindingID, log); err != nil {
1✔
157
                        log.Error(err, "failed to check if binding exist in sm due to unknown error")
×
NEW
158
                        return utils.HandleServiceManagerError(ctx, r.Client, serviceBinding, common.Unknown, err, false)
×
159
                } else if !bindingExist {
2✔
160
                        log.Info("binding not found in SM for this operator, updating status")
1✔
161
                        condition := metav1.Condition{
1✔
162
                                Type:               common.ConditionReady,
1✔
163
                                Status:             metav1.ConditionFalse,
1✔
164
                                ObservedGeneration: serviceBinding.Generation,
1✔
165
                                LastTransitionTime: metav1.NewTime(time.Now()),
1✔
166
                                Reason:             common.ResourceNotFound,
1✔
167
                                Message:            fmt.Sprintf(common.ResourceNotFoundMessageFormat, "binding", serviceBinding.Status.BindingID),
1✔
168
                        }
1✔
169
                        serviceBinding.Status.Conditions = []metav1.Condition{condition}
1✔
170
                        serviceBinding.Status.Ready = metav1.ConditionFalse
1✔
171
                        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceBinding)
1✔
172
                }
1✔
173
        }
174

175
        if controllerutil.AddFinalizer(serviceBinding, common.FinalizerName) {
2✔
176
                log.Info(fmt.Sprintf("added finalizer '%s' to service binding", common.FinalizerName))
1✔
177
                if err := r.Client.Update(ctx, serviceBinding); err != nil {
1✔
178
                        return ctrl.Result{}, err
×
179
                }
×
180
        }
181

182
        if utils.IsMarkedForDeletion(serviceInstance.ObjectMeta) {
2✔
183
                log.Info(fmt.Sprintf("service instance name: %s namespace: %s is marked for deletion, unable to create binding", serviceInstance.Name, serviceInstance.Namespace))
1✔
184
                utils.SetBlockedCondition(ctx, "instance is in deletion process", serviceBinding)
1✔
185
                return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceBinding)
1✔
186
        }
1✔
187

188
        if !serviceInstanceReady(serviceInstance) {
2✔
189
                log.Info(fmt.Sprintf("service instance name: %s namespace: %s is not ready, unable to create binding", serviceInstance.Name, serviceInstance.Namespace))
1✔
190
                utils.SetBlockedCondition(ctx, "service instance is not ready", serviceBinding)
1✔
191
                if err := utils.UpdateStatus(ctx, r.Client, serviceBinding); err != nil {
1✔
192
                        return ctrl.Result{}, err
×
193
                }
×
194
                return ctrl.Result{}, errors.New("ServiceInstance is not ready")
1✔
195
        }
196

197
        // should rotate creds
198
        if meta.IsStatusConditionTrue(serviceBinding.Status.Conditions, common.ConditionCredRotationInProgress) {
2✔
199
                log.Info("rotating credentials")
1✔
200
                if shouldUpdateStatus, err := r.rotateCredentials(ctx, serviceBinding, serviceInstance); err != nil {
2✔
201
                        if !shouldUpdateStatus {
2✔
202
                                log.Error(err, "internal error occurred during cred rotation, requeuing binding")
1✔
203
                                return ctrl.Result{}, err
1✔
204
                        }
1✔
205
                        return utils.HandleCredRotationError(ctx, r.Client, serviceBinding, err)
×
206
                }
207
        }
208

209
        // is binding ready
210
        if meta.IsStatusConditionTrue(serviceBinding.Status.Conditions, common.ConditionReady) {
2✔
211
                if isStaleServiceBinding(serviceBinding) {
2✔
212
                        log.Info("binding is stale, handling")
1✔
213
                        return r.handleStaleServiceBinding(ctx, serviceBinding)
1✔
214
                }
1✔
215

216
                if initCredRotationIfRequired(serviceBinding) {
2✔
217
                        log.Info("cred rotation required, updating status")
1✔
218
                        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceBinding)
1✔
219
                }
1✔
220

221
                log.Info("binding in final state, maintaining secret")
1✔
222
                return r.maintain(ctx, smClient, serviceBinding)
1✔
223
        }
224

225
        if serviceBinding.Status.BindingID == "" {
2✔
226
                if err := r.validateSecretNameIsAvailable(ctx, serviceBinding); err != nil {
2✔
227
                        log.Error(err, "secret validation failed")
1✔
228
                        utils.SetBlockedCondition(ctx, err.Error(), serviceBinding)
1✔
229
                        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceBinding)
1✔
230
                }
1✔
231

232
                smBinding, err := r.getBindingForRecovery(ctx, smClient, serviceBinding)
1✔
233
                if err != nil {
1✔
234
                        log.Error(err, "failed to check binding recovery")
×
NEW
235
                        return utils.HandleServiceManagerError(ctx, r.Client, serviceBinding, smClientTypes.CREATE, err, true)
×
236
                }
×
237
                if smBinding != nil {
2✔
238
                        return r.recover(ctx, serviceBinding, smBinding)
1✔
239
                }
1✔
240

241
                return r.createBinding(ctx, smClient, serviceInstance, serviceBinding)
1✔
242
        }
243

244
        log.Info("nothing to do for this binding")
1✔
245
        return ctrl.Result{}, nil
1✔
246
}
247

248
func (r *ServiceBindingReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
249

1✔
250
        return ctrl.NewControllerManagedBy(mgr).
1✔
251
                For(&v1.ServiceBinding{}).
1✔
252
                WithOptions(controller.Options{RateLimiter: workqueue.NewTypedItemExponentialFailureRateLimiter[reconcile.Request](r.Config.RetryBaseDelay, r.Config.RetryMaxDelay)}).
1✔
253
                Complete(r)
1✔
254
}
1✔
255

256
func (r *ServiceBindingReconciler) createBinding(ctx context.Context, smClient sm.Client, serviceInstance *v1.ServiceInstance, serviceBinding *v1.ServiceBinding) (ctrl.Result, error) {
1✔
257
        log := logutils.GetLogger(ctx)
1✔
258
        log.Info("Creating smBinding in SM")
1✔
259
        serviceBinding.Status.InstanceID = serviceInstance.Status.InstanceID
1✔
260
        bindingParameters, _, err := utils.BuildSMRequestParameters(serviceBinding.Namespace, serviceBinding.Spec.Parameters, serviceBinding.Spec.ParametersFrom)
1✔
261
        if err != nil {
1✔
262
                log.Error(err, "failed to parse smBinding parameters")
×
263
                return utils.HandleOperationFailure(ctx, r.Client, serviceBinding, smClientTypes.CREATE, err)
×
264
        }
×
265

266
        smBinding, operationURL, bindErr := smClient.Bind(&smClientTypes.ServiceBinding{
1✔
267
                Name: serviceBinding.Spec.ExternalName,
1✔
268
                Labels: smClientTypes.Labels{
1✔
269
                        common.NamespaceLabel: []string{serviceBinding.Namespace},
1✔
270
                        common.K8sNameLabel:   []string{serviceBinding.Name},
1✔
271
                        common.ClusterIDLabel: []string{r.Config.ClusterID},
1✔
272
                },
1✔
273
                ServiceInstanceID: serviceInstance.Status.InstanceID,
1✔
274
                Parameters:        bindingParameters,
1✔
275
        }, nil, utils.BuildUserInfo(ctx, serviceBinding.Spec.UserInfo))
1✔
276

1✔
277
        if bindErr != nil {
2✔
278
                log.Error(err, "failed to create service binding", "serviceInstanceID", serviceInstance.Status.InstanceID)
1✔
279
                return utils.HandleServiceManagerError(ctx, r.Client, serviceBinding, smClientTypes.CREATE, bindErr, true)
1✔
280
        }
1✔
281

282
        if operationURL != "" {
2✔
283
                var bindingID string
1✔
284
                if bindingID = sm.ExtractBindingID(operationURL); len(bindingID) == 0 {
1✔
285
                        return utils.HandleOperationFailure(ctx, r.Client, serviceBinding, smClientTypes.CREATE, fmt.Errorf("failed to extract smBinding ID from operation URL %s", operationURL))
×
286
                }
×
287
                log.Info(fmt.Sprintf("binding is being created async, bindingID=%s", bindingID))
1✔
288
                serviceBinding.Status.BindingID = bindingID
1✔
289

1✔
290
                log.Info("Create smBinding request is async")
1✔
291
                serviceBinding.Status.OperationURL = operationURL
1✔
292
                serviceBinding.Status.OperationType = smClientTypes.CREATE
1✔
293
                utils.SetInProgressConditions(ctx, smClientTypes.CREATE, "", serviceBinding, false)
1✔
294
                if err := utils.UpdateStatus(ctx, r.Client, serviceBinding); err != nil {
2✔
295
                        log.Error(err, "unable to update ServiceBinding status")
1✔
296
                        return ctrl.Result{}, err
1✔
297
                }
1✔
298
                return ctrl.Result{RequeueAfter: r.Config.PollInterval}, nil
1✔
299
        }
300

301
        log.Info("Binding created successfully")
1✔
302

1✔
303
        if err := r.storeBindingSecret(ctx, serviceBinding, smBinding); err != nil {
2✔
304
                return r.handleSecretError(ctx, smClientTypes.CREATE, err, serviceBinding)
1✔
305
        }
1✔
306

307
        subaccountID := ""
1✔
308
        if len(smBinding.Labels["subaccount_id"]) > 0 {
1✔
309
                subaccountID = smBinding.Labels["subaccount_id"][0]
×
310
        }
×
311

312
        serviceBinding.Status.BindingID = smBinding.ID
1✔
313
        serviceBinding.Status.SubaccountID = subaccountID
1✔
314
        serviceBinding.Status.Ready = metav1.ConditionTrue
1✔
315
        r.Retries.Reset(types.NamespacedName{Name: serviceBinding.Name, Namespace: serviceBinding.Namespace})
1✔
316
        utils.SetSuccessConditions(smClientTypes.CREATE, serviceBinding, false)
1✔
317
        log.Info("Updating binding", "bindingID", smBinding.ID)
1✔
318

1✔
319
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceBinding)
1✔
320
}
321

322
func (r *ServiceBindingReconciler) delete(ctx context.Context, smClient sm.Client, serviceBinding *v1.ServiceBinding) (ctrl.Result, error) {
1✔
323
        log := logutils.GetLogger(ctx)
1✔
324
        log.Info(fmt.Sprintf("binding in delete phase, marked for deletion=%v, bindingID=%s, ready=%s", utils.IsMarkedForDeletion(serviceBinding.ObjectMeta), serviceBinding.Status.BindingID, serviceBinding.Status.Ready))
1✔
325
        if controllerutil.ContainsFinalizer(serviceBinding, common.FinalizerName) {
2✔
326
                if len(serviceBinding.Status.BindingID) == 0 {
2✔
327
                        log.Info("No binding id found validating binding does not exists in SM before removing finalizer")
1✔
328
                        smBinding, err := r.getBindingForRecovery(ctx, smClient, serviceBinding)
1✔
329
                        if err != nil {
1✔
NEW
330
                                return utils.HandleServiceManagerError(ctx, r.Client, serviceBinding, smClientTypes.DELETE, err, true)
×
331
                        }
×
332
                        if smBinding != nil {
2✔
333
                                log.Info("binding exists in SM continue with deletion")
1✔
334
                                serviceBinding.Status.BindingID = smBinding.ID
1✔
335
                                utils.SetInProgressConditions(ctx, smClientTypes.DELETE, "delete after recovery", serviceBinding, false)
1✔
336
                                return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceBinding)
1✔
337
                        }
1✔
338

339
                        // make sure there's no secret stored for the binding
340
                        if err := r.deleteBindingSecret(ctx, serviceBinding); err != nil {
1✔
341
                                return ctrl.Result{}, err
×
342
                        }
×
343

344
                        log.Info("Binding does not exists in SM, removing finalizer")
1✔
345
                        if err := utils.RemoveFinalizer(ctx, r.Client, serviceBinding, common.FinalizerName); err != nil {
2✔
346
                                return ctrl.Result{}, err
1✔
347
                        }
1✔
348
                        return ctrl.Result{}, nil
1✔
349
                }
350

351
                if len(serviceBinding.Status.OperationURL) > 0 && serviceBinding.Status.OperationType == smClientTypes.DELETE {
1✔
352
                        // ongoing delete operation - poll status from SM
×
353
                        return r.poll(ctx, smClient, serviceBinding)
×
354
                }
×
355

356
                log.Info(fmt.Sprintf("Deleting binding with id %v from SM, resourceMarkedForDeletions=%v", serviceBinding.Status.BindingID, utils.IsMarkedForDeletion(serviceBinding.ObjectMeta)))
1✔
357
                operationURL, unbindErr := smClient.Unbind(serviceBinding.Status.BindingID, nil, utils.BuildUserInfo(ctx, serviceBinding.Spec.UserInfo))
1✔
358
                if unbindErr != nil {
2✔
359
                        return utils.HandleServiceManagerError(ctx, r.Client, serviceBinding, smClientTypes.DELETE, unbindErr, true)
1✔
360
                }
1✔
361

362
                if operationURL != "" {
2✔
363
                        log.Info("Deleting binding async")
1✔
364
                        serviceBinding.Status.OperationURL = operationURL
1✔
365
                        serviceBinding.Status.OperationType = smClientTypes.DELETE
1✔
366
                        utils.SetInProgressConditions(ctx, smClientTypes.DELETE, "", serviceBinding, false)
1✔
367
                        if err := utils.UpdateStatus(ctx, r.Client, serviceBinding); err != nil {
1✔
368
                                return ctrl.Result{}, err
×
369
                        }
×
370
                        return ctrl.Result{RequeueAfter: r.Config.PollInterval}, nil
1✔
371
                }
372

373
                log.Info("reset binding id after successful sync delete operation")
1✔
374
                serviceBinding.Status.BindingID = ""
1✔
375
                if err := utils.UpdateStatus(ctx, r.Client, serviceBinding); err != nil {
1✔
UNCOV
376
                        log.Error(err, "unable to update ServiceBinding status after deletion")
×
377
                        return ctrl.Result{}, err
×
378
                }
×
379
                log.Info("Binding was deleted successfully")
1✔
380
                return r.deleteSecretAndRemoveFinalizer(ctx, serviceBinding)
1✔
381
        }
382
        return ctrl.Result{}, nil
×
383
}
384

385
func (r *ServiceBindingReconciler) poll(ctx context.Context, smClient sm.Client, serviceBinding *v1.ServiceBinding) (ctrl.Result, error) {
1✔
386
        log := logutils.GetLogger(ctx)
1✔
387
        log.Info(fmt.Sprintf("binding resource is in progress, found operation url %s", serviceBinding.Status.OperationURL))
1✔
388

1✔
389
        status, statusErr := smClient.Status(serviceBinding.Status.OperationURL, nil)
1✔
390
        if statusErr != nil {
2✔
391
                log.Info(fmt.Sprintf("failed to fetch operation, got error from SM: %s", statusErr.Error()), "operationURL", serviceBinding.Status.OperationURL)
1✔
392
                utils.SetInProgressConditions(ctx, serviceBinding.Status.OperationType, string(smClientTypes.INPROGRESS), serviceBinding, false)
1✔
393
                freshStatus := v1.ServiceBindingStatus{
1✔
394
                        Conditions: serviceBinding.GetConditions(),
1✔
395
                }
1✔
396
                if utils.IsMarkedForDeletion(serviceBinding.ObjectMeta) {
2✔
397
                        freshStatus.BindingID = serviceBinding.Status.BindingID
1✔
398
                }
1✔
399
                serviceBinding.Status = freshStatus
1✔
400
                if err := utils.UpdateStatus(ctx, r.Client, serviceBinding); err != nil {
1✔
401
                        log.Error(err, "failed to update status during polling")
×
402
                }
×
403
                return ctrl.Result{}, statusErr
1✔
404
        }
405

406
        if status == nil {
1✔
407
                return utils.HandleOperationFailure(ctx, r.Client, serviceBinding, serviceBinding.Status.OperationType, fmt.Errorf("failed to get last operation status of %s", serviceBinding.Name))
×
408
        }
×
409
        switch status.State {
1✔
410
        case smClientTypes.INPROGRESS:
1✔
411
                fallthrough
1✔
412
        case smClientTypes.PENDING:
1✔
413
                log.Info(fmt.Sprintf("%s is still in progress", serviceBinding.Status.OperationURL))
1✔
414
                if len(status.Description) != 0 {
1✔
415
                        utils.SetInProgressConditions(ctx, status.Type, status.Description, serviceBinding, true)
×
416
                        if err := utils.UpdateStatus(ctx, r.Client, serviceBinding); err != nil {
×
417
                                log.Error(err, "unable to update ServiceBinding polling description")
×
418
                                return ctrl.Result{}, err
×
419
                        }
×
420
                }
421
                return ctrl.Result{RequeueAfter: r.Config.PollInterval}, nil
1✔
422
        case smClientTypes.FAILED:
1✔
423
                log.Info(fmt.Sprintf("%s ended with failure", serviceBinding.Status.OperationURL))
1✔
424
                utils.SetFailureConditions(status.Type, status.Description, serviceBinding, true)
1✔
425
                if serviceBinding.Status.OperationType == smClientTypes.CREATE ||
1✔
426
                        (serviceBinding.Status.OperationType == smClientTypes.DELETE && !utils.IsMarkedForDeletion(serviceBinding.ObjectMeta)) {
2✔
427
                        errMsg := getErrorMsgFromLastOperation(status)
1✔
428
                        log.Info(fmt.Sprintf("async binding failed for binding id %s, error: %s", serviceBinding.Status.BindingID, errMsg))
1✔
429
                        key := types.NamespacedName{Namespace: serviceBinding.GetNamespace(), Name: serviceBinding.GetName()}
1✔
430
                        newState := r.Retries.RegisterFailure(key, logutils.GetCorrelationID(ctx))
1✔
431
                        log.Info(fmt.Sprintf("async binding failed. attempts=%d nextRetry=%s currrent error=%s\n", newState.Attempts, newState.NextRetry.Format(time.RFC3339), errMsg))
1✔
432
                        return r.handleFailedAsyncBinding(ctx, smClient, serviceBinding)
1✔
433
                }
1✔
434
                serviceBinding.Status.OperationURL = ""
1✔
435
                serviceBinding.Status.OperationType = ""
1✔
436
                if err := utils.UpdateStatus(ctx, r.Client, serviceBinding); err != nil {
1✔
437
                        log.Error(err, "unable to update ServiceBinding status")
×
438
                        return ctrl.Result{}, err
×
439
                }
×
440
                errMsg := fmt.Sprintf("Async binding %s operation failed", serviceBinding.Status.OperationType)
1✔
441
                if status.Errors != nil {
1✔
442
                        errMsg = fmt.Sprintf("Async unbind operation failed, errors: %s", string(status.Errors))
×
443
                }
×
444
                return ctrl.Result{}, errors.New(errMsg)
1✔
445
        case smClientTypes.SUCCEEDED:
1✔
446
                log.Info(fmt.Sprintf("%s completed successfully", serviceBinding.Status.OperationURL))
1✔
447
                switch serviceBinding.Status.OperationType {
1✔
448
                case smClientTypes.CREATE:
1✔
449
                        smBinding, err := smClient.GetBindingByID(serviceBinding.Status.BindingID, nil)
1✔
450
                        if err != nil || smBinding == nil {
2✔
451
                                log.Error(err, fmt.Sprintf("binding %s succeeded but could not fetch it from SM", serviceBinding.Status.BindingID))
1✔
452
                                return ctrl.Result{}, err
1✔
453
                        }
1✔
454
                        if len(smBinding.Labels["subaccount_id"]) > 0 {
1✔
455
                                serviceBinding.Status.SubaccountID = smBinding.Labels["subaccount_id"][0]
×
456
                        }
×
457

458
                        if err := r.storeBindingSecret(ctx, serviceBinding, smBinding); err != nil {
1✔
459
                                return r.handleSecretError(ctx, smClientTypes.CREATE, err, serviceBinding)
×
460
                        }
×
461
                        utils.SetSuccessConditions(status.Type, serviceBinding, false)
1✔
462
                case smClientTypes.DELETE:
1✔
463
                        _, err := r.deleteSecretAndRemoveFinalizer(ctx, serviceBinding)
1✔
464
                        if err != nil {
1✔
465
                                log.Error(err, "failed to delete binding secret and remove finalizer after delete operation completed")
×
466
                                return ctrl.Result{}, err
×
467
                        }
×
468

469
                        log.Info("reset binding id after successful async delete operation")
1✔
470
                        serviceBinding.Status.BindingID = ""
1✔
471
                }
472
        }
473

474
        log.Info(fmt.Sprintf("finished polling operation %s '%s'", serviceBinding.Status.OperationType, serviceBinding.Status.OperationURL))
1✔
475
        serviceBinding.Status.OperationURL = ""
1✔
476
        serviceBinding.Status.OperationType = ""
1✔
477
        r.Retries.Reset(types.NamespacedName{Name: serviceBinding.Name, Namespace: serviceBinding.Namespace})
1✔
478

1✔
479
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceBinding)
1✔
480
}
481

482
func (r *ServiceBindingReconciler) handleFailedAsyncBinding(ctx context.Context, smClient sm.Client, serviceBinding *v1.ServiceBinding) (ctrl.Result, error) {
1✔
483
        log := logutils.GetLogger(ctx)
1✔
484
        log.Info(fmt.Sprintf("handleFailedAsyncBinding deleting binding id %s that failed from SM", serviceBinding.Status.BindingID))
1✔
485
        operationURL, unbindErr := smClient.Unbind(serviceBinding.Status.BindingID, nil, utils.BuildUserInfo(ctx, serviceBinding.Spec.UserInfo))
1✔
486
        if unbindErr != nil {
1✔
NEW
487
                log.Error(unbindErr, fmt.Sprintf("handleFailedAsyncBinding unbind binding with id %s failed", serviceBinding.Status.BindingID))
×
NEW
488
                return utils.HandleServiceManagerError(ctx, r.Client, serviceBinding, smClientTypes.DELETE, unbindErr, false)
×
NEW
489
        }
×
490

491
        if operationURL != "" {
1✔
NEW
492
                log.Info(fmt.Sprintf("handleFailedAsyncBinding unbind is async, operation url %s", operationURL))
×
NEW
493
                serviceBinding.Status.OperationURL = operationURL
×
NEW
494
                serviceBinding.Status.OperationType = smClientTypes.DELETE
×
NEW
495
                return ctrl.Result{RequeueAfter: r.Config.PollInterval}, utils.UpdateStatus(ctx, r.Client, serviceBinding)
×
NEW
496
        }
×
497

498
        log.Info(fmt.Sprintf("handleFailedAsyncBinding binding %s deleted successfully", serviceBinding.Status.BindingID))
1✔
499
        serviceBinding.Status.OperationURL = ""
1✔
500
        serviceBinding.Status.OperationType = ""
1✔
501
        serviceBinding.Status.BindingID = ""
1✔
502
        if err := r.Client.Status().Update(ctx, serviceBinding); err != nil {
1✔
NEW
503
                log.Error(err, "handleFailedAsyncBinding failed to update service binding status after deletion")
×
NEW
504
                return ctrl.Result{}, err
×
NEW
505
        }
×
506
        return ctrl.Result{RequeueAfter: r.Config.PollInterval}, nil
1✔
507
}
508

509
func (r *ServiceBindingReconciler) getBindingForRecovery(ctx context.Context, smClient sm.Client, serviceBinding *v1.ServiceBinding) (*smClientTypes.ServiceBinding, error) {
1✔
510
        log := logutils.GetLogger(ctx)
1✔
511
        nameQuery := fmt.Sprintf("name eq '%s'", serviceBinding.Spec.ExternalName)
1✔
512
        clusterIDQuery := fmt.Sprintf("context/clusterid eq '%s'", r.Config.ClusterID)
1✔
513
        namespaceQuery := fmt.Sprintf("context/namespace eq '%s'", serviceBinding.Namespace)
1✔
514
        k8sNameQuery := fmt.Sprintf("%s eq '%s'", common.K8sNameLabel, serviceBinding.Name)
1✔
515
        parameters := sm.Parameters{
1✔
516
                FieldQuery:    []string{nameQuery, clusterIDQuery, namespaceQuery},
1✔
517
                LabelQuery:    []string{k8sNameQuery},
1✔
518
                GeneralParams: []string{"attach_last_operations=true"},
1✔
519
        }
1✔
520
        log.Info(fmt.Sprintf("binding recovery query params: %s, %s, %s, %s", nameQuery, clusterIDQuery, namespaceQuery, k8sNameQuery))
1✔
521

1✔
522
        bindings, err := smClient.ListBindings(&parameters)
1✔
523
        if err != nil {
1✔
524
                log.Error(err, "failed to list bindings in SM")
×
525
                return nil, err
×
526
        }
×
527
        if bindings != nil {
2✔
528
                log.Info(fmt.Sprintf("found %d bindings", len(bindings.ServiceBindings)))
1✔
529
                if len(bindings.ServiceBindings) == 1 {
2✔
530
                        return &bindings.ServiceBindings[0], nil
1✔
531
                }
1✔
532
        }
533
        return nil, nil
1✔
534
}
535

536
func (r *ServiceBindingReconciler) maintain(ctx context.Context, smClient sm.Client, binding *v1.ServiceBinding) (ctrl.Result, error) {
1✔
537
        log := logutils.GetLogger(ctx)
1✔
538
        if err := r.maintainSecret(ctx, smClient, binding); err != nil {
2✔
539
                log.Error(err, "failed to maintain secret")
1✔
540
                return r.handleSecretError(ctx, smClientTypes.UPDATE, err, binding)
1✔
541
        }
1✔
542

543
        log.Info("maintain finished successfully")
1✔
544
        return ctrl.Result{}, nil
1✔
545
}
546

547
func (r *ServiceBindingReconciler) maintainSecret(ctx context.Context, smClient sm.Client, serviceBinding *v1.ServiceBinding) error {
1✔
548
        log := logutils.GetLogger(ctx)
1✔
549
        if common.GetObservedGeneration(serviceBinding) == serviceBinding.Generation {
2✔
550
                log.Info("observed generation is up to date, checking if secret exists")
1✔
551
                if _, err := r.getSecret(ctx, serviceBinding.Namespace, serviceBinding.Spec.SecretName); err == nil {
2✔
552
                        log.Info("secret exists, no need to maintain secret")
1✔
553
                        return nil
1✔
554
                }
1✔
555

556
                log.Info("binding's secret was not found")
1✔
557
                r.Recorder.Eventf(serviceBinding, nil, corev1.EventTypeWarning, "SecretDeleted", "SecretDeleted", "SecretDeleted")
1✔
558
        }
559

560
        log.Info("maintaining binding's secret")
1✔
561
        smBinding, err := smClient.GetBindingByID(serviceBinding.Status.BindingID, nil)
1✔
562
        if err != nil {
1✔
563
                log.Error(err, "failed to get binding for update secret")
×
564
                return err
×
565
        }
×
566
        if smBinding != nil {
2✔
567
                if smBinding.Credentials != nil {
2✔
568
                        if err = r.storeBindingSecret(ctx, serviceBinding, smBinding); err != nil {
2✔
569
                                return err
1✔
570
                        }
1✔
571
                        log.Info("Updating binding", "bindingID", smBinding.ID)
1✔
572
                        utils.SetSuccessConditions(smClientTypes.UPDATE, serviceBinding, false)
1✔
573
                }
574
        }
575

576
        return utils.UpdateStatus(ctx, r.Client, serviceBinding)
1✔
577
}
578

579
func (r *ServiceBindingReconciler) getServiceInstanceForBinding(ctx context.Context, binding *v1.ServiceBinding) (*v1.ServiceInstance, error) {
1✔
580
        log := logutils.GetLogger(ctx)
1✔
581
        serviceInstance := &v1.ServiceInstance{}
1✔
582
        namespace := binding.Namespace
1✔
583
        if len(binding.Spec.ServiceInstanceNamespace) > 0 {
2✔
584
                namespace = binding.Spec.ServiceInstanceNamespace
1✔
585
        }
1✔
586
        log.Info(fmt.Sprintf("getting service instance named %s in namespace %s for binding %s in namespace %s", binding.Spec.ServiceInstanceName, namespace, binding.Name, binding.Namespace))
1✔
587
        if err := r.Client.Get(ctx, types.NamespacedName{Name: binding.Spec.ServiceInstanceName, Namespace: namespace}, serviceInstance); err != nil {
2✔
588
                return serviceInstance, err
1✔
589
        }
1✔
590

591
        return serviceInstance.DeepCopy(), nil
1✔
592
}
593

594
func (r *ServiceBindingReconciler) resyncBindingStatus(ctx context.Context, k8sBinding *v1.ServiceBinding, smBinding *smClientTypes.ServiceBinding) {
1✔
595
        k8sBinding.Status.BindingID = smBinding.ID
1✔
596
        k8sBinding.Status.InstanceID = smBinding.ServiceInstanceID
1✔
597
        k8sBinding.Status.OperationURL = ""
1✔
598
        k8sBinding.Status.OperationType = ""
1✔
599

1✔
600
        bindingStatus := smClientTypes.SUCCEEDED
1✔
601
        operationType := smClientTypes.CREATE
1✔
602
        description := ""
1✔
603
        if smBinding.LastOperation != nil {
2✔
604
                bindingStatus = smBinding.LastOperation.State
1✔
605
                operationType = smBinding.LastOperation.Type
1✔
606
                description = smBinding.LastOperation.Description
1✔
607
        } else if !smBinding.Ready {
3✔
608
                bindingStatus = smClientTypes.FAILED
1✔
609
        }
1✔
610
        switch bindingStatus {
1✔
611
        case smClientTypes.PENDING:
×
612
                fallthrough
×
613
        case smClientTypes.INPROGRESS:
1✔
614
                k8sBinding.Status.OperationURL = sm.BuildOperationURL(smBinding.LastOperation.ID, smBinding.ID, smClientTypes.ServiceBindingsURL)
1✔
615
                k8sBinding.Status.OperationType = smBinding.LastOperation.Type
1✔
616
                utils.SetInProgressConditions(ctx, smBinding.LastOperation.Type, smBinding.LastOperation.Description, k8sBinding, false)
1✔
617
        case smClientTypes.SUCCEEDED:
1✔
618
                utils.SetSuccessConditions(operationType, k8sBinding, false)
1✔
619
        case smClientTypes.FAILED:
1✔
620
                utils.SetFailureConditions(operationType, description, k8sBinding, false)
1✔
621
        }
622
}
623

624
func (r *ServiceBindingReconciler) storeBindingSecret(ctx context.Context, k8sBinding *v1.ServiceBinding, smBinding *smClientTypes.ServiceBinding) error {
1✔
625
        log := logutils.GetLogger(ctx)
1✔
626
        logger := log.WithValues("bindingName", k8sBinding.Name, "secretName", k8sBinding.Spec.SecretName)
1✔
627

1✔
628
        var secret *corev1.Secret
1✔
629
        var err error
1✔
630

1✔
631
        if k8sBinding.Spec.SecretTemplate != "" {
2✔
632
                secret, err = r.createBindingSecretFromSecretTemplate(ctx, k8sBinding, smBinding)
1✔
633
        } else {
2✔
634
                secret, err = r.createBindingSecret(ctx, k8sBinding, smBinding)
1✔
635
        }
1✔
636

637
        if err != nil {
2✔
638
                return err
1✔
639
        }
1✔
640
        if err = controllerutil.SetControllerReference(k8sBinding, secret, r.Scheme); err != nil {
1✔
641
                logger.Error(err, "Failed to set secret owner")
×
642
                return err
×
643
        }
×
644

645
        if secret.Labels == nil {
1✔
646
                secret.Labels = map[string]string{}
×
647
        }
×
648
        secret.Labels[common.ManagedByBTPOperatorLabel] = "true"
1✔
649
        if len(k8sBinding.Labels) > 0 && len(k8sBinding.Labels[common.StaleBindingIDLabel]) > 0 {
2✔
650
                secret.Labels[common.StaleBindingIDLabel] = k8sBinding.Labels[common.StaleBindingIDLabel]
1✔
651
        }
1✔
652

653
        if secret.Annotations == nil {
1✔
654
                secret.Annotations = map[string]string{}
×
655
        }
×
656
        secret.Annotations["binding"] = k8sBinding.Name
1✔
657

1✔
658
        return r.createOrUpdateBindingSecret(ctx, k8sBinding, secret)
1✔
659
}
660

661
func (r *ServiceBindingReconciler) createBindingSecret(ctx context.Context, k8sBinding *v1.ServiceBinding, smBinding *smClientTypes.ServiceBinding) (*corev1.Secret, error) {
1✔
662
        credentialsMap, err := r.getSecretDefaultData(ctx, k8sBinding, smBinding)
1✔
663
        if err != nil {
2✔
664
                return nil, err
1✔
665
        }
1✔
666

667
        secret := &corev1.Secret{
1✔
668
                ObjectMeta: metav1.ObjectMeta{
1✔
669
                        Name:        k8sBinding.Spec.SecretName,
1✔
670
                        Annotations: map[string]string{"binding": k8sBinding.Name},
1✔
671
                        Labels:      map[string]string{common.ManagedByBTPOperatorLabel: "true"},
1✔
672
                        Namespace:   k8sBinding.Namespace,
1✔
673
                },
1✔
674
                Data: credentialsMap,
1✔
675
        }
1✔
676
        return secret, nil
1✔
677
}
678

679
func (r *ServiceBindingReconciler) getSecretDefaultData(ctx context.Context, k8sBinding *v1.ServiceBinding, smBinding *smClientTypes.ServiceBinding) (map[string][]byte, error) {
1✔
680
        log := logutils.GetLogger(ctx).WithValues("bindingName", k8sBinding.Name, "secretName", k8sBinding.Spec.SecretName)
1✔
681

1✔
682
        var credentialsMap map[string][]byte
1✔
683
        var credentialProperties []utils.SecretMetadataProperty
1✔
684

1✔
685
        if len(smBinding.Credentials) == 0 {
2✔
686
                log.Info("Binding credentials are empty")
1✔
687
                credentialsMap = make(map[string][]byte)
1✔
688
        } else if k8sBinding.Spec.SecretKey != nil {
3✔
689
                credentialsMap = map[string][]byte{
1✔
690
                        *k8sBinding.Spec.SecretKey: smBinding.Credentials,
1✔
691
                }
1✔
692
                credentialProperties = []utils.SecretMetadataProperty{
1✔
693
                        {
1✔
694
                                Name:      *k8sBinding.Spec.SecretKey,
1✔
695
                                Format:    string(utils.JSON),
1✔
696
                                Container: true,
1✔
697
                        },
1✔
698
                }
1✔
699
        } else {
2✔
700
                var err error
1✔
701
                credentialsMap, credentialProperties, err = utils.NormalizeCredentials(smBinding.Credentials)
1✔
702
                if err != nil {
2✔
703
                        log.Error(err, "Failed to store binding secret")
1✔
704
                        return nil, fmt.Errorf("failed to create secret. Error: %v", err.Error())
1✔
705
                }
1✔
706
        }
707

708
        metaDataProperties, err := r.addInstanceInfo(ctx, k8sBinding, credentialsMap)
1✔
709
        if err != nil {
1✔
710
                log.Error(err, "failed to enrich binding with service instance info")
×
711
        }
×
712

713
        if k8sBinding.Spec.SecretRootKey != nil {
2✔
714
                var err error
1✔
715
                credentialsMap, err = singleKeyMap(credentialsMap, *k8sBinding.Spec.SecretRootKey)
1✔
716
                if err != nil {
1✔
717
                        return nil, err
×
718
                }
×
719
        } else {
1✔
720
                metadata := map[string][]utils.SecretMetadataProperty{
1✔
721
                        "metaDataProperties":   metaDataProperties,
1✔
722
                        "credentialProperties": credentialProperties,
1✔
723
                }
1✔
724
                metadataByte, err := json.Marshal(metadata)
1✔
725
                if err != nil {
1✔
726
                        log.Error(err, "failed to enrich binding with metadata")
×
727
                } else {
1✔
728
                        credentialsMap[".metadata"] = metadataByte
1✔
729
                }
1✔
730
        }
731
        return credentialsMap, nil
1✔
732
}
733

734
func (r *ServiceBindingReconciler) createBindingSecretFromSecretTemplate(ctx context.Context, k8sBinding *v1.ServiceBinding, smBinding *smClientTypes.ServiceBinding) (*corev1.Secret, error) {
1✔
735
        log := logutils.GetLogger(ctx)
1✔
736
        logger := log.WithValues("bindingName", k8sBinding.Name, "secretName", k8sBinding.Spec.SecretName)
1✔
737

1✔
738
        logger.Info("Create Object using SecretTemplate from ServiceBinding Specs")
1✔
739
        inputSmCredentials := smBinding.Credentials
1✔
740
        smBindingCredentials := make(map[string]interface{})
1✔
741
        if inputSmCredentials != nil {
2✔
742
                err := json.Unmarshal(inputSmCredentials, &smBindingCredentials)
1✔
743
                if err != nil {
1✔
744
                        logger.Error(err, "failed to unmarshal given service binding credentials")
×
745
                        return nil, errors.Wrap(err, "failed to unmarshal given service binding credentials")
×
746
                }
×
747
        }
748

749
        instanceInfos, err := r.getInstanceInfo(ctx, k8sBinding)
1✔
750
        if err != nil {
1✔
751
                logger.Error(err, "failed to addInstanceInfo")
×
752
                return nil, errors.Wrap(err, "failed to add service instance info")
×
753
        }
×
754

755
        parameters := commonutils.GetSecretDataForTemplate(smBindingCredentials, instanceInfos)
1✔
756
        templateName := fmt.Sprintf("%s/%s", k8sBinding.Namespace, k8sBinding.Name)
1✔
757
        secret, err := commonutils.CreateSecretFromTemplate(templateName, k8sBinding.Spec.SecretTemplate, "missingkey=error", parameters)
1✔
758
        if err != nil {
2✔
759
                logger.Error(err, "failed to create secret from template")
1✔
760
                return nil, errors.Wrap(err, "failed to create secret from template")
1✔
761
        }
1✔
762
        secret.SetNamespace(k8sBinding.Namespace)
1✔
763
        secret.SetName(k8sBinding.Spec.SecretName)
1✔
764
        if secret.Labels == nil {
1✔
765
                secret.Labels = map[string]string{}
×
766
        }
×
767
        secret.Labels[common.ManagedByBTPOperatorLabel] = "true"
1✔
768

1✔
769
        // if no data provided use the default data
1✔
770
        if len(secret.Data) == 0 && len(secret.StringData) == 0 {
2✔
771
                credentialsMap, err := r.getSecretDefaultData(ctx, k8sBinding, smBinding)
1✔
772
                if err != nil {
1✔
773
                        return nil, err
×
774
                }
×
775
                secret.Data = credentialsMap
1✔
776
        }
777
        return secret, nil
1✔
778
}
779

780
func (r *ServiceBindingReconciler) createOrUpdateBindingSecret(ctx context.Context, binding *v1.ServiceBinding, secret *corev1.Secret) error {
1✔
781
        log := logutils.GetLogger(ctx)
1✔
782
        dbSecret := &corev1.Secret{}
1✔
783
        create := false
1✔
784
        if err := r.Client.Get(ctx, types.NamespacedName{Name: binding.Spec.SecretName, Namespace: binding.Namespace}, dbSecret); err != nil {
2✔
785
                if !apierrors.IsNotFound(err) {
1✔
786
                        return err
×
787
                }
×
788
                create = true
1✔
789
        }
790

791
        if create {
2✔
792
                log.Info("Creating binding secret", "name", secret.Name)
1✔
793
                if err := r.Client.Create(ctx, secret); err != nil {
1✔
794
                        if !apierrors.IsAlreadyExists(err) {
×
795
                                return err
×
796
                        }
×
797
                        return nil
×
798
                }
799
                r.Recorder.Eventf(binding, nil, corev1.EventTypeNormal, "SecretCreated", "SecretCreated", "SecretCreated")
1✔
800
                return nil
1✔
801
        }
802

803
        log.Info("Updating existing binding secret", "name", secret.Name)
1✔
804
        dbSecret.Data = secret.Data
1✔
805
        dbSecret.StringData = secret.StringData
1✔
806
        dbSecret.Labels = secret.Labels
1✔
807
        dbSecret.Annotations = secret.Annotations
1✔
808
        return r.Client.Update(ctx, dbSecret)
1✔
809
}
810

811
func (r *ServiceBindingReconciler) deleteBindingSecret(ctx context.Context, binding *v1.ServiceBinding) error {
1✔
812
        log := logutils.GetLogger(ctx)
1✔
813
        log.Info("Deleting binding secret")
1✔
814
        bindingSecret := &corev1.Secret{}
1✔
815
        if err := r.Client.Get(ctx, types.NamespacedName{
1✔
816
                Namespace: binding.Namespace,
1✔
817
                Name:      binding.Spec.SecretName,
1✔
818
        }, bindingSecret); err != nil {
2✔
819
                if !apierrors.IsNotFound(err) {
1✔
820
                        log.Error(err, "unable to fetch binding secret")
×
821
                        return err
×
822
                }
×
823

824
                // secret not found, nothing more to do
825
                log.Info("secret was deleted successfully")
1✔
826
                return nil
1✔
827
        }
828
        bindingSecret = bindingSecret.DeepCopy()
1✔
829

1✔
830
        if err := r.Client.Delete(ctx, bindingSecret); err != nil {
1✔
831
                log.Error(err, "Failed to delete binding secret")
×
832
                return err
×
833
        }
×
834

835
        log.Info("secret was deleted successfully")
1✔
836
        return nil
1✔
837
}
838

839
func (r *ServiceBindingReconciler) deleteSecretAndRemoveFinalizer(ctx context.Context, serviceBinding *v1.ServiceBinding) (ctrl.Result, error) {
1✔
840
        // delete binding secret if exist
1✔
841
        if err := r.deleteBindingSecret(ctx, serviceBinding); err != nil {
1✔
842
                return ctrl.Result{}, err
×
843
        }
×
844

845
        return ctrl.Result{}, utils.RemoveFinalizer(ctx, r.Client, serviceBinding, common.FinalizerName)
1✔
846
}
847

848
func (r *ServiceBindingReconciler) getSecret(ctx context.Context, namespace string, name string) (*corev1.Secret, error) {
1✔
849
        secret := &corev1.Secret{}
1✔
850
        err := utils.GetSecretWithFallback(ctx, types.NamespacedName{Namespace: namespace, Name: name}, secret)
1✔
851
        return secret, err
1✔
852
}
1✔
853

854
func (r *ServiceBindingReconciler) validateSecretNameIsAvailable(ctx context.Context, binding *v1.ServiceBinding) error {
1✔
855
        currentSecret, err := r.getSecret(ctx, binding.Namespace, binding.Spec.SecretName)
1✔
856
        if err != nil {
2✔
857
                return client.IgnoreNotFound(err)
1✔
858
        }
1✔
859

860
        if metav1.IsControlledBy(currentSecret, binding) {
2✔
861
                return nil
1✔
862
        }
1✔
863

864
        ownerRef := metav1.GetControllerOf(currentSecret)
1✔
865
        if ownerRef != nil {
2✔
866
                owner, err := schema.ParseGroupVersion(ownerRef.APIVersion)
1✔
867
                if err != nil {
1✔
868
                        return err
×
869
                }
×
870

871
                if owner.Group == binding.GroupVersionKind().Group && ownerRef.Kind == binding.Kind {
2✔
872
                        return fmt.Errorf(secretAlreadyOwnedErrorFormat, binding.Spec.SecretName, ownerRef.Name)
1✔
873
                }
1✔
874
        }
875

876
        return fmt.Errorf(secretNameTakenErrorFormat, binding.Spec.SecretName)
1✔
877
}
878

879
func (r *ServiceBindingReconciler) handleSecretError(ctx context.Context, op smClientTypes.OperationCategory, err error, binding *v1.ServiceBinding) (ctrl.Result, error) {
1✔
880
        log := logutils.GetLogger(ctx)
1✔
881
        log.Error(err, fmt.Sprintf("failed to store secret %s for binding %s", binding.Spec.SecretName, binding.Name))
1✔
882
        return utils.HandleOperationFailure(ctx, r.Client, binding, op, err)
1✔
883
}
1✔
884

885
func (r *ServiceBindingReconciler) getInstanceInfo(ctx context.Context, binding *v1.ServiceBinding) (map[string]string, error) {
1✔
886
        instance, err := r.getServiceInstanceForBinding(ctx, binding)
1✔
887
        if err != nil {
1✔
888
                return nil, err
×
889
        }
×
890
        instanceInfos := make(map[string]string)
1✔
891
        instanceInfos["instance_name"] = string(getInstanceNameForSecretCredentials(instance))
1✔
892
        instanceInfos["instance_guid"] = instance.Status.InstanceID
1✔
893
        instanceInfos["plan"] = instance.Spec.ServicePlanName
1✔
894
        instanceInfos["label"] = instance.Spec.ServiceOfferingName
1✔
895
        instanceInfos["type"] = instance.Spec.ServiceOfferingName
1✔
896
        if len(instance.Status.Tags) > 0 || len(instance.Spec.CustomTags) > 0 {
2✔
897
                tags := mergeInstanceTags(instance.Status.Tags, instance.Spec.CustomTags)
1✔
898
                instanceInfos["tags"] = strings.Join(tags, ",")
1✔
899
        }
1✔
900
        return instanceInfos, nil
1✔
901
}
902

903
func (r *ServiceBindingReconciler) addInstanceInfo(ctx context.Context, binding *v1.ServiceBinding, credentialsMap map[string][]byte) ([]utils.SecretMetadataProperty, error) {
1✔
904
        instance, err := r.getServiceInstanceForBinding(ctx, binding)
1✔
905
        if err != nil {
1✔
906
                return nil, err
×
907
        }
×
908

909
        credentialsMap["instance_name"] = getInstanceNameForSecretCredentials(instance)
1✔
910
        credentialsMap["instance_guid"] = []byte(instance.Status.InstanceID)
1✔
911
        credentialsMap["plan"] = []byte(instance.Spec.ServicePlanName)
1✔
912
        credentialsMap["label"] = []byte(instance.Spec.ServiceOfferingName)
1✔
913
        credentialsMap["type"] = []byte(instance.Spec.ServiceOfferingName)
1✔
914
        if len(instance.Status.Tags) > 0 || len(instance.Spec.CustomTags) > 0 {
2✔
915
                tagsBytes, err := json.Marshal(mergeInstanceTags(instance.Status.Tags, instance.Spec.CustomTags))
1✔
916
                if err != nil {
1✔
917
                        return nil, err
×
918
                }
×
919
                credentialsMap["tags"] = tagsBytes
1✔
920
        }
921

922
        metadata := []utils.SecretMetadataProperty{
1✔
923
                {
1✔
924
                        Name:   "instance_name",
1✔
925
                        Format: string(utils.TEXT),
1✔
926
                },
1✔
927
                {
1✔
928
                        Name:   "instance_guid",
1✔
929
                        Format: string(utils.TEXT),
1✔
930
                },
1✔
931
                {
1✔
932
                        Name:   "plan",
1✔
933
                        Format: string(utils.TEXT),
1✔
934
                },
1✔
935
                {
1✔
936
                        Name:   "label",
1✔
937
                        Format: string(utils.TEXT),
1✔
938
                },
1✔
939
                {
1✔
940
                        Name:   "type",
1✔
941
                        Format: string(utils.TEXT),
1✔
942
                },
1✔
943
        }
1✔
944
        if _, ok := credentialsMap["tags"]; ok {
2✔
945
                metadata = append(metadata, utils.SecretMetadataProperty{Name: "tags", Format: string(utils.JSON)})
1✔
946
        }
1✔
947

948
        return metadata, nil
1✔
949
}
950

951
func (r *ServiceBindingReconciler) rotateCredentials(ctx context.Context, binding *v1.ServiceBinding, serviceInstance *v1.ServiceInstance) (bool, error) {
1✔
952
        log := logutils.GetLogger(ctx)
1✔
953
        if err := r.removeForceRotateAnnotationIfNeeded(ctx, binding, log); err != nil {
1✔
954
                log.Info("Credentials rotation - failed to delete force rotate annotation")
×
955
                return false, err
×
956
        }
×
957

958
        credInProgressCondition := meta.FindStatusCondition(binding.GetConditions(), common.ConditionCredRotationInProgress)
1✔
959
        if credInProgressCondition.Reason == common.CredRotating {
2✔
960
                if len(binding.Status.BindingID) > 0 && binding.Status.Ready == metav1.ConditionTrue {
2✔
961
                        log.Info("Credentials rotation - finished successfully")
1✔
962
                        now := metav1.NewTime(time.Now())
1✔
963
                        binding.Status.LastCredentialsRotationTime = &now
1✔
964
                        return false, r.stopRotation(ctx, binding)
1✔
965
                }
1✔
966
                log.Info("Credentials rotation - waiting to finish")
1✔
967
                return false, nil
1✔
968
        }
969

970
        if len(binding.Status.BindingID) == 0 {
1✔
971
                log.Info("Credentials rotation - no binding id found nothing to do")
×
972
                return false, r.stopRotation(ctx, binding)
×
973
        }
×
974

975
        bindings := &v1.ServiceBindingList{}
1✔
976
        err := r.Client.List(ctx, bindings, client.MatchingLabels{common.StaleBindingIDLabel: binding.Status.BindingID}, client.InNamespace(binding.Namespace))
1✔
977
        if err != nil {
1✔
978
                return false, err
×
979
        }
×
980

981
        if len(bindings.Items) == 0 {
2✔
982
                // create the backup binding
1✔
983
                smClient, err := r.GetSMClient(ctx, serviceInstance)
1✔
984
                if err != nil {
1✔
985
                        return false, err
×
986
                }
×
987

988
                // rename current binding
989
                suffix := "-" + utils.RandStringRunes(6)
1✔
990
                log.Info("Credentials rotation - renaming binding to old in SM", "current", binding.Spec.ExternalName)
1✔
991
                if _, errRenaming := smClient.RenameBinding(binding.Status.BindingID, binding.Spec.ExternalName+suffix, binding.Name+suffix); errRenaming != nil {
1✔
992
                        log.Error(errRenaming, "Credentials rotation - failed renaming binding to old in SM", "binding", binding.Spec.ExternalName)
×
993
                        return true, errRenaming
×
994
                }
×
995

996
                log.Info("Credentials rotation - backing up old binding in K8S", "name", binding.Name+suffix)
1✔
997
                if err := r.createOldBinding(ctx, suffix, binding); err != nil {
1✔
998
                        log.Error(err, "Credentials rotation - failed to back up old binding in K8S")
×
999
                        return true, err
×
1000
                }
×
1001
        }
1002

1003
        log.Info("reset binding id after successful rotation")
1✔
1004
        binding.Status.BindingID = ""
1✔
1005
        binding.Status.Ready = metav1.ConditionFalse
1✔
1006
        utils.SetInProgressConditions(ctx, smClientTypes.CREATE, "rotating binding credentials", binding, false)
1✔
1007
        utils.SetCredRotationInProgressConditions(common.CredRotating, "", binding)
1✔
1008
        return false, utils.UpdateStatus(ctx, r.Client, binding)
1✔
1009
}
1010

1011
func (r *ServiceBindingReconciler) removeForceRotateAnnotationIfNeeded(ctx context.Context, binding *v1.ServiceBinding, log logr.Logger) error {
1✔
1012
        if binding.Annotations != nil {
2✔
1013
                if _, ok := binding.Annotations[common.ForceRotateAnnotation]; ok {
2✔
1014
                        log.Info("Credentials rotation - deleting force rotate annotation")
1✔
1015
                        delete(binding.Annotations, common.ForceRotateAnnotation)
1✔
1016
                        return r.Client.Update(ctx, binding)
1✔
1017
                }
1✔
1018
        }
1019
        return nil
1✔
1020
}
1021

1022
func (r *ServiceBindingReconciler) stopRotation(ctx context.Context, binding *v1.ServiceBinding) error {
1✔
1023
        conditions := binding.GetConditions()
1✔
1024
        meta.RemoveStatusCondition(&conditions, common.ConditionCredRotationInProgress)
1✔
1025
        binding.Status.Conditions = conditions
1✔
1026
        return utils.UpdateStatus(ctx, r.Client, binding)
1✔
1027
}
1✔
1028

1029
func (r *ServiceBindingReconciler) createOldBinding(ctx context.Context, suffix string, binding *v1.ServiceBinding) error {
1✔
1030
        oldBinding := newBindingObject(binding.Name+suffix, binding.Namespace)
1✔
1031
        err := controllerutil.SetControllerReference(binding, oldBinding, r.Scheme)
1✔
1032
        if err != nil {
1✔
1033
                return err
×
1034
        }
×
1035
        oldBinding.Labels = map[string]string{
1✔
1036
                common.StaleBindingIDLabel:         binding.Status.BindingID,
1✔
1037
                common.StaleBindingRotationOfLabel: truncateString(binding.Name, 63),
1✔
1038
        }
1✔
1039
        oldBinding.Annotations = map[string]string{
1✔
1040
                common.StaleBindingOrigBindingNameAnnotation: binding.Name,
1✔
1041
        }
1✔
1042
        spec := binding.Spec.DeepCopy()
1✔
1043
        spec.CredRotationPolicy.Enabled = false
1✔
1044
        spec.SecretName = spec.SecretName + suffix
1✔
1045
        spec.ExternalName = spec.ExternalName + suffix
1✔
1046
        oldBinding.Spec = *spec
1✔
1047
        return r.Client.Create(ctx, oldBinding)
1✔
1048
}
1049

1050
func (r *ServiceBindingReconciler) handleStaleServiceBinding(ctx context.Context, serviceBinding *v1.ServiceBinding) (ctrl.Result, error) {
1✔
1051
        log := logutils.GetLogger(ctx)
1✔
1052
        originalBindingName, ok := serviceBinding.Annotations[common.StaleBindingOrigBindingNameAnnotation]
1✔
1053
        if !ok {
2✔
1054
                //if the user removed the "OrigBindingName" annotation and rotationOf label not exist as well
1✔
1055
                //the stale binding should be deleted otherwise it will remain forever
1✔
1056
                if originalBindingName, ok = serviceBinding.Labels[common.StaleBindingRotationOfLabel]; !ok {
2✔
1057
                        log.Info("missing rotationOf label/annotation, unable to fetch original binding, deleting stale")
1✔
1058
                        return ctrl.Result{}, r.Client.Delete(ctx, serviceBinding)
1✔
1059
                }
1✔
1060
        }
1061
        origBinding := &v1.ServiceBinding{}
1✔
1062
        if err := r.Client.Get(ctx, types.NamespacedName{Namespace: serviceBinding.Namespace, Name: originalBindingName}, origBinding); err != nil {
1✔
UNCOV
1063
                if apierrors.IsNotFound(err) {
×
UNCOV
1064
                        log.Info("original binding not found, deleting stale binding")
×
UNCOV
1065
                        return ctrl.Result{}, r.Client.Delete(ctx, serviceBinding)
×
UNCOV
1066
                }
×
1067
                return ctrl.Result{}, err
×
1068
        }
1069
        if meta.IsStatusConditionTrue(origBinding.Status.Conditions, common.ConditionReady) {
2✔
1070
                return ctrl.Result{}, r.Client.Delete(ctx, serviceBinding)
1✔
1071
        }
1✔
1072

1073
        log.Info("not deleting stale binding since original binding is not ready")
1✔
1074
        if !meta.IsStatusConditionPresentAndEqual(serviceBinding.Status.Conditions, common.ConditionPendingTermination, metav1.ConditionTrue) {
2✔
1075
                pendingTerminationCondition := metav1.Condition{
1✔
1076
                        Type:               common.ConditionPendingTermination,
1✔
1077
                        Status:             metav1.ConditionTrue,
1✔
1078
                        Reason:             common.ConditionPendingTermination,
1✔
1079
                        Message:            "waiting for new credentials to be ready",
1✔
1080
                        ObservedGeneration: serviceBinding.GetGeneration(),
1✔
1081
                }
1✔
1082
                meta.SetStatusCondition(&serviceBinding.Status.Conditions, pendingTerminationCondition)
1✔
1083
                return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceBinding)
1✔
1084
        }
1✔
1085
        return ctrl.Result{}, nil
1✔
1086
}
1087

1088
func (r *ServiceBindingReconciler) recover(ctx context.Context, serviceBinding *v1.ServiceBinding, smBinding *smClientTypes.ServiceBinding) (ctrl.Result, error) {
1✔
1089
        log := logutils.GetLogger(ctx)
1✔
1090
        log.Info(fmt.Sprintf("found existing smBinding in SM with id %s, updating status", smBinding.ID))
1✔
1091

1✔
1092
        if smBinding.Credentials != nil {
2✔
1093
                if err := r.storeBindingSecret(ctx, serviceBinding, smBinding); err != nil {
1✔
1094
                        operationType := smClientTypes.CREATE
×
1095
                        if smBinding.LastOperation != nil {
×
1096
                                operationType = smBinding.LastOperation.Type
×
1097
                        }
×
1098
                        return r.handleSecretError(ctx, operationType, err, serviceBinding)
×
1099
                }
1100
        }
1101
        r.resyncBindingStatus(ctx, serviceBinding, smBinding)
1✔
1102

1✔
1103
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceBinding)
1✔
1104
}
1105

1106
func (r *ServiceBindingReconciler) handleInstanceForBindingNotFound(ctx context.Context, serviceBinding *v1.ServiceBinding) (ctrl.Result, error) {
1✔
1107
        log := logutils.GetLogger(ctx)
1✔
1108
        instanceNamespace := serviceBinding.Namespace
1✔
1109
        if len(serviceBinding.Spec.ServiceInstanceNamespace) > 0 {
1✔
1110
                instanceNamespace = serviceBinding.Spec.ServiceInstanceNamespace
×
1111
        }
×
1112
        errMsg := fmt.Sprintf("couldn't find the service instance '%s' in namespace '%s'", serviceBinding.Spec.ServiceInstanceName, instanceNamespace)
1✔
1113
        log.Info(errMsg)
1✔
1114
        utils.SetBlockedCondition(ctx, errMsg, serviceBinding)
1✔
1115
        if updateErr := utils.UpdateStatus(ctx, r.Client, serviceBinding); updateErr != nil {
2✔
1116
                return ctrl.Result{}, updateErr
1✔
1117
        }
1✔
1118
        return ctrl.Result{}, fmt.Errorf("instance %s not found in namespace %s", serviceBinding.Spec.ServiceInstanceName, instanceNamespace)
1✔
1119
}
1120

1121
func isStaleServiceBinding(binding *v1.ServiceBinding) bool {
1✔
1122
        if utils.IsMarkedForDeletion(binding.ObjectMeta) {
1✔
1123
                return false
×
1124
        }
×
1125

1126
        if binding.Labels != nil {
2✔
1127
                if _, ok := binding.Labels[common.StaleBindingIDLabel]; ok {
2✔
1128
                        if binding.Spec.CredRotationPolicy != nil {
2✔
1129
                                keepFor, _ := time.ParseDuration(binding.Spec.CredRotationPolicy.RotatedBindingTTL)
1✔
1130
                                if time.Since(binding.CreationTimestamp.Time) > keepFor {
2✔
1131
                                        return true
1✔
1132
                                }
1✔
1133
                        }
1134
                }
1135
        }
1136
        return false
1✔
1137
}
1138

1139
func initCredRotationIfRequired(binding *v1.ServiceBinding) bool {
1✔
1140
        if utils.IsFailed(binding) || !credRotationEnabled(binding) {
2✔
1141
                return false
1✔
1142
        }
1✔
1143
        _, forceRotate := binding.Annotations[common.ForceRotateAnnotation]
1✔
1144

1✔
1145
        lastCredentialRotationTime := binding.Status.LastCredentialsRotationTime
1✔
1146
        if lastCredentialRotationTime == nil {
2✔
1147
                ts := metav1.NewTime(binding.CreationTimestamp.Time)
1✔
1148
                lastCredentialRotationTime = &ts
1✔
1149
        }
1✔
1150

1151
        rotationInterval, _ := time.ParseDuration(binding.Spec.CredRotationPolicy.RotationFrequency)
1✔
1152
        if time.Since(lastCredentialRotationTime.Time) > rotationInterval || forceRotate {
2✔
1153
                utils.SetCredRotationInProgressConditions(common.CredPreparing, "", binding)
1✔
1154
                return true
1✔
1155
        }
1✔
1156

1157
        return false
1✔
1158
}
1159

1160
func credRotationEnabled(binding *v1.ServiceBinding) bool {
1✔
1161
        return binding.Spec.CredRotationPolicy != nil && binding.Spec.CredRotationPolicy.Enabled
1✔
1162
}
1✔
1163

1164
func mergeInstanceTags(offeringTags, customTags []string) []string {
1✔
1165
        var tags []string
1✔
1166

1✔
1167
        for _, tag := range append(offeringTags, customTags...) {
2✔
1168
                if !utils.SliceContains(tags, tag) {
2✔
1169
                        tags = append(tags, tag)
1✔
1170
                }
1✔
1171
        }
1172
        return tags
1✔
1173
}
1174

1175
func newBindingObject(name, namespace string) *v1.ServiceBinding {
1✔
1176
        return &v1.ServiceBinding{
1✔
1177
                TypeMeta: metav1.TypeMeta{
1✔
1178
                        APIVersion: v1.GroupVersion.String(),
1✔
1179
                        Kind:       "ServiceBinding",
1✔
1180
                },
1✔
1181
                ObjectMeta: metav1.ObjectMeta{
1✔
1182
                        Name:      name,
1✔
1183
                        Namespace: namespace,
1✔
1184
                },
1✔
1185
        }
1✔
1186
}
1✔
1187

1188
func serviceInstanceReady(instance *v1.ServiceInstance) bool {
1✔
1189
        return instance.Status.Ready == metav1.ConditionTrue
1✔
1190
}
1✔
1191

1192
func getInstanceNameForSecretCredentials(instance *v1.ServiceInstance) []byte {
1✔
1193
        if useMetaName, ok := instance.Annotations[common.UseInstanceMetadataNameInSecret]; ok && useMetaName == "true" {
2✔
1194
                return []byte(instance.Name)
1✔
1195
        }
1✔
1196
        return []byte(instance.Spec.ExternalName)
1✔
1197
}
1198

1199
func singleKeyMap(credentialsMap map[string][]byte, key string) (map[string][]byte, error) {
1✔
1200
        stringCredentialsMap := make(map[string]string)
1✔
1201
        for k, v := range credentialsMap {
2✔
1202
                stringCredentialsMap[k] = string(v)
1✔
1203
        }
1✔
1204

1205
        credBytes, err := json.Marshal(stringCredentialsMap)
1✔
1206
        if err != nil {
1✔
1207
                return nil, err
×
1208
        }
×
1209

1210
        return map[string][]byte{
1✔
1211
                key: credBytes,
1✔
1212
        }, nil
1✔
1213
}
1214

1215
func truncateString(str string, length int) string {
1✔
1216
        if len(str) > length {
2✔
1217
                return str[:length]
1✔
1218
        }
1✔
1219
        return str
1✔
1220
}
1221

1222
func isBindingExistInSM(smClient sm.Client, instance *v1.ServiceInstance, bindingID string, log logr.Logger) (bool, error) {
1✔
1223
        log.Info("checking if k8s instance status is NotFound")
1✔
1224
        instanceReadyCond := meta.FindStatusCondition(instance.GetConditions(), common.ConditionReady)
1✔
1225
        if instanceReadyCond != nil && instanceReadyCond.Reason == common.ResourceNotFound {
2✔
1226
                log.Info("k8s instance is in NotFound state -> invalid binding")
1✔
1227
                return false, nil
1✔
1228
        }
1✔
1229

1230
        log.Info(fmt.Sprintf("trying to get from SM binding with id %s", bindingID))
1✔
1231
        if _, err := smClient.GetBindingByID(bindingID, nil); err != nil {
1✔
1232
                var smError *sm.ServiceManagerError
×
1233
                if ok := errors.As(err, &smError); ok {
×
1234
                        log.Error(smError, fmt.Sprintf("SM returned status code %d", smError.StatusCode))
×
1235
                        if smError.StatusCode == http.StatusNotFound {
×
1236
                                return false, nil
×
1237
                        }
×
1238
                }
1239
                return false, err
×
1240
        }
1241
        log.Info("binding found in SM")
1✔
1242
        return true, nil
1✔
1243
}
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