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

SAP / sap-btp-service-operator / 21228193722

21 Jan 2026 10:35PM UTC coverage: 77.889% (-0.5%) from 78.346%
21228193722

Pull #599

github

kerenlahav
fix test
Pull Request #599: Async operation failure retry

16 of 39 new or added lines in 2 files covered. (41.03%)

7 existing lines in 2 files now uncovered.

2797 of 3591 relevant lines covered (77.89%)

0.88 hits per line

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

79.03
/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
        "sigs.k8s.io/controller-runtime/pkg/reconcile"
30

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

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

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

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

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

48
        "github.com/google/uuid"
49

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

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

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

74
func (r *ServiceInstanceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
1✔
75
        log := r.Log.WithValues("serviceinstance", req.NamespacedName).WithValues("correlation_id", uuid.New().String())
1✔
76
        ctx = context.WithValue(ctx, logutils.LogKey, log)
1✔
77

1✔
78
        serviceInstance := &v1.ServiceInstance{}
1✔
79
        if err := r.Client.Get(ctx, req.NamespacedName, serviceInstance); err != nil {
2✔
80
                if !apierrors.IsNotFound(err) {
1✔
81
                        log.Error(err, "unable to fetch ServiceInstance")
×
82
                }
×
83
                // we'll ignore not-found errors, since they can't be fixed by an immediate
84
                // requeue (we'll need to wait for a new notification), and we can get them
85
                // on deleted requests.
86
                return ctrl.Result{}, client.IgnoreNotFound(err)
1✔
87
        }
88
        serviceInstance = serviceInstance.DeepCopy()
1✔
89

1✔
90
        if utils.IsMarkedForDeletion(serviceInstance.ObjectMeta) {
2✔
91
                return r.deleteInstance(ctx, serviceInstance)
1✔
92
        }
1✔
93

94
        // If stored hash is MD5 (32 chars) and we're now using SHA256 (64 chars),
95
        // perform one-time migration by updating the stored hash without triggering update
96
        if len(serviceInstance.Status.HashedSpec) == 32 {
2✔
97
                // This is likely an MD5->SHA256 migration, update the stored hash silently
1✔
98
                // to prevent unnecessary service updates during FIPS migration
1✔
99
                log.Info(fmt.Sprintf("updated hashing for instance '%s' (id=%s)", serviceInstance.Name, serviceInstance.Status.InstanceID))
1✔
100
                updateHashedSpecValue(serviceInstance)
1✔
101
                return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
102
        }
1✔
103

104
        smClient, err := r.GetSMClient(ctx, serviceInstance)
1✔
105
        if err != nil {
1✔
106
                log.Error(err, "failed to get sm client")
×
107
                return utils.HandleOperationFailure(ctx, r.Client, serviceInstance, common.Unknown, err)
×
108
        }
×
109
        if len(serviceInstance.Status.InstanceID) > 0 {
2✔
110
                if _, err := smClient.GetInstanceByID(serviceInstance.Status.InstanceID, nil); err != nil {
2✔
111
                        var smError *sm.ServiceManagerError
1✔
112
                        if ok := errors.As(err, &smError); ok {
2✔
113
                                if smError.StatusCode == http.StatusNotFound {
2✔
114
                                        log.Info(fmt.Sprintf("instance %s not found in SM", serviceInstance.Status.InstanceID))
1✔
115
                                        condition := metav1.Condition{
1✔
116
                                                Type:               common.ConditionReady,
1✔
117
                                                Status:             metav1.ConditionFalse,
1✔
118
                                                ObservedGeneration: serviceInstance.Generation,
1✔
119
                                                LastTransitionTime: metav1.NewTime(time.Now()),
1✔
120
                                                Reason:             common.ResourceNotFound,
1✔
121
                                                Message:            fmt.Sprintf(common.ResourceNotFoundMessageFormat, "instance", serviceInstance.Status.InstanceID),
1✔
122
                                        }
1✔
123
                                        serviceInstance.Status.Conditions = []metav1.Condition{condition}
1✔
124
                                        serviceInstance.Status.Ready = metav1.ConditionFalse
1✔
125
                                        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
126
                                }
1✔
127
                        }
128
                        log.Error(err, fmt.Sprintf("failed to get instance %s from SM", serviceInstance.Status.InstanceID))
×
129
                        return ctrl.Result{}, err
×
130
                }
131
        }
132

133
        if len(serviceInstance.GetConditions()) == 0 {
2✔
134
                err := utils.InitConditions(ctx, r.Client, serviceInstance)
1✔
135
                if err != nil {
1✔
136
                        return ctrl.Result{}, err
×
137
                }
×
138
        }
139

140
        if len(serviceInstance.Status.OperationURL) > 0 {
2✔
141
                // ongoing operation - poll status from SM
1✔
142
                return r.poll(ctx, serviceInstance)
1✔
143
        }
1✔
144

145
        if isFinalState(ctx, serviceInstance) {
2✔
146
                if len(serviceInstance.Status.HashedSpec) == 0 {
2✔
147
                        updateHashedSpecValue(serviceInstance)
1✔
148
                        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
149
                }
1✔
150

151
                return ctrl.Result{}, nil
1✔
152
        }
153

154
        if controllerutil.AddFinalizer(serviceInstance, common.FinalizerName) {
2✔
155
                log.Info(fmt.Sprintf("added finalizer '%s' to service instance", common.FinalizerName))
1✔
156
                if err := r.Client.Update(ctx, serviceInstance); err != nil {
1✔
157
                        return ctrl.Result{}, err
×
158
                }
×
159
        }
160

161
        if serviceInstance.Status.InstanceID == "" {
2✔
162
                log.Info("Instance ID is empty, checking if instance exist in SM")
1✔
163
                smInstance, err := r.getInstanceForRecovery(ctx, smClient, serviceInstance)
1✔
164
                if err != nil {
1✔
165
                        log.Error(err, "failed to check instance recovery")
×
166
                        return utils.HandleServiceManagerError(ctx, r.Client, serviceInstance, smClientTypes.CREATE, err)
×
167
                }
×
168
                if smInstance != nil {
2✔
169
                        return r.recover(ctx, smClient, serviceInstance, smInstance)
1✔
170
                }
1✔
171

172
                // if instance was not recovered then create new instance
173
                return r.createInstance(ctx, smClient, serviceInstance)
1✔
174
        } else if serviceInstance.Status.Ready == metav1.ConditionFalse { //async provision failed
1✔
NEW
175
                return r.handleUnusableInstance(ctx, serviceInstance, smClient)
×
UNCOV
176
        }
×
177

178
        if updateRequired(serviceInstance) {
2✔
179
                return r.updateInstance(ctx, smClient, serviceInstance)
1✔
180
        }
1✔
181

182
        // share/unshare
183
        if shareOrUnshareRequired(serviceInstance) {
2✔
184
                return r.handleInstanceSharing(ctx, serviceInstance, smClient)
1✔
185
        }
1✔
186

187
        log.Info("No action required")
1✔
188
        return ctrl.Result{}, nil
1✔
189
}
190

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

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

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

