• 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

81.44
/controllers/serviceinstance_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
        "fmt"
23
        "net/http"
24
        "strings"
25
        "time"
26

27
        "github.com/SAP/sap-btp-service-operator/internal/utils/logutils"
28
        "github.com/pkg/errors"
29
        corev1 "k8s.io/api/core/v1"
30
        "sigs.k8s.io/controller-runtime/pkg/reconcile"
31

32
        "k8s.io/apimachinery/pkg/types"
33

34
        "sigs.k8s.io/controller-runtime/pkg/predicate"
35

36
        "github.com/SAP/sap-btp-service-operator/api/common"
37
        "github.com/SAP/sap-btp-service-operator/internal/config"
38
        "github.com/SAP/sap-btp-service-operator/internal/utils"
39
        "github.com/go-logr/logr"
40
        "k8s.io/apimachinery/pkg/runtime"
41
        "k8s.io/client-go/tools/events"
42

43
        "k8s.io/client-go/util/workqueue"
44
        "sigs.k8s.io/controller-runtime/pkg/controller"
45

46
        v1 "github.com/SAP/sap-btp-service-operator/api/v1"
47
        "k8s.io/apimachinery/pkg/api/meta"
48

49
        "github.com/google/uuid"
50

51
        "github.com/SAP/sap-btp-service-operator/client/sm"
52
        smClientTypes "github.com/SAP/sap-btp-service-operator/client/sm/types"
53
        apierrors "k8s.io/apimachinery/pkg/api/errors"
54
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
55
        ctrl "sigs.k8s.io/controller-runtime"
56
        "sigs.k8s.io/controller-runtime/pkg/client"
57
        "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
58
)
59

60
// ServiceInstanceReconciler reconciles a ServiceInstance object
61
type ServiceInstanceReconciler struct {
62
        client.Client
63
        Log         logr.Logger
64
        Scheme      *runtime.Scheme
65
        GetSMClient func(ctx context.Context, serviceInstance *v1.ServiceInstance) (sm.Client, error)
66
        Config      config.Config
67
        Recorder    events.EventRecorder
68
        Retries     *utils.RetryStore
69
}
70

71
// +kubebuilder:rbac:groups=services.cloud.sap.com,resources=serviceinstances,verbs=get;list;watch;create;update;patch;delete
72
// +kubebuilder:rbac:groups=services.cloud.sap.com,resources=serviceinstances/status,verbs=get;update;patch
73
// +kubebuilder:rbac:groups=core,resources=events,verbs=get;list;watch;create;update;patch;delete
74
// +kubebuilder:rbac:groups=coordination.k8s.io,resources=leases,verbs=get;list;create;update
75

