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

SAP / sap-btp-service-operator / 6639907680

25 Oct 2023 11:49AM UTC coverage: 81.156% (+0.3%) from 80.842%
6639907680

push

github

web-flow
bug fix - avoid instance update if spec not changed (#359)

47 of 47 new or added lines in 4 files covered. (100.0%)

2386 of 2940 relevant lines covered (81.16%)

0.92 hits per line

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

83.46
/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
        "crypto/md5"
22
        "encoding/hex"
23
        "encoding/json"
24
        "fmt"
25
        "net/http"
26

27
        "k8s.io/client-go/util/workqueue"
28
        "sigs.k8s.io/controller-runtime/pkg/controller"
29

30
        servicesv1 "github.com/SAP/sap-btp-service-operator/api/v1"
31
        "k8s.io/apimachinery/pkg/api/meta"
32
        "k8s.io/utils/pointer"
33

34
        "github.com/SAP/sap-btp-service-operator/api"
35

36
        "github.com/google/uuid"
37

38
        "github.com/SAP/sap-btp-service-operator/client/sm"
39
        smClientTypes "github.com/SAP/sap-btp-service-operator/client/sm/types"
40
        apierrors "k8s.io/apimachinery/pkg/api/errors"
41
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
42
        ctrl "sigs.k8s.io/controller-runtime"
43
        "sigs.k8s.io/controller-runtime/pkg/client"
44
        "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
45
)
46

47
// ServiceInstanceReconciler reconciles a ServiceInstance object
48
type ServiceInstanceReconciler struct {
49
        *BaseReconciler
50
}
51

52
// +kubebuilder:rbac:groups=services.cloud.sap.com,resources=serviceinstances,verbs=get;list;watch;create;update;patch;delete
53
// +kubebuilder:rbac:groups=services.cloud.sap.com,resources=serviceinstances/status,verbs=get;update;patch
54
// +kubebuilder:rbac:groups=core,resources=events,verbs=get;list;watch;create;update;patch;delete
55
// +kubebuilder:rbac:groups=coordination.k8s.io,resources=leases,verbs=get;list;create;update
56

57
func (r *ServiceInstanceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
1✔
58
        log := r.Log.WithValues("serviceinstance", req.NamespacedName).WithValues("correlation_id", uuid.New().String())
1✔
59
        ctx = context.WithValue(ctx, LogKey{}, log)
1✔
60

1✔
61
        serviceInstance := &servicesv1.ServiceInstance{}
1✔
62
        if err := r.Client.Get(ctx, req.NamespacedName, serviceInstance); err != nil {
2✔
63
                if !apierrors.IsNotFound(err) {
1✔
64
                        log.Error(err, "unable to fetch ServiceInstance")
×
65
                }
×
66
                // we'll ignore not-found errors, since they can't be fixed by an immediate
67
                // requeue (we'll need to wait for a new notification), and we can get them
68
                // on deleted requests.
69
                return ctrl.Result{}, client.IgnoreNotFound(err)
1✔
70
        }
71
        serviceInstance = serviceInstance.DeepCopy()
1✔
72

1✔
73
        if len(serviceInstance.GetConditions()) == 0 {
2✔
74
                err := r.init(ctx, serviceInstance)
1✔
75
                if err != nil {
1✔
76
                        return ctrl.Result{}, err
×
77
                }
×
78
        }
79

80
        if isFinalState(ctx, serviceInstance) {
2✔
81
                if len(serviceInstance.Status.HashedSpec) == 0 {
2✔
82
                        updateHashedSpecValue(serviceInstance)
1✔
83
                        return ctrl.Result{}, r.Client.Status().Update(ctx, serviceInstance)
1✔
84
                }
1✔
85
                return ctrl.Result{}, nil
1✔
86
        }
87

88
        if isMarkedForDeletion(serviceInstance.ObjectMeta) {
2✔
89
                // delete updates the generation
1✔
90
                serviceInstance.SetObservedGeneration(serviceInstance.Generation)
1✔
91
                return r.deleteInstance(ctx, serviceInstance)
1✔
92
        }
1✔
93

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

99
        if !controllerutil.ContainsFinalizer(serviceInstance, api.FinalizerName) {
2✔
100
                controllerutil.AddFinalizer(serviceInstance, api.FinalizerName)
1✔
101
                log.Info(fmt.Sprintf("added finalizer '%s' to service instance", api.FinalizerName))
1✔
102
                if err := r.Client.Update(ctx, serviceInstance); err != nil {
1✔
103
                        return ctrl.Result{}, err
×
104
                }
×
105
        }
106

107
        log.Info(fmt.Sprintf("instance is not in final state, handling... (generation: %d, observedGen: %d", serviceInstance.Generation, serviceInstance.Status.ObservedGeneration))
1✔
108
        serviceInstance.SetObservedGeneration(serviceInstance.Generation)
1✔
109

1✔
110
        smClient, err := r.getSMClient(ctx, serviceInstance, serviceInstance.Spec.SubaccountID)
1✔
111
        if err != nil {
1✔
112
                log.Error(err, "failed to get sm client")
×
113
                return r.markAsTransientError(ctx, Unknown, err.Error(), serviceInstance)
×
114
        }
×
115

116
        if serviceInstance.Status.InstanceID == "" {
2✔
117
                log.Info("Instance ID is empty, checking if instance exist in SM")
1✔
118
                smInstance, err := r.getInstanceForRecovery(ctx, smClient, serviceInstance)
1✔
119
                if err != nil {
1✔
120
                        log.Error(err, "failed to check instance recovery")
×
121
                        return r.markAsTransientError(ctx, Unknown, err.Error(), serviceInstance)
×
122
                }
×
123
                if smInstance != nil {
2✔
124
                        return r.recover(ctx, smClient, serviceInstance, smInstance)
1✔
125
                }
1✔
126

127
                // if instance was not recovered then create new instance
128
                return r.createInstance(ctx, smClient, serviceInstance)
1✔
129
        }
130

131
        // Update
132
        if updateRequired(serviceInstance) {
2✔
133
                if res, err := r.updateInstance(ctx, smClient, serviceInstance); err != nil {
2✔
134
                        log.Info("got error while trying to update instance")
1✔
135
                        return ctrl.Result{}, err
1✔
136
                } else if res.Requeue {
3✔
137
                        return res, nil
1✔
138
                }
1✔
139
        }
140

141
        // Handle instance share if needed
142
        if sharingUpdateRequired(serviceInstance) {
2✔
143
                return r.handleInstanceSharing(ctx, serviceInstance, smClient)
1✔
144
        }
1✔
145

146
        return ctrl.Result{}, nil
1✔
147
}
148