1✔
220
        if provisionErr != nil {
2✔
221
                log.Error(provisionErr, "failed to create service instance", "serviceOfferingName", serviceInstance.Spec.ServiceOfferingName,
1✔
222
                        "servicePlanName", serviceInstance.Spec.ServicePlanName)
1✔
223
                return utils.HandleServiceManagerError(ctx, r.Client, serviceInstance, smClientTypes.CREATE, provisionErr)
1✔
224
        }
1✔
225

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

237
        if provision.Location != "" {
2✔
238
                log.Info("Provision request is in progress (async)")
1✔
239
                serviceInstance.Status.OperationURL = provision.Location
1✔
240
                serviceInstance.Status.OperationType = smClientTypes.CREATE
1✔
241
                utils.SetInProgressConditions(ctx, smClientTypes.CREATE, "", serviceInstance, false)
1✔
242

1✔
243
                return ctrl.Result{RequeueAfter: r.Config.PollInterval}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
244
        }
1✔
245

246
        log.Info(fmt.Sprintf("Instance provisioned successfully, instanceID: %s, subaccountID: %s", serviceInstance.Status.InstanceID,
1✔
247
                serviceInstance.Status.SubaccountID))
1✔
248
        utils.SetSuccessConditions(smClientTypes.CREATE, serviceInstance, false)