76
func (r *ServiceInstanceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
1✔
77
        correlationID := uuid.New().String()
1✔
78
        retry := r.Retries.Get(req.NamespacedName)
1✔
79
        if retry != nil {
2✔
80
                correlationID = retry.CorrelationID
1✔
81
        }
1✔
82
        log := r.Log.WithValues("serviceinstance", req.NamespacedName).WithValues("correlation_id", correlationID)
1✔
83
        if retry != nil && time.Now().Before(retry.NextRetry) {
2✔
84
                remaining := time.Until(retry.NextRetry)
1✔
85
                log.Info(fmt.Sprintf("skipping instance reconcile due to backoff. attempts=%d retryIn=%s", retry.Attempts, remaining))
1✔
86

1✔
87
                return ctrl.Result{RequeueAfter: remaining}, nil
1✔
88
        }
1✔
89

90
        ctx = context.WithValue(ctx, logutils.LogKey, log)
1✔
91
        ctx = context.WithValue(ctx, logutils.CorrelationIDKey, correlationID)
1✔
92

1✔
93
        serviceInstance := &v1.ServiceInstance{}
1✔
94
        if err := r.Client.Get(ctx, req.NamespacedName, serviceInstance); err != nil {
2✔
95
                if !apierrors.IsNotFound(err) {
1✔
96
                        log.Error(err, "unable to fetch ServiceInstance")
×
97
                }
×
98
                // we'll ignore not-found errors, since they can't be fixed by an immediate
99
                // requeue (we'll need to wait for a new notification), and we can get them
100
                // on deleted requests.
101
                return ctrl.Result{}, client.IgnoreNotFound(err)
1✔
102
        }
103
        serviceInstance = serviceInstance.DeepCopy()
1✔
104

1✔
105
        log.Info(fmt.Sprintf("*** staring reconcile of ServiceInstance %s/%s ***", serviceInstance.Namespace, serviceInstance.Name))
1✔
106
        smClient, err := r.GetSMClient(ctx, serviceInstance)
1✔
107
        if err != nil {
1✔
108
                log.Error(err, "failed to get sm client")
×
109
                return utils.HandleOperationFailure(ctx, r.Client, serviceInstance, common.Unknown, err)
×
110
        }
×
111
        if len(serviceInstance.Status.OperationURL) > 0 &&
1✔
112
                (serviceInstance.Status.OperationType == smClientTypes.DELETE || !utils.IsMarkedForDeletion(serviceInstance.ObjectMeta)) {
2✔
113
                // ongoing operation - poll status from SM
1✔
114
                return r.poll(ctx, smClient, serviceInstance)
1✔
115
        }
1✔
116

117
        if utils.IsMarkedForDeletion(serviceInstance.ObjectMeta) {
2✔
118
                return r.deleteInstance(ctx, smClient, serviceInstance)
1✔
119
        }
1✔
120

121
        // If stored hash is MD5 (32 chars) and we're now using SHA256 (64 chars),
122
        // perform one-time migration by updating the stored hash without triggering update
123
        if len(serviceInstance.Status.HashedSpec) == 32 {
2✔
124
                // This is likely an MD5->SHA256 migration, update the stored hash silently
1✔
125
                // to prevent unnecessary service updates during FIPS migration
1✔
126
                log.Info(fmt.Sprintf("updated hashing for instance '%s' (id=%s)", serviceInstance.Name, serviceInstance.Status.InstanceID))
1✔
127
                updateHashedSpecValue(serviceInstance)
1✔
128
                return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
129
        }
1✔
130

131
        if len(serviceInstance.Status.InstanceID) > 0 {
2✔
132
                if _, err := smClient.GetInstanceByID(serviceInstance.Status.InstanceID, nil); err != nil {
2✔
133
                        var smError *sm.ServiceManagerError
1✔
134
                        if ok := errors.As(err, &smError); ok {
2✔
135
                                if smError.StatusCode == http.StatusNotFound {
2✔
136
                                        log.Info(fmt.Sprintf("instance %s not found in SM", serviceInstance.Status.InstanceID))
1✔
137
                                        condition := metav1.Condition{
1✔
138
                                                Type:               common.ConditionReady,
1✔
139
                                                Status:             metav1.ConditionFalse,
1✔
140
                                                ObservedGeneration: serviceInstance.Generation,
1✔
141
                                                LastTransitionTime: metav1.NewTime(time.Now()),
1✔
142
                                                Reason:             common.ResourceNotFound,
1✔
143
                                                Message:            fmt.Sprintf(common.ResourceNotFoundMessageFormat, "instance", serviceInstance.Status.InstanceID),
1✔
144
                                        }
1✔
145
                                        serviceInstance.Status.Conditions = []metav1.Condition{condition}
1✔
146
                                        serviceInstance.Status.Ready = metav1.ConditionFalse
1✔
147
                                        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
148
                                }
1✔
149
                        }
150
                        log.Error(err, fmt.Sprintf("failed to get instance %s from SM", serviceInstance.Status.InstanceID))
×
151
                        return ctrl.Result{}, err
×
152
                }
153
        }
154

155
        if len(serviceInstance.GetConditions()) == 0 {
2✔
156
                err := utils.InitConditions(ctx, r.Client, serviceInstance)
1✔
157
                if err != nil {
1✔
158
                        return ctrl.Result{}, err
×
159
                }
×
160
        }
161

162
        if isFinalState(ctx, serviceInstance) {
2✔
163
                return r.maintainFinalState(ctx, serviceInstance)
1✔
164
        }
1✔
165

166
        if controllerutil.AddFinalizer(serviceInstance, common.FinalizerName) {
2✔
167
                log.Info(fmt.Sprintf("added finalizer '%s' to service instance", common.FinalizerName))
1✔
168
                if err := r.Client.Update(ctx, serviceInstance); err != nil {
1✔
169
                        return ctrl.Result{}, err
×
170
                }
×
171
        }
172

173
        if serviceInstance.Status.InstanceID == "" {
2✔
174
                log.Info("Instance ID is empty, checking if instance exist in SM")
1✔
175
                smInstance, err := r.getInstanceForRecovery(ctx, smClient, serviceInstance)
1✔
176
                if err != nil {
1✔
177
                        log.Error(err, "failed to check instance recovery")
×
NEW
178
                        return utils.HandleServiceManagerError(ctx, r.Client, serviceInstance, smClientTypes.CREATE, err, true)
×
179
                }
×
180
                if smInstance != nil {
2✔
181
                        return r.recover(ctx, smClient, serviceInstance, smInstance)
1✔
182
                }
1✔
183

184
                // if instance was not recovered then create new instance
185
                return r.createInstance(ctx, smClient, serviceInstance)
1✔
186
        }
187

188
        if updateRequired(serviceInstance) {
2✔
189
                return r.updateInstance(ctx, smClient, serviceInstance)
1✔
190
        }
1✔
191

192
        // share/unshare
193
        if shareOrUnshareRequired(serviceInstance) {
2✔
194
                return r.handleInstanceSharing(ctx, serviceInstance, smClient)
1✔
195
        }
1✔
196

197
        log.Info("No action required")
1✔
198
        return ctrl.Result{}, nil
1✔
199
}
200

201
func (r *ServiceInstanceReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
202
        return ctrl.NewControllerManagedBy(mgr).
1✔
203
                For(&v1.ServiceInstance{}).
1✔
204
                WithOptions(controller.Options{RateLimiter: workqueue.NewTypedItemExponentialFailureRateLimiter[reconcile.Request](r.Config.RetryBaseDelay, r.Config.RetryMaxDelay)}).
1✔
205
                Complete(r)
1✔
206
}
1✔
207