149
func (r *ServiceInstanceReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
150
        return ctrl.NewControllerManagedBy(mgr).
1✔
151
                For(&servicesv1.ServiceInstance{}).
1✔
152
                WithOptions(controller.Options{RateLimiter: workqueue.NewItemExponentialFailureRateLimiter(r.Config.RetryBaseDelay, r.Config.RetryMaxDelay)}).
1✔
153
                Complete(r)
1✔
154
}
1✔
155

156
func (r *ServiceInstanceReconciler) createInstance(ctx context.Context, smClient sm.Client, serviceInstance *servicesv1.ServiceInstance) (ctrl.Result, error) {
1✔
157
        log := GetLogger(ctx)
1✔
158
        log.Info("Creating instance in SM")
1✔
159
        updateHashedSpecValue(serviceInstance)
1✔
160
        _, instanceParameters, err := buildParameters(r.Client, serviceInstance.Namespace, serviceInstance.Spec.ParametersFrom, serviceInstance.Spec.Parameters)
1✔
161
        if err != nil {
1✔
162
                // if parameters are invalid there is nothing we can do, the user should fix it according to the error message in the condition
×
163
                log.Error(err, "failed to parse instance parameters")
×
164
                return r.markAsNonTransientError(ctx, smClientTypes.CREATE, err.Error(), serviceInstance)
×
165
        }
×
166

167
        provision, provisionErr := smClient.Provision(&smClientTypes.ServiceInstance{
1✔
168
                Name:          serviceInstance.Spec.ExternalName,
1✔
169
                ServicePlanID: serviceInstance.Spec.ServicePlanID,
1✔
170
                Parameters:    instanceParameters,
1✔
171
                Labels: smClientTypes.Labels{
1✔
172
                        namespaceLabel: []string{serviceInstance.Namespace},
1✔
173
                        k8sNameLabel:   []string{serviceInstance.Name},
1✔
174
                        clusterIDLabel: []string{r.Config.ClusterID},
1✔
175
                },
1✔
176
        }, serviceInstance.Spec.ServiceOfferingName, serviceInstance.Spec.ServicePlanName, nil, buildUserInfo(ctx, serviceInstance.Spec.UserInfo), serviceInstance.Spec.DataCenter)
1✔
177

1✔
178
        if provisionErr != nil {
2✔
179
                log.Error(provisionErr, "failed to create service instance", "serviceOfferingName", serviceInstance.Spec.ServiceOfferingName,
1✔
180
                        "servicePlanName", serviceInstance.Spec.ServicePlanName)
1✔
181
                return r.handleError(ctx, smClientTypes.CREATE, provisionErr, serviceInstance)
1✔
182
        }
1✔
183

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

195
        if provision.Location != "" {
2✔
196
                log.Info("Provision request is in progress (async)")
1✔
197
                serviceInstance.Status.OperationURL = provision.Location
1✔
198
                serviceInstance.Status.OperationType = smClientTypes.CREATE
1✔
199
                setInProgressConditions(ctx, smClientTypes.CREATE, "", serviceInstance)
1✔
200

1✔
201
                return ctrl.Result{Requeue: true, RequeueAfter: r.Config.PollInterval}, r.updateStatus(ctx, serviceInstance)
1✔
202
        }
1✔
203

204
        log.Info(fmt.Sprintf("Instance provisioned successfully, instanceID: %s, subaccountID: %s", serviceInstance.Status.InstanceID,
1✔
205
                serviceInstance.Status.SubaccountID))
1✔
206
        setSuccessConditions(smClientTypes.CREATE, serviceInstance)
1✔
207
        return ctrl.Result{}, r.updateStatus(ctx, serviceInstance)
1✔
208
}
209