1✔
249
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
250
}
251

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

1✔
256
        instanceParameters, err := r.buildSMRequestParameters(ctx, serviceInstance)
1✔
257
        if err != nil {
2✔
258
                log.Error(err, "failed to parse instance parameters")
1✔
259
                return utils.HandleOperationFailure(ctx, r.Client, serviceInstance, smClientTypes.UPDATE, err)
1✔
260
        }
1✔
261

262
        updateHashedSpecValue(serviceInstance)
1✔
263
        _, operationURL, err := smClient.UpdateInstance(serviceInstance.Status.InstanceID, &smClientTypes.ServiceInstance{
1✔
264
                Name:          serviceInstance.Spec.ExternalName,
1✔
265
                ServicePlanID: serviceInstance.Spec.ServicePlanID,
1✔
266
                Parameters:    instanceParameters,
1✔
267
        }, serviceInstance.Spec.ServiceOfferingName, serviceInstance.Spec.ServicePlanName, nil, utils.BuildUserInfo(ctx, serviceInstance.Spec.UserInfo), serviceInstance.Spec.DataCenter)
1✔
268

1✔
269
        if err != nil {
2✔
270
                log.Error(err, fmt.Sprintf("failed to update service instance with ID %s", serviceInstance.Status.InstanceID))
1✔
271
                return utils.HandleServiceManagerError(ctx, r.Client, serviceInstance, smClientTypes.UPDATE, err)
1✔
272
        }
1✔
273

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

284
                return ctrl.Result{RequeueAfter: r.Config.PollInterval}, nil
1✔
285
        }
286
        log.Info("Instance updated successfully")
1✔
287
        utils.SetSuccessConditions(smClientTypes.UPDATE, serviceInstance, false)
1✔
288
        serviceInstance.Status.ForceReconcile = false
1✔
289
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
290
}
291

292
func (r *ServiceInstanceReconciler) deleteInstance(ctx context.Context, serviceInstance *v1.ServiceInstance) (ctrl.Result, error) {
1✔
293
        log := logutils.GetLogger(ctx)
1✔
294

1✔
295
        log.Info("deleting instance")
1✔
296
        if controllerutil.ContainsFinalizer(serviceInstance, common.FinalizerName) {
2✔
297
                for key, secretName := range serviceInstance.Labels {
2✔
298
                        if strings.HasPrefix(key, common.InstanceSecretRefLabel) {
2✔
299
                                if err := utils.RemoveWatchForSecret(ctx, r.Client, types.NamespacedName{Name: secretName, Namespace: serviceInstance.Namespace}, string(serviceInstance.UID)); err != nil {
1✔
300
                                        log.Error(err, fmt.Sprintf("failed to unwatch secret %s", secretName))
×
301
                                        return ctrl.Result{}, err
×
302
                                }
×
303
                        }
304
                }
305

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

327
                if len(serviceInstance.Status.OperationURL) > 0 && serviceInstance.Status.OperationType == smClientTypes.DELETE {
2✔
328
                        // ongoing delete operation - poll status from SM
1✔
329
                        return r.poll(ctx, serviceInstance)
1✔
330
                }
1✔
331

332
                log.Info(fmt.Sprintf("Deleting instance with id %v from SM", serviceInstance.Status.InstanceID))
1✔
333
                operationURL, deprovisionErr := smClient.Deprovision(serviceInstance.Status.InstanceID, nil, utils.BuildUserInfo(ctx, serviceInstance.Spec.UserInfo))
1✔
334
                if deprovisionErr != nil {
2✔
335
                        return utils.HandleServiceManagerError(ctx, r.Client, serviceInstance, smClientTypes.DELETE, deprovisionErr)
1✔
336
                }
1✔
337

338
                if operationURL != "" {
2✔
339
                        log.Info("Deleting instance async")
1✔
340
                        return r.handleAsyncDelete(ctx, serviceInstance, operationURL)
1✔
341
                }
1✔
342

343
                log.Info("Instance was deleted successfully, removing finalizer")
1✔
344
                // remove our finalizer from the list and update it.
1✔
345
                return ctrl.Result{}, utils.RemoveFinalizer(ctx, r.Client, serviceInstance, common.FinalizerName)
1✔
346
        }
347
        return ctrl.Result{}, nil
1✔
348
}
349

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

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

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

