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

SAP / sap-btp-service-operator / 17296567442

28 Aug 2025 12:58PM UTC coverage: 78.462% (-1.5%) from 80.011%
17296567442

Pull #538

github

kerenlahav
coverage
Pull Request #538: transient error - prototype

53 of 141 new or added lines in 5 files covered. (37.59%)

18 existing lines in 3 files now uncovered.

2754 of 3510 relevant lines covered (78.46%)

0.88 hits per line

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

80.31
/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
        "strings"
24

25
        "github.com/pkg/errors"
26
        "sigs.k8s.io/controller-runtime/pkg/reconcile"
27

28
        "k8s.io/apimachinery/pkg/types"
29

30
        "sigs.k8s.io/controller-runtime/pkg/predicate"
31

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

39
        "k8s.io/client-go/util/workqueue"
40
        "sigs.k8s.io/controller-runtime/pkg/controller"
41

42
        v1 "github.com/SAP/sap-btp-service-operator/api/v1"
43
        "k8s.io/apimachinery/pkg/api/meta"
44

45
        "github.com/google/uuid"
46

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

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

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

71
func (r *ServiceInstanceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
1✔
72
        log := r.Log.WithValues("serviceinstance", req.NamespacedName).WithValues("correlation_id", uuid.New().String())
1✔
73
        ctx = context.WithValue(ctx, utils.LogKey{}, log)
1✔
74

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

1✔
87
        if utils.IsMarkedForDeletion(serviceInstance.ObjectMeta) {
2✔
88
                return r.deleteInstance(ctx, serviceInstance)
1✔
89
        }
1✔
90
        if len(serviceInstance.GetConditions()) == 0 {
2✔
91
                err := utils.InitConditions(ctx, r.Client, serviceInstance)
1✔
92
                if err != nil {
1✔
93
                        return ctrl.Result{}, err
×
94
                }
×
95
        }
96

97
        if len(serviceInstance.Status.OperationURL) > 0 {
2✔
98
                // ongoing operation - poll status from SM
1✔
99
                return r.poll(ctx, serviceInstance)
1✔
100
        }
1✔
101

102
        if isFinalState(ctx, serviceInstance) {
2✔
103
                if len(serviceInstance.Status.HashedSpec) == 0 {
2✔
104
                        updateHashedSpecValue(serviceInstance)
1✔
105
                        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
106
                }
1✔
107

108
                return ctrl.Result{}, nil
1✔
109
        }
110

111
        if controllerutil.AddFinalizer(serviceInstance, common.FinalizerName) {
2✔
112
                log.Info(fmt.Sprintf("added finalizer '%s' to service instance", common.FinalizerName))
1✔
113
                if err := r.Client.Update(ctx, serviceInstance); err != nil {
1✔
114
                        return ctrl.Result{}, err
×
115
                }
×
116
        }
117

118
        smClient, err := r.GetSMClient(ctx, serviceInstance)
1✔
119
        if err != nil {
1✔
120
                log.Error(err, "failed to get sm client")
×
NEW
121
                return utils.HandleOperationFailure(ctx, r.Client, serviceInstance, common.Unknown, err)
×
122
        }
×
123

124
        if serviceInstance.Status.InstanceID == "" {
2✔
125
                log.Info("Instance ID is empty, checking if instance exist in SM")
1✔
126
                smInstance, err := r.getInstanceForRecovery(ctx, smClient, serviceInstance)
1✔
127
                if err != nil {
1✔
128
                        log.Error(err, "failed to check instance recovery")
×
NEW
129
                        return utils.HandleOperationFailure(ctx, r.Client, serviceInstance, common.Unknown, err)
×
130
                }
×
131
                if smInstance != nil {
2✔
132
                        return r.recover(ctx, smClient, serviceInstance, smInstance)
1✔
133
                }
1✔
134

135
                // if instance was not recovered then create new instance
136
                return r.createInstance(ctx, smClient, serviceInstance)
1✔
137
        }
138

139
        // Update
140
        if updateRequired(serviceInstance) {
2✔
141
                return r.updateInstance(ctx, smClient, serviceInstance)
1✔
142
        }
1✔
143

144
        // share/unshare
145
        if shareOrUnshareRequired(serviceInstance) {
2✔
146
                return r.handleInstanceSharing(ctx, serviceInstance, smClient)
1✔
147
        }
1✔
148

149
        log.Info("No action required")
1✔
150
        return ctrl.Result{}, nil
1✔
151
}
152