208
func (r *ServiceInstanceReconciler) createInstance(ctx context.Context, smClient sm.Client, serviceInstance *v1.ServiceInstance) (ctrl.Result, error) {
1✔
209
        log := logutils.GetLogger(ctx)
1✔
210
        log.Info("Creating instance in SM")
1✔
211
        updateHashedSpecValue(serviceInstance)
1✔
212
        instanceParameters, err := r.buildSMRequestParameters(ctx, serviceInstance)
1✔
213
        if err != nil {
2✔
214
                // if parameters are invalid there is nothing we can do, the user should fix it according to the error message in the condition
1✔
215
                log.Error(err, "failed to parse instance parameters")
1✔
216
                return utils.HandleOperationFailure(ctx, r.Client, serviceInstance, smClientTypes.CREATE, err)
1✔
217
        }
1✔
218

219
        provision, provisionErr := smClient.Provision(&smClientTypes.ServiceInstance{
1✔
220
                Name:          serviceInstance.Spec.ExternalName,
1✔
221
                ServicePlanID: serviceInstance.Spec.ServicePlanID,
1✔
222
                Parameters:    instanceParameters,
1✔
223
                Labels: smClientTypes.Labels{
1✔
224
                        common.NamespaceLabel: []string{serviceInstance.Namespace},
1✔
225
                        common.K8sNameLabel:   []string{serviceInstance.Name},
1✔
226
                        common.ClusterIDLabel: []string{r.Config.ClusterID},
1✔
227
                },
1✔
228
        }, serviceInstance.Spec.ServiceOfferingName, serviceInstance.Spec.ServicePlanName, nil, utils.BuildUserInfo(ctx, serviceInstance.Spec.UserInfo), serviceInstance.Spec.DataCenter)
1✔
229

1✔
230
        if provisionErr != nil {
2✔
231
                log.Error(provisionErr, "failed to create service instance", "serviceOfferingName", serviceInstance.Spec.ServiceOfferingName,
1✔
232
                        "servicePlanName", serviceInstance.Spec.ServicePlanName)
1✔
233
                return utils.HandleServiceManagerError(ctx, r.Client, serviceInstance, smClientTypes.CREATE, provisionErr, true)
1✔
234
        }
1✔
235

236
        serviceInstance.Status.InstanceID = provision.InstanceID
1✔
237
        serviceInstance.Status.SubaccountID = provision.SubaccountID
1✔
238
        if len(provision.Tags) > 0 {
2✔
239
                tags, err := getTags(provision.Tags)
1✔
240
                if err != nil {
1✔
241
                        log.Error(err, "failed to unmarshal tags")
×
242
                } else {
1✔
243
                        serviceInstance.Status.Tags = tags
1✔
244
                }
1✔
245
        }
246

247
        if provision.Location != "" {
2✔
248
                log.Info("Provision request is in progress (async)")
1✔
249
                serviceInstance.Status.OperationURL = provision.Location
1✔
250
                serviceInstance.Status.OperationType = smClientTypes.CREATE
1✔
251
                utils.SetInProgressConditions(ctx, smClientTypes.CREATE, "", serviceInstance, false)
1✔
252

1✔
253
                return ctrl.Result{RequeueAfter: r.Config.PollInterval}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
254
        }
1✔
255

256
        log.Info(fmt.Sprintf("Instance provisioned successfully, instanceID: %s, subaccountID: %s", serviceInstance.Status.InstanceID,
1✔
257
                serviceInstance.Status.SubaccountID))
1✔
258
        r.Retries.Reset(types.NamespacedName{Name: serviceInstance.Name, Namespace: serviceInstance.Namespace})
1✔
259
        utils.SetSuccessConditions(smClientTypes.CREATE, serviceInstance, false)
1✔
260
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
261
}
262

263
func (r *ServiceInstanceReconciler) updateInstance(ctx context.Context, smClient sm.Client, serviceInstance *v1.ServiceInstance) (ctrl.Result, error) {
1✔
264
        log := logutils.GetLogger(ctx)
1✔
265
        log.Info(fmt.Sprintf("updating instance %s in SM", serviceInstance.Status.InstanceID))
1✔
266

1✔
267
        instanceParameters, err := r.buildSMRequestParameters(ctx, serviceInstance)
1✔
268
        if err != nil {
2✔
269
                log.Error(err, "failed to parse instance parameters")
1✔
270
                return utils.HandleOperationFailure(ctx, r.Client, serviceInstance, smClientTypes.UPDATE, err)
1✔
271
        }
1✔
272

273
        updateHashedSpecValue(serviceInstance)
1✔
274
        _, operationURL, err := smClient.UpdateInstance(serviceInstance.Status.InstanceID, &smClientTypes.ServiceInstance{
1✔
275
                Name:          serviceInstance.Spec.ExternalName,
1✔
276
                ServicePlanID: serviceInstance.Spec.ServicePlanID,
1✔
277
                Parameters:    instanceParameters,
1✔
278
        }, serviceInstance.Spec.ServiceOfferingName, serviceInstance.Spec.ServicePlanName, nil, utils.BuildUserInfo(ctx, serviceInstance.Spec.UserInfo), serviceInstance.Spec.DataCenter)
1✔
279

1✔
280
        if err != nil {
2✔
281
                log.Error(err, fmt.Sprintf("failed to update service instance with ID %s", serviceInstance.Status.InstanceID))
1✔
282
                return utils.HandleServiceManagerError(ctx, r.Client, serviceInstance, smClientTypes.UPDATE, err, true)
1✔
283
        }
1✔
284

285
        if operationURL != "" {
2✔
286
                log.Info(fmt.Sprintf("Update request accepted, operation URL: %s", operationURL))
1✔
287
                serviceInstance.Status.OperationURL = operationURL
1✔
288
                serviceInstance.Status.OperationType = smClientTypes.UPDATE
1✔
289
                utils.SetInProgressConditions(ctx, smClientTypes.UPDATE, "", serviceInstance, false)
1✔
290
                serviceInstance.Status.ForceReconcile = false
1✔
291
                if err := utils.UpdateStatus(ctx, r.Client, serviceInstance); err != nil {
2✔
292
                        return ctrl.Result{}, err
1✔
293
                }
1✔
294

295
                return ctrl.Result{RequeueAfter: r.Config.PollInterval}, nil
1✔
296
        }
297
        log.Info("Instance updated successfully")
1✔
298
        utils.SetSuccessConditions(smClientTypes.UPDATE, serviceInstance, false)
1✔
299
        serviceInstance.Status.ForceReconcile = false
1✔
300
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
301
}
302