384
func (r *ServiceInstanceReconciler) poll(ctx context.Context, serviceInstance *v1.ServiceInstance) (ctrl.Result, error) {
1✔
385
        log := logutils.GetLogger(ctx)
1✔
386
        log.Info(fmt.Sprintf("resource is in progress, found operation url %s", serviceInstance.Status.OperationURL))
1✔
387
        smClient, err := r.GetSMClient(ctx, serviceInstance)
1✔
388
        if err != nil {
1✔
389
                log.Error(err, "failed to get sm client")
×
390
                return utils.HandleOperationFailure(ctx, r.Client, serviceInstance, common.Unknown, err)
×
391
        }
×
392

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
                if len(status.Description) > 0 {
1✔
418
                        log.Info(fmt.Sprintf("last operation description is '%s'", status.Description))
×
419
                        utils.SetInProgressConditions(ctx, status.Type, status.Description, serviceInstance, true)
×
420
                        if err := utils.UpdateStatus(ctx, r.Client, serviceInstance); err != nil {
×
421
                                log.Error(err, "unable to update ServiceInstance polling description")
×
422
                                return ctrl.Result{}, err
×
423
                        }
×
424
                }
425
                return ctrl.Result{RequeueAfter: r.Config.PollInterval}, nil
1✔
426
        case smClientTypes.FAILED:
1✔
427
                errMsg := getErrorMsgFromLastOperation(status)
1✔
428
                utils.SetFailureConditions(status.Type, errMsg, serviceInstance, true)
1✔
429
                // in order to delete eventually the object we need return with error
1✔
430
                if serviceInstance.Status.OperationType == smClientTypes.DELETE {
2✔
431
                        serviceInstance.Status.OperationURL = ""
1✔
432
                        serviceInstance.Status.OperationType = ""
1✔
433
                        if err := utils.UpdateStatus(ctx, r.Client, serviceInstance); err != nil {
1✔
434
                                return ctrl.Result{}, err
×
435
                        }
×
436
                        return ctrl.Result{}, errors.New(errMsg)
1✔
437
                }
438
        case smClientTypes.SUCCEEDED:
1✔
439
                if serviceInstance.Status.OperationType == smClientTypes.CREATE {
2✔
440
                        smInstance, err := smClient.GetInstanceByID(serviceInstance.Status.InstanceID, nil)
1✔
441
                        if err != nil {
1✔
442
                                log.Error(err, fmt.Sprintf("instance %s succeeded but could not fetch it from SM", serviceInstance.Status.InstanceID))
×
443
                                return ctrl.Result{}, err
×
444
                        }
×
445
                        if len(smInstance.Labels["subaccount_id"]) > 0 {
2✔
446
                                serviceInstance.Status.SubaccountID = smInstance.Labels["subaccount_id"][0]
1✔
447
                        }
1✔
448
                        serviceInstance.Status.Ready = metav1.ConditionTrue
1✔
449
                } else if serviceInstance.Status.OperationType == smClientTypes.DELETE {
2✔
450
                        // delete was successful - remove our finalizer from the list and update it.
1✔
451
                        if err := utils.RemoveFinalizer(ctx, r.Client, serviceInstance, common.FinalizerName); err != nil {
1✔
452
                                return ctrl.Result{}, err
×
453
                        }
×
454
                }
455
                utils.SetSuccessConditions(status.Type, serviceInstance, true)
1✔
456
        }