153
func (r *ServiceInstanceReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
154
        return ctrl.NewControllerManagedBy(mgr).
1✔
155
                For(&v1.ServiceInstance{}).
1✔
156
                WithOptions(controller.Options{RateLimiter: workqueue.NewTypedItemExponentialFailureRateLimiter[reconcile.Request](r.Config.RetryBaseDelay, r.Config.RetryMaxDelay)}).
1✔
157
                Complete(r)
1✔
158
}
1✔
159

160
func (r *ServiceInstanceReconciler) createInstance(ctx context.Context, smClient sm.Client, serviceInstance *v1.ServiceInstance) (ctrl.Result, error) {
1✔
161
        log := utils.GetLogger(ctx)
1✔
162
        log.Info("Creating instance in SM")
1✔
163
        updateHashedSpecValue(serviceInstance)
1✔
164
        instanceParameters, err := r.buildSMRequestParameters(ctx, serviceInstance)
1✔
165
        if err != nil {
2✔
166
                // if parameters are invalid there is nothing we can do, the user should fix it according to the error message in the condition
1✔
167
                log.Error(err, "failed to parse instance parameters")
1✔
168
                return utils.HandleOperationFailure(ctx, r.Client, serviceInstance, smClientTypes.CREATE, err)
1✔
169
        }
1✔
170

171
        provision, provisionErr := smClient.Provision(&smClientTypes.ServiceInstance{
1✔
172
                Name:          serviceInstance.Spec.ExternalName,
1✔
173
                ServicePlanID: serviceInstance.Spec.ServicePlanID,
1✔
174
                Parameters:    instanceParameters,
1✔
175
                Labels: smClientTypes.Labels{
1✔
176
                        common.NamespaceLabel: []string{serviceInstance.Namespace},
1✔
177
                        common.K8sNameLabel:   []string{serviceInstance.Name},
1✔
178
                        common.ClusterIDLabel: []string{r.Config.ClusterID},
1✔
179
                },
1✔
180
        }, serviceInstance.Spec.ServiceOfferingName, serviceInstance.Spec.ServicePlanName, nil, utils.BuildUserInfo(ctx, serviceInstance.Spec.UserInfo), serviceInstance.Spec.DataCenter)
1✔
181

1✔
182
        if provisionErr != nil {
2✔
183
                log.Error(provisionErr, "failed to create service instance", "serviceOfferingName", serviceInstance.Spec.ServiceOfferingName,
1✔
184
                        "servicePlanName", serviceInstance.Spec.ServicePlanName)
1✔
185
                return utils.HandleServiceManagerError(ctx, r.Client, serviceInstance, smClientTypes.CREATE, provisionErr)
1✔
186
        }
1✔
187

188
        serviceInstance.Status.InstanceID = provision.InstanceID
1✔
189
        serviceInstance.Status.SubaccountID = provision.SubaccountID
1✔
190
        if len(provision.Tags) > 0 {
2✔
191
                tags, err := getTags(provision.Tags)
1✔
192
                if err != nil {
1✔
193
                        log.Error(err, "failed to unmarshal tags")
×
194
                } else {
1✔
195
                        serviceInstance.Status.Tags = tags
1✔
196
                }
1✔
197
        }
198

199
        if provision.Location != "" {
2✔
200
                log.Info("Provision request is in progress (async)")
1✔
201
                serviceInstance.Status.OperationURL = provision.Location
1✔
202
                serviceInstance.Status.OperationType = smClientTypes.CREATE
1✔
203
                utils.SetInProgressConditions(ctx, smClientTypes.CREATE, "", serviceInstance, false)
1✔
204

1✔
205
                return ctrl.Result{RequeueAfter: r.Config.PollInterval}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
206
        }
1✔
207

208
        log.Info(fmt.Sprintf("Instance provisioned successfully, instanceID: %s, subaccountID: %s", serviceInstance.Status.InstanceID,
1✔
209
                serviceInstance.Status.SubaccountID))
1✔
210
        utils.SetSuccessConditions(smClientTypes.CREATE, serviceInstance, false)
1✔
211
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
212
}
213

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

1✔
218
        instanceParameters, err := r.buildSMRequestParameters(ctx, serviceInstance)
1✔
219
        if err != nil {
2✔
220
                log.Error(err, "failed to parse instance parameters")
1✔
221
                return utils.HandleOperationFailure(ctx, r.Client, serviceInstance, smClientTypes.UPDATE, err)
1✔
222
        }
1✔
223

224
        updateHashedSpecValue(serviceInstance)
