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

SAP / sap-btp-service-operator / 6651592994

26 Oct 2023 08:39AM UTC coverage: 80.639% (-0.4%) from 81.054%
6651592994

push

github

web-flow
Configure BTP access secret in instance spec (#363)

58 of 58 new or added lines in 5 files covered. (100.0%)

2374 of 2944 relevant lines covered (80.64%)

0.91 hits per line

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

83.11
/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 {
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.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
                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
                if serviceInstance.Status.OperationType == smClientTypes.CREATE {
2✔
381
                        smInstance, err := smClient.GetInstanceByID(serviceInstance.Status.InstanceID, nil)
1✔
382
                        if err != nil {
1✔
383
                                log.Error(err, fmt.Sprintf("instance %s succeeded but could not fetch it from SM", serviceInstance.Status.InstanceID))
×
384
                                return ctrl.Result{}, err
×
385
                        }
×
386
                        if len(smInstance.Labels["subaccount_id"]) > 0 {
2✔
387
                                serviceInstance.Status.SubaccountID = smInstance.Labels["subaccount_id"][0]
1✔
388
                        }
1✔
389
                        serviceInstance.Status.Ready = metav1.ConditionTrue
1✔
390
                } else if serviceInstance.Status.OperationType == smClientTypes.DELETE {
2✔
391
                        // delete was successful - remove our finalizer from the list and update it.
1✔
392
                        if err := r.removeFinalizer(ctx, serviceInstance, api.FinalizerName); err != nil {
1✔
393
                                return ctrl.Result{}, err
×
394
                        }
×
395
                }
396
                setSuccessConditions(status.Type, serviceInstance)
1✔
397
        }
398

399
        serviceInstance.Status.OperationURL = ""
1✔
400
        serviceInstance.Status.OperationType = ""
1✔
401

1✔
402
        return ctrl.Result{}, r.updateStatus(ctx, serviceInstance)
1✔
403
}
404

405
func (r *ServiceInstanceReconciler) handleAsyncDelete(ctx context.Context, serviceInstance *servicesv1.ServiceInstance, opURL string) (ctrl.Result, error) {
1✔
406
        serviceInstance.Status.OperationURL = opURL
1✔
407
        serviceInstance.Status.OperationType = smClientTypes.DELETE
1✔
408
        setInProgressConditions(ctx, smClientTypes.DELETE, "", serviceInstance)
1✔
409

1✔
410
        if err := r.updateStatus(ctx, serviceInstance); err != nil {
1✔
411
                return ctrl.Result{}, err
×
412
        }
×
413

414
        return ctrl.Result{Requeue: true, RequeueAfter: r.Config.PollInterval}, nil
1✔
415
}
416

417
func (r *ServiceInstanceReconciler) getInstanceForRecovery(ctx context.Context, smClient sm.Client, serviceInstance *servicesv1.ServiceInstance) (*smClientTypes.ServiceInstance, error) {
1✔
418
        log := GetLogger(ctx)
1✔
419
        parameters := sm.Parameters{
1✔
420
                FieldQuery: []string{
1✔
421
                        fmt.Sprintf("name eq '%s'", serviceInstance.Spec.ExternalName),
1✔
422
                        fmt.Sprintf("context/clusterid eq '%s'", r.Config.ClusterID),
1✔
423
                        fmt.Sprintf("context/namespace eq '%s'", serviceInstance.Namespace)},
1✔
424
                LabelQuery: []string{
1✔
425
                        fmt.Sprintf("%s eq '%s'", k8sNameLabel, serviceInstance.Name)},
1✔
426
                GeneralParams: []string{"attach_last_operations=true"},
1✔
427
        }
1✔
428

1✔
429
        instances, err := smClient.ListInstances(&parameters)
1✔
430
        if err != nil {
1✔
431
                log.Error(err, "failed to list instances in SM")
×
432
                return nil, err
×
433
        }
×
434

435
        if instances != nil && len(instances.ServiceInstances) > 0 {
2✔
436
                return &instances.ServiceInstances[0], nil
1✔
437
        }
1✔
438
        log.Info("instance not found in SM")
1✔
439
        return nil, nil
1✔
440
}
441

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

1✔
445
        log.Info(fmt.Sprintf("found existing instance in SM with id %s, updating status", smInstance.ID))
1✔
446
        updateHashedSpecValue(k8sInstance)