457

458
        serviceInstance.Status.OperationURL = ""
1✔
459
        serviceInstance.Status.OperationType = ""
1✔
460

1✔
461
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
462
}
463

464
func (r *ServiceInstanceReconciler) handleAsyncDelete(ctx context.Context, serviceInstance *v1.ServiceInstance, opURL string) (ctrl.Result, error) {
1✔
465
        serviceInstance.Status.OperationURL = opURL
1✔
466
        serviceInstance.Status.OperationType = smClientTypes.DELETE
1✔
467
        utils.SetInProgressConditions(ctx, smClientTypes.DELETE, "", serviceInstance, false)
1✔
468

1✔
469
        return ctrl.Result{RequeueAfter: r.Config.PollInterval}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
470
}
1✔
471

472
func (r *ServiceInstanceReconciler) getInstanceForRecovery(ctx context.Context, smClient sm.Client, serviceInstance *v1.ServiceInstance) (*smClientTypes.ServiceInstance, error) {
1✔
473
        log := logutils.GetLogger(ctx)
1✔
474
        parameters := sm.Parameters{
1✔
475
                FieldQuery: []string{
1✔
476
                        fmt.Sprintf("name eq '%s'", serviceInstance.Spec.ExternalName),
1✔
477
                        fmt.Sprintf("context/clusterid eq '%s'", r.Config.ClusterID),
1✔
478
                        fmt.Sprintf("context/namespace eq '%s'", serviceInstance.Namespace)},
1✔
479
                LabelQuery: []string{
1✔
480
                        fmt.Sprintf("%s eq '%s'", common.K8sNameLabel, serviceInstance.Name)},
1✔
481
                GeneralParams: []string{"attach_last_operations=true"},
1✔
482
        }
1✔
483

1✔
484
        instances, err := smClient.ListInstances(&parameters)
1✔
485
        if err != nil {
1✔
486
                log.Error(err, "failed to list instances in SM")
×
487
                return nil, err
×
488
        }
×
489

490
        if instances != nil && len(instances.ServiceInstances) > 0 {
2✔
491
                return &instances.ServiceInstances[0], nil
1✔
492
        }
1✔
493
        log.Info("instance not found in SM")
1✔
494
        return nil, nil
1✔
495
}
496

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

1✔
500
        log.Info(fmt.Sprintf("found existing instance in SM with id %s, updating status", smInstance.ID))
1✔
501
        updateHashedSpecValue(k8sInstance)
1✔
502
        if smInstance.Ready {
2✔
503
                k8sInstance.Status.Ready = metav1.ConditionTrue
1✔
504
        }
1✔
505
        if smInstance.Shared {
1✔
506
                utils.SetSharedCondition(k8sInstance, metav1.ConditionTrue, common.ShareSucceeded, "Instance shared successfully")
×
507
        }
×
508
        k8sInstance.Status.InstanceID = smInstance.ID
1✔
509
        k8sInstance.Status.OperationURL = ""
1✔
510
        k8sInstance.Status.OperationType = ""
1✔
511
        tags, err := getOfferingTags(smClient, smInstance.ServicePlanID)
1✔
512
        if err != nil {
2✔
513
                log.Error(err, "could not recover offering tags")
1✔
514
        }
1✔
515
        if len(tags) > 0 {
1✔
516
                k8sInstance.Status.Tags = tags
×
517
        }
×
518

519
        instanceState := smClientTypes.SUCCEEDED
1✔
520
        operationType := smClientTypes.CREATE
1✔
521
        description := ""
1✔
522
        if smInstance.LastOperation != nil {
2✔
523
                instanceState = smInstance.LastOperation.State
1✔
524
                operationType = smInstance.LastOperation.Type
1✔
525
                description = smInstance.LastOperation.Description
1✔
526
        } else if !smInstance.Ready {
3✔
527
                instanceState = smClientTypes.FAILED
1✔
528
        }
