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

SAP / sap-btp-service-operator / 21480241663

29 Jan 2026 01:35PM UTC coverage: 78.296% (-0.05%) from 78.346%
21480241663

Pull #599

github

kerenlahav
.
Pull Request #599: Async operation failure retry

61 of 92 new or added lines in 5 files covered. (66.3%)

8 existing lines in 4 files now uncovered.

2803 of 3580 relevant lines covered (78.3%)

0.88 hits per line

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

79.86
/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
2✔
175
                return r.handleUnusableInstance(ctx, serviceInstance, smClient)
1✔
176
        }
1✔
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 {
1✔
411
                log.Error(fmt.Errorf("last operation is nil"), fmt.Sprintf("polling %s returned nil", serviceInstance.Status.OperationURL))
×
412
                return ctrl.Result{}, fmt.Errorf("last operation is nil")
×
413
        }
×
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
                        if !utils.IsMarkedForDeletion(serviceInstance.ObjectMeta) {
1✔
NEW
452
                                serviceInstance.Status.OperationURL = ""
×
NEW
453
                                serviceInstance.Status.OperationType = ""
×
NEW
454
                                serviceInstance.Status.InstanceID = ""
×
NEW
455
                                return ctrl.Result{RequeueAfter: time.Second}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
×
NEW
456
                        }
×
457
                        // delete was successful - remove our finalizer from the list and update it.
458
                        if err := utils.RemoveFinalizer(ctx, r.Client, serviceInstance, common.FinalizerName); err != nil {
1✔
459
                                return ctrl.Result{}, err
×
460
                        }
×
461
                }
462
                utils.SetSuccessConditions(status.Type, serviceInstance, true)
1✔
463
        }
464

465
        serviceInstance.Status.OperationURL = ""
1✔
466
        serviceInstance.Status.OperationType = ""
1✔
467

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

471
func (r *ServiceInstanceReconciler) handleAsyncDelete(ctx context.Context, serviceInstance *v1.ServiceInstance, opURL string) (ctrl.Result, error) {
1✔
472
        serviceInstance.Status.OperationURL = opURL
1✔
473
        serviceInstance.Status.OperationType = smClientTypes.DELETE
1✔
474
        utils.SetInProgressConditions(ctx, smClientTypes.DELETE, "", serviceInstance, false)
1✔
475

1✔
476
        return ctrl.Result{RequeueAfter: r.Config.PollInterval}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
477
}
1✔
478

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

1✔
491
        instances, err := smClient.ListInstances(&parameters)
1✔
492
        if err != nil {
1✔
493
                log.Error(err, "failed to list instances in SM")
×
494
                return nil, err
×
495
        }
×
496

497
        if instances != nil && len(instances.ServiceInstances) > 0 {
2✔
498
                return &instances.ServiceInstances[0], nil
1✔
499
        }
1✔
500
        log.Info("instance not found in SM")
1✔
501
        return nil, nil
1✔
502
}
503

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

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

526
        instanceState := smClientTypes.SUCCEEDED
1✔
527
        operationType := smClientTypes.CREATE
1✔
528
        description := ""
1✔
529
        if smInstance.LastOperation != nil {
2✔
530
                instanceState = smInstance.LastOperation.State
1✔
531
                operationType = smInstance.LastOperation.Type
1✔
532
                description = smInstance.LastOperation.Description
1✔
533
        } else if !smInstance.Ready {
3✔
534
                instanceState = smClientTypes.FAILED
1✔
535
        }
1✔
536

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

551
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, k8sInstance)
1✔
552
}
553

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

572
                        if err := utils.AddWatchForSecretIfNeeded(ctx, r.Client, secret, string(serviceInstance.UID)); err != nil {
1✔
573
                                log.Error(err, fmt.Sprintf("failed to mark secret for watch %s", secret.Name))
×
574
                                return nil, err
×
575
                        }
×
576

577
                }
578
        }
579

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

602
        return instanceParameters, nil
1✔
603
}
604

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

613
        if operationURL != "" {
1✔
NEW
614
                log.Info(fmt.Sprintf("deprovision of instance %s is async", serviceInstance.Status.InstanceID))
×
NEW
615
                return r.handleAsyncDelete(ctx, serviceInstance, operationURL)
×
NEW
616
        }
×
617

618
        log.Info("instance was deleted successfully from sm")
1✔
619
        serviceInstance.Status.InstanceID = ""
1✔
620
        return ctrl.Result{RequeueAfter: time.Second}, r.Status().Update(ctx, serviceInstance)
1✔
621
}
622

623
func isFinalState(ctx context.Context, serviceInstance *v1.ServiceInstance) bool {
1✔
624
        log := logutils.GetLogger(ctx)
1✔
625

1✔
626
        if !serviceInstanceReady(serviceInstance) {
2✔
627
                return false
1✔
628
        }
1✔
629

630
        if serviceInstance.Status.ForceReconcile {
2✔
631
                log.Info("instance is not in final state, ForceReconcile is true")
1✔
632
                return false
1✔
633
        }
1✔
634

635
        observedGen := common.GetObservedGeneration(serviceInstance)
1✔
636
        if serviceInstance.Generation != observedGen {
2✔
637
                log.Info(fmt.Sprintf("instance is not in final state, generation: %d, observedGen: %d", serviceInstance.Generation, observedGen))
1✔
638
                return false
1✔
639
        }
1✔
640

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

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

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

659
        if serviceInstance.Status.ForceReconcile {
2✔
660
                return true
1✔
661
        }
1✔
662

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

668
        return serviceInstance.GetSpecHash() != serviceInstance.Status.HashedSpec
1✔
669
}
670

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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