210
func (r *ServiceInstanceReconciler) updateInstance(ctx context.Context, smClient sm.Client, serviceInstance *servicesv1.ServiceInstance) (ctrl.Result, error) {
1✔
211
        log := GetLogger(ctx)
1✔
212
        log.Info(fmt.Sprintf("updating instance %s in SM", serviceInstance.Status.InstanceID))
1✔
213

1✔
214
        updateHashedSpecValue(serviceInstance)
1✔
215

1✔
216
        _, instanceParameters, err := buildParameters(r.Client, serviceInstance.Namespace, serviceInstance.Spec.ParametersFrom, serviceInstance.Spec.Parameters)
1✔
217
        if err != nil {
1✔
218
                log.Error(err, "failed to parse instance parameters")
×
219
                return r.markAsNonTransientError(ctx, smClientTypes.UPDATE, fmt.Sprintf("failed to parse parameters: %v", err.Error()), serviceInstance)
×
220
        }
×
221

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

1✔
228
        if err != nil {
2✔
229
                log.Error(err, fmt.Sprintf("failed to update service instance with ID %s", serviceInstance.Status.InstanceID))
1✔
230
                return r.handleError(ctx, smClientTypes.UPDATE, err, serviceInstance)
1✔
231
        }
1✔
232

233
        if operationURL != "" {
2✔
234
                log.Info(fmt.Sprintf("Update request accepted, operation URL: %s", operationURL))
1✔
235
                serviceInstance.Status.OperationURL = operationURL
1✔
236
                serviceInstance.Status.OperationType = smClientTypes.UPDATE
1✔
237
                setInProgressConditions(ctx, smClientTypes.UPDATE, "", serviceInstance)
1✔
238

1✔
239
                if err := r.updateStatus(ctx, serviceInstance); err != nil {
2✔
240
                        return ctrl.Result{}, err
1✔
241
                }
1✔
242

243
                return ctrl.Result{Requeue: true, RequeueAfter: r.Config.PollInterval}, nil
1✔
244
        }
245
        log.Info("Instance updated successfully")
1✔
246
        setSuccessConditions(smClientTypes.UPDATE, serviceInstance)
1✔
247
        return ctrl.Result{}, r.updateStatus(ctx, serviceInstance)
1✔
248
}
249