303
func (r *ServiceInstanceReconciler) deleteInstance(ctx context.Context, smClient sm.Client, serviceInstance *v1.ServiceInstance) (ctrl.Result, error) {
1✔
304
        log := logutils.GetLogger(ctx)
1✔
305

1✔
306
        if controllerutil.ContainsFinalizer(serviceInstance, common.FinalizerName) {
2✔
307
                log.Info("instance has finalizer, deleting it from sm")
1✔
308
                if len(serviceInstance.Status.InstanceID) == 0 {
2✔
309
                        log.Info("No instance id found validating instance does not exists in SM before removing finalizer")
1✔
310
                        smInstance, err := r.getInstanceForRecovery(ctx, smClient, serviceInstance)
1✔
311
                        if err != nil {
1✔
NEW
312
                                return utils.HandleServiceManagerError(ctx, r.Client, serviceInstance, smClientTypes.DELETE, err, true)
×
313
                        }
×
314
                        if smInstance != nil {
2✔
315
                                log.Info("instance exists in SM continue with deletion")
1✔
316
                                serviceInstance.Status.InstanceID = smInstance.ID
1✔
317
                                utils.SetInProgressConditions(ctx, smClientTypes.DELETE, "delete after recovery", serviceInstance, false)
1✔
318
                                return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
319
                        }
1✔
320
                        log.Info("instance does not exists in SM, removing finalizer")
1✔
321
                        return ctrl.Result{}, utils.RemoveFinalizer(ctx, r.Client, serviceInstance, common.FinalizerName)
1✔
322
                }
323

324
                log.Info(fmt.Sprintf("Deleting instance with id %v from SM", serviceInstance.Status.InstanceID))
1✔
325
                operationURL, deprovisionErr := smClient.Deprovision(serviceInstance.Status.InstanceID, nil, utils.BuildUserInfo(ctx, serviceInstance.Spec.UserInfo))
1✔
326
                if deprovisionErr != nil {
2✔
327
                        return utils.HandleServiceManagerError(ctx, r.Client, serviceInstance, smClientTypes.DELETE, deprovisionErr, true)
1✔
328
                }
1✔
329

330
                if operationURL != "" {
2✔
331
                        log.Info("Deleting instance async")
1✔
332
                        return r.handleAsyncDelete(ctx, serviceInstance, operationURL)
1✔
333
                }
1✔
334

335
                for key, secretName := range serviceInstance.Labels {
2✔
336
                        if strings.HasPrefix(key, common.InstanceSecretRefLabel) {
2✔
337
                                if err := utils.RemoveWatchForSecret(ctx, r.Client, types.NamespacedName{Name: secretName, Namespace: serviceInstance.Namespace}, string(serviceInstance.UID)); err != nil {
1✔
338
                                        log.Error(err, fmt.Sprintf("failed to unwatch secret %s", secretName))
×
339
                                        return ctrl.Result{}, err
×
340
                                }
×
341
                        }
342
                }
343

344
                serviceInstance.Status.InstanceID = ""
1✔
345
                if err := r.Client.Status().Update(ctx, serviceInstance); err != nil {
1✔
UNCOV
346
                        log.Error(err, "failed to update service instance status after deletion")
×
347
                        return ctrl.Result{}, err
×
348
                }
×
349
                log.Info("Instance was deleted successfully, removing finalizer")
1✔
350
                // remove our finalizer from the list and update it.
1✔
351
                return ctrl.Result{}, utils.RemoveFinalizer(ctx, r.Client, serviceInstance, common.FinalizerName)
1✔
352
        }
353
        return ctrl.Result{}, nil
1✔
354
}
355

356
func (r *ServiceInstanceReconciler) handleInstanceSharing(ctx context.Context, serviceInstance *v1.ServiceInstance, smClient sm.Client) (ctrl.Result, error) {
1✔
357
        log := logutils.GetLogger(ctx)
1✔
358
        log.Info("Handling change in instance sharing")
1✔
359

1✔
360
        if serviceInstance.GetShared() {
2✔
361
                log.Info("Service instance appears to be unshared, sharing the instance")
1✔
362
                err := smClient.ShareInstance(serviceInstance.Status.InstanceID, utils.BuildUserInfo(ctx, serviceInstance.Spec.UserInfo))
1✔
363
                if err != nil {
2✔
364
                        log.Error(err, "failed to share instance")
1✔
365
                        return utils.HandleInstanceSharingError(ctx, r.Client, serviceInstance, metav1.ConditionFalse, common.ShareFailed, err)
1✔
366
                }
1✔
367
                log.Info("instance shared successfully")
1✔
368
                utils.SetSharedCondition(serviceInstance, metav1.ConditionTrue, common.ShareSucceeded, "instance shared successfully")
1✔
369
        } else { //un-share
1✔
370
                log.Info("Service instance appears to be shared, un-sharing the instance")
1✔
371
                err := smClient.UnShareInstance(serviceInstance.Status.InstanceID, utils.BuildUserInfo(ctx, serviceInstance.Spec.UserInfo))
1✔
372
                if err != nil {
2✔
373
                        log.Error(err, "failed to un-share instance")
1✔
374
                        return utils.HandleInstanceSharingError(ctx, r.Client, serviceInstance, metav1.ConditionTrue, common.UnShareFailed, err)
1✔
375
                }
1✔
376
                log.Info("instance un-shared successfully")
1✔
377
                if serviceInstance.Spec.Shared != nil {
2✔
378
                        utils.SetSharedCondition(serviceInstance, metav1.ConditionFalse, common.UnShareSucceeded, "instance un-shared successfully")
1✔
379
                } else {
2✔
380
                        log.Info("removing Shared condition since shared is undefined in instance")
1✔
381
                        conditions := serviceInstance.GetConditions()
1✔
382
                        meta.RemoveStatusCondition(&conditions, common.ConditionShared)
1✔
383
                        serviceInstance.SetConditions(conditions)
1✔
384
                }
1✔
385
        }
386

387
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
388
}
389