1✔
225
        _, operationURL, err := smClient.UpdateInstance(serviceInstance.Status.InstanceID, &smClientTypes.ServiceInstance{
1✔
226
                Name:          serviceInstance.Spec.ExternalName,
1✔
227
                ServicePlanID: serviceInstance.Spec.ServicePlanID,
1✔
228
                Parameters:    instanceParameters,
1✔
229
        }, serviceInstance.Spec.ServiceOfferingName, serviceInstance.Spec.ServicePlanName, nil, utils.BuildUserInfo(ctx, serviceInstance.Spec.UserInfo), serviceInstance.Spec.DataCenter)
1✔
230

1✔
231
        if err != nil {
2✔
232
                log.Error(err, fmt.Sprintf("failed to update service instance with ID %s", serviceInstance.Status.InstanceID))
1✔
233
                return utils.HandleServiceManagerError(ctx, r.Client, serviceInstance, smClientTypes.UPDATE, err)
1✔
234
        }
1✔
235

236
        if operationURL != "" {
2✔
237
                log.Info(fmt.Sprintf("Update request accepted, operation URL: %s", operationURL))
1✔
238
                serviceInstance.Status.OperationURL = operationURL
1✔
239
                serviceInstance.Status.OperationType = smClientTypes.UPDATE
1✔
240
                utils.SetInProgressConditions(ctx, smClientTypes.UPDATE, "", serviceInstance, false)
1✔
241
                serviceInstance.Status.ForceReconcile = false
1✔
242
                if err := utils.UpdateStatus(ctx, r.Client, serviceInstance); err != nil {
2✔
243
                        return ctrl.Result{}, err
1✔
244
                }
1✔
245

246
                return ctrl.Result{RequeueAfter: r.Config.PollInterval}, nil
1✔
247
        }
248
        log.Info("Instance updated successfully")
1✔
249
        utils.SetSuccessConditions(smClientTypes.UPDATE, serviceInstance, false)
1✔
250
        serviceInstance.Status.ForceReconcile = false
1✔
251
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
252
}
253

254
func (r *ServiceInstanceReconciler) deleteInstance(ctx context.Context, serviceInstance *v1.ServiceInstance) (ctrl.Result, error) {
1✔
255
        log := utils.GetLogger(ctx)
1✔
256

1✔
257
        log.Info("deleting instance")
1✔
258
        if controllerutil.ContainsFinalizer(serviceInstance, common.FinalizerName) {
2✔
259
                for key, secretName := range serviceInstance.Labels {
2✔
260
                        if strings.HasPrefix(key, common.InstanceSecretRefLabel) {
2✔
261
                                if err := utils.RemoveWatchForSecret(ctx, r.Client, types.NamespacedName{Name: secretName, Namespace: serviceInstance.Namespace}, string(serviceInstance.UID)); err != nil {
1✔
262
                                        log.Error(err, fmt.Sprintf("failed to unwatch secret %s", secretName))
×
263
                                        return ctrl.Result{}, err
×
264
                                }
×
265
                        }
266
                }
267

268
                smClient, err := r.GetSMClient(ctx, serviceInstance)
1✔
269
                if err != nil {
1✔
270
                        log.Error(err, "failed to get sm client")
×
NEW
271
                        return utils.HandleOperationFailure(ctx, r.Client, serviceInstance, common.Unknown, err)
×
272
                }
×
273
                if len(serviceInstance.Status.InstanceID) == 0 {
2✔
274
                        log.Info("No instance id found validating instance does not exists in SM before removing finalizer")
1✔
275
                        smInstance, err := r.getInstanceForRecovery(ctx, smClient, serviceInstance)
1✔
276
                        if err != nil {
1✔
277
                                return ctrl.Result{}, err
×
278
                        }
×
279
                        if smInstance != nil {
2✔
280
                                log.Info("instance exists in SM continue with deletion")
1✔
281
                                serviceInstance.Status.InstanceID = smInstance.ID
1✔
282
                                utils.SetInProgressConditions(ctx, smClientTypes.DELETE, "delete after recovery", serviceInstance, false)
1✔
283
                                return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
284
                        }
1✔
285
                        log.Info("instance does not exists in SM, removing finalizer")
1✔
286
                        return ctrl.Result{}, utils.RemoveFinalizer(ctx, r.Client, serviceInstance, common.FinalizerName)
1✔
287
                }
288

289
                if len(serviceInstance.Status.OperationURL) > 0 && serviceInstance.Status.OperationType == smClientTypes.DELETE {
2✔
290
                        // ongoing delete operation - poll status from SM
1✔
291
                        return r.poll(ctx, serviceInstance)
1✔
292
                }
1✔
293

294
                log.Info(fmt.Sprintf("Deleting instance with id %v from SM", serviceInstance.Status.InstanceID))
1✔
295
                operationURL, deprovisionErr := smClient.Deprovision(serviceInstance.Status.InstanceID, nil, utils.BuildUserInfo(ctx, serviceInstance.Spec.UserInfo))
1✔
296
                if deprovisionErr != nil {
2✔
297
                        // delete will proceed anyway
1✔
298
                        return utils.HandleServiceManagerError(ctx, r.Client, serviceInstance, smClientTypes.DELETE, deprovisionErr)
1✔
299
                }
1✔
300

301
                if operationURL != "" {
2✔
302
                        log.Info("Deleting instance async")
1✔
303
                        return r.handleAsyncDelete(ctx, serviceInstance, operationURL)
1✔
304
                }
1✔
305

306
                log.Info("Instance was deleted successfully, removing finalizer")
1✔
307
                // remove our finalizer from the list and update it.
1✔
308
                return ctrl.Result{}, utils.RemoveFinalizer(ctx, r.Client, serviceInstance, common.FinalizerName)
1✔
309
        }
310
        return ctrl.Result{}, nil
1✔
311
}
312