250
func (r *ServiceInstanceReconciler) deleteInstance(ctx context.Context, serviceInstance *servicesv1.ServiceInstance) (ctrl.Result, error) {
1✔
251
        log := GetLogger(ctx)
1✔
252

1✔
253
        if controllerutil.ContainsFinalizer(serviceInstance, api.FinalizerName) {
2✔
254
                smClient, err := r.getSMClient(ctx, serviceInstance, serviceInstance.Spec.SubaccountID)
1✔
255
                if err != nil {
1✔
256
                        log.Error(err, "failed to get sm client")
×
257
                        return r.markAsTransientError(ctx, Unknown, err.Error(), serviceInstance)
×
258
                }
×
259
                if len(serviceInstance.Status.InstanceID) == 0 {
2✔
260
                        log.Info("No instance id found validating instance does not exists in SM before removing finalizer")
1✔
261
                        smInstance, err := r.getInstanceForRecovery(ctx, smClient, serviceInstance)
1✔
262
                        if err != nil {
1✔
263
                                return ctrl.Result{}, err
×
264
                        }
×
265
                        if smInstance != nil {
2✔
266
                                log.Info("instance exists in SM continue with deletion")
1✔
267
                                serviceInstance.Status.InstanceID = smInstance.ID
1✔
268
                                setInProgressConditions(ctx, smClientTypes.DELETE, "delete after recovery", serviceInstance)
1✔
269
                                return ctrl.Result{}, r.updateStatus(ctx, serviceInstance)
1✔
270
                        }
1✔
271
                        log.Info("instance does not exists in SM, removing finalizer")
1✔
272
                        return ctrl.Result{}, r.removeFinalizer(ctx, serviceInstance, api.FinalizerName)
1✔
273
                }
274

275
                if len(serviceInstance.Status.OperationURL) > 0 && serviceInstance.Status.OperationType == smClientTypes.DELETE {
2✔
276
                        // ongoing delete operation - poll status from SM
1✔
277
                        return r.poll(ctx, serviceInstance)
1✔
278
                }
1✔
279

280
                log.Info(fmt.Sprintf("Deleting instance with id %v from SM", serviceInstance.Status.InstanceID))
1✔
281
                operationURL, deprovisionErr := smClient.Deprovision(serviceInstance.Status.InstanceID, nil, buildUserInfo(ctx, serviceInstance.Spec.UserInfo))
1✔
282
                if deprovisionErr != nil {
2✔
283
                        // delete will proceed anyway
1✔
284
                        return r.markAsNonTransientError(ctx, smClientTypes.DELETE, deprovisionErr.Error(), serviceInstance)
1✔
285
                }
1✔
286

287
                if operationURL != "" {
2✔
288
                        log.Info("Deleting instance async")
1✔
289
                        return r.handleAsyncDelete(ctx, serviceInstance, operationURL)
1✔
290
                }
1✔
291

292
                log.Info("Instance was deleted successfully, removing finalizer")
1✔
293
                // remove our finalizer from the list and update it.
1✔
294
                return ctrl.Result{}, r.removeFinalizer(ctx, serviceInstance, api.FinalizerName)
1✔
295
        }
296
        return ctrl.Result{}, nil
×
297
}
298

299
func (r *ServiceInstanceReconciler) handleInstanceSharing(ctx context.Context, serviceInstance *servicesv1.ServiceInstance, smClient sm.Client) (ctrl.Result, error) {
1✔
300
        log := GetLogger(ctx)
1✔
301
        log.Info("Handling change in instance sharing")
1✔
302

1✔
303
        if serviceInstance.ShouldBeShared() {
2✔
304
                log.Info("Service instance is shouldBeShared, sharing the instance")
1✔
305
                err := smClient.ShareInstance(serviceInstance.Status.InstanceID, buildUserInfo(ctx, serviceInstance.Spec.UserInfo))
1✔
306
                if err != nil {
2✔
307
                        log.Error(err, "failed to share instance")
1✔
308
                        return r.handleInstanceSharingError(ctx, serviceInstance, metav1.ConditionFalse, ShareFailed, err)
1✔
309
                }
1✔
310
                log.Info("instance shared successfully")
1✔
311
                setSharedCondition(serviceInstance, metav1.ConditionTrue, ShareSucceeded, "instance shared successfully")
1✔
312
        } else { //un-share
1✔
313
                log.Info("Service instance is un-shouldBeShared, un-sharing the instance")
1✔
314
                err := smClient.UnShareInstance(serviceInstance.Status.InstanceID, buildUserInfo(ctx, serviceInstance.Spec.UserInfo))
1✔
315
                if err != nil {
2✔
316
                        log.Error(err, "failed to un-share instance")
1✔
317
                        return r.handleInstanceSharingError(ctx, serviceInstance, metav1.ConditionTrue, UnShareFailed, err)
1✔
318
                }
1✔
319
                log.Info("instance un-shared successfully")
1✔
320
                if serviceInstance.Spec.Shared != nil {
2✔
321
                        setSharedCondition(serviceInstance, metav1.ConditionFalse, UnShareSucceeded, "instance un-shared successfully")
1✔
322
                } else {
2✔
323
                        log.Info("removing Shared condition since shared is undefined in instance")
1✔
324
                        conditions := serviceInstance.GetConditions()
1✔
325
                        meta.RemoveStatusCondition(&conditions, api.ConditionShared)
1✔
326
                        serviceInstance.SetConditions(conditions)
1✔
327
                }
1✔
328
        }
329

330
        return ctrl.Result{}, r.updateStatus(ctx, serviceInstance)
1✔
331
}
332