390
func (r *ServiceInstanceReconciler) poll(ctx context.Context, smClient sm.Client, serviceInstance *v1.ServiceInstance) (ctrl.Result, error) {
1✔
391
        log := logutils.GetLogger(ctx)
1✔
392
        log.Info(fmt.Sprintf("instance %s is '%s' in progress, polling operation %s", serviceInstance.Status.InstanceID, serviceInstance.Status.OperationType, serviceInstance.Status.OperationURL))
1✔
393
        status, statusErr := smClient.Status(serviceInstance.Status.OperationURL, nil)
1✔
394
        if statusErr != nil {
1✔
395
                log.Info(fmt.Sprintf("failed to fetch operation, got error from SM: %s", statusErr.Error()), "operationURL", serviceInstance.Status.OperationURL)
×
396
                utils.SetInProgressConditions(ctx, serviceInstance.Status.OperationType, string(smClientTypes.INPROGRESS), serviceInstance, false)
×
397
                // if failed to read operation status we cleanup the status to trigger re-sync from SM
×
398
                freshStatus := v1.ServiceInstanceStatus{Conditions: serviceInstance.GetConditions()}
×
399
                if utils.IsMarkedForDeletion(serviceInstance.ObjectMeta) {
×
400
                        freshStatus.InstanceID = serviceInstance.Status.InstanceID
×
401
                }
×
402
                serviceInstance.Status = freshStatus
×
403
                if err := utils.UpdateStatus(ctx, r.Client, serviceInstance); err != nil {
×
404
                        log.Error(err, "failed to update status during polling")
×
405
                }
×
406
                return ctrl.Result{}, statusErr
×
407
        }
408

409
        if status == nil {
1✔
410
                log.Error(fmt.Errorf("last operation is nil"), fmt.Sprintf("polling %s returned nil", serviceInstance.Status.OperationURL))
×
411
                return ctrl.Result{}, fmt.Errorf("last operation is nil")
×
412
        }
×
413
        switch status.State {
1✔
414
        case smClientTypes.INPROGRESS:
1✔
415
                fallthrough
1✔
416
        case smClientTypes.PENDING:
1✔
417
                log.Info(fmt.Sprintf("operation %s %s is still in progress", serviceInstance.Status.OperationType, serviceInstance.Status.OperationURL))
1✔
418
                if len(status.Description) > 0 {
1✔
419
                        log.Info(fmt.Sprintf("last operation description is '%s'", status.Description))
×
420
                        utils.SetInProgressConditions(ctx, status.Type, status.Description, serviceInstance, true)
×
421
                        if err := utils.UpdateStatus(ctx, r.Client, serviceInstance); err != nil {
×
422
                                log.Error(err, "unable to update ServiceInstance polling description")
×
423
                                return ctrl.Result{}, err
×
424
                        }
×
425
                }
426
                return ctrl.Result{RequeueAfter: r.Config.PollInterval}, nil
1✔
427
        case smClientTypes.FAILED:
1✔
428
                errMsg := getErrorMsgFromLastOperation(status)
1✔
429
                log.Info(fmt.Sprintf("operation %s %s failed, error: %s", serviceInstance.Status.OperationType, serviceInstance.Status.OperationURL, errMsg))
1✔
430
                utils.SetFailureConditions(status.Type, errMsg, serviceInstance, true)
1✔
431
                if serviceInstance.Status.OperationType == smClientTypes.CREATE ||
1✔
432
                        (serviceInstance.Status.OperationType == smClientTypes.DELETE && !utils.IsMarkedForDeletion(serviceInstance.ObjectMeta)) {
2✔
433
                        log.Info(fmt.Sprintf("async provision failed for instance %s", serviceInstance.Status.InstanceID))
1✔
434
                        key := types.NamespacedName{Namespace: serviceInstance.GetNamespace(), Name: serviceInstance.GetName()}
1✔
435
                        newState := r.Retries.RegisterFailure(key, logutils.GetCorrelationID(ctx))
1✔
436
                        log.Info(fmt.Sprintf("async provision failed. attempts=%d nextRetry=%s currrent error=%s\n", newState.Attempts, newState.NextRetry.Format(time.RFC3339), errMsg))
1✔
437
                        return r.handleFailedAsyncProvision(ctx, smClient, serviceInstance)
1✔
438
                }
1✔
439
                serviceInstance.Status.OperationURL = ""
1✔
440
                serviceInstance.Status.OperationType = ""
1✔
441
                if err := utils.UpdateStatus(ctx, r.Client, serviceInstance); err != nil {
1✔
UNCOV
442
                        return ctrl.Result{}, err
×
UNCOV
443
                }
×
444
                return ctrl.Result{}, errors.New(errMsg)
1✔
445
        case smClientTypes.SUCCEEDED:
1✔
446
                log.Info(fmt.Sprintf("operation %s %s completed succefully", serviceInstance.Status.OperationType, serviceInstance.Status.OperationURL))
1✔
447
                if serviceInstance.Status.OperationType == smClientTypes.CREATE {
2✔
448
                        smInstance, err := smClient.GetInstanceByID(serviceInstance.Status.InstanceID, nil)
1✔
449
                        if err != nil {
1✔
450
                                log.Error(err, fmt.Sprintf("instance %s succeeded but could not fetch it from SM", serviceInstance.Status.InstanceID))
×
451
                                return ctrl.Result{}, err
×
452
                        }
×
453
                        if len(smInstance.Labels["subaccount_id"]) > 0 {
2✔
454
                                serviceInstance.Status.SubaccountID = smInstance.Labels["subaccount_id"][0]
1✔
455
                        }
1✔
456
                        serviceInstance.Status.Ready = metav1.ConditionTrue
1✔
457
                } else if serviceInstance.Status.OperationType == smClientTypes.DELETE {
2✔
458
                        log.Info(fmt.Sprintf("instance %s deleted successfully from sm, removing finalizer", serviceInstance.Status.InstanceID))
1✔
459
                        if err := utils.RemoveFinalizer(ctx, r.Client, serviceInstance, common.FinalizerName); err != nil {
1✔
460
                                return ctrl.Result{}, err
×
461
                        }
×
462
                        serviceInstance.Status.InstanceID = ""
1✔
463
                }
464
                if serviceInstance.Status.OperationType != smClientTypes.DELETE {
2✔
465
                        utils.SetSuccessConditions(status.Type, serviceInstance, true)
1✔
466
                }
1✔
467
        }
468

469
        serviceInstance.Status.OperationURL = ""
1✔
470
        serviceInstance.Status.OperationType = ""
1✔
471
        r.Retries.Reset(types.NamespacedName{Name: serviceInstance.Name, Namespace: serviceInstance.Namespace})
1✔
472

1✔
473
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
474
}
475

