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

SAP / sap-btp-service-operator / 26086763542

19 May 2026 08:51AM UTC coverage: 77.787%. First build
26086763542

Pull #635

github

kerenlahav
fix
Pull Request #635: Async retry with backoff

117 of 151 new or added lines in 4 files covered. (77.48%)

2889 of 3714 relevant lines covered (77.79%)

0.88 hits per line

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

81.28
/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
        log := r.Log.WithValues("serviceinstance", req.NamespacedName).WithValues("correlation_id", uuid.New().String())
1✔
78

1✔
79
        retry := r.Retries.Get(req.NamespacedName)
1✔
80
        if retry != nil && time.Now().Before(retry.NextRetry) {
2✔
81
                remaining := time.Until(retry.NextRetry)
1✔
82
                log.Info(fmt.Sprintf("skipping instance reconcile due to backoff. attempts=%d retryIn=%s", retry.Attempts, remaining))
1✔
83

1✔
84
                return ctrl.Result{RequeueAfter: remaining}, nil
1✔
85
        }
1✔
86

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

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

112
        if utils.IsMarkedForDeletion(serviceInstance.ObjectMeta) {
2✔
113
                return r.deleteInstance(ctx, smClient, serviceInstance)
1✔
114
        }
1✔
115

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

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

150
        if len(serviceInstance.GetConditions()) == 0 {
2✔
151
                err := utils.InitConditions(ctx, r.Client, serviceInstance)
1✔
152
                if err != nil {
1✔
153
                        return ctrl.Result{}, err
×
154
                }
×
155
        }
156

157
        if isFinalState(ctx, serviceInstance) {
2✔
158
                return r.maintainFinalState(ctx, serviceInstance)
1✔
159
        }
1✔
160

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

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

179
                // if instance was not recovered then create new instance
180
                return r.createInstance(ctx, smClient, serviceInstance)
1✔
181
        }
182

183
        if updateRequired(serviceInstance) {
2✔
184
                return r.updateInstance(ctx, smClient, serviceInstance)
1✔
185
        }
1✔
186

187
        // share/unshare
188
        if shareOrUnshareRequired(serviceInstance) {
2✔
189
                return r.handleInstanceSharing(ctx, serviceInstance, smClient)
1✔
190
        }
1✔
191

192
        log.Info("No action required")
1✔
193
        return ctrl.Result{}, nil
1✔
194
}
195

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

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

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

1✔
225
        if provisionErr != nil {
2✔
226
                log.Error(provisionErr, "failed to create service instance", "serviceOfferingName", serviceInstance.Spec.ServiceOfferingName,
1✔
227
                        "servicePlanName", serviceInstance.Spec.ServicePlanName)
1✔
228
                return utils.HandleServiceManagerError(ctx, r.Client, serviceInstance, smClientTypes.CREATE, provisionErr, true)
1✔
229
        }
1✔
230

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

242
        if provision.Location != "" {
2✔
243
                log.Info("Provision request is in progress (async)")
1✔
244
                serviceInstance.Status.OperationURL = provision.Location
1✔
245
                serviceInstance.Status.OperationType = smClientTypes.CREATE
1✔
246
                utils.SetInProgressConditions(ctx, smClientTypes.CREATE, "", serviceInstance, false)
1✔
247

1✔
248
                return ctrl.Result{RequeueAfter: r.Config.PollInterval}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
249
        }
1✔
250

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

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

1✔
262
        instanceParameters, err := r.buildSMRequestParameters(ctx, serviceInstance)
1✔
263
        if err != nil {
2✔
264
                log.Error(err, "failed to parse instance parameters")
1✔
265
                return utils.HandleOperationFailure(ctx, r.Client, serviceInstance, smClientTypes.UPDATE, err)
1✔
266
        }
1✔
267

268
        updateHashedSpecValue(serviceInstance)
1✔
269
        _, operationURL, err := smClient.UpdateInstance(serviceInstance.Status.InstanceID, &smClientTypes.ServiceInstance{
1✔
270
                Name:          serviceInstance.Spec.ExternalName,
1✔
271
                ServicePlanID: serviceInstance.Spec.ServicePlanID,
1✔
272
                Parameters:    instanceParameters,
1✔
273
        }, serviceInstance.Spec.ServiceOfferingName, serviceInstance.Spec.ServicePlanName, nil, utils.BuildUserInfo(ctx, serviceInstance.Spec.UserInfo), serviceInstance.Spec.DataCenter)