1✔
447
        // set observed generation to 0 because we dont know which generation the current state in SM represents,
1✔
448
        // unless the generation is 1 and SM is in the same state as operator
1✔
449
        if k8sInstance.Generation == 1 {
2✔
450
                k8sInstance.SetObservedGeneration(1)
1✔
451
        } else {
1✔
452
                k8sInstance.SetObservedGeneration(0)
×
453
        }
×
454

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

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

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

496
        return ctrl.Result{}, r.updateStatus(ctx, k8sInstance)
1✔
497
}
498

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

1✔
502
        errMsg := err.Error()
1✔
503
        isTransient := false
1✔
504

1✔
505
        if smError, ok := err.(*sm.ServiceManagerError); ok {
2✔
506
                log.Info(fmt.Sprintf("SM returned error with status code %d", smError.StatusCode))
1✔
507
                isTransient = isTransientError(smError, log)
1✔
508
                errMsg = smError.Error()
1✔
509

1✔
510
                if smError.StatusCode == http.StatusTooManyRequests {
2✔
511
                        errMsg = "in progress"
1✔
512
                        reason = InProgress
1✔
513
                } else if reason == ShareFailed &&
2✔
514
                        (smError.StatusCode == http.StatusBadRequest || smError.StatusCode == http.StatusInternalServerError) {
2✔
515
                        /* non-transient error may occur only when sharing
1✔
516
                           SM return 400 when plan is not sharable
1✔
517
                           SM returns 500 when TOGGLES_ENABLE_INSTANCE_SHARE_FROM_OPERATOR feature toggle is off */
1✔
518
                        reason = ShareNotSupported
1✔
519
                }
1✔
520
        }
521

522
        setSharedCondition(object, status, reason, errMsg)
1✔
523
        return ctrl.Result{Requeue: isTransient}, r.updateStatus(ctx, object)
1✔
524
}
525

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

541
        // succeeded=false for current generation, and without failed=true --> transient error retry
542
        if isInProgress(serviceInstance) {
2✔
543
                log.Info("instance is not in final state, sync operation is in progress")
1✔
544
                return false
1✔
545
        }
1✔
546

547
        if sharingUpdateRequired(serviceInstance) {
2✔
548
                log.Info("instance is not in final state, need to sync sharing status")
1✔
549
                if len(serviceInstance.Status.HashedSpec) == 0 {
1✔
550
                        updateHashedSpecValue(serviceInstance)
×
551
                }
×
552
                return false
1✔
553
        }
554

555
        log.Info(fmt.Sprintf("instance is in final state (generation: %d)", serviceInstance.Generation))
1✔
556
        return true
1✔
557
}
558

559
// TODO unit test
560
func updateRequired(serviceInstance *servicesv1.ServiceInstance) bool {
1✔
561
        //update is not supported for failed instances (this can occur when instance creation was asynchronously)
1✔
562
        if serviceInstance.Status.Ready != metav1.ConditionTrue {
1✔
563
                return false
×
564
        }
×
565

566
        cond := meta.FindStatusCondition(serviceInstance.Status.Conditions, api.ConditionSucceeded)
1✔
567
        if cond != nil && cond.Reason == UpdateInProgress { //in case of transient error occurred
2✔
568
                return true
1✔
569
        }
1✔
570

571
        return getSpecHash(serviceInstance) != serviceInstance.Status.HashedSpec
1✔
572
}
573

574
// TODO unit test
575
func sharingUpdateRequired(serviceInstance *servicesv1.ServiceInstance) bool {
1✔
576
        //relevant only for non-shared instances - sharing instance is possible only for usable instances
1✔
577
        if serviceInstance.Status.Ready != metav1.ConditionTrue {
2✔
578
                return false
1✔
579
        }
1✔
580

581
        sharedCondition := meta.FindStatusCondition(serviceInstance.GetConditions(), api.ConditionShared)
1✔
582
        shouldBeShared := serviceInstance.ShouldBeShared()
1✔
583

1✔
584
        if sharedCondition == nil {
2✔
585
                return shouldBeShared
1✔
586
        }
1✔
587

588
        if sharedCondition.Reason == ShareNotSupported {
2✔
589
                return false
1✔
590
        }
1✔
591

592
        if sharedCondition.Reason == InProgress || sharedCondition.Reason == ShareFailed || sharedCondition.Reason == UnShareFailed {
2✔
593
                return true
1✔
594
        }
1✔
595

596
        if shouldBeShared {
2✔
597
                return sharedCondition.Status == metav1.ConditionFalse
1✔
598
        }
1✔
599

600
        return sharedCondition.Status == metav1.ConditionTrue
1✔
601
}
602