476
func (r *ServiceInstanceReconciler) handleFailedAsyncProvision(ctx context.Context, smClient sm.Client, serviceInstance *v1.ServiceInstance) (ctrl.Result, error) {
1✔
477
        log := logutils.GetLogger(ctx)
1✔
478
        log.Info(fmt.Sprintf("handleFailedAsyncProvision deleting instance that failed to be provisioned with id %s from SM", serviceInstance.Status.InstanceID))
1✔
479
        operationURL, deprovisionErr := smClient.Deprovision(serviceInstance.Status.InstanceID, nil, utils.BuildUserInfo(ctx, serviceInstance.Spec.UserInfo))
1✔
480
        if deprovisionErr != nil {
1✔
NEW
481
                log.Error(deprovisionErr, fmt.Sprintf("handleFailedAsyncProvision failed to deprovision instance: %s", serviceInstance.Status.InstanceID))
×
NEW
482
                return utils.HandleServiceManagerError(ctx, r.Client, serviceInstance, smClientTypes.DELETE, deprovisionErr, false)
×
NEW
483
        }
×
484

485
        if operationURL != "" {
1✔
NEW
486
                log.Info(fmt.Sprintf("handleFailedAsyncProvision deletion is async, operation url %s", operationURL))
×
NEW
487
                serviceInstance.Status.OperationURL = operationURL
×
NEW
488
                serviceInstance.Status.OperationType = smClientTypes.DELETE
×
NEW
489
                return ctrl.Result{RequeueAfter: r.Config.PollInterval}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
×
NEW
490
        }
×
491

492
        log.Info(fmt.Sprintf("handleFailedAsyncProvision instance %s deleted successfully", serviceInstance.Status.InstanceID))
1✔
493
        serviceInstance.Status.OperationURL = ""
1✔
494
        serviceInstance.Status.OperationType = ""
1✔
495
        serviceInstance.Status.InstanceID = ""
1✔
496
        if err := r.Client.Status().Update(ctx, serviceInstance); err != nil {
1✔
NEW
497
                log.Error(err, "handleFailedAsyncProvision failed to update service instance status after deletion")
×
NEW
498
                return ctrl.Result{}, err
×
NEW
499
        }
×
500
        return ctrl.Result{RequeueAfter: r.Config.PollInterval}, nil
1✔
501
}
502

503
func (r *ServiceInstanceReconciler) handleAsyncDelete(ctx context.Context, serviceInstance *v1.ServiceInstance, opURL string) (ctrl.Result, error) {
1✔
504
        serviceInstance.Status.OperationURL = opURL
1✔
505
        serviceInstance.Status.OperationType = smClientTypes.DELETE
1✔
506
        utils.SetInProgressConditions(ctx, smClientTypes.DELETE, "", serviceInstance, false)
1✔
507

1✔
508
        return ctrl.Result{RequeueAfter: r.Config.PollInterval}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
509
}
1✔
510

511
func (r *ServiceInstanceReconciler) getInstanceForRecovery(ctx context.Context, smClient sm.Client, serviceInstance *v1.ServiceInstance) (*smClientTypes.ServiceInstance, error) {
1✔
512
        log := logutils.GetLogger(ctx)
1✔
513
        parameters := sm.Parameters{
1✔
514
                FieldQuery: []string{
1✔
515
                        fmt.Sprintf("name eq '%s'", serviceInstance.Spec.ExternalName),
1✔
516
                        fmt.Sprintf("context/clusterid eq '%s'", r.Config.ClusterID),
1✔
517
                        fmt.Sprintf("context/namespace eq '%s'", serviceInstance.Namespace)},
1✔
518
                LabelQuery: []string{
1✔
519
                        fmt.Sprintf("%s eq '%s'", common.K8sNameLabel, serviceInstance.Name)},
1✔
520
                GeneralParams: []string{"attach_last_operations=true"},
1✔
521
        }
1✔
522

1✔
523
        instances, err := smClient.ListInstances(&parameters)
1✔
524
        if err != nil {
1✔
525
                log.Error(err, "failed to list instances in SM")
×
526
                return nil, err
×
527
        }
×
528

529
        if instances != nil && len(instances.ServiceInstances) > 0 {
2✔
530
                return &instances.ServiceInstances[0], nil
1✔
531
        }
1✔
532
        log.Info("instance not found in SM")
1✔
533
        return nil, nil
1✔
534
}
535

536
func (r *ServiceInstanceReconciler) recover(ctx context.Context, smClient sm.Client, k8sInstance *v1.ServiceInstance, smInstance *smClientTypes.ServiceInstance) (ctrl.Result, error) {
1✔
537
        log := logutils.GetLogger(ctx)
1✔
538

1✔
539
        log.Info(fmt.Sprintf("found existing instance in SM with id %s, updating status", smInstance.ID))
1✔
540
        updateHashedSpecValue(k8sInstance)
1✔
541
        if smInstance.Ready {
2✔
542
                k8sInstance.Status.Ready = metav1.ConditionTrue
1✔
543
        }
1✔
544
        if smInstance.Shared {
1✔
545
                utils.SetSharedCondition(k8sInstance, metav1.ConditionTrue, common.ShareSucceeded, "Instance shared successfully")
×
546
        }
×
547
        k8sInstance.Status.InstanceID = smInstance.ID
1✔
548
        k8sInstance.Status.OperationURL = ""
1✔
549
        k8sInstance.Status.OperationType = ""
1✔
550
        tags, err := getOfferingTags(smClient, smInstance.ServicePlanID)
1✔
551
        if err != nil {
2✔
552
                log.Error(err, "could not recover offering tags")
1✔
553
        }
1✔
554
        if len(tags) > 0 {
1✔
555
                k8sInstance.Status.Tags = tags
×
556
        }
×
557

558
        instanceState := smClientTypes.SUCCEEDED
1✔
559
        operationType := smClientTypes.CREATE
1✔
560
        description := ""
1✔
561
        if smInstance.LastOperation != nil {
2✔
562
                instanceState = smInstance.LastOperation.State
1✔
563
                operationType = smInstance.LastOperation.Type
1✔
564
                description = smInstance.LastOperation.Description
1✔
565
        } else if !smInstance.Ready {
3✔
566
                instanceState = smClientTypes.FAILED
1✔
567
        }
1✔
568

569
        switch instanceState {
1✔
570
        case smClientTypes.PENDING:
1✔
571
                fallthrough
1✔
572
        case smClientTypes.INPROGRESS:
1✔
573
                k8sInstance.Status.OperationURL = sm.BuildOperationURL(smInstance.LastOperation.ID, smInstance.ID, smClientTypes.ServiceInstancesURL)
1✔
574
                k8sInstance.Status.OperationType = smInstance.LastOperation.Type
1✔
575
                k8sInstance.Status.InstanceID = smInstance.ID
1✔
576
                utils.SetInProgressConditions(ctx, smInstance.LastOperation.Type, smInstance.LastOperation.Description, k8sInstance, false)
1✔
577
        case smClientTypes.SUCCEEDED:
1✔
578
                utils.SetSuccessConditions(operationType, k8sInstance, false)
1✔
579
        case smClientTypes.FAILED:
1✔
580
                utils.SetFailureConditions(operationType, description, k8sInstance, false)
1✔
581
                //if operationType == smClientTypes.CREATE {
582
                //        trueVal := true
583
                //        k8sInstance.Status.AsyncProvisionFailed = &trueVal
584
                //}
585
        }
586

587
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, k8sInstance)
1✔
588
}
589