313
func (r *ServiceInstanceReconciler) handleInstanceSharing(ctx context.Context, serviceInstance *v1.ServiceInstance, smClient sm.Client) (ctrl.Result, error) {
1✔
314
        log := utils.GetLogger(ctx)
1✔
315
        log.Info("Handling change in instance sharing")
1✔
316

1✔
317
        if serviceInstance.GetShared() {
2✔
318
                log.Info("Service instance appears to be unshared, sharing the instance")
1✔
319
                err := smClient.ShareInstance(serviceInstance.Status.InstanceID, utils.BuildUserInfo(ctx, serviceInstance.Spec.UserInfo))
1✔
320
                if err != nil {
2✔
321
                        log.Error(err, "failed to share instance")
1✔
322
                        return utils.HandleInstanceSharingError(ctx, r.Client, serviceInstance, metav1.ConditionFalse, common.ShareFailed, err)
1✔
323
                }
1✔
324
                log.Info("instance shared successfully")
1✔
325
                utils.SetSharedCondition(serviceInstance, metav1.ConditionTrue, common.ShareSucceeded, "instance shared successfully")
1✔
326
        } else { //un-share
1✔
327
                log.Info("Service instance appears to be shared, un-sharing the instance")
1✔
328
                err := smClient.UnShareInstance(serviceInstance.Status.InstanceID, utils.BuildUserInfo(ctx, serviceInstance.Spec.UserInfo))
1✔
329
                if err != nil {
2✔
330
                        log.Error(err, "failed to un-share instance")
1✔
331
                        return utils.HandleInstanceSharingError(ctx, r.Client, serviceInstance, metav1.ConditionTrue, common.UnShareFailed, err)
1✔
332
                }
1✔
333
                log.Info("instance un-shared successfully")
1✔
334
                if serviceInstance.Spec.Shared != nil {
2✔
335
                        utils.SetSharedCondition(serviceInstance, metav1.ConditionFalse, common.UnShareSucceeded, "instance un-shared successfully")
1✔
336
                } else {
2✔
337
                        log.Info("removing Shared condition since shared is undefined in instance")
1✔
338
                        conditions := serviceInstance.GetConditions()
1✔
339
                        meta.RemoveStatusCondition(&conditions, common.ConditionShared)
1✔
340
                        serviceInstance.SetConditions(conditions)
1✔
341
                }
1✔
342
        }
343

344
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
345
}
346