333
func (r *ServiceInstanceReconciler) poll(ctx context.Context, serviceInstance *servicesv1.ServiceInstance) (ctrl.Result, error) {
1✔
334
        log := GetLogger(ctx)
1✔
335
        log.Info(fmt.Sprintf("resource is in progress, found operation url %s", serviceInstance.Status.OperationURL))
1✔
336
        smClient, err := r.getSMClient(ctx, serviceInstance, serviceInstance.Spec.SubaccountID)
1✔
337
        if err != nil {
1✔
338
                log.Error(err, "failed to get sm client")
×
339
                return r.markAsTransientError(ctx, Unknown, err.Error(), serviceInstance)
×
340
        }
×
341

342
        status, statusErr := smClient.Status(serviceInstance.Status.OperationURL, nil)
1✔
343
        if statusErr != nil {
1✔
344
                log.Info(fmt.Sprintf("failed to fetch operation, got error from SM: %s", statusErr.Error()), "operationURL", serviceInstance.Status.OperationURL)
×
345
                setInProgressConditions(ctx, serviceInstance.Status.OperationType, statusErr.Error(), serviceInstance)
×
346
                // if failed to read operation status we cleanup the status to trigger re-sync from SM
×
347
                freshStatus := servicesv1.ServiceInstanceStatus{Conditions: serviceInstance.GetConditions(), ObservedGeneration: serviceInstance.Generation}
×
348
                if isMarkedForDeletion(serviceInstance.ObjectMeta) {
×
349
                        freshStatus.InstanceID = serviceInstance.Status.InstanceID
×
350
                }
×
351
                serviceInstance.Status = freshStatus
×
352
                if err := r.updateStatus(ctx, serviceInstance); err != nil {
×
353
                        log.Error(err, "failed to update status during polling")
×
354
                }
×
355
                return ctrl.Result{}, statusErr
×
356
        }
357

358
        if status == nil {
1✔
359
                log.Error(fmt.Errorf("last operation is nil"), fmt.Sprintf("polling %s returned nil", serviceInstance.Status.OperationURL))
×
360
                return ctrl.Result{}, fmt.Errorf("last operation is nil")
×
361
        }
×
362
        switch status.State {
1✔
363
        case smClientTypes.INPROGRESS:
1✔
364
                fallthrough
1✔
365
        case smClientTypes.PENDING:
1✔
366
                return ctrl.Result{Requeue: true, RequeueAfter: r.Config.PollInterval}, nil
1✔
367
        case smClientTypes.FAILED:
1✔
368
                errMsg := getErrorMsgFromLastOperation(status)
1✔
369
                setFailureConditions(status.Type, errMsg, serviceInstance)
1✔
370
                // in order to delete eventually the object we need return with error
1✔
371
                if serviceInstance.Status.OperationType == smClientTypes.DELETE {
2✔
372
                        serviceInstance.Status.OperationURL = ""
1✔
373
                        serviceInstance.Status.OperationType = ""
1✔
374
                        if err := r.updateStatus(ctx, serviceInstance); err != nil {
1✔
375
                                return ctrl.Result{}, err
×
376
                        }
×
377
                        return ctrl.Result{}, fmt.Errorf(errMsg)
1✔
378
                }
379
        case smClientTypes.SUCCEEDED:
1✔
380
                setSuccessConditions(status.Type, serviceInstance)
1✔
381
                if serviceInstance.Status.OperationType == smClientTypes.DELETE {
2✔
382
                        // delete was successful - remove our finalizer from the list and update it.
1✔
383
                        if err := r.removeFinalizer(ctx, serviceInstance, api.FinalizerName); err != nil {
1✔
384
                                return ctrl.Result{}, err
×
385
                        }
×
386
                } else if serviceInstance.Status.OperationType == smClientTypes.CREATE {
2✔
387
                        serviceInstance.Status.Ready = metav1.ConditionTrue
1✔
388
                }
1✔
389
        }
390

391
        serviceInstance.Status.OperationURL = ""
1✔
392
        serviceInstance.Status.OperationType = ""
1✔
393

1✔
394
        return ctrl.Result{}, r.updateStatus(ctx, serviceInstance)
1✔
395
}
396

397
func (r *ServiceInstanceReconciler) handleAsyncDelete(ctx context.Context, serviceInstance *servicesv1.ServiceInstance, opURL string) (ctrl.Result, error) {
1✔
398
        serviceInstance.Status.OperationURL = opURL
1✔
399
        serviceInstance.Status.OperationType = smClientTypes.DELETE
1✔
400
        setInProgressConditions(ctx, smClientTypes.DELETE, "", serviceInstance)
1✔
401

1✔
402
        if err := r.updateStatus(ctx, serviceInstance); err != nil {
1✔
403
                return ctrl.Result{}, err
×
404
        }
×
405

406
        return ctrl.Result{Requeue: true, RequeueAfter: r.Config.PollInterval}, nil
1✔
407
}
408