1✔
529

530
        switch instanceState {
1✔
531
        case smClientTypes.PENDING:
1✔
532
                fallthrough
1✔
533
        case smClientTypes.INPROGRESS:
1✔
534
                k8sInstance.Status.OperationURL = sm.BuildOperationURL(smInstance.LastOperation.ID, smInstance.ID, smClientTypes.ServiceInstancesURL)
1✔
535
                k8sInstance.Status.OperationType = smInstance.LastOperation.Type
1✔
536
                k8sInstance.Status.InstanceID = smInstance.ID
1✔
537
                utils.SetInProgressConditions(ctx, smInstance.LastOperation.Type, smInstance.LastOperation.Description, k8sInstance, false)
1✔
538
        case smClientTypes.SUCCEEDED:
1✔
539
                utils.SetSuccessConditions(operationType, k8sInstance, false)
1✔
540
        case smClientTypes.FAILED:
1✔
541
                utils.SetFailureConditions(operationType, description, k8sInstance, false)
1✔
542
        }
543

544
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, k8sInstance)
1✔
545
}
546

547
func (r *ServiceInstanceReconciler) buildSMRequestParameters(ctx context.Context, serviceInstance *v1.ServiceInstance) ([]byte, error) {
1✔
548
        log := logutils.GetLogger(ctx)
1✔
549
        instanceParameters, paramSecrets, err := utils.BuildSMRequestParameters(serviceInstance.Namespace, serviceInstance.Spec.Parameters, serviceInstance.Spec.ParametersFrom)
1✔
550
        if err != nil {
2✔
551
                log.Error(err, "failed to build instance parameters")
1✔
552
                return nil, err
1✔
553
        }
1✔
554
        instanceLabelsChanged := false
1✔
555
        newInstanceLabels := make(map[string]string)
1✔
556
        if serviceInstance.IsSubscribedToParamSecretsChanges() {
2✔
557
                // find all new secrets on the instance
1✔
558
                for _, secret := range paramSecrets {
2✔
559
                        labelKey := utils.GetLabelKeyForInstanceSecret(secret.Name)
1✔
560
                        newInstanceLabels[labelKey] = secret.Name
1✔
561
                        if _, ok := serviceInstance.Labels[labelKey]; !ok {
2✔
562
                                instanceLabelsChanged = true
1✔
563
                        }
1✔
564

565
                        if err := utils.AddWatchForSecretIfNeeded(ctx, r.Client, secret, string(serviceInstance.UID)); err != nil {
1✔
566
                                log.Error(err, fmt.Sprintf("failed to mark secret for watch %s", secret.Name))
×
567
                                return nil, err
×
568
                        }
×
569

570
                }
571
        }
572

573
        //sync instance labels
574
        for labelKey, labelValue := range serviceInstance.Labels {
2✔
575
                if strings.HasPrefix(labelKey, common.InstanceSecretRefLabel) {
2✔
576
                        if _, ok := newInstanceLabels[labelKey]; !ok {
2✔
577
                                log.Info(fmt.Sprintf("params secret named %s was removed, unwatching it", labelValue))
1✔
578
                                instanceLabelsChanged = true
1✔
579
                                if err := utils.RemoveWatchForSecret(ctx, r.Client, types.NamespacedName{Name: labelValue, Namespace: serviceInstance.Namespace}, string(serviceInstance.UID)); err != nil {
1✔
580
                                        log.Error(err, fmt.Sprintf("failed to unwatch secret %s", labelValue))
×
581
                                        return nil, err
×
582
                                }
×
583
                        }
584
                } else {
×
585
                        // this label not related to secrets, add it
×
586
                        newInstanceLabels[labelKey] = labelValue
×
587
                }
×
588
        }
589
        if instanceLabelsChanged {
2✔
590
                serviceInstance.Labels = newInstanceLabels
1✔
591
                log.Info("updating instance with secret labels")
1✔
592
                return instanceParameters, r.Client.Update(ctx, serviceInstance)
1✔
593
        }
1✔
594

595
        return instanceParameters, nil
1✔
596
}
597