1✔
274

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

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

290
                return ctrl.Result{RequeueAfter: r.Config.PollInterval}, nil
1✔
291
        }
292
        log.Info("Instance updated successfully")
1✔
293
        utils.SetSuccessConditions(smClientTypes.UPDATE, serviceInstance, false)
1✔
294
        serviceInstance.Status.ForceReconcile = false
1✔
295
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
296
}
297

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

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

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

325
                if operationURL != "" {
2✔
326
                        log.Info("Deleting instance async")
1✔
327
                        return r.handleAsyncDelete(ctx, serviceInstance, operationURL)
1✔
328
                }
1✔
329

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

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

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

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

382
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
383
}
384

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

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

464
        serviceInstance.Status.OperationURL = ""
1✔
465
        serviceInstance.Status.OperationType = ""
1✔
466
        r.Retries.Reset(types.NamespacedName{Name: serviceInstance.Name, Namespace: serviceInstance.Namespace})
1✔
467

1✔
468
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
469
}
470

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

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

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

498
func (r *ServiceInstanceReconciler) handleAsyncDelete(ctx context.Context, serviceInstance *v1.ServiceInstance, opURL string) (ctrl.Result, error) {
1✔
499
        serviceInstance.Status.OperationURL = opURL
1✔
500
        serviceInstance.Status.OperationType = smClientTypes.DELETE
1✔
501
        utils.SetInProgressConditions(ctx, smClientTypes.DELETE, "", serviceInstance, false)
1✔
502

1✔
503
        return ctrl.Result{RequeueAfter: r.Config.PollInterval}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
504
}
1✔
505

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

1✔
518
        instances, err := smClient.ListInstances(&parameters)
1✔
519
        if err != nil {
1✔
520
                log.Error(err, "failed to list instances in SM")
×
521
                return nil, err
×
522
        }
×
523

524
        if instances != nil && len(instances.ServiceInstances) > 0 {
2✔
525
                return &instances.ServiceInstances[0], nil
1✔
526
        }
1✔
527
        log.Info("instance not found in SM")
1✔
528
        return nil, nil
1✔
529
}
530

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

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

553
        instanceState := smClientTypes.SUCCEEDED
1✔
554
        operationType := smClientTypes.CREATE
1✔
555
        description := ""
1✔
556
        if smInstance.LastOperation != nil {
2✔
557
                instanceState = smInstance.LastOperation.State
1✔
558
                operationType = smInstance.LastOperation.Type
1✔
559
                description = smInstance.LastOperation.Description
1✔
560
        } else if !smInstance.Ready {
3✔
561
                instanceState = smClientTypes.FAILED
1✔
562
        }
1✔
563

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

582
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, k8sInstance)
1✔
583
}
584

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

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

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

632
        return instanceParameters, nil
1✔
633
}
634

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

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

655
        if len(serviceInstance.Status.HashedSpec) == 0 {
2✔
656
                log.Info("instance is missing HashedSpec value, updating it")
1✔
657
                updateHashedSpecValue(serviceInstance)
1✔
658
                return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
659
        }
1✔
660
        return ctrl.Result{}, nil
1✔
661
}
662

663
func isFinalState(ctx context.Context, serviceInstance *v1.ServiceInstance) bool {
1✔
664
        log := logutils.GetLogger(ctx)
1✔
665

1✔
666
        if !serviceInstanceReady(serviceInstance) {
2✔
667
                return false
1✔
668
        }
1✔
669

670
        if serviceInstance.Status.ForceReconcile {
2✔
671
                log.Info("instance is not in final state, ForceReconcile is true")
1✔
672
                return false
1✔
673
        }
1✔
674

675
        observedGen := common.GetObservedGeneration(serviceInstance)
1✔
676
        if serviceInstance.Generation != observedGen {
2✔
677
                log.Info(fmt.Sprintf("instance is not in final state, generation: %d, observedGen: %d", serviceInstance.Generation, observedGen))
1✔
678
                return false
1✔
679
        }
1✔
680

681
        if shareOrUnshareRequired(serviceInstance) {
2✔
682
                log.Info("instance is not in final state, need to sync sharing status")
1✔
683
                if len(serviceInstance.Status.HashedSpec) == 0 {
1✔
684
                        updateHashedSpecValue(serviceInstance)
×
685
                }
×
686
                return false
1✔
687
        }
688

689
        log.Info(fmt.Sprintf("instance is in final state (generation: %d)", serviceInstance.Generation))
1✔
690
        return true
1✔
691
}
692

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