347
func (r *ServiceInstanceReconciler) poll(ctx context.Context, serviceInstance *v1.ServiceInstance) (ctrl.Result, error) {
1✔
348
        log := utils.GetLogger(ctx)
1✔
349
        log.Info(fmt.Sprintf("resource is in progress, found operation url %s", serviceInstance.Status.OperationURL))
1✔
350
        smClient, err := r.GetSMClient(ctx, serviceInstance)
1✔
351
        if err != nil {
1✔
352
                log.Error(err, "failed to get sm client")
×
NEW
353
                return utils.HandleOperationFailure(ctx, r.Client, serviceInstance, common.Unknown, err)
×
354
        }
×
355

356
        status, statusErr := smClient.Status(serviceInstance.Status.OperationURL, nil)
1✔
357
        if statusErr != nil {
1✔
358
                log.Info(fmt.Sprintf("failed to fetch operation, got error from SM: %s", statusErr.Error()), "operationURL", serviceInstance.Status.OperationURL)
×
359
                utils.SetInProgressConditions(ctx, serviceInstance.Status.OperationType, string(smClientTypes.INPROGRESS), serviceInstance, false)
×
360
                // if failed to read operation status we cleanup the status to trigger re-sync from SM
×
361
                freshStatus := v1.ServiceInstanceStatus{Conditions: serviceInstance.GetConditions()}
×
362
                if utils.IsMarkedForDeletion(serviceInstance.ObjectMeta) {
×
363
                        freshStatus.InstanceID = serviceInstance.Status.InstanceID
×
364
                }
×
365
                serviceInstance.Status = freshStatus
×
366
                if err := utils.UpdateStatus(ctx, r.Client, serviceInstance); err != nil {
×
367
                        log.Error(err, "failed to update status during polling")
×
368
                }
×
369
                return ctrl.Result{}, statusErr
×
370
        }
371

372
        if status == nil {
1✔
373
                log.Error(fmt.Errorf("last operation is nil"), fmt.Sprintf("polling %s returned nil", serviceInstance.Status.OperationURL))
×
374
                return ctrl.Result{}, fmt.Errorf("last operation is nil")
×
375
        }
×
376
        switch status.State {
1✔
377
        case smClientTypes.INPROGRESS:
1✔
378
                fallthrough
1✔
379
        case smClientTypes.PENDING:
1✔
380
                if len(status.Description) > 0 {
1✔
381
                        log.Info(fmt.Sprintf("last operation description is '%s'", status.Description))
×
382
                        utils.SetInProgressConditions(ctx, status.Type, status.Description, serviceInstance, true)
×
383
                        if err := utils.UpdateStatus(ctx, r.Client, serviceInstance); err != nil {
×
384
                                log.Error(err, "unable to update ServiceInstance polling description")
×
385
                                return ctrl.Result{}, err
×
386
                        }
×
387
                }
388
                return ctrl.Result{RequeueAfter: r.Config.PollInterval}, nil
1✔
389
        case smClientTypes.FAILED:
1✔
390
                errMsg := getErrorMsgFromLastOperation(status)
1✔
391
                utils.SetFailureConditions(status.Type, errMsg, serviceInstance, true)
1✔
392
                // in order to delete eventually the object we need return with error
1✔
393
                if serviceInstance.Status.OperationType == smClientTypes.DELETE {
2✔
394
                        serviceInstance.Status.OperationURL = ""
1✔
395
                        serviceInstance.Status.OperationType = ""
1✔
396
                        if err := utils.UpdateStatus(ctx, r.Client, serviceInstance); err != nil {
1✔
397
                                return ctrl.Result{}, err
×
398
                        }
×
399
                        return ctrl.Result{}, errors.New(errMsg)
1✔
400
                }
401
        case smClientTypes.SUCCEEDED:
1✔
402
                if serviceInstance.Status.OperationType == smClientTypes.CREATE {
2✔
403
                        smInstance, err := smClient.GetInstanceByID(serviceInstance.Status.InstanceID, nil)
1✔
404
                        if err != nil {
1✔
405
                                log.Error(err, fmt.Sprintf("instance %s succeeded but could not fetch it from SM", serviceInstance.Status.InstanceID))
×
406
                                return ctrl.Result{}, err
×
407
                        }
×
408
                        if len(smInstance.Labels["subaccount_id"]) > 0 {
2✔
409
                                serviceInstance.Status.SubaccountID = smInstance.Labels["subaccount_id"][0]
1✔
410
                        }
1✔
411
                        serviceInstance.Status.Ready = metav1.ConditionTrue
1✔
412
                } else if serviceInstance.Status.OperationType == smClientTypes.DELETE {
2✔
413
                        // delete was successful - remove our finalizer from the list and update it.
1✔
414
                        if err := utils.RemoveFinalizer(ctx, r.Client, serviceInstance, common.FinalizerName); err != nil {
1✔
415
                                return ctrl.Result{}, err
×
416
                        }
×
417
                }
418
                utils.SetSuccessConditions(status.Type, serviceInstance, true)
1✔
419
        }
420

421
        serviceInstance.Status.OperationURL = ""
1✔
422
        serviceInstance.Status.OperationType = ""
1✔
423

1✔
424
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, serviceInstance)
1✔
425
}
426

427
func (r *ServiceInstanceReconciler) handleAsyncDelete(ctx context.Context, serviceInstance *v1.ServiceInstance, opURL string) (ctrl.Result, error) {
1✔
428
        serviceInstance.Status.OperationURL = opURL
1✔
429
        serviceInstance.Status.OperationType = smClientTypes.DELETE
1✔
430
        utils.SetInProgressConditions(ctx, smClientTypes.DELETE, "", serviceInstance, false)
1✔
431

1✔
432
        if err := utils.UpdateStatus(ctx, r.Client, serviceInstance); err != nil {
1✔
433
                return ctrl.Result{}, err
×
434
        }
×
435

436
        return ctrl.Result{RequeueAfter: r.Config.PollInterval}, nil
1✔
437
}
438

