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

SAP / sap-btp-service-operator / 21266033426

22 Jan 2026 09:46PM UTC coverage: 78.086% (-0.3%) from 78.346%
21266033426

Pull #599

github

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

37 of 70 new or added lines in 3 files covered. (52.86%)

5 existing lines in 3 files now uncovered.

2790 of 3573 relevant lines covered (78.09%)

0.88 hits per line

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

79.5
/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
        if controllerutil.ContainsFinalizer(serviceInstance, common.FinalizerName) {
2✔
296
                log.Info("instance has finalizer, deleting it from sm")
1✔
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
                        log.Info("instance deletion is already in progress, checking status")
1✔
330
                        return r.poll(ctx, serviceInstance)
1✔
331
                }
1✔
332

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

339
                if operationURL != "" {
2✔
340
                        log.Info("Deleting instance async")
1✔
341
                        return r.handleAsyncDelete(ctx, serviceInstance, operationURL)
1✔
342
                }
1✔
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, serviceInstance *v1.ServiceInstance) (ctrl.Result, error) {
1✔
386
        log := logutils.GetLogger(ctx)
1✔
387
        log.Info(fmt.Sprintf("resource is in progress, found operation url %s", serviceInstance.Status.OperationURL))
1✔
388
        smClient, err := r.GetSMClient(ctx, serviceInstance)
1✔
389
        if err != nil {
1✔
390
                log.Error(err, "failed to get sm client")
×
391
                return utils.HandleOperationFailure(ctx, r.Client, serviceInstance, common.Unknown, err)
×
392
        }
×
393

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

571
                }
572
        }
573

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

596
        return instanceParameters, nil
1✔
597
}
598

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

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

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

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

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

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

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

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

643
        log.Info(fmt.Sprintf("instance is in final state (generation: %d)", serviceInstance.Generation))
1✔
644
        return true
1✔
645
}
646

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

653
        if serviceInstance.Status.ForceReconcile {
2✔
654
                return true
1✔
655
        }
1✔
656

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

662
        return serviceInstance.GetSpecHash() != serviceInstance.Status.HashedSpec
1✔
663
}
664

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

671
        sharedCondition := meta.FindStatusCondition(serviceInstance.GetConditions(), common.ConditionShared)
1✔
672
        if sharedCondition == nil {
2✔
673
                return serviceInstance.GetShared()
1✔
674
        }
1✔
675

676
        if sharedCondition.Reason == common.ShareNotSupported {
2✔
677
                return false
1✔
678
        }
1✔
679

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

685
        // instance appears to be shared, should unshare it if shared is not requested
686
        return !serviceInstance.GetShared()
1✔
687
}
688

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

698
        if plans == nil || len(plans.ServicePlans) != 1 {
2✔
699
                return nil, fmt.Errorf("could not find plan with id %s", planID)
1✔
700
        }
1✔
701

702
        offeringQuery := &sm.Parameters{
×
703
                FieldQuery: []string{fmt.Sprintf("id eq '%s'", plans.ServicePlans[0].ServiceOfferingID)},
×
704
        }
×
705

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

714
        var tags []string
×
715
        if err := json.Unmarshal(offerings.ServiceOfferings[0].Tags, &tags); err != nil {
×
716
                return nil, err
×
717
        }
×
718
        return tags, nil
×
719
}
720

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

729
func updateHashedSpecValue(serviceInstance *v1.ServiceInstance) {
1✔
730
        serviceInstance.Status.HashedSpec = serviceInstance.GetSpecHash()
1✔
731
}
1✔
732

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

1✔
740
        if err := json.Unmarshal(status.Errors, &errMap); err != nil {
1✔
741
                return errMsg
×
742
        }
×
743

744
        if description, found := errMap["description"]; found {
2✔
745
                if descStr, ok := description.(string); ok {
2✔
746
                        errMsg = descStr
1✔
747
                }
1✔
748
        }
749
        return errMsg
1✔
750
}
751

752
type SecretPredicate struct {
753
        predicate.Funcs
754
}
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