409
func (r *ServiceInstanceReconciler) getInstanceForRecovery(ctx context.Context, smClient sm.Client, serviceInstance *servicesv1.ServiceInstance) (*smClientTypes.ServiceInstance, error) {
1✔
410
        log := GetLogger(ctx)
1✔
411
        parameters := sm.Parameters{
1✔
412
                FieldQuery: []string{
1✔
413
                        fmt.Sprintf("name eq '%s'", serviceInstance.Spec.ExternalName),
1✔
414
                        fmt.Sprintf("context/clusterid eq '%s'", r.Config.ClusterID),
1✔
415
                        fmt.Sprintf("context/namespace eq '%s'", serviceInstance.Namespace)},
1✔
416
                LabelQuery: []string{
1✔
417
                        fmt.Sprintf("%s eq '%s'", k8sNameLabel, serviceInstance.Name)},
1✔
418
                GeneralParams: []string{"attach_last_operations=true"},
1✔
419
        }
1✔
420

1✔
421
        instances, err := smClient.ListInstances(&parameters)
1✔
422
        if err != nil {
1✔
423
                log.Error(err, "failed to list instances in SM")
×
424
                return nil, err
×
425
        }
×
426

427
        if instances != nil && len(instances.ServiceInstances) > 0 {
2✔
428
                return &instances.ServiceInstances[0], nil
1✔
429
        }
1✔
430
        log.Info("instance not found in SM")
1✔
431
        return nil, nil
1✔
432
}
433

434
func (r *ServiceInstanceReconciler) recover(ctx context.Context, smClient sm.Client, k8sInstance *servicesv1.ServiceInstance, smInstance *smClientTypes.ServiceInstance) (ctrl.Result, error) {
1✔
435
        log := GetLogger(ctx)
1✔
436

1✔
437
        log.Info(fmt.Sprintf("found existing instance in SM with id %s, updating status", smInstance.ID))
1✔
438
        updateHashedSpecValue(k8sInstance)
1✔
439
        // set observed generation to 0 because we dont know which generation the current state in SM represents,
1✔
440
        // unless the generation is 1 and SM is in the same state as operator
1✔
441
        if k8sInstance.Generation == 1 {
2✔
442
                k8sInstance.SetObservedGeneration(1)
1✔
443
        } else {
1✔
444
                k8sInstance.SetObservedGeneration(0)
×
445
        }
×
446

447
        if smInstance.Ready {
2✔
448
                k8sInstance.Status.Ready = metav1.ConditionTrue
1✔
449
        }
1✔
450
        if smInstance.Shared {
1✔
451
                setSharedCondition(k8sInstance, metav1.ConditionTrue, ShareSucceeded, "Instance shared successfully")
×
452
        }
×
453
        k8sInstance.Status.InstanceID = smInstance.ID
1✔
454
        k8sInstance.Status.OperationURL = ""
1✔
455
        k8sInstance.Status.OperationType = ""
1✔
456
        tags, err := getOfferingTags(smClient, smInstance.ServicePlanID)
1✔
457
        if err != nil {
2✔
458
                log.Error(err, "could not recover offering tags")
1✔
459
        }
1✔
460
        if len(tags) > 0 {
1✔
461
                k8sInstance.Status.Tags = tags
×
462
        }
×
463

464
        instanceState := smClientTypes.SUCCEEDED
1✔
465
        operationType := smClientTypes.CREATE
1✔
466
        description := ""
1✔
467
        if smInstance.LastOperation != nil {
2✔
468
                instanceState = smInstance.LastOperation.State
1✔
469
                operationType = smInstance.LastOperation.Type
1✔
470
                description = smInstance.LastOperation.Description
1✔
471
        } else if !smInstance.Ready {
3✔
472
                instanceState = smClientTypes.FAILED
1✔
473
        }
1✔
474

475
        switch instanceState {
1✔
476
        case smClientTypes.PENDING:
1✔
477
                fallthrough
1✔
478
        case smClientTypes.INPROGRESS:
1✔
479
                k8sInstance.Status.OperationURL = sm.BuildOperationURL(smInstance.LastOperation.ID, smInstance.ID, smClientTypes.ServiceInstancesURL)
1✔
480
                k8sInstance.Status.OperationType = smInstance.LastOperation.Type
1✔
481
                setInProgressConditions(ctx, smInstance.LastOperation.Type, smInstance.LastOperation.Description, k8sInstance)
1✔
482
        case smClientTypes.SUCCEEDED:
1✔
483
                setSuccessConditions(operationType, k8sInstance)
1✔
484
        case smClientTypes.FAILED:
1✔
485
                setFailureConditions(operationType, description, k8sInstance)
1✔
486
        }
487

488
        return ctrl.Result{}, r.updateStatus(ctx, k8sInstance)
1✔
489
}
490

