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

SAP / sap-btp-service-operator / 21910678631

11 Feb 2026 03:12PM UTC coverage: 78.394% (+0.1%) from 78.254%
21910678631

push

github

web-flow
Async operation failure retry (#599)

88 of 118 new or added lines in 5 files covered. (74.58%)

11 existing lines in 4 files now uncovered.

2801 of 3573 relevant lines covered (78.39%)

0.88 hits per line

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

80.8
/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 shouldInstanceBeDeleted(serviceInstance) {
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
        }
175

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

316
                if len(serviceInstance.Status.OperationURL) > 0 && serviceInstance.Status.OperationType == smClientTypes.DELETE {
2✔
317
                        // ongoing delete operation - poll status from SM
1✔
318
                        log.Info("instance deletion is already in progress, checking status")
1✔
319
                        return r.poll(ctx, serviceInstance)
1✔
320
                }
1✔
321

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

578
                }
579
        }
580

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

603
        return instanceParameters, nil
1✔
604
}
605

606
func isFinalState(ctx context.Context, serviceInstance *v1.ServiceInstance) bool {
1✔
607
        log := logutils.GetLogger(ctx)
1✔
608

1✔
609
        if !serviceInstanceReady(serviceInstance) {
2✔
610
                return false
1✔
611
        }
1✔
612

613
        if serviceInstance.Status.ForceReconcile {
2✔
614
                log.Info("instance is not in final state, ForceReconcile is true")
1✔
615
                return false
1✔
616
        }
1✔
617

618
        observedGen := common.GetObservedGeneration(serviceInstance)
1✔
619
        if serviceInstance.Generation != observedGen {
2✔
620
                log.Info(fmt.Sprintf("instance is not in final state, generation: %d, observedGen: %d", serviceInstance.Generation, observedGen))
1✔
621
                return false
1✔
622
        }
1✔
623

624
        if shareOrUnshareRequired(serviceInstance) {
2✔
625
                log.Info("instance is not in final state, need to sync sharing status")
1✔
626
                if len(serviceInstance.Status.HashedSpec) == 0 {
1✔
627
                        updateHashedSpecValue(serviceInstance)
×
628
                }
×
629
                return false
1✔
630
        }
631

632
        log.Info(fmt.Sprintf("instance is in final state (generation: %d)", serviceInstance.Generation))
1✔
633
        return true
1✔
634
}
635

636
func updateRequired(serviceInstance *v1.ServiceInstance) bool {
1✔
637
        //update is not supported for failed instances (this can occur when instance creation was asynchronously)
1✔
638
        if serviceInstance.Status.Ready != metav1.ConditionTrue {
1✔
639
                return false
×
640
        }
×
641

642
        if serviceInstance.Status.ForceReconcile {
2✔
643
                return true
1✔
644
        }
1✔
645

646
        cond := meta.FindStatusCondition(serviceInstance.Status.Conditions, common.ConditionSucceeded)
1✔
647
        if cond != nil && cond.Reason == common.UpdateInProgress { //in case of transient error occurred
1✔
648
                return true
×
649
        }
×
650

651
        return serviceInstance.GetSpecHash() != serviceInstance.Status.HashedSpec
1✔
652
}
653

654
func shareOrUnshareRequired(serviceInstance *v1.ServiceInstance) bool {
1✔
655
        //relevant only for non-shared instances - sharing instance is possible only for usable instances
1✔
656
        if serviceInstance.Status.Ready != metav1.ConditionTrue {
1✔
UNCOV
657
                return false
×
UNCOV
658
        }
×
659

660
        sharedCondition := meta.FindStatusCondition(serviceInstance.GetConditions(), common.ConditionShared)
1✔
661
        if sharedCondition == nil {
2✔
662
                return serviceInstance.GetShared()
1✔
663
        }
1✔
664

665
        if sharedCondition.Reason == common.ShareNotSupported {
2✔
666
                return false
1✔
667
        }
1✔
668

669
        if sharedCondition.Status == metav1.ConditionFalse {
2✔
670
                // instance does not appear to be shared, should share it if shared is requested
1✔
671
                return serviceInstance.GetShared()
1✔
672
        }
1✔
673

674
        // instance appears to be shared, should unshare it if shared is not requested
675
        return !serviceInstance.GetShared()
1✔
676
}
677

678
func getOfferingTags(smClient sm.Client, planID string) ([]string, error) {
1✔
679
        planQuery := &sm.Parameters{
1✔
680
                FieldQuery: []string{fmt.Sprintf("id eq '%s'", planID)},
1✔
681
        }
1✔
682
        plans, err := smClient.ListPlans(planQuery)
1✔
683
        if err != nil {
1✔
684
                return nil, err
×
685
        }
×
686

687
        if plans == nil || len(plans.ServicePlans) != 1 {
2✔
688
                return nil, fmt.Errorf("could not find plan with id %s", planID)
1✔
689
        }
1✔
690

691
        offeringQuery := &sm.Parameters{
×
692
                FieldQuery: []string{fmt.Sprintf("id eq '%s'", plans.ServicePlans[0].ServiceOfferingID)},
×
693
        }
×
694

×
695
        offerings, err := smClient.ListOfferings(offeringQuery)
×
696
        if err != nil {
×
697
                return nil, err
×
698
        }
×
699
        if offerings == nil || len(offerings.ServiceOfferings) != 1 {
×
700
                return nil, fmt.Errorf("could not find offering with id %s", plans.ServicePlans[0].ServiceOfferingID)
×
701
        }
×
702

703
        var tags []string
×
704
        if err := json.Unmarshal(offerings.ServiceOfferings[0].Tags, &tags); err != nil {
×
705
                return nil, err
×
706
        }
×
707
        return tags, nil
×
708
}
709

710
func getTags(tags []byte) ([]string, error) {
1✔
711
        var tagsArr []string
1✔
712
        if err := json.Unmarshal(tags, &tagsArr); err != nil {
1✔
713
                return nil, err
×
714
        }
×
715
        return tagsArr, nil
1✔
716
}
717

718
func updateHashedSpecValue(serviceInstance *v1.ServiceInstance) {
1✔
719
        serviceInstance.Status.HashedSpec = serviceInstance.GetSpecHash()
1✔
720
}
1✔
721

722
func getErrorMsgFromLastOperation(status *smClientTypes.Operation) string {
1✔
723
        errMsg := "async operation error"
1✔
724
        if status == nil || len(status.Errors) == 0 {
1✔
725
                return errMsg
×
726
        }
×
727
        var errMap map[string]interface{}
1✔
728

1✔
729
        if err := json.Unmarshal(status.Errors, &errMap); err != nil {
1✔
730
                return errMsg
×
731
        }
×
732

733
        if description, found := errMap["description"]; found {
2✔
734
                if descStr, ok := description.(string); ok {
2✔
735
                        errMsg = descStr
1✔
736
                }
1✔
737
        }
738
        return errMsg
1✔
739
}
740

741
func shouldInstanceBeDeleted(serviceInstance *v1.ServiceInstance) bool {
1✔
742
        return utils.IsMarkedForDeletion(serviceInstance.ObjectMeta) ||
1✔
743
                (len(serviceInstance.Status.OperationURL) == 0 && len(serviceInstance.Status.InstanceID) > 0 && serviceInstance.Status.Ready == metav1.ConditionFalse) //async provision failed
1✔
744
}
1✔
745

746
type SecretPredicate struct {
747
        predicate.Funcs
748
}
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