590
func (r *ServiceInstanceReconciler) buildSMRequestParameters(ctx context.Context, serviceInstance *v1.ServiceInstance) ([]byte, error) {
1✔
591
        log := logutils.GetLogger(ctx)
1✔
592
        instanceParameters, paramSecrets, err := utils.BuildSMRequestParameters(serviceInstance.Namespace, serviceInstance.Spec.Parameters, serviceInstance.Spec.ParametersFrom)
1✔
593
        if err != nil {
2✔
594
                log.Error(err, "failed to build instance parameters")
1✔
595
                return nil, err
1✔
596
        }
1✔
597
        instanceLabelsChanged := false
1✔
598
        newInstanceLabels := make(map[string]string)
1✔
599
        if serviceInstance.IsSubscribedToParamSecretsChanges() {
2✔
600
                // find all new secrets on the instance
1✔
601
                for _, secret := range paramSecrets {
2✔
602
                        labelKey := utils.GetLabelKeyForInstanceSecret(secret.Name)
1✔
603
                        newInstanceLabels[labelKey] = secret.Name
1✔
604
                        if _, ok := serviceInstance.Labels[labelKey]; !ok {
2✔
605
                                instanceLabelsChanged = true
1✔
606
                        }
1✔
607

608
                        if err := utils.AddWatchForSecretIfNeeded(ctx, r.Client, secret, string(serviceInstance.UID)); err != nil {
1✔
609
                                log.Error(err, fmt.Sprintf("failed to mark secret for watch %s", secret.Name))
×
610
                                return nil, err
×
611
                        }
×
612
                }
613
        }
614

615
        //sync instance labels
616
        for labelKey, labelValue := range serviceInstance.Labels {
2✔
617
                if strings.HasPrefix(labelKey, common.InstanceSecretRefLabel) {
2✔
618
                        if _, ok := newInstanceLabels[labelKey]; !ok {
2✔
619
                                log.Info(fmt.Sprintf("params secret named %s was removed, unwatching it", labelValue))
1✔
620
                                instanceLabelsChanged = true
1✔
621
                                if err := utils.RemoveWatchForSecret(ctx, r.Client, types.NamespacedName{Name: labelValue, Namespace: serviceInstance.Namespace}, string(serviceInstance.UID)); err != nil {
1✔
622
                                        log.Error(err, fmt.Sprintf("failed to unwatch secret %s", labelValue))
×
623
                                        return nil, err
×
624
                                }
×
625
                        }
626
                } else {
×
627
                        // this label not related to secrets, add it
×
628
                        newInstanceLabels[labelKey] = labelValue
×
629
                }
×
630
        }
631
        if instanceLabelsChanged {
2✔
632
                serviceInstance.Labels = newInstanceLabels
1✔
633
                log.Info("updating instance with secret labels")
1✔
634
                return instanceParameters, r.Client.Update(ctx, serviceInstance)
1✔
635
        }
1✔
636

637
        return instanceParameters, nil
1✔
638
}
639

640
func (r *ServiceInstanceReconciler) maintainFinalState(ctx context.Context, serviceInstance *v1.ServiceInstance) (ctrl.Result, error) {
1✔
641
        log := logutils.GetLogger(ctx)
1✔
642

1✔
643
        if serviceInstance.IsSubscribedToParamSecretsChanges() {
2✔
644
                log.Info("instance is in final state, WatchParametersFromChanges is true, validating that all parameters secrets are watched")
1✔
645
                for _, param := range serviceInstance.Spec.ParametersFrom {
2✔
646
                        if param.SecretKeyRef != nil {
2✔
647
                                secret := &corev1.Secret{}
1✔
648
                                if err := utils.GetSecretWithFallback(ctx, types.NamespacedName{Name: param.SecretKeyRef.Name, Namespace: serviceInstance.Namespace}, secret); err != nil {
1✔
649
                                        log.Error(err, fmt.Sprintf("failed to get secret %s", param.SecretKeyRef.Name))
×
650
                                        return ctrl.Result{}, err
×
651
                                }
×
652
                                if err := utils.AddWatchForSecretIfNeeded(ctx, r.Client, secret, string(serviceInstance.UID)); err != nil {
1✔
653
                                        log.Error(err, fmt.Sprintf("failed to mark secret for watch %s", param.SecretKeyRef.Name))
×
654
                                        return ctrl.Result{}, err
×
655
                                }
×
656
                        }
657
                }
658
        }
659

660
        if len(serviceInstance.Status.HashedSpec) == 0 {
2✔
661
                log.Info("instance is missing HashedSpec value, updating it")
1✔
662
                updateHashedSpecValue(serviceInstance)
1✔
663
                return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
664
        }
1✔
665
        return ctrl.Result{}, nil
1✔
666
}
667