491
func (r *ServiceInstanceReconciler) handleInstanceSharingError(ctx context.Context, object api.SAPBTPResource, status metav1.ConditionStatus, reason string, err error) (ctrl.Result, error) {
1✔
492
        log := GetLogger(ctx)
1✔
493

1✔
494
        errMsg := err.Error()
1✔
495
        isTransient := false
1✔
496

1✔
497
        if smError, ok := err.(*sm.ServiceManagerError); ok {
2✔
498
                log.Info(fmt.Sprintf("SM returned error with status code %d", smError.StatusCode))
1✔
499
                isTransient = isTransientError(smError, log)
1✔
500
                errMsg = smError.Error()
1✔
501

1✔
502
                if smError.StatusCode == http.StatusTooManyRequests {
2✔
503
                        errMsg = "in progress"
1✔
504
                        reason = InProgress
1✔
505
                } else if reason == ShareFailed &&
2✔
506
                        (smError.StatusCode == http.StatusBadRequest || smError.StatusCode == http.StatusInternalServerError) {
2✔
507
                        /* non-transient error may occur only when sharing
1✔
508
                           SM return 400 when plan is not sharable
1✔
509
                           SM returns 500 when TOGGLES_ENABLE_INSTANCE_SHARE_FROM_OPERATOR feature toggle is off */
1✔
510
                        reason = ShareNotSupported
1✔
511
                }
1✔
512
        }
513

514
        setSharedCondition(object, status, reason, errMsg)
1✔
515
        return ctrl.Result{Requeue: isTransient}, r.updateStatus(ctx, object)
1✔
516
}
517

518
func isFinalState(ctx context.Context, serviceInstance *servicesv1.ServiceInstance) bool {
1✔
519
        log := GetLogger(ctx)
1✔
520
        if isMarkedForDeletion(serviceInstance.ObjectMeta) {
2✔
521
                log.Info("instance is not in final state, it is marked for deletion")
1✔
522
                return false
1✔
523
        }
1✔
524
        if len(serviceInstance.Status.OperationURL) > 0 {
2✔
525
                log.Info(fmt.Sprintf("instance is not in final state, async operation is in progress (%s)", serviceInstance.Status.OperationURL))
1✔
526
                return false
1✔
527
        }
1✔
528
        if serviceInstance.Generation != serviceInstance.GetObservedGeneration() {
2✔
529
                log.Info(fmt.Sprintf("instance is not in final state, generation: %d, observedGen: %d", serviceInstance.Generation, serviceInstance.GetObservedGeneration()))
1✔
530
                return false
1✔
531
        }
1✔
532

533
        // succeeded=false for current generation, and without failed=true --> transient error retry
534
        if isInProgress(serviceInstance) {
2✔
535
                log.Info("instance is not in final state, sync operation is in progress")
1✔
536
                return false
1✔
537
        }
1✔
538

539
        if sharingUpdateRequired(serviceInstance) {
2✔
540
                log.Info("instance is not in final state, need to sync sharing status")
1✔
541
                if len(serviceInstance.Status.HashedSpec) == 0 {
1✔
542
                        updateHashedSpecValue(serviceInstance)
×
543
                }
×
544
                return false
1✔
545
        }
546

547
        log.Info(fmt.Sprintf("instance is in final state (generation: %d)", serviceInstance.Generation))
1✔
548
        return true
1✔
549
}
550

551
// TODO unit test
552
func updateRequired(serviceInstance *servicesv1.ServiceInstance) bool {
1✔
553
        //update is not supported for failed instances (this can occur when instance creation was asynchronously)
1✔
554
        if serviceInstance.Status.Ready != metav1.ConditionTrue {
1✔
555
                return false
×
556
        }
×
557

558
        cond := meta.FindStatusCondition(serviceInstance.Status.Conditions, api.ConditionSucceeded)
1✔
559
        if cond != nil && cond.Reason == UpdateInProgress { //in case of transient error occurred
2✔
560
                return true
1✔
561
        }
1✔
562

563
        return getSpecHash(serviceInstance) != serviceInstance.Status.HashedSpec
1✔
564
}
565

