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

SAP / sap-btp-service-operator / 17354254741

31 Aug 2025 07:45AM UTC coverage: 78.37% (-1.6%) from 80.011%
17354254741

Pull #538

github

kerenlahav
delete irrelevant comments
Pull Request #538: transient error - prototype

52 of 142 new or added lines in 5 files covered. (36.62%)

17 existing lines in 3 files now uncovered.

2750 of 3509 relevant lines covered (78.37%)

0.88 hits per line

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

80.27
/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.HandleServiceManagerError(ctx, r.Client, serviceInstance, smClientTypes.CREATE, 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✔
NEW
277
                                return utils.HandleServiceManagerError(ctx, r.Client, serviceInstance, smClientTypes.DELETE, 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
                        return utils.HandleServiceManagerError(ctx, r.Client, serviceInstance, smClientTypes.DELETE, deprovisionErr)
1✔
298
                }
1✔
299

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

535
                }
536
        }
537

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

560
        return instanceParameters, nil
1✔
561
}
562

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

© 2025 Coveralls, Inc