699
        if serviceInstance.Status.ForceReconcile {
2✔
700
                return true
1✔
701
        }
1✔
702

703
        cond := meta.FindStatusCondition(serviceInstance.Status.Conditions, common.ConditionSucceeded)
1✔
704
        if cond != nil && cond.Reason == common.UpdateInProgress { //in case of transient error occurred
1✔
705
                return true
×
706
        }
×
707

708
        return serviceInstance.GetSpecHash() != serviceInstance.Status.HashedSpec
1✔
709
}
710

711
func shareOrUnshareRequired(serviceInstance *v1.ServiceInstance) bool {
1✔
712
        //relevant only for non-shared instances - sharing instance is possible only for usable instances
1✔
713
        if serviceInstance.Status.Ready != metav1.ConditionTrue {
2✔
714
                return false
1✔
715
        }
1✔
716

717
        sharedCondition := meta.FindStatusCondition(serviceInstance.GetConditions(), common.ConditionShared)
1✔
718
        if sharedCondition == nil {
2✔
719
                return serviceInstance.GetShared()
1✔
720
        }
1✔
721

722
        if sharedCondition.Reason == common.ShareNotSupported {
2✔
723
                return false
1✔
724
        }
1✔
725

726
        if sharedCondition.Status == metav1.ConditionFalse {
2✔
727
                // instance does not appear to be shared, should share it if shared is requested
1✔
728
                return serviceInstance.GetShared()
1✔
729
        }
1✔
730

731
        // instance appears to be shared, should unshare it if shared is not requested
732
        return !serviceInstance.GetShared()
1✔
733
}
734

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

744
        if plans == nil || len(plans.ServicePlans) != 1 {
2✔
745
                return nil, fmt.Errorf("could not find plan with id %s", planID)
1✔
746
        }
1✔
747

748
        offeringQuery := &sm.Parameters{
×
749
                FieldQuery: []string{fmt.Sprintf("id eq '%s'", plans.ServicePlans[0].ServiceOfferingID)},
×
750
        }
×
751

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

760
        var tags []string
×
761
        if err := json.Unmarshal(offerings.ServiceOfferings[0].Tags, &tags); err != nil {
×
762
                return nil, err
×
763
        }
×
764
        return tags, nil
×
765
}
766

767
func getTags(tags []byte) ([]string, error) {
1✔
768
        var tagsArr []string
1✔
769
        if err := json.Unmarshal(tags, &tagsArr); err != nil {
1✔
770
                return nil, err
×
771
        }
×
772
        return tagsArr, nil
1✔
773
}
774

775
func updateHashedSpecValue(serviceInstance *v1.ServiceInstance) {
1✔
776
        serviceInstance.Status.HashedSpec = serviceInstance.GetSpecHash()
1✔
777
}
1✔
778

779
func getErrorMsgFromLastOperation(status *smClientTypes.Operation) string {
1✔
780
        errMsg := "async operation error"
1✔
781
        if status == nil || len(status.Errors) == 0 {
2✔
782
                return errMsg
1✔
783
        }
1✔
784
        var errMap map[string]interface{}
1✔
785

1✔
786
        if err := json.Unmarshal(status.Errors, &errMap); err != nil {
1✔
787
                return errMsg
×
788
        }
×
789

790
        if description, found := errMap["description"]; found {
2✔
791
                if descStr, ok := description.(string); ok {
2✔
792
                        errMsg = descStr
1✔
793
                }
1✔
794
        }
795
        return errMsg
1✔
796
}
797

798
type SecretPredicate struct {
799
        predicate.Funcs
800
}
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