439
func (r *ServiceInstanceReconciler) getInstanceForRecovery(ctx context.Context, smClient sm.Client, serviceInstance *v1.ServiceInstance) (*smClientTypes.ServiceInstance, error) {
1✔
440
        log := utils.GetLogger(ctx)
1✔
441
        parameters := sm.Parameters{
1✔
442
                FieldQuery: []string{
1✔
443
                        fmt.Sprintf("name eq '%s'", serviceInstance.Spec.ExternalName),
1✔
444
                        fmt.Sprintf("context/clusterid eq '%s'", r.Config.ClusterID),
1✔
445
                        fmt.Sprintf("context/namespace eq '%s'", serviceInstance.Namespace)},
1✔
446
                LabelQuery: []string{
1✔
447
                        fmt.Sprintf("%s eq '%s'", common.K8sNameLabel, serviceInstance.Name)},
1✔
448
                GeneralParams: []string{"attach_last_operations=true"},
1✔
449
        }
1✔
450

1✔
451
        instances, err := smClient.ListInstances(&parameters)
1✔
452
        if err != nil {
1✔
453
                log.Error(err, "failed to list instances in SM")
×
454
                return nil, err
×
455
        }
×
456

457
        if instances != nil && len(instances.ServiceInstances) > 0 {
2✔
458
                return &instances.ServiceInstances[0], nil
1✔
459
        }
1✔
460
        log.Info("instance not found in SM")
1✔
461
        return nil, nil
1✔
462
}
463

464
func (r *ServiceInstanceReconciler) recover(ctx context.Context, smClient sm.Client, k8sInstance *v1.ServiceInstance, smInstance *smClientTypes.ServiceInstance) (ctrl.Result, error) {
1✔
465
        log := utils.GetLogger(ctx)
1✔
466

1✔
467
        log.Info(fmt.Sprintf("found existing instance in SM with id %s, updating status", smInstance.ID))
1✔
468
        updateHashedSpecValue(k8sInstance)
1✔
469
        if smInstance.Ready {
2✔
470
                k8sInstance.Status.Ready = metav1.ConditionTrue
1✔
471
        }
1✔
472
        if smInstance.Shared {
1✔
NEW
473
                utils.SetSharedCondition(k8sInstance, metav1.ConditionTrue, common.ShareSucceeded, "Instance shared successfully")
×
474
        }
×
475
        k8sInstance.Status.InstanceID = smInstance.ID
1✔
476
        k8sInstance.Status.OperationURL = ""
1✔
477
        k8sInstance.Status.OperationType = ""
1✔
478
        tags, err := getOfferingTags(smClient, smInstance.ServicePlanID)
1✔
479
        if err != nil {
2✔
480
                log.Error(err, "could not recover offering tags")
1✔
481
        }
1✔
482
        if len(tags) > 0 {
1✔
483
                k8sInstance.Status.Tags = tags
×
484
        }
×
485

486
        instanceState := smClientTypes.SUCCEEDED
1✔
487
        operationType := smClientTypes.CREATE
1✔
488
        description := ""
1✔
489
        if smInstance.LastOperation != nil {
2✔
490
                instanceState = smInstance.LastOperation.State
1✔
491
                operationType = smInstance.LastOperation.Type
1✔
492
                description = smInstance.LastOperation.Description
1✔
493
        } else if !smInstance.Ready {
3✔
494
                instanceState = smClientTypes.FAILED
1✔
495
        }
1✔
496

497
        switch instanceState {
1✔
498
        case smClientTypes.PENDING:
1✔
499
                fallthrough
1✔
500
        case smClientTypes.INPROGRESS:
1✔
501
                k8sInstance.Status.OperationURL = sm.BuildOperationURL(smInstance.LastOperation.ID, smInstance.ID, smClientTypes.ServiceInstancesURL)
1✔
502
                k8sInstance.Status.OperationType = smInstance.LastOperation.Type
1✔
503
                utils.SetInProgressConditions(ctx, smInstance.LastOperation.Type, smInstance.LastOperation.Description, k8sInstance, false)
1✔
504
        case smClientTypes.SUCCEEDED:
1✔
505
                utils.SetSuccessConditions(operationType, k8sInstance, false)
1✔
506
        case smClientTypes.FAILED:
1✔
507
                utils.SetFailureConditions(operationType, description, k8sInstance, false)
1✔
508
        }
509

510
        return ctrl.Result{}, utils.UpdateStatus(ctx, r.Client, k8sInstance)
1✔
511
}
512