NEW
598
func (r *ServiceInstanceReconciler) handleUnusableInstance(ctx context.Context, serviceInstance *v1.ServiceInstance, smClient sm.Client) (ctrl.Result, error) {
×
NEW
599
        log := logutils.GetLogger(ctx)
×
NEW
600
        log.Info(fmt.Sprintf("instance %s failed during async provision, deleting it", serviceInstance.Status.InstanceID))
×
NEW
601
        operationURL, deprovisionErr := smClient.Deprovision(serviceInstance.Status.InstanceID, nil, utils.BuildUserInfo(ctx, serviceInstance.Spec.UserInfo))
×
NEW
602
        if deprovisionErr != nil {
×
NEW
603
                return utils.HandleServiceManagerError(ctx, r.Client, serviceInstance, smClientTypes.DELETE, deprovisionErr)
×
NEW
604
        }
×
605

NEW
606
        if operationURL != "" {
×
NEW
607
                log.Info(fmt.Sprintf("deprovision of instance %s is async", serviceInstance.Status.InstanceID))
×
NEW
608
                return r.handleAsyncDelete(ctx, serviceInstance, operationURL)
×
NEW
609
        }
×
610

NEW
611
        log.Info("instance was deleted successfully")
×
NEW
612
        serviceInstance.Status.InstanceID = ""
×
NEW
613
        return ctrl.Result{RequeueAfter: time.Second}, r.Update(ctx, serviceInstance)
×
614
}
615

616
func isFinalState(ctx context.Context, serviceInstance *v1.ServiceInstance) bool {
1✔
617
        log := logutils.GetLogger(ctx)
1✔
618

1✔
619
        if len(serviceInstance.Status.InstanceID) == 0 {
2✔
620
                return false
1✔
621
        }
1✔
622

623
        if serviceInstance.Status.ForceReconcile {
2✔
624
                log.Info("instance is not in final state, ForceReconcile is true")
1✔
625
                return false
1✔
626
        }
1✔
627

628
        observedGen := common.GetObservedGeneration(serviceInstance)
1✔
629
        if serviceInstance.Generation != observedGen {
2✔
630
                log.Info(fmt.Sprintf("instance is not in final state, generation: %d, observedGen: %d", serviceInstance.Generation, observedGen))
1✔
631
                return false
1✔
632
        }
1✔
633

634
        if utils.ShouldRetryOperation(serviceInstance) {
2✔
635
                log.Info("instance is not in final state, last operation failed, retrying")
1✔
636
                return false
1✔
637
        }
1✔
638

639
        if shareOrUnshareRequired(serviceInstance) {
2✔
640
                log.Info("instance is not in final state, need to sync sharing status")
1✔
641
                if len(serviceInstance.Status.HashedSpec) == 0 {
1✔
642
                        updateHashedSpecValue(serviceInstance)
×
643
                }
×
644
                return false
1✔
645
        }
646

647
        log.Info(fmt.Sprintf("instance is in final state (generation: %d)", serviceInstance.Generation))
1✔
648
        return true
1✔
649
}
650

651
func updateRequired(serviceInstance *v1.ServiceInstance) bool {
1✔
652
        //update is not supported for failed instances (this can occur when instance creation was asynchronously)
1✔
653
        if serviceInstance.Status.Ready != metav1.ConditionTrue {
1✔
654
                return false
×
655
        }
×
656

657
        if serviceInstance.Status.ForceReconcile {
2✔
658
                return true
1✔
659
        }
1✔
660

661
        cond := meta.FindStatusCondition(serviceInstance.Status.Conditions, common.ConditionSucceeded)
1✔
662
        if cond != nil && cond.Reason == common.UpdateInProgress { //in case of transient error occurred
1✔
663
                return true
×
664
        }
×
665

666
        return serviceInstance.GetSpecHash() != serviceInstance.Status.HashedSpec
1✔
667
}
668

