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

SAP / sap-btp-service-operator / 7047761642

30 Nov 2023 02:18PM UTC coverage: 80.82% (+0.2%) from 80.639%
7047761642

push

github

web-flow
include async operation description in status (#372)

1 of 7 new or added lines in 1 file covered. (14.29%)

6 existing lines in 3 files now uncovered.

2385 of 2951 relevant lines covered (80.82%)

0.91 hits per line

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

81.85
/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.BTPAccessCredentialsSecret)
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 {
1✔
UNCOV
240
                        return ctrl.Result{}, err
×
UNCOV
241
                }
×
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.BTPAccessCredentialsSecret)
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.BTPAccessCredentialsSecret)
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
                if len(status.Description) > 0 {
1✔
NEW
367
                        log.Info("last operation description is '%s'", status.Description)
×
NEW
368
                        setInProgressConditions(ctx, status.Type, status.Description, serviceInstance)
×
NEW
369
                        if err := r.updateStatus(ctx, serviceInstance); err != nil {
×
NEW
370
                                log.Error(err, "unable to update ServiceInstance polling description")
×
NEW
371
                                return ctrl.Result{}, err
×
NEW
372
                        }
×
373
                }
374
                return ctrl.Result{Requeue: true, RequeueAfter: r.Config.PollInterval}, nil
1✔
375
        case smClientTypes.FAILED:
1✔
376
                errMsg := getErrorMsgFromLastOperation(status)
1✔
377
                setFailureConditions(status.Type, errMsg, serviceInstance)
1✔
378
                // in order to delete eventually the object we need return with error
1✔
379
                if serviceInstance.Status.OperationType == smClientTypes.DELETE {
2✔
380
                        serviceInstance.Status.OperationURL = ""
1✔
381
                        serviceInstance.Status.OperationType = ""
1✔
382
                        if err := r.updateStatus(ctx, serviceInstance); err != nil {
1✔
383
                                return ctrl.Result{}, err
×
384
                        }
×
385
                        return ctrl.Result{}, fmt.Errorf(errMsg)
1✔
386
                }
387
        case smClientTypes.SUCCEEDED:
1✔
388
                if serviceInstance.Status.OperationType == smClientTypes.CREATE {
2✔
389
                        smInstance, err := smClient.GetInstanceByID(serviceInstance.Status.InstanceID, nil)
1✔
390
                        if err != nil {
1✔
391
                                log.Error(err, fmt.Sprintf("instance %s succeeded but could not fetch it from SM", serviceInstance.Status.InstanceID))
×
392
                                return ctrl.Result{}, err
×
393
                        }
×
394
                        if len(smInstance.Labels["subaccount_id"]) > 0 {
2✔
395
                                serviceInstance.Status.SubaccountID = smInstance.Labels["subaccount_id"][0]
1✔
396
                        }
1✔
397
                        serviceInstance.Status.Ready = metav1.ConditionTrue
1✔
398
                } else if serviceInstance.Status.OperationType == smClientTypes.DELETE {
2✔
399
                        // delete was successful - remove our finalizer from the list and update it.
1✔
400
                        if err := r.removeFinalizer(ctx, serviceInstance, api.FinalizerName); err != nil {
1✔
401
                                return ctrl.Result{}, err
×
402
                        }
×
403
                }
404
                setSuccessConditions(status.Type, serviceInstance)
1✔
405
        }
406

407
        serviceInstance.Status.OperationURL = ""
1✔
408
        serviceInstance.Status.OperationType = ""
1✔
409

1✔
410
        return ctrl.Result{}, r.updateStatus(ctx, serviceInstance)
1✔
411
}
412

413
func (r *ServiceInstanceReconciler) handleAsyncDelete(ctx context.Context, serviceInstance *servicesv1.ServiceInstance, opURL string) (ctrl.Result, error) {
1✔
414
        serviceInstance.Status.OperationURL = opURL
1✔
415
        serviceInstance.Status.OperationType = smClientTypes.DELETE
1✔
416
        setInProgressConditions(ctx, smClientTypes.DELETE, "", serviceInstance)
1✔
417

1✔
418
        if err := r.updateStatus(ctx, serviceInstance); err != nil {
1✔
419
                return ctrl.Result{}, err
×
420
        }
×
421

422
        return ctrl.Result{Requeue: true, RequeueAfter: r.Config.PollInterval}, nil
1✔
423
}
424

425
func (r *ServiceInstanceReconciler) getInstanceForRecovery(ctx context.Context, smClient sm.Client, serviceInstance *servicesv1.ServiceInstance) (*smClientTypes.ServiceInstance, error) {
1✔
426
        log := GetLogger(ctx)
1✔
427
        parameters := sm.Parameters{
1✔
428
                FieldQuery: []string{
1✔
429
                        fmt.Sprintf("name eq '%s'", serviceInstance.Spec.ExternalName),
1✔
430
                        fmt.Sprintf("context/clusterid eq '%s'", r.Config.ClusterID),
1✔
431
                        fmt.Sprintf("context/namespace eq '%s'", serviceInstance.Namespace)},
1✔
432
                LabelQuery: []string{
1✔
433
                        fmt.Sprintf("%s eq '%s'", k8sNameLabel, serviceInstance.Name)},
1✔
434
                GeneralParams: []string{"attach_last_operations=true"},
1✔
435
        }
1✔
436

1✔
437
        instances, err := smClient.ListInstances(&parameters)
1✔
438
        if err != nil {
1✔
439
                log.Error(err, "failed to list instances in SM")
×
440
                return nil, err
×
441
        }
×
442

443
        if instances != nil && len(instances.ServiceInstances) > 0 {
2✔
444
                return &instances.ServiceInstances[0], nil
1✔
445
        }
1✔
446
        log.Info("instance not found in SM")
1✔
447
        return nil, nil
1✔
448
}
449

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

1✔
453
        log.Info(fmt.Sprintf("found existing instance in SM with id %s, updating status", smInstance.ID))
1✔
454
        updateHashedSpecValue(k8sInstance)
1✔
455
        // set observed generation to 0 because we dont know which generation the current state in SM represents,
1✔
456
        // unless the generation is 1 and SM is in the same state as operator
1✔
457
        if k8sInstance.Generation == 1 {
2✔
458
                k8sInstance.SetObservedGeneration(1)
1✔
459
        } else {
1✔
460
                k8sInstance.SetObservedGeneration(0)
×
461
        }
×
462

463
        if smInstance.Ready {
2✔
464
                k8sInstance.Status.Ready = metav1.ConditionTrue
1✔
465
        }
1✔
466
        if smInstance.Shared {
1✔
467
                setSharedCondition(k8sInstance, metav1.ConditionTrue, ShareSucceeded, "Instance shared successfully")
×
468
        }
×
469
        k8sInstance.Status.InstanceID = smInstance.ID
1✔
470
        k8sInstance.Status.OperationURL = ""
1✔
471
        k8sInstance.Status.OperationType = ""
1✔
472
        tags, err := getOfferingTags(smClient, smInstance.ServicePlanID)
1✔
473
        if err != nil {
2✔
474
                log.Error(err, "could not recover offering tags")
1✔
475
        }
1✔
476
        if len(tags) > 0 {
1✔
477
                k8sInstance.Status.Tags = tags
×
478
        }
×
479

480
        instanceState := smClientTypes.SUCCEEDED
1✔
481
        operationType := smClientTypes.CREATE
1✔
482
        description := ""
1✔
483
        if smInstance.LastOperation != nil {
2✔
484
                instanceState = smInstance.LastOperation.State
1✔
485
                operationType = smInstance.LastOperation.Type
1✔
486
                description = smInstance.LastOperation.Description
1✔
487
        } else if !smInstance.Ready {
3✔
488
                instanceState = smClientTypes.FAILED
1✔
489
        }
1✔
490

491
        switch instanceState {
1✔
492
        case smClientTypes.PENDING:
1✔
493
                fallthrough
1✔
494
        case smClientTypes.INPROGRESS:
1✔
495
                k8sInstance.Status.OperationURL = sm.BuildOperationURL(smInstance.LastOperation.ID, smInstance.ID, smClientTypes.ServiceInstancesURL)
1✔
496
                k8sInstance.Status.OperationType = smInstance.LastOperation.Type
1✔
497
                setInProgressConditions(ctx, smInstance.LastOperation.Type, smInstance.LastOperation.Description, k8sInstance)
1✔
498
        case smClientTypes.SUCCEEDED:
1✔
499
                setSuccessConditions(operationType, k8sInstance)
1✔
500
        case smClientTypes.FAILED:
1✔
501
                setFailureConditions(operationType, description, k8sInstance)
1✔
502
        }
503

504
        return ctrl.Result{}, r.updateStatus(ctx, k8sInstance)
1✔
505
}
506

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

1✔
510
        errMsg := err.Error()
1✔
511
        isTransient := false
1✔
512

1✔
513
        if smError, ok := err.(*sm.ServiceManagerError); ok {
2✔
514
                log.Info(fmt.Sprintf("SM returned error with status code %d", smError.StatusCode))
1✔
515
                isTransient = isTransientError(smError, log)
1✔
516
                errMsg = smError.Error()
1✔
517

1✔
518
                if smError.StatusCode == http.StatusTooManyRequests {
2✔
519
                        errMsg = "in progress"
1✔
520
                        reason = InProgress
1✔
521
                } else if reason == ShareFailed &&
2✔
522
                        (smError.StatusCode == http.StatusBadRequest || smError.StatusCode == http.StatusInternalServerError) {
2✔
523
                        /* non-transient error may occur only when sharing
1✔
524
                           SM return 400 when plan is not sharable
1✔
525
                           SM returns 500 when TOGGLES_ENABLE_INSTANCE_SHARE_FROM_OPERATOR feature toggle is off */
1✔
526
                        reason = ShareNotSupported
1✔
527
                }
1✔
528
        }
529

530
        setSharedCondition(object, status, reason, errMsg)
1✔
531
        return ctrl.Result{Requeue: isTransient}, r.updateStatus(ctx, object)
1✔
532
}
533

534
func isFinalState(ctx context.Context, serviceInstance *servicesv1.ServiceInstance) bool {
1✔
535
        log := GetLogger(ctx)
1✔
536
        if isMarkedForDeletion(serviceInstance.ObjectMeta) {
2✔
537
                log.Info("instance is not in final state, it is marked for deletion")
1✔
538
                return false
1✔
539
        }
1✔
540
        if len(serviceInstance.Status.OperationURL) > 0 {
2✔
541
                log.Info(fmt.Sprintf("instance is not in final state, async operation is in progress (%s)", serviceInstance.Status.OperationURL))
1✔
542
                return false
1✔
543
        }
1✔
544
        if serviceInstance.Generation != serviceInstance.GetObservedGeneration() {
2✔
545
                log.Info(fmt.Sprintf("instance is not in final state, generation: %d, observedGen: %d", serviceInstance.Generation, serviceInstance.GetObservedGeneration()))
1✔
546
                return false
1✔
547
        }
1✔
548

549
        // succeeded=false for current generation, and without failed=true --> transient error retry
550
        if isInProgress(serviceInstance) {
2✔
551
                log.Info("instance is not in final state, sync operation is in progress")
1✔
552
                return false
1✔
553
        }
1✔
554

555
        if sharingUpdateRequired(serviceInstance) {
2✔
556
                log.Info("instance is not in final state, need to sync sharing status")
1✔
557
                if len(serviceInstance.Status.HashedSpec) == 0 {
1✔
558
                        updateHashedSpecValue(serviceInstance)
×
559
                }
×
560
                return false
1✔
561
        }
562

563
        log.Info(fmt.Sprintf("instance is in final state (generation: %d)", serviceInstance.Generation))
1✔
564
        return true
1✔
565
}
566

567
// TODO unit test
568
func updateRequired(serviceInstance *servicesv1.ServiceInstance) bool {
1✔
569
        //update is not supported for failed instances (this can occur when instance creation was asynchronously)
1✔
570
        if serviceInstance.Status.Ready != metav1.ConditionTrue {
1✔
571
                return false
×
572
        }
×
573

574
        cond := meta.FindStatusCondition(serviceInstance.Status.Conditions, api.ConditionSucceeded)
1✔
575
        if cond != nil && cond.Reason == UpdateInProgress { //in case of transient error occurred
2✔
576
                return true
1✔
577
        }
1✔
578

579
        return getSpecHash(serviceInstance) != serviceInstance.Status.HashedSpec
1✔
580
}
581

582
// TODO unit test
583
func sharingUpdateRequired(serviceInstance *servicesv1.ServiceInstance) bool {
1✔
584
        //relevant only for non-shared instances - sharing instance is possible only for usable instances
1✔
585
        if serviceInstance.Status.Ready != metav1.ConditionTrue {
2✔
586
                return false
1✔
587
        }
1✔
588

589
        sharedCondition := meta.FindStatusCondition(serviceInstance.GetConditions(), api.ConditionShared)
1✔
590
        shouldBeShared := serviceInstance.ShouldBeShared()
1✔
591

1✔
592
        if sharedCondition == nil {
2✔
593
                return shouldBeShared
1✔
594
        }
1✔
595

596
        if sharedCondition.Reason == ShareNotSupported {
2✔
597
                return false
1✔
598
        }
1✔
599

600
        if sharedCondition.Reason == InProgress || sharedCondition.Reason == ShareFailed || sharedCondition.Reason == UnShareFailed {
2✔
601
                return true
1✔
602
        }
1✔
603

604
        if shouldBeShared {
2✔
605
                return sharedCondition.Status == metav1.ConditionFalse
1✔
606
        }
1✔
607

608
        return sharedCondition.Status == metav1.ConditionTrue
1✔
609
}
610

611
func getOfferingTags(smClient sm.Client, planID string) ([]string, error) {
1✔
612
        planQuery := &sm.Parameters{
1✔
613
                FieldQuery: []string{fmt.Sprintf("id eq '%s'", planID)},
1✔
614
        }
1✔
615
        plans, err := smClient.ListPlans(planQuery)
1✔
616
        if err != nil {
1✔
617
                return nil, err
×
618
        }
×
619

620
        if plans == nil || len(plans.ServicePlans) != 1 {
2✔
621
                return nil, fmt.Errorf("could not find plan with id %s", planID)
1✔
622
        }
1✔
623

624
        offeringQuery := &sm.Parameters{
×
625
                FieldQuery: []string{fmt.Sprintf("id eq '%s'", plans.ServicePlans[0].ServiceOfferingID)},
×
626
        }
×
627

×
628
        offerings, err := smClient.ListOfferings(offeringQuery)
×
629
        if err != nil {
×
630
                return nil, err
×
631
        }
×
632
        if offerings == nil || len(offerings.ServiceOfferings) != 1 {
×
633
                return nil, fmt.Errorf("could not find offering with id %s", plans.ServicePlans[0].ServiceOfferingID)
×
634
        }
×
635

636
        var tags []string
×
637
        if err := json.Unmarshal(offerings.ServiceOfferings[0].Tags, &tags); err != nil {
×
638
                return nil, err
×
639
        }
×
640
        return tags, nil
×
641
}
642

643
func getTags(tags []byte) ([]string, error) {
1✔
644
        var tagsArr []string
1✔
645
        if err := json.Unmarshal(tags, &tagsArr); err != nil {
1✔
646
                return nil, err
×
647
        }
×
648
        return tagsArr, nil
1✔
649
}
650

651
func getSpecHash(serviceInstance *servicesv1.ServiceInstance) string {
1✔
652
        spec := serviceInstance.Spec
1✔
653
        spec.Shared = pointer.Bool(false)
1✔
654
        specBytes, _ := json.Marshal(spec)
1✔
655
        s := string(specBytes)
1✔
656
        return generateEncodedMD5Hash(s)
1✔
657
}
1✔
658

659
func generateEncodedMD5Hash(str string) string {
1✔
660
        hash := md5.Sum([]byte(str))
1✔
661
        return hex.EncodeToString(hash[:])
1✔
662
}
1✔
663

664
func setSharedCondition(object api.SAPBTPResource, status metav1.ConditionStatus, reason, msg string) {
1✔
665
        conditions := object.GetConditions()
1✔
666
        // align all conditions to latest generation
1✔
667
        for _, cond := range object.GetConditions() {
2✔
668
                if cond.Type != api.ConditionShared {
2✔
669
                        cond.ObservedGeneration = object.GetGeneration()
1✔
670
                        meta.SetStatusCondition(&conditions, cond)
1✔
671
                }
1✔
672
        }
673

674
        shareCondition := metav1.Condition{
1✔
675
                Type:    api.ConditionShared,
1✔
676
                Status:  status,
1✔
677
                Reason:  reason,
1✔
678
                Message: msg,
1✔
679
                // shared condition does not contain observed generation
1✔
680
        }
1✔
681

1✔
682
        // remove shared condition and add it as new (in case it has observed generation)
1✔
683
        meta.RemoveStatusCondition(&conditions, api.ConditionShared)
1✔
684
        meta.SetStatusCondition(&conditions, shareCondition)
1✔
685

1✔
686
        object.SetConditions(conditions)
1✔
687
}
688

689
func updateHashedSpecValue(serviceInstance *servicesv1.ServiceInstance) {
1✔
690
        serviceInstance.Status.HashedSpec = getSpecHash(serviceInstance)
1✔
691
}
1✔
692

693
func getErrorMsgFromLastOperation(status *smClientTypes.Operation) string {
1✔
694
        errMsg := "async operation error"
1✔
695
        if status == nil || len(status.Errors) == 0 {
1✔
696
                return errMsg
×
697
        }
×
698
        var errMap map[string]interface{}
1✔
699

1✔
700
        if err := json.Unmarshal(status.Errors, &errMap); err != nil {
1✔
701
                return errMsg
×
702
        }
×
703

704
        if description, found := errMap["description"]; found {
2✔
705
                if descStr, ok := description.(string); ok {
2✔
706
                        errMsg = descStr
1✔
707
                }
1✔
708
        }
709
        return errMsg
1✔
710
}
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