513
func (r *ServiceInstanceReconciler) buildSMRequestParameters(ctx context.Context, serviceInstance *v1.ServiceInstance) ([]byte, error) {
1✔
514
        log := utils.GetLogger(ctx)
1✔
515
        instanceParameters, paramSecrets, err := utils.BuildSMRequestParameters(serviceInstance.Namespace, serviceInstance.Spec.Parameters, serviceInstance.Spec.ParametersFrom)
1✔
516
        if err != nil {
2✔
517
                log.Error(err, "failed to build instance parameters")
1✔
518
                return nil, err
1✔
519
        }
1✔
520
        instanceLabelsChanged := false
1✔
521
        newInstanceLabels := make(map[string]string)
1✔
522
        if serviceInstance.IsSubscribedToParamSecretsChanges() {
2✔
523
                // find all new secrets on the instance
1✔
524
                for _, secret := range paramSecrets {
2✔
525
                        labelKey := utils.GetLabelKeyForInstanceSecret(secret.Name)
1✔
526
                        newInstanceLabels[labelKey] = secret.Name
1✔
527
                        if _, ok := serviceInstance.Labels[labelKey]; !ok {
2✔
528
                                instanceLabelsChanged = true
1✔
529
                        }
1✔
530

531
                        if err := utils.AddWatchForSecretIfNeeded(ctx, r.Client, secret, string(serviceInstance.UID)); err != nil {
1✔
532
                                log.Error(err, fmt.Sprintf("failed to mark secret for watch %s", secret.Name))
×
533
                                return nil, err
×
534
                        }
×
535

536
                }
537
        }
538

539
        //sync instance labels
540
        for labelKey, labelValue := range serviceInstance.Labels {
2✔
541
                if strings.HasPrefix(labelKey, common.InstanceSecretRefLabel) {
2✔
542
                        if _, ok := newInstanceLabels[labelKey]; !ok {
2✔
543
                                log.Info(fmt.Sprintf("params secret named %s was removed, unwatching it", labelValue))
1✔
544
                                instanceLabelsChanged = true
1✔
545
                                if err := utils.RemoveWatchForSecret(ctx, r.Client, types.NamespacedName{Name: labelValue, Namespace: serviceInstance.Namespace}, string(serviceInstance.UID)); err != nil {
1✔
546
                                        log.Error(err, fmt.Sprintf("failed to unwatch secret %s", labelValue))
×
547
                                        return nil, err
×
548
                                }
×
549
                        }
550
                } else {
×
551
                        // this label not related to secrets, add it
×
552
                        newInstanceLabels[labelKey] = labelValue
×
553
                }
×
554
        }
555
        if instanceLabelsChanged {
2✔
556
                serviceInstance.Labels = newInstanceLabels
1✔
557
                log.Info("updating instance with secret labels")
1✔
558
                return instanceParameters, r.Client.Update(ctx, serviceInstance)
1✔
559
        }
1✔
560

561
        return instanceParameters, nil
1✔
562
}
563

564
func isFinalState(ctx context.Context, serviceInstance *v1.ServiceInstance) bool {
1✔
565
        log := utils.GetLogger(ctx)
1✔
566

1✔
567
        if serviceInstance.Status.ForceReconcile {
2✔
568
                log.Info("instance is not in final state, ForceReconcile is true")
1✔
569
                return false
1✔
570
        }
1✔
571

572
        observedGen := common.GetObservedGeneration(serviceInstance)
1✔
573
        if serviceInstance.Generation != observedGen {
2✔
574
                log.Info(fmt.Sprintf("instance is not in final state, generation: %d, observedGen: %d", serviceInstance.Generation, observedGen))
1✔
575
                return false
1✔
576
        }
1✔
577

578
        if utils.ShouldRetryOperation(serviceInstance) {
2✔
579
                log.Info("instance is not in final state, last operation failed, retrying")
1✔
580
                return false
1✔
581
        }
1✔
582

583
        if shareOrUnshareRequired(serviceInstance) {
2✔
584
                log.Info("instance is not in final state, need to sync sharing status")
1✔
585
                if len(serviceInstance.Status.HashedSpec) == 0 {
1✔
586
                        updateHashedSpecValue(serviceInstance)
×
587
                }
×
588
                return false
1✔
589
        }
590

591
        log.Info(fmt.Sprintf("instance is in final state (generation: %d)", serviceInstance.Generation))
1✔
592
        return true
1✔
593
}
594