566
// TODO unit test
567
func sharingUpdateRequired(serviceInstance *servicesv1.ServiceInstance) bool {
1✔
568
        //relevant only for non-shared instances - sharing instance is possible only for usable instances
1✔
569
        if serviceInstance.Status.Ready != metav1.ConditionTrue {
2✔
570
                return false
1✔
571
        }
1✔
572

573
        sharedCondition := meta.FindStatusCondition(serviceInstance.GetConditions(), api.ConditionShared)
1✔
574
        shouldBeShared := serviceInstance.ShouldBeShared()
1✔
575

1✔
576
        if sharedCondition == nil {
2✔
577
                return shouldBeShared
1✔
578
        }
1✔
579

580
        if sharedCondition.Reason == ShareNotSupported {
2✔
581
                return false
1✔
582
        }
1✔
583

584
        if sharedCondition.Reason == InProgress || sharedCondition.Reason == ShareFailed || sharedCondition.Reason == UnShareFailed {
2✔
585
                return true
1✔
586
        }
1✔
587

588
        if shouldBeShared {
2✔
589
                return sharedCondition.Status == metav1.ConditionFalse
1✔
590
        }
1✔
591

592
        return sharedCondition.Status == metav1.ConditionTrue
1✔
593
}
594

595
func getOfferingTags(smClient sm.Client, planID string) ([]string, error) {
1✔
596
        planQuery := &sm.Parameters{
1✔
597
                FieldQuery: []string{fmt.Sprintf("id eq '%s'", planID)},
1✔
598
        }
1✔
599
        plans, err := smClient.ListPlans(planQuery)
1✔
600
        if err != nil {
1✔
601
                return nil, err
×
602
        }
×
603

604
        if plans == nil || len(plans.ServicePlans) != 1 {
2✔
605
                return nil, fmt.Errorf("could not find plan with id %s", planID)
1✔
606
        }
1✔
607

608
        offeringQuery := &sm.Parameters{
×
609
                FieldQuery: []string{fmt.Sprintf("id eq '%s'", plans.ServicePlans[0].ServiceOfferingID)},
×
610
        }
×
611

×
612
        offerings, err := smClient.ListOfferings(offeringQuery)
×
613
        if err != nil {
×
614
                return nil, err
×
615
        }
×
616
        if offerings == nil || len(offerings.ServiceOfferings) != 1 {
×
617
                return nil, fmt.Errorf("could not find offering with id %s", plans.ServicePlans[0].ServiceOfferingID)
×
618
        }
×
619

620
        var tags []string
×
621
        if err := json.Unmarshal(offerings.ServiceOfferings[0].Tags, &tags); err != nil {
×
622
                return nil, err
×
623
        }
×
624
        return tags, nil
×
625
}
626

627
func getTags(tags []byte) ([]string, error) {
1✔
628
        var tagsArr []string
1✔
629
        if err := json.Unmarshal(tags, &tagsArr); err != nil {
1✔
630
                return nil, err
×
631
        }
×
632
        return tagsArr, nil
1✔
633
}
634

635
func getSpecHash(serviceInstance *servicesv1.ServiceInstance) string {
1✔
636
        spec := serviceInstance.Spec
1✔
637
        spec.Shared = pointer.Bool(false)
1✔
638
        specBytes, _ := json.Marshal(spec)
1✔
639
        s := string(specBytes)
1✔
640
        return generateEncodedMD5Hash(s)
1✔
641
}
1✔
642

643
func generateEncodedMD5Hash(str string) string {
1✔
644
        hash := md5.Sum([]byte(str))
1✔
645
        return hex.EncodeToString(hash[:])
1✔
646
}
1✔
647

648
func setSharedCondition(object api.SAPBTPResource, status metav1.ConditionStatus, reason, msg string) {
1✔
649
        conditions := object.GetConditions()
1✔
650
        // align all conditions to latest generation
1✔
651
        for _, cond := range object.GetConditions() {
2✔
652
                if cond.Type != api.ConditionShared {
2✔
653
                        cond.ObservedGeneration = object.GetGeneration()
1✔
654
                        meta.SetStatusCondition(&conditions, cond)
1✔
655
                }
1✔
656
        }
657

658
        shareCondition := metav1.Condition{
1✔
659
                Type:    api.ConditionShared,
1✔
660
                Status:  status,
1✔
661
                Reason:  reason,
1✔
662
                Message: msg,
1✔
663
                // shared condition does not contain observed generation
1✔
664
        }
1✔
665

1✔
666
        // remove shared condition and add it as new (in case it has observed generation)
1✔
667
        meta.RemoveStatusCondition(&conditions, api.ConditionShared)
1✔
668
        meta.SetStatusCondition(&conditions, shareCondition)
1✔
669

1✔
670
        object.SetConditions(conditions)
1✔
671
}
672

673
func updateHashedSpecValue(serviceInstance *servicesv1.ServiceInstance) {
1✔
674
        serviceInstance.Status.HashedSpec = getSpecHash(serviceInstance)
1✔
675
}
1✔
676

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

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

688
        if description, found := errMap["description"]; found {
2✔
689
                if descStr, ok := description.(string); ok {
2✔
690
                        errMsg = descStr
1✔
691
                }
1✔
692
        }
693
        return errMsg
1✔
694
}
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