668
func isFinalState(ctx context.Context, serviceInstance *v1.ServiceInstance) bool {
1✔
669
        log := logutils.GetLogger(ctx)
1✔
670

1✔
671
        if !serviceInstanceReady(serviceInstance) {
2✔
672
                return false
1✔
673
        }
1✔
674

675
        if serviceInstance.Status.ForceReconcile {
2✔
676
                log.Info("instance is not in final state, ForceReconcile is true")
1✔
677
                return false
1✔
678
        }
1✔
679

680
        observedGen := common.GetObservedGeneration(serviceInstance)
1✔
681
        if serviceInstance.Generation != observedGen {
2✔
682
                log.Info(fmt.Sprintf("instance is not in final state, generation: %d, observedGen: %d", serviceInstance.Generation, observedGen))
1✔
683
                return false
1✔
684
        }
1✔
685

686
        if shareOrUnshareRequired(serviceInstance) {
2✔
687
                log.Info("instance is not in final state, need to sync sharing status")
1✔
688
                if len(serviceInstance.Status.HashedSpec) == 0 {
1✔
689
                        updateHashedSpecValue(serviceInstance)
×
690
                }
×
691
                return false
1✔
692
        }
693

694
        log.Info(fmt.Sprintf("instance is in final state (generation: %d)", serviceInstance.Generation))
1✔
695
        return true
1✔
696
}
697

698
func updateRequired(serviceInstance *v1.ServiceInstance) bool {
1✔
699
        //update is not supported for failed instances (this can occur when instance creation was asynchronously)
1✔
700
        if serviceInstance.Status.Ready != metav1.ConditionTrue {
2✔
701
                return false
1✔
702
        }
1✔
703

704
        if serviceInstance.Status.ForceReconcile {
2✔
705
                return true
1✔
706
        }
1✔
707

708
        cond := meta.FindStatusCondition(serviceInstance.Status.Conditions, common.ConditionSucceeded)
1✔
709
        if cond != nil && cond.Reason == common.UpdateInProgress { //in case of transient error occurred
1✔
710
                return true
×
711
        }
×
712

713
        return serviceInstance.GetSpecHash() != serviceInstance.Status.HashedSpec
1✔
714
}
715

716
func shareOrUnshareRequired(serviceInstance *v1.ServiceInstance) bool {
1✔
717
        //relevant only for non-shared instances - sharing instance is possible only for usable instances
1✔
718
        if serviceInstance.Status.Ready != metav1.ConditionTrue {
2✔
719
                return false
1✔
720
        }
1✔
721

722
        sharedCondition := meta.FindStatusCondition(serviceInstance.GetConditions(), common.ConditionShared)
1✔
723
        if sharedCondition == nil {
2✔
724
                return serviceInstance.GetShared()
1✔
725
        }
1✔
726

727
        if sharedCondition.Reason == common.ShareNotSupported {
2✔
728
                return false
1✔
729
        }
1✔
730

731
        if sharedCondition.Status == metav1.ConditionFalse {
2✔
732
                // instance does not appear to be shared, should share it if shared is requested
1✔
733
                return serviceInstance.GetShared()
1✔
734
        }
1✔
735

736
        // instance appears to be shared, should unshare it if shared is not requested
737
        return !serviceInstance.GetShared()
1✔
738
}
739

740
func getOfferingTags(smClient sm.Client, planID string) ([]string, error) {
1✔
741
        planQuery := &sm.Parameters{
1✔
742
                FieldQuery: []string{fmt.Sprintf("id eq '%s'", planID)},
1✔
743
        }
1✔
744
        plans, err := smClient.ListPlans(planQuery)
1✔
745
        if err != nil {
1✔
746
                return nil, err
×
747
        }
×
748

749
        if plans == nil || len(plans.ServicePlans) != 1 {
2✔
750
                return nil, fmt.Errorf("could not find plan with id %s", planID)
1✔
751
        }
1✔
752

753
        offeringQuery := &sm.Parameters{
×
754
                FieldQuery: []string{fmt.Sprintf("id eq '%s'", plans.ServicePlans[0].ServiceOfferingID)},
×
755
        }
×
756

×
757
        offerings, err := smClient.ListOfferings(offeringQuery)
×
758
        if err != nil {
×
759
                return nil, err
×
760
        }
×
761
        if offerings == nil || len(offerings.ServiceOfferings) != 1 {
×
762
                return nil, fmt.Errorf("could not find offering with id %s", plans.ServicePlans[0].ServiceOfferingID)
×
763
        }
×
764

765
        var tags []string
×
766
        if err := json.Unmarshal(offerings.ServiceOfferings[0].Tags, &tags); err != nil {
×
767
                return nil, err
×
768
        }
×
769
        return tags, nil
×
770
}
771

772
func getTags(tags []byte) ([]string, error) {
1✔
773
        var tagsArr []string
1✔
774
        if err := json.Unmarshal(tags, &tagsArr); err != nil {
1✔
775
                return nil, err
×
776
        }
×
777
        return tagsArr, nil
1✔
778
}
779

780
func updateHashedSpecValue(serviceInstance *v1.ServiceInstance) {
1✔
781
        serviceInstance.Status.HashedSpec = serviceInstance.GetSpecHash()
1✔
782
}
1✔
783

784
func getErrorMsgFromLastOperation(status *smClientTypes.Operation) string {
1✔
785
        errMsg := "async operation error"
1✔
786
        if status == nil || len(status.Errors) == 0 {
2✔
787
                return errMsg
1✔
788
        }
1✔
789
        var errMap map[string]interface{}
1✔
790

1✔
791
        if err := json.Unmarshal(status.Errors, &errMap); err != nil {
1✔
792
                return errMsg
×
793
        }
×
794

795
        if description, found := errMap["description"]; found {
2✔
796
                if descStr, ok := description.(string); ok {
2✔
797
                        errMsg = descStr
1✔
798
                }
1✔
799
        }
800
        return errMsg
1✔
801
}
802

803
type SecretPredicate struct {
804
        predicate.Funcs
805
}
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