595
func updateRequired(serviceInstance *v1.ServiceInstance) bool {
1✔
596
        //update is not supported for failed instances (this can occur when instance creation was asynchronously)
1✔
597
        if serviceInstance.Status.Ready != metav1.ConditionTrue {
1✔
598
                return false
×
599
        }
×
600

601
        if serviceInstance.Status.ForceReconcile {
2✔
602
                return true
1✔
603
        }
1✔
604

605
        cond := meta.FindStatusCondition(serviceInstance.Status.Conditions, common.ConditionSucceeded)
1✔
606
        if cond != nil && cond.Reason == common.UpdateInProgress { //in case of transient error occurred
1✔
UNCOV
607
                return true
×
UNCOV
608
        }
×
609

610
        return serviceInstance.GetSpecHash() != serviceInstance.Status.HashedSpec
1✔
611
}
612

613
func shareOrUnshareRequired(serviceInstance *v1.ServiceInstance) bool {
1✔
614
        //relevant only for non-shared instances - sharing instance is possible only for usable instances
1✔
615
        if serviceInstance.Status.Ready != metav1.ConditionTrue {
2✔
616
                return false
1✔
617
        }
1✔
618

619
        sharedCondition := meta.FindStatusCondition(serviceInstance.GetConditions(), common.ConditionShared)
1✔
620
        if sharedCondition == nil {
2✔
621
                return serviceInstance.GetShared()
1✔
622
        }
1✔
623

624
        if sharedCondition.Reason == common.ShareNotSupported {
2✔
625
                return false
1✔
626
        }
1✔
627

628
        if sharedCondition.Status == metav1.ConditionFalse {
2✔
629
                // instance does not appear to be shared, should share it if shared is requested
1✔
630
                return serviceInstance.GetShared()
1✔
631
        }
1✔
632

633
        // instance appears to be shared, should unshare it if shared is not requested
634
        return !serviceInstance.GetShared()
1✔
635
}
636

637
func getOfferingTags(smClient sm.Client, planID string) ([]string, error) {
1✔
638
        planQuery := &sm.Parameters{
1✔
639
                FieldQuery: []string{fmt.Sprintf("id eq '%s'", planID)},
1✔
640
        }
1✔
641
        plans, err := smClient.ListPlans(planQuery)
1✔
642
        if err != nil {
1✔
643
                return nil, err
×
644
        }
×
645

646
        if plans == nil || len(plans.ServicePlans) != 1 {
2✔
647
                return nil, fmt.Errorf("could not find plan with id %s", planID)
1✔
648
        }
1✔
649

650
        offeringQuery := &sm.Parameters{
×
651
                FieldQuery: []string{fmt.Sprintf("id eq '%s'", plans.ServicePlans[0].ServiceOfferingID)},
×
652
        }
×
653

×
654
        offerings, err := smClient.ListOfferings(offeringQuery)
×
655
        if err != nil {
×
656
                return nil, err
×
657
        }
×
658
        if offerings == nil || len(offerings.ServiceOfferings) != 1 {
×
659
                return nil, fmt.Errorf("could not find offering with id %s", plans.ServicePlans[0].ServiceOfferingID)
×
660
        }
×
661

662
        var tags []string
×
663
        if err := json.Unmarshal(offerings.ServiceOfferings[0].Tags, &tags); err != nil {
×
664
                return nil, err
×
665
        }
×
666
        return tags, nil
×
667
}
668

669
func getTags(tags []byte) ([]string, error) {
1✔
670
        var tagsArr []string
1✔
671
        if err := json.Unmarshal(tags, &tagsArr); err != nil {
1✔
672
                return nil, err
×
673
        }
×
674
        return tagsArr, nil
1✔
675
}
676

677
func updateHashedSpecValue(serviceInstance *v1.ServiceInstance) {
1✔
678
        serviceInstance.Status.HashedSpec = serviceInstance.GetSpecHash()
1✔
679
}
1✔
680

681
func getErrorMsgFromLastOperation(status *smClientTypes.Operation) string {
1✔
682
        errMsg := "async operation error"
1✔
683
        if status == nil || len(status.Errors) == 0 {
1✔
684
                return errMsg
×
685
        }
×
686
        var errMap map[string]interface{}
1✔
687

1✔
688
        if err := json.Unmarshal(status.Errors, &errMap); err != nil {
1✔
689
                return errMsg
×
690
        }
×
691

692
        if description, found := errMap["description"]; found {
2✔
693
                if descStr, ok := description.(string); ok {
2✔
694
                        errMsg = descStr
1✔
695
                }
1✔
696
        }
697
        return errMsg
1✔
698
}
699

700
type SecretPredicate struct {
701
        predicate.Funcs
702
}
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