603
func getOfferingTags(smClient sm.Client, planID string) ([]string, error) {
1✔
604
        planQuery := &sm.Parameters{
1✔
605
                FieldQuery: []string{fmt.Sprintf("id eq '%s'", planID)},
1✔
606
        }
1✔
607
        plans, err := smClient.ListPlans(planQuery)
1✔
608
        if err != nil {
1✔
609
                return nil, err
×
610
        }
×
611

612
        if plans == nil || len(plans.ServicePlans) != 1 {
2✔
613
                return nil, fmt.Errorf("could not find plan with id %s", planID)
1✔
614
        }
1✔
615

616
        offeringQuery := &sm.Parameters{
×
617
                FieldQuery: []string{fmt.Sprintf("id eq '%s'", plans.ServicePlans[0].ServiceOfferingID)},
×
618
        }
×
619

×
620
        offerings, err := smClient.ListOfferings(offeringQuery)
×
621
        if err != nil {
×
622
                return nil, err
×
623
        }
×
624
        if offerings == nil || len(offerings.ServiceOfferings) != 1 {
×
625
                return nil, fmt.Errorf("could not find offering with id %s", plans.ServicePlans[0].ServiceOfferingID)
×
626
        }
×
627

628
        var tags []string
×
629
        if err := json.Unmarshal(offerings.ServiceOfferings[0].Tags, &tags); err != nil {
×
630
                return nil, err
×
631
        }
×
632
        return tags, nil
×
633
}
634

635
func getTags(tags []byte) ([]string, error) {
1✔
636
        var tagsArr []string
1✔
637
        if err := json.Unmarshal(tags, &tagsArr); err != nil {
1✔
638
                return nil, err
×
639
        }
×
640
        return tagsArr, nil
1✔
641
}
642

643
func getSpecHash(serviceInstance *servicesv1.ServiceInstance) string {
1✔
644
        spec := serviceInstance.Spec
1✔
645
        spec.Shared = pointer.Bool(false)
1✔
646
        specBytes, _ := json.Marshal(spec)
1✔
647
        s := string(specBytes)
1✔
648
        return generateEncodedMD5Hash(s)
1✔
649
}
1✔
650

651
func generateEncodedMD5Hash(str string) string {
1✔
652
        hash := md5.Sum([]byte(str))
1✔
653
        return hex.EncodeToString(hash[:])
1✔
654
}
1✔
655

656
func setSharedCondition(object api.SAPBTPResource, status metav1.ConditionStatus, reason, msg string) {
1✔
657
        conditions := object.GetConditions()
1✔
658
        // align all conditions to latest generation
1✔
659
        for _, cond := range object.GetConditions() {
2✔
660
                if cond.Type != api.ConditionShared {
2✔
661
                        cond.ObservedGeneration = object.GetGeneration()
1✔
662
                        meta.SetStatusCondition(&conditions, cond)
1✔
663
                }
1✔
664
        }
665

666
        shareCondition := metav1.Condition{
1✔
667
                Type:    api.ConditionShared,
1✔
668
                Status:  status,
1✔
669
                Reason:  reason,
1✔
670
                Message: msg,
1✔
671
                // shared condition does not contain observed generation
1✔
672
        }
1✔
673

1✔
674
        // remove shared condition and add it as new (in case it has observed generation)
1✔
675
        meta.RemoveStatusCondition(&conditions, api.ConditionShared)
1✔
676
        meta.SetStatusCondition(&conditions, shareCondition)
1✔
677

1✔
678
        object.SetConditions(conditions)
1✔
679
}
680

681
func updateHashedSpecValue(serviceInstance *servicesv1.ServiceInstance) {
1✔
682
        serviceInstance.Status.HashedSpec = getSpecHash(serviceInstance)
1✔
683
}
1✔
684

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

1✔
692
        if err := json.Unmarshal(status.Errors, &errMap); err != nil {
1✔
693
                return errMsg
×
694
        }
×
695

696
        if description, found := errMap["description"]; found {
2✔
697
                if descStr, ok := description.(string); ok {
2✔
698
                        errMsg = descStr
1✔
699
                }
1✔
700
        }
701
        return errMsg
1✔
702
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc