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

SAP / sap-btp-service-operator / 6389632336

03 Oct 2023 06:47AM UTC coverage: 80.893% (-0.1%) from 81.007%
6389632336

push

github

web-flow
support multiple subaccounts in one namespace (#341)


Co-authored-by: I501080 <keren.lahav@sap.com>

118 of 118 new or added lines in 6 files covered. (100.0%)

2354 of 2910 relevant lines covered (80.89%)

0.91 hits per line

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

82.15
/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/apimachinery/pkg/api/meta"
28
        "k8s.io/client-go/util/workqueue"
29
        "k8s.io/utils/pointer"
30
        "sigs.k8s.io/controller-runtime/pkg/controller"
31

32
        servicesv1 "github.com/SAP/sap-btp-service-operator/api/v1"
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
        serviceInstance.SetObservedGeneration(serviceInstance.Generation)
1✔
73

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

81
        smClient, err := r.getSMClient(ctx, serviceInstance, serviceInstance.Spec.SubaccountID)
1✔
82
        if err != nil {
1✔
83
                log.Error(err, "failed to get sm client")
×
84
                return r.markAsTransientError(ctx, Unknown, err.Error(), serviceInstance)
×
85
        }
×
86

87
        if isDelete(serviceInstance.ObjectMeta) {
2✔
88
                return r.deleteInstance(ctx, smClient, serviceInstance)
1✔
89
        }
1✔
90

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

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

104
        if isFinalState(serviceInstance) {
2✔
105
                log.Info(fmt.Sprintf("Final state, spec did not change, and we are not in progress - ignoring... Generation is - %v", serviceInstance.Generation))
1✔
106
                if len(serviceInstance.Status.HashedSpec) == 0 {
1✔
107
                        updateHashedSpecValue(serviceInstance)
×
108
                        return ctrl.Result{}, r.Client.Status().Update(ctx, serviceInstance)
×
109
                }
×
110
                return ctrl.Result{}, nil
1✔
111
        }
112

113
        if serviceInstance.Status.InstanceID == "" {
2✔
114
                // Recovery
1✔
115
                log.Info("Instance ID is empty, checking if instance exist in SM")
1✔
116
                instance, err := r.getInstanceForRecovery(ctx, smClient, serviceInstance)
1✔
117
                if err != nil {
1✔
118
                        log.Error(err, "failed to check instance recovery")
×
119
                        return r.markAsTransientError(ctx, Unknown, err.Error(), serviceInstance)
×
120
                }
×
121
                if instance != nil {
2✔
122
                        log.Info(fmt.Sprintf("found existing instance in SM with id %s, updating status", instance.ID))
1✔
123
                        r.resyncInstanceStatus(ctx, smClient, serviceInstance, instance)
1✔
124
                        return ctrl.Result{}, r.updateStatus(ctx, serviceInstance)
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) handleInstanceSharing(ctx context.Context, serviceInstance *servicesv1.ServiceInstance, smClient sm.Client) (ctrl.Result, error) {
1✔
150
        log := GetLogger(ctx)
1✔
151
        log.Info("Handling change in instance sharing")
1✔
152

1✔
153
        if serviceInstance.ShouldBeShared() {
2✔
154
                log.Info("Service instance is shouldBeShared, sharing the instance")
1✔
155
                err := smClient.ShareInstance(serviceInstance.Status.InstanceID, buildUserInfo(ctx, serviceInstance.Spec.UserInfo))
1✔
156
                if err != nil {
2✔
157
                        log.Error(err, "failed to share instance")
1✔
158
                        return r.handleInstanceSharingError(ctx, serviceInstance, metav1.ConditionFalse, ShareFailed, err)
1✔
159
                }
1✔
160
                log.Info("instance shared successfully")
1✔
161
                setSharedCondition(serviceInstance, metav1.ConditionTrue, ShareSucceeded, "instance shared successfully")
1✔
162
        } else { //un-share
1✔
163
                log.Info("Service instance is un-shouldBeShared, un-sharing the instance")
1✔
164
                err := smClient.UnShareInstance(serviceInstance.Status.InstanceID, buildUserInfo(ctx, serviceInstance.Spec.UserInfo))
1✔
165
                if err != nil {
2✔
166
                        log.Error(err, "failed to un-share instance")
1✔
167
                        return r.handleInstanceSharingError(ctx, serviceInstance, metav1.ConditionTrue, UnShareFailed, err)
1✔
168
                }
1✔
169
                log.Info("instance un-shared successfully")
1✔
170
                if serviceInstance.Spec.Shared != nil {
2✔
171
                        setSharedCondition(serviceInstance, metav1.ConditionFalse, UnShareSucceeded, "instance un-shared successfully")
1✔
172
                } else {
2✔
173
                        log.Info("removing Shared condition since shared is undefined in instance")
1✔
174
                        conditions := serviceInstance.GetConditions()
1✔
175
                        meta.RemoveStatusCondition(&conditions, api.ConditionShared)
1✔
176
                        serviceInstance.SetConditions(conditions)
1✔
177
                }
1✔
178
        }
179

180
        return ctrl.Result{}, r.updateStatus(ctx, serviceInstance)
1✔
181
}
182

183
func (r *ServiceInstanceReconciler) poll(ctx context.Context, smClient sm.Client, serviceInstance *servicesv1.ServiceInstance) (ctrl.Result, error) {
1✔
184
        log := GetLogger(ctx)
1✔
185
        log.Info(fmt.Sprintf("resource is in progress, found operation url %s", serviceInstance.Status.OperationURL))
1✔
186
        status, statusErr := smClient.Status(serviceInstance.Status.OperationURL, nil)
1✔
187
        if statusErr != nil {
1✔
188
                log.Info(fmt.Sprintf("failed to fetch operation, got error from SM: %s", statusErr.Error()), "operationURL", serviceInstance.Status.OperationURL)
×
189
                setInProgressConditions(serviceInstance.Status.OperationType, statusErr.Error(), serviceInstance)
×
190
                // if failed to read operation status we cleanup the status to trigger re-sync from SM
×
191
                freshStatus := servicesv1.ServiceInstanceStatus{Conditions: serviceInstance.GetConditions(), ObservedGeneration: serviceInstance.Generation}
×
192
                if isDelete(serviceInstance.ObjectMeta) {
×
193
                        freshStatus.InstanceID = serviceInstance.Status.InstanceID
×
194
                }
×
195
                serviceInstance.Status = freshStatus
×
196
                if err := r.updateStatus(ctx, serviceInstance); err != nil {
×
197
                        log.Error(err, "failed to update status during polling")
×
198
                }
×
199
                return ctrl.Result{}, statusErr
×
200
        }
201

202
        if status == nil {
1✔
203
                log.Error(fmt.Errorf("last operation is nil"), fmt.Sprintf("polling %s returned nil", serviceInstance.Status.OperationURL))
×
204
                return ctrl.Result{}, fmt.Errorf("last operation is nil")
×
205
        }
×
206
        switch status.State {
1✔
207
        case smClientTypes.INPROGRESS:
1✔
208
                fallthrough
1✔
209
        case smClientTypes.PENDING:
1✔
210
                return ctrl.Result{Requeue: true, RequeueAfter: r.Config.PollInterval}, nil
1✔
211
        case smClientTypes.FAILED:
1✔
212
                errMsg := getErrorMsg(status)
1✔
213
                setFailureConditions(status.Type, errMsg, serviceInstance)
1✔
214
                // in order to delete eventually the object we need return with error
1✔
215
                if serviceInstance.Status.OperationType == smClientTypes.DELETE {
2✔
216
                        serviceInstance.Status.OperationURL = ""
1✔
217
                        serviceInstance.Status.OperationType = ""
1✔
218
                        if err := r.updateStatus(ctx, serviceInstance); err != nil {
1✔
219
                                return ctrl.Result{}, err
×
220
                        }
×
221
                        return ctrl.Result{}, fmt.Errorf(errMsg)
1✔
222
                }
223
        case smClientTypes.SUCCEEDED:
1✔
224
                setSuccessConditions(status.Type, serviceInstance)
1✔
225
                if serviceInstance.Status.OperationType == smClientTypes.DELETE {
2✔
226
                        // delete was successful - remove our finalizer from the list and update it.
1✔
227
                        if err := r.removeFinalizer(ctx, serviceInstance, api.FinalizerName); err != nil {
1✔
228
                                return ctrl.Result{}, err
×
229
                        }
×
230
                } else if serviceInstance.Status.OperationType == smClientTypes.CREATE {
2✔
231
                        serviceInstance.Status.Ready = metav1.ConditionTrue
1✔
232
                }
1✔
233
        }
234

235
        serviceInstance.Status.OperationURL = ""
1✔
236
        serviceInstance.Status.OperationType = ""
1✔
237

1✔
238
        return ctrl.Result{}, r.updateStatus(ctx, serviceInstance)
1✔
239
}
240

241
func getErrorMsg(status *smClientTypes.Operation) string {
1✔
242
        errMsg := "async operation error"
1✔
243
        if status == nil || len(status.Errors) == 0 {
1✔
244
                return errMsg
×
245
        }
×
246
        var errMap map[string]interface{}
1✔
247

1✔
248
        if err := json.Unmarshal(status.Errors, &errMap); err != nil {
1✔
249
                return errMsg
×
250
        }
×
251

252
        if description, found := errMap["description"]; found {
2✔
253
                if descStr, ok := description.(string); ok {
2✔
254
                        errMsg = descStr
1✔
255
                }
1✔
256
        }
257
        return errMsg
1✔
258
}
259

260
func (r *ServiceInstanceReconciler) createInstance(ctx context.Context, smClient sm.Client, serviceInstance *servicesv1.ServiceInstance) (ctrl.Result, error) {
1✔
261
        log := GetLogger(ctx)
1✔
262
        log.Info("Creating instance in SM")
1✔
263
        updateHashedSpecValue(serviceInstance)
1✔
264
        _, instanceParameters, err := buildParameters(r.Client, serviceInstance.Namespace, serviceInstance.Spec.ParametersFrom, serviceInstance.Spec.Parameters)
1✔
265
        if err != nil {
1✔
266
                // if parameters are invalid there is nothing we can do, the user should fix it according to the error message in the condition
×
267
                log.Error(err, "failed to parse instance parameters")
×
268
                return r.markAsNonTransientError(ctx, smClientTypes.CREATE, err.Error(), serviceInstance)
×
269
        }
×
270

271
        provision, provisionErr := smClient.Provision(&smClientTypes.ServiceInstance{
1✔
272
                Name:          serviceInstance.Spec.ExternalName,
1✔
273
                ServicePlanID: serviceInstance.Spec.ServicePlanID,
1✔
274
                Parameters:    instanceParameters,
1✔
275
                Labels: smClientTypes.Labels{
1✔
276
                        namespaceLabel: []string{serviceInstance.Namespace},
1✔
277
                        k8sNameLabel:   []string{serviceInstance.Name},
1✔
278
                        clusterIDLabel: []string{r.Config.ClusterID},
1✔
279
                },
1✔
280
        }, serviceInstance.Spec.ServiceOfferingName, serviceInstance.Spec.ServicePlanName, nil, buildUserInfo(ctx, serviceInstance.Spec.UserInfo), serviceInstance.Spec.DataCenter)
1✔
281

1✔
282
        if provisionErr != nil {
2✔
283
                log.Error(provisionErr, "failed to create service instance", "serviceOfferingName", serviceInstance.Spec.ServiceOfferingName,
1✔
284
                        "servicePlanName", serviceInstance.Spec.ServicePlanName)
1✔
285
                return r.handleError(ctx, smClientTypes.CREATE, provisionErr, serviceInstance)
1✔
286
        }
1✔
287

288
        if provision.Location != "" {
2✔
289
                serviceInstance.Status.InstanceID = provision.InstanceID
1✔
290
                serviceInstance.Status.SubaccountID = provision.SubaccountID
1✔
291
                if len(provision.Tags) > 0 {
1✔
292
                        tags, err := getTags(provision.Tags)
×
293
                        if err != nil {
×
294
                                log.Error(err, "failed to unmarshal tags")
×
295
                        } else {
×
296
                                serviceInstance.Status.Tags = tags
×
297
                        }
×
298
                }
299

300
                log.Info("Provision request is in progress")
1✔
301
                serviceInstance.Status.OperationURL = provision.Location
1✔
302
                serviceInstance.Status.OperationType = smClientTypes.CREATE
1✔
303
                setInProgressConditions(smClientTypes.CREATE, "", serviceInstance)
1✔
304

1✔
305
                if err := r.updateStatus(ctx, serviceInstance); err != nil {
1✔
306
                        return ctrl.Result{}, err
×
307
                }
×
308

309
                return ctrl.Result{Requeue: true, RequeueAfter: r.Config.PollInterval}, nil
1✔
310
        }
311

312
        serviceInstance.Status.InstanceID = provision.InstanceID
1✔
313
        serviceInstance.Status.SubaccountID = provision.SubaccountID
1✔
314
        log.Info(fmt.Sprintf("Instance provisioned successfully, instanceID: %s, subaccountID: %s", serviceInstance.Status.InstanceID,
1✔
315
                serviceInstance.Status.SubaccountID))
1✔
316

1✔
317
        if len(provision.Tags) > 0 {
2✔
318
                tags, err := getTags(provision.Tags)
1✔
319
                if err != nil {
1✔
320
                        log.Error(err, "failed to unmarshal tags")
×
321
                } else {
1✔
322
                        serviceInstance.Status.Tags = tags
1✔
323
                }
1✔
324
        }
325

326
        serviceInstance.Status.Ready = metav1.ConditionTrue
1✔
327
        setSuccessConditions(smClientTypes.CREATE, serviceInstance)
1✔
328
        return ctrl.Result{}, r.updateStatus(ctx, serviceInstance)
1✔
329
}
330

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

1✔
335
        updateHashedSpecValue(serviceInstance)
1✔
336

1✔
337
        _, instanceParameters, err := buildParameters(r.Client, serviceInstance.Namespace, serviceInstance.Spec.ParametersFrom, serviceInstance.Spec.Parameters)
1✔
338
        if err != nil {
1✔
339
                log.Error(err, "failed to parse instance parameters")
×
340
                return r.markAsNonTransientError(ctx, smClientTypes.UPDATE, fmt.Sprintf("failed to parse parameters: %v", err.Error()), serviceInstance)
×
341
        }
×
342

343
        _, operationURL, err := smClient.UpdateInstance(serviceInstance.Status.InstanceID, &smClientTypes.ServiceInstance{
1✔
344
                Name:          serviceInstance.Spec.ExternalName,
1✔
345
                ServicePlanID: serviceInstance.Spec.ServicePlanID,
1✔
346
                Parameters:    instanceParameters,
1✔
347
        }, serviceInstance.Spec.ServiceOfferingName, serviceInstance.Spec.ServicePlanName, nil, buildUserInfo(ctx, serviceInstance.Spec.UserInfo), serviceInstance.Spec.DataCenter)
1✔
348

1✔
349
        if err != nil {
2✔
350
                log.Error(err, fmt.Sprintf("failed to update service instance with ID %s", serviceInstance.Status.InstanceID))
1✔
351
                return r.handleError(ctx, smClientTypes.UPDATE, err, serviceInstance)
1✔
352
        }
1✔
353

354
        if operationURL != "" {
2✔
355
                log.Info(fmt.Sprintf("Update request accepted, operation URL: %s", operationURL))
1✔
356
                serviceInstance.Status.OperationURL = operationURL
1✔
357
                serviceInstance.Status.OperationType = smClientTypes.UPDATE
1✔
358
                setInProgressConditions(smClientTypes.UPDATE, "", serviceInstance)
1✔
359

1✔
360
                if err := r.updateStatus(ctx, serviceInstance); err != nil {
2✔
361
                        return ctrl.Result{}, err
1✔
362
                }
1✔
363

364
                return ctrl.Result{Requeue: true, RequeueAfter: r.Config.PollInterval}, nil
1✔
365
        }
366
        log.Info("Instance updated successfully")
1✔
367
        setSuccessConditions(smClientTypes.UPDATE, serviceInstance)
1✔
368
        return ctrl.Result{}, r.updateStatus(ctx, serviceInstance)
1✔
369
}
370

371
func (r *ServiceInstanceReconciler) deleteInstance(ctx context.Context, smClient sm.Client, serviceInstance *servicesv1.ServiceInstance) (ctrl.Result, error) {
1✔
372
        log := GetLogger(ctx)
1✔
373
        if controllerutil.ContainsFinalizer(serviceInstance, api.FinalizerName) {
2✔
374
                if len(serviceInstance.Status.InstanceID) == 0 {
2✔
375
                        log.Info("No instance id found validating instance does not exists in SM before removing finalizer")
1✔
376

1✔
377
                        smInstance, err := r.getInstanceForRecovery(ctx, smClient, serviceInstance)
1✔
378
                        if err != nil {
1✔
379
                                return ctrl.Result{}, err
×
380
                        }
×
381
                        if smInstance != nil {
2✔
382
                                log.Info("instance exists in SM continue with deletion")
1✔
383
                                serviceInstance.Status.InstanceID = smInstance.ID
1✔
384
                                setInProgressConditions(smClientTypes.DELETE, "delete after recovery", serviceInstance)
1✔
385
                                return ctrl.Result{}, r.updateStatus(ctx, serviceInstance)
1✔
386
                        }
1✔
387
                        log.Info("instance does not exists in SM, removing finalizer")
1✔
388
                        return ctrl.Result{}, r.removeFinalizer(ctx, serviceInstance, api.FinalizerName)
1✔
389
                }
390

391
                if len(serviceInstance.Status.OperationURL) > 0 && serviceInstance.Status.OperationType == smClientTypes.DELETE {
2✔
392
                        // ongoing delete operation - poll status from SM
1✔
393
                        return r.poll(ctx, smClient, serviceInstance)
1✔
394
                }
1✔
395

396
                log.Info(fmt.Sprintf("Deleting instance with id %v from SM", serviceInstance.Status.InstanceID))
1✔
397
                operationURL, deprovisionErr := smClient.Deprovision(serviceInstance.Status.InstanceID, nil, buildUserInfo(ctx, serviceInstance.Spec.UserInfo))
1✔
398
                if deprovisionErr != nil {
2✔
399
                        // delete will proceed anyway
1✔
400
                        return r.markAsNonTransientError(ctx, smClientTypes.DELETE, deprovisionErr.Error(), serviceInstance)
1✔
401
                }
1✔
402

403
                if operationURL != "" {
2✔
404
                        log.Info("Deleting instance async")
1✔
405
                        return r.handleAsyncDelete(ctx, serviceInstance, operationURL)
1✔
406
                }
1✔
407

408
                // remove our finalizer from the list and update it.
409
                if err := r.removeFinalizer(ctx, serviceInstance, api.FinalizerName); err != nil {
1✔
410
                        return ctrl.Result{}, err
×
411
                }
×
412

413
                log.Info("Instance was deleted successfully")
1✔
414
                serviceInstance.Status.InstanceID = ""
1✔
415
                setSuccessConditions(smClientTypes.DELETE, serviceInstance)
1✔
416
                if err := r.updateStatus(ctx, serviceInstance); err != nil {
2✔
417
                        return ctrl.Result{}, err
1✔
418
                }
1✔
419

420
                // Stop reconciliation as the item is being deleted
421
                return ctrl.Result{}, nil
×
422

423
        }
424
        return ctrl.Result{}, nil
×
425
}
426

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

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

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

439
func (r *ServiceInstanceReconciler) resyncInstanceStatus(ctx context.Context, smClient sm.Client, k8sInstance *servicesv1.ServiceInstance, smInstance *smClientTypes.ServiceInstance) {
1✔
440
        log := GetLogger(ctx)
1✔
441

1✔
442
        updateHashedSpecValue(k8sInstance)
1✔
443
        // set observed generation to 0 because we dont know which generation the current state in SM represents,
1✔
444
        // unless the generation is 1 and SM is in the same state as operator
1✔
445
        if k8sInstance.Generation == 1 {
2✔
446
                k8sInstance.SetObservedGeneration(1)
1✔
447
        } else {
1✔
448
                k8sInstance.SetObservedGeneration(0)
×
449
        }
×
450

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

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

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

493
func (r *ServiceInstanceReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
494
        return ctrl.NewControllerManagedBy(mgr).
1✔
495
                For(&servicesv1.ServiceInstance{}).
1✔
496
                WithOptions(controller.Options{RateLimiter: workqueue.NewItemExponentialFailureRateLimiter(r.Config.RetryBaseDelay, r.Config.RetryMaxDelay)}).
1✔
497
                Complete(r)
1✔
498
}
1✔
499

500
func (r *ServiceInstanceReconciler) getInstanceForRecovery(ctx context.Context, smClient sm.Client, serviceInstance *servicesv1.ServiceInstance) (*smClientTypes.ServiceInstance, error) {
1✔
501
        log := GetLogger(ctx)
1✔
502
        parameters := sm.Parameters{
1✔
503
                FieldQuery: []string{
1✔
504
                        fmt.Sprintf("name eq '%s'", serviceInstance.Spec.ExternalName),
1✔
505
                        fmt.Sprintf("context/clusterid eq '%s'", r.Config.ClusterID),
1✔
506
                        fmt.Sprintf("context/namespace eq '%s'", serviceInstance.Namespace)},
1✔
507
                LabelQuery: []string{
1✔
508
                        fmt.Sprintf("%s eq '%s'", k8sNameLabel, serviceInstance.Name)},
1✔
509
                GeneralParams: []string{"attach_last_operations=true"},
1✔
510
        }
1✔
511

1✔
512
        instances, err := smClient.ListInstances(&parameters)
1✔
513
        if err != nil {
1✔
514
                log.Error(err, "failed to list instances in SM")
×
515
                return nil, err
×
516
        }
×
517

518
        if instances != nil && len(instances.ServiceInstances) > 0 {
2✔
519
                return &instances.ServiceInstances[0], nil
1✔
520
        }
1✔
521
        log.Info("instance not found in SM")
1✔
522
        return nil, nil
1✔
523
}
524

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

1✔
528
        errMsg := err.Error()
1✔
529
        isTransient := false
1✔
530

1✔
531
        if smError, ok := err.(*sm.ServiceManagerError); ok {
2✔
532
                log.Info(fmt.Sprintf("SM returned error with status code %d", smError.StatusCode))
1✔
533
                isTransient = isTransientError(smError, log)
1✔
534
                errMsg = smError.Error()
1✔
535

1✔
536
                if smError.StatusCode == http.StatusTooManyRequests {
2✔
537
                        errMsg = "in progress"
1✔
538
                        reason = InProgress
1✔
539
                } else if reason == ShareFailed &&
2✔
540
                        (smError.StatusCode == http.StatusBadRequest || smError.StatusCode == http.StatusInternalServerError) {
2✔
541
                        /* non-transient error may occur only when sharing
1✔
542
                           SM return 400 when plan is not sharable
1✔
543
                           SM returns 500 when TOGGLES_ENABLE_INSTANCE_SHARE_FROM_OPERATOR feature toggle is off */
1✔
544
                        reason = ShareNotSupported
1✔
545
                }
1✔
546
        }
547

548
        setSharedCondition(object, status, reason, errMsg)
1✔
549
        return ctrl.Result{Requeue: isTransient}, r.updateStatus(ctx, object)
1✔
550
}
551

552
func isFinalState(serviceInstance *servicesv1.ServiceInstance) bool {
1✔
553
        // succeeded condition represents last operation, and it is constantly synced with generation
1✔
554
        succeededCondition := meta.FindStatusCondition(serviceInstance.GetConditions(), api.ConditionSucceeded)
1✔
555
        if succeededCondition == nil || succeededCondition.ObservedGeneration != serviceInstance.Generation {
2✔
556
                return false
1✔
557
        }
1✔
558

559
        // succeeded=false for current generation, and without failed=true --> transient error retry
560
        if isInProgress(serviceInstance) {
2✔
561
                return false
1✔
562
        }
1✔
563

564
        // for cases of instance update while polling for create/update
565
        if getSpecHash(serviceInstance) != serviceInstance.Status.HashedSpec {
2✔
566
                return false
1✔
567
        }
1✔
568

569
        return !sharingUpdateRequired(serviceInstance)
1✔
570
}
571

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

579
        cond := meta.FindStatusCondition(serviceInstance.Status.Conditions, api.ConditionSucceeded)
1✔
580
        if cond != nil && cond.Reason == UpdateInProgress { //in case of transient error occurred
2✔
581
                return true
1✔
582
        }
1✔
583

584
        return getSpecHash(serviceInstance) != serviceInstance.Status.HashedSpec
1✔
585
}
586

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

594
        sharedCondition := meta.FindStatusCondition(serviceInstance.GetConditions(), api.ConditionShared)
1✔
595
        shouldBeShared := serviceInstance.ShouldBeShared()
1✔
596

1✔
597
        if sharedCondition == nil {
2✔
598
                return shouldBeShared
1✔
599
        }
1✔
600

601
        if sharedCondition.Reason == ShareNotSupported {
2✔
602
                return false
1✔
603
        }
1✔
604

605
        if sharedCondition.Reason == InProgress || sharedCondition.Reason == ShareFailed || sharedCondition.Reason == UnShareFailed {
2✔
606
                return true
1✔
607
        }
1✔
608

609
        if shouldBeShared {
2✔
610
                return sharedCondition.Status == metav1.ConditionFalse
1✔
611
        }
1✔
612

613
        return sharedCondition.Status == metav1.ConditionTrue
1✔
614
}
615

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

625
        if plans == nil || len(plans.ServicePlans) != 1 {
2✔
626
                return nil, fmt.Errorf("could not find plan with id %s", planID)
1✔
627
        }
1✔
628

629
        offeringQuery := &sm.Parameters{
×
630
                FieldQuery: []string{fmt.Sprintf("id eq '%s'", plans.ServicePlans[0].ServiceOfferingID)},
×
631
        }
×
632

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

641
        var tags []string
×
642
        if err := json.Unmarshal(offerings.ServiceOfferings[0].Tags, &tags); err != nil {
×
643
                return nil, err
×
644
        }
×
645
        return tags, nil
×
646
}
647

648
func getTags(tags []byte) ([]string, error) {
1✔
649
        var tagsArr []string
1✔
650
        if err := json.Unmarshal(tags, &tagsArr); err != nil {
1✔
651
                return nil, err
×
652
        }
×
653
        return tagsArr, nil
1✔
654
}
655

656
func getSpecHash(serviceInstance *servicesv1.ServiceInstance) string {
1✔
657
        spec := serviceInstance.Spec
1✔
658
        spec.Shared = pointer.Bool(false)
1✔
659
        specBytes, _ := json.Marshal(spec)
1✔
660
        s := string(specBytes)
1✔
661
        return generateEncodedMD5Hash(s)
1✔
662
}
1✔
663

664
func generateEncodedMD5Hash(str string) string {
1✔
665
        hash := md5.Sum([]byte(str))
1✔
666
        return hex.EncodeToString(hash[:])
1✔
667
}
1✔
668

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

679
        shareCondition := metav1.Condition{
1✔
680
                Type:    api.ConditionShared,
1✔
681
                Status:  status,
1✔
682
                Reason:  reason,
1✔
683
                Message: msg,
1✔
684
                // shared condition does not contain observed generation
1✔
685
        }
1✔
686

1✔
687
        // remove shared condition and add it as new (in case it has observed generation)
1✔
688
        meta.RemoveStatusCondition(&conditions, api.ConditionShared)
1✔
689
        meta.SetStatusCondition(&conditions, shareCondition)
1✔
690

1✔
691
        object.SetConditions(conditions)
1✔
692
}
693

694
func updateHashedSpecValue(serviceInstance *servicesv1.ServiceInstance) {
1✔
695
        serviceInstance.Status.HashedSpec = getSpecHash(serviceInstance)
1✔
696
}
1✔
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