669
func shareOrUnshareRequired(serviceInstance *v1.ServiceInstance) bool {
1✔
670
        //relevant only for non-shared instances - sharing instance is possible only for usable instances
1✔
671
        if serviceInstance.Status.Ready != metav1.ConditionTrue {
2✔
672
                return false
1✔
673
        }
1✔
674

675
        sharedCondition := meta.FindStatusCondition(serviceInstance.GetConditions(), common.ConditionShared)
1✔
676
        if sharedCondition == nil {
2✔
677
                return serviceInstance.GetShared()
1✔
678
        }
1✔
679

680
        if sharedCondition.Reason == common.ShareNotSupported {
2✔
681
                return false
1✔
682
        }
1✔
683

684
        if sharedCondition.Status == metav1.ConditionFalse {
2✔
685
                // instance does not appear to be shared, should share it if shared is requested
1✔
686
                return serviceInstance.GetShared()
1✔
687
        }
1✔
688

689
        // instance appears to be shared, should unshare it if shared is not requested
690
        return !serviceInstance.GetShared()
1✔
691
}
692

693
func getOfferingTags(smClient sm.Client, planID string) ([]string, error) {
1✔
694
        planQuery := &sm.Parameters{
1✔
695
                FieldQuery: []string{fmt.Sprintf("id eq '%s'", planID)},
1✔
696
        }
1✔
697
        plans, err := smClient.ListPlans(planQuery)
1✔
698
        if err != nil {
1✔
699
                return nil, err
×
700
        }
×
701

702
        if plans == nil || len(plans.ServicePlans) != 1 {
2✔
703
                return nil, fmt.Errorf("could not find plan with id %s", planID)
1✔
704
        }
1✔
705

706
        offeringQuery := &sm.Parameters{
×
707
                FieldQuery: []string{fmt.Sprintf("id eq '%s'", plans.ServicePlans[0].ServiceOfferingID)},
×
708
        }
×
709

×
710
        offerings, err := smClient.ListOfferings(offeringQuery)
×
711
        if err != nil {
×
712
                return nil, err
×
713
        }
×
714
        if offerings == nil || len(offerings.ServiceOfferings) != 1 {
×
715
                return nil, fmt.Errorf("could not find offering with id %s", plans.ServicePlans[0].ServiceOfferingID)
×
716
        }
×
717

718
        var tags []string
×
719
        if err := json.Unmarshal(offerings.ServiceOfferings[0].Tags, &tags); err != nil {
×
720
                return nil, err
×
721
        }
×
722
        return tags, nil
×
723
}
724

725
func getTags(tags []byte) ([]string, error) {
1✔
726
        var tagsArr []string
1✔
727
        if err := json.Unmarshal(tags, &tagsArr); err != nil {
1✔
728
                return nil, err
×
729
        }
×
730
        return tagsArr, nil
1✔
731
}
732

733
func updateHashedSpecValue(serviceInstance *v1.ServiceInstance) {
1✔
734
        serviceInstance.Status.HashedSpec = serviceInstance.GetSpecHash()
1✔
735
}
1✔
736

737
func getErrorMsgFromLastOperation(status *smClientTypes.Operation) string {
1✔
738
        errMsg := "async operation error"
1✔
739
        if status == nil || len(status.Errors) == 0 {
1✔
740
                return errMsg
×
741
        }
×
742
        var errMap map[string]interface{}
1✔
743

1✔
744
        if err := json.Unmarshal(status.Errors, &errMap); err != nil {
1✔
745
                return errMsg
×
746
        }
×
747

748
        if description, found := errMap["description"]; found {
2✔
749
                if descStr, ok := description.(string); ok {
2✔
750
                        errMsg = descStr
1✔
751
                }
1✔
752
        }
753
        return errMsg
1✔
754
}
755

756
type SecretPredicate struct {
757
        predicate.Funcs
758
}
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